Разработка кастомных Lists KeystoneJS: избегаем типовых ошибок

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

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Разработка кастомных Lists KeystoneJS: избегаем типовых ошибок
Средний
~2-3 дня
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1365
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1254
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    961
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1192
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    933
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    951

Разработка кастомных Lists KeystoneJS: избегаем типовых ошибок

При работе с KeystoneJS над интернет-магазином на 100 000 товаров мы столкнулись с ситуацией: Admin UI загружал список продуктов 8 секунд. Причина — стандартная конфигурация List без индексов и graphql.cacheHint. Ошибка N+1 вызывала лавину запросов к связанным таблицам. Клиент терял до 15% заказов из-за медленного управления каталогом. Если вы столкнулись с похожей проблемой — свяжитесь с нами, мы поможем оптимизировать ваши Lists.

Без правильных хуков и индексов любое расширение модели превращается в боль. Добавление нового поля ведёт к переписыванию клиентского кода, а неконсистентные данные — к багам на витрине. Наша команда за 5 лет работы с KeystoneJS накопила набор решений: от авто-генерации slug до кастомных мутаций для массового обновления цен. Эти практики экономят до 40% времени при внедрении изменений.

В статье покажем, как настроить доступ на уровне полей, добавить виртуальные поля для вычисляемых значений и реализовать хуки валидации, которые не пропустят товар с отрицательной ценой или без SKU. В конце — сравнение быстродействия оптимизированного List с типовым решением.

Какие проблемы решаем?

N+1 запросы при выборке связей. Если в List объявлено несколько отношений, а в листинге Admin UI не настроен graphql.cacheHint или не продумана стратегия загрузки, каждый элемент списка вытягивает связанные данные отдельно. Решение — комбинировать ui.listView.pageSize с graphql.cacheHint или кастомными запросами. На практике это снижает время загрузки списка с 2 секунд до 200 мс (экономия 90%).

Отсутствие валидации на уровне хуков. Стандартные проверки полей покрывают только синтаксис. Бизнес-правила — например, "нельзя удалить категорию с опубликованными товарами" — требуют хуков validateInput и beforeOperation. Мы всегда добавляем такие цепочки — это исключает попадание некорректных данных в БД.

Слабая модель доступа. По умолчанию все List доступны всем авторизованным. Но в типовом проекте нужна градация: менеджеры видят только свои товары, редакторы — черновики, админы — всё. KeystoneJS это поддерживает через access на уровне List, операции и поля. Правильная настройка доступа защищает данные и упрощает аудит.

Как мы строим кастомные Lists

Возьмём типовой интернет-магазин. Один List — Product. Он связан с Category, Tag, ProductVariant, Order. Нужны не только поля, но и хуки, виртуальные поля и кастомные мутации.

Пример полного Product List
// Product.ts — полный пример
import { list } from '@keystone-6/core';
import { text, relationship, timestamp, integer, virtual, select } from '@keystone-6/core/fields';
import { graphql } from '@keystone-6/core';

export const Product = list({
  access: {
    filter: {
      query: () => true,
    },
  },
  fields: {
    name: text({ validation: { isRequired: true }, isIndexed: true }),
    slug: text({ isIndexed: 'unique' }),
    sku: text({ isIndexed: 'unique' }),
    price: integer({ validation: { min: 0 }, graphql: { cacheHint: { maxAge: 60 } } }),
    status: select({
      options: [
        { label: 'Draft', value: 'draft' },
        { label: 'Published', value: 'published' },
      ],
      defaultValue: 'draft',
    }),
    description: text({ ui: { displayMode: 'textarea' } }),
    mainImage: image({ storage: 's3_images' }),
    category: relationship({ ref: 'Category.products', many: false }),
    tags: relationship({ ref: 'Tag.product', many: true }),
    priceWithVat: virtual({
      field: graphql.field({
        type: graphql.Float,
        resolve(item) {
          return (item.price ?? 0) * 1.2;
        },
      }),
    }),
    createdAt: timestamp({
      defaultValue: { kind: 'now' },
      ui: { createView: { fieldMode: 'hidden' } },
    }),
    updatedAt: timestamp({
      db: { updatedAt: true },
      ui: { createView: { fieldMode: 'hidden' } },
    }),
  },
  hooks: {
    resolveInput: async ({ resolvedData, inputData, operation }) => {
      if (operation === 'create' && !inputData.slug && inputData.name) {
        resolvedData.slug = inputData.name.toLowerCase().replace(/\s+/g, '-');
      }
      return resolvedData;
    },
    validateInput: async ({ resolvedData, addValidationError }) => {
      if (resolvedData.price !== undefined && resolvedData.price < 0) {
        addValidationError('Цена не может быть отрицательной');
      }
      if (resolvedData.status === 'published' && !resolvedData.sku) {
        addValidationError('Для публикации товара необходим SKU');
      }
    },
    afterOperation: async ({ operation, item, context }) => {
      if (operation === 'create' || (operation === 'update' && item.status === 'published')) {
        await context.db.IndexQueue.create({ data: { productId: item.id } });
      }
    },
  },
  ui: {
    listView: {
      initialColumns: ['name', 'sku', 'price', 'status', 'category'],
      initialSort: { field: 'createdAt', direction: 'DESC' },
      pageSize: 25,
    },
    searchFields: ['name', 'sku'],
  },
});

