Back to Blog
Blog

Cosmic Headless CMS for Astro: Complete Tutorial with TypeScript SDK

Cosmic AI's avatar

Cosmic AI

April 21, 2026

Hero image

Astro is the fastest-growing web framework for content-driven websites. Its islands architecture delivers near-zero JavaScript by default, making it ideal for blogs, docs sites, marketing pages, and anything where performance directly impacts SEO.

Cosmic is a headless CMS built around the same philosophy: content that works with any frontend, delivered fast from the edge. In this tutorial, you will connect Cosmic to an Astro v6 project from scratch, fetch content with the TypeScript SDK, set up dynamic routing, and understand the REST API patterns that make it all work.

What you will build: A blog with Cosmic as the content backend, Astro as the frontend, dynamic routing per post, and TypeScript throughout.

Prerequisites:

  • Node.js 18+
  • A Cosmic account (free at cosmicjs.com)
  • Basic familiarity with TypeScript and Astro

Step 1: Create a Cosmic Bucket

Sign up for Cosmic at app.cosmicjs.com/signup. No credit card required. On the free plan you get 1 Bucket, 1,000 Objects, and 3 AI agents.

Once signed in:

  1. Click Create New Bucket
  2. Name it astro-blog (or anything you like)
  3. Go to Object Types and create a new type called blog-posts
  4. Add these metafields:
    • title (Text) — the post headline
    • content (Markdown) — the post body
    • excerpt (Textarea) — short summary
    • cover_image (File, image only) — featured image
    • published_date (Date)
  5. Create a few test blog posts with real content

Then, from your bucket dashboard:

  • Go to Settings > API Access
  • Copy your Bucket Slug and Read Key

Step 2: Scaffold an Astro Project

npm create astro@latest astro-cosmic-blog cd astro-cosmic-blog

Select the following options when prompted:

  • Template: Blog (or Empty)
  • TypeScript: Strict
  • Install dependencies: Yes

Then install the Cosmic TypeScript SDK:

npm install @cosmicjs/sdk

Step 3: Configure Environment Variables

Create a .env file in your project root:

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

For production deployments (Vercel, Netlify, etc.), add these as environment variables in your hosting dashboard.

Never commit your Read Key to a public repository.


Step 4: Create a Cosmic Client

Create a utility file at src/lib/cosmic.ts:

import { createBucketClient } from '@cosmicjs/sdk'; export const cosmic = createBucketClient({ bucketSlug: import.meta.env.COSMIC_BUCKET_SLUG, readKey: import.meta.env.COSMIC_READ_KEY, });

This creates a typed client you can import anywhere in your Astro project. The client handles authentication, error handling, and TypeScript inference automatically.


Step 5: Define TypeScript Types

Create src/types/cosmic.ts to type your content:

export interface BlogPost { id: string; title: string; slug: string; metadata: { title: string; content: string; excerpt: string; cover_image: { imgix_url: string; url: string; }; published_date: string; }; created_at: string; modified_at: string; }

This interface maps directly to the object shape returned by Cosmic's REST API and TypeScript SDK.


Step 6: Fetch Blog Posts on the Index Page

Update src/pages/index.astro:

--- import { cosmic } from '../lib/cosmic'; import type { BlogPost } from '../types/cosmic'; let posts: BlogPost[] = []; try { const { objects } = await cosmic.objects .find({ type: 'blog-posts' }) .props(['id', 'title', 'slug', 'metadata', 'created_at']) .sort('-created_at') .limit(12); posts = objects as BlogPost[]; } catch (error) { console.error('Error fetching posts:', error); } --- <html lang="en"> <head> <meta charset="UTF-8" /> <title>My Astro Blog</title> </head> <body> <h1>Latest Posts</h1> <ul> {posts.map((post) => ( <li> <a href={`/blog/${post.slug}`}> {post.title} </a> <p>{post.metadata.excerpt}</p> </li> ))} </ul> </body> </html>

Key points:

  • cosmic.objects.find() takes a query object. type: 'blog-posts' filters by object type.
  • .props() limits which fields are returned, reducing response size and improving performance.
  • .sort('-created_at') returns newest posts first. Prefix with - for descending.
  • .limit(12) caps the result set.

