Getting started
Deploy a starter on Vercel
Full Next.js apps with shadcn-style UI (npm @next-md-blog/core). Set NEXT_PUBLIC_SITE_URL after the first deploy. More detail: Deployment.
Single locale
i18n
1. Install the core library
npm install @next-md-blog/core
# or
pnpm add @next-md-blog/corePeer dependencies (next, react, react-dom) should already be present in your Next.js app.
2. Scaffold with the CLI (recommended)
From your Next.js project root:
npx @next-md-blog/cliWhen blog page generation is enabled, the CLI can create a posts/ directory, example markdown, next-md-blog.config.ts, and App Router pages under routes you choose (defaults: /blog/[slug] and /blogs). It also writes sitemap.ts, robots.ts, and feed.xml/route.ts (RSS) next to your App Router root—either app/ or src/app/. It installs @tailwindcss/typography and adjusts globals.css, and can add an opengraph-image route.
Non-interactive example:
npx @next-md-blog/cli --non-interactive \
--content-dir=posts \
--blog-route=blog \
--blogs-route=blogs \
--site-name="My Blog" \
--site-url="https://example.com" \
--author="Ada Lovelace" \
--twitter="@ada"Full flag list: CLI reference.
SEO files only (no new blog pages or installs)—for example after upgrading the CLI or if you skipped pages on first run:
npx @next-md-blog/cli seo --non-interactive --content-dir=postsUse the same --content-dir, --i18n-enabled, --locales, and --locale-folder flags as a full init so the generated code matches your project. --force overwrites existing sitemap, robots, and feed files. Details: CLI reference.
3. Manual minimal setup
- Add
next-md-blog.config.tsat the project root:
import { createConfig } from '@next-md-blog/core';
export default createConfig({
siteName: 'My Blog',
siteUrl: process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000',
defaultAuthor: 'You',
twitterHandle: '@you',
defaultLang: 'en',
});-
Put markdown files in
posts/(or another directory; passpostsDirin options). -
Add a dynamic route that loads a post. Use async
params(Next.js 16 App Router):
import {
getBlogPost,
getAllBlogPosts,
generateBlogPostMetadata,
MarkdownContent,
} from '@next-md-blog/core';
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';
import blogConfig from '@/next-md-blog.config';
export async function generateStaticParams() {
const posts = await getAllBlogPosts({ config: blogConfig });
return posts.map((post) => ({ slug: post.slug }));
}
export async function generateMetadata(props: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await props.params;
const post = await getBlogPost(slug, { config: blogConfig });
if (!post) return { title: 'Not found' };
return generateBlogPostMetadata(post, blogConfig);
}
export default async function Page(props: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await props.params;
const post = await getBlogPost(slug, { config: blogConfig });
if (!post) notFound();
return (
<article className="prose dark:prose-invert max-w-none">
<h1>{post.frontmatter.title ?? post.slug}</h1>
<MarkdownContent content={post.content} />
</article>
);
}- Wrap rendered markdown in
proseclasses from@tailwindcss/typographyfor readable typography (the CLI can wire this up).
4. Styling
The library does not ship a global stylesheet for article body text. Use Tailwind Typography (prose, dark:prose-invert) or your own CSS. See Configuration for config-related author and URL fields used in SEO.