Blog
Learn how to create, manage, and customize blog posts
TanStarter includes a blog system built on Content Collections.
Blog System Architecture
The blog system is built with Content Collections, with the collection configuration defined in content-collections.ts at the project root.
Data Source Configuration
The blog system uses defineCollection in the content-collections.ts file to define the data collection:
import { defineCollection, defineConfig } from '@content-collections/core';
import { z } from 'zod';
const blog = defineCollection({
name: 'blog',
directory: 'content/blog',
include: '**/*.md',
schema: z.object({
title: z.string(),
description: z.string(),
date: z.string(),
category: z.string(),
content: z.string(),
image: z.url(),
}),
transform: (doc) => ({
...doc,
slug: doc._meta.path,
}),
});
export default defineConfig({
collections: [blog],
});Then use the data provided by content-collections for queries in src/lib/blog.ts:
import { allBlogs } from 'content-collections';
import type { Blog } from 'content-collections';
export type BlogPost = Blog & { slug: string };
export function getSortedPosts(): BlogPost[] {
return [...(allBlogs as BlogPost[])].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
);
}
export function getPostBySlug(slug: string): BlogPost | undefined {
return (allBlogs as BlogPost[]).find((p) => p.slug === slug);
}
export function getPaginatedPosts(page: number) {
const sorted = getSortedPosts();
// ... pagination logic
}Creating Blog Content
Adding New Blog Posts
Create a new .md file in the content/blog directory:
---
title: My First Blog Post
description: This is a brief description of my first blog post.
date: 2026-01-15
category: Tutorial
image: https://example.com/images/blog/my-first-post.jpg
---
# Introduction
This is my first blog post. You can use **Markdown** here.
## Section 1
Some content here...
## Section 2
More content here...Frontmatter Fields
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Post title |
description | string | Yes | Post description |
date | string | Yes | Publication date (format: YYYY-MM-DD) |
category | string | Yes | Post category (single category) |
image | URL | Yes | Cover image URL |
TanStarter's blog uses .md Markdown files. Each post has a single category (string), not a multi-category array.
Routes and Pages
The blog uses TanStack Router's file-based routing system:
src/routes/blog/index.tsx: Blog listing page with paginationsrc/routes/blog/$slug.tsx: Blog post detail page
import { createFileRoute } from '@tanstack/react-router';
import { getPaginatedPosts } from '@/lib/blog';
export const Route = createFileRoute('/blog/')({
loader: ({ location }) => {
const page = Number(new URLSearchParams(location.search).get('page')) || 1;
return getPaginatedPosts(page);
},
component: BlogListPage,
});import { createFileRoute, notFound } from '@tanstack/react-router';
import { getPostBySlug } from '@/lib/blog';
export const Route = createFileRoute('/blog/$slug')({
loader: async ({ params }) => {
const post = getPostBySlug(params.slug);
if (!post) throw notFound();
return post;
},
component: BlogPostPage,
});Customization
Configuring Pagination
Configure the number of posts per page in src/config/website.ts:
export const websiteConfig: WebsiteConfig = {
// ...other config
blog: {
enable: true,
paginationSize: 6,
},
// ...other config
}Changing Blog Card Layout
Customize the blog card component in src/components/blog/blog-card.tsx:
import type { BlogPost } from '@/lib/blog';
interface BlogCardProps {
post: BlogPost;
}
export function BlogCard({ post }: BlogCardProps) {
return (
<div className="group flex flex-col border rounded-lg overflow-hidden h-full bg-card shadow-sm hover:shadow-md transition-shadow">
<h3>{post.title}</h3>
<p>{post.description}</p>
<span>{post.category}</span>
<time>{post.date}</time>
{/* ... rest of the component */}
</div>
);
}Customizing Blog Post Data Structure
To add new fields to blog posts:
- Modify the schema in
content-collections.ts - Update components to display the new fields
Example: adding a "featured" field
const blog = defineCollection({
name: 'blog',
directory: 'content/blog',
include: '**/*.md',
schema: z.object({
title: z.string(),
description: z.string(),
date: z.string(),
category: z.string(),
content: z.string(),
image: z.url(),
// Add new field
featured: z.boolean().default(false),
}),
transform: (doc) => ({
...doc,
slug: doc._meta.path,
}),
});Then you can use this field in your blog posts:
---
title: Important Announcement
description: Read this important announcement
date: 2026-01-15
category: Announcement
image: https://example.com/images/blog/announcement.jpg
featured: true
---
Content here...Querying Posts Programmatically
You can use the utility functions in src/lib/blog.ts to query posts:
import { getSortedPosts, getPostBySlug, getPaginatedPosts } from '@/lib/blog';
// Get all posts sorted by date
const allPosts = getSortedPosts();
// Get a specific post by slug
const post = getPostBySlug('hello-world');
// Get paginated posts
const { posts, totalPages, currentPage } = getPaginatedPosts(1);Build Process
The blog system uses the content-collections build process:
- Development: During development, content-collections watches the
content/directory for changes and automatically regenerates - Build: Vite automatically processes content-collections during build, no additional commands needed
- Generated files: The
.content-collectionsdirectory contains generated TypeScript files
content-collections is integrated with Vite. Development and build processes are fully automated, no manual commands needed.
Best Practices
- Use high-quality images: Use properly sized and optimized images for blog posts
- Consistent categories: Maintain consistent category names across posts
- Write clear metadata: Write clear titles and descriptions for better SEO
- Structured content: Use proper headings and sections in your blog post content
- Date format: Use
YYYY-MM-DDformat for date strings - Use Zod schemas: Use Zod schemas for type-safe content validation
Next Steps
Now that you understand how to work with the blog system in TanStarter, explore these related features:
TanStarter Docs