pimenov.ai

База знаний

Notion как Headless CMS: контент-движок для сайта

Как использовать Notion в качестве headless CMS: API, Content Collections в Astro, структура баз данных, лимиты и сравнение с альтернативами.

Опубликовано Обновлено
СейчасЧто такое Headless CMS
  1. Что такое Headless CMS
  2. Почему Notion
  3. Архитектура и поток данных
  4. Поток данных
  5. Полезные сценарии
  6. Сценарий 1: публикация статических страниц из Notion
  7. Сценарий 2: автоматический ребилд после изменения страницы
  8. Notion API: основы
  9. Получение токена
  10. Ключевые эндпоинты
  11. Пример: запрос статей из источника данных
  12. Пагинация
  13. Подключение Notion к Astro
  14. Кастомный загрузчик
  15. Подключение в Content Collections
  16. Генерация страниц
  17. Структура базы данных для CMS
  18. Конвертация блоков Notion в HTML
  19. Кастомные трансформеры
  20. Работа с изображениями
  21. Решение: скачивание при билде
  22. Тарифы и лимиты
  23. Автоматический ребилд после изменения контента
  24. Вариант 1: Webhook Notion
  25. Вариант 2: Ручной
  26. Вариант 3: GitHub Actions по расписанию
  27. Вариант 4: Polling через n8n / Make
  28. Сравнение с альтернативами
  29. Типичные проблемы и решения
  30. Проверка результата
  31. Быстрый старт
  32. Полезные ссылки
  33. Следующий шаг
  34. Связанные материалы

Практическое руководство по использованию Notion в качестве headless CMS — бэкенда для контента сайта. Контент редактируется в Notion, а сайт отображает его через API.

📌
Версия API: Notion API 2026-03-11

Документация: официальный API Quickstart

Оглавление

  1. Что такое Headless CMS — как разделяются контент и фронтенд
  2. Почему Notion — возможности и ограничения связки
  3. Архитектура и поток данных — что происходит от редактирования до публикации
  4. Полезные сценарии — два рабочих варианта применения
  5. Notion API: основы — токены, доступ, запросы и пагинация
  6. Подключение Notion к Astro — кастомный загрузчик и генерация страниц
  7. Структура базы данных — рекомендуемые свойства контентной базы
  8. Конвертация блоков — Markdown, HTML и трансформеры
  9. Работа с изображениями — временные URL и локальное сохранение
  10. Тарифы и лимиты — ограничения, которые нужно учитывать
  11. Автоматический ребилд — webhook, расписание и polling
  12. Сравнение с альтернативами — как выбрать направление
  13. Типичные проблемы — быстрый поиск причин и решений
  14. Быстрый старт и ссылки — минимальная последовательность действий

Что такое Headless CMS

💡
Headless CMS — система управления контентом без собственного фронтенда. Контент хранится и редактируется в одном месте, а отображается на сайте через API. «Голова» (интерфейс для посетителей) — отдельная, вы строите её сами.

Традиционная CMS обычно объединяет редактор и отображение сайта в одной системе. В headless CMS эти роли разделены:

КритерийТрадиционная CMSHeadless CMS
РедактированиеВстроенный редакторОтдельный интерфейс (Notion, Contentful, Sanity)
ОтображениеЗависит от возможностей CMSЗависит от выбранного фронтенда (Astro, Next.js, Hugo)
ГибкостьЗависит от тем и расширенийФронтенд и дизайн контролирует команда проекта
Скорость сайтаЗависит от CMS и конфигурацииЗависит от фронтенда и инфраструктуры

Почему Notion

Notion можно использовать как источник контента для сайта, если редактору удобно работать со страницами и базами:

  • Блочный редактор — страницы состоят из блоков, а API работает с блочной моделью и Notion-flavored Markdown
  • Базы данных и источники данных — помогают хранить записи и свойства, по которым можно строить запросы
  • API — HTTP API позволяет создавать страницы, читать контент и работать с объектами программно
  • Совместная работа — подходит для командного редактирования контента
  • Привычный интерфейс — редактору не приходится осваивать отдельную CMS
