DEV Community

Cover image for Building a Static Site with Astro and Cosmic CMS
Tony Spiro
Tony Spiro

Posted on • Originally published at cosmicjs.com

Building a Static Site with Astro and Cosmic CMS

Astro's HTML-first philosophy is having a moment. A post on Hacker News this week about doubling user growth by going HTML-first cracked the top 10 with 383 points, and the replies were full of developers reconsidering their stacks.

The short version: ship less JavaScript, get faster sites, keep more visitors. Astro is purpose-built for exactly this. And when you pair it with Cosmic as your content layer, you get a stack that is fast to build, fast to run, and easy for non-developers to manage content without touching code.

This tutorial walks through building a production-ready blog with Astro and Cosmic from scratch.

Why Astro + Cosmic

Astro generates zero-JavaScript HTML by default. Every page is a static file until you opt into interactivity with islands. That means:

  • Lighthouse scores in the high 90s out of the box
  • No hydration overhead for content pages
  • Content editors can update copy in Cosmic without a developer involved

Cosmic delivers content via REST API and the @cosmicjs/sdk. Your Astro build fetches content at build time, bakes it into static HTML, and serves it from a CDN. Fast, structured, and zero runtime CMS overhead.

Prerequisites

  • Node.js 18+
  • A free Cosmic account (sign up here)
  • Familiarity with TypeScript basics

Step 1: Create Your Cosmic Bucket

Log in to Cosmic and create a new bucket. Then create a blog-posts Object Type with these metafields:

  • title (text)
  • slug (text, unique)
  • published_date (date)
  • teaser (textarea)
  • content (markdown)
  • image (file, image)

Add a few sample posts so you have content to render.

Step 2: Scaffold the Astro Project

npm create astro@latest cosmic-astro-blog
cd cosmic-astro-blog
npm install @cosmicjs/sdk
Enter fullscreen mode Exit fullscreen mode

Choose the "Empty" starter when prompted. We'll wire up everything manually so you understand each piece.

Step 3: Initialize the Cosmic Client

Create 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,
});
Enter fullscreen mode Exit fullscreen mode

Add your credentials to .env:

COSMIC_BUCKET_SLUG=your-bucket-slug
COSMIC_READ_KEY=your-read-key
Enter fullscreen mode Exit fullscreen mode

You'll find both values in your Cosmic bucket under Settings > API Keys.

Step 4: Fetch Posts at Build Time

Create src/pages/blog/index.astro:

---
import { cosmic } from '../../lib/cosmic';

const { objects: posts } = await cosmic.objects
  .find({ type: 'blog-posts' })
  .props('id,title,slug,metadata.teaser,metadata.published_date,metadata.image')
  .sort('-metadata.published_date');
---

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Blog</title>
  </head>
  <body>
    <h1>Blog</h1>
    <ul>
      {posts.map((post) => (
        <li>
          <a href={`/blog/${post.slug}`}>
            <h2>{post.title}</h2>
            <p>{post.metadata.teaser}</p>
          </a>
        </li>
      ))}
    </ul>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Step 5: Generate Individual Post Pages

Create src/pages/blog/[slug].astro for dynamic routing:

---
import { cosmic } from '../../lib/cosmic';

export async function getStaticPaths() {
  const { objects: posts } = await cosmic.objects
    .find({ type: 'blog-posts' })
    .props('slug');

  return posts.map((post) => ({
    params: { slug: post.slug },
  }));
}

const { slug } = Astro.params;

const { object: post } = await cosmic.objects
  .findOne({ type: 'blog-posts', slug })
  .props('title,metadata');
---

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>{post.title}</title>
  </head>
  <body>
    <article>
      <h1>{post.title}</h1>
      {post.metadata.image && (
        <img
          src={`${post.metadata.image.imgix_url}?w=1200&auto=format`}
          alt={post.title}
          width="1200"
          height="630"
        />
      )}
      <div set:html={post.metadata.content} />
    </article>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Astro's getStaticPaths runs at build time, fetches all post slugs from Cosmic, and generates one HTML file per post. Zero runtime API calls.

Step 6: Handle Images with imgix

Cosmic stores all media on imgix, which means you get URL-based image transformations for free. Append query params to any image URL:

// Resize and auto-format for modern browsers
const heroUrl = `${post.metadata.image.imgix_url}?w=1200&h=630&fit=crop&auto=format`;

// Thumbnail
const thumbUrl = `${post.metadata.image.imgix_url}?w=400&h=300&fit=crop&auto=format`;

// WebP with fallback
const webpUrl = `${post.metadata.image.imgix_url}?w=800&fm=webp&q=80`;
Enter fullscreen mode Exit fullscreen mode

No image processing pipeline needed. imgix handles it at the CDN edge.

Step 7: Deploy to Vercel

npm install -g vercel
vercel
Enter fullscreen mode Exit fullscreen mode

Add your environment variables in the Vercel dashboard under Settings > Environment Variables:

  • COSMIC_BUCKET_SLUG
  • COSMIC_READ_KEY

Set up a deploy webhook in Cosmic (Settings > Webhooks) so every content publish triggers a new Vercel build. Your editors publish in Cosmic, the site rebuilds automatically, no developer involvement required.

Step 8: Enable Incremental Builds (Optional)

For larger sites, Astro's content collections combined with Vercel's ISR let you rebuild only changed pages. Configure in astro.config.mjs:

import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';

export default defineConfig({
  output: 'hybrid',
  adapter: vercel(),
});
Enter fullscreen mode Exit fullscreen mode

Then mark individual pages for server-side rendering when you need fresh data, while keeping everything else static.

Performance Baseline

A default Astro + Cosmic blog with no optimization effort typically scores:

  • Performance: 95-100
  • Accessibility: 90+
  • Best Practices: 100
  • SEO: 90+

The main lever is images. Use imgix transforms and the loading="lazy" attribute on below-the-fold images and you'll stay in the green across the board.

Adding an AI Agent (Optional)

Once your Cosmic bucket has content, you can add a Cosmic AI agent to manage it. From the Cosmic dashboard, connect an agent to your bucket. It can draft new posts, update metadata, generate featured images, and publish on a schedule. Your Astro site rebuilds automatically via webhook every time the agent publishes.

This is what the HTML-first HN crowd is discovering: a static frontend plus an API-first CMS is the right architecture for AI-assisted content workflows. The agent writes to the CMS API. The static site rebuilds. No CMS logic in your frontend, no CMS vendor in your runtime.

What's Next

  • Add a search index with Pagefind (runs at build time, zero runtime JS)
  • Add RSS with @astrojs/rss
  • Add a sitemap with @astrojs/sitemap
  • Add an MCP server so AI assistants like Claude and Cursor can read and write your Cosmic content directly

Get Started

Free Cosmic plan includes 1 bucket, 1,000 objects, and 2 team members, enough to build and launch a full blog.

Start building for free or book a demo with Tony if you want to talk through your architecture.


Questions? Join the Cosmic Discord or check the Astro + Cosmic docs.

Top comments (1)

Collapse
 
hayrullahkar profile image
Hayrullah Kar

The HTML-first philosophy of Astro paired with a headless layer like Cosmic is the ultimate stack for modern web performance. Fetching content at build time and offloading image optimization directly to the imgix CDN edge is brilliant. Zero runtime overhead and effortless deployment workflows. Dropping a star to this stack!