Перейти к содержимому
Pre-MVP. Кодовая база 1.0 не является выпущенным продуктом — см. границы продукта 1.0.

Публичный read API (v1)

Каждый эндпоинт ниже определяет сайт по заголовку Host и больше ни по чему. Нет параметра сайта, нет заголовка сайта и нет способа для вызывающей стороны выбрать сайт — именно это делает чтение между сайтами структурно невозможным, а не просто запрещённым.

Обслуживает ли этот хост сайт. Отвечает только для хоста, который известен этому развёртыванию. Неизвестный хост получает здесь 404 точно так же, как и везде, поэтому этот эндпоинт нельзя использовать, чтобы выяснить, какие сайты существуют.

Статус Значение
200 Этот хост обслуживает сайт, и его slug находится в теле ответа.
404 На этом хосте не отвечает ни один сайт.
Окно терминала
curl https://example.com/health
{ "ok": true, "site": "acme" }

Список опубликованных элементов одной модели. Только опубликованные элементы. Сортировка, фильтрация и пагинация выполняются в базе данных, поэтому страница никогда не оказывается молча короче limit из-за того, что что-то было отброшено после запроса.

Параметр Обязательный Значение
locale нет Какой язык отдавать. По умолчанию — язык сайта по умолчанию. Подставляется ли резервный язык при отсутствии перевода или возвращается 404 — это политика локалей САЙТА, а не этого параметра; потребитель не может её переопределить, и это намеренно.
limit нет Размер страницы. Ограничен сервером; запрос большего значения возвращает предел.
cursor нет Непрозрачный. Берите его из next_cursor и отправляйте обратно без изменений — его содержимое не является контрактом и будет меняться.
order нет asc, desc — с какого конца списка читать, по дате публикации. desc — сначала новые, это значение по умолчанию.
relation нет Фильтр по полю связи, в виде relation=field:id. Можно повторять; повторы объединяются через AND — это единственная трактовка, которая позволяет потребителю сужать выборку, а не расширять.
tag нет Фильтр по полю тегов, в виде tag=field:value. Можно повторять, объединяются через AND. Не более 16 пар relation и tag на запрос, с учётом повторов: при большем количестве запрос получает отказ 400 public_filter_too_many, который содержит max. Запрос никогда не сужается молча, чтобы уложиться в предел.
Статус Значение
200 Страница элементов в data, рядом с ней page.limit, page.has_more и page.next_cursor.
400 Параметр, который этот эндпоинт не принимает, или некорректное значение.
404 На этом сайте нет такой модели, этот хост не обслуживает сайт, ИЛИ модель приватная и действительный токен не был отправлен. Один ответ для всех случаев, намеренно: §7.1 не позволяет публичной поверхности раскрывать структуру сайта, а 401 подтвердил бы, что модель существует.
Окно терминала
curl 'https://example.com/v1/article?locale=th&limit=10&order=desc'
{ "data": [ { "id": "ci_…", "slug": "hello", "locale": "th", "fields": { } } ],
"page": { "limit": 10, "has_more": true, "next_cursor": "…" } }

Прочитать один опубликованный элемент. Один элемент, по slug. Slug уникален в пределах модели на сайте, но никогда не между сайтами — сайт определяется хостом и ничем из того, что отправляет вызывающая сторона.

Параметр Обязательный Значение
locale нет Какой язык отдавать. По умолчанию — язык сайта по умолчанию. Подставляется ли резервный язык при отсутствии перевода или возвращается 404 — это политика локалей САЙТА, а не этого параметра; потребитель не может её переопределить, и это намеренно.
preview нет Право предпросмотра (preview capability), выпущенное CMS. Оно отдаёт ЧЕРНОВИК ОДНОГО элемента, истекает через несколько минут и никогда не кешируется. Оно не делает приватную модель читаемой, а отказ в предпросмотре отвечает 404, а не 403, чтобы отказ ничего не подтверждал.
Статус Значение
200 Элемент, спроецированный в запрошенную локаль.
400 Параметр, который этот эндпоинт не принимает.
404 Нет такого элемента, он не опубликован, нет контента в локали, которую политика готова отдать, токен предпросмотра не прошёл проверку, или приватная модель без действительного токена. Один ответ для всех случаев, намеренно (§7.1).
Окно терминала
curl 'https://example.com/v1/article/hello?locale=th'
{ "data": { "id": "ci_…", "slug": "hello", "locale": "th", "fields": { } } }

