Публичный read API (v1)
Каждый эндпоинт ниже определяет сайт по заголовку Host и больше ни по чему. Нет параметра сайта, нет заголовка сайта и нет способа для вызывающей стороны выбрать сайт — именно это делает чтение между сайтами структурно невозможным, а не просто запрещённым.
GET /health
Заголовок раздела «GET /health»Обслуживает ли этот хост сайт. Отвечает только для хоста, который известен этому развёртыванию. Неизвестный хост получает здесь 404 точно так же, как и везде, поэтому этот эндпоинт нельзя использовать, чтобы выяснить, какие сайты существуют.
| Статус | Значение |
|---|---|
| 200 | Этот хост обслуживает сайт, и его slug находится в теле ответа. |
| 404 | На этом хосте не отвечает ни один сайт. |
curl https://example.com/health{ "ok": true, "site": "acme" }GET /v1/:model
Заголовок раздела «GET /v1/:model»Список опубликованных элементов одной модели. Только опубликованные элементы. Сортировка, фильтрация и пагинация выполняются в базе данных, поэтому страница никогда не оказывается молча короче 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": "…" } }GET /v1/:model/:slug
Заголовок раздела «GET /v1/:model/:slug»Прочитать один опубликованный элемент. Один элемент, по 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();