⚖️
Компромисс: Notion не создавался как CMS. API имеет ограничения, а для Notion-hosted изображений выдаются временные URL вместо постоянной ссылки. Connection webhooks позволяют реагировать на изменения без постоянного polling, но требуют публичного HTTPS-эндпоинта и отдельной настройки подписки.

Архитектура и поток данных

flowchart LR
    A["Notion\n(редактирование контента)"] -->|"Notion API"| B["Astro\n(генератор сайта)"]
    B -->|"npm run build"| C["HTML/CSS/JS\n(статические файлы)"]
    C -->|"rsync / deploy"| D["VPS / Хостинг\n(nginx)"]
    D -->|"HTTPS"| E["Посетитель сайта"]

Поток данных

  1. Редактирование — вы пишете и редактируете контент в Notion, как обычно
  2. Билд — Astro через API забирает контент из источника данных Notion
  3. Генерация — каждая запись превращается в страницу сайта
  4. Деплой — готовые файлы загружаются на сервер
  5. Просмотр — посетитель получает статические файлы сайта
📌
Ключевой момент: контент подтягивается на этапе билда, а не при каждом запросе посетителя. Поэтому после редактирования в Notion нужно запустить ребилд сайта — вручную, по расписанию или через webhook.

Полезные сценарии

Сценарий 1: публикация статических страниц из Notion

Задача: редактору нужно вести статьи в Notion, а посетителям — получать отдельные страницы сайта.

Условия: есть источник данных Notion, connection или персональный токен с доступом к нему, а Astro-проект умеет выполнять сборку.

Действия: на этапе билда запросите опубликованные записи, получите содержимое страниц и преобразуйте его в HTML. Notion-hosted изображения скачайте в каталог сайта или замените внешними URL.

Проверяемый результат: после npm run build в каталоге сборки появляется HTML-страница статьи, а локальный путь к скачанному изображению открывается вместе со страницей.

Ограничение: изменение текста в Notion само по себе не меняет уже собранные статические файлы. Нужен повторный билд.

Сценарий 2: автоматический ребилд после изменения страницы

Задача: запускать сборку после изменения контента без постоянного polling API.

Условия: connection webhook, публичный HTTPS-эндпоинт и автоматизация, которая умеет вызвать CI или команду сборки.

Действия: создайте подписку на событие, подтвердите её через verification_token, проверяйте X-Notion-Signature, затем запускайте сборку по идентификатору объекта из payload.

Проверяемый результат: после изменения тестовой страницы сервер получает POST, запись появляется в журнале обработчика, а новая сборка содержит изменённый текст.

Ограничение: некоторые события агрегируются и приходят с задержкой; после события актуальное содержимое нужно получить отдельным запросом к API.


Notion API: основы

Получение токена

Для личного доступа используйте персональный токен доступа (Personal Access Token, PAT):

  1. Откройте раздел Personal access tokens в Developer portal
  2. Нажмите New token
  3. Укажите имя и выберите возможность Notion API
  4. Создайте токен, скопируйте его и сохраните в безопасном месте: повторно показать значение нельзя

Для доступа через подключение (connection) откройте Settings → Connections, установите connection и добавьте его к конкретной странице или базе через меню ...Add connections. В меню управления connection доступно действие Retrieve an internal API token.

⚠️
Важно: подключение нужно добавить к конкретной странице или базе данных. Без необходимого доступа API не сможет прочитать объект даже с правильным токеном.

Ключевые эндпоинты

Для примера ниже используется endpoint источника данных. В документации Notion разделы Data sources и Databases (deprecated) ведутся отдельно; при миграции проверяйте endpoint и заголовок Notion-Version по выбранной версии API.

