VOOZH about

URL: https://dev.to/tonyspiro/headless-cms-for-tanstack-start-build-a-blog-with-cosmic-k6f

⇱ Headless CMS for TanStack Start: Build a Blog with Cosmic - DEV Community


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.