--- name: publishing-astro-websites description: | Comprehensive guidance for building and deploying static websites with the Astro framework. This skill should be used when asked to "create astro site", "deploy astro to firebase", "set up content collections", "add mermaid diagrams to astro", "configure astro i18n", "build static blog", or "astro markdown setup". Covers SSG fundamentals, Content Collections, Markdown/MDX, partial hydration, islands architecture, and deployment to Netlify, Vercel, GitHub Pages, or GCP/Firebase. license: MIT metadata: version: 1.0.0 category: web-development author: Claude Code Skills triggers: - astro - astro website - astro static site - astro content collections - astro deployment - astro firebase - astro mermaid - starlight - build astro site tags: - astro - static-site-generation - markdown - deployment --- # Publishing Astro Websites Build fast, content-driven static websites with Astro's zero-runtime SSG approach, partial hydration, and extensive Markdown support. ## Contents - [Quick Start](#quick-start) - [When Not to Use](#when-not-to-use) - [Project Structure](#project-structure) - [SSG vs SSR vs Hybrid](#ssg-vs-ssr-vs-hybrid) - [Content Collections](#content-collections) — Legacy, Content Layer API, Custom Loaders - [Syntax Highlighting](#syntax-highlighting) — Shiki, Transformers, Expressive Code - [Diagram Integration](#diagram-integration) — Mermaid, PlantUML, Dark Mode Theming - [Client-Side Search](#client-side-search) — Pagefind (controls, weighting), Fuse.js - [Versioned Documentation](#versioned-documentation) — Starlight, Multi-version - [Internationalization](#internationalization-i18n) — Routing, Fallbacks - [Common Patterns](#common-patterns) — Pagination, Tags, RSS, Forms - [Performance Best Practices](#performance-best-practices) — Prefetching, Critical CSS - [Deployment](#deployment) — Firebase URL Config, GitHub Pages - [Pre-Deploy Checklist](#pre-deploy-checklist) - [Testing & Quality](#testing--quality) — Vitest, Playwright, Link Checking - [Troubleshooting](#troubleshooting) ## Quick Start ```bash # Create new project (use Blog template for Markdown sites) npm create astro@latest # Development npm run dev # Local server at http://localhost:4321 npm run build # Generate static files in dist/ npm run preview # Preview production build ``` ## When Not to Use This skill focuses on **static site generation (SSG)**. Consider other approaches for: - **Real-time data applications** - Use SSR mode with database connections - **User authentication flows** - Requires server-side session handling - **E-commerce with dynamic inventory** - Use hybrid mode or full SSR - **Single-page applications (SPAs)** - Consider React/Vue frameworks directly For hybrid SSG+SSR patterns, see Astro's adapter documentation. ## Project Structure ``` src/ components/ # Astro, React, Vue, Svelte components content/ # Content Collections (Markdown/MDX) config.ts # Collection schemas docs/ # Example collection layouts/ # Page wrappers with slots pages/ # File-based routing public/ # Static assets (images, fonts, favicons) astro.config.mjs # Framework configuration ``` ## SSG vs SSR vs Hybrid | Mode | When Pages Render | Use Case | |------|-------------------|----------| | **SSG** (default) | Build time | Blogs, docs, marketing sites | | **SSR** | Each request | Dynamic data, personalization | | **Hybrid** | Mix of both | Static pages + dynamic endpoints | For pure static sites, use default `output: 'static'` - no adapter needed. ## Content Collections ### Legacy Pattern (Astro 4.x) Define schemas in `src/content/config.ts`: ```typescript import { defineCollection, z } from "astro:content"; export const collections = { docs: defineCollection({ schema: z.object({ title: z.string(), description: z.string().optional(), tags: z.array(z.string()).optional(), order: z.number().optional(), draft: z.boolean().default(false) }) }) }; ``` ### Content Layer API (Astro 5.0+) New pattern with `glob()` loader - up to 75% faster builds for large sites: ```typescript // src/content.config.ts (note: different filename) import { defineCollection } from 'astro:content'; import { glob } from 'astro/loaders'; import { z } from 'astro/zod'; const blog = defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/data/blog' }), schema: ({ image }) => z.object({ title: z.string(), pubDate: z.coerce.date(), draft: z.boolean().default(false), cover: image(), // Validates image exists author: reference('authors'), // Cross-collection reference }) }); export const collections = { blog }; ``` ### Advanced Schema Patterns ```typescript schema: ({ image }) => z.object({ cover: image(), // Validates image in src/ category: z.enum(['tech', 'news']), author: reference('authors'), // Cross-collection ref relatedPosts: z.array(reference('blog')).optional(), }) ``` ### Custom Loaders (Remote Content) Fetch content from external APIs (GitHub releases, CMS, etc.): ```typescript // src/loaders/github-releases.ts import type { Loader } from 'astro/loaders'; export function githubReleasesLoader(repo: string): Loader { return { name: 'github-releases', load: async ({ store, logger }) => { logger.info(`Fetching releases for ${repo}`); const response = await fetch(`https://api.github.com/repos/${repo}/releases`); const releases = await response.json(); for (const release of releases) { store.set({ id: release.tag_name, data: { version: release.tag_name, published_at: release.published_at, body: release.body // Markdown release notes } }); } } }; } ``` Register in `content.config.ts`: ```typescript import { githubReleasesLoader } from './loaders/github-releases'; const releases = defineCollection({ loader: githubReleasesLoader('owner/repo'), schema: z.object({ version: z.string(), published_at: z.string(), body: z.string(), }) }); ``` Query and render collections: ```astro --- import { getCollection } from "astro:content"; export async function getStaticPaths() { const docs = await getCollection("docs"); return docs.map(doc => ({ params: { slug: doc.slug }, props: { doc } })); } const { doc } = Astro.props; const { Content } = await doc.render(); ---

