To build an Astro blog with a headless CMS, fetch published posts during the build and generate each article through getStaticPaths(). With genko, the SDK's getAll() method retrieves the complete collection, while a webhook receiver can trigger a new build when content changes.

This article is written by the genko.me team. It follows the genko Astro integration guide and SDK source checked on October 20, 2026. The implementation below uses static generation; previewing drafts requires an additional server-rendered route.

Should an Astro blog use Markdown or a headless CMS?

Start with who will write and approve articles. A developer-maintained blog can keep its posts beside its application code. A team with editors who work in a browser may prefer to separate the content workspace from the website repository.

Approach

A practical fit

How published changes reach readers

Local Markdown with Content Collections

Writers are comfortable reviewing changes in Git

Build after a repository change

CMS content fetched through the SDK

Editors publish in a browser

Build after a CMS change

CMS content rendered on demand

Content needs request-time retrieval

Server fetching and your cache policy

This tutorial calls the SDK directly. Astro Content Collections are another way to organize local or remote content, but they are not a prerequisite for this integration. We do not assume a genko-specific content loader exists. Consider a custom loader when central collection validation or multiple content sources justify the additional integration code.

Prepare your Astro project and CMS schema

  1. Start with an existing Astro project.

  2. Create a workspace at genko.me and a list API with the endpoint blog.

  3. Add a text field named title and a rich-text field named body.

  4. Save and publish at least one article.

  5. Issue an API key that permits reads from blog, then set GENKO_API_KEY in the build environment.

The quickstart covers workspace setup. Replace your-workspace throughout the example with your actual workspace identifier.

bash
npm install @genko-me/sdk

Keep the key in a private build variable. A PUBLIC_ variable is inappropriate here because the key is not needed by browser JavaScript. For a fully static website, the machine serving the finished files does not need access to the CMS credential.

Define a typed genko client

Create these two files. The TypeScript definitions describe an existing CMS schema; they do not create fields in the editor. Match the field identifiers and types before testing the connection.

ts
// src/lib/schema.tsimport { listApi } from "@genko-me/sdk";
export type Blog = { title: string; body: string };export const apis = {  blog: listApi<Blog>({    fields: [      { id: "title", type: "text" },      { id: "body", type: "richtext" },    ],  }),};
ts
// src/lib/genko.tsimport { createGenko } from "@genko-me/sdk";import { apis } from "./schema";
const apiKey = import.meta.env.GENKO_API_KEY;if (!apiKey) throw new Error("GENKO_API_KEY is required");export const client = createGenko({  workspace: "your-workspace",  apiKey,  apis,}).apis;

This client deliberately reads its credential at build time. If you later add an on-demand preview route, use the runtime secret mechanism supported by your deployment adapter instead. The Astro tutorial includes a Node-server approach.

Keep the client out of browser scripts and hydrated UI components. A browser can receive the post title and rendered article without receiving the credential used to fetch them.

Generate the Astro blog index

astro
---// src/pages/blog/index.astroimport { client } from "../../lib/genko";const posts = await client.blog.getAll({ orders: "-publishedAt" });---<html lang="en">  <head><title>Blog</title></head>  <body>    <h1>Blog</h1>    <ul>      {posts.map(post => (        <li><a href={`/blog/${post.id}`}>{post.title}</a></li>      ))}    </ul>    {posts.length === 0 && <p>No published posts.</p>}  </body></html>

The SDK paginates internally when getAll() runs. That matters because a blog which works with a few test posts can silently omit older articles if its route generation only reads the first list response.

For a small blog, a complete index is straightforward. As the archive grows, split the listing into pages so that the index itself does not become unnecessarily large. Use list() when you want explicit control over the requested page rather than fetching the whole archive for every listing.

If you optimize a list request with fields, id is always returned, so links still work. Do not apply a title-only selection to the detail generation below: its page props need the full article body.

Generate article pages with getStaticPaths

astro
---// src/pages/blog/[id].astroimport { client } from "../../lib/genko";
export async function getStaticPaths() {  const posts = await client.blog.getAll();  return posts.map(post => ({    params: { id: post.id },    props: { post },  }));}const { post } = Astro.props;---<html lang="en">  <head><title>{post.title}</title></head>  <body>    <article>      <h1>{post.title}</h1>      <div set:html={post.body} />    </article>  </body></html>