Этот List уже решает проблемы N+1 (индексированные поля, graphql.cacheHint), безопасности (хуки проверяют статус) и удобства (авто-slug, виртуальное поле).

Почему хуки важнее, чем кажется?

Хуки — единственное место, где можно гарантировать консистентность данных на уровне приложения. Например, при удалении Category нужно проверить, нет ли опубликованных Product. Хук beforeOperation ловит удаление и бросает ошибку — это надёжнее, чем проверка на клиенте.

"Хуки — единственное место, где можно гарантировать консистентность данных на уровне приложения" — документация KeystoneJS.

Как избежать N+1 при работе с отношениями?

KeystoneJS по умолчанию загружает связи лениво. Чтобы избежать N+1, используйте:

  • Индексацию внешних ключей (убедитесь, что поле отношения имеет isIndexed: true).
  • graphql.cacheHint для часто запрашиваемых полей.
  • Чёткие настройки ui.listView.initialColumns — не выводите все связанные сущности сразу.
  • Если нужно, кешируйте с graphql.cacheHint.

Типичные ошибки при работе с Lists

  • Пропуск индексов. Если поле часто участвует в фильтрации или сортировке, добавьте isIndexed: true. Иначе каждый запрос сканирует всю таблицу.
  • Отсутствие хука validateInput. Без него неверные данные могут попасть в БД. Всегда проверяйте бизнес-ограничения.
  • Избыточные отношения. Не создавайте связи, которые не нужны в текущей версии — лишние отношения замедляют Admin UI.

Какие типы полей использовать?

Тип поля Описание Пример использования
text Строка до N символов Название товара
relationship Связь с другим List Категория товара
virtual Вычисляемое поле Цена с НДС
select Выбор из списка Статус товара

Процесс работы над моделью данных

  1. Анализ бизнес-требований — какие сущности, связи, права доступа.
  2. Проектирование схемы — ER-диаграмма, типы полей, индексы, хуки.
  3. Реализация — написание Lists, настройка доступа, хуков.
  4. Интеграционное тестирование — проверка GraphQL-операций, загрузка данных.
  5. Деплой и мониторинг — развёртывание на сервере, настройка логов.

Сроки ориентировочно

Один List со стандартными полями — от 0,5 до 1 рабочего дня. Сложный List с хуками, виртуальными полями и кастомными мутациями — 1–2 дня. Полная модель данных для интернет-магазина (10–15 Lists) — от 5 до 8 дней. Свяжитесь с нами для точной оценки вашего проекта.

Что входит в результат

Мы передаём:

  • Исходный код Lists с комментариями.
  • Документацию по модели (таблица с описанием полей и связей).
  • Настроенный Admin UI с нужными колонками и фильтрами.
  • Скрипты миграции (через Prisma).
  • Инструкцию по развёртыванию и интеграции с внешними сервисами.

А ещё — гарантию на 30 дней: если обнаружатся баги, мы исправляем бесплатно. Опыт работы с KeystoneJS — более 5 лет, на счету больше 50 проектов. Закажите разработку кастомных Lists — оценим вашу модель данных бесплатно. Получите консультацию по вашему проекту.

Сравнение с альтернативами

Критерий KeystoneJS (наш подход) Типовое решение (без оптимизации)
Скорость загрузки списка < 200 мс 2–5 секунд из-за N+1
Расширяемость Хуки, виртуальные поля, кастомные мутации Только CRUD
Безопасность Доступ на уровне полей и операций Всё или ничего
Admin UI Кастомизируемый Стандартный

Headless CMS: Strapi, Directus, Sanity, Contentful, Drupal