{doc.data.title}

``` ## Syntax Highlighting ### Basic Shiki Configuration ```javascript import { defineConfig } from "astro/config"; export default defineConfig({ markdown: { shikiConfig: { theme: "github-dark", wrap: true } } }); ``` ### Dual Light/Dark Theme ```javascript shikiConfig: { themes: { light: 'github-light', dark: 'github-dark', }, } ``` Add CSS to switch themes: ```css @media (prefers-color-scheme: dark) { .astro-code, .astro-code span { color: var(--shiki-dark) !important; background-color: var(--shiki-dark-bg) !important; } } ``` ### Line Highlighting and Transformers ~~~markdown ```typescript {2,4} const a = 1; const b = 2; // highlighted const c = 3; console.log(a + b + c); // highlighted ``` ~~~ **Shiki Transformers (Astro 4.14+):** ```javascript import { transformerNotationFocus, transformerNotationDiff } from '@shikijs/transformers'; shikiConfig: { transformers: [transformerNotationFocus(), transformerNotationDiff()], } ``` Use notation comments in code: - `// [!code focus]` - Focus this line - `// [!code ++]` - Mark as addition (green) - `// [!code --]` - Mark as deletion (red) ### Expressive Code (Recommended for Docs) Rich code blocks with copy buttons, filenames, diff highlighting: ```bash npm install astro-expressive-code ``` ```javascript import expressiveCode from 'astro-expressive-code'; export default defineConfig({ integrations: [expressiveCode()], }); ``` Features: Copy button, file tabs, line markers, terminal frames, text markers. ## Diagram Integration ### Mermaid (Recommended) Install the Astro integration: ```bash npm install astro-mermaid mermaid ``` ```javascript // astro.config.mjs import { defineConfig } from 'astro/config'; import mermaid from 'astro-mermaid'; export default defineConfig({ integrations: [mermaid({ theme: 'default' })] }); ``` Use in Markdown: ~~~markdown ```mermaid graph TD; A-->B; B-->C; ``` ~~~ Features: Client-side rendering, automatic theme switching, offline capable, no Playwright required. **Alternative (build-time static SVG):** Use `rehype-mermaid` with Playwright for pre-rendered diagrams (`npx playwright install --with-deps` required). **Dark Mode Theming Strategies:** 1. **CSS Variables** - Let browser resolve colors at runtime: ```javascript // mermaid config mermaid.initialize({ theme: 'base', themeVariables: { primaryColor: 'var(--diagram-primary)', lineColor: 'var(--diagram-line)' } }); ``` 2. **Picture Element** - Generate both themes, swap with media query: ```html Flow diagram ``` 3. **Inline SVG** - Target SVG classes with CSS (risk: style collisions): ```css .dark .mermaid-svg .node rect { fill: var(--bg-dark); } ``` ### PlantUML ```bash npx astro add plantuml ``` Use in Markdown: ~~~markdown ```plantuml @startuml Alice -> Bob: Hello Bob --> Alice: Hi! @enduml ``` ~~~ ## Client-Side Search ### Pagefind (Recommended for Large Sites) Zero-config static search that indexes at build time: ```bash npm install pagefind ``` Add to `package.json`: ```json { "scripts": { "build": "astro build && npx pagefind --site dist", "postbuild": "pagefind --site dist" } } ``` Use in components: ```astro ``` Features: No external service, works offline, automatic indexing, small bundle (~10KB). **Granular Indexing Control:** ```html