ЭндпоинтМетодЧто делает
/v1/data_sources/{id}/queryPOSTПолучить страницы источника данных с фильтрами и сортировкой
/v1/pages/{id}GETПолучить свойства страницы
/v1/blocks/{id}/childrenGETПолучить контент страницы (блоки)
/v1/searchPOSTПоиск по доступным подключению страницам и источникам данных

Пример: запрос статей из источника данных

const response = await fetch(
  `https://api.notion.com/v1/data_sources/${DATA_SOURCE_ID}/query`,
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${NOTION_TOKEN}`,
      'Notion-Version': '2026-03-11',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      filter: {
        property: 'Статус',
        select: { equals: 'Опубликовано' },
      },
      sorts: [
        { property: 'Порядок', direction: 'ascending' },
      ],
    }),
  }
);

const data = await response.json();
// data.results — массив страниц со свойствами

Пагинация

API использует курсорную пагинацию. Если ответ содержит has_more: true, передайте next_cursor в следующем запросе:

let allPages = [];
let cursor = undefined;

do {
  const response = await notion.dataSources.query({
    data_source_id: DATA_SOURCE_ID,
    start_cursor: cursor,
  });
  allPages.push(...response.results);
  cursor = response.has_more ? response.next_cursor : undefined;
} while (cursor);

Подключение Notion к Astro

Notion + Astro связываются через Content Collections и кастомный загрузчик. Astro поддерживает загрузчики, которые получают данные на этапе сборки. Для удалённого контента готового встроенного загрузчика нет: его нужно написать самостоятельно или подключить библиотеку сообщества.

Кастомный загрузчик

Загрузчик — функция, которая забирает данные из Notion API и превращает их в формат, понятный Astro. Асинхронный функциональный загрузчик должен вернуть массив записей с уникальным полем id либо объект, где ключи являются уникальными идентификаторами:

// src/loaders/notion-loader.ts
import { Client } from '@notionhq/client';
import { NotionToMarkdown } from 'notion-to-md';

const notion = new Client({ auth: import.meta.env.NOTION_TOKEN });
const n2m = new NotionToMarkdown({ notionClient: notion });

export async function notionLoader(dataSourceId: string) {
  const pages = await getAllPages(dataSourceId);

  return Promise.all(
    pages.map(async (page) => {
      const mdBlocks = await n2m.pageToMarkdown(page.id);
      const content = n2m.toMarkdownString(mdBlocks).parent;

      return {
        id: page.id,
        slug: getProperty(page, 'Slug'),
        title: getProperty(page, 'Name'),
        description: getProperty(page, 'Описание'),
        category: getProperty(page, 'Категория'),
        tags: getProperty(page, 'Теги'),
        order: getProperty(page, 'Порядок'),
        publishedAt: getProperty(page, 'Дата публикации'),
        content,
      };
    })
  );
}

Подключение в Content Collections

// src/content.config.ts
import { defineCollection } from 'astro:content';
import { z } from 'astro/zod';
import { notionLoader } from './loaders/notion-loader';

const knowledgeBase = defineCollection({
  loader: () => notionLoader(import.meta.env.NOTION_KB_DATA_SOURCE_ID),
  schema: z.object({
    slug: z.string(),
    title: z.string(),
    description: z.string(),
    category: z.string(),
    tags: z.array(z.string()),
    order: z.number(),
    publishedAt: z.string().optional(),
    content: z.string(),
  }),
});

export const collections = { knowledgeBase };

Генерация страниц

В этом примере content содержит Markdown. Перед передачей в set:html его нужно преобразовать в HTML; ниже для этого используется marked.

---
// src/pages/knowledge/[slug].astro
import { getCollection } from 'astro:content';
import { marked } from 'marked';
import BaseLayout from '../../layouts/BaseLayout.astro';

export async function getStaticPaths() {
  const articles = await getCollection('knowledgeBase');
  return articles.map((article) => ({
    params: { slug: article.data.slug },
    props: { article },
  }));
}

const { article } = Astro.props;
const contentHtml = await marked.parse(article.data.content);
---
<BaseLayout title={article.data.title}>
  <article class="prose dark:prose-invert max-w-prose mx-auto">
    <h1>{article.data.title}</h1>
    <p class="text-gray-500">{article.data.description}</p>
    <Fragment set:html={contentHtml} />
  </article>
</BaseLayout>

Структура базы данных для CMS

Рекомендуемая схема свойств для контентной базы:

СвойствоТипЗачем
NameTitleЗаголовок статьи, отображается на сайте
SlugTextURL-адрес: /knowledge/astro-framework
ОписаниеTextМета-описание для SEO и карточек
КатегорияSelectГруппировка: Фреймворки, Инструменты, Методологии
ТегиMulti-selectПерекрёстная навигация и фильтрация
СтатусSelectЧерновик / Опубликовано — фильтр при билде
ОбложкаFileИзображение для карточки и OG-тега
Дата публикацииDateСортировка и отображение на сайте
ПорядокNumberРучная сортировка внутри категории
💡
Совет: фильтруйте по статусу на этапе API-запроса, а не в коде. Так черновики не попадут в билд, даже если забудете проверку в шаблоне.

Конвертация блоков Notion в HTML

Notion API работает с блочной моделью. Кроме того, API поддерживает Notion-flavored Markdown в отдельных операциях, например при создании страницы; блочная модель нужна для точного контроля форматирования, цветов, вложенности и типов блоков.

{
  "object": "block",
  "type": "heading_2",
  "heading_2": {
    "rich_text": [
      { "plain_text": "Заголовок раздела", "annotations": { "bold": false } }
    ]
  }
}

Для конвертации блоков в Markdown или HTML можно использовать библиотеку notion-to-md:

npm install @notionhq/client notion-to-md marked
import { NotionToMarkdown } from 'notion-to-md';

const n2m = new NotionToMarkdown({ notionClient: notion });
const mdBlocks = await n2m.pageToMarkdown(pageId);
const markdown = n2m.toMarkdownString(mdBlocks).parent;

Если результатом является Markdown, перед выводом через set:html преобразуйте его выбранным Markdown-рендерером и примените подходящую для проекта санитизацию.

Кастомные трансформеры

Стандартная конвертация не покрывает все кейсы. Для callout-блоков, таблиц или встраиваний нужны кастомные трансформеры:

n2m.setCustomTransformer('callout', async (block) => {
  const text = block.callout.rich_text
    .map((t) => t.plain_text)
    .join('');
  const icon = block.callout.icon?.emoji || '💡';
  return `<div class="callout">${icon} ${text}</div>`;
});

Работа с изображениями

Notion API различает три источника файлов:

ТипИсточникДоступ
Notion-hosted (file)Файл, загруженный вручную через интерфейс NotionВременный URL, действующий 1 час
File upload (file_upload)Файл, загруженный через File Upload APIВ API представляется идентификатором загрузки
External (external)Публичная ссылка на вашем сервере или CDNВозвращается без изменения и не истекает
⚠️
Критично: временный URL Notion-hosted файла действует один час. Не кэшируйте и не вставляйте такой URL в статическую страницу. Скачайте файл на этапе билда либо повторно запросите объект файла, чтобы получить свежую ссылку.

Решение: скачивание при билде

import fs from 'fs';
import path from 'path';

async function downloadImage(url, slug, index) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Image download failed: ${response.status}`);
  }
  const buffer = await response.arrayBuffer();
  const contentType = response.headers.get('content-type') || '';
  const ext = contentType.includes('png') ? 'png' : 'jpg';
  const filename = `${slug}-${index}.${ext}`;
  const filepath = path.join('public/images/content', filename);

  fs.mkdirSync(path.dirname(filepath), { recursive: true });
  fs.writeFileSync(filepath, Buffer.from(buffer));

  return `/images/content/${filename}`;
}

