Разработка сайта на CMS KeystoneJS
KeystoneJS 6 — это не SaaS-платформа и не готовый движок, а code-first headless CMS: вы описываете модели данных на TypeScript, а система генерирует GraphQL API, Admin UI и схему базы данных автоматически. Это делает KeystoneJS особенно сильным выбором для проектов с нестандартной доменной логикой, где гибкость важнее скорости первоначальной настройки.
Когда выбирать KeystoneJS
KeystoneJS подходит, когда:
- Нужен полный контроль над схемой данных и бизнес-логикой
- Команда комфортно работает с Node.js и TypeScript
- Требуется кастомная логика доступа, сложные вычисляемые поля, хуки на уровне данных
- GraphQL API — первичный интерфейс для фронтенда
Не подходит, если нужен хостинг «из коробки» без DevOps или проект требует богатого UI для нетехнических редакторов (тогда смотрите на Strapi или Payload).
Структура проекта
my-keystone-project/
├── keystone.ts # Точка входа: конфиг БД, сессий, UI
├── schema.ts # Агрегирует все Lists
├── lists/
│ ├── Post.ts
│ ├── Author.ts
│ ├── Tag.ts
│ └── Category.ts
├── auth.ts # Настройка аутентификации
├── migrations/ # Prisma-миграции
└── frontend/ # Next.js или любой другой фронтенд
Конфигурация и запуск
// keystone.ts
import { config } from '@keystone-6/core';
import { lists } from './schema';
import { withAuth, session } from './auth';
export default withAuth(
config({
db: {
provider: 'postgresql',
url: process.env.DATABASE_URL!,
enableLogging: process.env.NODE_ENV === 'development',
idField: { kind: 'uuid' },
},
lists,
session,
ui: {
// Ограничение доступа к Admin UI
isAccessAllowed: (context) => !!context.session?.data,
},
server: {
cors: {
origin: [process.env.FRONTEND_URL!],
credentials: true,
},
},
})
);
Моделирование контента
// lists/Post.ts
import { list } from '@keystone-6/core';
import { allowAll, denyAll } from '@keystone-6/core/access';
import {
text, relationship, timestamp, select,
checkbox, image, document,
} from '@keystone-6/core/fields';
import { document as documentField } from '@keystone-6/fields-document';
export const Post = list({
access: {
operation: {
query: allowAll,
create: ({ session }) => !!session,
update: ({ session }) => !!session,
delete: ({ session }) => session?.data?.role === 'admin',
},
},
fields: {
title: text({ validation: { isRequired: true } }),
slug: text({
validation: { isRequired: true },
isIndexed: 'unique',
hooks: {
resolveInput: ({ resolvedData, inputData }) => {
if (inputData.title && !inputData.slug) {
return inputData.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
return resolvedData.slug;
},
},
}),
status: select({
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
{ label: 'Archived', value: 'archived' },
],
defaultValue: 'draft',
ui: { displayMode: 'segmented-control' },
}),
publishedAt: timestamp(),
content: documentField({
formatting: true,
links: true,
dividers: true,
layouts: [[1, 1], [1, 1, 1]],
}),
author: relationship({
ref: 'Author.posts',
ui: { displayMode: 'select' },
}),
tags: relationship({
ref: 'Tag.posts',
many: true,
ui: { displayMode: 'cards', cardFields: ['name'] },
}),
featuredImage: image({ storage: 'local_images' }),
seoTitle: text(),
seoDescription: text({ ui: { displayMode: 'textarea' } }),
},
hooks: {
beforeOperation: async ({ operation, item, resolvedData, context }) => {
if (operation === 'update' && resolvedData.status === 'published') {
resolvedData.publishedAt = new Date();
}
},
},
ui: {
listView: {
initialColumns: ['title', 'status', 'author', 'publishedAt'],
initialSort: { field: 'publishedAt', direction: 'DESC' },
},
},
});
GraphQL API: работа с фронтендом
После запуска npx keystone dev доступен GraphQL Playground на http://localhost:3000/api/graphql.
# Запрос опубликованных постов с пагинацией
query GetPosts($skip: Int, $take: Int) {
posts(
where: { status: { equals: "published" } }
orderBy: { publishedAt: desc }
skip: $skip
take: $take
) {
id
title
slug
publishedAt
author {
name
avatar {
url
}
}
tags {
name
slug
}
}
postsCount(where: { status: { equals: "published" } })
}
В Next.js с Apollo Client или urql:
// lib/keystoneClient.ts
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
export const keystoneClient = new ApolloClient({
link: createHttpLink({ uri: process.env.KEYSTONE_API_URL }),
cache: new InMemoryCache(),
defaultOptions: {
query: { fetchPolicy: 'network-only' },
},
});
Хранилище файлов и изображений
// keystone.ts — настройка хранилищ
storage: {
local_images: {
kind: 'local',
type: 'image',
generateUrl: (path) => `${process.env.BASE_URL}/images${path}`,
serverRoute: { path: '/images' },
storagePath: 'public/images',
},
s3_files: {
kind: 's3',
type: 'file',
bucketName: process.env.S3_BUCKET!,
region: process.env.S3_REGION!,
accessKeyId: process.env.S3_ACCESS_KEY!,
secretAccessKey: process.env.S3_SECRET!,
signed: { expiry: 3600 },
},
},
Аутентификация и сессии
KeystoneJS использует stateful-сессии через cookie. Конфигурация аутентификации:
// auth.ts
import { createAuth } from '@keystone-6/auth';
import { statelessSessions } from '@keystone-6/core/session';
const { withAuth } = createAuth({
listKey: 'User',
identityField: 'email',
secretField: 'password',
initFirstItem: {
fields: ['name', 'email', 'password'],
},
sessionData: 'id name email role',
passwordResetLink: {
sendToken: async ({ itemId, identity, token, context }) => {
await sendPasswordResetEmail(identity, token);
},
},
});
export const session = statelessSessions({
maxAge: 60 * 60 * 24 * 30,
secret: process.env.SESSION_SECRET!,
});
Деплой
KeystoneJS разворачивается как обычное Node.js-приложение. Требования: PostgreSQL или MySQL, Node.js 18+, достаточно памяти для процесса (~256MB).
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npx keystone build
EXPOSE 3000
CMD ["npx", "keystone", "start"]
Для Railway, Render, Fly.io — стандартный Node.js deploy. Необходимо запустить keystone prisma migrate deploy перед стартом в production.
Сроки разработки типового сайта
| Этап | Время |
|---|---|
| Установка, конфиг БД, базовые Lists | 1 день |
| 3–5 моделей с отношениями | 2–3 дня |
| Настройка доступа и ролей | 1 день |
| Аутентификация + сессии | 0.5 дня |
| Интеграция с Next.js фронтендом | 2–3 дня |
| Деплой + миграции | 0.5–1 день |
| Итого для среднего сайта | 7–10 дней |
Для корпоративных проектов со сложными правами доступа, мультисайтовостью и CI/CD — 3–5 недель.







