Stage 2 of 5

Ship a Site on Cosmic with Astro

6 min readLesson 5 · Stage 2
Ship a Site on Cosmic with Astro

Build a blog on Astro that reads from your Cosmic bucket. Covers the SDK, lean queries with props, static routes with getStaticPaths, and deploying to Vercel.

This lesson builds a blog on Astro that reads from your Cosmic bucket, then deploys it. By the end you will have an index page and individual post pages, generated as static HTML, live on Vercel.

This is a developer lesson. It covers the same ground as the Next.js lesson, so pick whichever framework you are actually using rather than doing both.


Before You Start

  • Node.js 18 or later

  • A Cosmic bucket with a content type holding a few published objects. blog-posts is used throughout; substitute your own slug

  • A body field on that type. New object types come with only a title and a slug, so add a Markdown metafield with the key content, which is what the examples below read

  • Your bucket slug and read key, from Settings > API keys in your bucket

  • A Vercel account for the final step


Step 1: Create the Project

npm create astro@latest my-site cd my-site npm install @cosmicjs/sdk

Take the minimal or empty template when prompted. The blog starter ships with its own content collections setup, which you would then have to unpick.


Step 2: Add Your Keys

Create .env in the project root:

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

No PUBLIC_ prefix. In Astro, variables prefixed with PUBLIC_ are exposed to the browser, and these should not be. Your write key does not belong in this project at all.


Step 3: Create the Client

Add 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, });

Astro reads environment variables through import.meta.env, not process.env. This is the most common porting mistake when moving example code over from a Next.js tutorial.


Step 4: Build the Index Page

Create src/pages/blog/index.astro:

--- import { cosmic } from '../../lib/cosmic'; const { objects: posts } = await cosmic.objects .find({ type: 'blog-posts' }) .props(['title', 'slug', 'metadata']) .sort('-created_at'); --- <html lang="en"> <body> <main> <h1>Blog</h1> <ul> {posts.map((post: any) => ( <li> <a href={`/blog/${post.slug}`}>{post.title}</a> </li> ))} </ul> </main> </body> </html>

Run npm run dev and open /blog.

Everything between the --- fences is the frontmatter script. It runs on the server at build time only, never in the browser, which is why it is safe to use your read key there. The part below renders to HTML.

By default Astro builds this to a static file. The fetch happens once when you build, not when a visitor arrives.


Step 5: Fetch Only What You Render

.props() limits the fields in the response. Without it, every post comes back complete, including metafields the page never displays.

// Everything .find({ type: 'blog-posts' }) // Three fields .find({ type: 'blog-posts' }).props(['title', 'slug', 'metadata'])

Dotted paths let you reach a single metafield rather than the whole object:

.props(['title', 'slug', 'metadata.featured_image'])

A list page usually needs a title, a slug, and an image. Asking for full post bodies you are not rendering is the easiest performance mistake to make and the easiest to fix.


Step 6: Add Dynamic Post Pages

Create src/pages/blog/[slug].astro:

--- import { cosmic } from '../../lib/cosmic'; export async function getStaticPaths() { const { objects } = await cosmic.objects .find({ type: 'blog-posts' }) .props(['title', 'slug', 'metadata']) .depth(1); return objects.map((post: any) => ({ params: { slug: post.slug }, props: { post }, })); } const { post } = Astro.props; --- <html lang="en"> <body> <article> <h1>{post.title}</h1> <div>{post.metadata.content}</div> </article> </body> </html>

metadata.content is the Markdown metafield you added. Both the Markdown and Rich Text metafield types return markdown, so a real site would run it through a markdown renderer and then use set:html. Passing markdown straight to set:html renders the source, asterisks and all.

getStaticPaths is correct here. Astro uses it to enumerate the dynamic routes to build. This is worth flagging because the Next.js App Router uses generateStaticParams instead, and the two are not interchangeable.

The useful trick is props in the returned object. Because getStaticPaths already fetched every post, you can hand the whole post to the page through Astro.props and skip a second lookup per page. One request builds the entire blog.

.depth(1) resolves relationships, so an author reference comes back as the full author object rather than an ID.


A Tip for Empty Results

This catches nearly everyone on a first Cosmic build.

When a query matches nothing, the SDK throws a 404 rather than returning an empty array:

No objects found for your query in bucket 'your-bucket-slug'

Both find() and findOne() do this. A content type with nothing published yet will fail your build rather than produce an empty page.

Guard the calls that can legitimately come back empty:

let posts = []; try { const res = await cosmic.objects .find({ type: 'blog-posts' }) .props(['title', 'slug']); posts = res.objects; } catch { // Nothing published yet }

This bites harder in Astro than in Next.js, because the fetch runs at build time. A 404 from Cosmic fails the whole build rather than one page. If npm run build dies on a Cosmic error, check that the content type actually has published objects in it. Drafts are not returned by default.


Step 7: Deploy

A static Astro build deploys to Vercel with no adapter:

npx vercel

Link the project when prompted. The first build will fail on the content fetch, because your environment variables are not there yet.

Add them:

npx vercel env add COSMIC_BUCKET_SLUG npx vercel env add COSMIC_READ_KEY

Then deploy for real:

npx vercel --prod

Because the site is fully static, publishing new content in Cosmic will not change the live site until you rebuild. That is the tradeoff for serving plain HTML, and the next section covers how to handle it.


Going Further

Two ways to keep a static Astro site current. Simplest is a deploy hook: Vercel gives you a URL that triggers a rebuild, and Cosmic can call it with a webhook when an object is published. Publishing then kicks off a rebuild automatically.

If you need pages that update without any rebuild, install @astrojs/vercel and switch the relevant routes to server rendering. You give up some of the speed of static HTML in exchange for always-current content, so it is worth doing per route rather than site-wide.


Do This in Your Project

  1. Run npm create astro@latest my-site and pick the minimal template

  2. Install the SDK with npm install @cosmicjs/sdk

  3. Put your bucket slug and read key in .env

  4. Add src/lib/cosmic.ts using import.meta.env

  5. Build src/pages/blog/index.astro and confirm your posts list at /blog

  6. Add src/pages/blog/[slug].astro using getStaticPaths, passing the post through props

  7. Deploy with npx vercel, add the two environment variables, then npx vercel --prod

Run npm run build locally before deploying. Build-time fetching means most errors show up there rather than in the browser.


Up Next

Lesson 6: Model Your Content, Metafields and Schema

Lesson 6 goes back to the dashboard and covers how content types and metafields are structured, and how those choices shape the API responses you have been consuming in this lesson.

Up next

Model Your Content: Metafields and Schema