Альтернатива — хранить изображения на внешнем сервисе (Cloudinary, imgix, S3) и вставлять в Notion как внешние HTTPS-ссылки.


Тарифы и лимиты

📌
Проверено 25 августа 2026 года: актуальная версия API в официальном quickstart — 2026-03-11. Для изменяемых тарифов и ограничений рабочего пространства сверяйтесь с действующей страницей тарифов Notion перед внедрением.
ПараметрЗначение
Rate limitВ среднем 3 запроса в секунду; при превышении лимита повторяйте запрос с задержкой
ПагинацияКурсорная: has_more, next_cursor, start_cursor
Блоки страницыСписок дочерних блоков может быть разбит на страницы
ВложенностьДочерние блоки нужно запрашивать отдельно
ВебхукиПоддерживаются для событий страниц, баз, источников данных, комментариев и других объектов
Notion-hosted файлыВременный публичный URL действует 1 час
💡
На практике: добавьте к API-клиенту обработку пагинации, повтор запросов после превышения лимита и кэширование неизменившегося контента. Продолжительность полного билда зависит от числа страниц, вложенных блоков и скачиваемых файлов.

Автоматический ребилд после изменения контента

Notion поддерживает connection webhooks: при изменении страницы или базы сервис отправляет HTTP POST на ваш публичный HTTPS-эндпоинт. Это позволяет запускать ребилд без постоянного polling.