Page Title

Important intro text

``` | Attribute | Purpose | |-----------|---------| | `data-pagefind-body` | Limit indexing to this element only | | `data-pagefind-ignore` | Exclude element from index | | `data-pagefind-meta="key"` | Define metadata field | | `data-pagefind-weight="10"` | Boost relevance (default: 1) | ### Pagefind vs Fuse.js | Feature | Pagefind | Fuse.js | |---------|----------|---------| | Architecture | Pre-built binary chunks | Runtime in-memory | | Bandwidth | Low (loads only needed chunks) | High (downloads full index) | | Scalability | 10,000+ pages | < 500 pages | | Multilingual | Native stemming | Manual config | | Use Case | Global site search | Small list filtering | ### Fuse.js (Lightweight Alternative) For smaller sites with custom UI needs: ```astro --- import { getCollection } from "astro:content"; const posts = await getCollection('blog'); const searchIndex = JSON.stringify(posts.map(post => ({ title: post.data.title, slug: post.slug, body: post.body.slice(0, 500) }))); --- ``` For enterprise needs, consider Algolia (hosted search API). ## Versioned Documentation ### Starlight (Recommended for Docs Sites) Purpose-built documentation framework on Astro: ```bash npm create astro@latest -- --template starlight ``` Key features: Built-in search (Pagefind), i18n, sidebar navigation, dark mode, component overrides. ```javascript // astro.config.mjs import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; export default defineConfig({ integrations: [ starlight({ title: 'My Docs', sidebar: [ { label: 'Guides', autogenerate: { directory: 'guides' } }, { label: 'Reference', autogenerate: { directory: 'reference' } }, ], }), ], }); ``` ### Multi-Version Docs Pattern Use folder-based structure: ``` src/content/docs/ v1/ getting-started.md api-reference.md v2/ getting-started.md api-reference.md new-feature.md ``` For Starlight versioning, use `starlight-utils` plugin: ```bash npm install starlight-utils ``` ```javascript import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; import starlightUtils from 'starlight-utils'; export default defineConfig({ integrations: [ starlight({ plugins: [starlightUtils({ multiSidebar: { switcherStyle: 'dropdown' } })], sidebar: [ { label: 'v2', items: [{ label: 'Guides', autogenerate: { directory: 'v2' } }] }, { label: 'v1', items: [{ label: 'Guides', autogenerate: { directory: 'v1' } }] }, ], }), ], }); ``` ## Internationalization (i18n) Configure in `astro.config.mjs`: ```javascript export default defineConfig({ i18n: { defaultLocale: "en", locales: ["en", "fr", "es"], routing: { prefixDefaultLocale: false } } }); ``` Structure content by locale: ``` src/content/docs/ en/ getting-started.md fr/ getting-started.md ``` Detect locale in components: ```astro --- const locale = Astro.currentLocale || 'en'; --- ``` ### Fallback for Missing Translations Show English content with a banner when translations don't exist: ```astro --- // src/pages/[lang]/[...slug].astro import { getCollection, getEntry } from "astro:content"; const languages = ['en', 'es', 'fr']; const defaultLang = 'en'; export async function getStaticPaths() { const englishDocs = await getCollection('docs', ({ id }) => id.startsWith('en/')); const paths = []; for (const doc of englishDocs) { const slug = doc.id.replace(/^en\//, ''); for (const lang of languages) { const localizedId = `${lang}/${slug}`; const localizedDoc = await getEntry('docs', localizedId); paths.push({ params: { lang, slug }, props: { entry: localizedDoc || doc, // Fallback to English isFallback: !localizedDoc } }); } } return paths; } const { entry, isFallback } = Astro.props; const { Content } = await entry.render(); --- {isFallback && (
This page is not yet available in your language.
)} ``` ## Common Patterns ### Paginated Listings For sites with 50+ posts, split into pages: ```astro --- // src/pages/blog/page/[page].astro import { getCollection } from "astro:content"; const POSTS_PER_PAGE = 10; export async function getStaticPaths() { const allPosts = await getCollection("blog"); const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE); return Array.from({ length: totalPages }, (_, i) => ({ params: { page: String(i + 1) }, })); } const { page } = Astro.params; const pageNum = parseInt(page); const allPosts = await getCollection("blog"); const sortedPosts = allPosts.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime() ); const start = (pageNum - 1) * POSTS_PER_PAGE; const posts = sortedPosts.slice(start, start + POSTS_PER_PAGE); const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE); --- {posts.map(post =>
{post.data.title}
)} ``` ### Tag/Category Archives Generate a page for each tag: ```astro --- // src/pages/tags/[tag].astro import { getCollection } from "astro:content"; export async function getStaticPaths() { const allPosts = await getCollection("blog"); const tags = new Set(); allPosts.forEach(post => post.data.tags?.forEach(tag => tags.add(tag))); return Array.from(tags).map(tag => ({ params: { tag }, props: { tag }, })); } const { tag } = Astro.props; const allPosts = await getCollection("blog"); const postsWithTag = allPosts.filter(post => post.data.tags?.includes(tag)); ---

