База знаний
Notion как Headless CMS: контент-движок для сайта
Как использовать Notion в качестве headless CMS: API, Content Collections в Astro, структура баз данных, лимиты и сравнение с альтернативами.
СейчасЧто такое Headless CMS
- Что такое Headless CMS
- Почему Notion
- Архитектура и поток данных
- Поток данных
- Полезные сценарии
- Сценарий 1: публикация статических страниц из Notion
- Сценарий 2: автоматический ребилд после изменения страницы
- Notion API: основы
- Получение токена
- Ключевые эндпоинты
- Пример: запрос статей из источника данных
- Пагинация
- Подключение Notion к Astro
- Кастомный загрузчик
- Подключение в Content Collections
- Генерация страниц
- Структура базы данных для CMS
- Конвертация блоков Notion в HTML
- Кастомные трансформеры
- Работа с изображениями
- Решение: скачивание при билде
- Тарифы и лимиты
- Автоматический ребилд после изменения контента
- Вариант 1: Webhook Notion
- Вариант 2: Ручной
- Вариант 3: GitHub Actions по расписанию
- Вариант 4: Polling через n8n / Make
- Сравнение с альтернативами
- Типичные проблемы и решения
- Проверка результата
- Быстрый старт
- Полезные ссылки
- Следующий шаг
- Связанные материалы
Практическое руководство по использованию Notion в качестве headless CMS — бэкенда для контента сайта. Контент редактируется в Notion, а сайт отображает его через API.
Оглавление
- Что такое Headless CMS — как разделяются контент и фронтенд
- Почему Notion — возможности и ограничения связки
- Архитектура и поток данных — что происходит от редактирования до публикации
- Полезные сценарии — два рабочих варианта применения
- Notion API: основы — токены, доступ, запросы и пагинация
- Подключение Notion к Astro — кастомный загрузчик и генерация страниц
- Структура базы данных — рекомендуемые свойства контентной базы
- Конвертация блоков — Markdown, HTML и трансформеры
- Работа с изображениями — временные URL и локальное сохранение
- Тарифы и лимиты — ограничения, которые нужно учитывать
- Автоматический ребилд — webhook, расписание и polling
- Сравнение с альтернативами — как выбрать направление
- Типичные проблемы — быстрый поиск причин и решений
- Быстрый старт и ссылки — минимальная последовательность действий
Что такое Headless CMS
Традиционная CMS обычно объединяет редактор и отображение сайта в одной системе. В headless CMS эти роли разделены:
| Критерий | Традиционная CMS | Headless CMS |
| Редактирование | Встроенный редактор | Отдельный интерфейс (Notion, Contentful, Sanity) |
| Отображение | Зависит от возможностей CMS | Зависит от выбранного фронтенда (Astro, Next.js, Hugo) |
| Гибкость | Зависит от тем и расширений | Фронтенд и дизайн контролирует команда проекта |
| Скорость сайта | Зависит от CMS и конфигурации | Зависит от фронтенда и инфраструктуры |
Почему Notion
Notion можно использовать как источник контента для сайта, если редактору удобно работать со страницами и базами:
- Блочный редактор — страницы состоят из блоков, а API работает с блочной моделью и Notion-flavored Markdown
- Базы данных и источники данных — помогают хранить записи и свойства, по которым можно строить запросы
- API — HTTP API позволяет создавать страницы, читать контент и работать с объектами программно
- Совместная работа — подходит для командного редактирования контента
- Привычный интерфейс — редактору не приходится осваивать отдельную CMS
Архитектура и поток данных
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["Посетитель сайта"]Поток данных
- Редактирование — вы пишете и редактируете контент в Notion, как обычно
- Билд — Astro через API забирает контент из источника данных Notion
- Генерация — каждая запись превращается в страницу сайта
- Деплой — готовые файлы загружаются на сервер
- Просмотр — посетитель получает статические файлы сайта
Полезные сценарии
Сценарий 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):
- Откройте раздел Personal access tokens в Developer portal
- Нажмите New token
- Укажите имя и выберите возможность Notion API
- Создайте токен, скопируйте его и сохраните в безопасном месте: повторно показать значение нельзя
Для доступа через подключение (connection) откройте Settings → Connections, установите connection и добавьте его к конкретной странице или базе через меню ... → Add connections. В меню управления connection доступно действие Retrieve an internal API token.
Ключевые эндпоинты
Для примера ниже используется endpoint источника данных. В документации Notion разделы Data sources и Databases (deprecated) ведутся отдельно; при миграции проверяйте endpoint и заголовок Notion-Version по выбранной версии API.
| Эндпоинт | Метод | Что делает |
/v1/data_sources/{id}/query | POST | Получить страницы источника данных с фильтрами и сортировкой |
/v1/pages/{id} | GET | Получить свойства страницы |
/v1/blocks/{id}/children | GET | Получить контент страницы (блоки) |
/v1/search | POST | Поиск по доступным подключению страницам и источникам данных |
Пример: запрос статей из источника данных
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
Рекомендуемая схема свойств для контентной базы:
| Свойство | Тип | Зачем |
| Name | Title | Заголовок статьи, отображается на сайте |
| Slug | Text | URL-адрес: /knowledge/astro-framework |
| Описание | Text | Мета-описание для SEO и карточек |
| Категория | Select | Группировка: Фреймворки, Инструменты, Методологии |
| Теги | Multi-select | Перекрёстная навигация и фильтрация |
| Статус | Select | Черновик / Опубликовано — фильтр при билде |
| Обложка | File | Изображение для карточки и OG-тега |
| Дата публикации | Date | Сортировка и отображение на сайте |
| Порядок | Number | Ручная сортировка внутри категории |
Конвертация блоков 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 markedimport { 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 | Возвращается без изменения и не истекает |
Решение: скачивание при билде
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-ссылки.
Тарифы и лимиты
2026-03-11. Для изменяемых тарифов и ограничений рабочего пространства сверяйтесь с действующей страницей тарифов Notion перед внедрением.| Параметр | Значение |
| Rate limit | В среднем 3 запроса в секунду; при превышении лимита повторяйте запрос с задержкой |
| Пагинация | Курсорная: has_more, next_cursor, start_cursor |
| Блоки страницы | Список дочерних блоков может быть разбит на страницы |
| Вложенность | Дочерние блоки нужно запрашивать отдельно |
| Вебхуки | Поддерживаются для событий страниц, баз, источников данных, комментариев и других объектов |
| Notion-hosted файлы | Временный публичный URL действует 1 час |
Автоматический ребилд после изменения контента
Notion поддерживает connection webhooks: при изменении страницы или базы сервис отправляет HTTP POST на ваш публичный HTTPS-эндпоинт. Это позволяет запускать ребилд без постоянного polling.
Вариант 1: Webhook Notion
- Откройте настройки нужного connection
- Перейдите на вкладку Webhooks и создайте подписку
- Укажите публичный HTTPS URL и выберите события, например
page.content_updated - Получите
verification_tokenв первом POST-запросе и подтвердите его в настройках подписки - Проверяйте заголовок
X-Notion-Signatureперед запуском автоматизации - После подтверждённого события вызывайте GitHub Actions, API хостинга или собственную очередь сборки
События изменения содержимого могут агрегироваться и приходить с небольшой задержкой. Payload содержит метаданные и идентификатор объекта; актуальное содержимое после события нужно получить отдельным запросом к API.
Вариант 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 предпочтительнее там, где его можно безопасно принять.
Сравнение с альтернативами
| Критерий | Notion | Contentful | Sanity | Strapi |
| Тип | Workspace + API | Профильная CMS | Профильная CMS | CMS для самостоятельного размещения |
| Редактор | Блочная модель | Проверяйте актуальную документацию продукта | Проверяйте актуальную документацию продукта | Проверяйте актуальную документацию и конфигурацию |
| API | REST | Проверяйте актуальную документацию продукта | Проверяйте актуальную документацию продукта | Проверяйте актуальную документацию продукта |
| Вебхуки | ✅ | Проверяйте актуальную документацию продукта | Проверяйте актуальную документацию продукта | Проверяйте актуальную документацию продукта |
| Медиа | Временные URL для Notion-hosted файлов; внешние URL поддерживаются | Зависит от продукта и конфигурации | Зависит от продукта и конфигурации | Зависит от продукта и конфигурации |
| Тарифы | Сверяйте актуальные условия Notion | Сверяйте актуальные условия продукта | Сверяйте актуальные условия продукта | Сверяйте модель размещения и актуальные условия |
| Для кого | Если вы уже используете Notion и принимаете build-time workflow | Если нужны специализированная CMS и командные процессы | Если важны гибкая модель контента и кастомизация | Если нужен контроль над самостоятельным размещением |
Типичные проблемы и решения
| Проблема | Причина | Решение |
| Ошибка доступа при запросе объекта | Connection не подключён к странице или базе | Notion → страница или база → ... → Add connections |
| Пустой контент страницы | Запрашиваете свойства, а не блоки | Используйте /blocks/{id}/children для контента |
| Сломанные картинки через час | Временный URL истёк | Скачивайте изображения при билде или повторно запрашивайте file object |
| Медленный билд | Много запросов к API | Кэшируйте ответы, используйте инкрементальный билд |
| Вложенные блоки не загружаются | Дочерние блоки не получены отдельным запросом | Рекурсивно запрашивайте блоки с has_children: true |
| Webhook не приходит | Подписка не подтверждена, connection не имеет доступа или не включена нужная capability | Проверьте статус подписки, доступ к объекту и выбранные события |
| Webhook приходит, но подпись не совпадает | Подпись проверяется по повторно сериализованному JSON | Передавайте в HMAC-проверку исходное тело запроса без изменений |
Проверка результата
После первого запуска проверьте наблюдаемые признаки успешной связки:
npm run buildзавершается без ошибки.- В каталоге сборки появляется HTML для тестовой записи из Notion.
- В HTML присутствует обновлённый заголовок или текст тестовой страницы.
- Notion-hosted изображение открывается по локальному пути сайта после скачивания при билде.
- После изменения тестовой страницы и повторного билда в HTML появляется новая версия текста.
- Для webhook-сценария сервер получает POST, проверка подписи проходит, а запуск сборки появляется в журнале CI.
Этот материал описывает настройку по официальной документации и примерам; фактический запуск в конкретном проекте нужно подтвердить этими проверками.
Быстрый старт
- Создайте персональный токен доступа в Developer portal либо настройте connection в разделе
Settings → Connections - Если используете connection, добавьте его к нужной странице или базе
- Установите зависимости:
npm install @notionhq/client notion-to-md marked- Создайте
.env:
NOTION_TOKEN=ntn_xxxxxxxxxxxx
NOTION_KB_DATA_SOURCE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx- Напишите загрузчик и подключите его в
content.config.ts - Запустите билд:
npm run build - Измените тестовую страницу и повторите билд
- Проверьте, что новая версия текста появилась в сгенерированном HTML, а Notion-hosted изображения сохранены локально или заменены стабильными внешними URL
- Для автоматического обновления создайте и подтвердите webhook-подписку либо настройте сборку по расписанию
Полезные ссылки
- Notion API — документация
- Notion API Quickstart
- Notion Webhooks
- Объекты файлов Notion
- Notion SDK для JavaScript
- notion-to-md — конвертер блоков в Markdown
- Astro Content Loader API
- Notion API Changelog
Следующий шаг
Если вы используете Astro для сайта, руководство по Astro: фреймворку для молниеносных сайтов поможет продолжить настройку фронтенда и сборки.
Связанные материалы
- Статья: Как два ИИ-агента и один человек собрали этот сайт за ночь
- Блог: Notion делает агентов правильно — и вот почему это заметно
- База знаний: Notion как рабочая база
Если вы выстраиваете контентный стек для сайта, эти материалы помогут сопоставить CMS, автоматизацию и процесс сборки. Они пригодятся тем, кто выбирает между готовым инструментом и собственной связкой.
Если захотите обсудить, как это применить у себя или в команде — пишите в Telegram @pimenov
Если хотите разобрать свою задачу — напишите мне Если хотите разобрать свою задачу — напишите мне.
Можно прийти с идеей, черновым контекстом или уже живой задачей. Помогу быстро понять, где реальный следующий шаг, а где лишний шум.
Обычно хватает 2–3 сообщений, чтобы понять, могу ли я здесь реально помочь и в каком формате лучше двигаться дальше.
Дальше по теме
Notion запустил новый тип представления баз данных — Dashboards. Доски, таблицы, графики и таймлайны теперь живут в одном виде. Разбираюсь, зачем это нужно и кому пригодится.