Use Cases (Updated: 6/2/2026)

Building Astro Sites with Claude Code: Content Collections, SSG, and Validation

Use Claude Code to build Astro content sites with current collections, pages, tags, hydration, and build checks.

Building Astro Sites with Claude Code: Content Collections, SSG, and Validation

Why Claude Code works well with Astro

Astro is a strong choice for blogs, documentation, portfolios, product pages, and other sites where most pages are meant to be read. You can still use React, Vue, Svelte, or plain Astro components, but Astro does not force every page to ship a large client-side app. Its island architecture means static content can stay static, while only the pieces that need interaction are hydrated in the browser.

Claude Code helps most when you ask it to understand the project before editing. An Astro site is not just one page file. The shape of astro.config.mjs, src/content.config.ts, src/pages/, src/components/, images, routes, and build commands all affect the final result. If you let Claude Code inspect those files first, it can propose a smaller implementation plan and avoid unnecessary libraries.

This guide uses current Astro content collection patterns, including src/content.config.ts, astro/loaders, and astro/zod. Check the primary docs when you apply this to your own project: Claude Code Quickstart, Astro Content Collections, Astro template directives, and Astro routing reference. For broader architecture choices, pair this with the SSR/SSG comparison and SEO optimization guide.

Start with a narrow project brief

The first failure mode is giving Claude Code a vague prompt such as “make an Astro blog.” That can lead to unrelated CMS choices, extra state management, or a design-heavy rewrite before the content model is stable. Give it a concrete scope: content site, static output, MDX support, sitemap, typed frontmatter, article list, tag pages, and build verification.

npm create astro@latest my-astro-site
cd my-astro-site
npx astro add mdx sitemap tailwind
npm run dev

After the project exists, ask Claude Code to read before editing.

claude "Review this Astro project as a content site. Read astro.config.mjs, src/content.config.ts, src/pages, and src/components. Before editing, explain the implementation plan and the commands you will use to verify it."

That extra sentence matters. Astro development is easy to over-broaden because pages, content collections, images, and navigation all connect. You want Claude Code to describe the route it will take, then make a contained change.

The base config can stay small:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import tailwind from '@astrojs/tailwind';

export default defineConfig({
  site: 'https://example.com',
  output: 'static',
  integrations: [mdx(), sitemap(), tailwind()],
  markdown: {
    shikiConfig: {
      theme: 'github-dark',
    },
  },
});

Set site to your production domain before publishing. Sitemap and canonical URL generation depend on it, and leaving an example domain in place weakens SEO checks later.

Define Content Collections the current way

Content Collections turn Markdown and MDX files into typed data. Instead of hoping every article has the same frontmatter, you define a schema and let Astro catch missing or malformed fields during development. This is especially useful for multilingual sites, where translators can accidentally change frontmatter while editing body text.

// src/content.config.ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const blog = defineCollection({
  loader: glob({
    base: './src/content/blog',
    pattern: '**/*.{md,mdx}',
  }),
  schema: z.object({
    title: z.string().max(80),
    description: z.string().max(120),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
    heroImage: z.string().optional(),
  }),
});

export const collections = { blog };

Ask Claude Code to review the schema against the installed Astro version:

claude "Review src/content.config.ts for Astro 5. Check whether it uses the current Content Collections API, validates descriptions, excludes drafts cleanly, and handles optional updatedDate without breaking existing posts."

The practical benefit is not just type safety. This schema gives you a clear publishing contract. A post without a title, a too-long description, an invalid date, or a broken tags array is caught before deployment.

Build the paginated article list

Use case one is the normal blog index. The important detail is sorting. Do not rely on the order returned by getCollection(). Sort by pubDate, filter drafts, and pass the entries to a small card component.

---
// src/pages/blog/[...page].astro
import { getCollection } from 'astro:content';
import PostCard from '../../components/PostCard.astro';

export async function getStaticPaths({ paginate }) {
  const posts = (await getCollection('blog', ({ data }) => data.draft !== true))
    .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

  return paginate(posts, { pageSize: 12 });
}

const { page } = Astro.props;
---

<section class="mx-auto max-w-5xl px-4 py-10">
  <h1 class="text-3xl font-bold">Blog</h1>
  <div class="mt-8 grid gap-6 md:grid-cols-2">
    {page.data.map((post) => <PostCard post={post} />)}
  </div>
  <nav class="mt-10 flex justify-between">
    {page.url.prev ? <a href={page.url.prev}>Previous</a> : <span />}
    {page.url.next ? <a href={page.url.next}>Next</a> : <span />}
  </nav>
</section>

Here is a copy-pasteable card component:

---
// src/components/PostCard.astro
import type { CollectionEntry } from 'astro:content';

type Props = {
  post: CollectionEntry<'blog'>;
  lang?: string;
};

