Strapi — самая популярная open-source Headless CMS. В версии 5 она получила переработанный API, улучшенную типизацию и Document API. В паре с Astro Strapi предоставляет мощный административный интерфейс для контент-менеджеров, а Astro — молниеносный фронтенд.
Strapi vs PocketBase: когда что выбрать?
| Параметр | Strapi | PocketBase |
|---|---|---|
| Размер | Тяжёлый (Node.js + npm) | Лёгкий (один бинарник) |
| Кастомизация | Высокая (плагины, middleware) | Средняя |
| Интерфейс | ✅ Полнофункциональный | ✅ Простой |
| Производительность | Средняя | Высокая |
| GraphQL | ✅ Встроен (плагин) | ❌ Только REST |
| Роли и права | ✅ Гибкие | ✅ Базовые |
| Команда | 5-20+ человек | 1-5 человек |
Установка Strapi v5
npx create-strapi-app@latest my-cms --quickstart
cd my-cms
npm run develop
# → Admin: http://localhost:1337/admin В Admin UI создайте Collection Type Article с полями:
title(Text, Required)content(Rich Text, Blocks)description(Text, Short)slug(UID, Required, from title)cover(Media, Single image)publishedAt(управляется Strapi автоматически)tags(Relation или Component)
Настройка прав доступа (Public API)
В Admin UI → Settings → USERS & PERMISSIONS → Roles → Public:
Для Article: ✅ find, ✅ findOne (чтобы Astro мог читать данные при сборке).
Получение данных из Strapi REST API
// src/lib/strapi.ts
const STRAPI_URL = import.meta.env.STRAPI_URL ?? 'http://localhost:1337';
const STRAPI_TOKEN = import.meta.env.STRAPI_API_TOKEN; // API Token из Admin
export interface StrapiArticle {
id: number;
documentId: string;
title: string;
description: string;
slug: string;
content: StrapiBlock[]; // Rich Text Blocks
cover: {
url: string;
alternativeText: string | null;
width: number;
height: number;
} | null;
publishedAt: string;
createdAt: string;
}
interface StrapiListResponse<T> {
data: T[];
meta: {
pagination: { page: number; pageSize: number; pageCount: number; total: number };
};
}
async function strapiGet<T>(path: string): Promise<T> {
const res = await fetch(`${STRAPI_URL}/api${path}`, {
headers: STRAPI_TOKEN ? { Authorization: `Bearer ${STRAPI_TOKEN}` } : {},
});
if (!res.ok) throw new Error(`Strapi API ${res.status}: ${path}`);
return res.json();
}
export async function getAllArticles(): Promise<StrapiArticle[]> {
const data = await strapiGet<StrapiListResponse<StrapiArticle>>(
'/articles?populate=cover,tags&pagination[pageSize]=100&sort=publishedAt:desc',
);
return data.data;
}
export async function getArticleBySlug(slug: string): Promise<StrapiArticle | null> {
const data = await strapiGet<StrapiListResponse<StrapiArticle>>(
`/articles?filters[slug][$eq]=${slug}&populate=cover,tags`,
);
return data.data[0] ?? null;
} Генерация страниц из Strapi
---
// src/pages/articles/[slug].astro
import { getAllArticles, getArticleBySlug } from '../../lib/strapi';
import { BlocksRenderer } from '@strapi/blocks-react-renderer';
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const articles = await getAllArticles();
return articles.map((article) => ({
params: { slug: article.slug },
props: { article },
}));
}
const { article } = Astro.props;
const coverUrl = article.cover
? `${import.meta.env.STRAPI_URL}${article.cover.url}`
: null;
---
<BaseLayout
title={article.title}
description={article.description}
image={coverUrl ?? undefined}
>
<article>
{
coverUrl && (
<img
src={coverUrl}
alt={article.cover?.alternativeText ?? article.title}
width={article.cover?.width}
height={article.cover?.height}
loading="eager"
/>
)
}
<h1>{article.title}</h1>
<time datetime={article.publishedAt}>
{new Date(article.publishedAt).toLocaleDateString('ru-RU')}
</time>
<!-- Strapi Blocks Renderer (React-остров) -->
<!-- Или рендеринг вручную через рекурсию -->
<div class="article-content">
{/* Рендеринг Rich Text Blocks вручную */}
{
article.content?.map((block) => {
if (block.type === 'paragraph') {
return <p>{block.children.map((c) => c.text).join('')}</p>;
}
if (block.type === 'heading') {
const Tag = `h${block.level}` as any;
return <Tag>{block.children.map((c) => c.text).join('')}</Tag>;
}
return null;
})
}
</div>
</article>
</BaseLayout> GraphQL API (альтернатива REST)
Установите плагин в Strapi:
# В папке Strapi
npm install @strapi/plugin-graphql # Запрос всех статей через GraphQL
query GetArticles {
articles(sort: "publishedAt:desc", pagination: { limit: 100 }) {
documentId
title
description
slug
publishedAt
cover {
url
alternativeText
width
height
}
tags {
name
slug
}
}
} // src/lib/strapi-graphql.ts
export async function getAllArticlesGQL() {
const res = await fetch(`${import.meta.env.STRAPI_URL}/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query {
articles(sort: "publishedAt:desc", pagination: { limit: 100 }) {
documentId slug title description publishedAt
cover { url alternativeText width height }
}
}
`,
}),
});
const { data } = await res.json();
return data.articles;
} Webhook: Автопересборка при публикации
В Strapi Admin → Settings → Webhooks → Create:
Name: Rebuild Astro
URL: https://api.cloudflare.com/client/v4/pages/webhooks/deploy_hooks/HOOK_ID
Events: ✅ entry.publish ✅ entry.unpublish ✅ entry.update Деплой Strapi
Вариант 1: Railway (простейший):
# railway.toml в корне Strapi
[build]
builder = "nixpacks"
buildCommand = "npm run build"
[deploy]
startCommand = "npm start"
restartPolicyType = "on-failure" Вариант 2: VPS с Docker:
# docker-compose.yml
services:
strapi:
build: .
ports: ['1337:1337']
environment:
- DATABASE_CLIENT=postgres
- DATABASE_URL=postgres://user:pass@db:5432/strapi
depends_on: [db]
db:
image: postgres:15
volumes: [./data:/var/lib/postgresql/data] Переменные окружения
# .env (Astro)
STRAPI_URL=https://cms.yourdomain.ru
STRAPI_API_TOKEN=xxxxxxxxxxxx # Admin → API Tokens → Create
# .env.local (Strapi)
DATABASE_CLIENT=postgres
DATABASE_URL=postgres://...
APP_KEYS=key1,key2,key3,key4
JWT_SECRET=your-secret Итог
Strapi v5 + Astro — enterprise-уровень для команд с нетехническими редакторами. Мощный Admin UI с гибкими правами доступа, REST и GraphQL API, поддержка сложных структур данных (компоненты, динамические зоны). Для небольших команд — рассмотрите PocketBase, который проще в эксплуатации. Для серьёзных медиапроектов с большой редакцией — Strapi без компромиссов.