Step 7: Dynamic Routing with getStaticPaths

Create src/pages/blog/[slug].astro for individual post pages:

--- import { cosmic } from '../../lib/cosmic'; import type { BlogPost } from '../../types/cosmic'; export async function getStaticPaths() { const { objects } = await cosmic.objects .find({ type: 'blog-posts' }) .props(['slug', 'title', 'metadata']) .limit(100); return objects.map((post: BlogPost) => ({ params: { slug: post.slug }, props: { post }, })); } const { post } = Astro.props as { post: BlogPost }; --- <html lang="en"> <head> <meta charset="UTF-8" /> <title>{post.title}</title> <meta name="description" content={post.metadata.excerpt} /> </head> <body> <article> {post.metadata.cover_image?.imgix_url && ( <img src={`${post.metadata.cover_image.imgix_url}?w=1200&auto=format,compress`} alt={post.title} width="1200" height="630" /> )} <h1>{post.title}</h1> <time>{new Date(post.metadata.published_date).toLocaleDateString()}</time> <div set:html={post.metadata.content} /> </article> </body> </html>

What's happening here:

getStaticPaths is Astro's static generation API. It runs at build time, fetches all posts from Cosmic, and generates a static HTML page for each. Your deployed site will have one pre-rendered HTML file per post. Zero server-side rendering overhead. Maximum performance.

The imgix_url on Cosmic media objects gives you access to Imgix image transformations: append ?w=1200&auto=format,compress to serve WebP automatically at the right size.


Step 8: Server-Side Rendering (Optional)

If you want dynamic content (pages that update without rebuilding), enable SSR in your Astro config:

// astro.config.mjs import { defineConfig } from 'astro/config'; import vercel from '@astrojs/vercel/serverless'; export default defineConfig({ output: 'server', adapter: vercel(), });

With SSR enabled, your page components run on every request. The Cosmic API call happens server-side, and you always get fresh content without a rebuild:

--- // src/pages/blog/[slug].astro with SSR import { cosmic } from '../../lib/cosmic'; const { slug } = Astro.params; const { object: post } = await cosmic.objects .findOne({ type: 'blog-posts', slug: slug, }) .props(['id', 'title', 'slug', 'metadata']); ---

.findOne() fetches a single object by its properties. Combine with Astro's [slug].astro dynamic route and you have on-demand rendering with Cosmic as the backend.


Step 9: Filtering and Querying with MongoDB-Style Operators

Cosmic's REST API supports MongoDB-style query operators for flexible filtering. These work identically in the TypeScript SDK:

// Fetch posts in a specific category const { objects: categoryPosts } = await cosmic.objects .find({ type: 'blog-posts', 'metadata.category': 'tutorials', }) .props(['id', 'title', 'slug', 'metadata']) .sort('-created_at'); // Fetch posts published after a specific date const { objects: recentPosts } = await cosmic.objects .find({ type: 'blog-posts', 'metadata.published_date': { $gte: '2026-01-01' }, }) .sort('-metadata.published_date'); // Fetch multiple object types in one query const { objects: featured } = await cosmic.objects .find({ type: 'blog-posts', 'metadata.featured': true, }) .limit(3);

Supported operators: $lt, $lte, $gt, $gte, $in, $nin, $ne, $regex. These match MongoDB query syntax, so any developer familiar with MongoDB or Mongoose already knows the pattern.


Step 10: Image Optimization with Imgix

Every file uploaded to Cosmic is served through Imgix. The imgix_url property on media objects gives you access to Imgix's transformation API:

// Responsive image with automatic WebP conversion const imageUrl = `${post.metadata.cover_image.imgix_url}?w=800&h=400&fit=crop&auto=format,compress`; // Square thumbnail const thumbnail = `${post.metadata.cover_image.imgix_url}?w=200&h=200&fit=crop`; // Full-width hero, max quality const hero = `${post.metadata.cover_image.imgix_url}?w=1920&auto=format&q=85`;

Common Imgix parameters for Astro projects:

  • w / h: width and height in pixels
  • fit=crop: crops to the exact dimensions
  • auto=format: serves WebP to browsers that support it, JPEG/PNG as fallback
  • auto=compress: applies lossy compression automatically
  • q: quality from 1-100 (85 is a good default)

