Notion можно использовать как базу данных для сайта — через официальный Notion API с полноценным CRUD. Типичный сценарий: контент-менеджер редактирует данные в привычном интерфейсе Notion, а сайт получает их через API (паттерн headless CMS). Ниже — как это реализовать, где граница разумного и как не потерять контент, который живёт только в чужом облаке.
Что такое Notion Database
«База данных» в Notion — таблица, строки которой являются страницами. Каждая строка — одновременно запись и полноценная страница с контентом. Свойства (Properties) — аналог колонок в SQL.
Типы свойств:
- Title — обязательный заголовок;
- Text, Number, Date, Checkbox — базовые типы;
- Select, Multi-select — списки значений;
- Relation — связь с другой базой (аналог внешнего ключа);
- Formula, Rollup — вычисляемые поля;
- Files & media — файлы;
- Person — участники воркспейса;
- URL, Email, Phone — специализированные текстовые поля.
Пример структуры блога в Notion:
| Название статьи | Slug | Статус | Теги | Дата публикации | Автор | |---|---|---|---|---|---| | Что такое MongoDB | mongodb-guide | Published | MongoDB, NoSQL | 2026-03-01 | Иван | | Уязвимости Redis | redis-security | Draft | Redis, Security | — | Мария |
Notion API: что умеет и как работает
Для работы с REST API нужно:
- Создать интеграцию на notion.so/my-integrations.
- Получить токен интеграции (Internal Integration Secret).
- Дать интеграции доступ к нужным страницам (меню страницы → Connections).
Примеры ниже используют версию API 2022-06-28. В более новых версиях Notion разделил базы данных и источники данных (data sources), и эндпоинты запросов изменились — при обновлении SDK сверяйтесь с документацией и всегда явно фиксируйте версию.
Базовые запросы
# Записи из базы с фильтром и сортировкой
curl -X POST "https://api.notion.com/v1/databases/$DATABASE_ID/query" \
-H "Authorization: Bearer $NOTION_TOKEN" \
-H "Notion-Version: 2022-06-28" \
-H "Content-Type: application/json" \
-d '{
"filter": {
"property": "Статус",
"select": { "equals": "Published" }
},
"sorts": [
{ "property": "Дата публикации", "direction": "descending" }
]
}'
# Содержимое страницы (блоки)
curl "https://api.notion.com/v1/blocks/$PAGE_ID/children" \
-H "Authorization: Bearer $NOTION_TOKEN" \
-H "Notion-Version: 2022-06-28"
Реализация: Notion как CMS для блога на Next.js
Установка
npm install @notionhq/client notion-to-md react-markdown
Клиент и запросы к API
// lib/notion.ts
import { Client } from '@notionhq/client'
import { NotionToMarkdown } from 'notion-to-md'
const notion = new Client({
auth: process.env.NOTION_TOKEN,
notionVersion: '2022-06-28',
})
const n2m = new NotionToMarkdown({ notionClient: notion })
const DATABASE_ID = process.env.NOTION_DATABASE_ID!
export async function getAllPosts() {
const response = await notion.databases.query({
database_id: DATABASE_ID,
filter: {
property: 'Статус',
select: { equals: 'Published' }
},
sorts: [
{ property: 'Дата публикации', direction: 'descending' }
]
})
return response.results.map((page: any) => ({
id: page.id,
title: page.properties['Название статьи'].title[0]?.plain_text || '',
slug: page.properties['Slug'].rich_text[0]?.plain_text || '',
date: page.properties['Дата публикации'].date?.start || null,
tags: page.properties['Теги'].multi_select.map((t: any) => t.name),
author: page.properties['Автор'].people[0]?.name || ''
}))
}
export async function getPostBySlug(slug: string) {
const response = await notion.databases.query({
database_id: DATABASE_ID,
filter: {
property: 'Slug',
rich_text: { equals: slug }
}
})
if (!response.results.length) return null
const page = response.results[0] as any
const mdBlocks = await n2m.pageToMarkdown(page.id)
const markdown = n2m.toMarkdownString(mdBlocks)
return {
id: page.id,
title: page.properties['Название статьи'].title[0]?.plain_text || '',
content: markdown.parent,
date: page.properties['Дата публикации'].date?.start || null,
}
}
Страница списка статей
// app/blog/page.tsx
import { getAllPosts } from '@/lib/notion'
export default async function BlogPage() {
const posts = await getAllPosts()
return (
<div>
<h1>Блог</h1>
{posts.map(post => (
<article key={post.id}>
<h2><a href={`/blog/${post.slug}`}>{post.title}</a></h2>
<p>{post.date}</p>
<div>{post.tags.map((tag: string) => <span key={tag}>{tag}</span>)}</div>
</article>
))}
</div>
)
}
Страница статьи с рендерингом Markdown
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import ReactMarkdown from 'react-markdown'
import { getPostBySlug, getAllPosts } from '@/lib/notion'
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({ slug: post.slug }))
}
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPostBySlug(slug)
if (!post) notFound()
return (
<article>
<h1>{post.title}</h1>
<time>{post.date}</time>
<ReactMarkdown>{post.content}</ReactMarkdown>
</article>
)
}
Реализация на Python (Flask/FastAPI)
pip install notion-client
import os
from notion_client import Client
notion = Client(auth=os.environ["NOTION_TOKEN"], notion_version="2022-06-28")
DATABASE_ID = os.environ["NOTION_DATABASE_ID"]
def get_published_posts():
response = notion.databases.query(
database_id=DATABASE_ID,
filter={"property": "Статус", "select": {"equals": "Published"}},
sorts=[{"property": "Дата публикации", "direction": "descending"}],
)
posts = []
for page in response["results"]:
props = page["properties"]
posts.append({
"id": page["id"],
"title": props["Название статьи"]["title"][0]["plain_text"]
if props["Название статьи"]["title"] else "",
"slug": props["Slug"]["rich_text"][0]["plain_text"]
if props["Slug"]["rich_text"] else "",
"date": props["Дата публикации"]["date"]["start"]
if props["Дата публикации"]["date"] else None,
})
return posts
def create_post(title: str, slug: str, content: str):
notion.pages.create(
parent={"database_id": DATABASE_ID},
properties={
"Название статьи": {"title": [{"text": {"content": title}}]},
"Slug": {"rich_text": [{"text": {"content": slug}}]},
"Статус": {"select": {"name": "Draft"}},
},
children=[{
"object": "block",
"type": "paragraph",
"paragraph": {
"rich_text": [{"type": "text", "text": {"content": content}}]
}
}]
)
Кэширование: обязательно для продакшена
Notion API ограничивает частоту запросов — в среднем около 3 запросов в секунду на интеграцию, при превышении возвращается 429. При трафике без кэша сайт упрётся в лимит.
Кэширование в Next.js
// Кэш на 1 час
export async function getAllPostsRaw() {
const response = await fetch(
`https://api.notion.com/v1/databases/${process.env.NOTION_DATABASE_ID}/query`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NOTION_TOKEN}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json'
},
next: { revalidate: 3600 }
}
)
return response.json()
}
Кэширование через Redis
import json
import redis
r = redis.Redis(host="localhost", port=6379, db=0)
def get_posts_cached():
cached = r.get("notion:posts")
if cached:
return json.loads(cached)
posts = get_published_posts()
r.setex("notion:posts", 3600, json.dumps(posts, ensure_ascii=False))
return posts
Инвалидация кэша по событию
Чтобы сайт обновлялся сразу после правки в Notion, подпишите свой эндпоинт на вебхуки интеграции Notion или используйте автоматизации базы с отправкой вебхука (на платных тарифах). Альтернатива — Zapier или Make: триггер «страница обновлена» → HTTP-запрос на ваш эндпоинт очистки кэша.
Ограничения Notion как базы данных
| Ограничение | Что означает | |---|---| | Rate limit ~3 req/s | Нужен кэш для трафика | | До 100 записей за запрос | Большие базы читаются постранично, медленно | | Нет транзакций | Нельзя атомарно обновить несколько записей | | Ограниченный поиск через API | Полнотекстовый поиск нужно делать у себя | | Файлы по временным ссылкам | Ссылки на загруженные файлы истекают примерно через час | | Зависимость от Notion | Если Notion недоступен и нет кэша — сайт не работает | | Данные за рубежом | Персональные данные россиян по 152-ФЗ хранить нельзя |
Бэкап и экспорт Notion
Весь контент — свойства записей, тексты страниц, файлы — хранится только в облаке Notion. История версий страниц и корзина помогают отменить ошибку, но глубина истории зависит от тарифа, а при потере доступа к аккаунту или удалении базы через API не поможет ничего, кроме внешней копии.
Ручной экспорт. Меню страницы или базы → Export → «Markdown & CSV» (с подстраницами): база выгружается в CSV, страницы — в .md. Владелец воркспейса может экспортировать всё через настройки воркспейса. Это удобно разово, но не по расписанию.
Выгрузка через API. Скрипт по cron забирает все записи (пагинацию берёт на себя хелпер SDK) и складывает их в JSON и SQLite:
# notion_dump.py
import datetime, json, os, sqlite3
from notion_client import Client
from notion_client.helpers import collect_paginated_api
notion = Client(auth=os.environ["NOTION_TOKEN"], notion_version="2022-06-28")
stamp = datetime.date.today().isoformat()
pages = collect_paginated_api(notion.databases.query, database_id=os.environ["NOTION_DATABASE_ID"])
with open(f"notion-{stamp}.json", "w", encoding="utf-8") as f:
json.dump(pages, f, ensure_ascii=False, indent=2)
db = sqlite3.connect(f"notion-{stamp}.db")
db.execute("CREATE TABLE pages (id TEXT PRIMARY KEY, edited TEXT, properties TEXT)")
db.executemany("INSERT INTO pages VALUES (?, ?, ?)",
[(p["id"], p["last_edited_time"], json.dumps(p["properties"], ensure_ascii=False)) for p in pages])
db.commit()
db.close()
Если важен текст страниц, добавьте выгрузку блоков в Markdown через notion-to-md, а файлы скачивайте сразу — ссылки истекают. Готовую копию отправляйте за пределы Notion — в S3 или в dbsend, где у каждой версии видно число записей и diff с предыдущей:
python notion_dump.py
dbsend backup "notion-$(date +%F).db" -d "$DBSEND_DATABASE_ID" -e sqlite -l "notion-$(date +%F)"
Команды CLI — в документации, про хранение JSON в SQLite — в статье JSON как база данных. Раз в квартал проверяйте копию: импортируйте CSV в пустую тестовую страницу или восстановите несколько записей через pages.create — см. как проверить, что бэкап рабочий.
Когда переезжать на SQLite или PostgreSQL: контент стал данными приложения (заказы, пользователи), нужны транзакции и сложные запросы, упираетесь в rate limit или 152-ФЗ. Тот же дамп — отправная точка миграции; похожий подход для Airtable описан в статье Airtable как база данных.
Когда Notion как БД — хороший выбор
- блог или контент-сайт с умеренным числом материалов;
- портфолио с редко меняющимся контентом;
- внутренние инструменты команды (трекеры, дашборды);
- MVP, где главное — скорость запуска;
- продукт, где нетехнические люди редактируют контент без отдельной CMS;
- лендинги с динамическими блоками (FAQ, отзывы, команда).
Когда Notion — плохой выбор
- высоконагруженные сайты без серьёзного кэширования;
- транзакционные системы (заказы, платежи);
- приложения с пользовательскими и персональными данными;
- сложные поисковые сценарии;
- данные, которые должны жить долго без зависимости от SaaS.
Инструменты для работы с Notion как БД
@notionhq/client— официальный SDK для JavaScript/TypeScript;notion-client— Python SDK;notion-to-md— конвертация страниц в Markdown;react-notion-x— рендеринг публичных страниц Notion в React;- Super.so, Feather, Notaku — сайты и блоги поверх Notion без кода.
Notion для внутренних инструментов
Notion хорошо подходит для внутренних задач команды: трекер с дополнительными полями, простая CRM, редакционный план, база знаний, дашборд проекта с rollup-вычислениями.
Через API их можно автоматизировать:
# Создать задачу при новом заказе
def create_task_from_order(order):
notion.pages.create(
parent={"database_id": TASKS_DB_ID},
properties={
"Название": {"title": [{"text": {"content": f"Обработать заказ #{order['id']}"}}]},
"Приоритет": {"select": {"name": "Высокий"}},
"Дедлайн": {"date": {"start": order["deadline"]}},
"Статус": {"select": {"name": "Новая"}},
}
)
Частые вопросы
Можно ли использовать Notion как основной backend для продакшн-приложения?
Для контентных сайтов — да, с кэшированием. Для приложений с большим числом пользователей, транзакциями или персональными данными — нет: используйте PostgreSQL или другую полноценную БД.
Как часто Notion меняет API?
Нечасто, но изменения бывают, в том числе ломающие. Всегда указывайте версию в заголовке Notion-Version и следите за changelog на developers.notion.com.
Бесплатен ли Notion API?
API доступен на всех планах, включая бесплатный. Ограничения бесплатного плана касаются в основном участников, загрузки файлов и глубины истории версий.
Что лучше для CMS: Notion или Contentful?
Contentful и Sanity — специализированные headless CMS с предсказуемым API, CDN и вебхуками. Notion удобнее команде, которая уже в нём работает. Для серьёзного контент-проекта — специализированная CMS, для MVP или внутреннего инструмента — Notion.
Как защитить токен Notion?
Не хардкодьте токен, храните его в переменных окружения. Не вызывайте Notion API из браузера — токен увидит любой посетитель. Давайте интеграции доступ только к тем страницам, которые ей нужны.
Как сделать бэкап базы Notion автоматически?
Встроенного экспорта по расписанию нет. Запускайте по cron скрипт, который выгружает записи через API в JSON или SQLite, скачивает файлы и отправляет копию во внешнее хранилище. Пример — в разделе «Бэкап и экспорт Notion» выше.



