Fetching latest headlines…
Headless CMS for TanStack Start: Build a Blog with Cosmic
NORTH AMERICA
🇺🇸 United StatesApril 18, 2026

Headless CMS for TanStack Start: Build a Blog with Cosmic

2 views0 likes0 comments
Originally published byDev.to

You want SSR, fast routing, and a CMS your whole team can edit without touching code. Here's how to build that stack in under an hour.

TanStack Start pairs naturally with Cosmic: Start handles full-document SSR, streaming, and type-safe routing via Vite and TanStack Router, while Cosmic gives you a structured, API-first content layer your editors can use without a developer in the room. The result is a modern content stack that's fast to build, easy to maintain, and genuinely pleasant to work with.

This tutorial walks through building a content-driven TanStack Start blog powered by Cosmic. You'll fetch posts from Cosmic using the JavaScript SDK, render them with server functions, and have a working SSR blog in under 30 minutes.

Prerequisites

  • Node.js 18 or later
  • A free Cosmic account with a bucket set up
  • Basic familiarity with React and TypeScript

1. Create a TanStack Start Project

The fastest way to scaffold a new project is with the TanStack CLI:

npx create-tsrouter-app@latest my-cosmic-app --template start-basic
cd my-cosmic-app
npm install

This gives you a working TanStack Start app with file-based routing, SSR enabled, and Vite as the bundler.

2. Install the Cosmic SDK

npm install @cosmicjs/sdk

3. Configure Your Environment Variables

Create a .env file at the root of your project:

COSMIC_BUCKET_SLUG=your-bucket-slug
COSMIC_READ_KEY=your-read-key

You can find both values in your Cosmic dashboard under Bucket > Settings > API Keys.

TanStack Start uses Vite under the hood. Server-side environment variables are accessed via process.env inside server functions. For client-side access, prefix with VITE_ — but keep your read key on the server only.

4. Create a Cosmic Client

Add a shared client file at src/lib/cosmic.ts:

import { createBucketClient } from '@cosmicjs/sdk'

export const cosmic = createBucketClient({
  bucketSlug: process.env.COSMIC_BUCKET_SLUG!,
  readKey: process.env.COSMIC_READ_KEY!,
})

5. Fetch Posts with a Server Function

TanStack Start's server functions run exclusively on the server, making them the right place to call external APIs and keep keys out of the client bundle.

Create src/server/posts.ts:

import { createServerFn } from '@tanstack/start'
import { cosmic } from '../lib/cosmic'

export type Post = {
  id: string
  title: string
  slug: string
  metadata: {
    teaser: string
    published_date: string
    image?: { imgix_url: string }
  }
}

export const fetchPosts = createServerFn({ method: 'GET' }).handler(
  async () => {
    const { objects } = await cosmic.objects
      .find({ type: 'blog-posts' })
      .props(['id', 'title', 'slug', 'metadata.teaser', 'metadata.published_date', 'metadata.image'])
      .limit(10)

    return objects as Post[]
  }
)

export const fetchPost = createServerFn({ method: 'GET' })
  .validator((slug: string) => slug)
  .handler(async ({ data: slug }) => {
    const { object } = await cosmic.objects
      .findOne({ type: 'blog-posts', slug })
      .props(['id', 'title', 'slug', 'metadata'])
      .depth(1)

    return object
  })

6. Create the Blog Index Route

TanStack Start uses file-based routing. Create src/routes/blog/index.tsx:

import { createFileRoute, Link } from '@tanstack/react-router'
import { fetchPosts } from '../../server/posts'

export const Route = createFileRoute('/blog/')({
  loader: () => fetchPosts(),
  component: BlogIndex,
})

function BlogIndex() {
  const posts = Route.useLoaderData()

  return (
    <main className="max-w-2xl mx-auto py-12 px-4">
      <h1 className="text-3xl font-bold mb-8">Blog</h1>
      <ul className="space-y-6">
        {posts.map((post) => (
          <li key={post.id}>
            <Link
              to="/blog/$slug"
              params={{ slug: post.slug }}
              className="group"
            >
              <h2 className="text-xl font-semibold group-hover:underline">
                {post.title}
              </h2>
              {post.metadata.teaser && (
                <p className="text-gray-600 mt-1">{post.metadata.teaser}</p>
              )}
              {post.metadata.published_date && (
                <time className="text-sm text-gray-400">
                  {new Date(post.metadata.published_date).toLocaleDateString()}
                </time>
              )}
            </Link>
          </li>
        ))}
      </ul>
    </main>
  )
}

7. Create the Post Detail Route

Install react-markdown for safe, component-based markdown rendering:

npm install react-markdown

Create src/routes/blog/$slug.tsx:

import { createFileRoute, notFound } from '@tanstack/react-router'
import ReactMarkdown from 'react-markdown'
import { fetchPost } from '../../server/posts'

export const Route = createFileRoute('/blog/$slug')({
  loader: async ({ params }) => {
    const post = await fetchPost({ data: params.slug })
    if (!post) throw notFound()
    return post
  },
  component: BlogPost,
})

function BlogPost() {
  const post = Route.useLoaderData()

  return (
    <main className="max-w-2xl mx-auto py-12 px-4">
      {post.metadata.image?.imgix_url && (
        <img
          src={`${post.metadata.image.imgix_url}?w=800&auto=format`}
          alt={post.title}
          className="w-full rounded-lg mb-8"
        />
      )}
      <h1 className="text-3xl font-bold mb-4">{post.title}</h1>
      {post.metadata.published_date && (
        <time className="text-sm text-gray-400 block mb-8">
          {new Date(post.metadata.published_date).toLocaleDateString()}
        </time>
      )}
      <div className="prose">
        <ReactMarkdown>{post.metadata.markdown_content || ''}</ReactMarkdown>
      </div>
    </main>
  )
}

8. Run the Dev Server

npm run dev

Open http://localhost:3000/blog and you should see your Cosmic posts rendered server-side via TanStack Start.

Deploy to Vercel

TanStack Start supports Vercel out of the box. From the project root:

npm install -g vercel
vercel

Add your environment variables in the Vercel dashboard under Project > Settings > Environment Variables:

  • COSMIC_BUCKET_SLUG
  • COSMIC_READ_KEY

Deploy and you're live.

What to Build Next

  • Localization: Cosmic's Localization add-on lets you manage content in multiple languages from the same bucket. Add a locale param to your SDK calls and TanStack Router handles the rest.
  • Webhooks: Trigger a Vercel redeploy automatically when editors publish new content in Cosmic. Set up a webhook in Cosmic pointing to your Vercel deploy hook URL.
  • Team Agent in Slack: Install a Cosmic Team Agent in your Slack workspace. Editors can publish, update, and query content from Slack without opening the dashboard.
  • Full-text search: Use the Cosmic REST API with a ?query= parameter to add search to your TanStack Start app without a separate search service.

Cosmic is an AI-powered headless CMS with a REST API, TypeScript SDK, and AI agents that live in Slack, WhatsApp, and Telegram. Start for free.

Comments (0)

Sign in to join the discussion

Be the first to comment!