Deep dive into the data pipeline: database schema, property aliases, ISR revalidation, cover image proxying, and how filterPosts decides what goes live.
Each row in your Notion database represents one blog post. The title column may be named Name or title — the template resolves both. Required select fields are status (Public) and type (Post). Without them, posts are filtered out before render.
Optional but recommended fields include summary, category, tags, date, thumbnail, SEO Title, and SEO Description. Slugs should be lowercase with hyphens; if empty, the app slugifies the title at build time.
// Simplified flow
getPosts() → fetchNotionPages(databaseId, 'Post')
filterPosts() → keep Public + valid slug + past publish date
getPostBlocks(pageId) → recursive Notion blocks → ContentRendererNotion file URLs are signed and expire. The /api/cover/[slug] route fetches thumbnailSource from the post record, resizes with sharp when a width query param is present, and caches the response for an hour.
NotionBlockRenderer supports headings, paragraphs, lists, quotes, callouts, code fences, images, toggles, and tables. Complex layouts like column_list are flattened recursively during block fetch.
When you outgrow this schema, extend src/config/notionSchema.ts rather than hardcoding new property names inside getPosts.ts.