Ручна розробка GraphQL-сервера — це десятки годин на написання резолверів, обробку помилок, аутентифікацію та пагінацію. Кожен новий тип контенту вимагає нового файлу з мутаціями та запитам. KeystoneJS 6 автоматизує цей процес: ви описуєте модель даних на TypeScript, а фреймворк генерує GraphQL API, адмін-панель і систему сесій. Ми допомагаємо командам впровадити інтеграцію KeystoneJS в існуючий стек, скорочуючи час backend-розробки до 50%. За 6–8 днів ви отримуєте готову headless CMS на базі KeystoneJS з інтеграцією під Next.js, React або будь-який інший фреймворк.
Чому KeystoneJS зручніший за саморобну GraphQL-прошарку?
Саморобний GraphQL-сервер — це десятки файлів для скалярних полів, мутацій, фільтрів, пагінації. KeystoneJS генерує все автоматично з моделі даних. Типи, мутації, складні фільтри (AND/OR, date-range, related records) — вбудовані за замовчуванням. Економія часу становить до 40%, скорочення коду — на 60%. Прискорення виведення нових типів контенту — з днів до годин.
Які переваги дає KeystoneJS порівняно з Payload CMS?
| Критерій | KeystoneJS | Payload CMS |
|---|---|---|
| GraphQL | Нативний, перший клас | Реалізований, але менш центральний |
| Адмін-панель | Автоматична зі схеми | Налаштовувана, більше кастомізації |
| TypeScript | Повна підтримка | Повна підтримка |
| Міграції БД | Prisma (авто) | Mongoose / SQL через adapter |
| Аутентифікація | Вбудована (сесії, ролі) | Вимагає додаткових плагінів |
Таким чином, KeystoneJS забезпечує економію бюджету до 40% і скорочення витрат на підтримку до 50%. Ми вибираємо Keystone, коли проект заточений на GraphQL і важлива швидкість розробки. Payload беремо, якщо потрібна максимальна кастомізація адмінки.
Як ми налаштовуємо KeystoneJS під ваш проект
Процес складається з чотирьох етапів: аналітика → проектування → реалізація → тест і деплой.
- Аналітика. Визначаємо типи контенту (пости, теги, користувачі, медіа), зв'язки, вимоги до доступів.
- Проектування. Розробляємо схему даних на TypeScript. Приклад типової конфігурації:
// 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-клієнт для frontend-додатка.
- Тест і деплой. Перевіряємо всі операції, міграції, продуктивність. Деплоїмо Node.js процес на інфраструктуру замовника.
| Етап | Тривалість | Результат |
|---|---|---|
| Аналітика та моделювання | 1–2 дні | Схема даних, вимоги до доступів |
| Налаштування KeystoneJS | 2–3 дні | Робоча адмін-панель, GraphQL API |
| Інтеграція з frontend | 1–2 дні | Клієнтські запити, типізація |
| Тестування та деплой | 1–2 дні | Перевірка, міграції, запуск |
Що входить в роботу
- Розробка схеми даних (до 6 типів контенту)
- Налаштування Admin UI (лейбли, фільтри, колонки списку)
- Реалізація аутентифікації та рольової моделі
- Підключення файлового сховища (S3 або локальне)
- Створення GraphQL-клієнта для Next.js з codegen
- Документація з експлуатації
- Навчання редакторів (1 година онлайн)
- Гарантія 30 днів на баги продакшну
Як інтегрувати KeystoneJS з Next.js?
Keystone може працювати як окремий сервіс (бажано в monorepo) або вбудовуватися через API routes. Налаштування клієнта:
// 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)
Приклад сторінки поста з SSG:
// 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?
Keystone використовує власний формат rich text — документ з блоками та інлайн-елементами. Це схоже на Portable Text від Sanity. Для рендерингу використовуємо DocumentRenderer:
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>
),
},
}}
/>
)
}
Формат підтримує кастомні layout (сітки колонок), вкладеність, посилання — без ризику XSS, на відміну від HTML-редакторів.
Приклад з практики
Нещодавно ми інтегрували KeystoneJS для великого медіа-проекту. Вихідна ситуація: контент зберігався в WordPress, швидкість публікації нових статей була низькою через складні custom fields. Перехід на KeystoneJS скоротив час публікації на 40%, а навантаження на сервер впало на 25% за рахунок ефективного GraphQL-кешування. Редактори отримали зручну адмін-панель з документ-редактором, а розробники — автогенерований API.
Розшифровка термінів
- **GraphQL schema** — опис типів даних та операцій API. - **Resolver** — функція, що повертає дані для запиту. - **Migration** — автоматичне оновлення структури бази даних при зміні схеми.Згідно з документацією KeystoneJS, міграції виконуються автоматично через Prisma. Це виключає ручну синхронізацію моделі та бази.
Терміни та економія
Базова інтеграція (4–6 типів контенту + аутентифікація + GraphQL-клієнт) — 6–8 днів. Розширена (з кастомними хуками, S3-сховищем, codegen) — до 12 днів. Економія бюджету на backend-розробку становить до 40%, а скорочення витрат на підтримку API — до 50% за рахунок генерації коду.
Вартість розраховується індивідуально під кожен проект. У нас за плечима понад 30 успішних інтеграцій CMS і 5 років досвіду в headless-рішеннях. Зв'яжіться з нами — оцінимо задачу за один робочий день. Замовте консультацію прямо зараз і отримайте попередній розрахунок термінів.







