Интеграция CMS KeystoneJS для управления контентом
KeystoneJS 6 — это headless CMS и application framework в одном. Конфиг схемы пишется на TypeScript, из неё автоматически генерируется GraphQL API, REST-подобные эндпоинты и административный интерфейс. Ближайший аналог по подходу — Payload, но Keystone ставит GraphQL в центр архитектуры.
Сильные стороны Keystone
Автоматическая генерация GraphQL-схемы из модели данных экономит много ручной работы. Типы, мутации, фильтры, пагинация — всё появляется само. Плюс встроенная поддержка сессий, аутентификации, ролей — собирать это вручную не нужно.
Работает с PostgreSQL и SQLite через Prisma — миграции генерируются автоматически.
Установка
npm create keystone-app@latest my-project
cd my-project
npm install
Либо добавление в существующий проект:
npm install @keystone-6/core
Схема данных
// keystone.ts
import { config, list } from '@keystone-6/core'
import { allowAll, denyAll, isSignedIn } from '@keystone-6/core/access'
import {
text, relationship, password, timestamp,
select, checkbox, image, document
} from '@keystone-6/core/fields'
import { document as documentField } from '@keystone-6/fields-document'
export default config({
db: {
provider: 'postgresql',
url: process.env.DATABASE_URL!,
idField: { kind: 'cuid' },
},
lists: {
Post: list({
access: {
operation: {
query: allowAll,
create: isSignedIn,
update: isSignedIn,
delete: isSignedIn,
},
},
fields: {
title: text({ validation: { isRequired: true } }),
slug: text({ isIndexed: 'unique' }),
content: documentField({
formatting: true,
dividers: true,
links: true,
layouts: [[1, 1], [1, 2, 1]],
}),
publishedAt: timestamp(),
status: select({
options: ['draft', 'published', 'archived'],
defaultValue: 'draft',
ui: { displayMode: 'segmented-control' },
}),
author: relationship({ ref: 'User.posts' }),
tags: relationship({ ref: 'Tag.posts', many: true }),
cover: image({ storage: 'local_images' }),
},
hooks: {
resolveInput: async ({ resolvedData, operation }) => {
if (operation === 'create' && !resolvedData.slug) {
resolvedData.slug = resolvedData.title
?.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
}
return resolvedData
},
},
}),
Tag: list({
access: allowAll,
fields: {
name: text({ isIndexed: 'unique' }),
posts: relationship({ ref: 'Post.tags', many: true }),
},
}),
User: list({
access: {
operation: {
query: isSignedIn,
create: ({ session }) => session?.data?.role === 'admin',
update: isSignedIn,
delete: ({ session }) => session?.data?.role === 'admin',
},
},
fields: {
name: text({ validation: { isRequired: true } }),
email: text({ isIndexed: 'unique', validation: { isRequired: true } }),
password: password({ validation: { isRequired: true } }),
role: select({ options: ['admin', 'editor', 'author'], defaultValue: 'author' }),
posts: relationship({ ref: 'Post.author', many: true }),
},
}),
},
session: statelessSessions({
secret: process.env.SESSION_SECRET!,
maxAge: 60 * 60 * 24 * 30,
}),
storage: {
local_images: {
kind: 'local',
type: 'image',
generateUrl: (path) => `${process.env.ASSET_BASE_URL}/images${path}`,
serverRoute: { path: '/images' },
storagePath: 'public/images',
},
},
})
GraphQL API — примеры запросов
Из этой схемы Keystone автоматически генерирует:
# Получить опубликованные посты с тегами
query {
posts(
where: { status: { equals: "published" } }
orderBy: { publishedAt: desc }
take: 10
) {
id
title
slug
publishedAt
author {
name
}
tags {
name
}
cover {
url
width
height
}
}
}
# Создать пост
mutation {
createPost(data: {
title: "Новый пост"
content: { document: [] }
status: "draft"
author: { connect: { id: "cuid123" } }
tags: { connect: [{ id: "tag1" }, { id: "tag2" }] }
}) {
id
slug
}
}
Фильтрация поддерживает сложные условия:
query {
posts(where: {
AND: [
{ status: { equals: "published" } },
{ publishedAt: { lte: "2025-01-01T00:00:00Z" } },
{ tags: { some: { name: { equals: "TypeScript" } } } }
]
}) {
id
title
}
}
Интеграция с Next.js
Keystone можно поднять отдельным процессом или встроить в Next.js через next.config.js. Для monorepo — отдельный сервис предпочтительнее:
// lib/keystoneClient.ts
import { GraphQLClient } from 'graphql-request'
export const keystoneClient = new GraphQLClient(
process.env.KEYSTONE_API_URL || 'http://localhost:3000/api/graphql',
{
headers: { 'x-api-key': process.env.KEYSTONE_API_KEY! },
}
)
// Типизированные запросы через graphql-codegen
import { getSdk } from './__generated__/sdk'
export const cms = getSdk(keystoneClient)
// app/blog/[slug]/page.tsx
import { cms } from '@/lib/keystoneClient'
export default async function PostPage({ params }) {
const { post } = await cms.getPostBySlug({ slug: params.slug })
if (!post) notFound()
return <ArticleLayout post={post} />
}
export async function generateStaticParams() {
const { posts } = await cms.getAllPostSlugs()
return posts.map(p => ({ slug: p.slug }))
}
Keystone Document Field
Встроенный rich text — не просто строка, а структурированный документ (похоже на Portable Text):
import { DocumentRenderer } from '@keystone-6/document-renderer'
function PostContent({ content }) {
return (
<DocumentRenderer
document={content.document}
renderers={{
block: {
paragraph: ({ children, textAlign }) => (
<p style={{ textAlign }} className="mb-4">{children}</p>
),
layout: ({ layout, children }) => (
<div className={`grid grid-cols-${layout.join('-')}`}>
{children}
</div>
),
},
inline: {
link: ({ children, href }) => (
<a href={href} className="text-blue-600 underline">{children}</a>
),
},
}}
/>
)
}
Деплой
Keystone — это Node.js процесс. Для production:
keystone build # сборка
keystone start # запуск
На Railway, Fly.io, Render — dockerfile стандартный. PostgreSQL через Supabase или собственный инстанс. Переменные окружения: DATABASE_URL, SESSION_SECRET, ASSET_BASE_URL.
Сроки
Стандартная интеграция с 4–6 типами контента, настройкой аутентификации и GraphQL-клиентом в Next.js: 6–8 дней. С настройкой codegen, кастомными хуками и S3-хранилищем: до 12 дней.







