A Next.js 14 blog that reads content from Notion and local Markdown files.
The Start application workflow runs pnpm dev → next dev -p 5000 -H 0.0.0.0.
First compile takes ~10–25 seconds. Subsequent page loads are <1 second.
| Secret | Purpose |
|---|---|
NOTION_PAGE_ID |
Root Notion page with blog posts |
NOTION_SPACES_ID |
Notion workspace ID |
GITHUB_ID |
GitHub OAuth App client ID (for comments) |
GITHUB_SECRET |
GitHub OAuth App client secret (for comments) |
NEXTAUTH_SECRET |
Random secret for NextAuth JWT signing |
Optional: NOTION_ACCESS_TOKEN (for private pages).
- Framework: Next.js 14.2 (Pages Router + App Router for API routes), React 19
- CMS: Notion via
notion-client/react-notion-x+ local Markdown files - Styling: Tailwind CSS v3 + custom CSS
- Comment system: Fuma Comment with GitHub OAuth (NextAuth v4) and Replit PostgreSQL
- Package manager: pnpm
| File | Purpose |
|---|---|
blog.config.js |
Blog settings (title, author, Notion IDs, features) |
pages/index.js |
Home page — reads posts via getStaticProps |
pages/_app.js |
App shell — Header, Footer, ThemeProvider, NProgress |
pages/api/auth/[...nextauth].js |
NextAuth GitHub OAuth handler |
lib/notion/ |
Server-side Notion API helpers |
lib/auth-options.js |
NextAuth configuration (GitHub provider + JWT + user upsert) |
lib/db.js |
Drizzle ORM postgres-js database client |
lib/comment-schema.js |
Drizzle schemas for comments, rates, roles, fuma_users |
lib/comment-config.js |
Fuma Comment storage + auth adapters |
app/api/comments/[[...comment]]/route.js |
Fuma Comment API route handler (App Router) |
components/Post/FumaComments.js |
Fuma Comment React widget (dynamically loaded) |
components/Post/Comments.js |
Comment provider router (utterances / supacomments / fuma) |
styles/fuma-comment.css |
Extracted Fuma Comment styles (Tailwind layers stripped) |
lib/markdown/getAllMarkdownPosts.js |
Reads content/*.md, returns same post shape as Notion |
lib/markdown/getMarkdownContent.js |
Converts Markdown body to HTML (remark + remark-gfm) |
content/ |
Local Markdown post files (slug.md) |
The blog uses Fuma Comment for self-hosted comments with GitHub OAuth login.
- Go to https://github.com/settings/developers → "New OAuth App"
- Homepage URL: your blog URL
- Authorization callback URL:
https://YOUR-REPLIT-DOMAIN/api/auth/callback/github - Copy the Client ID →
GITHUB_IDsecret - Generate a Client Secret →
GITHUB_SECRETsecret - Generate a random secret (e.g.,
openssl rand -base64 32) →NEXTAUTH_SECRET
Tables are auto-created in Replit PostgreSQL (DATABASE_URL):
comments— post commentsrates— likes/dislikesroles— admin rolesfuma_users— GitHub user profiles (populated on first sign-in)
In blog.config.js, change comment.provider to:
'fuma'— Fuma Comment (default)'utterances'— GitHub Issues based'supacomments'— Supabase based''— disabled
Add .md files to the content/ directory. Each file needs frontmatter:
---
title: My Post Title
slug: my-post
date: 2026-01-01
tags: [tag1, tag2]
summary: Optional summary
status: Published
type: Post # Post | Page | Newsletter | Hidden
---
Your content here…Markdown posts appear alongside Notion posts in the home feed, search, and tag pages.
pages/s/[subpage].js was returning blank/404 content due to two bugs:
-
Breadcrumb index:
notion-utilsgetPageBreadcrumbsreturns breadcrumbs in root→leaf order. The old code usedbreadcrumbs[0](root) as the active page title. Fixed to usebreadcrumbs.at(-1). -
pageAllowedspace-ID mismatch: The Notion API now returnsblock.spaceId(camelCase) instead of the oldblock.value.space_id. TheNOTION_SPACES_IDsecret was configured with an old value that no longer matches. Fixed by fetching the blog root page on the first subpage request, extracting its realspaceId, caching it module-level, and comparing the subpage'sblock.spaceIdagainst that cached value. This is robust regardless of what valueNOTION_SPACES_IDis set to.
globals.cssCSS@importordering: Moved@importstatements before@tailwinddirectives to fix PostCSS error.NotionRenderer.jsnull guard: Added null check forblockMapbefore accessing.blockto prevent crash when Notion data is not yet loaded.@tiptap/extensionspackage: Installed explicitly since tiptap v3 re-exports from it but doesn't auto-install it as a dependency of@fuma-comment/react.- PostgreSQL
NOTICElog suppression: Setonnotice: () => {}on the postgres client inlib/db.jsto silence noisy DB init notices. _document.jsandSEO.jskey props: Addedkeyprops to all children of<Head>to eliminate most sources of the key warning.
Several performance fixes were required to compile in a reasonable time:
-
Tailwind CSS v4 → v3: The project was using
@tailwindcss/postcss(v4) which caused webpack to hang during CSS PostCSS processing. Downgraded totailwindcss@3+autoprefixer. Updatedpostcss.config.jsand replaced@import "tailwindcss"with@tailwinddirectives inglobals.css. -
Heroicons barrel imports → direct imports: All
@heroicons/react/24/outlinebarrel imports were converted to per-icon direct imports (e.g.,import HomeIcon from '@heroicons/react/24/outline/HomeIcon') to avoid webpack processing 323 re-exports. -
react-notion-x → dynamic import:
NotionRenderer.jswraps react-notion-x usingnext/dynamicto prevent the entire ESM package tree from being compiled client-side. -
framer-motion removed: Replaced
motion.divwith regulardivin Header, Footer, BlogPost, and TransitionEffect components. -
Webpack server externals: Heavy ESM packages (
notion-client,got,p-map, etc.) are externalized from the server bundle so webpack doesn't need to bundle them. -
Native fetch:
lib/notion/previewImages.jsuses Node.js nativefetchinstead ofgot(ESM-only) to avoid bundling issues. -
Rewrite refactor: The
next.config.jsrewrites were restructured to useafterFiles/fallbackto prevent the craft.do proxy from intercepting Next.js API routes (/api/auth/*,/api/comments/*). -
Fuma Comment CSS: The package CSS uses Tailwind
@layerdirectives that need special handling. A pre-processed copy (styles/fuma-comment.css) with layers stripped is used instead.