Skip to Content
GuidesBlog System

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.mdx

Add 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:

  1. Create a Notion integration 
  2. Share your database with the integration
  3. 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:

  1. Create a Sanity project 
  2. 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:

  1. 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 }
  1. 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 compiler

Styling

The blog listing layout currently lives in src/app/(site)/blog/page.tsx. Customize the card layout there:

<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 );

Next Steps

Last updated on