Розробка сайту на 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 тижнів.