Вариант 1: Webhook Notion

  1. Откройте настройки нужного connection
  2. Перейдите на вкладку Webhooks и создайте подписку
  3. Укажите публичный HTTPS URL и выберите события, например page.content_updated
  4. Получите verification_token в первом POST-запросе и подтвердите его в настройках подписки
  5. Проверяйте заголовок X-Notion-Signature перед запуском автоматизации
  6. После подтверждённого события вызывайте GitHub Actions, API хостинга или собственную очередь сборки

События изменения содержимого могут агрегироваться и приходить с небольшой задержкой. Payload содержит метаданные и идентификатор объекта; актуальное содержимое после события нужно получить отдельным запросом к API.

⚠️
Безопасность: для production проверяйте HMAC-SHA256 подпись по исходному телу запроса. Повторная сериализация JSON меняет байты и приводит к ошибке проверки.

Вариант 2: Ручной

ssh user@server 'cd /var/www/site && npm run build'

Вариант 3: GitHub Actions по расписанию

# .github/workflows/rebuild.yml
name: Rebuild site
on:
  schedule:
    - cron: '0 */6 * * *'  # каждые 6 часов
  workflow_dispatch:         # ручной запуск

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build
        env:
          NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
      - name: Deploy
        run: rsync -avz ./dist/ user@server:/var/www/site/

Вариант 4: Polling через n8n / Make

Если публичный webhook-обработчик недоступен, настройте polling: раз в N минут проверяйте last_edited_time. При изменении запускайте GitHub Actions через API. Этот вариант создаёт лишние запросы и реагирует медленнее, поэтому webhook предпочтительнее там, где его можно безопасно принять.


Сравнение с альтернативами

