next-blogpanel
v1.4.0
Published
A drop-in blog engine for Next.js — admin panel, block editor, SEO, and media, all backed by your own MongoDB.
Maintainers
Readme
Next BlogPanel
A drop-in blog engine for Next.js — admin panel, block editor, SEO, and media, all backed by your own MongoDB.
next-blogpanel scaffolds a working blog into an existing Next.js App Router project: public pages, an admin CMS, and API routes. You own the database, the files, and the URLs. There is no hosted service and no third-party account to create.
Features
- Block editor — TipTap with slash commands, image upload, Shiki code blocks, callouts, FAQ and How-To blocks, tables, task lists
- Admin panel — dashboard, post list, block editor, media library, category manager, redirects, settings, scoped API tokens, audit log
- Roles — every API token is
admin,editororauthor; authors can create and edit only their own posts and cannot delete anything. The master key (BLOGPANEL_API_KEY) is alwaysadminand bypasses every check - Audit trail — every create, update and delete of a post, category, media item, setting, token or redirect is recorded with who did it and when, viewable from the admin panel, with configurable retention
- SEO — meta tags, Open Graph, Twitter Cards, one linked JSON-LD entity graph (
BlogPosting,BreadcrumbList,FAQPage,HowTo,Organization,Person,WebSite), sitemap, RSS, and a 24-check scorer — 17 SEO checks plus 7 AEO checks scoring how likely an AI answer engine is to cite the post - AEO surface —
llms.txt/llms-full.txtlist every published post the moment it goes live, IndexNow pings Bing/Yandex/Naver/Seznam on publish/slug-change/removal, and every post gets an Open Graph card automatically generated if you never uploaded one - AI crawlers are welcome by default —
robots.txtallows GPTBot, ClaudeBot, PerplexityBot and the rest out of the box; block a specific one from Settings → Crawlers & AI if you don't want it - Redirects — a slug change on a published post creates a 301 automatically so inbound links never break, plus a manual redirect manager for everything else, with no-chain/no-loop/no-shadowing rules enforced server-side
- Images stored in your MongoDB by default; Cloudflare R2 optional; anything else pluggable — GridFS needs no setup at all, R2 takes over when you supply its five environment variables, and
StorageProvideris public API for the rest - MongoDB backend — CRUD, slug generation, revision history, full-text search
- Components — two page-level components with slot injection, plus eleven atomic ones for building your own layout
- Theming — colours, fonts, radii and shadows are
--nbp-*CSS variables, dark mode included - CLI —
init,migrate,seed,health
Prerequisites
- Node.js 18+
- Next.js 14+ using the App Router
- MongoDB — Atlas free tier is enough, and it stores your images too
No object storage account is required.
Quick Start
1. Install
pnpm add next-blogpanel # or: npm install next-blogpanel2. Scaffold
npx next-blogpanel initInto your existing app/ (or src/app/) directory this writes:
app/blog/— layout, list page,[slug]post page,category/[slug]pageapp/admin/blog/— layout, dashboard, posts, new,[id]/edit, media, categories, settings, auditapp/api/blog/— posts, media,media/file/[...key], categories, settings, tokens, audit,sitemap.xml,rss.xmlapp/next-blogpanel.setup.ts— hands your config file to the server at runtime; every scaffolded file imports it
…and at the project root, next-blogpanel.config.ts and .env.local.example. The API routes and admin pages are one-line re-exports; the blog pages are real source built from the atomic components, so they are the ones to edit when you want a different layout. Files that already exist are skipped, never overwritten.
3. Configure the environment
cp .env.local.example .env.localBLOGPANEL_MONGODB_URI=mongodb+srv://user:[email protected]/mydb
BLOGPANEL_API_KEY=a-secure-key-of-at-least-32-charactersThose two are the only variables you need. Generate the key with openssl rand -hex 32.
The generated
.env.local.examplealso contains fiveBLOGPANEL_R2_*placeholder lines. Delete or comment them out unless you have a real Cloudflare R2 bucket. All five being set — even to placeholder text — is what switches image storage from MongoDB to R2, and uploads will then fail against credentials that do not exist. See docs/storage.md.
Other optional variables: BLOGPANEL_MONGODB_DB (overrides the database named in the URI), BLOGPANEL_SITE_URL and BLOGPANEL_SITE_NAME (used in meta tags, sitemap and RSS; default to "" and "Blog"), BLOGPANEL_STORAGE, and BLOGPANEL_AUDIT_RETENTION_DAYS (days the audit trail is kept, default 90, 0 for forever — see docs/configuration.md).
BLOGPANEL_SITE_URL is optional to start but required before you deploy. Images stored in GridFS get root-relative URLs, and og:image, twitter:image, JSON-LD image and RSS <enclosure> all have to be absolute — so without it those four are omitted (with a warning) rather than emitted as URLs no crawler can resolve.
4. Create the indexes
npx next-blogpanel migrateCreates the MongoDB indexes, including the audit trail's retention (TTL) index, sized from BLOGPANEL_AUDIT_RETENTION_DAYS (default 90 days, 0 to keep entries forever) — set it before you run this if 90 days is not what you want, and re-run migrate any time you change it. Set it everywhere migrate runs, not only where the app runs: a run without it never rewrites an index that already exists (it warns and leaves it alone), so the setting is sticky — returning to the default means setting 90, not unsetting the variable. See docs/configuration.md. If it finds media documents from a pre-1.0 install it upgrades their schema at the same time, and if it finds API tokens with no role (anything created before 1.2.0) it backfills them to admin — the same access they had before roles existed. It also generates the IndexNow key the first time it finds none, and never overwrites one already there — see docs/configuration.md — IndexNow key. Safe to re-run. See docs/api.md — Upgrading to 1.2.0 and Upgrading to 1.3.0 if you're upgrading an existing install.
5. Run
pnpm dev- Blog — http://localhost:3000/blog
- Admin — http://localhost:3000/admin/blog
The admin panel asks for your BLOGPANEL_API_KEY on first visit and validates it against the server before letting you in. From there, New Post opens the block editor — type / for the command menu — and publishing puts the post on the blog immediately. To see the whole thing populated before writing anything, npx next-blogpanel seed inserts three categories and an example post.
AEO: visible to AI answer engines by default
Publishing a post does more than put it on the blog. It also, immediately and with nothing further to configure: adds it to llms.txt/llms-full.txt (the conventions ChatGPT, Claude and Perplexity read to answer questions about your content), pings IndexNow so Bing/Yandex/Naver/Seznam learn about it within seconds, and gets it an Open Graph card even if you never uploaded a cover image. Change its slug later and a redirect is created automatically, so nothing that already linked to it breaks.
AI crawlers are welcome by default. robots.txt allows GPTBot, ClaudeBot, PerplexityBot and every other bot in KNOWN_AI_CRAWLERS out of the box — AEO is the point of this feature set, so opting in to it isn't something you should have to do. If you want to keep a specific bot out, block it per-bot from Settings → Crawlers & AI; nothing else about the site changes. See docs/api.md — Crawler surface and docs/configuration.md — Crawlers & AI.
Paths
Three independent paths, and all three are defaults you own. Set them at
initand every scaffolded folder, link, and fetch follows. Nothing in the package hardcodes the word "blog".npx next-blogpanel init --blog-path /articles --admin-path /admin/articles --api-path /api/articles
The three flags default to /blog, /admin/blog and /api/blog. Whatever you pass is written into the scaffolded directory names, into the basePath/adminPath/apiPath props of every generated file, and into next-blogpanel.config.ts. init refuses paths that resolve to the site root, and refuses any two paths that resolve to the same directory.
Changing the paths later means re-running init with the new flags, updating next-blogpanel.config.ts, and removing the old folders. init warns you when it spots a tree left behind from a previous run.
Configuration
next-blogpanel.config.ts is generated with just the paths and auth strategy. Everything else falls back to a built-in default:
import { defineConfig } from 'next-blogpanel';
export default defineConfig({
basePath: '/blog',
adminPath: '/admin/blog',
apiPath: '/api/blog',
postsPerPage: 10,
editor: {
maxImageSize: 2 * 1024 * 1024, // stored ceiling — reached by compression
maxUploadSize: 10 * 1024 * 1024, // ingest ceiling — enforced by rejection
autosaveInterval: 30000,
},
seo: {
titleTemplate: '%s | %siteName%',
generateRSS: true,
generateSitemap: true,
structuredData: true,
},
features: {
search: true,
relatedPosts: true,
readingProgress: true,
tableOfContents: true,
shareButtons: true,
darkMode: true,
scheduling: true,
revisionHistory: true,
imageOptimization: true,
},
auth: { strategy: 'api-key' },
});The file only takes effect because app/next-blogpanel.setup.ts imports it and calls setConfig(), and every scaffolded file imports that module in turn. If you hand-write a route handler or server component that calls a next-blogpanel API, import the setup module at the top of it too — docs/configuration.md explains why the import has to be repeated rather than done once.
Audit retention is deliberately not in this file: next-blogpanel migrate is the only thing that acts on it, it runs as its own process, and it cannot read a TypeScript config file — so it lives in BLOGPANEL_AUDIT_RETENTION_DAYS instead. See docs/configuration.md.
The two image limits are not the same knob and are not interchangeable. maxUploadSize (10MB) is the largest file the upload endpoint will accept — anything bigger is rejected with a 413. maxImageSize (2MB) is the largest file that will ever be stored — anything above it is compressed down, not refused. Full reference: docs/configuration.md.
Storage
Uploaded images go into your MongoDB via GridFS by default. That needs no configuration, no extra account, and no extra bill. Budget for it honestly, though: one upload is stored as up to seven objects — the WebP original plus five responsive widths and a 200x200 thumbnail — so a photo whose original lands at 200KB occupies roughly 500-800KB in total. On the Atlas free tier that puts 512MB at several hundred images, not the couple of thousand the original alone would suggest. Deleting a media item reclaims only the original; the derivatives are not tracked and stay behind. See docs/storage.md.
Add the five BLOGPANEL_R2_* variables and Cloudflare R2 takes over automatically. Set BLOGPANEL_STORAGE to force a specific backend regardless of what is configured.
Neither one? Implement the exported StorageProvider interface, hand the instance to registerStorageProvider() in the generated setup module, and set BLOGPANEL_STORAGE to its name — Vercel Blob, Supabase Storage, Backblaze B2, ImageKit, UploadThing and plain S3 all fit it. The name is recorded on every media document and read back to route deletes to the right backend; a name that cannot be resolved fails loudly rather than deleting from the wrong one. Worked example in docs/storage.md.
Every upload is re-encoded to WebP and stepped down through quality levels (85 → 75 → 65 → 55) and then width caps (2560 → 1920) until it fits under 2MB. A 5MB photo is stored, just smaller. Only files over the 10MB ingest ceiling are refused outright, along with SVGs over 2MB — sharp cannot meaningfully shrink those. Details, cost comparison and the provider interface: docs/storage.md.
Documentation
| Guide | Contents |
|---|---|
| docs/configuration.md | Every config key and environment variable, CLI flags, what init scaffolds |
| docs/components.md | Page-level and atomic components, props, slots, admin components, stylesheets |
| docs/api.md | REST endpoints, auth, request/response shapes, package exports, slash commands |
| docs/theming.md | All CSS variables, colour presets, dark mode |
| docs/storage.md | GridFS vs R2, free-tier comparison, the compression ladder, writing a custom StorageProvider |
| docs/migrating-from-nextblogkit.md | Upgrading from nextblogkit 0.7.x |
CLI
npx next-blogpanel init # scaffold into your Next.js project
npx next-blogpanel migrate # create indexes, upgrade a pre-1.0 media schema, backfill token roles
npx next-blogpanel seed # insert example categories and a post
npx next-blogpanel health # check MongoDB, R2 and environment variablesTech Stack
| Layer | Technology | |---|---| | Framework | Next.js 14+ (App Router) | | Editor | TipTap 2.x | | Database | MongoDB (native driver) | | Storage | MongoDB GridFS, Cloudflare R2, or any backend you register | | Image processing | sharp | | Syntax highlighting | Shiki | | Validation | Zod | | CLI | Commander.js | | Build | tsup (ESM + CJS) |
Developing the package
git clone https://github.com/butterblogs/next-blogpanel.git
cd next-blogpanel
pnpm install
pnpm build # or: pnpm dev, for watch mode
pnpm testTo try a local build in a real app, npm pack here and install the resulting tarball there, then rm -rf .next before restarting the dev server.
License
MIT
Sponsored by ButterBlogs
Next BlogPanel is built and sponsored by ButterBlogs, an AI content platform that takes a blog post from research to publication.
The two fit together through the API: draft and optimise in ButterBlogs, then POST to your own /api/blog/posts endpoint, or paste into the admin editor. ButterBlogs handles content SEO — keyword targeting, EEAT signals, internal linking. Next BlogPanel handles technical SEO — structured data, sitemap, RSS, meta tags — on infrastructure you control.
