LogoTanStarter Docs
LogoTanStarter Docs
HomepageIntroductionCodebaseGetting StartedEnvironments
Configuration
Deployment

Integrations

CloudflareDatabaseAuthenticationEmailNewsletterStoragePaymentNotificationsAnalyticsChatboxAffiliates

Customization

MetadataPagesLanding PageBlogComponentsUser ManagementAPI Key Management

Codebase

Project StructureFormatting & LintingEditor SetupUpdating the Codebase
X (Twitter)

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.

hello-world.md
getting-started.md
deploy-to-production.md
index.tsx
$slug.tsx
blog-card.tsx
blog-grid.tsx
blog-pagination.tsx
blog.ts
content-collections.ts

Data Source Configuration

The blog system uses defineCollection in the content-collections.ts file to define the data collection:

content-collections.ts
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:

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:

content/blog/my-first-post.md
---
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

FieldTypeRequiredDescription
titlestringYesPost title
descriptionstringYesPost description
datestringYesPublication date (format: YYYY-MM-DD)
categorystringYesPost category (single category)
imageURLYesCover 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 pagination
  • src/routes/blog/$slug.tsx: Blog post detail page
src/routes/blog/index.tsx
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,
});
src/routes/blog/$slug.tsx
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:

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:

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:

  1. Modify the schema in content-collections.ts
  2. Update components to display the new fields

Example: adding a "featured" field

content-collections.ts
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:

content/blog/important-post.md
---
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:

  1. Development: During development, content-collections watches the content/ directory for changes and automatically regenerates
  2. Build: Vite automatically processes content-collections during build, no additional commands needed
  3. Generated files: The .content-collections directory contains generated TypeScript files

content-collections is integrated with Vite. Development and build processes are fully automated, no manual commands needed.

Best Practices

  1. Use high-quality images: Use properly sized and optimized images for blog posts
  2. Consistent categories: Maintain consistent category names across posts
  3. Write clear metadata: Write clear titles and descriptions for better SEO
  4. Structured content: Use proper headings and sections in your blog post content
  5. Date format: Use YYYY-MM-DD format for date strings
  6. 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:

Newsletter

Configure newsletter subscriptions

Configuration

Configure website settings

Pages

Customize pages

Deployment

Deploy to Cloudflare Workers

Table of Contents

Blog System Architecture
Data Source Configuration
Creating Blog Content
Adding New Blog Posts
Frontmatter Fields
Routes and Pages
Customization
Configuring Pagination
Changing Blog Card Layout
Customizing Blog Post Data Structure
Querying Posts Programmatically
Build Process
Best Practices
Next Steps