Claude Code for Astro: Static Sites, Content Collections, and Island Architecture — Claude Skills 360 Blog
Blog / Development / Claude Code for Astro: Static Sites, Content Collections, and Island Architecture
Development

Claude Code for Astro: Static Sites, Content Collections, and Island Architecture

Published: June 15, 2026
Read time: 8 min read
By: Claude Skills 360

Astro is the best choice for content-heavy sites: blogs, documentation, marketing sites, and portfolios. Its island architecture means you ship zero JavaScript by default and hydrate only the components that need interactivity. Claude Code generates Astro code in the modern pattern — content collections with type-safe schemas, View Transitions, and server-side rendering where needed.

This guide covers Astro with Claude Code: content collections, component islands, SSR mode, and deployment.

Project Setup and CLAUDE.md

Set up an Astro 5 project for a blog.
I need: content collections, dark mode toggle (island), 
syntax highlighting, RSS feed, and deploy to Cloudflare Pages.
## Astro Project Conventions
- Astro 5 with TypeScript
- Content in src/content/ — use Content Layer API for collections
- Islands: use only for interactive components (search, dark mode toggle, forms)
- Framework: Solid.js for islands (smaller than React, ideal for small interactivity)
- Styling: Tailwind CSS
- Markdown: remark-gfm + rehype-pretty-code for syntax highlighting
- Images: Astro's <Image> component for all images (auto-optimization)
- Deploy: @astrojs/cloudflare adapter
- No client-side routing — use View Transitions for page animations

Content Collections

Set up a content collection for blog posts.
Schema needs: title, description, pubDate, author, tags, image (optional), draft.
I need type-safe frontmatter validation.
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
  schema: ({ image }) => z.object({
    title: z.string().max(100),
    description: z.string().max(200),
    pubDate: z.string(),          // ISO date string
    author: z.string().default('Site Author'),
    tags: z.array(z.string()).default([]),
    image: image().optional(),    // Astro's image() validator
    draft: z.boolean().default(false),
    featured: z.boolean().default(false),
  }),
});

export const collections = { blog };
---
// src/pages/blog/[...slug].astro
import { getCollection, getEntry, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import FormattedDate from '../../components/FormattedDate.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => !data.draft);
  return posts.map(post => ({
    params: { slug: post.id },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content, headings } = await render(post);
---

<BaseLayout
  title={post.data.title}
  description={post.data.description}
>
  <article>
    <header>
      <h1>{post.data.title}</h1>
      <FormattedDate date={post.data.pubDate} />
      <div class="tags">
        {post.data.tags.map(tag => <span class="tag">{tag}</span>)}
      </div>
    </header>

    {post.data.image && (
      <Image
        src={post.data.image}
        alt={post.data.title}
        width={1200}
        height={630}
      />
    )}

    <Content />
  </article>
</BaseLayout>

Component Islands

Add a search feature to the blog.
Search should filter posts by title and tags as the user types.
Use minimal JavaScript — don't hydrate the entire page.
// src/components/BlogSearch.tsx (Solid.js island)
import { createSignal, For, Show } from 'solid-js';

interface Post {
  slug: string;
  title: string;
  tags: string[];
  description: string;
}

export default function BlogSearch({ posts }: { posts: Post[] }) {
  const [query, setQuery] = createSignal('');

  const filtered = () => {
    const q = query().toLowerCase();
    if (!q) return posts;
    return posts.filter(p =>
      p.title.toLowerCase().includes(q) ||
      p.tags.some(t => t.toLowerCase().includes(q))
    );
  };

  return (
    <div>
      <input
        type="search"
        placeholder="Search posts..."
        value={query()}
        onInput={(e) => setQuery(e.currentTarget.value)}
        aria-label="Search blog posts"
      />

      <Show
        when={filtered().length > 0}
        fallback={<p>No posts match "{query()}"</p>}
      >
        <ul>
          <For each={filtered()}>
            {post => (
              <li>
                <a href={`/blog/${post.slug}`}>
                  <strong>{post.title}</strong>
                  <p>{post.description}</p>
                </a>
              </li>
            )}
          </For>
        </ul>
      </Show>
    </div>
  );
}
---
// src/pages/blog/index.astro
// The island — only this component hydrates
import BlogSearch from '../../components/BlogSearch';
import { getCollection } from 'astro:content';

const posts = await getCollection('blog', ({ data }) => !data.draft);
const postData = posts.map(p => ({
  slug: p.id,
  title: p.data.title,
  tags: p.data.tags,
  description: p.data.description,
}));
---

<h1>Blog</h1>

<!-- client:load = hydrate immediately (for above-the-fold interactive content) -->
<!-- client:visible = hydrate when in viewport (lazy load) -->
<BlogSearch client:load posts={postData} />

Server-Side Rendering

Add a contact form. The form submission should happen server-side.
I don't want to expose an API key to the client.
---
// src/pages/contact.astro
import { actions, isInputError } from 'astro:actions';

// Handle form submission on the server
const result = Astro.getActionResult(actions.contact.send);
const error = result && !result?.ok ? result.error : undefined;
---

<form method="POST" action={actions.contact.send}>
  <label for="email">Email</label>
  <input id="email" name="email" type="email" required />
  {isInputError(error) && error.fields.email && (
    <span role="alert">{error.fields.email.join(', ')}</span>
  )}

  <label for="message">Message</label>
  <textarea id="message" name="message" required rows="5" />

  {result?.ok && <p class="success">Message sent!</p>}
  {error && !isInputError(error) && <p class="error">Failed to send. Please try again.</p>}

  <button type="submit">Send message</button>
</form>
// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';
import { Resend } from 'resend';

const resend = new Resend(import.meta.env.RESEND_API_KEY);

export const server = {
  contact: {
    send: defineAction({
      accept: 'form',
      input: z.object({
        email: z.string().email(),
        message: z.string().min(10).max(5000),
      }),
      handler: async ({ email, message }) => {
        await resend.emails.send({
          from: '[email protected]',
          to: '[email protected]',
          subject: `Contact from ${email}`,
          text: message,
        });
        return { sent: true };
      },
    }),
  },
};

RSS Feed

Generate an RSS feed for the blog.
Include full content in the feed, not just summaries.
// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import type { APIContext } from 'astro';

export async function GET(context: APIContext) {
  const posts = await getCollection('blog', ({ data }) => !data.draft);
  
  const sortedPosts = posts.sort(
    (a, b) => new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime()
  );

  return rss({
    title: 'My Blog',
    description: 'Articles about web development',
    site: context.site!,
    items: sortedPosts.map(post => ({
      title: post.data.title,
      description: post.data.description,
      pubDate: new Date(post.data.pubDate),
      link: `/blog/${post.id}/`,
      categories: post.data.tags,
    })),
  });
}

For deploying Astro to Cloudflare Pages with the same pipeline used by claudeskills360.com itself, see the serverless guide. For SvelteKit as an Astro alternative for more dynamic sites, see the SvelteKit guide. The Claude Skills 360 bundle is itself built with Astro — the bundle includes Astro skill sets for content sites, blogs, and documentation. Start with the free tier to try Astro component and collection generation.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free