Notion как headless CMS: без зависимостей и платных сервисов
Выбор CMS для блога — одно из самых времязатратных решений владельца небольшого сайта. Ставишь Strapi — нужно содержать сервер. Берёшь Contentful — ждёт платный тариф. Идёшь с markdown-файлами — каждый пост означает commit + deploy.
Я выбрал третий путь: Notion сам является CMS, а сайт читает его через API. Уже год madrimov.uz работает по этой схеме — ноль серверов, ноль дополнительных расходов, а публикация статьи сводится к отметке чекбокса "Published".
Почему именно Notion?
- Опыт письма уже готов — редактор, мобильное приложение, история версий.
- Колонки database идеальны для метаданных: slug, теги, язык, дата — всё это property.
- API бесплатный и стабильный; на объёмах личного блога в лимиты не упрёшься.
Важно: для этого решения не нужен ни один npm-пакет. SDK @notionhq/client удобен, но обычный fetch делает ту же работу.
Ядро: клиент на 60 строк
Чтение database Notion — один POST-запрос. Главное — не забыть про пагинацию:
const NOTION_API = "https://api.notion.com/v1";
async function queryAll(dbId: string) {
const pages = [];
let cursor: string | undefined;
do {
const res = await fetch(`${NOTION_API}/databases/${dbId}/query`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NOTION_TOKEN}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
},
body: JSON.stringify({
filter: { property: "Published", checkbox: { equals: true } },
...(cursor ? { start_cursor: cursor } : {}),
}),
next: { revalidate: 300 },
});
if (!res.ok) break;
const data = await res.json();
pages.push(...data.results);
cursor = data.has_more ? data.next_cursor : undefined;
} while (cursor);
return pages;
}Обрати внимание: next: { revalidate: 300 } — это механизм ISR в Next.js. Сайт обращается к Notion не на каждый запрос, а раз в 5 минут. Даже если Notion упадёт, сайт продолжит отдавать старый кеш.
Рендеринг блоков
Notion возвращает содержимое страницы как дерево "блоков": paragraph, heading, код, списки. Превратить их в React-элементы — простой switch:
function renderBlock(block: any) {
switch (block.type) {
case "paragraph":
return <p>{richText(block.paragraph.rich_text)}</p>;
case "heading_2":
return <h2>{richText(block.heading_2.rich_text)}</h2>;
case "code":
return (
<pre>
<code>{plain(block.code.rich_text)}</code>
</pre>
);
default:
return null;
}
}Поддерживать всё не обязательно — хватит 8-10 типов блоков, которые ты реально используешь. Если остальные вернут null, сайт не упадёт.
Три правила спустя год
- Строго синхронизируй имена property с кодом. Переименуешь колонку в Notion — сайт молча вернёт пустоту. Это самый частый "баг".
- Всегда `try/catch` + пустой массив как fallback. Если Notion API замедлился или отдал 500, страница блога с кодом 200 и пустым списком лучше, чем ошибка 500.
- Помни про срок signed URL для картинок. Файлы Notion приходят с подписанной ссылкой на ~1 час — если твой интервал ISR короче, проблема решается сама.
Итог
Notion + fetch + ISR — достаточная и не избыточная CMS для личного блога. Без сервера, без подписки, контент без деплоя. Следующим этапом поверх этой схемы строится автоматическая дистрибуция (Telegram, LinkedIn) — но это тема другой статьи.