| 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 | [object Object] |
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
- When Not to Use
- Project Structure
- SSG vs SSR vs Hybrid
- Content Collections — Legacy, Content Layer API, Custom Loaders
- Syntax Highlighting — Shiki, Transformers, Expressive Code
- Diagram Integration — Mermaid, PlantUML, Dark Mode Theming
- Client-Side Search — Pagefind (controls, weighting), Fuse.js
- Versioned Documentation — Starlight, Multi-version
- Internationalization — Routing, Fallbacks
- Common Patterns — Pagination, Tags, RSS, Forms
- Performance Best Practices — Prefetching, Critical CSS
- Deployment — Firebase URL Config, GitHub Pages
- Pre-Deploy Checklist
- Testing & Quality — Vitest, Playwright, Link Checking
- Troubleshooting
Quick Start
# 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:
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:
// 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
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.):
// 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:
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:
---
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();
---
<article>
<h1>{doc.data.title}</h1>
<Content />
</article>
Syntax Highlighting
Basic Shiki Configuration
import { defineConfig } from "astro/config";
export default defineConfig({
markdown: {
shikiConfig: {
theme: "github-dark",
wrap: true
}
}
});
Dual Light/Dark Theme
shikiConfig: {
themes: {
light: 'github-light',
dark: 'github-dark',
},
}
Add CSS to switch themes:
@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
```typescript {2,4}
const a = 1;
const b = 2; // highlighted
const c = 3;
console.log(a + b + c); // highlighted
```
Shiki Transformers (Astro 4.14+):
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:
npm install astro-expressive-code
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:
npm install astro-mermaid mermaid
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mermaid from 'astro-mermaid';
export default defineConfig({
integrations: [mermaid({ theme: 'default' })]
});
Use in 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:
- CSS Variables - Let browser resolve colors at runtime:
// mermaid config
mermaid.initialize({
theme: 'base',
themeVariables: {
primaryColor: 'var(--diagram-primary)',
lineColor: 'var(--diagram-line)'
}
});
- Picture Element - Generate both themes, swap with media query:
<picture>
<source srcset="/diagrams/flow-dark.svg" media="(prefers-color-scheme: dark)">
<img src="/diagrams/flow-light.svg" alt="Flow diagram">
</picture>
- Inline SVG - Target SVG classes with CSS (risk: style collisions):
.dark .mermaid-svg .node rect {
fill: var(--bg-dark);
}
PlantUML
npx astro add plantuml
Use in 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:
npm install pagefind
Add to package.json:
{
"scripts": {
"build": "astro build && npx pagefind --site dist",
"postbuild": "pagefind --site dist"
}
}
Use in components:
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script src="/pagefind/pagefind-ui.js" type="text/javascript"></script>
<div id="search"></div>
<script>
window.addEventListener('DOMContentLoaded', () => {
new PagefindUI({ element: '#search', showSubResults: true });
});
</script>
Features: No external service, works offline, automatic indexing, small bundle (~10KB).
Granular Indexing Control:
<!-- Only index main content, not headers/sidebars -->
<main data-pagefind-body>
<h1 data-pagefind-meta="title">Page Title</h1>
<p data-pagefind-weight="10">Important intro text</p>
<!-- Exclude from search snippets -->
<nav data-pagefind-ignore>
<a href="/related">Related Posts</a>
</nav>
</main>
| 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:
---
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)
})));
---
<input type="search" id="search" placeholder="Search..." />
<ul id="results"></ul>
<script define:vars={{ searchIndex }}>
import Fuse from 'fuse.js';
const fuse = new Fuse(JSON.parse(searchIndex), {
keys: ['title', 'body'],
threshold: 0.3
});
document.getElementById('search').addEventListener('input', (e) => {
const results = fuse.search(e.target.value);
const resultsEl = document.getElementById('results');
// Clear previous results safely
resultsEl.replaceChildren();
// Build results using safe DOM methods
results.forEach(r => {
const li = document.createElement('li');
const link = document.createElement('a');
link.href = `/blog/${r.item.slug}`;
link.textContent = r.item.title;
li.appendChild(link);
resultsEl.appendChild(li);
});
});
</script>
For enterprise needs, consider Algolia (hosted search API).
Versioned Documentation
Starlight (Recommended for Docs Sites)
Purpose-built documentation framework on Astro:
npm create astro@latest -- --template starlight
Key features: Built-in search (Pagefind), i18n, sidebar navigation, dark mode, component overrides.
// 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:
npm install starlight-utils
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:
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:
---
const locale = Astro.currentLocale || 'en';
---
Fallback for Missing Translations
Show English content with a banner when translations don't exist:
---
// 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 && (
<div class="translation-notice">
This page is not yet available in your language.
</div>
)}
<Content />
Common Patterns
Paginated Listings
For sites with 50+ posts, split into pages:
---
// 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 => <article>{post.data.title}</article>)}
<nav>
{pageNum > 1 && <a href={`/blog/page/${pageNum - 1}`}>← Previous</a>}
<span>Page {pageNum} of {totalPages}</span>
{pageNum < totalPages && <a href={`/blog/page/${pageNum + 1}`}>Next →</a>}
</nav>
Tag/Category Archives
Generate a page for each tag:
---
// 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));
---
<h1>Posts tagged: {tag}</h1>
{postsWithTag.map(post => <a href={`/blog/${post.slug}`}>{post.data.title}</a>)}
RSS Feed
// 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):
<form action="https://formspree.io/f/YOUR_FORM_ID" method="POST">
<input type="text" name="name" required />
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>
Netlify Forms:
<form name="contact" method="POST" data-netlify="true">
<input type="hidden" name="form-name" value="contact" />
<input type="text" name="name" required />
<button type="submit">Send</button>
</form>
JSON-LD Structured Data
Add rich snippets for SEO:
---
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 },
};
---
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
Dark Mode Toggle
<button id="theme-toggle">🌙</button>
<script>
const toggle = document.getElementById("theme-toggle");
const html = document.documentElement;
// Load saved preference or detect system preference
const savedTheme = localStorage.getItem("theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (savedTheme === "dark" || (!savedTheme && prefersDark)) {
html.classList.add("dark");
}
toggle?.addEventListener("click", () => {
html.classList.toggle("dark");
localStorage.setItem("theme", html.classList.contains("dark") ? "dark" : "light");
});
</script>
With Tailwind, enable darkMode: "class" in config.
Performance Best Practices
Partial Hydration: Use
client:*directives only where neededclient:load- Hydrate immediatelyclient:idle- Hydrate when browser is idleclient:visible- Hydrate when in viewportclient:media- Hydrate on media query matchclient:only="react"- Skip server render, client-only
Image Optimization: Use Astro's
<Image />componentKeep Static Where Possible: Islands architecture means most content remains static HTML
Asset Fingerprinting: Automatic in production builds
Prefetching: Auto-load links before user clicks
// 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).
- Critical CSS: Inline above-the-fold CSS with astro-critters
npm install astro-critters
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
- Build: Run
npm run buildand verifydist/output - Preview: Test with
npm run previewat localhost:4321 - Configure: Set
site,base, andtrailingSlashin astro.config.mjs - Platform Setup: Initialize hosting (e.g.,
firebase init hosting) - Deploy: Push to platform (
firebase deploy,vercel, or git push) - Verify: Check live URL, test 404 page, validate assets load
Quick Deploy Commands
# 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:
// astro.config.mjs
export default defineConfig({
site: 'https://username.github.io',
base: '/repo-name'
});
Firebase Hosting:
npm install -g firebase-tools
firebase login
firebase init hosting # Set public to 'dist'
npm run build
firebase deploy
firebase.json (recommended configuration):
{
"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:
// 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 buildcompletes without errors -
npm run previewshows site correctly at localhost:4321 - All Content Collection schemas validate (
astro check) - Images use
<Image />component or are inpublic/ - SEO metadata present on all pages (title, description, og:*)
- 404.astro page exists and renders correctly
-
basepath configured if deploying to subdirectory - Environment variables set on deployment platform
-
trailingSlashsetting 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
# 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
npm install -D vitest @vitest/ui @astrojs/testing
// vitest.config.ts
import { getViteConfig } from 'astro/config';
export default getViteConfig({
test: {
include: ['src/**/*.test.ts'],
},
});
// 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
npm install -D @playwright/test
npx playwright install
// 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/);
});
// package.json
{
"scripts": {
"test": "vitest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
}
}
Link Checking
Validate internal links don't break:
npm install -D linkinator
npx linkinator dist --recurse
Add to CI:
# .github/workflows/links.yml
- run: npm run build
- run: npx linkinator dist --recurse --skip "^(?!http://localhost)"
.astro File Anatomy
---
// 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');
---
<!-- Template: HTML with JSX expressions -->
<Layout title={title}>
<h1>{title}</h1>
<ul>
{posts.map(post => (
<li><a href={`/blog/${post.slug}`}>{post.data.title}</a></li>
))}
</ul>
</Layout>
<style>
/* Scoped to this component */
h1 { color: navy; }
</style>
<script>
// Client-side JavaScript
console.log('Runs in browser');
</script>
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:
---
export function getStaticPaths() {
return [
{ params: { slug: 'post-1' } },
{ params: { slug: 'post-2' } }
];
}
---
SEO Essentials
Manual Approach
---
const { title, description, image } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<head>
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:type" content="website" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
</head>
astro-seo (Simplified)
npm install astro-seo
---
import { SEO } from 'astro-seo';
---
<head>
<SEO
title="Page Title"
description="Page description"
openGraph={{
basic: {
title: "OG Title",
type: "website",
image: "/og-image.png",
}
}}
twitter={{ creator: "@handle" }}
/>
</head>
Handles meta tags, Open Graph, Twitter Cards, and canonical URLs automatically.
Essential Integrations
# 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
basepath 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 checkfor validation details - Ensure frontmatter matches Zod schema exactly
Assets not loading
- Use
importfor processed assets - Use
public/for unprocessed static files
References
For detailed guides on specific topics, see:
references/markdown-deep-dive.md- Advanced Markdown/MDX patternsreferences/deployment-platforms.md- Platform-specific deployment details