Інтеграція CMS KeystoneJS для управління контентом
KeystoneJS 6 — це headless CMS та application framework в одному. Конфіг схеми пишеться на TypeScript, з неї автоматично генеруються GraphQL API, REST-подібні endpoints та адміністративний інтерфейс. Найближчий аналог за підходом — 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 днів.