КритерийNotionContentfulSanityStrapi
ТипWorkspace + APIПрофильная CMSПрофильная CMSCMS для самостоятельного размещения
РедакторБлочная модельПроверяйте актуальную документацию продуктаПроверяйте актуальную документацию продуктаПроверяйте актуальную документацию и конфигурацию
APIRESTПроверяйте актуальную документацию продуктаПроверяйте актуальную документацию продуктаПроверяйте актуальную документацию продукта
ВебхукиПроверяйте актуальную документацию продуктаПроверяйте актуальную документацию продуктаПроверяйте актуальную документацию продукта
МедиаВременные URL для Notion-hosted файлов; внешние URL поддерживаютсяЗависит от продукта и конфигурацииЗависит от продукта и конфигурацииЗависит от продукта и конфигурации
ТарифыСверяйте актуальные условия NotionСверяйте актуальные условия продуктаСверяйте актуальные условия продуктаСверяйте модель размещения и актуальные условия
Для когоЕсли вы уже используете Notion и принимаете build-time workflowЕсли нужны специализированная CMS и командные процессыЕсли важны гибкая модель контента и кастомизацияЕсли нужен контроль над самостоятельным размещением
📌
Сведения об альтернативах приведены как ориентир для выбора направления. Перед внедрением проверяйте текущую документацию, модель размещения и условия конкретного продукта.
📌
Вывод: Notion подходит, если контент уже ведётся в его страницах и базах, а сайту нужен отдельный фронтенд. Для проектов, где критичны специализированное моделирование контента, встроенный медиаконвейер или полный контроль над инфраструктурой, сравните его с профильной headless CMS.

Типичные проблемы и решения

ПроблемаПричинаРешение
Ошибка доступа при запросе объектаConnection не подключён к странице или базеNotion → страница или база → ... → Add connections
Пустой контент страницыЗапрашиваете свойства, а не блокиИспользуйте /blocks/{id}/children для контента
Сломанные картинки через часВременный URL истёкСкачивайте изображения при билде или повторно запрашивайте file object
Медленный билдМного запросов к APIКэшируйте ответы, используйте инкрементальный билд
Вложенные блоки не загружаютсяДочерние блоки не получены отдельным запросомРекурсивно запрашивайте блоки с has_children: true
Webhook не приходитПодписка не подтверждена, connection не имеет доступа или не включена нужная capabilityПроверьте статус подписки, доступ к объекту и выбранные события
Webhook приходит, но подпись не совпадаетПодпись проверяется по повторно сериализованному JSONПередавайте в HMAC-проверку исходное тело запроса без изменений

Проверка результата

После первого запуска проверьте наблюдаемые признаки успешной связки:

  1. npm run build завершается без ошибки.
  2. В каталоге сборки появляется HTML для тестовой записи из Notion.
  3. В HTML присутствует обновлённый заголовок или текст тестовой страницы.
  4. Notion-hosted изображение открывается по локальному пути сайта после скачивания при билде.
  5. После изменения тестовой страницы и повторного билда в HTML появляется новая версия текста.
  6. Для webhook-сценария сервер получает POST, проверка подписи проходит, а запуск сборки появляется в журнале CI.

Этот материал описывает настройку по официальной документации и примерам; фактический запуск в конкретном проекте нужно подтвердить этими проверками.


Быстрый старт

  1. Создайте персональный токен доступа в Developer portal либо настройте connection в разделе Settings → Connections
  2. Если используете connection, добавьте его к нужной странице или базе
  3. Установите зависимости:
npm install @notionhq/client notion-to-md marked
  1. Создайте .env:
NOTION_TOKEN=ntn_xxxxxxxxxxxx
NOTION_KB_DATA_SOURCE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  1. Напишите загрузчик и подключите его в content.config.ts
  2. Запустите билд: npm run build
  3. Измените тестовую страницу и повторите билд
  4. Проверьте, что новая версия текста появилась в сгенерированном HTML, а Notion-hosted изображения сохранены локально или заменены стабильными внешними URL
  5. Для автоматического обновления создайте и подтвердите webhook-подписку либо настройте сборку по расписанию

Полезные ссылки


Следующий шаг

Если вы используете Astro для сайта, руководство по Astro: фреймворку для молниеносных сайтов поможет продолжить настройку фронтенда и сборки.

Связанные материалы

Если вы выстраиваете контентный стек для сайта, эти материалы помогут сопоставить CMS, автоматизацию и процесс сборки. Они пригодятся тем, кто выбирает между готовым инструментом и собственной связкой.

Если захотите обсудить, как это применить у себя или в команде — пишите в Telegram @pimenov