Skip to content

feat: Notion blog with granular cache and stable slugs#61

Merged
adrienlupo merged 12 commits intomainfrom
feat/notion-blog
Mar 5, 2026
Merged

feat: Notion blog with granular cache and stable slugs#61
adrienlupo merged 12 commits intomainfrom
feat/notion-blog

Conversation

@adrienlupo
Copy link
Contributor

@adrienlupo adrienlupo commented Mar 3, 2026

Replace Lorem ipsum placeholder articles with real content fetched from the Notion database. Articles are cached using Next.js 16 "use cache" directive and revalidated on-demand via the existing webhook endpoint.

Changes

Notion integration

  • Add @notionhq/client SDK and lib/notion.ts with cached data functions
  • Add NotionRenderer server component for rendering Notion blocks
  • Update blog listing, article page, and homepage preview
  • Extract author from Notion "Author" people property
  • Wrap BlogPreview in Suspense for async server component
  • Update TypeScript and @types/react for async component support
  • Delete hardcoded app/blog/data.ts

Granular cache and stable slugs (latest)

  • Stable slugs: use Notion page ID as slug instead of slugify(title) -- URLs no longer break when titles change
  • Direct page fetch: fetchArticleById() uses notion.pages.retrieve() (1 API call) instead of querying the full database
  • Granular cache tags: blog-list for article listing, blog-article-{id} per article (instead of a single shared blog-articles tag)
  • cacheLife("max"): aggressive caching invalidated only by webhook
  • Targeted revalidation: webhook handler parses Notion event types (page.content_updated, page.properties_updated, page.created, page.deleted) to invalidate only the relevant cache tags

Replace Lorem ipsum placeholder articles with real content fetched from
the Notion database. Articles are cached using Next.js 16 "use cache"
directive and revalidated on-demand via the existing webhook endpoint.

- Add @notionhq/client SDK and lib/notion.ts with cached data functions
- Add NotionRenderer server component for rendering Notion blocks
- Update blog listing, article page, and homepage preview
- Extract author from Notion "Author" people property
- Show 2-line article preview (skipping metadata blocks)
- Wrap BlogPreview in Suspense for async server component
- Update TypeScript and @types/react for async component support
- Delete hardcoded app/blog/data.ts
The "" || props.alt expression is always props.alt since "" is falsy.
TypeScript 5.x strict mode catches this — simplified to just props.alt.
@vercel
Copy link

vercel bot commented Mar 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
website Ready Ready Preview Mar 5, 2026 10:31am

Request Review

- Catch errors in generateStaticParams so transient Notion failures
  don't crash the build (pages generate on-demand instead)
- Increase Notion SDK retry limit from 2 to 3
Add support for a "Slug" property from Notion database to prevent
URL breakage when article titles change. Falls back to slugify(title)
when the property is empty or missing.

Remove image block rendering from NotionRenderer since Notion-hosted
file URLs expire after ~1h and break cached pages.
- BlogCard: replace 6 individual props with article: ArticleMeta
- Deduplicate fetchArticleBySlug calls with React cache()
- Fix missing French accents in not-found page
- Extract getBlockRichText helper to replace ternary chains
- Remove redundant userNameCache Map (already covered by "use cache")
Paginate articles 10 per page using ?page=N query parameter
for shareability and SEO. Adds a Pagination server component
with prev/next links and page number navigation.
Move article fetching and pagination into a child async component
wrapped in <Suspense> so the static shell can be prerendered
without blocking on the dynamic searchParams promise.
Drop the preview field from ArticleMeta and the extra Notion API calls
that fetched first-paragraph excerpts. Combine date and author on a
single line in BlogCard for a cleaner, more compact listing.
- Use Notion page ID as slug instead of slugified title to prevent URL breakage on title changes
- Replace fetchArticleBySlug with fetchArticleById using direct page retrieve (1 API call instead of full DB query)
- Add granular cache tags: "blog-list" for article listing, "blog-article-{id}" per article
- Add cacheLife("max") for aggressive caching invalidated only by webhook
- Parse Notion webhook event types for targeted cache invalidation
@adrienlupo adrienlupo changed the title feat: replace hardcoded blog with Notion-fetched content feat: Notion blog with granular cache and stable slugs Mar 4, 2026
- Extract extractPageMeta() and resolveAuthor() shared helpers
- Remove slug from ArticleMeta (always equal to id)
- Parallelize author + blocks fetch in fetchArticleById
- Parallelize searchParams + fetchArticles in blog page
- Default webhook case is now a no-op (ignore unknown events)
- Remove unnecessary UUID normalization in revalidate route
revalidateTag with "max" does not trigger immediate purge in Next.js
16.1.5. Switching to { expire: 0 } forces instant server + CDN cache
invalidation when Notion webhooks fire.
- Sanitize link hrefs to prevent XSS (reject non-http schemes)
- Harden getBlockRichText with null safety instead of unsafe cast
- Add image and bookmark block rendering to NotionRenderer
- Remove verification_token bypass in revalidate webhook
- Use "max" cache profile for revalidateTag calls with lookup table
- Remove unused "blog-article" group cache tag
- Add runtime guard for NOTION_DATABASE_ID env var
- Limit fetchAllBlocks recursion depth to 5
- Fix French month accents (fev -> fev, aou -> aou)
- Add error fallback in fetchArticles (return [] on failure)
- Extract first paragraph as meta description instead of title
- Add ellipsis pagination truncation for large page counts
@adrienlupo adrienlupo merged commit 0535ae7 into main Mar 5, 2026
2 checks passed
@adrienlupo adrienlupo deleted the feat/notion-blog branch March 5, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant