md0 CMS works seamlessly with Astro's content collections. Edit markdown visually while Astro handles ultra-fast builds and modern web features.
How It Works
The Architecture
GitHub Repository
├── src/
│ └── content/
│ ├── blog/ ← md0 CMS edits here
│ └── docs/
└── astro.config.mjs
Workflow:
- Edit in md0 CMS
- Commits to GitHub
- Astro Content Collections API reads files
- Static pages generated
Setup
Prerequisites
- Astro project (v3.0+ recommended)
- GitHub repository
- Content in
src/content/directory
Install Dependencies
Astro includes content collections support by default:
npm create astro@latest
# or update existing project
npm install astro@latest
Content Collections
Define Collection Schema
src/content/config.ts:
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
date: z.date(),
excerpt: z.string(),
author: z.string(),
tags: z.array(z.string()),
featured_image: z.string().optional(),
published: z.boolean().default(false),
}),
});
const docs = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string(),
category: z.string(),
order: z.number().default(0),
}),
});
export const collections = { blog, docs };
md0 CMS Collections
Map to Astro's content structure:
# Blog collection
Name: Blog Posts
Path: src/content/blog/**/*.md
# Docs collection
Name: Documentation
Path: src/content/docs/**/*.md
Blog Example
List All Posts
src/pages/blog/index.astro:
---
import { getCollection } from 'astro:content'
const posts = await getCollection('blog', ({ data }) => {
return data.published === true
})
const sortedPosts = posts.sort((a, b) =>
b.data.date.valueOf() - a.data.date.valueOf()
)
---
<html>
<body>
<h1>Blog</h1>
{sortedPosts.map(post => (
<article>
<a href={`/blog/${post.slug}`}>
<h2>{post.data.title}</h2>
</a>
<time>{post.data.date.toLocaleDateString()}</time>
<p>{post.data.excerpt}</p>
<div>
{post.data.tags.map(tag => (
<span>{tag}</span>
))}
</div>
</article>
))}
</body>
</html>
Generate Post Pages
src/pages/blog/[...slug].astro:
---
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
const posts = await getCollection('blog')
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}))
}
const { post } = Astro.props
const { Content } = await post.render()
---
<html>
<body>
<article>
<h1>{post.data.title}</h1>
<time>{post.data.date.toLocaleDateString()}</time>
<p>By {post.data.author}</p>
{post.data.featured_image && (
<img src={post.data.featured_image} alt={post.data.title} />
)}
<Content />
</article>
</body>
</html>
MDX Support
Enable MDX
astro.config.mjs:
import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";
export default defineConfig({
integrations: [mdx()],
});
MDX Collection Schema
src/content/config.ts:
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
date: z.date(),
}),
});
Astro handles both .md and .mdx files automatically.
Using MDX Components
src/pages/blog/[...slug].astro:
---
import { getCollection } from 'astro:content'
import Button from '@/components/Button.astro'
export async function getStaticPaths() {
const posts = await getCollection('blog')
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}))
}
const { post } = Astro.props
const { Content } = await post.render()
---
<article>
<Content components={{ Button }} />
</article>
Advanced Features
Image Optimization
Install Astro Image:
npm install @astrojs/image
Use in template:
---
import { Image } from 'astro:assets'
import { getEntry } from 'astro:content'
const post = await getEntry('blog', Astro.params.slug)
---
{post.data.featured_image && (
<Image
src={post.data.featured_image}
alt={post.data.title}
width={1200}
height={630}
/>
)}
Content Filtering
Filter by tag:
---
import { getCollection } from 'astro:content'
const tag = Astro.params.tag
const posts = await getCollection('blog', ({ data }) => {
return data.tags.includes(tag) && data.published
})
---
Filter by category:
---
const category = Astro.params.category
const docs = await getCollection('docs', ({ data }) => {
return data.category === category
})
---
Pagination
src/pages/blog/[...page].astro:
---
import { getCollection } from 'astro:content'
export async function getStaticPaths({ paginate }) {
const posts = await getCollection('blog')
const sortedPosts = posts.sort((a, b) =>
b.data.date.valueOf() - a.data.date.valueOf()
)
return paginate(sortedPosts, { pageSize: 10 })
}
const { page } = Astro.props
---
<div>
{page.data.map(post => (
<article>
<h2>{post.data.title}</h2>
</article>
))}
{page.url.prev && <a href={page.url.prev}>Previous</a>}
{page.url.next && <a href={page.url.next}>Next</a>}
</div>
Related Posts
---
import { getCollection } from 'astro:content'
const currentPost = await getEntry('blog', Astro.params.slug)
const allPosts = await getCollection('blog')
// Find posts with similar tags
const related = allPosts
.filter(post =>
post.slug !== currentPost.slug &&
post.data.tags.some(tag => currentPost.data.tags.includes(tag))
)
.slice(0, 3)
---
<aside>
<h3>Related Posts</h3>
{related.map(post => (
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
))}
</aside>
TypeScript Support
Type-Safe Content
Astro generates types from your schema:
import type { CollectionEntry } from "astro:content";
interface Props {
post: CollectionEntry<"blog">;
}
const { post } = Astro.props;
// post.data is fully typed
Custom Types
src/types.ts:
import type { CollectionEntry } from "astro:content";
export type BlogPost = CollectionEntry<"blog">;
export type DocPage = CollectionEntry<"docs">;
Deployment
Vercel
npm install -g vercel
vercel
Auto-deploys on GitHub commits.
Netlify
netlify.toml:
[build]
command = "npm run build"
publish = "dist"
Cloudflare Pages
Connect repository and set:
- Build command:
npm run build - Build output:
dist
Static Hosting
Build and upload dist folder:
npm run build
# Upload dist/ to hosting
md0 CMS Schema
Blog Schema
fields:
- name: title
type: string
required: true
- name: date
type: date
required: true
- name: excerpt
type: text
required: true
- name: author
type: string
required: true
- name: tags
type: array
items: string
- name: featured_image
type: string
- name: published
type: boolean
default: false
Docs Schema
fields:
- name: title
type: string
required: true
- name: description
type: text
required: true
- name: category
type: select
options:
- Getting Started
- Guides
- API Reference
- name: order
type: number
default: 0
Best Practices
Content Structure
src/
└── content/
├── blog/
│ ├── 2024/
│ │ └── my-post.md
│ └── 2023/
└── docs/
├── getting-started/
└── guides/
Frontmatter Format
---
title: "Post Title"
date: 2024-01-15
excerpt: "Brief summary"
author: "Author Name"
tags: ["astro", "cms"]
featured_image: "/images/hero.jpg"
published: true
---
Image Paths
Store images in public/:
public/
└── images/
├── blog/
└── docs/
Reference in frontmatter:
featured_image: "/images/blog/hero.jpg"
Troubleshooting
Schema Validation Errors
Check:
- Frontmatter matches Zod schema
- Required fields are present
- Data types are correct
- Dates are valid
Collection Not Found
Check:
- Files are in
src/content/[collection]/ - Collection is defined in
config.ts - File extension is
.mdor.mdx - Frontmatter is valid
Build Errors
Check:
- All markdown files have valid frontmatter
- Schema matches content
- No syntax errors in Astro components
- Dependencies are installed
Performance
Build Speed
Astro is extremely fast. For large sites:
// astro.config.mjs
export default defineConfig({
output: "static",
build: {
inlineStylesheets: "auto",
},
});
Content Caching
Astro caches content collections automatically during development.
Next Steps
- Create Collections for Astro content
- Define Schemas matching Zod schema
- Start Editing in md0 CMS