const { post, lang = 'en' } = Astro.props;
const href = lang === 'ja' ? `/blog/${post.id}/` : `/${lang}/blog/${post.id}/`;
const date = post.data.updatedDate ?? post.data.pubDate;
---

<article class="rounded border border-slate-200 p-5">
  <p class="text-sm text-slate-500">
    <time datetime={date.toISOString()}>{date.toLocaleDateString('en-US')}</time>
  </p>
  <h2 class="mt-2 text-xl font-semibold">
    <a href={href}>{post.data.title}</a>
  </h2>
  <p class="mt-3 text-slate-700">{post.data.description}</p>
  <ul class="mt-4 flex flex-wrap gap-2">
    {post.data.tags.map((tag) => <li class="rounded bg-slate-100 px-2 py-1 text-xs">{tag}</li>)}
  </ul>
</article>

This pattern fits at least three real use cases: a personal engineering blog, an internal knowledge base, and a product changelog. In all three, the content model is more important than decorative layout. Ask Claude Code to preserve date, title, description, and tags before it touches styling.

Add tag pages and hydrate only what needs JavaScript

Use case two is a tag archive. The pitfall is using raw tag labels as URL parameters. Tags can contain spaces, uppercase letters, or non-Latin characters. Keep a URL slug and a display label separately.

---
// src/pages/tags/[tag]/[...page].astro
import { getCollection } from 'astro:content';
import PostCard from '../../../components/PostCard.astro';

const tagSlug = (tag) =>
  encodeURIComponent(tag.toLowerCase().trim().replace(/\s+/g, '-'));

export async function getStaticPaths({ paginate }) {
  const posts = await getCollection('blog', ({ data }) => data.draft !== true);
  const groups = new Map();

  for (const post of posts) {
    for (const label of post.data.tags) {
      const slug = tagSlug(label);
      const group = groups.get(slug) ?? { slug, label, posts: [] };
      group.posts.push(post);
      groups.set(slug, group);
    }
  }

  return [...groups.values()].flatMap((group) =>
    paginate(group.posts, {
      params: { tag: group.slug },
      props: { tag: group.label },
      pageSize: 10,
    }),
  );
}

const { page, tag } = Astro.props;
---

<section class="mx-auto max-w-5xl px-4 py-10">
  <h1 class="text-3xl font-bold">Tag: {tag}</h1>
  <div class="mt-8 grid gap-6 md:grid-cols-2">
    {page.data.map((post) => <PostCard post={post} />)}
  </div>
</section>

Use case three is search. Do not turn the whole page into a client-side app. Keep static content in Astro and hydrate only the interactive component.

---
// src/pages/index.astro
import Hero from '../components/Hero.astro';
import SearchBox from '../components/SearchBox.tsx';
import LatestPosts from '../components/LatestPosts.astro';
---

<Hero />
<SearchBox client:visible />
<LatestPosts />

client:load is for UI that must be interactive immediately. client:visible is better when the user can wait until the component enters the viewport. Ask Claude Code to explain why a directive is needed before adding it.

Pitfalls to avoid

The first pitfall is copying old Content Collections examples. If a tutorial uses src/content/config.ts or older collection options, compare it with the current Astro docs and your installed version before letting Claude Code apply it.

The second pitfall is a huge prompt. “Improve the whole site” is too broad. Use smaller requests: create the tag page, add the post card, validate frontmatter, then run the build.

The third pitfall is forgetting images and social previews. A content site needs a stable heroImage, meaningful descriptions, and clean Open Graph output. For search-specific work, use the internal SEO optimization guide.

The fourth pitfall is treating a successful build as the only quality bar. You still need to check pagination, internal links, external links, mobile rendering, and the next action you want readers to take. The daily publishing checklist is a good follow-up when the site grows.

Verification and monetization path

Run the checks before publishing:

npm run build
npx astro check
npm run preview

If the build fails, make Claude Code classify the error first.

claude "Read the npm run build failure. Classify it as Astro syntax, Content Collections schema, MDX code fence, route generation, or link issue. Then propose the smallest fix."

For ClaudeCodeLab-style content, the revenue path should feel useful rather than forced. Beginners can start with the free cheat sheet, template-focused readers can browse products, and teams that want Astro publishing, review rules, and Claude Code adoption can use training and consultation. The CTA should match the unresolved problem: learning commands, applying templates, or designing a team workflow.

What I verified in practice

When applying this workflow, Claude Code was fast at generating .astro files and connecting pages to Content Collections. The places that needed human review were current Astro API usage, tag URL encoding, overuse of client:load, and build verification. The stable workflow was: let Claude Code read the project, constrain the edit, ask for current-doc alignment, run the build, then review the rendered page. That produces better Astro sites than asking the agent to redesign everything in one pass.

#Claude Code #Astro #framework #SSG #frontend
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.