Blog System
This template includes a powerful multi-source blog system that can aggregate content from MDX files, Notion, Sanity, and your database.
Content Sources
The blog system supports four different content sources:
1. MDX Files (Default)
MDX files are stored in content/blog/ and support rich content with React components.
Create a new post:
touch content/blog/my-post.mdxAdd frontmatter:
---
title: "My First Post"
date: "2025-12-19"
summary: "A brief summary of my post"
tags:
- Tutorial
- Next.js
draft: false
---
# My First Post
Your content here with **Markdown** and JSX components!
<Callout type="info">This is a custom component!</Callout>2. Notion Integration
Connect your Notion workspace to publish posts directly from Notion.
Setup:
- Create a Notion integration
- Share your database with the integration
- Add to
.env:
NOTION_API_KEY="secret_..."
NOTION_DATABASE_ID="your-database-id"Configure your Notion database with these properties:
- Title (title)
- Date (date)
- Summary (text)
- Tags (multi-select)
- Draft (checkbox)
3. Sanity CMS
Use Sanity Studio for a full CMS experience.
Setup:
- Create a Sanity project
- Add to
.env:
SANITY_PROJECT_ID="your-project-id"
SANITY_DATASET="production"
SANITY_READ_TOKEN="your-read-token"Schema example (sanity/schemas/post.ts):
export default {
name: "post",
type: "document",
title: "Post",
fields: [
{ name: "title", type: "string", title: "Title" },
{ name: "slug", type: "slug", title: "Slug" },
{ name: "date", type: "datetime", title: "Date" },
{ name: "summary", type: "text", title: "Summary" },
{ name: "content", type: "array", of: [{ type: "block" }] },
{ name: "tags", type: "array", of: [{ type: "string" }] },
],
};4. Database Posts
Store posts directly in your database for complete control.
Create a post:
import { db } from "~/server/db";
import { posts } from "~/server/db/schema";
await db.insert(posts).values({
title: "Database Post",
slug: "database-post",
content: "Post content here",
summary: "A database-backed post",
publishedAt: new Date(),
tags: ["database", "example"],
});How It Works
Source Aggregation
The blog system aggregates all sources in src/server/blog/sources/index.ts:
export async function getAllPostsFromSources() {
const [mdxPosts, notionPosts, sanityPosts, dbPosts] = await Promise.all([
getMdxPosts(),
getNotionPosts(),
getSanityPosts(),
getDatabasePosts(),
]);
const allPosts = [...mdxPosts, ...notionPosts, ...sanityPosts, ...dbPosts];
return sortByDate(allPosts);
}Post Type
All posts are normalized to a common type:
type BlogPost = {
id: string;
title: string;
slug: string;
summary?: string;
publishedAt?: string;
tags: string[];
source: "mdx" | "notion" | "sanity" | "database";
url: string;
};Customization
Disable Sources
To disable a source, modify src/server/blog/sources/index.ts:
export async function getAllPostsFromSources() {
// Only use MDX and database
const [mdxPosts, dbPosts] = await Promise.all([
getMdxPosts(),
getDatabasePosts(),
]);
return sortByDate([...mdxPosts, ...dbPosts]);
}Custom Frontmatter Fields
Add custom fields to your MDX frontmatter:
- Update the type in src/server/blog/types.ts:
export interface MdxFrontmatter {
title?: string;
date?: string;
summary?: string;
tags?: string[];
draft?: boolean;
author?: string; // New field
coverImage?: string; // New field
}- Use in your MDX:
---
title: "My Post"
author: "John Doe"
coverImage: "/images/cover.jpg"
---Custom Components in MDX
Create custom MDX components in src/components/blog/:
// src/components/blog/callout.tsx
export function Callout({
type,
children,
}: {
type: "info" | "warning" | "error";
children: React.ReactNode;
}) {
return <div className={`callout callout-${type}`}>{children}</div>;
}Register in your MDX renderer:
import { Callout } from "~/components/blog/callout";
const components = {
Callout,
};
// Pass to your MDX compilerStyling
Blog styles are in src/app/blog/page.tsx. Customize the card layout:
<Card className="custom-blog-card">{/* Your custom layout */}</Card>API Routes
Get All Posts
import { getAllPosts } from "~/server/blog";
const posts = await getAllPosts();Get Single Post
import { getPostBySlug } from "~/server/blog";
const post = await getPostBySlug("my-post");Filter by Tag
const posts = await getAllPosts();
const filtered = posts.filter((post) => post.tags.includes("Tutorial"));Best Practices
1. Use Draft Mode
Keep posts private while editing:
---
draft: true
---2. Optimize Images
Use Next.js Image component:
import Image from "next/image";
<Image src="/images/post.jpg" alt="Description" width={800} height={600} />3. Add Metadata
Include rich metadata for SEO:
---
title: "Complete Guide to Next.js"
summary: "Learn everything about Next.js in this comprehensive guide"
tags: ["Next.js", "React", "Tutorial"]
---4. Cache Posts
The blog system includes caching in src/server/blog/utils/cache.ts. Use it for expensive operations:
import { withCache } from "~/server/blog/utils/cache";
export const getCachedPosts = withCache(
() => getAllPosts(),
"all-posts",
{ revalidate: 3600 }, // Cache for 1 hour
);