GuardLabs
demo / payload-cms
Payload CMS · headless

Payload CMS без дублей и утечек: локализация, роли и хуки на живом REST

На этой странице работает настоящий Payload 2 с базой на этом же сервере. Каждая кнопка бьёт в его живой REST API. Стенд закрывает три вещи, на которых обычно спотыкаются проекты на Payload: мультиязычный контент, доступ по ролям и поля, которые должны считаться сами.

Боль

Три места, где Payload-проекты обычно текут

Двуязычный контент

  • Наивное решение: две коллекции articles_uk и articles_ru, которые расходятся через месяц.
  • Правильное: localized поля, обе языковые версии живут в одном документе.

Черновики наружу

  • Фильтр «status = published» на фронтенде не защита: REST API открыт напрямую.
  • Правильное: access-функция возвращает query constraint, и аноним физически не может получить черновик.

Поля руками

  • Slug, время чтения, дата публикации, заполняемые редактором, со временем врут.
  • Правильное: хуки beforeValidate и beforeChange считают их при каждом сохранении.

Живой артефакт

Четыре запроса к настоящему Payload REST API

В базе три статьи: две опубликованы, одна черновик. Все запросы уходят как аноним, без токена. Справа сырой ответ API без правок.

Что видит аноним

Выберите запрос: он выполнится в живом Payload.
// сырой JSON ответа появится здесь

Черновик существует: он называется «Чернетка: хуки рахують readingTime». Получить его анонимным запросом не выйдет даже прямым фильтром «всё, что не published», потому что ограничение вшито в access на уровне API, а не в код витрины.

Как устроено

Конфиг, который это обеспечивает

// payload.config.js стенда, фрагменты без сокращений по смыслу
localization: { locales: ['uk', 'ru'], defaultLocale: 'uk', fallback: true },

access: {
  read: ({ req }) => {
    if (req.user) return true;               // редактор видит всё
    return { status: { equals: 'published' } }; // анониму query constraint
  },
},

hooks: {
  beforeChange: [({ data }) => {
    const words = String(data.body || '').trim().split(/\s+/).length;
    data.readingTime = Math.max(1, Math.round(words / 180));
    if (data.status === 'published' && !data.publishedAt)
      data.publishedAt = new Date().toISOString();
    return data;
  }],
}

// slug: beforeValidate, транслитерация из title, стабилен при переводах

Полный конфиг стенда: коллекции users с ролями admin и editor плюс articles с локализованными title, excerpt, body и группой seo. Админка на публичном стенде отключена, наружу проксируются только GET-запросы.

Как проверить

Из терминала, без нашей страницы

# украинская и русская версия одного документа
curl "https://guardlabs.online/demo/payload-cms/api/articles?locale=uk"
curl "https://guardlabs.online/demo/payload-cms/api/articles?locale=ru"

# попытка вытащить черновики напрямую: totalDocs будет 0
curl "https://guardlabs.online/demo/payload-cms/api/articles?where\[status\]\[not_equals\]=published"