Astro calendar_today 23 апр. 2026 г. schedule 2 мин

Astro + Strapi: Headless CMS с собственным API

Полное руководство по интеграции Strapi v5 с Astro.js: настройка коллекций, REST и GraphQL API, типизация TypeScript, webhook-пересборка и деплой.

person
Журналист
Автор
Astro + Strapi — headless CMS интеграция

Strapi — самая популярная open-source Headless CMS. В версии 5 она получила переработанный API, улучшенную типизацию и Document API. В паре с Astro Strapi предоставляет мощный административный интерфейс для контент-менеджеров, а Astro — молниеносный фронтенд.

Strapi vs PocketBase: когда что выбрать?

ПараметрStrapiPocketBase
РазмерТяжёлый (Node.js + npm)Лёгкий (один бинарник)
КастомизацияВысокая (плагины, middleware)Средняя
Интерфейс✅ Полнофункциональный✅ Простой
ПроизводительностьСредняяВысокая
GraphQL✅ Встроен (плагин)❌ Только REST
Роли и права✅ Гибкие✅ Базовые
Команда5-20+ человек1-5 человек

Установка Strapi v5

code
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

code
// 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

code
---
// 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:

code
# В папке Strapi
npm install @strapi/plugin-graphql
code
# Запрос всех статей через GraphQL
query GetArticles {
  articles(sort: "publishedAt:desc", pagination: { limit: 100 }) {
    documentId
    title
    description
    slug
    publishedAt
    cover {
      url
      alternativeText
      width
      height
    }
    tags {
      name
      slug
    }
  }
}
code
// 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:

code
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 (простейший):

code
# railway.toml в корне Strapi
[build]
  builder = "nixpacks"
  buildCommand = "npm run build"
[deploy]
  startCommand = "npm start"
  restartPolicyType = "on-failure"

Вариант 2: VPS с Docker:

code
# 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]

Переменные окружения

code
# .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 без компромиссов.

Портрет автора Дмитрий Соколов

Senior Frontend Engineer / Tech Writer

Senior Frontend Engineer с 9-летним опытом. Специализируется на Astro.js и JAMstack.

Комментарии

Загрузка комментариев...

Оставить комментарий

Комментарии проходят модерацию перед публикацией. Правила

Рекомендуем к прочтению