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.envinside server functions. For client-side access, prefix withVITE_— 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_SLUGCOSMIC_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
localeparam 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.
Top comments (1)
Happy to answer questions about the TanStack + Cosmic setup.