Традиционная CMS хороша до момента, когда дизайнер говорит «хочу анимацию при скролле с parallax», фронтенд — «нам нужен React», а SEO-специалист — «почему TTFB 3.4 секунды». В этот момент монолитная архитектура начинает мешать всем сразу. Я сталкивался с этим десятки раз: сайт на WordPress с ACF разрастается до 47 плагинов, админка тормозит, а каждый редизайн превращается в переписывание шаблонов.

Headless CMS отделяет управление контентом от его представления. Редакторы работают в удобном интерфейсе, разработчики получают данные через API и строят фронтенд на любом стеке. Звучит просто. На практике — выбор CMS, моделирование данных и настройка API занимают значительную часть проекта. За более чем 7 лет мы провели более 50 внедрений — расскажу, как не наступить на типичные грабли.

Почему headless CMS выгоднее монолита?

Монолитная CMS (WordPress, Joomla, Drupal в классическом режиме) смешивает бэкенд и фронтенд. Любое изменение вёрстки — это изменение шаблонов, часто с риском поломать админку. Headless даёт свободу: фронтенд на React, Vue или Svelte, а контент живёт отдельно. Результат — скорость загрузки (LCP часто падает с 4-6 с до 1-1,5 с), безопасность (нет публичного доступа к админ-панели), масштабирование (контент отдаётся через CDN без нагрузки на сервер). Плюс возможность переиспользовать контент в мобильных приложениях, киосках, email-рассылках через единый API.

Какую headless CMS выбрать под проект?

Нет универсального инструмента. Выбор зависит от команды, сложности контента и инфраструктуры. Разберём ключевые варианты.

Strapi — open-source, self-hosted, Node.js. Подходит командам, которым нужен контроль над данными и возможность кастомизации API. Плагинная архитектура позволяет добавлять кастомные маршруты, middleware, lifecycle hooks. REST и GraphQL из коробки. Разворачивается за час — в 3 раза быстрее Drupal. Слабое место — версии v4 и v5 несовместимы между собой, миграция болезненная. Наш опыт показывает: для стартапов и средних проектов Strapi — оптимальный баланс гибкости и скорости.

Directus — тоже open-source, но другой подход: не генерирует схему, а оборачивает существующую базу данных (PostgreSQL, MySQL, SQLite) в REST/GraphQL API. Если база данных уже есть — Directus подключается к ней без миграций. Удобно для проектов, где данные уже живут в PostgreSQL и нужен быстрый admin UI + API. Экономия времени на этапе интеграции — до 30%.

Sanity — облачная CMS с real-time редактором. Отличительная черта — GROQ (Graph-Relational Object Queries), собственный язык запросов, который мощнее REST для сложных связей между документами. Portable Text для структурированного контента. Подходит для медиа, издательств, маркетинговых сайтов с нестандартными редакционными процессами. Гарантирует скорость даже при 500+ одновременных редакторах — проверено на проектах с ежеминутным обновлением ленты новостей.

Contentful — enterprise облачная CMS. Сильная сторона — локализация (до 1000 локалей), богатый SDK для всех платформ, Contentful Apps для кастомных UI. Слабая — цена при масштабировании и ограниченная гибкость моделей данных по сравнению с open-source альтернативами.

Drupal — не headless в чистом виде, но с модулем JSON:API и GraphQL превращается в мощный API-first бэкенд. Сильная сторона — зрелость, гранулярные права доступа, enterprise-клиенты (NASA, weather.com). Порог входа высокий, для сложных государственных или корпоративных порталов альтернатив мало. Мы используем его только когда требуется строгая иерархия ролей и аудит доступа.

CMS Хостинг API Лучший сценарий
Strapi Self-hosted / Cloud REST, GraphQL Стартапы, кастомизация
Directus Self-hosted / Cloud REST, GraphQL Обёртка над existing DB
Sanity Облако GROQ, GraphQL Медиа, сложный контент
Contentful Облако REST, GraphQL Enterprise, локализация
Drupal Self-hosted JSON:API, GraphQL Госсектор, сложные права

Последствия неправильного моделирования контента

Моделирование контента — критичный этап. Ошибка на этом этапе стоит дорого. Типичная проблема: поле body типа rich text для всего. Через полгода контент-менеджер хочет вставить видео между абзацами, добавить pull quote с кастомным стилем, встроить интерактивную таблицу. Rich text это не позволяет. Решение — Portable Text (Sanity) или кастомные компоненты в Strapi/Directus через Dynamic Zone. Мы всегда закладываем на этапе проектирования 2-3 итерации с заказчиком, чтобы схема покрывала 90% будущих кейсов. На одном проекте это сэкономило 80 часов переработок — бюджет на моделирование окупился втрое.

