Blog
Create, manage and localize blog posts using content-collections and MDX
Blog
MDX-powered blog with categories, authors and i18n. The listing page is /blog and supports category filtering and SEO-friendly ordering.
Posts live in content/blog. Use locale suffixes like post.zh.mdx for Chinese.
Blog System Structure
- Content:
content/blog,content/author,content/category - App routes:
src/app/[locale]/(site)/blog(listing) andsrc/app/[locale]/(site)/blog/[...slug](post) - Components:
src/components/blog(blog-grid.tsx,blog-card.tsx, filters, etc.) - Utils:
src/lib/blog/get-categories.ts
Source Configuration
This project uses Content Collections. The schemas are defined in content-collections.ts:
// Posts collection
const posts = defineCollection({
name: "post",
directory: "content/blog",
include: "**/*.mdx",
schema: z.object({
title: z.string(),
description: z.string(),
image: z.string().optional(),
date: z.string().datetime(),
published: z.boolean().default(true),
categories: z.array(z.string()),
author: z.string(),
tags: z.array(z.string()).optional(),
estimatedTime: z.number().optional(),
}),
})
// Authors & categories collections are also defined similarlyThe listing imports generated data from .content-collections/generated and applies locale/category filters:
import { allPosts } from "../../../../../.content-collections/generated"
const filteredPosts = allPosts
.filter((post) => post.locale === locale && /* optional cat filter */ true)
.sort((a, b) => toTime(b.date) - toTime(a.date))Creating Blog Content
---
slug: product
name: Product
description: Product announcements and updates
------
title: Welcome to VibeAny
description: Build faster with AI, auth, payments, and i18n
image: /docs/blog/image.avif
date: "2025-01-01"
published: true
categories: ["product"]
author: "vibe-any"
---
Your content here…Multi-language Support
Create filename.mdx for the default locale and filename.zh.mdx for Chinese. Authors and categories follow the same pattern.
Best Practices
- Always set
dateandpublishedfor predictable ordering - Provide an
imagefor richer cards and social previews
- Keep titles concise; use
descriptionfor context - Prefer localized files for fully translated experiences
Programmatic Queries (optional)
import { allPosts } from "@/.content-collections/generated"
export const getPostsByCategory = (slug: string, locale: string) =>
allPosts.filter((p) => p.locale === locale && p.categories?.some((c: any) => c?.slug === slug))