Posts tagged: {tag}

{postsWithTag.map(post => {post.data.title})} ``` ### RSS Feed ```javascript // src/pages/rss.xml.js import rss from "@astrojs/rss"; import { getCollection } from "astro:content"; export async function GET(context) { const blog = await getCollection("blog"); return rss({ title: "My Blog", description: "A blog about Astro", site: context.site, items: blog.map(post => ({ title: post.data.title, description: post.data.description, pubDate: post.data.pubDate, link: `/blog/${post.slug}/`, })), }); } ``` ### Static Forms For SSG sites, use third-party form handlers: **Formspree (Easiest):** ```html
``` **Netlify Forms:** ```html
``` ### JSON-LD Structured Data Add rich snippets for SEO: ```astro --- const jsonLd = { "@context": "https://schema.org", "@type": "BlogPosting", headline: entry.data.title, description: entry.data.description, datePublished: entry.data.pubDate?.toISOString(), author: { "@type": "Person", name: entry.data.author }, }; --- ``` With Tailwind, enable `darkMode: "class"` in config. ## Performance Best Practices 1. **Partial Hydration**: Use `client:*` directives only where needed - `client:load` - Hydrate immediately - `client:idle` - Hydrate when browser is idle - `client:visible` - Hydrate when in viewport - `client:media` - Hydrate on media query match - `client:only="react"` - Skip server render, client-only 2. **Image Optimization**: Use Astro's `` component 3. **Keep Static Where Possible**: Islands architecture means most content remains static HTML 4. **Asset Fingerprinting**: Automatic in production builds 5. **Prefetching**: Auto-load links before user clicks ```javascript // astro.config.mjs export default defineConfig({ prefetch: { prefetchAll: true, // Prefetch all links defaultStrategy: 'viewport' // When links enter viewport } }); ``` Options: `'tap'` (on hover/focus), `'viewport'` (when visible), `'load'` (on page load). 6. **Critical CSS**: Inline above-the-fold CSS with astro-critters ```bash npm install astro-critters ``` ```javascript import critters from 'astro-critters'; export default defineConfig({ integrations: [critters()] }); ``` Extracts critical CSS and inlines it, deferring the rest for faster first paint. ## Deployment ### Deployment Workflow 1. **Build**: Run `npm run build` and verify `dist/` output 2. **Preview**: Test with `npm run preview` at localhost:4321 3. **Configure**: Set `site`, `base`, and `trailingSlash` in astro.config.mjs 4. **Platform Setup**: Initialize hosting (e.g., `firebase init hosting`) 5. **Deploy**: Push to platform (`firebase deploy`, `vercel`, or git push) 6. **Verify**: Check live URL, test 404 page, validate assets load ### Quick Deploy Commands ```bash # Build for production npm run build # Preview before deploy npm run preview ``` ### Platform-Specific **Netlify/Vercel/Cloudflare Pages:** Connect Git repository - auto-deploys on push. **GitHub Pages:** ```javascript // astro.config.mjs export default defineConfig({ site: 'https://username.github.io', base: '/repo-name' }); ``` **Firebase Hosting:** ```bash npm install -g firebase-tools firebase login firebase init hosting # Set public to 'dist' npm run build firebase deploy ``` `firebase.json` (recommended configuration): ```json { "hosting": { "public": "dist", "ignore": ["firebase.json", "**/.*"], "cleanUrls": true, "trailingSlash": false, "headers": [ { "source": "/_astro/**", "headers": [{"key": "Cache-Control", "value": "public, max-age=31536000, immutable"}] } ] } } ``` **Align Astro config** to prevent redirect loops: ```javascript // astro.config.mjs - match Firebase settings export default defineConfig({ trailingSlash: 'never', // Must match Firebase trailingSlash: false build: { format: 'directory' // Default - generates /about/index.html } }); ``` | Firebase Setting | Astro Setting | Result | |------------------|---------------|--------| | `trailingSlash: false` | `trailingSlash: 'never'` | `/about` (no slash) | | `trailingSlash: true` | `trailingSlash: 'always'` | `/about/` (with slash) | | Mismatch | Mismatch | Redirect loops! | ### Common Deployment Gotchas | Issue | Solution | |-------|----------| | Trailing slash problems | Set `trailingSlash: 'always'` or `'never'` | | Assets not loading on subpath | Configure `base` in astro.config.mjs | | 404 not working | Create custom `404.astro` page | | Build fails on deploy | Check Node version matches local | ## Pre-Deploy Checklist Before deploying, verify: - [ ] `npm run build` completes without errors - [ ] `npm run preview` shows site correctly at localhost:4321 - [ ] All Content Collection schemas validate (`astro check`) - [ ] Images use `` component or are in `public/` - [ ] SEO metadata present on all pages (title, description, og:*) - [ ] 404.astro page exists and renders correctly - [ ] `base` path configured if deploying to subdirectory - [ ] Environment variables set on deployment platform - [ ] `trailingSlash` setting matches hosting platform expectations - [ ] RSS feed working (`/rss.xml`) - [ ] Sitemap generated (`/sitemap-index.xml`) - [ ] Lighthouse score > 90 - [ ] Component tests pass (`npm run test`) - [ ] E2E tests pass (`npm run test:e2e`) - [ ] Link checker finds no broken links (`npx linkinator dist`) ## Testing & Quality ### Static Analysis ```bash # Type checking and validation npx astro check # Linting (with ESLint) npm install -D eslint npx eslint . # Preview production build npm run build && npm run preview ``` **Build-time validation** happens automatically with Content Collections - schema errors fail the build. ### Component Testing with Vitest ```bash npm install -D vitest @vitest/ui @astrojs/testing ``` ```typescript // vitest.config.ts import { getViteConfig } from 'astro/config'; export default getViteConfig({ test: { include: ['src/**/*.test.ts'], }, }); ``` ```typescript // src/components/Button.test.ts import { experimental_AstroContainer as AstroContainer } from 'astro/container'; import { expect, test } from 'vitest'; import Button from './Button.astro'; test('Button renders with text', async () => { const container = await AstroContainer.create(); const result = await container.renderToString(Button, { props: { text: 'Click me' } }); expect(result).toContain('Click me'); }); ``` ### E2E Testing with Playwright ```bash npm install -D @playwright/test npx playwright install ``` ```typescript // tests/homepage.spec.ts import { test, expect } from '@playwright/test'; test('homepage loads correctly', async ({ page }) => { await page.goto('/'); await expect(page).toHaveTitle(/My Site/); await expect(page.locator('h1')).toBeVisible(); }); test('navigation works', async ({ page }) => { await page.goto('/'); await page.click('a[href="/about"]'); await expect(page).toHaveURL(/about/); }); ``` ```json // package.json { "scripts": { "test": "vitest", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui" } } ``` ### Link Checking Validate internal links don't break: ```bash npm install -D linkinator npx linkinator dist --recurse ``` Add to CI: ```yaml # .github/workflows/links.yml - run: npm run build - run: npx linkinator dist --recurse --skip "^(?!http://localhost)" ``` ## .astro File Anatomy ```astro --- // Frontmatter: JavaScript/TypeScript runs at build time import Layout from '../layouts/Layout.astro'; import { getCollection } from 'astro:content'; const { title } = Astro.props; const posts = await getCollection('blog'); ---

