Claude Code for Astro Advanced: Content Collections, View Transitions, and Islands — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Astro Advanced: Content Collections, View Transitions, and Islands
Frontend

Claude Code for Astro Advanced: Content Collections, View Transitions, and Islands

Published: December 24, 2026
Read time: 8 min read
By: Claude Skills 360

Astro 5’s Content Layer API replaces the old content collections with a unified data system that handles Markdown, JSON, YAML, external APIs, and databases under one interface. View Transitions give SPA-like navigation without the JavaScript overhead. Server Islands render dynamic components at request time while the rest of the page stays static. The output modes — static, server, hybrid — give precise control over what gets prerendered. Claude Code generates Astro content schemas, component patterns, routing configurations, and the deployment adapters for production Astro sites.

CLAUDE.md for Astro Projects

## Astro Stack
- Version: Astro 5.x with TypeScript
- Rendering: hybrid (pages default to static, opt-in server mode)
- Content: Content Layer API (defineCollection with loaders)
- UI: React islands for interactive components (Preact for smaller bundle)
- Styling: Tailwind CSS v4 (new CSS-first config)
- Images: astro:assets with <Image /> component
- Transitions: View Transitions API (client:only animation)
- Deploy: Cloudflare Pages adapter (prefer over Vercel for edge)
- MDX: @astrojs/mdx for component integration in Markdown

Project Structure

src/
├── content/
│   ├── blog/          # Markdown/MDX posts
│   └── authors/       # YAML author profiles
├── pages/
│   ├── index.astro
│   ├── blog/
│   │   ├── index.astro
│   │   └── [slug].astro
│   └── api/
│       └── newsletter.ts    # Server endpoint
├── components/
│   ├── BlogCard.astro
│   ├── Newsletter.tsx       # React island
│   └── Search.tsx           # Interactive island
├── layouts/
│   ├── Base.astro
│   └── BlogPost.astro
├── middleware.ts
└── content.config.ts        # Content Layer (Astro 5)

Content Layer Configuration

// src/content.config.ts — Astro 5 Content Layer API
import { defineCollection, z } from 'astro:content'
import { glob, file } 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(300),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    author: z.string().default('CS360 Team'),
    cover: image().optional(),
    coverAlt: z.string().optional(),
    tags: z.array(z.string()).default([]),
    featured: z.boolean().default(false),
    draft: z.boolean().default(false),
    readTime: z.string().optional(),
  }),
})

const authors = defineCollection({
  loader: file('./src/data/authors.json'),
  schema: z.object({
    id: z.string(),
    name: z.string(),
    bio: z.string().optional(),
    avatar: z.string().url().optional(),
    twitter: z.string().optional(),
    github: z.string().optional(),
  }),
})

// External API loader — fetch from CMS
const products = defineCollection({
  loader: async () => {
    const resp = await fetch('https://cms.example.com/api/products')
    const data = await resp.json()
    return data.map((p: any) => ({ id: p.slug, ...p }))
  },
  schema: z.object({
    id: z.string(),
    name: z.string(),
    price: z.number(),
    description: z.string(),
    inStock: z.boolean(),
  }),
})

export const collections = { blog, authors, products }

Blog Listing Page

---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content'
import Base from '@/layouts/Base.astro'
import BlogCard from '@/components/BlogCard.astro'

// Filter drafts in production
const posts = (await getCollection('blog', ({ data }) => {
  return import.meta.env.PROD ? !data.draft : true
})).sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())

const featured = posts.filter(p => p.data.featured).slice(0, 3)
const recent = posts.filter(p => !p.data.featured).slice(0, 12)

// Group by tag for filtering
const tagMap = posts.reduce((acc, post) => {
  post.data.tags.forEach(tag => {
    if (!acc[tag]) acc[tag] = []
    acc[tag].push(post)
  })
  return acc
}, {} as Record<string, typeof posts>)

const allTags = Object.keys(tagMap).sort()
---

<Base title="Blog" description="Articles about Claude Code and AI development">
  <main class="max-w-6xl mx-auto px-4 py-12">
    {featured.length > 0 && (
      <section class="mb-16">
        <h2 class="text-2xl font-bold mb-6">Featured</h2>
        <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
          {featured.map(post => (
            <BlogCard post={post} variant="featured" />
          ))}
        </div>
      </section>
    )}

    <section>
      <div class="flex gap-2 flex-wrap mb-8">
        <a href="/blog" class="tag-pill active">All</a>
        {allTags.map(tag => (
          <a href={`/blog/tag/${tag}`} class="tag-pill">{tag}</a>
        ))}
      </div>

      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {recent.map(post => <BlogCard post={post} />)}
      </div>
    </section>
  </main>
</Base>

Blog Post Page

---
// src/pages/blog/[slug].astro
import { getCollection, getEntry, render } from 'astro:content'
import Base from '@/layouts/Base.astro'
import { Image } from 'astro:assets'

// Generate all static paths
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, remarkPluginFrontmatter } = await render(post)

