A full-stack Blog CMS built with Next.js (App Router), Supabase, TailwindCSS, and shadcn/ui. Features Supabase Auth with role-based access control, a WYSIWYG editor, draft/publish workflow, newsletter subscriptions, an AI writing assistant, a headless REST API, and MCP-powered development workflows.
- Authentication via Supabase Auth
- Role-Based Access Control — Admin and Author roles enforced through Supabase RLS
- WYSIWYG editor powered by TipTap with rich text, images, and formatting
- Draft and publish workflow
- Tags and categories
- Comments — authenticated, thread-style, with admin management
- SEO-friendly public blog pages with meta title and description support
- Developer API — generate API keys in the dashboard to create posts from external tools (n8n, Postman, scripts)
- AI Writing Assistant — chat with uploaded books (PDF) using Claude, Gemini, or OpenAI; generate full blog post drafts from conversation context
- LLM provider key management — store encrypted API keys (AES-256-GCM) for Claude, Gemini, and OpenAI per user
- Headless AI post generation via
POST /api/ai-assistant/generate - Newsletter subscriptions — readers subscribe from a widget on every post; email sent automatically on publish via Resend after a configurable delay; one-click unsubscribe via token
- REST API for posts — list, create, read, update, delete via authenticated endpoints
- In-memory rate limiting on API routes
- Favicon support
- Fast Vercel deployment
- Frontend: Next.js (App Router)
- Backend: Supabase (Postgres + Auth + Storage)
- Styling: TailwindCSS + shadcn/ui
- Editor: TipTap
- AI Providers: Anthropic (Claude), Google (Gemini), OpenAI
- Deployment: Vercel
- AI Dev Layer: Claude Code + MCP Servers
This project is optimized for AI-assisted development using MCP servers:
github-mcp— repo management, PRs, commitssupabase-mcp— database schema, queries, RLSvercel-mcp— deployments and env managementfilesystem-mcp— file editing and refactoringbrowser-mcp— UI testing and debuggingpostgres-mcp(optional) — query optimization
app/
(public)/ → public blog pages
(dashboard)/ → admin & author dashboard
(ai)/ → AI assistant (full-screen layout)
api/ → backend routes
components/
ui/ → reusable UI (shadcn/ui)
editor/ → TipTap WYSIWYG editor
blog/ → blog components
features/
posts/
users/
auth/
comments/
lib/
supabase/
permissions/
utils/
database/
schema.sql
migrations/
policies/
agents/
frontend.agent.md
backend.agent.md
database.agent.md
git clone https://github.com/frank-mendez/nextjs-blog-cms.git
cd nextjs-blog-cmsnpm installCreate a .env.local file:
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
LLM_KEY_ENCRYPTION_SECRET= # 32-character secret for AES-256-GCM key encryption
# Newsletter
RESEND_API_KEY=
RESEND_FROM_EMAIL= # verified sender address, e.g. noreply@yourdomain.com
NEWSLETTER_DELAY_MINUTES=60 # delay between publish and send (default: 60)
WEBHOOK_SECRET= # shared secret used to authenticate the /api/newsletter/send cron call- Run
database/schema.sqlin the Supabase SQL editor - Apply RLS policies from
database/policies/ - Optionally seed with
database/seed.sql
npm run dev| Role | Access |
|---|---|
| Admin | Full control (users, posts, roles, comments, developer settings) |
| Author | Create and manage own posts, delete own comments |
Enforced using Supabase Row Level Security (RLS).
The AI assistant allows authors to upload a PDF book, chat with it using their preferred LLM, and generate a full blog post draft from the conversation.
Supported providers: Claude (Anthropic), Gemini (Google), OpenAI
How it works:
- Navigate to Dashboard → AI Assistant
- Add your LLM API key under Dashboard → Developer → LLM Providers
- Start a new chat — upload a PDF and select a model
- Chat with the book, then click Generate Post to create a draft
PDF text is extracted on upload and stored as plain text. The LLM receives the extracted text as context. API keys are encrypted with AES-256-GCM and never stored in plaintext.
Admins can generate API keys to allow external tools to create posts without a browser session.
- Log in as Admin
- Go to Dashboard → Developer
- Click Generate New Key, name it, and copy the key — shown only once
Keys are prefixed with fmblog_ followed by 64 hex characters. Only a SHA-256 hash is stored in the database.
Create a new post from any HTTP client.
Headers:
Authorization: Bearer fmblog_your_key_here
Content-Type: application/json
Body:
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Post title |
content |
string | Yes | HTML content (TipTap-compatible) |
slug |
string | No | URL slug — auto-generated from title if omitted |
status |
draft | published |
No | Defaults to draft |
excerpt |
string | No | Plain-text summary |
meta_title |
string | No | SEO title — defaults to title |
meta_description |
string | No | SEO description — defaults to excerpt |
tags |
string[] | No | Tag names — created automatically if they don't exist |
category |
string | No | Category name — matched by name or slug |
image_url |
string | No | Featured image URL |
Example:
curl -X POST https://your-domain.com/api/posts/create \
-H "Authorization: Bearer fmblog_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"title": "Hello from n8n",
"content": "<p>This post was created via the API.</p>",
"status": "draft",
"tags": ["automation", "n8n"],
"category": "Technology"
}'Response (201):
{
"success": true,
"data": {
"id": "uuid",
"title": "Hello from n8n",
"slug": "hello-from-n8n",
"status": "draft"
}
}Generate a blog post headlessly using the AI assistant.
Headers:
Authorization: Bearer fmblog_your_key_here
Content-Type: application/json
List posts with pagination and filters.
Retrieve a single post by ID.
Update a post by ID.
Delete a post by ID.
- Raw API keys are never stored — only SHA-256 hashes
- The key is shown exactly once after generation
- Keys can be revoked or deleted at any time from Developer Settings
author_idis always set to the user who owns the API key- API routes are rate-limited in-memory
Readers subscribe via a widget at the bottom of every blog post. When a post is published, a send is queued in the newsletter_sends table and dispatched after a configurable delay.
- Reader submits their email on any blog post — stored in
newsletter_subscriptions - When a post is published, a row is inserted into
newsletter_sendswithscheduled_at = now() + NEWSLETTER_DELAY_MINUTES - A Vercel Cron Job (or any HTTP scheduler) calls
POST /api/newsletter/sendevery minute - The endpoint claims pending sends past their
scheduled_at, emails all active subscribers via Resend, and marks the send assent
Every email contains a unique unsubscribe link: GET /api/newsletter/unsubscribe?token=<token>. Clicking it sets unsubscribed_at and redirects to /newsletter/unsubscribed.
Go to Dashboard → Admin → Newsletter (admin only) to see:
- Active subscribers, sends dispatched, and unsubscribed counts
- Pending and in-progress scheduled sends
- Recent subscriber list with status badges
- CSV export of all subscribers
A vercel.json is included at the repo root that configures the cron to fire every minute. The endpoint requires a x-webhook-secret header matching WEBHOOK_SECRET — add this to your Vercel project environment variables. Vercel Cron sends the header automatically when the secret is configured in the project settings.
Covers lib utilities, API routes, services, and UI components with 80%+ thresholds across lines, branches, functions, and statements.
npm test # watch mode
npm run test:run # single run
npm run test:coverage # coverage reportTests the five posts REST API routes (GET, POST, PATCH, DELETE) against a real Next.js dev server and a dedicated Supabase test project. No browser — pure HTTP via APIRequestContext.
Prerequisites:
.env.localmust point to a separate Supabase test project (not production)- The test project must have the full schema applied (
database/schema.sql)
npm run test:e2e # run the full suite (19 tests)
npm run test:e2e:report # open the HTML reportGlobal setup seeds a test user, API key, and three posts before the suite runs. Global teardown deletes all seeded data by user_id after the suite finishes.
- Import repo to Vercel
- Add environment variables
- Assign a domain (e.g.
blog.yourdomain.com)
This project is designed to work seamlessly with Claude Code:
- Modular, feature-based architecture for safe refactoring
- Dedicated
agents/instruction files - MCP servers for full-stack automation
Public Blog — SEO-friendly article listing
AI Assistant — Chat with books and generate blog posts
Developer Settings — API Key Management and LLM Providers
- Comments system
- Developer API with API key management
- AI Writing Assistant (Claude, Gemini, OpenAI)
- PDF text extraction and LLM context
- REST API for posts
- Newsletter subscriptions with auto-send on publish
- Analytics dashboard
- Scheduled posts
- Multi-author collaboration
- Headless CMS API
Contributions are welcome. To contribute:
- Fork the repository
- Create a feature branch (
git checkout -b feature/your-feature) - Commit your changes with clear messages
- Open a Pull Request — describe what changed and why
For significant changes, open an issue first to discuss the approach.
This project follows the Contributor Covenant Code of Conduct. By participating, you agree to uphold a respectful and inclusive environment. Report unacceptable behavior to the project maintainer.
Frank Mendez