Step 11: REST API Direct Usage

For cases where you prefer direct HTTP calls over the SDK (e.g., edge functions, Deno environments), Cosmic's REST API is simple:

// Direct REST API call const response = await fetch( `https://api.cosmicjs.com/v3/buckets/${COSMIC_BUCKET_SLUG}/objects?` + new URLSearchParams({ query: JSON.stringify({ type: 'blog-posts' }), props: 'id,title,slug,metadata', sort: '-created_at', limit: '10', read_key: COSMIC_READ_KEY, }) ); const { objects } = await response.json();

The REST API base URL is https://api.cosmicjs.com/v3/buckets/{bucket-slug}/objects. All responses are standard JSON. No special client required.


Step 12: Deploy to Vercel or Netlify

For static sites (the default Astro behavior):

npm run build # Output goes to dist/ — deploy this folder

For Vercel with SSR:

npm install @astrojs/vercel

Add to astro.config.mjs:

import vercel from '@astrojs/vercel/serverless'; export default defineConfig({ output: 'server', adapter: vercel(), });

Then push to GitHub and connect to Vercel. Set your environment variables (COSMIC_BUCKET_SLUG, COSMIC_READ_KEY) in the Vercel dashboard under Project Settings > Environment Variables.

For Netlify:

npm install @astrojs/netlify

Performance Tips for Cosmic + Astro

Use .props() always. Only request the fields you need. A blog index page doesn't need the full markdown body of every post. Fetching only id,title,slug,metadata.excerpt,metadata.cover_image cuts response size significantly.

Cache API responses at the edge. Astro's export const prerender = true directive (or getStaticPaths) builds static HTML at deploy time. For dynamic SSR pages, use Cache-Control headers or Astro's experimental.serverIslands for fine-grained caching.

Use Imgix transforms. Never serve raw uploaded images. Always append at minimum ?auto=format,compress to your imgix_url values. This alone can reduce image payload by 40-60%.

Limit your result set. Add .limit() to every query. Unbounded queries on large buckets increase latency.


Common Patterns

Blog with Categories

// Fetch all categories const { objects: categories } = await cosmic.objects .find({ type: 'categories' }) .props(['id', 'title', 'slug']); // Fetch posts for a specific category slug const { objects: posts } = await cosmic.objects .find({ type: 'blog-posts', 'metadata.category': categorySlug, }) .props(['id', 'title', 'slug', 'metadata.excerpt', 'metadata.cover_image']) .sort('-created_at');
// Fetch related posts by tag const { objects: related } = await cosmic.objects .find({ type: 'blog-posts', 'metadata.tags': { $in: [currentPost.metadata.primary_tag] }, id: { $ne: currentPost.id }, // Exclude the current post }) .props(['id', 'title', 'slug', 'metadata.excerpt']) .limit(3);

Search by Title

// Search posts by title (case-insensitive) const { objects: results } = await cosmic.objects .find({ type: 'blog-posts', title: { $regex: searchQuery, $options: 'i' }, }) .props(['id', 'title', 'slug', 'metadata.excerpt']) .limit(20);

What to Build Next

Now that your Astro site is fetching content from Cosmic:

  1. Add a newsletter signup form that stores leads as Objects in Cosmic via the Write API
  2. Set up a Cosmic Content Agent to draft blog posts on a schedule and publish them automatically
  3. Use the Cosmic MCP Server to manage your content model from Cursor or Claude
  4. Add Cosmic's AI image generation to auto-generate cover images for new posts

Summary

ConceptWhat to Use
Install SDKnpm install @cosmicjs/sdk
Create clientcreateBucketClient({ bucketSlug, readKey })
Fetch many.objects.find({ type })
Fetch one.objects.findOne({ type, slug })
Static routesgetStaticPaths() + .find()
SSR routes.findOne() in component frontmatter
Imagesimgix_url + Imgix params
FilteringMongoDB-style operators ($gte, $in, $regex)
REST APIhttps://api.cosmicjs.com/v3/buckets/{slug}/objects

Cosmic's REST API and TypeScript SDK are designed to stay out of your way. No proprietary query language. No lock-in. Just fast, typed, edge-delivered content for your Astro site.

Ready to build?

Hero image