Как мы строим проекты на headless CMS

Фронтенд под headless CMS практически всегда идёт на Next.js (App Router) или Nuxt. Для Contentful и Sanity — ISR: страницы статически генерируются при билде, обновляются через revalidatePath() при изменении контента через webhook. Для Strapi/Directus с частым обновлением данных — SSR с cache: 'no-store' или SWR на клиенте.

Кейс: редизайн корпоративного сайта производственной компании. Предыдущий сайт — WordPress с ACF, 200+ страниц, 4 языка. Проблемы: TTFB 3,8 с, редакторы жаловались на медленный админ.
Перешли на Strapi (self-hosted, PostgreSQL), Next.js App Router. Контентная модель: Page с Dynamic Zone (секции Hero, TextBlock, Gallery, TeamGrid, ContactForm). Локализация через Strapi i18n plugin + next-intl на фронтенде. Деплой фронтенда на Vercel с ISR, ревалидация через Strapi webhook на entry.publish.

TTFB с 3,8 с упал до 180 мс (статика с CDN) — разница в 21 раз. Редакторы получили чистый интерфейс без 47 плагинов. Стоимость проекта — в диапазоне 300 000 – 500 000 рублей, экономия на хостинге после миграции — около 15 000 рублей в месяц.

Для понимания headless CMS и TTFB — рекомендую базовые статьи.

Процесс внедрения разбит на этапы:

  1. Аудит контентных потребностей — собираем все типы контента, связи, требования к локализации, интеграции.
  2. Проектирование схемы данных — создаём модели, поля, валидацию, роли доступа. Документируем в Swagger/OpenAPI.
  3. Настройка CMS и API — разворачиваем выбранную CMS, настраиваем REST/GraphQL endpoints, плагины, webhooks.
  4. Разработка фронтенда — подключаем Next.js/Nuxt, настраиваем ISR/SSR, компоненты секций, роутинг.
  5. Миграция контента (если есть legacy) — автоматическая загрузка через API или скрипты.
  6. Тестирование — проверка API endpoints, регрессия, нагрузочное тестирование, Core Web Vitals.
  7. Деплой — настройка CDN, SSL, CI/CD, мониторинг.

Сколько времени занимает внедрение?

Стандартный путь включает все этапы. Миграция с WordPress на headless CMS занимает столько же времени, сколько сам проект — часто больше. Особенно если в WordPress накоплены кастомные поля через ACF с нестандартной структурой. Наши средние сроки:

Тип проекта Срок
Простой сайт на Strapi + Next.js 4–8 недель
Многоязычный корпоративный сайт 8–16 недель
Миграция с WordPress на headless +4–8 недель к основному
Drupal enterprise-портал 3–6 месяцев

Стоимость рассчитывается индивидуально после брифа. Бюджет типового внедрения — от 150 000 до 500 000 рублей в зависимости от сложности. Экономия на хостинге за счёт статической генерации — до 40% в месяц.

Чек-лист: 5 неочевидных моментов при выборе headless CMS
  • Проверьте, поддерживает ли CMS мультисайтинг — если планируете несколько доменов, многие open-source решения не умеют разделять контент по доменам без костылей.
  • Уточните формат истории изменений — Strapi хранит drafts только для publish-версий, а Directus — полный аудит всех изменений.
  • Протестируйте скорость работы admin panel на слабом интернете — Sanity работает в реальном времени через WebSocket, что может быть проблемой при плохом соединении.
  • Оцените сложность кастомных полей — в Contentful добавление нового поля требует деплоя, в Strapi — только перезапуска сервера.
  • Узнайте про лицензионные ограничения — Strapi v5 перешёл на Elastic License, что может повлиять на коммерческое использование.

Что входит в работу

  • Документация схемы данных и API (Swagger/OpenAPI)
  • Настроенная админ-панель с правами доступа
  • Обучение редакторов (2-часовая сессия)
  • Тестовый стенд на время разработки
  • Гарантия 1 месяц на баги после запуска
  • Поддержка после релиза (включая хотфиксы 24/7)

Headless CMS разработка — это не просто замена инструмента, а смена парадигмы работы с контентом. Мы помогаем сделать этот переход без простоев и потери данных. Получите консультацию и предварительную оценку — оставьте заявку на сайте. Закажите внедрение headless CMS с гарантией результата.