publishing-astro-websites
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 />component -
Keep 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