Модель, помеченная как приватная, доступна для чтения только с API-токеном сайта, отправленным как Authorization: Bearer dw_site_…. Токены выпускаются в админке в разделе Settings → Public API, несут scope, в котором перечислены модели, которые они могут читать, и показываются один раз.

Запрос без токена и запрос с неправильным токеном получают ОДИН И ТОТ ЖЕ ответ. Это сделано намеренно: их различение сообщило бы вызывающей стороне, у которой есть догадка, что модель существует.

По умолчанию этот API отвечает no-store: ничего не кешируется на edge, и каждое чтение доходит до Worker. Это единственное значение по умолчанию, которое корректно без учётных данных для очистки кеша.

Кеширование включается явно. Установка PUBLIC_CACHE_MAX_AGE в положительное число секунд — и в публичном, и в admin Worker — включает его: ответы тогда кешируются и помечаются тегами, а публикация элемента очищает ровно те ответы, в которых он упоминается. Для этого нужны CF_PURGE_ZONE_ID и CF_PURGE_API_TOKEN с правом Zone → Cache Purge; без них изменение сообщает, что не смогло вытеснить кеш, а не скрывает это.

Две вещи никогда не кешируются, каким бы ни был TTL: ответ на запрос, несущий API-токен сайта, и предпросмотр.

СТАБИЛЬНО в пределах v1: пути, имена параметров, значения кодов статуса и форма ответа верхнего уровня — data и, на маршруте списка, объект page, содержащий limit, has_more и next_cursor. Собственные ключи элемента — id, slug, model, locale, version, published_at, fields, relations и media.

НЕ стабильно, и не разбирайте: содержимое next_cursor, точный текст сообщения об ошибке и порядок ключей. Новый необязательный параметр или новое поле внутри элемента могут появиться в пределах v1; ничто из уже задокументированного здесь не изменит значения без новой версии в пути.

Fetch, с правильно написанным циклом по курсору

Заголовок раздела «Fetch, с правильно написанным циклом по курсору»
async function* everyArticle(origin, locale = 'en') {
let cursor;
do {
const url = new URL(`${origin}/v1/article`);
url.searchParams.set('locale', locale);
url.searchParams.set('limit', '50');
// The cursor is opaque. Send back exactly what you were given; do not
// decode it, and do not construct one.
if (cursor) url.searchParams.set('cursor', cursor);
const response = await fetch(url);
if (!response.ok) throw new Error(`${response.status} from ${url}`);
const body = await response.json();
yield* body.data;
// Pagination lives in `page`, beside `data` rather than above it.
cursor = body.page.has_more ? body.page.next_cursor : undefined;
} while (cursor);
}
Окно терминала
curl -H "Authorization: Bearer $DEE_WAN_SITE_TOKEN" \
'https://example.com/v1/pricing'

Приватная модель без действительного токена отвечает 404, а не 401 — тот же ответ, что и для несуществующей модели. Это сделано намеренно (§7.1): 401 подтвердил бы, что модель есть, а публичная поверхность ничего не раскрывает о структуре сайта. Это также означает, что 404 нельзя трактовать как «неверный URL»; сначала проверьте токен.

Использование сгенерированного TypeScript-клиента

Заголовок раздела «Использование сгенерированного TypeScript-клиента»

Аутентифицированный admin API выдаёт клиент без зависимостей для выбранного сайта:

Окно терминала
curl -H "X-Site-Id: $DEE_WAN_SITE_ID" -b "$DEE_WAN_SESSION_COOKIE" \
'https://admin.example.com/api/models/public-client.ts' \
> src/dee-wan-public.ts

Он содержит оболочку ответа, union-тип локалей, точные типы полей, типы запросов с учётом модели и по одному методу на модель. Сгенерируйте его заново после изменения модели; Dee Wan не обновляет репозитории потребителей.

Если вы вызываете эндпоинт без сгенерированного клиента, типизируйте оболочку ответа и поля модели напрямую:

type Item<F> = { id: string; slug: string; locale: string; fields: F };
type Page<F> = {
data: Item<F>[];
page: { limit: number; has_more: boolean; next_cursor: string | null };
};
type Article = { title: string; body: string };
const page: Page<Article> = await (await fetch(url)).json();