// Get related posts
const allPosts = await getCollection('blog', ({ data }) => !data.draft)
const related = allPosts
  .filter(p => p.id !== post.id && p.data.tags.some(t => post.data.tags.includes(t)))
  .slice(0, 3)
---

<Base
  title={post.data.title}
  description={post.data.description}
  image={post.data.cover?.src}
>
  <article class="max-w-3xl mx-auto px-4 py-12">
    <header class="mb-8">
      <div class="flex gap-2 mb-4">
        {post.data.tags.map(tag => (
          <a href={`/blog/tag/${tag}`} class="tag-pill text-sm">{tag}</a>
        ))}
      </div>

      <h1 class="text-4xl font-bold mb-4">{post.data.title}</h1>
      <p class="text-xl text-gray-600 mb-6">{post.data.description}</p>

      <div class="flex items-center gap-4 text-sm text-gray-500">
        <time datetime={post.data.pubDate.toISOString()}>
          {post.data.pubDate.toLocaleDateString('en-US', {
            year: 'numeric', month: 'long', day: 'numeric'
          })}
        </time>
        {post.data.readTime && <span>{post.data.readTime}</span>}
      </div>

      {post.data.cover && (
        <Image
          src={post.data.cover}
          alt={post.data.coverAlt ?? post.data.title}
          class="w-full rounded-xl mt-6"
          widths={[400, 800, 1200]}
          sizes="(max-width: 768px) 100vw, 800px"
        />
      )}
    </header>

    <div class="prose prose-lg max-w-none">
      <Content />
    </div>
  </article>
</Base>

View Transitions

---
// src/layouts/Base.astro — enable view transitions globally
import { ViewTransitions } from 'astro:transitions'

interface Props {
  title: string
  description?: string
  image?: string
}

const { title, description, image } = Astro.props
---

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>{title} | Claude Skills 360</title>
  {description && <meta name="description" content={description} />}
  
  <!-- Enables smooth page transitions -->
  <ViewTransitions />
</head>
<body>
  <nav>
    <!-- Links navigate with transitions automatically -->
    <a href="/">Home</a>
    <a href="/blog">Blog</a>
  </nav>
  
  <slot />
</body>
</html>
---
// Per-element transition names for coordinated animations
---
<article>
  <!-- Named transitions persist element across pages -->
  <img
    src={post.cover}
    alt={post.title}
    transition:name={`post-cover-${post.id}`}
    transition:animate="initial"
  />
  <h2 transition:name={`post-title-${post.id}`}>
    {post.title}
  </h2>
</article>

Server Islands and Hybrid Rendering

---
// src/pages/dashboard.astro — hybrid: static shell, dynamic island
export const prerender = false  // This page serves dynamically
import { getSession } from '@/lib/auth'

const session = await getSession(Astro.request)
if (!session) return Astro.redirect('/login')
---

<Base title="Dashboard">
  <h1>Welcome, {session.user.name}</h1>

  <!-- Server island: renders at request time, rest is edge-cached -->
  <astro:island
    component="./components/LiveStats.astro"
    server:defer
    fallback="loading"
  >
    <div slot="fallback" class="animate-pulse">Loading stats...</div>
  </astro:island>

  <!-- Static content — cached at edge -->
  <section>
    <h2>Quick Links</h2>
    <!-- ... -->
  </section>
</Base>

Middleware

// src/middleware.ts — request middleware
import { defineMiddleware } from 'astro:middleware'
import { getSession } from '@/lib/auth'

export const onRequest = defineMiddleware(async (context, next) => {
  const { pathname } = new URL(context.request.url)

  // Auth guard for protected routes
  if (pathname.startsWith('/dashboard') || pathname.startsWith('/admin')) {
    const session = await getSession(context.request)

    if (!session) {
      return context.redirect('/login?from=' + encodeURIComponent(pathname))
    }

    // Add session to locals for downstream use
    context.locals.session = session
    context.locals.user = session.user
  }

  // Security headers for all responses
  const response = await next()

  response.headers.set('X-Frame-Options', 'DENY')
  response.headers.set('X-Content-Type-Options', 'nosniff')
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline'"
  )

  return response
})

API Endpoints

// src/pages/api/newsletter.ts — server endpoint
import type { APIRoute } from 'astro'
import { z } from 'zod'

const SubscribeSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
})

export const POST: APIRoute = async ({ request }) => {
  const body = await request.json()
  const parsed = SubscribeSchema.safeParse(body)

  if (!parsed.success) {
    return new Response(JSON.stringify({ error: 'Invalid input' }), {
      status: 400,
      headers: { 'Content-Type': 'application/json' },
    })
  }

  await subscribeToNewsletter(parsed.data)

  return new Response(JSON.stringify({ success: true }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
  })
}

For the React components used as interactive islands within Astro pages, see the React 19 guide for server component patterns that complement Astro’s architecture. For the SvelteKit alternative framework with similar SSR‐first philosophy, the SvelteKit advanced guide covers form actions and load functions. The Claude Skills 360 bundle includes Astro skill sets covering content collections, view transitions, and server islands. Start with the free tier to try Astro site generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

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