
Cosmic AI
April 21, 2026
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:
- Click Create New Bucket
- Name it
astro-blog(or anything you like) - Go to Object Types and create a new type called
blog-posts - Add these metafields:
title(Text) — the post headlinecontent(Markdown) — the post bodyexcerpt(Textarea) — short summarycover_image(File, image only) — featured imagepublished_date(Date)
- 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 pixelsfit=crop: crops to the exact dimensionsauto=format: serves WebP to browsers that support it, JPEG/PNG as fallbackauto=compress: applies lossy compression automaticallyq: 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');
Related Posts
// 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:
- Add a newsletter signup form that stores leads as Objects in Cosmic via the Write API
- Set up a Cosmic Content Agent to draft blog posts on a schedule and publish them automatically
- Use the Cosmic MCP Server to manage your content model from Cursor or Claude
- Add Cosmic's AI image generation to auto-generate cover images for new posts
Summary
| Concept | What to Use |
|---|---|
| Install SDK | npm install @cosmicjs/sdk |
| Create client | createBucketClient({ bucketSlug, readKey }) |
| Fetch many | .objects.find({ type }) |
| Fetch one | .objects.findOne({ type, slug }) |
| Static routes | getStaticPaths() + .find() |
| SSR routes | .findOne() in component frontmatter |
| Images | imgix_url + Imgix params |
| Filtering | MongoDB-style operators ($gte, $in, $regex) |
| REST API | https://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?







