Notion как база данных для сайта — как работает, примеры кода, ограничения и альтернативы

Notion можно использовать как базу данных для сайта — это не хак и не обходной путь, это официальный Notion API с полноценным CRUD. Правильный сценарий: контент-менеджер редактирует данные в удобном Notion-интерфейсе, сайт получает их через API. Это называется Headless CMS паттерн. Разберём, как это реализовать, и где граница разумного.


Что такое Notion Database

В Notion «база данных» (Database) — это таблица со страницами в качестве строк. Каждая строка — это одновременно запись в таблице и полноценная страница с контентом. У каждой строки есть свойства (Properties) — аналог колонок в SQL.

Типы свойств в Notion:

  • Title — обязательное текстовое поле-заголовок
  • Text, Number, Date, Checkbox — базовые типы
  • Select, Multi-select — выпадающие списки
  • Relation — связь с другой базой данных (аналог Foreign Key)
  • Formula — вычисляемые поля
  • 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: что умеет и как работает

Notion предоставляет REST API. Для работы с ним нужно:

  1. Создать интеграцию на notion.so/my-integrations
  2. Получить Internal Integration Token (Bearer token)
  3. Предоставить интеграции доступ к нужным страницам (Share → Add connections)

Базовые запросы через API

# Получить все записи из базы данных
curl -X POST https://api.notion.com/v1/databases/{database_id}/query \
  -H "Authorization: Bearer secret_ваш_токен" \
  -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 secret_ваш_токен" \
  -H "Notion-Version: 2022-06-28"

Реализация: Notion как CMS для Next.js блога

Установка

npm install @notionhq/client notion-to-md

Клиент и запросы к API

// lib/notion.ts
import { Client } from '@notionhq/client'
import { NotionToMarkdown } from 'notion-to-md'

const notion = new Client({
  auth: process.env.NOTION_TOKEN
})

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 || ''
  }))
}

// Получить статью по slug
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 => <span key={tag}>{tag}</span>)}</div>
        </article>
      ))}
    </div>
  )
}

Страница статьи с рендерингом Markdown

// app/blog/[slug]/page.tsx
import { getPostBySlug, getAllPosts } from '@/lib/notion'
import ReactMarkdown from 'react-markdown'

export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map(post => ({ slug: post.slug }))
}

export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug)
  if (!post) return <div>Статья не найдена</div>

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.date}</time>
      <ReactMarkdown>{post.content}</ReactMarkdown>
    </article>
  )
}

Реализация на Python (Flask/FastAPI)

pip install notion-client
# notion_client_example.py
from notion_client import Client
import os

notion = Client(auth=os.environ["NOTION_TOKEN"])
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 имеет rate limit: примерно 3 запроса в секунду. При высоком трафике вы упрётесь в лимиты и данные будут загружаться медленно. Решение — кэширование.

Кэширование в Next.js (ISR — Incremental Static Regeneration)

// Next.js 14+ — кэш на 1 час
export async function getAllPosts() {
  const response = await fetch(
    `https://api.notion.com/v1/databases/${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 redis
import json
from notion_client import Client

r = redis.Redis(host='localhost', port=6379, db=0)

def get_posts_cached():
    # Проверить кэш
    cached = r.get('notion:posts')
    if cached:
        return json.loads(cached)

    # Запросить из Notion
    posts = get_published_posts()

    # Сохранить в кэш на 1 час
    r.setex('notion:posts', 3600, json.dumps(posts, ensure_ascii=False))
    return posts

Webhook-инвалидация кэша

Для мгновенного обновления при изменении данных в Notion используйте Zapier или Make.com:

  • Триггер: обновление страницы в Notion
  • Действие: HTTP-запрос на ваш endpoint для очистки кэша

Ограничения Notion как базы данных

| Ограничение | Значение | Что означает | |---|---|---| | Rate limit | ~3 req/s | Нужен кэш для высокого трафика | | Размер базы данных | ~50 000 строк комфортно | Для большого каталога медленнее | | Вложенность блоков | Ограничена | Сложные страницы парсятся дольше | | Сортировка | По одному полю | Нет сложных ORDER BY | | Полнотекстовый поиск | Через API ограниченно | Нужна отдельная реализация | | Транзакции | Нет | Нельзя атомарно обновить несколько записей | | Бинарные данные | Файлы через CDN Notion | Ссылки временные (1 час) | | Автономность | Зависимость от Notion.so | Если Notion лежит — сайт не работает |


Когда Notion как БД — хороший выбор

  • Блог или контент-сайт с несколькими сотнями статей
  • Портфолио, где контент редко меняется
  • Внутренние инструменты команды (дашборды, трекеры)
  • MVP-проект, где главное — скорость запуска
  • Продукт, где нетехнические люди должны редактировать контент без CMS
  • Лендинги с динамическим контентом (FAQ, отзывы, команда)

Когда Notion — плохой выбор

  • Высоконагруженные сайты (без серьёзного кэширования)
  • Транзакционные системы (заказы, платежи)
  • Приложения с пользовательскими данными (не храните персональные данные в Notion)
  • Сложные поисковые сценарии
  • Данные, которые нужно долгосрочно хранить без зависимости от SaaS

Инструменты для работы с Notion как БД

  • @notionhq/client — официальный JavaScript/TypeScript SDK
  • notion-client — Python SDK
  • notion-to-md — конвертация Notion-страниц в Markdown
  • react-notion-x — рендеринг Notion-страниц в React без API (читает публичные страницы)
  • Notaku — платформа для создания сайтов на основе Notion
  • Super.so — кастомизируемые сайты из Notion
  • Feather — блог-платформа поверх Notion

Notion для внутренней разработки (не публичный сайт)

Notion отлично подходит для внутренних инструментов команды:

  • Трекер задач с дополнительными полями (бюджет, ответственный, дедлайн)
  • CRM-подобная система для небольших команд
  • Редакционный план для контент-маркетинга
  • База знаний с поиском
  • Дашборд проекта с rollup-вычислениями

Через Notion 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": "Новая"}},
        }
    )

FAQ

Можно ли использовать Notion как основной backend для продакшн-приложения? Для content-driven сайтов — да, с кэшированием. Для приложений с большим числом пользователей, транзакциями или чувствительными данными — нет. Используйте PostgreSQL или другую реальную БД.

Как часто Notion меняет API? Notion обновляет API относительно редко, но изменения бывают. Всегда указывайте версию в заголовке Notion-Version: 2022-06-28. Следите за changelog на developers.notion.com.

Бесплатен ли Notion API? API доступен на всех планах Notion, включая бесплатный. Но на бесплатном плане есть ограничения по числу участников и размеру загружаемых файлов.

Что лучше: Notion или Contentful для CMS? Contentful специализируется на Headless CMS и имеет более предсказуемый API, CDN, webhook-и из коробки. Notion удобнее для команды, которая уже работает в Notion. Для серьёзного контент-проекта — Contentful или Sanity. Для быстрого MVP или внутреннего инструмента — Notion.

Как защитить Notion Token? Никогда не хардкодьте токен в коде. Используйте .env файл и переменные окружения. Никогда не выполняйте Notion API запросы на клиентской стороне (браузер) — токен будет виден всем. Все запросы — только через серверный код.