Agent Skills: Publishing Astro Websites

|

web-developmentID: SpillwaveSolutions/publishing-astro-websites-agentic-skill/publishing-astro-websites

Install this agent skill to your local

pnpm dlx add-skill https://github.com/SpillwaveSolutions/publishing-astro-websites-agentic-skill/tree/HEAD/publishing-astro-websites

Skill Files

Browse the full folder contents for publishing-astro-websites.

Download Skill

Loading file tree…

publishing-astro-websites/SKILL.md

Skill Metadata

Name
publishing-astro-websites
Description
|

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

# 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:

  1. CSS Variables - Let browser resolve colors at runtime:
// mermaid config
mermaid.initialize({
  theme: 'base',
  themeVariables: {
    primaryColor: 'var(--diagram-primary)',
    lineColor: 'var(--diagram-line)'
  }
});
  1. 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>
  1. 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

  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 <Image /> 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

// 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).

  1. 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

  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

# 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 build completes without errors
  • [ ] npm run preview shows site correctly at localhost:4321
  • [ ] All Content Collection schemas validate (astro check)
  • [ ] Images use <Image /> 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

# 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 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