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

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

Версия API: Notion API 2022-06-28 (стабильная)

Лицензия: проприетарный SaaS, есть бесплатный план

Сайт: notion.so


Что такое Headless CMS

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

Традиционные CMS (WordPress, Tilda) — это и редактор, и сайт в одном. Headless CMS разделяет эти роли:

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

Почему Notion

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

  • Удобный редактор — блочный, с поддержкой rich text, таблиц, кода, callout-блоков, изображений
  • Базы данных — структурированное хранение с фильтрами, сортировкой, свойствами (статус, категория, теги, дата)
  • API — полноценный REST API для чтения контента
  • Бесплатный план — достаточен для персонального сайта
  • Совместное редактирование — если работаете не в одиночку
  • Привычный интерфейс — не нужно учить новый инструмент, если вы уже в Notion
Компромисс: Notion не создавался как CMS. API имеет лимиты, нет вебхуков в реальном времени, нет CDN для изображений. Для небольших и средних сайтов (до ~500 страниц) — отлично работает. Для высоконагруженных проектов стоит смотреть на Contentful или Sanity.

Архитектура: как это работает

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. Генерация — каждая страница превращается в статический HTML
  4. Деплой — готовые файлы загружаются на сервер
  5. Просмотр — посетители видят быстрый статический сайт
Ключевой момент: контент подтягивается на этапе билда, а не при каждом запросе посетителя. Поэтому после редактирования в Notion нужно запустить ребилд сайта.

Notion API: основы

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

  1. Перейдите на notion.so/my-integrations
  2. Создайте новую интеграцию (Internal Integration)
  3. Скопируйте Internal Integration Secret — это ваш API-токен
  4. Откройте нужную базу данных в Notion → меню ... → Connections → добавьте вашу интеграцию
Важно: интеграцию нужно подключить к каждой базе данных отдельно. Без этого API вернёт 404 даже с правильным токеном.

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

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

Пример: запрос статей из базы

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

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

Пагинация

API возвращает максимум 100 записей за запрос. Для больших баз используйте курсор:

let allPages = [];
let cursor = undefined;

do {
  const response = await notion.databases.query({
    database_id: DATABASE_ID,
    start_cursor: cursor,
    page_size: 100,
  });
  allPages.push(...response.results);
  cursor = response.has_more ? response.next_cursor : undefined;
} while (cursor);

Интеграция с Astro

Notion + Astro связываются через Content Collections и кастомный лоадер.

Кастомный лоадер

Лоадер — функция, которая забирает данные из Notion API и превращает их в формат, понятный Astro:

// 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(databaseId: string) {
  const pages = await getAllPages(databaseId);

  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_DATABASE_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 };

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

---
// src/pages/knowledge/[slug].astro
import { getCollection } from 'astro:content';
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;
---
<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={article.data.content} />
  </article>
</BaseLayout>

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

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

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

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

Notion API возвращает контент не как HTML или Markdown, а как массив блоков со своей структурой. Каждый блок — это объект с типом и данными:

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

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

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

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

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

Стандартная конвертация не покрывает все кейсы. Для 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 хранит изображения двумя способами:

ТипИсточникСрок жизни URL
Загруженные в Notionfile — S3-ссылка Notion1 час (signed URL)
Внешниеexternal — ваша ссылкаБессрочно
Критично: URL загруженных в Notion изображений истекают через 1 час. Если вы генерируете статический сайт, изображения нужно скачивать на этапе билда и сохранять локально. Иначе через час после билда все картинки на сайте сломаются.

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

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

async function downloadImage(url, slug, index) {
  const response = await fetch(url);
  const buffer = await response.arrayBuffer();
  const ext = url.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 как внешние ссылки.


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

ПараметрЗначение
Rate limit3 запроса/сек (средняя скорость)
Размер запросаМакс. 100 результатов на запрос
Блоки на страницуМакс. 100 блоков за запрос (нужна пагинация)
ВложенностьДочерние блоки нужно запрашивать отдельно
ВебхукиНет (нужен polling или ручной триггер ребилда)
Бесплатный планAPI доступен, ограничения по размеру файлов (5 МБ)
Plus ($10/мес)Неограниченные файлы, 30 дней истории
На практике: для сайта с 50–100 статьями полный билд занимает 30–60 секунд (зависит от количества изображений). Rate limit не является проблемой при таком объёме.

Триггер ребилда

Поскольку у Notion API нет вебхуков, ребилд нужно запускать вручную или автоматизировать:

Вариант 1: Ручной (простейший)

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

Вариант 2: 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/

Вариант 3: Webhook через n8n / Make

Настройте polling в n8n: раз в N минут проверять last_edited_time базы. Если есть изменения — триггерить GitHub Actions через API.


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

КритерийNotionContentfulSanityStrapi
ТипWorkspace + APIHeadless CMSHeadless CMSSelf-hosted CMS
Редактор⭐ Один из лучшихСреднийХорошийСредний
APIREST, базовыйREST + GraphQLGROQ + GraphQLREST + GraphQL
Вебхуки
CDN для медиа❌ (signed URL, 1 час)Нужна настройка
Бесплатный планДостаточен5 пользователей, 25K записей3 пользователя, 100K записейБесплатно (self-hosted)
Для когоУже используете NotionКоманды, продакшнРазработчики, кастомизацияПолный контроль
Вывод: Notion — отличный выбор, если вы уже ведёте в нём заметки и базы знаний. Контент уже структурирован, редактор знаком, дополнительных инструментов не нужно. Для крупных команд или проектов с частыми обновлениями лучше смотреть на Contentful или Sanity.

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

ПроблемаПричинаРешение
404 при запросе базыИнтеграция не подключена к базеNotion → база → ... → Connections → добавить интеграцию
Пустой контент страницыЗапрашиваете свойства, а не блокиИспользуйте /blocks/{id}/children для контента
Сломанные картинки через часSigned URL истёкСкачивайте изображения при билде
Медленный билдМного запросов к APIКэшируйте ответы, используйте инкрементальный билд
Вложенные блоки не загружаютсяAPI не возвращает children автоматическиРекурсивно запрашивайте has_children: true блоки

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

  1. Создайте интеграцию на notion.so/my-integrations
  2. Подключите интеграцию к нужной базе данных
  3. Установите зависимости:
npm install @notionhq/client notion-to-md
  1. Создайте .env:
NOTION_TOKEN=ntn_xxxxxxxxxxxx
NOTION_KB_DATABASE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  1. Напишите лоадер и подключите в content.config.ts
  2. Запустите билд: npm run build

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

© 2026 ИП Пименов Сергей Викторович ИНН 616271176890 ОГРН 316619600255641