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

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

Next Steps