Each returned identifier becomes a generated detail page. Passing the post through props avoids making a second request for the same content while rendering that page.

The rich-text field is delivered as HTML sanitized when it is saved in genko. The example renders that field using set:html. This directive does not sanitize arbitrary HTML from unrelated sources; treat any additional HTML input according to its own trust boundary.

The generated route set is a snapshot. Adding a post in the CMS does not create a new file on your static host. Removing a post does not remove an old deployed file until the replacement build is deployed. Test both operations before handing the workflow to an editor.

Handle images in an Astro CMS blog

Begin by displaying images already embedded in the rich-text body and constraining their maximum width in your article styles. If you need a separate cover image, add that actual field to the CMS schema and your TypeScript definition first.

The SDK can construct a transformed URL for an uploaded media image:

ts
import { buildImageUrl } from "@genko-me/sdk";
const imageUrl = buildImageUrl(  "https://media.genko.me/your-image.jpg",  { w: 960, format: "webp", fit: "inside" },);

Replace the sample URL with a real uploaded media URL. This code constructs a URL; it does not generate an image file or upload an asset. Use the result as an image source, supply appropriate alternative text, and choose dimensions for the actual placement.

Changing a cover-image URL does not rewrite every image embedded in an article's HTML. Review body images separately, especially when an editor uploads a large original. Also verify the result at narrow screen widths and reserve layout space where the image dimensions are known.

Rebuild an Astro site after CMS updates

  1. Identify how your hosting environment starts a build.

  2. Add a public HTTPS receiver that verifies signed CMS notifications.

  3. Register its URL and shared secret on the blog API in genko.

  4. Queue the build from the receiver and acknowledge the notification promptly.

  5. Exercise article creation, updates, removal, and publication ending.

A genko webhook sends a JSON POST. Do not assume a hosting provider's deploy hook accepts that payload or supports genko's signature scheme. A small receiver can bridge the two systems. The webhook guide documents verifyGenkoSignature() and the notification shape.

Manual publication arrives through creation or update events, so listening only for entry.published misses that workflow. An update that turns a published article into a draft can also require removing its previous static page. Make the rebuild decision around what changed in the published site, including removals.

For repeated saves, consider combining pending rebuild requests rather than launching a build for every click. Keep a manual rebuild path for failed notifications, and inspect the finished deployment rather than treating successful webhook receipt as proof that the site has changed.

Can a static Astro blog preview CMS drafts?

A static HTML file cannot fetch a different draft for each incoming draftKey. Keep public pages static and add a separate preview route using an adapter that supports on-demand rendering.

That route should retrieve a saved draft on the server, avoid shared caching, and provide metadata that excludes it from indexing. Never include draft content in the public build just to make previewing easier. Follow the Astro preview tutorial for the server-side configuration.

Check the complete publishing workflow

Run npm run build, then npm run preview. Open the index and follow links to every generated detail page. Check the title, canonical URL, article images, an empty archive, and the response for a removed article.

For production, decide what happens when the CMS cannot be reached during a build. A failed content fetch should not quietly replace the site with an empty archive. Preserve the last successful deployment and make the failed build visible to the person responsible for publishing.

The related static-site CMS guide discusses update workflows. For request-time alternatives, see the Nuxt guide and SvelteKit guide.

FAQ

Does an Astro blog need a headless CMS?

No. Markdown can work well when writers already use Git; a CMS is useful when contributors need a browser-based editing workflow.

Why is my Astro blog still showing old CMS content?

In this SSG setup, readers receive the last deployed build. Check that the content change triggered a successful build and deployment, then examine any cache serving an older result.

Should I use getAll or list for an Astro blog?

Use getAll() to collect the articles needed for complete route generation. Use list() when building a paginated listing with explicit limits and offsets.

Does the deployed static website need my CMS API key?

The fully static implementation needs the key during the build only. Any additional route that fetches CMS content at request time needs its own server-side secret configuration.

Sources

Official sources checked on October 20, 2026.