A modern technical blog built with Next.js 15, Fumadocs MDX, Tailwind CSS, Clerk, Neon, Drizzle, and TanStack Query. It includes article pages, SEO metadata, an embedded AI assistant, and a synced engagement system with article upvotes, comments, and comment upvotes.
- Next.js 15 App Router blog with MDX-powered content
- Automatic read-time generation for every post
- Embedded article-aware AI assistant
- Clerk-authenticated engagement system
- Private admin analytics dashboard with first-party article tracking
- TanStack Query-powered client caching, syncing, and optimistic engagement updates
- Neon Postgres persistence through Drizzle ORM
- Responsive UI, dark mode, RSS, sitemap, and Open Graph images
- Next.js 15
- React 19
- TypeScript
- Fumadocs MDX
- Tailwind CSS 4
- TanStack Query
- Clerk
- Neon Postgres
- Drizzle ORM and Drizzle Kit
- OpenRouter or any OpenAI-compatible inference endpoint
TanStack Query is used for client-side server state, especially around engagement features such as article upvotes, comments, and comment upvotes.
- A shared
QueryClientis created incomponents/query-provider.tsx. - Engagement queries and mutations are organized in
lib/hooks/use-engagement.ts. - Comment tree update helpers live in
lib/engagement-client.ts. - Current defaults use a 30 second
staleTime, a 5 minutegcTime, background refetching on window focus and reconnect, and optimistic mutation updates for engagement interactions.
- Node.js 20.9 or newer
- pnpm 10 or newer
- Clone the repository and install dependencies.
git clone https://github.com/al-husayn/blog
cd blog
pnpm install- Create a
.env.localfile and add the values your environment needs.
NEXT_PUBLIC_SITE_URL=http://localhost:3000
DATABASE_URL=postgres://user:password@host/dbname?sslmode=require
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_publishable_key
CLERK_SECRET_KEY=sk_test_your_secret_key
ADMIN_USER_IDS=user_123,user_456
ADMIN_EMAILS=admin@example.com
AI_API_BASE_URL=https://openrouter.ai/api/v1
AI_API_KEY=your_cloud_api_key_here
AI_MODEL=openrouter/free- Sync the database schema.
pnpm db:push- Start the development server.
pnpm dev- Build for production when needed.
pnpm buildNEXT_PUBLIC_SITE_URL: Public site URL used for local and deployed metadata.
DATABASE_URL: Neon Postgres connection string.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: Clerk publishable key for client auth.CLERK_SECRET_KEY: Clerk secret key for server auth.ADMIN_USER_IDS: Comma-separated Clerk user IDs allowed to access/admin.ADMIN_EMAILS: Comma-separated email addresses allowed to access/admin(case-insensitive).
AI_API_BASE_URL: OpenAI-compatible base URL. The default setup uses OpenRouter.AI_API_KEY: Provider API key.AI_MODEL: Model identifier used by the blog assistant.
The engagement system is backed by Neon and Drizzle and is tied to Clerk authentication.
- Article likes and dislikes have been removed.
- Article upvotes are persisted per signed-in user.
- Comments are persisted and linked to the authenticated Clerk user.
- Comment upvotes are persisted per signed-in user.
- The schema is aligned with the live engagement tables:
article_comments,article_upvotes, andcomment_upvotes. - The analytics dashboard stores article page views and share events in
article_page_viewsandarticle_share_events.
Visit /admin after signing in with a Clerk user ID listed in ADMIN_USER_IDS or an email address listed in ADMIN_EMAILS.
The dashboard includes:
- 7d, 30d, 90d, and all-time pageviews
- Unique visitors and new vs returning reader ratios
- Top posts by 30d and all-time views
- Traffic source attribution for direct, organic, social, and referrals
- Average engaged time, bounce rate, and scroll depth completion
- Organic search trend lines and top referrer keywords when available
- Share tracking and first-48-hour comment velocity
Article analytics are collected with lightweight first-party tracking on blog post pages and the built-in share controls.
Useful commands:
pnpm db:generate
pnpm db:migrate
pnpm db:push
pnpm db:studioThe Drizzle config reads DATABASE_URL from .env.local or .env, so pnpm db:push works outside the Next.js runtime as well.
Each article page includes an assistant that is grounded in the active post content. By default, the project is configured for OpenRouter, but any OpenAI-compatible provider can be used by updating AI_API_BASE_URL and AI_MODEL.
If your chosen model or route changes, restart the dev server after updating environment variables.
Create a new .mdx file inside blog/content/.
Example frontmatter:
---
title: 'Your Blog Post Title'
description: 'A brief description of your post'
date: '2026-03-17'
tags: ['JavaScript', 'Next.js', 'Tutorial']
featured: true
author: 'al'
thumbnail: '/blog/example-cover.png'
---
Your blog post content here...Notes:
authorshould be an author key fromlib/authors.ts, not a display name.readTimeis generated automatically from the MDX content.- Read times are regenerated during
pnpm dev,pnpm build, and withpnpm run readtime.
Author records live in lib/authors.ts.
Current built-in keys:
alal-husseinhamdan
Add a new author there, then reference that key in your post frontmatter.
pnpm dev: Generate read times, compile MDX sources, and start the dev server.pnpm build: Generate read times, compile MDX sources, and build the app.pnpm start: Start the production server.pnpm lint: Run Next.js linting.pnpm readtime: Regeneratelib/generated/read-times.json.pnpm db:generate: Generate Drizzle migration files from the schema.pnpm db:migrate: Run Drizzle migrations.pnpm db:push: Push the current schema to the database.pnpm db:studio: Open Drizzle Studio.
- The site includes RSS, sitemap, metadata, and Open Graph image generation.
- Engagement routes live under
app/api/engagement. - Client-side engagement state is cached and synchronized through TanStack Query.
- If Clerk keys are missing, the site still renders, but synced engagement features remain unavailable until auth is configured.
This project is open source and available under the MIT License.