md0 CMS FOR
ASTRO
INTEGRATION GUIDE
md0 connects directly to your GitHub repository and gives your team a visual editor for markdown and MDX content. No build-time API calls, no external database, no separate CMS hosting.
Before You Start
Prerequisites
An Astro project on GitHub
Any Astro version that uses content collections works. Your content files need to live inside src/content/. That is where md0 will read and write. If you are still using Markdown pages outside of content collections, you can point md0 at any directory.
A GitHub account
md0 uses GitHub OAuth. You grant access to specific repositories, not your entire account. md0 reads and writes files via the GitHub API. No SSH keys, deploy tokens, or GitHub Apps setup needed.
An md0 account
Free for public repositories. Sign up at cms.md0.io. GitHub OAuth gets you in within 30 seconds. No credit card required for public repos.
Step 1
Connect Your GitHub Repository
Sign in with GitHub OAuth
Go to cms.md0.io and click Sign in with GitHub. GitHub asks you to authorize md0 and choose which repositories to grant access to. You can limit access to just the Astro repo you want to manage. md0 does not require access to your entire account.
md0 requests read and write access to repository contents. Read access is needed to display your existing content. Write access is needed to commit changes when editors save.
Select your Astro repo
After authentication, the repository selection screen lists all repos you granted access to. Pick your Astro project. If it does not appear, go to your GitHub OAuth app settings and add the repo there.
Once you select the repo, md0 scans the file tree. It lists all .md and .mdx files it finds and identifies directories that look like content collections, specifically anything under src/content/.
What md0 detects automatically
md0 reads your repo structure and identifies:
- Directories containing markdown files (likely collection folders)
- Frontmatter field names already present in your existing files
- Whether files use
.mdor.mdxextensions - Your default branch name
This lets md0 suggest a starting schema based on your existing content without manual setup.
Step 2
Define Your Content Schema
A schema tells md0 where your content lives and which frontmatter fields to show in the visual editor. You build schemas through md0's schema builder. No YAML or JSON to write manually. md0 stores the config as a file in your repo.
Here is what a schema for an Astro blog collection looks like. The field names must match what you defined in your Astro content config at src/content/config.ts:
Example: Astro blog collection schema
{
"name": "blog",
"path": "src/content/blog",
"extension": "mdx",
"fields": [
{ "name": "title", "type": "text", "required": true },
{ "name": "pubDate", "type": "date" },
{ "name": "description", "type": "textarea" },
{ "name": "draft", "type": "boolean", "default": false }
]
}path
This must match the directory Astro reads your collection from. If your src/content/config.ts defines a collection named blog, the files live at src/content/blog. That is what goes here. A mismatch between the md0 path and Astro's collection path is the most common setup mistake.
fields match your Astro schema
Each field name in the md0 schema must exactly match the key you defined in your Astro content schema. If your Astro config uses pubDate, the md0 field must also be pubDate, not date or publishedAt. Astro validates frontmatter against your schema at build time and will throw an error if the field names do not match.
Supported field types:
text: single-line input (titles, slugs)textarea: multi-line input (descriptions, excerpts)date: date picker, outputs ISO 8601boolean: toggle (draft flags, featured flags)
Your Astro content config for reference
Here is a typical src/content/config.ts that matches the md0 schema above. The field names in both files must align:
// src/content/config.ts
import { defineCollection, z } from 'astro:content'
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
description: z.string().optional(),
draft: z.boolean().default(false),
}),
})
export const collections = { blog }Step 3
Read Content in Astro
md0 does not introduce any client library or API to your Astro project. Your code uses Astro's built-in content collections API exactly as it does today. If you already have a working Astro blog, you do not need to change a single line.
Dynamic route: src/pages/blog/[slug].astro
---
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft)
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}))
}
const { post } = Astro.props
const { Content } = await post.render()
---
<article>
<h1>{post.data.title}</h1>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
{post.data.description && <p>{post.data.description}</p>}
<Content />
</article>What each part does
getCollection with a filter
The second argument to getCollection is a filter function. Passing ({ data }) => !data.draft excludes any post where draft: true. This prevents draft content from reaching production. When an editor in md0 flips the draft toggle to off and saves, the next Astro build includes that post automatically.
post.render()
Astro compiles the MDX body on demand. The returned Content component handles all custom MDX components, remark plugins, and rehype transformations you have configured. md0 does not interfere with any of that pipeline.
post.data
This object contains the typed, validated frontmatter fields from your Astro content schema. Accessing post.data.pubDate gives you a JavaScript Date object because of the z.coerce.date() transform in your Zod schema. Astro handles the ISO string to Date conversion automatically.
Listing all published posts: src/pages/blog/index.astro
---
import { getCollection } from 'astro:content'
const posts = (await getCollection('blog', ({ data }) => !data.draft))
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime())
---
<main>
<h1>Blog</h1>
<ul>
{posts.map(post => (
<li>
<a href={'/blog/' + post.slug}>{post.data.title}</a>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString()}
</time>
{post.data.description && <p>{post.data.description}</p>}
</li>
))}
</ul>
</main>Content collections vs markdown pages
The examples above use Astro's content collections API (getCollection). If your Astro site uses the older markdown pages pattern with files directly in src/pages/, md0 works there too. Set the md0 collection path to src/pages/blog and md0 writes files there. Your getStaticPaths reads them from the filesystem just as before.
Step 4
Deploy and Test
md0 commits to your GitHub repo
When you save a collection schema in md0, it commits a config file to your repository. When an editor saves content, md0 commits the markdown file directly to your repo. You do not push anything manually. Each commit appears in your GitHub history like any other commit. Reviewable, revertable, branchable.
Astro rebuilds automatically
Vercel and Netlify both listen for GitHub push events. When md0 commits, the platform detects it and starts a new Astro build. Astro's build is fast, typically 5 to 15 seconds for small sites. New content goes live within a minute of the editor saving.
Your team uses md0 as the content interface
Invite team members to your md0 workspace. They see a form-based editor matching the fields in your schema. No knowledge of Git, markdown, or frontmatter required. They write in the visual editor and click Save.
Permissions follow GitHub. Anyone with write access to the repo can publish through md0. Read-only collaborators can view content but not commit.
The full workflow
Troubleshooting
Common Issues
Field name mismatches between md0 schema and Astro content config
Astro validates frontmatter against the Zod schema in your src/content/config.ts at build time. If md0 writes a field called date but your Astro schema expects pubDate, Astro throws a validation error and the build fails.
To diagnose: open a recently committed file in GitHub and check the frontmatter block:
--- title: My Post Title pubDate: 2024-06-01 description: A short summary draft: false ---
Then compare those key names against your content/config.ts schema definition. Fix any discrepancies in the md0 schema field names.
Draft posts appearing in production
This happens when the getCollection call does not filter by the draft field. Add the filter function as the second argument:
// Correct: filters out draft posts
const posts = await getCollection('blog', ({ data }) => !data.draft)
// Wrong: returns everything including drafts
const posts = await getCollection('blog')Image paths and Astro's image optimization pipeline
Astro 3+ has an asset pipeline that optimizes images referenced from content collections using the image() helper in your Zod schema. md0 currently writes image paths as strings. If your Astro schema defines an image field with z.string(), it works without issues. If you use image() for Astro's asset transforms, keep your image fields as plain string paths and handle optimization at the component level instead.
For images uploaded through md0's media library, set the upload path to public/images. Your markdown can then reference images at /images/filename.jpg without any Astro asset processing.
Collection not found error
Astro requires that collection names in getCollection('blog') match the keys in your collections export in src/content/config.ts. If your md0 schema's path points to src/content/articles but your config exports a collection named blog, Astro will not find the content. Make sure the directory name, the Astro collection name, and the getCollection call all match.
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, open Applications, find the md0 OAuth app, and grant access to your Astro repo.
Compatibility
Works With
Content Collections API
Astro's getCollection and getEntry APIs work without changes. md0 writes standard frontmatter that Astro parses and validates against your Zod schema at build time.
MDX and Markdown
md0 supports both .md and .mdx files. Set the extension field in your md0 schema to match what your Astro collection uses. MDX files with custom component imports are handled by Astro's renderer, not md0.
Vercel and Netlify
Both platforms detect GitHub push events and trigger an Astro build automatically. There is no additional webhook setup. Connecting your repo to Vercel or Netlify once is sufficient.
GitHub Pages
If you deploy via GitHub Actions to GitHub Pages, your workflow already triggers on push. md0's commits trigger the same workflow. No changes to your Actions config are needed.
Keep Going
Next Steps
Learn the visual editor
md0's visual markdown editor outputs standard GitHub-flavored markdown and MDX. 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 multiselect options. The visual schema builder lets you design an editing experience that matches your Astro content model exactly.
Schema builder documentation →Use md0 with other frameworks
md0 works with any static site generator that reads markdown from a Git repository. The same GitHub-native approach applies across frameworks.
CONNECT YOUR
ASTRO REPO
Free for public repositories. No API integration. No backend to host. Set up in under five minutes.
CONNECT YOUR ASTRO REPO