md0 CMS FOR
NEXT.JS
INTEGRATION GUIDE
md0 connects directly to your GitHub repository and gives your team a visual editor for markdown content. No backend, no database, no separate CMS hosting.
Before You Start
Prerequisites
A Next.js project on GitHub
App Router (Next.js 13+) and Pages Router both work. Your markdown files just need to live somewhere in the repo — content/posts/, _posts/, whatever you already use.
A GitHub account
md0 uses GitHub OAuth. You grant access to the specific repo, not your entire account. md0 reads and writes files via the GitHub API — no SSH keys or deploy tokens needed.
An md0 account
Free for public repositories. Sign up at cms.md0.io — it takes about 30 seconds with GitHub OAuth.
Step 1
Connect Your GitHub Repository
Sign in with GitHub OAuth
Go to cms.md0.io and click Sign in with GitHub. You will be taken to GitHub to authorize md0. Grant access to the repositories you want to manage — you can limit this to specific repos rather than granting access to everything.
md0 requests read and write access to repository contents. This is required to read your existing markdown files and to commit new content when editors save their work.
Select your Next.js repo
After authentication, you land on the repository selection screen. Pick your Next.js project from the list. If the repo does not appear, you may need to grant md0 access to it in your GitHub OAuth app settings.
Once you select the repo, md0 scans the directory tree and lists all .md and .mdx files it finds. You do not need to add any config files to your repo at this stage.
What md0 detects automatically
md0 reads your repo structure and identifies:
- — Directories that contain markdown files (likely content collections)
- — Frontmatter field names already used in your existing files
- — Whether files use
.mdor.mdxextensions - — Your default branch
This auto-detection means md0 can suggest a starting schema for your content without you doing any manual setup.
Step 2
Define Your Content Schema
A schema tells md0 where your content lives and which frontmatter fields to display in the editor. You define schemas through md0's visual schema builder — no YAML or JSON to write by hand. md0 stores the resulting config as a file in your repo.
Here is what a schema definition looks like for a blog post collection. md0 generates this and commits it to your repo:
Example: Blog post collection schema
{
"name": "posts",
"path": "content/posts",
"extension": "mdx",
"fields": [
{ "name": "title", "type": "text", "required": true },
{ "name": "date", "type": "date" },
{ "name": "excerpt", "type": "textarea" },
{ "name": "published", "type": "boolean", "default": false }
]
}name
The collection name. This appears in the md0 sidebar and is used internally to identify the collection. Keep it lowercase with no spaces — posts, docs, authors.
path
The directory in your repo where content files live. This must match exactly where your Next.js code looks for files. If your code reads from content/posts, this field must say content/posts. A mismatch here is the most common setup mistake.
extension
Either md or mdx. New files created through md0 will use this extension. Existing files are read regardless of extension.
fields
Each field maps to a frontmatter key in your markdown files. md0 generates a form input for each field type:
text— single-line text input (use for titles, slugs)textarea— multi-line text input (use for excerpts, descriptions)date— date picker that outputs ISO 8601 formatboolean— toggle switch (use for published, featured, draft flags)
Fields match your frontmatter exactly
If your existing posts use publishedAt instead of date, name the field publishedAt in the schema. md0 reads the field name from your existing frontmatter and displays whatever value is there. It does not rename or transform your existing content.
Step 3
Fetch Content in Next.js
md0 does not introduce any API client or SDK to your Next.js project. Your code reads markdown files from the filesystem exactly as it does today. If you already have a working blog, you do not need to change a single line.
Below are copy-paste-ready implementations for both App Router and Pages Router.
App Router — app/blog/[slug]/page.tsx
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
export async function generateStaticParams() {
const postsDir = path.join(process.cwd(), 'content/posts')
const files = fs.readdirSync(postsDir)
return files
.filter(file => /\.mdx?$/.test(file))
.map(file => ({
slug: file.replace(/\.mdx?$/, ''),
}))
}
export default async function BlogPost({
params,
}: {
params: { slug: string }
}) {
const filePath = path.join(
process.cwd(),
'content/posts',
`${params.slug}.mdx`
)
const fileContent = fs.readFileSync(filePath, 'utf8')
const { data: frontmatter, content } = matter(fileContent)
return (
<article>
<h1>{frontmatter.title}</h1>
<p>{frontmatter.date}</p>
<div>{/* render content with your MDX renderer */}</div>
</article>
)
}What each part does
generateStaticParams
Reads the content/posts directory at build time and generates a route for each .md or .mdx file. When md0 creates a new post and commits it to GitHub, your next Vercel/Netlify build picks it up automatically — no code change required.
matter(fileContent)
gray-matter parses the frontmatter YAML at the top of each file and separates it from the body content. The data object contains your frontmatter fields (title, date, excerpt, etc). The content string is the raw markdown body.
Install gray-matter if you haven't already
npm install gray-matter
Listing all posts — app/blog/page.tsx
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import Link from 'next/link'
export default async function BlogIndex() {
const postsDir = path.join(process.cwd(), 'content/posts')
const files = fs.readdirSync(postsDir).filter(f => /\.mdx?$/.test(f))
const posts = files
.map(file => {
const slug = file.replace(/\.mdx?$/, '')
const raw = fs.readFileSync(path.join(postsDir, file), 'utf8')
const { data } = matter(raw)
return { slug, ...data }
})
.filter(post => post.published)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
return (
<main>
<h1>Blog</h1>
<ul>
{posts.map(post => (
<li key={post.slug}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
<p>{post.date}</p>
</li>
))}
</ul>
</main>
)
}Filtering by published status
The .filter(post => post.published) line filters out posts where published: false. This is how you keep draft content out of your production build. Editors set the toggle in md0 when they are ready to publish. The next build automatically includes the post.
Pages Router — pages/blog/[slug].tsx
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import type { GetStaticPaths, GetStaticProps } from 'next'
type Props = {
frontmatter: Record<string, string>
content: string
}
export const getStaticPaths: GetStaticPaths = async () => {
const postsDir = path.join(process.cwd(), 'content/posts')
const files = fs.readdirSync(postsDir)
const paths = files
.filter(f => /\.mdx?$/.test(f))
.map(f => ({ params: { slug: f.replace(/\.mdx?$/, '') } }))
return { paths, fallback: false }
}
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
const slug = params?.slug as string
const filePath = path.join(process.cwd(), 'content/posts', `${slug}.mdx`)
const fileContent = fs.readFileSync(filePath, 'utf8')
const { data: frontmatter, content } = matter(fileContent)
return {
props: { frontmatter: frontmatter as Record<string, string>, content },
}
}
export default function BlogPost({ frontmatter, content }: Props) {
return (
<article>
<h1>{frontmatter.title}</h1>
<p>{frontmatter.date}</p>
</article>
)
}Pages Router with ISR
If you want new posts to appear without a full rebuild, add revalidate to getStaticProps:
return {
props: { frontmatter, content },
revalidate: 60, // seconds
}With ISR, Next.js will regenerate the page in the background after 60 seconds if a request comes in. This means new posts from md0 go live within a minute without waiting for a full deploy.
Step 4
Deploy and Test
Push your schema config to GitHub
When you save a collection schema in md0, it commits a config file to your repository automatically. You do not need to push anything manually. The commit appears in your GitHub repo history just like any other commit, so you can review it, revert it, or branch off it.
md0 detects the commit and updates the editor
After the config commit lands, refresh the md0 editor. Your collection appears in the sidebar. Click it to see all existing posts listed. Each post shows the frontmatter fields you defined in the schema as form inputs, and the body renders in the visual editor.
Your team can now edit content through md0
Invite team members to your md0 workspace. They see the same visual editor with the form fields you configured. They do not need to know anything about Git, markdown syntax, or frontmatter YAML. They write in the visual editor and click Save.
Permissions are controlled through GitHub. Anyone with write access to the repo can edit through md0. Read-only collaborators see content but cannot publish.
Every save creates a GitHub commit
When an editor saves in md0, md0 commits the changes to your GitHub repo. The commit message follows this format:
content: update "Your Post Title" via md0 CMSIf you have a Vercel or Netlify deployment connected to the repo, GitHub triggers a webhook on every push. Vercel starts a new build automatically. Your Next.js site rebuilds and the new content goes live in about 30 to 60 seconds.
The full workflow
Troubleshooting
Common Issues
My content is not showing up in Next.js
The most likely cause is a path mismatch. Check that the path in your md0 schema exactly matches the directory your Next.js code reads from.
If your code does this:
const postsDir = path.join(process.cwd(), 'content/posts')Your md0 schema path must be:
"path": "content/posts"Build fails after md0 edits
This usually means a frontmatter field name in the committed file does not match what your code expects. For example, if your schema defines a field called date but your Next.js component reads frontmatter.publishedAt, the value will be undefined.
Check your schema field names match exactly. Open a post file on GitHub after an md0 edit and confirm the frontmatter looks right:
--- title: My Post Title date: 2024-06-01 excerpt: A short description published: true ---
Images are not working
By default, md0 uploads images to a directory in your repo. For Next.js, images need to be in the public/ directory to be served at runtime.
In your md0 media library settings, set the upload path to public/images. Images uploaded through md0 will then be available at /images/filename.jpg in your Next.js app. Your markdown can reference them with a relative path like .
My repo does not appear in md0
GitHub OAuth apps require explicit per-repo access unless you granted access to all repos during sign-in. Go to your GitHub account settings, find the md0 OAuth app under Applications, and grant access to the specific repo you want to use.
Builds are slow after md0 saves
This is a deployment platform issue, not an md0 issue. If you are on Vercel, consider enabling ISR with revalidate (Pages Router) or next: { revalidate: 60 } (App Router) so individual pages update without a full rebuild.
Compatibility
Works With
App Router
Next.js 13+ App Router with Server Components. Read markdown files directly in async Server Components. No client-side fetching needed — content loads at build time or request time.
Pages Router
Classic Pages Router with getStaticProps, getStaticPaths, and ISR. The same fs + gray-matter pattern works in all three data-fetching methods.
MDX Libraries
Works with next-mdx-remote, contentlayer, gray-matter, remark, rehype, and any library that reads markdown from the filesystem. md0 does not lock you into a specific renderer.
Vercel and Netlify
Deploy on Vercel, Netlify, or anywhere that builds from GitHub. Every md0 save triggers a GitHub push, which triggers your deployment webhook. No manual rebuild steps needed.
Keep Going
Next Steps
Learn the visual editor
md0's visual markdown editor outputs standard GitHub-flavored markdown. Editors get formatting controls, image uploads, and a live preview without ever seeing raw markdown syntax.
Visual markdown editor →Build a more complex schema
Beyond basic text and boolean fields, md0 supports image fields, select dropdowns, number inputs, and nested objects. The visual schema builder lets you design the exact editing experience your team needs.
Schema builder documentation →Use md0 with other frameworks
md0 works with any static site generator that reads markdown files from a Git repo. The same GitHub-native approach applies to Astro, Hugo, and others.
Comparing your options?
- Comparing md0 to Contentful for Next.js? See the comparison →
- Comparing md0 to Sanity for Next.js? See the comparison →
- Coming from Netlify CMS? Netlify CMS alternative →
CONNECT YOUR
NEXT.JS REPO
Free for public repositories. No API integration. No backend to host. Set up in under five minutes.
CONNECT YOUR NEXT.JS REPO