{title}

``` ## File-Based Routing | File | Route | |------|-------| | `src/pages/index.astro` | `/` | | `src/pages/about.astro` | `/about` | | `src/pages/blog/index.astro` | `/blog` | | `src/pages/blog/[slug].astro` | `/blog/:slug` (dynamic) | | `src/pages/[...path].astro` | Catch-all | Dynamic routes require `getStaticPaths()` for SSG: ```astro --- export function getStaticPaths() { return [ { params: { slug: 'post-1' } }, { params: { slug: 'post-2' } } ]; } --- ``` ## SEO Essentials ### Manual Approach ```astro --- const { title, description, image } = Astro.props; const canonicalURL = new URL(Astro.url.pathname, Astro.site); --- {title} ``` ### astro-seo (Simplified) ```bash npm install astro-seo ``` ```astro --- import { SEO } from 'astro-seo'; --- ``` Handles meta tags, Open Graph, Twitter Cards, and canonical URLs automatically. ## Essential Integrations ```bash # Add integrations npx astro add react # React components npx astro add tailwind # Tailwind CSS npx astro add mdx # MDX support npx astro add sitemap # Auto-generate sitemap # RSS feed npm install @astrojs/rss ``` ## Troubleshooting **"Works locally but breaks on deploy"** - Check environment variables are set on host - Verify `base` path configuration - Ensure Node version matches (v18+ recommended) **Dynamic routes missing pages** - Verify `getStaticPaths()` returns all needed paths - Check for typos in params **Content Collection schema errors** - Run `astro check` for validation details - Ensure frontmatter matches Zod schema exactly **Assets not loading** - Use `import` for processed assets - Use `public/` for unprocessed static files ## References For detailed guides on specific topics, see: - `references/markdown-deep-dive.md` - Advanced Markdown/MDX patterns - `references/deployment-platforms.md` - Platform-specific deployment details ## Key Resources - [Astro Documentation](https://docs.astro.build/) - [Content Collections](https://docs.astro.build/en/guides/content-collections/) - [Markdown in Astro](https://docs.astro.build/en/guides/markdown-content/) - [Deploy Astro](https://docs.astro.build/en/guides/deploy/) - [Starlight Docs Theme](https://starlight.astro.build/)