astro-sites-manager
Comprehensive skill for building, migrating, and maintaining Astro v7 projects. Covers best practices from the official AGENTS.md, the v6→v7 migration path, validation of breaking/deprecated patterns, AI-enhanced dev server usage (background mode, JSON logging), advanced routing
Install
npx skills add https://github.com/fabricioctelles/skills/tree/main/skills/astro-sites-manager
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install fabricioctelles-skills@llmmart
git clone https://github.com/fabricioctelles/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole fabricioctelles/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Astro Framework — v7
MCP Documentation Access
This skill works alongside the Astro Docs MCP server. Before answering Astro questions, check if the astro-docs MCP tool is available and query it for the latest documentation. The MCP server provides real-time access to docs.astro.build and is the single source of truth for current APIs.
MCP Server: astro-docs
Tool: search_astro_docs
If the MCP server is unavailable, fall back to the reference material in this skill and https://docs.astro.build.
Best Practices
Component Design
- One
.astrocomponent per file. Keep components small and focused. - Use frontmatter (
---) for data fetching and logic; template below for markup only. - Prefer Astro components over framework components unless client interactivity is needed.
- Use
client:*directives sparingly — each adds JavaScript to the bundle. - Directive hierarchy:
client:idle>client:visible>client:load(prefer lazy).
Routing & Pages
- Use file-based routing in
src/pages/. Dynamic routes:[slug].astro,[...path].astro. - Always export
getStaticPaths()for prerendered dynamic routes. - For SSR pages:
export const prerender = falseat the top. - Use
src/fetch.ts(v7) only when you need control beyond middleware — don't use it for simple auth.
Content Collections
- Define schemas in
content.config.tswith Zod — never trust untyped content. - Use
getCollection()for lists,getEntry()for single items. - Prefer
glob()loader for local files, custom loaders for CMS data.
Performance
- Default to static (
prerender = true). Use SSR only for personalized/dynamic content. - Use
<Image />fromastro:assets— never raw<img>for local images. - Prefer Sätteri (default v7) over unified for Markdown — it's significantly faster.
- Use Server Islands (
server:defer) for mixing static shells with dynamic fragments.
Styling
- Scoped
<style>in.astrofiles is default and preferred. - Use
is:globalonly when truly needed (third-party component styling). - Tailwind: install with
astro add tailwind, don't configure manually.
TypeScript
- Run
astro syncafter changing content schemas or env variables. - Run
astro checkbefore committing — catches template type errors other tools miss. - Use
astro:env/serverandastro:env/clientfor typed env variables (neverprocess.envdirectly).
Development Workflow
- Use
astro devfor HMR. Never usepython -m http.serveror other static servers. - Use
astro addfor official integrations — don't manually edit config for them. - Use
astro build && astro previewto test production behavior locally. - In AI agent workflows: use
astro dev --backgroundand validate via/_astro/status.
CLI Commands
npx astro dev # Dev server (foreground)
npx astro dev --background # Dev server (detached, for AI agents)
npx astro dev --json # Dev server with JSON structured logs
npx astro build # Production build
npx astro preview # Serve production build locally
npx astro check # Type checking and diagnostics
npx astro sync # Generate TypeScript types
npx astro add <integration># Install and configure integration
Background Dev Server (AI Agents)
When working as an AI agent, use background mode:
# Start (blocks until ready, then detaches)
astro dev --background
# → Dev server running at http://localhost:4321 (pid 12345)
# Check status
astro dev status
# Read logs
astro dev logs
# Stop
astro dev stop
# Health check endpoint (JSON)
curl http://localhost:4321/_astro/status
# → {"ok": true}
Key behaviors:
- Lockfile prevents duplicate instances — starting again returns existing instance
- All commands are idempotent (stop when not running = silent success)
- Auto-detected when running inside an AI agent (no flag needed)
- Opt out:
ASTRO_DEV_BACKGROUND=0 astro dev
Project Structure
src/
├── pages/ # File-based routing (.astro, .md, .mdx)
├── layouts/ # Reusable page layouts
├── components/ # Astro & framework components
├── content/ # Content collections (type-safe)
├── middleware.ts # Request middleware
├── fetch.ts # Advanced routing (v7, optional)
├── styles/ # Global CSS
├── assets/ # Optimized assets (images, fonts)
├── actions/ # Server actions
└── env.d.ts # Environment type declarations
astro.config.mjs # Main configuration
content.config.ts # Content collection schemas
tsconfig.json # TypeScript config
Configuration (v7)
import { defineConfig, memoryCache, logHandlers } from 'astro/config';
export default defineConfig({
// Output mode
output: 'static', // or configure per-page with server adapter
// Route caching (stable in v7)
cache: {
provider: memoryCache(),
},
routeRules: {
'/blog/[...path]': { maxAge: 300, swr: 60 },
},
// Logger (stable in v7)
logger: logHandlers.json(), // or .console(), or .compose(...)
// Markdown (Sätteri is default in v7)
markdown: {
// No config needed for defaults (GFM, smartypants, heading IDs)
// For extra features:
// processor: satteri({ features: { directive: true, math: true } })
},
// Advanced routing file (default: src/fetch.ts)
// fetchFile: null, // disable if src/fetch.ts is used for other purposes
// Whitespace (v7 default: 'jsx')
compressHTML: 'jsx', // or true (v6 behavior), or false (preserve all)
});
Islands Architecture (client:* directives)
<!-- Load immediately (interactive above the fold) -->
<Counter client:load />
<!-- Load when browser is idle (non-critical interactivity) -->
<Newsletter client:idle />
<!-- Load when scrolled into viewport (below the fold) -->
<Comments client:visible />
<!-- Load on media query match (mobile-only widget) -->
<MobileMenu client:media="(max-width: 768px)" />
<!-- Client-only, skip SSR entirely (browser APIs needed) -->
<MapWidget client:only="react" />
<!-- Server Island: static shell, fetched at request time (v6+) -->
<UserGreeting server:defer />
Decision guide: No directive (default) = zero JS, static HTML. Add directive only when user interaction is required.
Image Optimization
---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---
<!-- Local image (optimized, lazy-loaded, responsive) -->
<Image src={heroImage} alt="Hero" width={1200} />
<!-- Remote image (must allowlist domain in config) -->
<Image src="https://cdn.example.com/photo.jpg" alt="Photo" width={800} height={600} />
Config for remote images:
// astro.config.mjs
image: {
domains: ['cdn.example.com'],
remotePatterns: [{ protocol: 'https', hostname: '**.cloudinary.com' }],
}
Detailed References
- Install MCP Server — Setup Astro Docs MCP for any AI tool (Kiro, Claude, Cursor, VS Code, etc.)
- Migration Guide v6→v7 — Step-by-step upgrade plan with breaking changes checklist
- Validation Checklist — Verify installation, detect breaking/deprecated patterns
- AI Dev Server — Background mode, JSON logging, agent detection
- Astro v7 Features — Rust compiler, Sätteri, Advanced Routing, Route Caching, CDN providers
- Astro v6 Features — Content Collections v2, Actions, Sessions, Server Islands, env module
- Related Content — Vector embeddings para posts relacionados, deploy leve no Coolify sem modelo
- Testing — Vitest components, Playwright E2E, link checking, CI pipeline
- SEO Full Stack — JSON-LD graph, agent discovery, IndexNow, OG images, build-time validation, performance
- Starlight & Patterns — Docs sites, Pagefind search, i18n, pagination, RSS
- Deployment — Cloudflare, Vercel, Netlify, Firebase, GitHub Pages, Docker/Coolify, Azure
- Coolify Deploy — Self-hosted deploy on Coolify (Dockerfile, API, gotchas, recommended stack)
Files (skills)
-
references
-
ai-dev-server.md 4.1 KB
# AI Dev Server Guide Reference for AI agents interacting with the Astro development server programmatically. --- ## 1. Background Mode Start the dev server as a detached background process that blocks until the server is fully ready to accept requests: ```bash astro dev --background ``` ### Auto-Detection Astro automatically detects AI agent environments and enables background mode without explicit flags. This applies to known CI/agent runtimes. ### Lockfile A lockfile at `.astro/dev.json` prevents duplicate server instances. If a server is already running, the lockfile ensures a second `astro dev --background` call returns the existing instance info instead of spawning a new process. ### Commands | Command | Description | |---------|-------------| | `astro dev --background` | Start detached server, block until ready | | `astro dev stop` | Stop the running background server | | `astro dev status` | Check if a background server is running | | `astro dev logs` | Stream logs from the background server | ### Opt-Out Disable automatic background mode by setting the environment variable: ```bash ASTRO_DEV_BACKGROUND=0 astro dev ``` --- ## 2. Health Endpoint Verify the dev server is ready before making requests: ``` GET /_astro/status ``` Response: ```json {"ok": true} ``` > **Important:** This endpoint is only available in development mode. It does not exist in production builds. ### Usage ```bash curl http://localhost:4321/_astro/status ``` Wait for a `200` response with `{"ok": true}` before issuing any page requests. --- ## 3. JSON Logging Enable structured JSON output for machine-readable log parsing: ```bash astro dev --json ``` ### Configuration in `astro.config.mjs` ```js import { logHandlers } from 'astro'; export default defineConfig({ logger: logHandlers.json(), }); ``` ### Compose Multiple Handlers Output to both console and JSON simultaneously: ```js import { logHandlers } from 'astro'; export default defineConfig({ logger: logHandlers.compose( logHandlers.console(), logHandlers.json() ), }); ``` ### Auto-Enabled JSON logging is automatically enabled when an AI agent environment is detected. ### Use Cases - Error parsing — structured error objects with file, line, column - Build status — track compilation progress programmatically - HMR events — detect when hot module replacement completes after file changes --- ## 4. Agent Workflow Step-by-step workflow for AI agents developing with Astro: ```bash # 1. Start the dev server in background (blocks until ready) astro dev --background # 2. Verify the server is ready curl http://localhost:4321/_astro/status # 3. Make changes to source files # (edit .astro, .ts, .css files as needed) # 4. Verify output after HMR processes changes curl http://localhost:4321/page-to-test # 5. Cleanup when done astro dev stop ``` ### Notes - Step 2 should return `{"ok": true}` before proceeding. - After step 3, wait briefly for HMR to process before step 4. - Always run step 5 to avoid orphaned processes. --- ## 5. Idempotency Rules The dev server commands are designed to be safely called multiple times: | Scenario | Behavior | |----------|----------| | Start when already running | Returns existing instance info (port, PID) | | Stop when not running | Silent success (exit code 0) | | Crash or unexpected termination | Lockfile is cleaned up, no zombie processes | These guarantees mean agents can call `astro dev --background` at the start of every task without checking current state first, and call `astro dev stop` at cleanup without error handling. --- ## 6. MCP Integration The Astro Docs MCP server provides real-time documentation access: - **Endpoint:** `https://mcp.docs.astro.build/mcp` - **Tool:** `search_astro_docs` ### Usage Always query the MCP server for the latest API details, configuration options, and component references rather than relying on cached knowledge. ``` search_astro_docs("dev server background mode") search_astro_docs("content collections config") ``` This ensures agents work with current documentation even as Astro's API evolves between versions. -
coolify-deploy.md 14.2 KB
# Deploying Astro on Coolify Production-tested patterns for deploying Astro sites on self-hosted Coolify (v4.x). Based on 17+ live deployments. --- ## Build Pack Decision | Scenario | build_pack | Notes | |----------|-----------|-------| | Astro v6+ (requires Node ≥22.12.0) | `dockerfile` | Nixpacks can't pin minor version | | Astro v5 or earlier | `nixpacks` | `NIXPACKS_NODE_VERSION=22` works | | Astro `output: 'static'` with package.json | `nixpacks` | start: `npx serve dist -l 80 -s` | | Astro `output: 'static'` (Dockerfile) | `dockerfile` | nginx serves directly | | HTML/CSS only (no package.json) | `static` | `static_image: nginx:alpine` | **Rule:** For Astro v6+ and v7, always use `dockerfile`. Nixpacks resolves Node 22.11.0 from its internal nixpkgs archive, but Astro v6+ requires ≥22.12.0. **Astro v7 Docker base image rule:** Use `node:22-slim` (Debian/glibc) for the build stage, NOT `node:22-alpine`. Sätteri's native binding only supports glibc. The runtime stage can still use Alpine/Caddy since it only serves files. --- ## Dockerfile — Astro SSR (Node Adapter) > **Requires `@astrojs/node@^11.0.0`** for Astro v7. The v10 adapter crashes at runtime with `TypeError: app.getAdapterLogger is not a function`. ```dockerfile # Use node:22-slim (NOT alpine) — Sätteri needs glibc for Astro v7 FROM node:22-slim AS build WORKDIR /app ENV NODE_OPTIONS="--max-old-space-size=512" # Coolify injects env vars as ARG — must convert to ENV for npm run build ARG MY_API_KEY ARG PUBLIC_SITE_URL ENV MY_API_KEY=$MY_API_KEY ENV PUBLIC_SITE_URL=$PUBLIC_SITE_URL COPY package*.json .npmrc ./ RUN npm ci COPY . . RUN npm run build FROM node:22-slim WORKDIR /app COPY --from=build /app/dist ./dist COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/package.json ./ ENV HOST=0.0.0.0 ENV PORT=4321 EXPOSE 4321 CMD ["node", "dist/server/entry.mjs"] ``` ## Dockerfile — Astro Static (nginx) ```dockerfile # Use node:22-slim (NOT alpine) — Sätteri needs glibc FROM node:22-slim AS build WORKDIR /app ENV NODE_OPTIONS="--max-old-space-size=512" COPY package*.json .npmrc ./ RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80 ``` --- ## Critical Gotchas ### ARG vs ENV — Build-time secrets Coolify injects variables as Docker `ARG`. But `ARG` does NOT become an environment variable for child processes like `npm run build`. Astro/Vite resolves `import.meta.env.VAR` during build — if the variable doesn't exist in the process environment, it silently becomes `undefined`. **Fix:** For every secret the build needs: ```dockerfile ARG RESEND_API_KEY ENV RESEND_API_KEY=$RESEND_API_KEY ``` ### OOM on Resource-Limited Servers The `astro build` process can die with exit code 255 and no clear error message on servers with limited RAM (~2GB). **Fix:** Add to build stage: ```dockerfile ENV NODE_OPTIONS="--max-old-space-size=512" ``` ### Nixpacks Node Version Nixpacks only accepts **major version**. `NIXPACKS_NODE_VERSION=22` can resolve to 22.11.0, causing: ``` Node.js v22.11.0 is not supported by Astro! ``` **Fix options:** 1. Use Dockerfile instead (recommended for Astro v6+) 2. Set `NIXPACKS_NODE_VERSION=24` (skips a major) 3. Pin nixpkgs archive via `nixpacks.toml`: ```toml [phases.setup] nixpkgsArchive = "5ef6c8a1bf89a0bfe4e15e7baf5bab7feeff86a5" ``` ### Nixpacks Timeout Nixpacks downloads ~600MB nixpkgs archive during build. On servers with limited bandwidth, build dies silently during `unpacking` step. **Fix:** Switch to Dockerfile. `node:22-alpine` is ~50MB vs ~600MB. ### Sätteri Native Binding on Alpine (Astro v7) Astro v7 uses Sätteri (Rust-based Markdown) by default. Sätteri ships native bindings but **only for glibc** (`@bruits/satteri-linux-x64-gnu`). Alpine uses musl libc — no musl binding exists, and the WASM fallback has a cpu platform check that also fails. ``` Cannot find module '@bruits/satteri-linux-x64-musl' ``` **Fix:** Use `node:22-slim` (Debian/glibc) for the build stage. The runtime stage can still use Alpine since it only serves static files: ```dockerfile FROM node:22-slim AS build # glibc — satteri works WORKDIR /app COPY package*.json .npmrc ./ RUN npm ci COPY . . RUN npm run build FROM caddy:2-alpine # runtime doesn't need Node COPY --from=build /app/dist /srv ``` **Affected projects:** Any Astro v7 project using Sätteri (default) or Starlight 0.40+ on Alpine. **Not affected:** Projects using `unified()` processor explicitly (they bypass Sätteri). ### Sätteri Native Binding on ARM64 (Cross-Platform Lockfile) When the dev machine is x86_64 but the Coolify build server is ARM64 (e.g., OCI Ampere), `npm ci` and even `npm install --include=optional` fail with: ``` Cannot find module '@bruits/satteri-linux-arm64-gnu' Require stack: - /app/node_modules/satteri/index.js ``` **Root cause:** The `package-lock.json` was generated on x86_64 and only includes `@bruits/satteri-linux-x64-gnu` in its optional dependency tree. npm respects the lockfile's platform resolution even on a different architecture — this is [npm bug #4828](https://github.com/npm/cli/issues/4828). **What does NOT work:** - `.npmrc` with `include=optional` — npm still reads the lockfile's platform tree - `npm install --include=optional` in Dockerfile — lockfile still constrains resolution - Adding `@bruits/satteri-linux-arm64-gnu` to `optionalDependencies` — npm may still skip it **Fix:** Do NOT copy `package-lock.json` into the Docker build. Let npm resolve fresh on arm64: ```dockerfile FROM node:22-slim AS build WORKDIR /app ENV NODE_OPTIONS="--max-old-space-size=512" COPY package.json .npmrc ./ # Deliberately omit package-lock.json — forces fresh resolution on arm64 RUN npm install COPY . . RUN npm run build FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80 ``` **Tradeoff:** Build is slightly less deterministic (no lockfile pinning in Docker). For static sites this is acceptable. For SSR with strict reproducibility needs, generate the lockfile inside an arm64 container instead. **Affected:** Any Astro v7 project building on ARM64 servers when lockfile was generated on x86_64. **Confirmed working:** valeria.med.br on OCI Ampere A1 via Coolify (2026-07-03). ### legacy-peer-deps and npm ci in Docker When using `--legacy-peer-deps` locally (required for Astro v7 due to transient peer dep conflicts in Starlight plugins), Docker's `npm ci` will fail unless the `.npmrc` is copied into the container. **Fix:** Always copy `.npmrc` before `npm ci`: ```dockerfile COPY package*.json .npmrc ./ RUN npm ci ``` The `.npmrc` must contain: ``` legacy-peer-deps=true ``` ### @astrojs/node Must Be v11+ for Astro v7 Astro v7's runtime API changed — `app.getAdapterLogger()` was added and the standalone entry module depends on it. If `@astrojs/node` stays at v10, the container builds fine but **crashes at startup**: ``` TypeError: app.getAdapterLogger is not a function at createAppHandler (dist/server/entry.mjs) ``` Coolify shows `restarting:unknown` or `exited:unhealthy` — the build log looks green, but the container crash-loops. **Fix:** Always upgrade `@astrojs/node` to v11 together with Astro v7: ```bash npm install astro@latest @astrojs/node@latest ``` **Checklist for SSR v7 migration:** - `astro` → `^7.0.0` - `@astrojs/node` → `^11.0.0` - `@astrojs/mdx` → `^7.0.0` (if used) ### pnpm approve-builds in Docker (pnpm 11.9+) pnpm 11.9+ blocks install scripts (postinstall, install) by default. Packages like `esbuild` and `sharp` need native binaries built after install. Without approval, `pnpm install --frozen-lockfile` fails: ``` [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.28.1, sharp@0.34.5 Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts. ``` **Fix:** Run `pnpm approve-builds` locally, which creates `pnpm-workspace.yaml` with: ```yaml allowBuilds: esbuild: true sharp: true ``` Then **copy `pnpm-workspace.yaml` into the Docker container** alongside the lockfile: ```dockerfile COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ RUN pnpm install --frozen-lockfile ``` Missing this file = build fails in CI/Docker but works locally (because local node_modules already has the binaries). ### package-lock.json Desync After Major Upgrade After `npm install --legacy-peer-deps` for a major version upgrade, the lockfile may reference packages that `npm ci` (strict mode) cannot resolve. Symptoms: `npm ci` fails with "lock file's X does not satisfy Y". **Fix:** Delete lockfile and regenerate: ```bash rm package-lock.json node_modules -rf npm install --legacy-peer-deps # Then test: npm ci must pass ``` ### Integrations That Download ML Models (transformers.js, ONNX) Integrations like `@philnash/astro-related-content` download ONNX models (~300MB) during `astro build` to generate embeddings. On Coolify servers with limited bandwidth/disk, this causes 20+ minute builds or disk exhaustion. **Pattern:** Generate artifacts locally, commit them, skip the heavy integration in CI. **Fix:** Dual-mode config with `ENV CI=true` in Dockerfile. See [Related Content reference](references/related-content.md) for complete implementation. **Key principle:** - **Local:** Integration runs fully (downloads model, generates embeddings) - **CI/Docker:** Vite plugin serves pre-built `data.json` (zero model download) - **Cache commitado:** `.astro-related-content/data.json` + `vectors.json` go in git (~750KB for 32 posts) This pattern applies to ANY integration that downloads large artifacts at build time. --- ## Coolify API — Create App ```bash COOLIFY_URL="https://cool.example.com/api/v1" COOLIFY_KEY="your-token" curl -sS -X POST "$COOLIFY_URL/applications/private-deploy-key" \ -H "Authorization: Bearer $COOLIFY_KEY" \ -H "Content-Type: application/json" \ -d '{ "project_uuid": "PROJECT_UUID", "environment_name": "production", "server_uuid": "SERVER_UUID", "private_key_uuid": "SSH_KEY_UUID", "git_repository": "git@gitlab.com:user/project.git", "git_branch": "main", "build_pack": "dockerfile", "dockerfile_location": "/Dockerfile", "ports_exposes": "4321", "name": "my-astro-site" }' ``` ### Set Domain ```bash # Use "domains", NOT "fqdn" — fqdn returns "field not allowed" curl -sS -X PATCH "$COOLIFY_URL/applications/$APP_UUID" \ -H "Authorization: Bearer $COOLIFY_KEY" \ -H "Content-Type: application/json" \ -d '{"domains": "https://mysite.com"}' ``` ### Set Environment Variables ```bash curl -sS -X POST "$COOLIFY_URL/applications/$APP_UUID/envs" \ -H "Authorization: Bearer $COOLIFY_KEY" \ -H "Content-Type: application/json" \ -d '{"key": "MY_VAR", "value": "secret-value", "is_preview": false}' ``` > Do NOT send `is_build_time` — API rejects it. ### GitLab Webhook (auto-deploy on push) ```bash curl -sS -X POST "https://gitlab.com/api/v4/projects/$PROJECT_ID/hooks" \ -H "PRIVATE-TOKEN: $GITLAB_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://cool.example.com/webhooks/source/gitlab/events/manual", "token": "WEBHOOK_SECRET_FROM_APP", "push_events": true, "enable_ssl_verification": true }' ``` > **Critical:** The secret goes in the `"token"` field (sent as `X-Gitlab-Token` header), NEVER as `?secret=` query parameter in the URL. With secret in URL, Coolify returns 200 but does NOT trigger deploy. ### Deploy and Validate ```bash # Trigger deploy (Coolify 4.1+ — uses GET, NOT POST) # The /deploy endpoint accepts uuid as query param, force=true rebuilds from scratch curl -sS -X GET "$COOLIFY_URL/deploy?uuid=$APP_UUID&force=true" \ -H "Authorization: Bearer $COOLIFY_KEY" # Returns: {"deployments":[{"message":"Application X deployment queued.","resource_uuid":"...","deployment_uuid":"..."}]} # ⚠️ POST /applications/$UUID/deploy returns "Not found" in Coolify 4.1 # ⚠️ POST /applications/$UUID/restart only restarts existing container (no rebuild) # Use restart when image is already built. Use /deploy?uuid=...&force=true for rebuild. # Check status (~60s wait for build + container start) curl -sS -H "Authorization: Bearer $COOLIFY_KEY" \ "$COOLIFY_URL/applications/$APP_UUID" | python3 -c " import sys,json; d=json.load(sys.stdin); print(d['status'])" # Verify HTTP response curl -sS -o /dev/null -w "HTTP %{http_code}\n" https://mysite.com ``` **Status interpretation:** | Status | Meaning | |--------|---------| | `running:healthy` | Container up and health check passing | | `running:unknown` | Container up, no health check configured | | `restarting:unknown` | Container crash-looping — check runtime logs | | `exited:unhealthy` | Container stopped — likely build or startup failure | --- ## Astro Config for Coolify SSR ```typescript // astro.config.mjs import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; export default defineConfig({ output: 'server', // or hybrid with per-page prerender adapter: node({ mode: 'standalone' }), server: { host: '0.0.0.0', port: 4321 }, }); ``` For static output, no adapter needed — the Dockerfile handles nginx serving. --- ## Port Configuration | Output Mode | Port | CMD | |-------------|------|-----| | SSR (Node adapter) | 4321 | `node dist/server/entry.mjs` | | Static (nginx) | 80 | nginx default | | Static (serve) | 80 | `npx serve dist -l 80 -s` | Set `ports_exposes` in Coolify to match. --- ## Recommended Stack (Homelab-Tested) Based on 17 production Astro sites: ```javascript import seoGraph from '@jdevalk/astro-seo-graph/integration'; import agentmarkup from '@agentmarkup/astro'; import UnoCSS from '@unocss/astro'; import critters from 'astro-critters'; import compress from '@playform/compress'; // Key: compress() MUST be last integration integrations: [mdx(), UnoCSS(), sitemap(), seoGraph(), agentmarkup(), critters(), compress()] ``` | Tool | Why | |------|-----| | UnoCSS > Tailwind | 5x faster build, smaller bundle | | @playform/compress > astro-compress | Better maintained | | astro-critters | Critical CSS inlining | | @jdevalk/astro-seo-graph | All-in-one SEO (replaces astro-seo + robots-txt + indexnow) | | @agentmarkup/astro | LLM visibility (llms.txt, markdown mirrors) | | Plausible > GA4 | 1kb script, no cookie banner, self-hosted | | @philnash/astro-related-content | Semantic related posts via local embeddings (CI: use prebuilt data.json) | -
deployment.md 8 KB
# Deployment Guide > Astro 7 deployment across all major platforms. Covers adapters, route caching, and platform-specific gotchas. --- ## 1. General Build ```bash astro build # output in dist/ astro preview # test production build locally ``` Key config in `astro.config.mjs`: ```js import { defineConfig } from 'astro/config'; export default defineConfig({ site: 'https://example.com', base: '/', trailingSlash: 'never', // 'always' | 'never' | 'ignore' }); ``` - `site` — full production URL (required for sitemaps, canonical URLs, RSS) - `base` — subpath when deploying to a subdirectory (e.g., `/docs`) - `trailingSlash` — MUST match hosting platform expectations to avoid redirect loops --- ## 2. Cloudflare Pages **Adapter:** `@astrojs/cloudflare` ```bash npx astro add cloudflare ``` ```js // astro.config.mjs import { defineConfig } from 'astro/config'; import cloudflare from '@astrojs/cloudflare'; export default defineConfig({ output: 'server', adapter: cloudflare(), }); ``` **Route Caching (private beta):** ```js // astro.config.mjs import { cacheCloudflare } from '@astrojs/cloudflare/cache'; export default defineConfig({ output: 'server', adapter: cloudflare(), experimental: { serverIslands: true, }, routeCache: cacheCloudflare(), }); ``` **Deploy:** - Connect git repo in Cloudflare Dashboard → Pages → Create a project - Build command: `astro build` - Build output directory: `dist` - Node.js compatibility flag is set automatically by the adapter --- ## 3. Vercel **Adapter:** `@astrojs/vercel` ```bash npx astro add vercel ``` ```js // astro.config.mjs import { defineConfig } from 'astro/config'; import vercel from '@astrojs/vercel'; import { cacheVercel } from '@astrojs/vercel/cache'; export default defineConfig({ output: 'server', adapter: vercel(), routeCache: cacheVercel(), }); ``` **ISR via routeRules:** ```js // astro.config.mjs export default defineConfig({ output: 'server', adapter: vercel({ isr: true, // enable ISR globally }), routeCache: cacheVercel({ routeRules: { '/blog/**': { revalidate: 60 }, // revalidate every 60s '/static/**': { prerender: true }, // fully static at build }, }), }); ``` **Deploy:** - Connect repo via Vercel Dashboard or `vercel` CLI - Framework preset: Astro (auto-detected) --- ## 4. Netlify **Adapter:** `@astrojs/netlify` ```bash npx astro add netlify ``` ```js // astro.config.mjs import { defineConfig } from 'astro/config'; import netlify from '@astrojs/netlify'; import { cacheNetlify } from '@astrojs/netlify/cache'; export default defineConfig({ output: 'server', adapter: netlify(), routeCache: cacheNetlify(), }); ``` **Deploy:** - Connect repo in Netlify Dashboard - Build command: `astro build` - Publish directory: `dist` - Functions auto-detected from adapter output --- ## 5. Firebase Hosting **Static only** — no adapter needed for SSG output. ```js // astro.config.mjs export default defineConfig({ output: 'static', trailingSlash: 'never', // CRITICAL: must match Firebase config }); ``` **firebase.json:** ```json { "hosting": { "public": "dist", "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], "trailingSlash": false, "rewrites": [ { "source": "**", "destination": "/404.html" } ] } } ``` **Deploy:** ```bash astro build firebase deploy --only hosting ``` ### CRITICAL: trailingSlash Alignment Mismatch between Firebase and Astro causes **infinite redirect loops**. | Firebase `trailingSlash` | Astro `trailingSlash` | Result | |---|---|---| | `false` | `'never'` | ✅ Works | | `true` | `'always'` | ✅ Works | | `false` | `'always'` | ❌ Redirect loop | | `true` | `'never'` | ❌ Redirect loop | --- ## 6. GitHub Pages **Static output only** — no adapter needed. ```js // astro.config.mjs export default defineConfig({ site: 'https://username.github.io', base: '/repo-name', // omit for username.github.io root output: 'static', }); ``` **GitHub Actions workflow** (`.github/workflows/deploy.yml`): ```yaml name: Deploy to GitHub Pages on: push: branches: [main] permissions: contents: read pages: write id-token: write jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - run: npm ci - run: npm run build - uses: actions/upload-pages-artifact@v3 with: path: dist deploy: needs: build runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment uses: actions/deploy-pages@v4 ``` --- ## 7. Docker / Self-Hosted (Coolify) **Adapter:** `@astrojs/node` ```bash npx astro add node ``` ```js // astro.config.mjs import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone', }), server: { host: '0.0.0.0', // REQUIRED for Docker port: 4321, }, }); ``` **Dockerfile:** ```dockerfile FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:22-alpine AS runtime WORKDIR /app COPY --from=build /app/dist ./dist COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/package.json ./ ENV HOST=0.0.0.0 ENV PORT=4321 EXPOSE 4321 HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:4321/api/health || exit 1 CMD ["node", "./dist/server/entry.mjs"] ``` **Health check endpoint** (`src/pages/api/health.ts`): ```ts import type { APIRoute } from 'astro'; export const GET: APIRoute = () => { return new Response(JSON.stringify({ status: 'ok' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); }; ``` **Coolify:** Set Dockerfile build pack, expose port 4321, configure health check to `/api/health`. --- ## 8. Azure Static Web Apps Works with **static output** (SSG). For SSR, use Azure Functions integration. **GitHub Actions workflow** (`.github/workflows/azure-swa.yml`): ```yaml name: Azure Static Web Apps on: push: branches: [main] jobs: build_and_deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - run: npm ci - run: npm run build - uses: Azure/static-web-apps-deploy@v1 with: azure_static_web_apps_api_token: ${{ secrets.AZURE_SWA_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }} action: upload app_location: / output_location: dist skip_app_build: true ``` The `skip_app_build: true` pattern means we build ourselves (for control over Node version and env vars) and only upload the output. **staticwebapp.config.json:** ```json { "navigationFallback": { "rewrite": "/404.html" }, "globalHeaders": { "X-Frame-Options": "DENY", "X-Content-Type-Options": "nosniff" }, "routes": [ { "route": "/api/*", "allowedRoles": ["authenticated"] } ] } ``` For **private registry auth** (private npm packages): ```yaml - run: | echo "//npm.pkg.github.com/:_authToken=${{ secrets.NPM_TOKEN }}" >> .npmrc - run: npm ci ``` --- ## 9. Pre-Deploy Checklist - [ ] `astro build` exits 0 - [ ] `astro check` reports no errors - [ ] `astro preview` works correctly (test production build locally) - [ ] Images use `<Image/>` component or are in `public/` - [ ] SEO metadata present on all pages (title, description, og tags) - [ ] `src/pages/404.astro` exists - [ ] Environment variables set on target platform - [ ] `trailingSlash` matches hosting platform expectations - [ ] Sitemap generating correctly (`@astrojs/sitemap`) - [ ] RSS feed working if applicable (`@astrojs/rss`) - [ ] Route caching configured for SSR pages (platform-specific cache helper) - [ ] `robots.txt` present and correct - [ ] HTTPS redirect configured on platform - [ ] Custom domain DNS configured and propagated -
install-mcp.md 3.2 KB
# Installing the Astro Docs MCP Server The Astro Docs MCP server provides real-time access to the latest Astro documentation via the Model Context Protocol. - **URL:** `https://mcp.docs.astro.build/mcp` - **Transport:** Streamable HTTP - **Tool:** `search_astro_docs` - **Source:** Open-source, powered by kapa.ai --- ## By Tool ### Kiro CLI ```bash kiro-cli mcp add --name astro-docs --scope global --command npx --args "-y" --args "mcp-remote" --args "https://mcp.docs.astro.build/mcp" ``` Or create/edit `~/.kiro/settings/mcp.json`: ```json { "mcpServers": { "astro-docs": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.docs.astro.build/mcp"], "env": {} } } } ``` ### Claude Code CLI ```bash claude mcp add --transport http astro-docs https://mcp.docs.astro.build/mcp ``` ### Codex CLI Add to `~/.codex/config.toml`: ```toml [mcp_servers.astro-docs] command = "npx" args = ["-y", "mcp-remote", "https://mcp.docs.astro.build/mcp"] ``` ### Cursor Use the deeplink or add to `.cursor/mcp.json`: ```json { "mcpServers": { "Astro docs": { "type": "http", "url": "https://mcp.docs.astro.build/mcp" } } } ``` ### VS Code (Copilot Chat) Add to `.vscode/mcp.json`: ```json { "mcpServers": { "Astro docs": { "type": "http", "url": "https://mcp.docs.astro.build/mcp" } } } ``` ### Windsurf Edit `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "Astro docs": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.docs.astro.build/mcp"] } } } ``` ### Gemini CLI Add to `.gemini/settings.json`: ```json { "mcpServers": { "Astro docs": { "httpUrl": "https://mcp.docs.astro.build/mcp" } } } ``` ### Zed Add to `~/.config/zed/settings.json`: ```json { "context_servers": { "Astro docs": { "settings": {}, "enabled": true, "url": "https://mcp.docs.astro.build/mcp" } } } ``` ### Claude.ai / Claude Desktop 1. Go to Settings → Connectors 2. Click "Add custom connector" 3. URL: `https://mcp.docs.astro.build/mcp` 4. Name: `Astro docs` ### Warp Settings → AI → MCP Servers → Add: ```json { "mcpServers": { "Astro docs": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.docs.astro.build/mcp"], "start_on_launch": true } } } ``` --- ## Generic (any tool supporting MCP) **Streamable HTTP** (preferred): ```json { "mcpServers": { "Astro docs": { "type": "http", "url": "https://mcp.docs.astro.build/mcp" } } } ``` **Local Proxy** (for tools that only support stdio): ```json { "mcpServers": { "Astro docs": { "type": "stdio", "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.docs.astro.build/mcp"] } } } ``` --- ## Troubleshooting | Issue | Fix | |---|---| | Server not responding | Verify URL is exactly `https://mcp.docs.astro.build/mcp` | | Tool not connecting | Check internet access; some firewalls block MCP | | Stale results | MCP always fetches latest docs — no cache to clear | | Local proxy crashes | Ensure `npx` is in PATH and Node.js ≥ 18 installed | Issues: https://github.com/withastro/docs-mcp/issues -
migration-v6-to-v7.md 15.5 KB
# Migration Guide: Astro v6 → v7 > Official reference: https://docs.astro.build/en/guides/upgrade-to/v7/ --- ## 1. Pre-Migration Checklist - [ ] **Backup** — commit all changes, create a branch: `git checkout -b feat/astro-v7-upgrade` - [ ] **Node.js ≥ 22** — required (v22.5.0+ for `node:sqlite` if replacing `@astrojs/db`) ```bash node -v # must be >= 22 ``` - [ ] **Audit dependencies** — check for packages that depend on Vite internals or the Go compiler ```bash npx astro info ``` - [ ] **Review remark/rehype plugins** — if you have any, plan for Sätteri migration or `@astrojs/markdown-remark` fallback - [ ] **Check for `src/fetch.ts`** — if this file exists for non-routing purposes, plan a rename - [ ] **Check for `@astrojs/db`** usage — plan a replacement (Drizzle, node:sqlite, Turso, Neon) --- ## 2. Upgrade Commands ```bash # npm npx @astrojs/upgrade # pnpm pnpm dlx @astrojs/upgrade # yarn yarn dlx @astrojs/upgrade ``` This upgrades Astro and all official integrations together. For manual control: ```bash npm install astro@latest npm install @astrojs/react@latest @astrojs/mdx@latest # repeat for each integration ``` --- ## 3. Breaking Changes ### 3.1 Vite 8 Astro v7 upgrades to [Vite 8](https://vite.dev/blog/announcing-vite8). The main impact is on **custom Vite plugins** and projects using Vite internals directly. Key Vite 8 changes: - **esbuild → Rolldown** as the production bundler (Rolldown is a Rust-based Rollup replacement) - Plugin API surface changes — check the [Vite 8 migration guide](https://vite.dev/guide/migration) **What to do:** - If you have custom Vite plugins in `astro.config.mjs`, verify they work with Vite 8 - If you use `esbuild`-specific options (e.g. `esbuild.target`, `esbuild.jsxFactory`), check if they still apply under Rolldown ```js // Before: esbuild-specific config (may need review) export default defineConfig({ vite: { esbuild: { target: 'esnext', jsxFactory: 'h', }, }, }); // After: verify compatibility — most configs carry over, but test your build export default defineConfig({ vite: { // Rolldown handles bundling; esbuild options may behave differently // Test `astro build` and check output }, }); ``` > **Most Astro users need no changes.** This primarily affects integration authors and projects with custom Vite plugins. --- ### 3.2 Rust Compiler The Rust-based compiler is now the **default and only compiler**, replacing the Go-based compiler. It is stricter about HTML syntax. #### Unclosed tags now produce errors ```astro <!-- Before: Go compiler silently accepted this --> <p>Hello world <!-- After: Rust compiler requires closing tags --> <p>Hello world</p> ``` ```astro --- import Layout from '../layouts/Layout.astro'; --- <!-- Before: unclosed component tag accepted --> <Layout> <p>Content here <!-- After: all tags must be closed --> <Layout> <p>Content here</p> </Layout> ``` > **Void elements** (`<br>`, `<img>`, `<input>`, `<hr>`) do NOT need closing tags. #### No HTML auto-correction The Go compiler silently reordered invalid HTML (e.g. `<div>` inside `<p>`). The Rust compiler passes markup through as-is. ```astro <!-- Before: compiler restructured this silently --> <p> <div>Block inside paragraph</div> </p> <!-- After: browser handles it (will close <p> early, breaking layout) --> <!-- Fix: use valid nesting --> <div> <div>Block content here</div> </div> ``` #### JSX whitespace handling See [Section 3.5 compressHTML](#35-compresshtml-jsx-is-new-default) for the related whitespace changes. #### CSS output differences (cosmetic, no action needed) - Named colors may become hex: `rebeccapurple` → `#639` - `url()` values may gain/lose quotes: `url(/path)` ↔ `url('/path')` --- ### 3.3 Reserved File Name: `src/fetch.ts` `src/fetch.ts` (or `.js`) is now reserved for [advanced routing](https://docs.astro.build/en/guides/routing/#advanced-routing) configuration. ```js // Before: you had src/fetch.ts for custom fetch logic // src/fetch.ts — your custom utility export function fetchData() { /* ... */ } // After: Option A — rename your file // src/fetcher.ts (or src/api-client.ts, etc.) export function fetchData() { /* ... */ } // Update all imports: // import { fetchData } from '../fetch' → import { fetchData } from '../fetcher' ``` ```js // After: Option B — disable advanced routing in astro.config.mjs import { defineConfig } from 'astro/config'; export default defineConfig({ fetchFile: null, // disables advanced routing, keeps your src/fetch.ts }); ``` ```js // After: Option C — point fetchFile elsewhere import { defineConfig } from 'astro/config'; export default defineConfig({ fetchFile: './src/router.ts', // use a different file for advanced routing }); ``` --- ### 3.4 New Default Markdown Processor: Sätteri [Sätteri](https://satteri.bruits.org/) replaces the remark/rehype (unified) pipeline as the default Markdown processor. `@astrojs/markdown-remark` is no longer installed by default. **If you DON'T use remark/rehype plugins:** no action needed. Sätteri applies GFM and SmartyPants like before. **If you DO use remark/rehype plugins:** ```bash # Install the unified pipeline package npm install @astrojs/markdown-remark ``` ```js // Before: plugins configured directly (worked because unified was the default) import { defineConfig } from 'astro/config'; import remarkToc from 'remark-toc'; import rehypeSlug from 'rehype-slug'; export default defineConfig({ markdown: { remarkPlugins: [remarkToc], rehypePlugins: [rehypeSlug], }, }); // After: explicitly set unified() as processor + install @astrojs/markdown-remark import { defineConfig } from 'astro/config'; import { unified } from '@astrojs/markdown-remark'; import remarkToc from 'remark-toc'; import rehypeSlug from 'rehype-slug'; export default defineConfig({ markdown: { processor: unified({ remarkPlugins: [remarkToc], rehypePlugins: [rehypeSlug], }), }, }); ``` **Alternative:** Port your plugins to Sätteri MDAST/HAST plugins: ```js // Using Sätteri with its native plugin model import { defineConfig } from 'astro/config'; import { satteri } from '@astrojs/markdown-satteri'; import { myMdastPlugin } from './my-satteri-plugin.mjs'; export default defineConfig({ markdown: { processor: satteri({ mdastPlugins: [myMdastPlugin()], features: { directive: true }, }), }, }); ``` --- ### 3.5 `compressHTML: 'jsx'` is New Default Whitespace between inline elements is now stripped using JSX rules (like React), instead of HTML-aware compression. ```astro <!-- Before (v6): renders as "hello world" (space preserved) --> <span>hello</span> <em>world</em> <!-- After (v7): renders as "helloworld" (space removed) --> <span>hello</span> <em>world</em> ``` **Fix: add explicit space with `{' '}`:** ```astro <!-- After: explicit space between inline elements --> <span>hello</span>{' '}<em>world</em> ``` **Or revert to v6 behavior globally:** ```js // astro.config.mjs import { defineConfig } from 'astro/config'; export default defineConfig({ compressHTML: true, // v6 HTML-aware behavior // compressHTML: false // preserve ALL whitespace }); ``` --- ## 4. Deprecated ### `getContainerRenderer()` from package root Importing `getContainerRenderer()` from the integration's package root is deprecated. Use the dedicated `/container-renderer` entrypoint. ```js // Before import { getContainerRenderer } from '@astrojs/react'; // After import { getContainerRenderer } from '@astrojs/react/container-renderer'; ``` Available for: `@astrojs/react`, `@astrojs/preact`, `@astrojs/solid-js`, `@astrojs/svelte`, `@astrojs/vue`, `@astrojs/mdx`. --- ## 5. Removed ### 5.1 `@astrojs/db` The package is removed and no longer maintained. Replace with: | Alternative | Use case | |---|---| | `node:sqlite` | Node.js adapter, local SQLite (Node ≥ 22.5.0) | | [Drizzle ORM](https://orm.drizzle.team/) | Schema-based queries with any DB | | [Turso](https://turso.tech/) | Edge SQLite (libSQL) | | [Neon](https://neon.tech/) | Serverless Postgres | ```bash # Remove npm uninstall @astrojs/db ``` ```js // Before: @astrojs/db import { db, sql } from 'astro:db'; const results = await db.select().from(Posts).all(); // After: Drizzle ORM example import { drizzle } from 'drizzle-orm/node-postgres'; import { posts } from './schema'; const db = drizzle(process.env.DATABASE_URL); const results = await db.select().from(posts); ``` Remove `db` from `astro.config.mjs` integrations array and delete `db/` config files. --- ### 5.2 `astro:transitions` Internals The following exports are removed: | Removed API | Replacement | |---|---| | `TRANSITION_BEFORE_PREPARATION` | `'astro:before-preparation'` | | `TRANSITION_AFTER_PREPARATION` | `'astro:after-preparation'` | | `TRANSITION_BEFORE_SWAP` | `'astro:before-swap'` | | `TRANSITION_AFTER_SWAP` | `'astro:after-swap'` | | `TRANSITION_PAGE_LOAD` | `'astro:page-load'` | | `isTransitionBeforePreparationEvent()` | `event.type === 'astro:before-preparation'` | | `isTransitionBeforeSwapEvent()` | `event.type === 'astro:before-swap'` | | `createAnimationScope()` | Remove entirely | ```js // Before import { TRANSITION_AFTER_SWAP, isTransitionBeforePreparationEvent, } from 'astro:transitions/client'; document.addEventListener(TRANSITION_AFTER_SWAP, (event) => { if (isTransitionBeforePreparationEvent(event)) { /* ... */ } }); // After document.addEventListener('astro:after-swap', (event) => { if (event.type === 'astro:before-preparation') { /* ... */ } }); ``` --- ## 6. Experimental Flags to Remove (Now Stable) Remove these from your `astro.config.mjs` `experimental` block: | Flag | Status in v7 | |---|---| | `experimental.logger` | Stable — use top-level `logger` field | | `experimental.queuedRendering` | Default behavior — just remove | | `experimental.rustCompiler` | Default and only compiler — just remove | | `experimental.advancedRouting` | Default — just remove (note: `src/fetch.ts` is now reserved) | | `experimental.cache` | Stable — move to top-level `cache` field | | `experimental.routeRules` | Stable — move to top-level `routeRules` field | ```js // Before import { defineConfig, logHandlers, memoryCache } from 'astro/config'; export default defineConfig({ experimental: { logger: logHandlers.json({ pretty: true }), queuedRendering: { enabled: true }, rustCompiler: true, advancedRouting: true, cache: { provider: memoryCache() }, routeRules: { '/blog/[...path]': { maxAge: 300, swr: 60 }, }, }, }); // After import { defineConfig, logHandlers, memoryCache } from 'astro/config'; export default defineConfig({ logger: logHandlers.json({ pretty: true }), cache: { provider: memoryCache() }, routeRules: { '/blog/[...path]': { maxAge: 300, swr: 60 }, }, }); ``` --- ## 7. Post-Migration Validation Run these steps after upgrading: ```bash # 1. Install dependencies npm install # 2. Run the dev server — check for compiler errors npm run dev # 3. Run a full production build npm run build # 4. Preview the production build npm run preview # 5. Check for visual regressions (especially whitespace issues from compressHTML) # Open key pages and inspect inline element spacing # 6. Run tests if you have them npm test # 7. Check TypeScript npx astro check ``` **What to look for:** - ❌ Compiler errors about unclosed tags → add missing closing tags - ❌ Layout shifts or broken nesting → fix invalid HTML (block elements inside `<p>`, etc.) - ❌ Missing spaces between inline elements → add `{' '}` where needed - ❌ Markdown rendering issues → install `@astrojs/markdown-remark` if using remark/rehype plugins - ❌ Build errors mentioning `src/fetch.ts` → rename or set `fetchFile: null` - ❌ Import errors for `@astrojs/db` → replace with alternative DB solution - ❌ Import errors for `TRANSITION_*` constants → use event name strings directly --- ## Quick Reference: Search & Replace | Find | Replace with | |---|---| | `from '@astrojs/react'` (for getContainerRenderer) | `from '@astrojs/react/container-renderer'` | | `TRANSITION_BEFORE_PREPARATION` | `'astro:before-preparation'` | | `TRANSITION_AFTER_PREPARATION` | `'astro:after-preparation'` | | `TRANSITION_BEFORE_SWAP` | `'astro:before-swap'` | | `TRANSITION_AFTER_SWAP` | `'astro:after-swap'` | | `TRANSITION_PAGE_LOAD` | `'astro:page-load'` | | `isTransitionBeforePreparationEvent(e)` | `e.type === 'astro:before-preparation'` | | `isTransitionBeforeSwapEvent(e)` | `e.type === 'astro:before-swap'` | | `createAnimationScope` | (remove entirely) | | `experimental.rustCompiler` | (remove) | | `experimental.queuedRendering` | (remove) | | `experimental.advancedRouting` | (remove) | --- ## Ecosystem Compatibility (learned from real upgrades) ### Starlight 0.40+ Sidebar Schema Change Starlight 0.40 (required for Astro v7) changed the sidebar schema. `autogenerate` can no longer be a direct property of a sidebar group — it must be inside `items`: ```javascript // BEFORE (Starlight 0.38): { label: 'Reference', autogenerate: { directory: 'reference' } } // AFTER (Starlight 0.40): { label: 'Reference', items: [{ autogenerate: { directory: 'reference' } }] } ``` ### astro-mermaid — Incompatible with v7 `astro-mermaid` (all versions through 2.0.4) uses `isUnifiedProcessor()` which was removed in Astro v7. Replace with Mermaid CDN client-side script: ```javascript // In Starlight head config or Layout.astro: { tag: 'script', attrs: { type: 'module' }, content: ` import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs'; mermaid.initialize({ startOnLoad: false }); document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('pre > code.language-mermaid').forEach((el) => { const pre = el.parentElement; const div = document.createElement('div'); div.className = 'mermaid'; div.textContent = el.textContent; pre.replaceWith(div); }); mermaid.run(); }); `, } ``` ### Docker Alpine — Sätteri native binding missing Sätteri only ships `linux-x64-gnu` (glibc). Alpine uses musl. Build stage MUST use `node:22-slim`: ```dockerfile FROM node:22-slim AS build # NOT alpine ``` ### .npmrc required in Dockerfile Astro v7 with Starlight plugins causes peer dependency conflicts. The `.npmrc` with `legacy-peer-deps=true` must be copied into Docker: ```dockerfile COPY package*.json .npmrc ./ RUN npm ci ``` ### @astrojs/node Must Be v11 for Astro v7 `@astrojs/node@10` builds fine but **crashes at runtime** with Astro v7: ``` TypeError: app.getAdapterLogger is not a function at createAppHandler (dist/server/entry.mjs) ``` The build succeeds, the image is created, the container starts — then immediately exits. Coolify shows `restarting:unknown` or `exited:unhealthy`. **Fix:** Always upgrade `@astrojs/node` alongside Astro: ```bash npm install astro@latest @astrojs/node@latest @astrojs/mdx@latest ``` **Required versions for v7:** | Package | Minimum | |---------|---------| | `astro` | `^7.0.0` | | `@astrojs/node` | `^11.0.0` | | `@astrojs/mdx` | `^7.0.0` | | `@astrojs/sitemap` | `^3.7.2` (unchanged) | ### pnpm 11.9+ — approve-builds Required in Docker pnpm 11.9 blocks postinstall scripts by default. `esbuild` and `sharp` need native compilation after install. Without approval the Docker build fails: ``` [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.28.1, sharp@0.34.5 ``` **Fix:** Run `pnpm approve-builds` locally (generates `pnpm-workspace.yaml`), then copy it in Docker: ```dockerfile COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ RUN pnpm install --frozen-lockfile ``` -
related-content.md 12.6 KB
# Related Content com Vector Embeddings Conteúdo relacionado semântico para Astro content collections usando `@philnash/astro-related-content`. Gera sugestões de posts relacionados via vector embeddings locais (transformers.js) sem depender de APIs externas em runtime. --- ## Conceito A integração calcula similaridade semântica entre posts usando embeddings (vetores numéricos que representam o "significado" do texto). Posts com vetores próximos são semanticamente similares. Tudo roda em build time — zero impacto no visitante. --- ## Instalação ```bash npm install @philnash/astro-related-content ``` --- ## Configuração Básica ```typescript // astro.config.ts import astroRelatedContent from '@philnash/astro-related-content' export default defineConfig({ integrations: [ astroRelatedContent({ collections: ['blog'], generation: { limit: 4, // posts relacionados por item watch: false, // não regenerar em dev mode (economiza CPU) }, embeddings: { model: 'onnx-community/embeddinggemma-300m-ONNX', dtype: 'fp32', pooling: 'mean', batchSize: 1, }, }), ], }) ``` --- ## Escolha de Modelo | Modelo | Idiomas | Context | Tamanho | Pooling | Uso | |--------|---------|---------|---------|---------|-----| | `Xenova/all-MiniLM-L6-v2` | EN only | 256 tokens | ~22MB | `mean` | Default, ruim para PT-BR | | `onnx-community/embeddinggemma-300m-ONNX` | Multilingual | 2048 tokens | ~300MB | `mean` | **Recomendado para PT-BR** | | `onnx-community/Qwen3-Embedding-0.6B-ONNX` | Multilingual | 32k tokens | ~600MB | `last_token` | Posts muito longos | | `onnx-community/granite-embedding-small-english-r2-ONNX` | EN | 8192 tokens | ~130MB | `cls` | EN com context longo | **Regra:** Para conteúdo em português, NUNCA usar o modelo default (`all-MiniLM-L6-v2`). Use `embeddinggemma-300m-ONNX` ou superior. --- ## Custom Provider (LiteLLM, OpenAI, etc.) A integração aceita custom providers via interface `EmbeddingProvider`: ```typescript // litellm-provider.ts import { createEmbeddingProvider } from '@philnash/astro-related-content/providers' export const litellmProvider = createEmbeddingProvider({ name: 'litellm', version: '1.0.0', async embed(texts, options) { const response = await fetch(`${options.baseUrl}/embeddings`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${options.apiKey}`, }, body: JSON.stringify({ model: options.model, input: texts }), }) const data = await response.json() return data.data.map((item: any) => item.embedding) }, getMetadata(options) { return { model: options.model, baseUrl: options.baseUrl } }, }) ``` ```typescript // astro.config.ts import { litellmProvider } from './litellm-provider' astroRelatedContent({ collections: ['blog'], embeddings: { provider: litellmProvider, baseUrl: 'http://localhost:4000', apiKey: 'sk-...', model: 'text-embedding-3-small', batchSize: 10, }, }) ``` --- ## Artefatos Gerados A integração gera na pasta `.astro-related-content/`: | Arquivo | Conteúdo | Tamanho típico (32 posts) | |---------|----------|---------------------------| | `data.json` | Rankings (top N related por post) | ~14KB | | `vectors.json` | Cache de embeddings + metadata | ~736KB | O modelo ONNX é cacheado em `.astro/astro-related-content/models/` (não vai pro repo — `.astro/` está no `.gitignore`). --- ## Uso no Componente ### Bug de compatibilidade Astro v7 O `getRelatedContent()` do virtual module não funciona com Astro v7 glob loader. O motivo: a integração gera IDs como `slug/index` no `data.json`, mas o Astro v7 usa `slug` (sem `/index`) como `entry.id`. **Workaround:** Usar `getRelatedContentMatches()` + lookup manual: ```astro --- // RelatedPosts.astro import { getPostRoute } from '@/lib/data-utils' import { formatDate } from '@/lib/utils' import { Icon } from 'astro-icon/components' import { Image } from 'astro:assets' import { getCollection, type CollectionEntry } from 'astro:content' import { getRelatedContentMatches } from 'virtual:astro-related-content' import Link from './Link.astro' interface Props { postId: string } const { postId } = Astro.props const matches = getRelatedContentMatches('blog', `${postId}/index`) let relatedContent: { entry: CollectionEntry<'blog'>; score: number }[] = [] if (matches.length > 0) { const allEntries = await getCollection('blog') const entryById = new Map(allEntries.map((e) => [e.id, e])) relatedContent = matches.flatMap((match) => { // match.id = "slug/index", entry.id no Astro v7 = "slug" const normalizedId = match.id.replace(/\/index$/, '') const entry = entryById.get(normalizedId) || entryById.get(match.id) return entry ? [{ entry, score: match.score }] : [] }) } --- { relatedContent.length > 0 && ( <section class="mt-12 border-t pt-8"> <h2 class="mb-6 flex items-center gap-2 text-xl font-medium"> <Icon name="lucide:sparkles" class="size-5" /> Leitura Relacionada </h2> <div class="grid gap-4 sm:grid-cols-2"> {relatedContent.map((item) => ( <Link href={getPostRoute(item.entry)} class="hover:bg-muted/50 flex gap-3 rounded-xl border p-3 transition-colors duration-300" > {item.entry.data.image && ( <div class="hidden w-16 shrink-0 sm:block"> <Image src={item.entry.data.image} alt={item.entry.data.title} width={128} height={67} class="rounded-md object-cover" /> </div> )} <div class="min-w-0"> <h3 class="mb-1 truncate text-sm font-medium"> {item.entry.data.title} </h3> <p class="text-muted-foreground text-xs"> {formatDate(item.entry.data.date)} </p> </div> </Link> ))} </div> </section> ) } ``` --- ## Deploy no Coolify — Sem Baixar Modelo em Produção ### O Problema Na primeira build Docker, a integração baixa o modelo ONNX (~300MB) e processa todos os embeddings. Em um servidor com bandwidth limitada, isso pode levar 20+ minutos e esgotar disco. ### A Solução: Dual-Mode (Local + CI) **Princípio:** Gerar embeddings localmente, commitar o cache, e em CI usar apenas o `data.json` pré-gerado via Vite plugin leve (sem modelo, sem transformers.js). #### 1. Commitar os artefatos Garantir que `.astro-related-content/` **NÃO** está no `.gitignore`: ```bash # Verificar grep "astro-related-content" .gitignore # Se aparecer, remover a linha # Commitar cache git add .astro-related-content/ git commit -m "chore: cache embeddings related content" ``` #### 2. Configuração condicional no astro.config.ts ```typescript import { existsSync } from 'node:fs' import { resolve } from 'node:path' import astroRelatedContent from '@philnash/astro-related-content' // Em CI: usa data.json pré-gerado sem baixar modelo // Local: roda integração completa com embeddings const isCI = Boolean(process.env.CI || process.env.DOCKER) const dataJsonPath = resolve('.astro-related-content/data.json') const hasPrebuiltData = existsSync(dataJsonPath) const relatedContentIntegrations = isCI && hasPrebuiltData ? [] // Virtual module vem do Vite plugin abaixo : [ astroRelatedContent({ collections: ['blog'], generation: { limit: 4 }, embeddings: { model: 'onnx-community/embeddinggemma-300m-ONNX', dtype: 'fp32', pooling: 'mean', batchSize: 1, }, }), ] // Plugin Vite leve para CI — serve virtual module do data.json commitado function relatedContentVitePlugin() { const VIRTUAL_ID = 'virtual:astro-related-content' const RESOLVED_ID = '\0' + VIRTUAL_ID return { name: 'related-content-prebuilt', resolveId(id: string) { if (id === VIRTUAL_ID) return RESOLVED_ID }, load(id: string) { if (id !== RESOLVED_ID) return const absPath = resolve('.astro-related-content/data.json') return ` import { getCollection } from "astro:content"; import relatedContentData from ${JSON.stringify(`/@fs/${absPath}`)}; export function getRelatedContentMatches(collection, id) { const collectionData = relatedContentData[collection]; if (!collectionData) return []; const matches = collectionData[id]; return Array.isArray(matches) ? matches.map((m) => ({ ...m })) : []; } export function getRelatedContentIds(collection, id) { return getRelatedContentMatches(collection, id).map((m) => m.id); } export async function getRelatedContent(collection, id) { const matches = getRelatedContentMatches(collection, id); const entries = await getCollection(collection); const entryById = new Map( entries.flatMap((entry) => { const normalizedId = String(entry.id).replace(/\\.(md|mdx)$/, ""); return normalizedId === entry.id ? [[entry.id, entry]] : [[entry.id, entry], [normalizedId, entry]]; }), ); return matches.flatMap((match) => { const entry = entryById.get(match.id); return entry ? [{ entry, score: match.score }] : []; }); } ` }, } } export default defineConfig({ integrations: [ // ... outras integrações ...relatedContentIntegrations, ], vite: { plugins: [ // ... outros plugins ...(isCI && hasPrebuiltData ? [relatedContentVitePlugin()] : []), ], }, }) ``` #### 3. Dockerfile com `ENV CI=true` ```dockerfile FROM node:22-slim AS build WORKDIR /app ENV CI=true ENV NODE_OPTIONS="--max-old-space-size=512" COPY package*.json .npmrc ./ RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html EXPOSE 80 ``` O `ENV CI=true` ativa o Vite plugin leve. Nenhum modelo é baixado. Build completa em ~10-30s. --- ## Workflow Operacional | Ação | Onde | O que acontece | |------|------|----------------| | Novo post | Local | `astro build` → regenera embedding só do post novo → commit cache → push | | Editar post | Local | `astro build` → recalcula embedding do editado → commit cache → push | | Deploy | Coolify | Usa `data.json` pré-commitado → build rápido (~30s) | | Primeiro setup | Local | Download modelo (~300MB) + embeddings de todos os posts (1-20min) | ### Tempos Reais (32 posts, EmbeddingGemma 300m) | Etapa | Tempo | |-------|-------| | Primeira geração (download modelo + 32 embeddings) | ~22 min | | Build subsequente local (cache hit) | ~10 s | | Build CI com data.json pré-gerado | ~10 s | | Build CI sem cache (modelo baixando) | ~22+ min ❌ | --- ## Checklist de Implementação - [ ] `npm install @philnash/astro-related-content` - [ ] Configurar integração no `astro.config.ts` (com lógica CI/local) - [ ] Criar componente `RelatedPosts.astro` (com workaround Astro v7) - [ ] Integrar componente no template de post (`[...id].astro` ou similar) - [ ] Rodar `astro build` localmente para gerar embeddings - [ ] Verificar `.astro-related-content/` NÃO está no `.gitignore` - [ ] Commitar `data.json` + `vectors.json` - [ ] Setar `ENV CI=true` no Dockerfile - [ ] Deploy e validar no Coolify --- ## Troubleshooting ### Build no Coolify demora 20+ minutos **Causa:** `CI=true` não setado no Dockerfile, ou `.astro-related-content/data.json` não commitado. A integração completa está rodando e baixando o modelo. **Fix:** Setar `ENV CI=true` no Dockerfile E commitar a pasta `.astro-related-content/`. ### Related posts não renderizam (array vazio) **Causa:** Bug de ID entre integração e Astro v7. O `getRelatedContent()` do virtual module não faz match porque IDs diferem. **Fix:** Usar `getRelatedContentMatches()` + lookup manual com `normalizedId = match.id.replace(/\/index$/, '')`. ### Embeddings ruins para português **Causa:** Usando modelo default (`all-MiniLM-L6-v2`) que é English-only. **Fix:** Usar `onnx-community/embeddinggemma-300m-ONNX` (multilingual). ### Cache invalidado a cada build **Causa:** A metadata do provider (model, dtype, pooling, version) mudou entre builds. A integração invalida todo o cache quando metadata difere. **Fix:** Não alterar configuração de embeddings após gerar o cache. Se precisar mudar modelo, regenerar tudo localmente e re-commitar. ### `Cannot find module '@huggingface/transformers'` em CI **Causa:** O pacote `@huggingface/transformers` é dependência transitiva só necessária quando a integração completa roda. Em CI com o Vite plugin, não é necessário. **Fix:** Se usar o dual-mode (CI plugin), isso não acontece. Se rodar integração em CI, garantir que `npm ci` instala todas deps. -
seo-full-stack.md 21.4 KB
# SEO Full Stack for Astro Complete reference for implementing technical SEO, structured data, agent discovery, and performance in Astro sites. Based on the `@jdevalk/astro-seo-graph` stack + complementary patterns. > **Sources:** [Astro SEO: the definitive guide](https://joost.blog/astro-seo-complete-guide/) by Joost de Valk + official [astro-seo-graph](https://github.com/jdevalk/seo-graph/tree/main/packages/astro-seo-graph) documentation. --- ## 1. Installation ```bash pnpm add @jdevalk/astro-seo-graph @jdevalk/seo-graph-core ``` `@jdevalk/seo-graph-core` is a transitive dep, but depending on it explicitly lets you pin the version and import piece builders directly. --- ## 2. `<Seo>` Component — Unified Head Metadata A single component replaces all manual `<head>` management: ```astro --- import Seo from '@jdevalk/astro-seo-graph/Seo.astro'; --- <Seo title="My Post | My Site" description="A concise description for search engines." canonical="https://example.com/my-post/" ogType="article" ogImage="https://example.com/og/my-post.jpg" ogImageAlt="My Post" ogImageWidth={1200} ogImageHeight={675} siteName="My Site" twitter={{ card: 'summary_large_image', site: '@handle' }} article={{ publishedTime: publishDate, tags: ['Astro', 'SEO'] }} graph={graph} extraLinks={[ { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }, { rel: 'sitemap', href: '/sitemap-index.xml' }, { rel: 'alternate', type: 'application/rss+xml', href: '/feed.xml', title: 'RSS' }, ]} /> ``` ### Automatic behaviors - **Canonical** derived from Astro's `site` config, query params stripped by default (UTMs don't create duplicates) - **Robots** always includes `max-snippet:-1`, `max-image-preview:large`, `max-video-preview:-1` - **Canonical omitted when `noindex: true`** (per Google's recommendation) - **Duplicate Twitter tags suppressed** — Twitter falls back to OG automatically - **hreflang alternates** with BCP 47 normalization and automatic `x-default` - **`og:locale:alternate`** emitted automatically from the `alternates` prop --- ## 3. Connected JSON-LD Graph (`@graph`) A standalone `BlogPosting` isn't enough. The goal is an interlinked graph via `@id`: ```typescript // src/utils/schema.ts import { buildWebSite, buildBlog, buildPerson, buildWebPage, buildArticle, buildBreadcrumbList, makeIds, } from '@jdevalk/seo-graph-core'; const SITE_URL = 'https://example.com'; const ids = makeIds({ siteUrl: SITE_URL }); export function buildBlogPostGraph(post: { title: string; url: string; publishDate: Date; description: string }) { return { '@context': 'https://schema.org', '@graph': [ buildWebSite({ url: SITE_URL, name: 'My Site', publisher: { '@id': ids.person }, potentialAction: { '@type': 'SearchAction', target: { '@type': 'EntryPoint', urlTemplate: `${SITE_URL}/search?q={search_term_string}` }, 'query-input': 'required name=search_term_string', }, }, ids), buildBlog({ url: `${SITE_URL}/blog/`, name: 'Blog', publisher: { '@id': ids.person } }, ids), buildPerson({ url: SITE_URL, name: 'Your Name', knowsAbout: ['Astro', 'SEO', 'Web Development'], sameAs: ['https://github.com/your-user', 'https://linkedin.com/in/your-user'], }, ids), buildWebPage({ url: post.url, name: post.title, isPartOf: { '@id': ids.website }, breadcrumb: { '@id': ids.breadcrumb(post.url) }, datePublished: post.publishDate, }, ids), buildArticle({ url: post.url, isPartOf: { '@id': ids.webPage(post.url) }, author: { '@id': ids.person }, publisher: { '@id': ids.person }, headline: post.title, description: post.description, datePublished: post.publishDate, }, ids, 'BlogPosting'), ], }; } ``` ### Trust Signals in the Schema Include these to strengthen authority: | Property | Where | Purpose | |---|---|---| | `publishingPrinciples` | `WebSite` / `Person` | Editorial policy | | `copyrightHolder` + `copyrightYear` | `WebPage` | Copyright ownership | | `knowsAbout` | `Person` | Topical authority | | `SearchAction` | `WebSite` | Tells agents how to search the site | | `sameAs` | `Person` / `Organization` | Social profiles = identity verification | ### `articleBody` in Schema Include full text (up to 10K chars) so agents can access content via structured data without scraping: ```typescript buildArticle({ // ... articleBody: post.bodyText.slice(0, 10000), }, ids, 'BlogPosting'), ``` --- ## 4. Breadcrumbs Linked to the Graph ```typescript import { breadcrumbsFromUrl } from '@jdevalk/astro-seo-graph'; import { buildBreadcrumbList, makeIds } from '@jdevalk/seo-graph-core'; const ids = makeIds({ siteUrl: 'https://example.com' }); const items = breadcrumbsFromUrl({ url: Astro.url, siteUrl: 'https://example.com', pageName: post.data.title, names: { blog: 'Blog', category: 'Category' }, }); const breadcrumb = buildBreadcrumbList({ url: Astro.url.href, items }, ids); ``` Each breadcrumb item can reference a graph entity via `@id`, communicating the structural relationship between page and section. --- ## 5. Content Schema Validation (Zod) ```typescript // src/content.config.ts import { defineCollection, z } from 'astro:content'; import { seoSchema, imageSchema } from '@jdevalk/astro-seo-graph'; const blog = defineCollection({ schema: ({ image }) => z.object({ title: z.string(), publishDate: z.coerce.date(), featureImage: imageSchema(image).optional(), seo: seoSchema(image).optional(), }), }); ``` - `seoSchema` validates title (5–120 chars) and description (15–160 chars) — build fails if outside limits - `imageSchema` requires `alt` — image without alt won't compile --- ## 6. Build-Time Validation ```typescript // astro.config.mjs import seoGraph from '@jdevalk/astro-seo-graph/integration'; export default defineConfig({ integrations: [ seoGraph({ // All enabled by default: validateH1: true, // 0 or >1 H1 = warning validateUniqueMetadata: true, // Duplicate title/desc across pages validateImageAlt: true, // <img> without alt validateMetadataLength: { // SERP-safe bounds title: { min: 30, max: 65 }, description: { min: 70, max: 200 }, }, validateInternalLinks: { // Broken internal links or missing trailing slash skip: (href) => href.startsWith('/api/'), }, }), ], }); ``` ### What each validation catches: - **H1**: Templates with duplicate or missing H1 - **Duplicates**: Paginated pages sharing the same title (corpus-level bug) - **Alt text**: Images missed over the years - **Meta length**: Titles truncated in SERP or invisible descriptions - **Internal links**: `/about-me` without trailing slash that works via 301 but wastes a round-trip ### CI: External Broken Link Checker For external links (internal validation doesn't cover), use [lychee](https://github.com/lycheeverse/lychee-action) in GitHub Actions: ```yaml # .github/workflows/links.yml name: Check Links on: push: paths: ['src/content/**'] schedule: - cron: '0 6 * * 1' # Weekly for link rot jobs: links: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: lycheeverse/lychee-action@v2 with: args: --verbose --no-progress 'src/content/**/*.md' ``` --- ## 7. Advanced Sitemaps ### Per-Collection with chunks ```typescript import sitemap from '@astrojs/sitemap'; sitemap({ entryLimit: 1000, chunks: { posts: (item) => { if (/^\/blog\/[^/]+/.test(new URL(item.url).pathname)) return item; }, pages: (item) => item, // default bucket }, }); ``` Produces: `sitemap-posts-0.xml`, `sitemap-pages-0.xml` — makes debugging easier in Google Search Console. ### Git-based lastmod ```typescript import { gitLastmod } from '@jdevalk/astro-seo-graph'; // In the sitemap serialize callback: serialize(item) { const filePath = urlToFilePath(item.url); // your logic const lastmod = gitLastmod(filePath, { excludeCommits: ['abc1234'], // bulk imports that don't count }); return { ...item, lastmod: lastmod ?? item.lastmod }; } ``` `gitLastmod` uses `git log` for the real timestamp of the last commit that touched the file — doesn't depend on filesystem `mtime` (which resets on CI). --- ## 8. IndexNow — Active Notification IndexNow notifies Bing, Yandex, and others that URLs changed, instead of waiting for passive crawl. ### Configuration ```typescript // astro.config.mjs seoGraph({ indexNow: { key: process.env.INDEXNOW_KEY!, host: 'example.com', siteUrl: 'https://example.com', filter: (url) => !/^\/blog\/\d+\/$/.test(new URL(url).pathname), // Exclude pagination }, }); ``` ### Key Route (ownership verification) ```typescript // src/pages/[your-key-here].txt.ts import { createIndexNowKeyRoute } from '@jdevalk/astro-seo-graph'; export const GET = createIndexNowKeyRoute({ key: 'your-key-here' }); ``` ### Deploy order matters 1. Deploy the key route first 2. Confirm `https://example.com/your-key.txt` returns 200 3. Only then enable `indexNow` in the integration > Submissions before the key is reachable = HTTP 403 and key permanently invalidated. ### Direct IndexNow API For manual or custom submission: ```bash # Single URL curl "https://api.indexnow.org/indexnow?url=https://example.com/new-post/&key=YOUR_KEY" # Batch (up to 10,000 URLs per POST) curl -X POST https://api.indexnow.org/indexnow \ -H "Content-Type: application/json" \ -d '{ "host": "example.com", "key": "YOUR_KEY", "urlList": [ "https://example.com/post-1/", "https://example.com/post-2/" ] }' ``` --- ## 9. Auto-Generated OG Images Pipeline: **satori** (JSX → SVG) → **sharp** (SVG → JPEG) ```typescript // src/pages/og/[...slug].jpg.ts import satori from 'satori'; import sharp from 'sharp'; import { getCollection } from 'astro:content'; export async function getStaticPaths() { const posts = await getCollection('blog'); return posts.map((p) => ({ params: { slug: p.id } })); } export async function GET({ params }) { const posts = await getCollection('blog'); const post = posts.find((p) => p.id === params.slug); if (!post) return new Response('Not found', { status: 404 }); const fontData = await fetch('https://cdn.example.com/fonts/Inter-Bold.ttf') .then((r) => r.arrayBuffer()); const svg = await satori( { type: 'div', props: { style: { width: '100%', height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '60px', background: 'linear-gradient(135deg, #1a1a2e, #16213e)', color: '#ffffff', fontFamily: 'Inter', }, children: [ { type: 'div', props: { style: { fontSize: '48px', fontWeight: 700, lineHeight: 1.2 }, children: post.data.title } }, { type: 'div', props: { style: { fontSize: '24px', marginTop: '20px', opacity: 0.8 }, children: 'example.com' } }, ], }, }, { width: 1200, height: 675, fonts: [{ name: 'Inter', data: fontData, weight: 700 }] }, ); const jpeg = await sharp(Buffer.from(svg)).jpeg({ quality: 80 }).toBuffer(); return new Response(jpeg, { headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'public, max-age=31536000, immutable' }, }); } ``` **Why JPEG and not WebP/AVIF?** Social platforms don't reliably support modern formats yet. **Size: 1200×675** — Google Discover requires ≥1200px width, and 16:9 works well cross-platform. The `<Seo>` component derives the OG image URL from the slug automatically: ```typescript const slug = Astro.url.pathname.replace(/^\/|\/$/g, ''); const ogImage = new URL(`/og/${slug || 'index'}.jpg`, SITE_URL).toString(); ``` --- ## 10. Agent Discovery ### Schema Endpoints (corpus-wide JSON-LD) ```typescript // src/pages/schema/post.json.ts import { getCollection } from 'astro:content'; import { createSchemaEndpoint } from '@jdevalk/astro-seo-graph'; import { buildArticle, buildWebPage, makeIds } from '@jdevalk/seo-graph-core'; const ids = makeIds({ siteUrl: 'https://example.com' }); export const GET = createSchemaEndpoint({ entries: () => getCollection('blog'), mapper: (post) => { const url = `https://example.com/${post.id}/`; return [ buildWebPage({ url, name: post.data.title, isPartOf: { '@id': ids.website }, datePublished: post.data.publishDate }, ids), buildArticle({ url, isPartOf: { '@id': ids.webPage(url) }, author: { '@id': ids.person }, headline: post.data.title, description: post.data.description ?? '', datePublished: post.data.publishDate }, ids, 'BlogPosting'), ]; }, }); ``` ### Schema Map (`/schemamap.xml`) ```typescript // src/pages/schemamap.xml.ts import { createSchemaMap } from '@jdevalk/astro-seo-graph'; export const GET = createSchemaMap({ siteUrl: 'https://example.com', entries: [ { path: '/schema/post.json', lastModified: new Date() }, { path: '/schema/page.json', lastModified: new Date() }, ], }); ``` ### API Catalog (RFC 9727) ```typescript // src/pages/.well-known/api-catalog.ts import { createApiCatalog } from '@jdevalk/astro-seo-graph'; export const GET = createApiCatalog({ siteUrl: 'https://example.com', schemaEndpoints: [ { path: '/schema/post.json', schemaType: 'BlogPosting', serviceDoc: '/about/' }, ], schemaMap: { path: '/schemamap.xml' }, }); ``` ### Markdown Alternates Serve a `.md` version of every page so agents can consume content without HTML parsing: ```typescript // src/pages/blog/[...slug].md.ts import { getCollection } from 'astro:content'; import { createMarkdownEndpoint } from '@jdevalk/astro-seo-graph'; export const getStaticPaths = async () => { const posts = await getCollection('blog'); return posts.map((p) => ({ params: { slug: p.id } })); }; export const GET = createMarkdownEndpoint({ entries: () => getCollection('blog'), mapper: (post, slug) => post.id !== slug ? null : { frontmatter: { title: post.data.title, canonical: `https://example.com/blog/${post.id}/`, pubDate: post.data.publishDate }, body: post.body ?? '', }, }); ``` Enable the discovery link: ```typescript // astro.config.mjs seoGraph({ markdownAlternate: true }); ``` Emits `<link rel="alternate" type="text/markdown" href="…">` on every page. ### Content Negotiation via Cloudflare (no SSR) Transform Rule in the dashboard (works on free plan): ``` When: http.request.headers["accept"][0] contains "text/markdown" AND ends_with(http.request.uri.path, "/") AND NOT starts_with(http.request.uri.path, "/_") Rewrite URI path (dynamic): wildcard_replace(http.request.uri.path, "*/", "${1}.md") ``` Turns `/blog/post/` → `/blog/post.md` before cache lookup. No need for `Vary: Accept` header — Cloudflare strips custom Vary values. ### llms.txt ```typescript seoGraph({ llmsTxt: { title: 'My Site', siteUrl: 'https://example.com', summary: 'A blog about web development, Astro, and SEO.', }, }); ``` Generates `/llms.txt` automatically at build time listing all pages. ### NLWeb Discovery `<link>` tag for conversational endpoint (Microsoft protocol): ```html <link rel="nlweb" href="https://example.com/api/nlweb" /> ``` NLWeb allows AI agents to make conversational queries against site content via schema.org structured data. Still early days but the setup is trivial. --- ## 11. Performance SEO ### No-Vary-Search UTM params break caching: `?utm_source=linkedin` and `?utm_source=email` are different resources to the browser. Header that fixes it: ``` No-Vary-Search: key-order, params=("utm_source" "utm_medium" "utm_campaign" "utm_content" "utm_term") ``` **Status:** IETF draft (`draft-ietf-httpbis-no-vary-search`), supported in Chrome, degrades gracefully elsewhere. Configure in `_headers` (Cloudflare Pages / Netlify): ``` /* No-Vary-Search: key-order, params=("utm_source" "utm_medium" "utm_campaign" "utm_content" "utm_term") ``` ### CDN Cache Headers ``` # _headers (Cloudflare Pages) /_astro/* Cache-Control: public, max-age=31536000, immutable /og/* Cache-Control: public, max-age=31536000, immutable ``` Hashed assets under `/_astro/` never need revalidation — the filename changes when content changes. ### View Transitions Prefetch ```astro --- // src/layouts/Base.astro import { ClientRouter } from 'astro:transitions'; --- <head> <ClientRouter defaultStrategy="viewport" /> </head> ``` `defaultStrategy: 'viewport'` prefetches links as they scroll into view, making navigation feel instant while keeping initial load minimal. ### Font Preloading ```html <link rel="preload" href="/fonts/Inter.woff2" as="font" type="font/woff2" crossorigin /> ``` --- ## 12. Redirects ### Per platform | Platform | File | Format | |---|---|---| | Cloudflare Pages | `public/_redirects` | `/old /new 301` | | Netlify | `public/_redirects` or `netlify.toml` | Same format | | Vercel | `vercel.json` | `{ "source": "/old", "destination": "/new", "permanent": true }` | ### FuzzyRedirect on 404 Safety net for URLs that slip through redirect tables: ```astro --- // src/pages/404.astro import FuzzyRedirect from '@jdevalk/astro-seo-graph/FuzzyRedirect.astro'; --- <html lang="en"> <head><title>Page not found</title></head> <body> <h1>Page not found</h1> <p>The page you're looking for doesn't exist.</p> <FuzzyRedirect /> <p><a href="/">Go to the homepage</a></p> </body> </html> ``` Behavior: - Fetches `/sitemap-index.xml`, computes Levenshtein similarity - **0.6–0.85 similarity**: shows "Did you mean /correct-path/?" - **>0.85**: auto-redirects with `window.location.replace` - **<0.6**: does nothing --- ## 13. RSS with Full Content ```typescript // src/pages/rss.xml.ts import rss from '@astrojs/rss'; import { getCollection } from 'astro:content'; export async function GET(context) { const posts = await getCollection('blog'); return rss({ title: 'My Blog', description: 'Latest posts', site: context.site, items: posts.map((post) => ({ title: post.data.title, pubDate: post.data.publishDate, description: post.data.description, link: `/blog/${post.id}/`, content: post.body, // Full content, not excerpts })), }); } ``` **Include full content in the feed** — truncated feeds frustrate readers and give AI systems less to work with. --- ## 14. Dynamic robots.txt ```typescript // src/pages/robots.txt.ts export function GET() { return new Response( `User-agent: * Allow: / Sitemap: https://example.com/sitemap-index.xml Schemamap: https://example.com/schemamap.xml `, { headers: { 'Content-Type': 'text/plain' } }, ); } ``` The `Schemamap:` directive points agents to the schema map — similar to `Sitemap:` but for structured data. --- ## 15. Implementation Checklist - [ ] `@jdevalk/astro-seo-graph` installed and `<Seo>` in all layouts - [ ] JSON-LD `@graph` with full entities (WebSite, Person, WebPage, Article, BreadcrumbList) - [ ] Trust signals: `publishingPrinciples`, `knowsAbout`, `SearchAction` - [ ] `seoSchema` in content collection with title/desc validation - [ ] `seoGraph()` integration with all validations enabled - [ ] Per-collection sitemaps with `gitLastmod` - [ ] IndexNow configured and key route deployed - [ ] Auto-generated OG images (1200×675 JPEG) - [ ] Schema endpoints + `/schemamap.xml` - [ ] Markdown alternates with `<link rel="alternate" type="text/markdown">` - [ ] `llms.txt` generated automatically - [ ] `<link rel="nlweb">` (when endpoint available) - [ ] `No-Vary-Search` header for UTM params - [ ] CDN cache: immutable for `/_astro/*` - [ ] View Transitions with viewport prefetch - [ ] FuzzyRedirect on 404 - [ ] RSS with full content - [ ] `robots.txt` with Sitemap + Schemamap - [ ] Lychee in CI for broken external links - [ ] `/.well-known/api-catalog` (RFC 9727) --- ## References - [astro-seo-graph README](https://github.com/jdevalk/seo-graph/tree/main/packages/astro-seo-graph) - [astro-seo-graph AGENTS.md](https://github.com/jdevalk/seo-graph/blob/main/AGENTS.md) — 3000+ lines with recipes for 14 site types - [seo-graph-core](https://github.com/jdevalk/seo-graph/tree/main/packages/seo-graph-core) - [IndexNow documentation](https://www.indexnow.org/documentation) - [NLWeb protocol](https://github.com/nlweb-ai/NLWeb) - [satori](https://github.com/vercel/satori) — JSX → SVG - [sharp](https://sharp.pixelplumbing.com/) — SVG → JPEG/PNG - [No-Vary-Search (MDN)](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/No-Vary-Search) - [RFC 9727 — API Catalog](https://www.rfc-editor.org/rfc/rfc9727) - [llms.txt standard](https://llmstxt.org) - [Joost: Astro SEO definitive guide](https://joost.blog/astro-seo-complete-guide/) - [Joost: Agent-ready static blog](https://joost.blog/agent-ready/) -
starlight-and-patterns.md 14.5 KB
# Starlight & Common Patterns ## 1. Starlight Documentation Sites ### Setup ```bash npm create astro@latest -- --template starlight ``` Or add to an existing Astro project: ```bash npx astro add starlight ``` ### Configuration ```js // astro.config.mjs import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; export default defineConfig({ site: 'https://docs.example.com', integrations: [ starlight({ title: 'My Docs', defaultLocale: 'en', locales: { en: { label: 'English' }, pt: { label: 'Português', lang: 'pt-BR' }, }, sidebar: [ { label: 'Home', link: '/' }, { label: 'Guides', items: [ { slug: 'guides/getting-started' }, { slug: 'guides/configuration' }, ], }, { label: 'Reference', autogenerate: { directory: 'reference' }, }, ], customCss: ['./src/styles/custom.css'], }), ], }); ``` ### Sidebar Gotchas **`link` and `items` are mutually exclusive.** A sidebar item is ONE of: - `link` — a single URL (requires `label`) - `slug` — reference to internal page (uses page title as label) - `items` — array of child links/groups (requires `label`) - `autogenerate` — auto-generates from a directory ```ts // ❌ WRONG — cannot mix link with items { label: 'Guides', link: '/guides/', items: [...] } // ✅ CORRECT — group with items { label: 'Guides', items: [{ slug: 'guides/intro' }] } // ✅ CORRECT — single link { label: 'Guides', link: '/guides/' } ``` **Autogenerate limitations:** - Only generates from files in `src/content/docs/<directory>/` - Sorted alphabetically by filename (use numeric prefixes like `01-intro.md` to control order) - Cannot filter files — all `.md`/`.mdx` in the directory are included - Subfolders become nested groups automatically ### Built-in Components: Card vs LinkCard | Component | Purpose | Required Props | Has `href`? | Accepts children? | |-----------|---------|---------------|-------------|-------------------| | `Card` | Display content in a styled box | `title` | ❌ NO | ✅ Yes | | `LinkCard` | Prominent clickable link | `title`, `href` | ✅ YES | ❌ No | ```mdx import { Card, LinkCard, CardGrid } from '@astrojs/starlight/components'; {/* Card — displays content, NOT a link */} <Card title="Feature A" icon="star"> Description of feature A goes here. </Card> {/* LinkCard — entire card is a clickable link */} <LinkCard title="Getting Started" href="/guides/getting-started/" description="Learn how to set up your project." /> {/* Group in a grid */} <CardGrid stagger> <Card title="Fast" icon="rocket">Built for speed.</Card> <Card title="Simple" icon="pencil">Easy to use.</Card> </CardGrid> ``` ### Component Overrides Override any built-in Starlight UI component: ```js // astro.config.mjs starlight({ components: { // Replace the SocialIcons component SocialIcons: './src/components/MyLinks.astro', // Replace the Header Header: './src/components/CustomHeader.astro', }, }); ``` Reuse the built-in component inside your override: ```astro --- // src/components/CustomHeader.astro import Default from '@astrojs/starlight/components/Header.astro'; --- <Default><slot /></Default> <div class="announcement-bar">New release available!</div> ``` Full list of overridable components: see [Overrides Reference](https://starlight.astro.build/reference/overrides/). ### Theming Starlight uses a semantic color system via CSS custom properties. The naming is **counter-intuitive**: | Variable | Meaning | |----------|---------| | `--sl-color-white` | **Foreground** (text) color | | `--sl-color-black` | **Background** color | | `--sl-color-gray-1` to `--sl-color-gray-6` | Gray scale (1 = lightest in dark mode) | | `--sl-color-accent-low` | Accent background | | `--sl-color-accent` | Accent mid (links, highlights) | | `--sl-color-accent-high` | Accent foreground | **You MUST define both `:root` (dark) and `:root[data-theme='light']` (light):** ```css /* src/styles/custom.css */ /* Dark mode (default) */ :root { --sl-color-white: #ffffff; --sl-color-black: #181818; --sl-color-gray-1: #eee; --sl-color-gray-2: #c2c2c2; --sl-color-gray-3: #8b8b8b; --sl-color-gray-4: #585858; --sl-color-gray-5: #383838; --sl-color-gray-6: #272727; --sl-color-accent-low: #1a1047; --sl-color-accent: #8b5cf6; --sl-color-accent-high: #c4b5fd; } /* Light mode — invert the logic */ :root[data-theme='light'] { --sl-color-white: #181818; --sl-color-black: #ffffff; --sl-color-gray-1: #272727; --sl-color-gray-2: #383838; --sl-color-gray-3: #585858; --sl-color-gray-4: #8b8b8b; --sl-color-gray-5: #c2c2c2; --sl-color-gray-6: #eee; --sl-color-accent-low: #c4b5fd; --sl-color-accent: #6d28d9; --sl-color-accent-high: #1a1047; } ``` **CSS Layer:** Starlight uses `@layer starlight` internally. Unlayered custom CSS automatically overrides it. For explicit layer control: ```css @layer my-reset, starlight, my-overrides; @layer my-overrides { :root { --sl-content-width: 50rem; } } ``` ### Versioned Docs with starlight-utils multiSidebar ```bash npm install @lorenzo_lewis/starlight-utils ``` ```js // astro.config.mjs import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; import starlightUtils from '@lorenzo_lewis/starlight-utils'; export default defineConfig({ integrations: [ starlight({ title: 'My Docs', plugins: [ starlightUtils({ multiSidebar: { switcherStyle: 'dropdown', }, }), ], sidebar: [ // Each top-level group becomes a separate sidebar { label: 'v2', items: [{ autogenerate: { directory: 'v2' } }], }, { label: 'v1', items: [{ autogenerate: { directory: 'v1' } }], }, ], }), ], }); ``` --- ## 2. Search (Pagefind) ### Install and Build Pagefind indexes static HTML after build. Starlight includes Pagefind by default. For non-Starlight Astro sites: ```bash npm install -D pagefind ``` Add to your build script in `package.json`: ```json { "scripts": { "build": "astro build && npx pagefind --site dist" } } ``` ### Indexing Controls ```html <!-- Only index content inside this element --> <main data-pagefind-body> <h1>Indexed heading</h1> <p>This paragraph is searchable.</p> <!-- Exclude specific elements --> <nav data-pagefind-ignore> <p>This won't appear in search results.</p> </nav> <!-- Boost heading weight in results --> <h2 data-pagefind-weight="2">Important Section</h2> </main> ``` | Attribute | Effect | |-----------|--------| | `data-pagefind-body` | Only index inside this element (page-level) | | `data-pagefind-ignore` | Exclude element from indexing | | `data-pagefind-ignore="all"` | Exclude element and all descendants | | `data-pagefind-weight="N"` | Boost ranking (default: 1, higher = more relevant) | | `data-pagefind-meta="key:value"` | Add metadata to search results | ### UI Component Integration ```astro --- // src/pages/search.astro --- <html> <head> <link href="/pagefind/pagefind-ui.css" rel="stylesheet" /> </head> <body> <div id="search"></div> <script> import '/pagefind/pagefind-ui.js'; new PagefindUI({ element: '#search', showSubResults: true }); </script> </body> </html> ``` ### Pagefind vs Fuse.js Decision Table | Criteria | Pagefind | Fuse.js | |----------|----------|---------| | Index size | Pre-built, loads fragments on demand | Entire index in memory | | Best for | Static sites with 50+ pages | Small datasets (<100 items), dynamic data | | Setup | Build step required | No build step, works at runtime | | Fuzzy matching | Limited (typo tolerance) | Excellent (configurable threshold) | | Performance | O(1) per query chunk (WASM) | Degrades with data size | | Works offline | ✅ Yes | ✅ Yes | | SSR compatible | ❌ No (needs static HTML) | ✅ Yes | | Custom data | Indexes HTML only | Indexes any JSON array | | Bundle size | ~50KB (WASM) + on-demand chunks | ~25KB + full index | **Rule of thumb:** Use Pagefind for documentation/blog search. Use Fuse.js for in-page filtering (command palettes, dropdown search, dynamic lists). --- ## 3. SEO > **Full reference:** see [SEO Full Stack](seo-full-stack.md) — covers `@jdevalk/astro-seo-graph`, JSON-LD graph, IndexNow, auto-generated OG images, agent discovery, performance SEO, and build-time validation. Below is just the minimal setup for Starlight (which already includes automatic sitemap): ### Starlight SEO Basics Starlight generates a sitemap automatically — just set `site` in your config. For RSS and custom meta tags in non-Starlight sites, see the full reference file. ```js // astro.config.mjs — minimum for Starlight SEO export default defineConfig({ site: 'https://docs.example.com', // Required for sitemap and canonical integrations: [starlight({ title: 'My Docs' })], }); ``` --- ## 4. i18n Patterns ### Configuration ```js // astro.config.mjs export default defineConfig({ i18n: { defaultLocale: 'en', locales: ['en', 'pt-br', 'es'], routing: { prefixDefaultLocale: false, // /about (en), /pt-br/about, /es/about }, fallback: { 'pt-br': 'en', es: 'en', }, }, }); ``` For Starlight, i18n is configured inside the integration: ```js starlight({ defaultLocale: 'root', locales: { root: { label: 'English', lang: 'en' }, 'pt-br': { label: 'Português', lang: 'pt-BR' }, }, }); ``` ### Content Collections per Locale ``` src/content/docs/ ├── index.md ← English (root locale) ├── guides/ │ └── intro.md └── pt-br/ ├── index.md ← Portuguese └── guides/ └── intro.md ``` ### Fallback Strategy Show default locale content with a banner when translation is missing: ```astro --- // src/components/TranslationBanner.astro import { getEntry } from 'astro:content'; const currentLocale = Astro.currentLocale ?? 'en'; const slug = Astro.params.slug; // Check if translation exists const localizedEntry = await getEntry('docs', `${currentLocale}/${slug}`); const isFallback = !localizedEntry && currentLocale !== 'en'; --- {isFallback && ( <aside class="translation-banner" role="alert"> ⚠️ This page is not yet translated to {currentLocale}. Showing English version. </aside> )} ``` In Starlight, fallback is automatic — missing translations show the `defaultLocale` content with a built-in notice. ### getRelativeLocaleUrl Helper ```astro --- import { getRelativeLocaleUrl } from 'astro:i18n'; const locale = Astro.currentLocale ?? 'en'; --- <nav> <a href={getRelativeLocaleUrl(locale, 'about')}>About</a> <a href={getRelativeLocaleUrl(locale, 'guides/intro')}>Guide</a> </nav> ``` --- ## 5. Common Recipes ### Pagination ```astro --- // src/pages/blog/[...page].astro import { getCollection } from 'astro:content'; import type { GetStaticPaths } from 'astro'; const POSTS_PER_PAGE = 10; export const getStaticPaths: GetStaticPaths = async ({ paginate }) => { const allPosts = await getCollection('blog'); const sorted = allPosts.sort( (a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf() ); return paginate(sorted, { pageSize: POSTS_PER_PAGE }); }; const { page } = Astro.props; --- <h1>Blog — Page {page.currentPage}</h1> <ul> {page.data.map((post) => ( <li> <a href={`/blog/${post.id}/`}>{post.data.title}</a> </li> ))} </ul> <nav> {page.url.prev && <a href={page.url.prev}>← Previous</a>} <span>Page {page.currentPage} of {page.lastPage}</span> {page.url.next && <a href={page.url.next}>Next →</a>} </nav> ``` ### Tag/Category Archives ```astro --- // src/pages/tags/[tag]/[...page].astro import { getCollection } from 'astro:content'; export async function getStaticPaths({ paginate }) { const allPosts = await getCollection('blog'); const allTags = [...new Set(allPosts.flatMap((post) => post.data.tags))]; return allTags.flatMap((tag) => { const filtered = allPosts.filter((post) => post.data.tags.includes(tag)); return paginate(filtered, { params: { tag }, pageSize: 10, }); }); } const { page } = Astro.props; const { tag } = Astro.params; --- <h1>Posts tagged "{tag}"</h1> <ul> {page.data.map((post) => ( <li><a href={`/blog/${post.id}/`}>{post.data.title}</a></li> ))} </ul> ``` Tag index page: ```astro --- // src/pages/tags/index.astro import { getCollection } from 'astro:content'; const allPosts = await getCollection('blog'); const tags = [...new Set(allPosts.flatMap((post) => post.data.tags))].sort(); --- <h1>All Tags</h1> <ul> {tags.map((tag) => ( <li><a href={`/tags/${tag}/1/`}>{tag}</a></li> ))} </ul> ``` ### Static Forms **Formspree:** ```astro <form action="https://formspree.io/f/{form_id}" method="POST"> <label> Email <input type="email" name="email" required /> </label> <label> Message <textarea name="message" required></textarea> </label> <button type="submit">Send</button> </form> ``` **Netlify Forms:** ```astro <form name="contact" method="POST" data-netlify="true" netlify-honeypot="bot-field"> <input type="hidden" name="form-name" value="contact" /> <p class="hidden"><input name="bot-field" /></p> <label> Email <input type="email" name="email" required /> </label> <label> Message <textarea name="message" required></textarea> </label> <button type="submit">Send</button> </form> ``` ### Dark Mode Toggle ```astro --- // src/components/ThemeToggle.astro --- <button id="theme-toggle" aria-label="Toggle dark mode" type="button"> <span class="sun">☀️</span> <span class="moon">🌙</span> </button> <script> const toggle = document.getElementById('theme-toggle')!; function getTheme(): 'light' | 'dark' { return ( (localStorage.getItem('theme') as 'light' | 'dark') ?? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') ); } function setTheme(theme: 'light' | 'dark') { document.documentElement.dataset.theme = theme; localStorage.setItem('theme', theme); } // Apply on load setTheme(getTheme()); toggle.addEventListener('click', () => { setTheme(getTheme() === 'dark' ? 'light' : 'dark'); }); </script> <style> #theme-toggle { background: none; border: none; cursor: pointer; font-size: 1.25rem; } :root[data-theme='dark'] .sun { display: none; } :root[data-theme='light'] .moon { display: none; } </style> ``` > **Note:** Starlight includes a built-in theme toggle. This pattern is for custom Astro sites. -
testing.md 7.3 KB
# Testing Astro Projects Complete guide for testing Astro projects — from unit/component tests to E2E, link checking, type safety, and CI pipelines. --- ## 1. Component Testing with Vitest ### Setup ```bash npm install -D vitest @vitest/ui ``` ### vitest.config.ts ```ts /// <reference types="vitest" /> import { getViteConfig } from 'astro/config'; export default getViteConfig({ test: { include: ['tests/**/*.{test,spec}.{js,ts}'], }, }); ``` ### AstroContainer API The `AstroContainer` API renders Astro components in isolation without a full dev server. ```ts import { experimental_AstroContainer as AstroContainer } from 'astro/container'; import { expect, test } from 'vitest'; import Greeting from '../src/components/Greeting.astro'; test('renders greeting with name prop', async () => { const container = await AstroContainer.create(); const result = await container.renderToString(Greeting, { props: { name: 'World' }, }); expect(result).toContain('Hello, World'); }); ``` ### Testing Props ```ts test('renders default when no name provided', async () => { const container = await AstroContainer.create(); const result = await container.renderToString(Greeting, { props: {}, }); expect(result).toContain('Hello, stranger'); }); ``` ### Testing Slots ```ts import Card from '../src/components/Card.astro'; test('renders slot content', async () => { const container = await AstroContainer.create(); const result = await container.renderToString(Card, { slots: { default: '<p>Slot content here</p>' }, }); expect(result).toContain('Slot content here'); }); ``` ### Testing Conditional Rendering ```ts import Alert from '../src/components/Alert.astro'; test('renders error variant', async () => { const container = await AstroContainer.create(); const result = await container.renderToString(Alert, { props: { type: 'error', message: 'Something failed' }, }); expect(result).toContain('class="alert-error"'); expect(result).toContain('Something failed'); }); test('does not render when hidden', async () => { const container = await AstroContainer.create(); const result = await container.renderToString(Alert, { props: { type: 'info', message: 'Hidden', visible: false }, }); expect(result).not.toContain('Hidden'); }); ``` ### Run Tests ```bash npx vitest npx vitest --ui # browser UI ``` --- ## 2. E2E Testing with Playwright ### Setup ```bash npm install -D @playwright/test npx playwright install ``` ### playwright.config.ts ```ts import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './e2e', webServer: { command: 'npm run preview', port: 4321, reuseExistingServer: !process.env.CI, }, use: { baseURL: 'http://localhost:4321', }, }); ``` > **Note:** Run `astro build` before E2E tests so `preview` has something to serve. ### Example: Page Load ```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(); }); ``` ### Example: Navigation ```ts test('navigates to about page', async ({ page }) => { await page.goto('/'); await page.click('a[href="/about"]'); await expect(page).toHaveURL('/about'); await expect(page.locator('h1')).toContainText('About'); }); ``` ### Example: Dynamic Routes ```ts test('blog post renders from content collection', async ({ page }) => { await page.goto('/blog/first-post'); await expect(page.locator('article h1')).toBeVisible(); await expect(page.locator('article')).not.toBeEmpty(); }); ``` ### Testing View Transitions ```ts test('view transitions work between pages', async ({ page }) => { await page.goto('/'); const transitionPromise = page.waitForEvent('load'); await page.click('a[href="/about"]'); await transitionPromise; await expect(page).toHaveURL('/about'); }); ``` ### Run E2E Tests ```bash npx astro build npx playwright test npx playwright test --ui # interactive mode ``` --- ## 3. Link Checking ### linkinator Checks all links in the built output for broken references. ```bash npx astro build npx linkinator dist --recurse ``` Options: ```bash npx linkinator dist --recurse --skip "^https://external-site.com" ``` ### CI Integration (GitHub Actions) ```yaml - name: Check links run: npx linkinator dist --recurse --retry --retry-errors ``` --- ## 4. Type Checking ### Astro Template Validation ```bash npx astro check ``` Validates `.astro` files for type errors in expressions, prop types, and component usage. ### TypeScript Checking ```bash npx tsc --noEmit ``` Validates all `.ts` and `.tsx` files without emitting output. ### package.json Scripts ```json { "scripts": { "check": "astro check && tsc --noEmit" } } ``` --- ## 5. Content Collection Validation ### Schema Enforcement Content collections validate against Zod schemas at build time. Invalid content **fails the build automatically**: ```ts // src/content.config.ts import { defineCollection, z } from 'astro:content'; const blog = defineCollection({ type: 'content', schema: z.object({ title: z.string(), date: z.date(), draft: z.boolean().default(false), }), }); export const collections = { blog }; ``` A frontmatter error produces: ``` [ERROR] blog → "bad-post.md" frontmatter does not match schema. "title" is required. ``` ### Draft Filtering Filter drafts in production queries: ```astro --- import { getCollection } from 'astro:content'; const posts = await getCollection('blog', ({ data }) => { return import.meta.env.PROD ? !data.draft : true; }); --- ``` Test that drafts are excluded by checking the built output does not contain draft post URLs. --- ## 6. Pre-Deploy Verification Script Save as `scripts/verify.sh`: ```bash #!/bin/bash set -e echo "→ Type checking..." npx astro check echo "→ Building..." npx astro build echo "→ Checking links..." npx linkinator dist --recurse echo "→ Running E2E tests..." npx playwright test echo "✓ All checks passed" ``` ```bash chmod +x scripts/verify.sh ./scripts/verify.sh ``` --- ## 7. CI Pipeline (GitHub Actions) Save as `.github/workflows/test.yml`: ```yaml name: Test on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - run: npm ci - name: Type check run: npx astro check && npx tsc --noEmit - name: Build run: npx astro build - name: Component tests run: npx vitest run - name: Install Playwright run: npx playwright install --with-deps chromium - name: E2E tests run: npx playwright test - name: Link check run: npx linkinator dist --recurse --retry - uses: actions/upload-artifact@v4 if: failure() with: name: playwright-report path: playwright-report/ ``` --- ## Quick Reference | Task | Command | |------|---------| | Component tests | `npx vitest` | | E2E tests | `npx playwright test` | | Type check | `npx astro check && tsc --noEmit` | | Link check | `npx linkinator dist --recurse` | | Full verification | `./scripts/verify.sh` | -
v6-features.md 5.4 KB
# Astro v6 Features (still current in v7) These features were introduced or stabilized in Astro v6 and remain fully supported in v7. --- ## 1. Content Collections v2 Type-safe content management with Zod schemas and flexible data loaders. ```ts // content.config.ts import { defineCollection, z } from 'astro:content'; const blog = defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/blog' }), schema: z.object({ title: z.string(), date: z.date(), draft: z.boolean().default(false), }), }); export const collections = { blog }; ``` **Loaders:** - `file()` — single file (JSON, YAML) - `glob()` — match files by pattern - Custom loaders — fetch from CMS at build or request time (live collections) **Querying:** ```ts import { getCollection, getEntry } from 'astro:content'; const posts = await getCollection('blog', ({ data }) => !data.draft); const post = await getEntry('blog', 'my-post'); ``` --- ## 2. Server Actions Type-safe RPC endpoints with Zod validation. ```ts // src/actions/index.ts import { defineAction } from 'astro:actions'; import { z } from 'astro:schema'; export const server = { subscribe: defineAction({ input: z.object({ email: z.string().email() }), handler: async ({ email }) => { // process subscription return { success: true }; }, }), }; ``` **Usage in components:** ```ts import { actions } from 'astro:actions'; const result = await actions.subscribe({ email: 'user@example.com' }); ``` **Form integration with progressive enhancement:** ```astro <form method="POST" action={actions.subscribe}> <input type="email" name="email" /> <button type="submit">Subscribe</button> </form> ``` --- ## 3. Sessions Server-side session management with pluggable drivers. **Config:** ```js // astro.config.mjs export default defineConfig({ session: { driver: 'cookie', // also: node-fs, redis, etc. }, }); ``` **Usage:** ```ts // In pages/endpoints const user = await Astro.session.get('user'); await Astro.session.set('user', { name: 'Alice' }); // In middleware const user = await context.session.get('user'); ``` --- ## 4. Server Islands Defer component rendering to request time while keeping the page static. ```astro --- import UserGreeting from '../components/UserGreeting.astro'; --- <UserGreeting server:defer /> ``` - Placeholder rendered at build time - Component fetched and rendered at request time - Perfect for personalized content in otherwise static pages --- ## 5. Environment Variables (astro:env) Type-safe, validated environment variables. ```ts import { MY_SECRET } from 'astro:env/server'; import { PUBLIC_API_URL } from 'astro:env/client'; ``` **Schema definition:** ```js // astro.config.mjs export default defineConfig({ env: { schema: { MY_SECRET: envField.string({ context: 'server', access: 'secret' }), PUBLIC_API_URL: envField.string({ context: 'client', access: 'public' }), }, }, }); ``` Variables are validated at build time — missing or invalid values cause build failures. --- ## 6. On-Demand Rendering Hybrid static/SSR on a per-page basis. ```astro --- // This page renders on every request export const prerender = false; --- ``` **Adapters:** - `@astrojs/node` - `@astrojs/cloudflare` - `@astrojs/netlify` - `@astrojs/vercel` **Hybrid mode:** static by default, opt individual pages into SSR with `prerender = false`. --- ## 7. View Transitions Client-side navigation with animated transitions between pages. ```astro --- import { ViewTransitions } from 'astro:transitions'; --- <head> <ViewTransitions /> </head> <h1 transition:name="title" transition:animate="slide">Hello</h1> <div transition:persist> <!-- State preserved across navigation --> </div> ``` **Lifecycle events:** - `astro:before-preparation` - `astro:after-swap` - `astro:page-load` --- ## 8. Middleware Request/response pipeline with access to context. ```ts // src/middleware.ts import { defineMiddleware, sequence } from 'astro:middleware'; const auth = defineMiddleware(async (context, next) => { const token = context.cookies.get('token'); context.locals.user = await validateToken(token?.value); return next(); }); const logging = defineMiddleware(async (context, next) => { console.log(context.url.pathname); return next(); }); export const onRequest = sequence(auth, logging); ``` Access to `context.locals`, `context.cookies`, `context.redirect()`. --- ## 9. Image Optimization Built-in image processing with automatic optimization. ```astro --- import { Image } from 'astro:assets'; import hero from '../assets/hero.png'; --- <Image src={hero} alt="Hero" width={800} /> ``` - Automatic format conversion, lazy loading, responsive sizes - Remote images configured via `image.domains` and `image.remotePatterns`: ```js // astro.config.mjs export default defineConfig({ image: { domains: ['cdn.example.com'], remotePatterns: [{ protocol: 'https', hostname: '**.example.com' }], }, }); ``` --- ## 10. Internationalization (i18n) Built-in i18n routing with locale-aware URL generation. ```js // astro.config.mjs export default defineConfig({ i18n: { defaultLocale: 'en', locales: ['en', 'pt-br', 'es'], routing: { prefixDefaultLocale: false, }, }, }); ``` **URL generation:** ```ts import { getRelativeLocaleUrl } from 'astro:i18n'; getRelativeLocaleUrl('pt-br', '/about'); // → /pt-br/about ``` **Strategies:** pathname prefixes or domain-based routing. -
v7-features.md 12.3 KB
# Astro v7 Features Complete reference for all major features introduced in Astro v7. --- ## 1. Vite 8 + Rolldown Astro v7 ships with **Vite 8**, which replaces the previous esbuild + Rollup bundling pipeline with **Rolldown** — a Rust-based bundler. ### Key Points - **Rust-based bundler** replacing both esbuild (transform) and Rollup (bundling) in a single tool - **10-30x faster** than Rollup for production builds - **Same plugin API** — fully backwards compatible with existing Vite/Rollup plugins - **Compatibility layer** auto-converts `build.rollupOptions` and esbuild-specific options to their Rolldown equivalents ### Migration No changes required for most projects. If you use `vite.build.rollupOptions` in `astro.config.mjs`, the compatibility layer handles the conversion automatically. Warnings are emitted for any options that cannot be directly mapped. ```js // astro.config.mjs — works as before import { defineConfig } from 'astro/config'; export default defineConfig({ vite: { build: { // Automatically converted to Rolldown equivalents rollupOptions: { output: { manualChunks: { vendor: ['react', 'react-dom'] } } } } } }); ``` --- ## 2. Rust Compiler The Astro template compiler has been rewritten in **Rust**, replacing the previous Go-based compiler. Built on **oxc** (JavaScript/TypeScript parser) and **Lightning CSS**. ### Key Points - **Native binaries** for all major platforms with **WASM fallback** for unsupported architectures - **Strict parsing**: unclosed tags are now errors (no HTML auto-correction) - **JSX whitespace rules**: newlines between inline elements produce no whitespace in output - **CSS differences**: minor cosmetic changes to color serialization and `url()` quoting (output-only, no behavioral change) ### Breaking Changes #### Strict HTML Parsing ```astro <!-- ❌ Error in v7 — unclosed tag --> <div> <p>Hello world </div> <!-- ✅ Correct --> <div> <p>Hello world</p> </div> ``` #### JSX Whitespace Rules ```astro <!-- In v7, newline between inline elements = no space in output --> <span>Hello</span> <span>World</span> <!-- Renders: "HelloWorld" --> <!-- Add explicit space --> <span>Hello</span>{' '} <span>World</span> <!-- Renders: "Hello World" --> ``` #### CSS Cosmetic Differences ```css /* v6 output */ background: url(image.png); color: #ff0000; /* v7 output (functionally identical) */ background: url("image.png"); color: red; ``` --- ## 3. Sätteri (Markdown/MDX in Rust) **Sätteri** is Astro v7's default Markdown and MDX processor, replacing the unified/remark/rehype pipeline with a Rust-native implementation. ### Key Points - Built on **pulldown-cmark** (Markdown parsing) + **oxc** (MDX/JSX) - Default processor — no configuration needed for standard usage - Replaces unified/remark/rehype with dramatically faster processing ### Built-in Features | Feature | Description | |---------|-------------| | GFM | Tables, strikethrough, task lists, autolinks | | Smart punctuation | Curly quotes, em/en dashes | | Heading IDs | Auto-generated anchor IDs | | Directives | Container/leaf/text directives (`::: note`, etc.) | | Math | LaTeX math blocks (`$$...$$`) and inline (`$...$`) | | Frontmatter | YAML frontmatter parsing | | Superscript/Subscript | `^super^` and `~sub~` syntax | | Wikilinks | `[[page]]` and `[[page|text]]` syntax | ### Configuration ```js // astro.config.mjs import { defineConfig } from 'astro/config'; import { satteri } from '@astrojs/markdown-satteri'; export default defineConfig({ markdown: { processor: satteri({ gfm: true, smartPunctuation: true, headingIds: true, math: true, wikilinks: true, directives: true, }) } }); ``` ### Plugin API Sätteri plugins declare which node types they handle, skipping all others. This is significantly cheaper than unified's visitor pattern. ```js // my-satteri-plugin.js export default function myPlugin() { return { name: 'my-plugin', nodes: ['heading', 'paragraph'], // only visit these types transform(node, context) { if (node.type === 'heading') { // transform heading nodes } } }; } ``` ### Fallback to unified/remark/rehype For projects relying on existing remark/rehype plugins: ```js // astro.config.mjs import { defineConfig } from 'astro/config'; import { unified } from '@astrojs/markdown-remark'; import remarkToc from 'remark-toc'; import rehypePrism from 'rehype-prism'; export default defineConfig({ markdown: { processor: unified({ remarkPlugins: [remarkToc], rehypePlugins: [rehypePrism], }) } }); ``` ### Docker Deployment Note Sätteri ships native bindings only for **glibc** (`@bruits/satteri-linux-x64-gnu`). Alpine Linux uses musl — there is no musl binding. Docker build stages MUST use `node:22-slim` (Debian/glibc), not `node:22-alpine`. This affects all Astro v7 projects using Sätteri (the default), including Starlight sites. Projects using `unified()` explicitly are NOT affected (they bypass Sätteri entirely). --- ## 4. Queued Rendering Astro v7's rendering engine uses a **queue/stack-based** approach instead of recursive rendering. ### Key Points - **~2.4x faster** for expression-dense pages (many dynamic expressions, loops, conditionals) - **Now stable and default** — no configuration needed - Eliminates deep call-stack issues on complex component trees - Reduces memory pressure through iterative processing ### Migration No action required. This is an internal engine change that is fully transparent to user code. --- ## 5. Advanced Routing (`src/fetch.ts`) Astro v7 introduces a **standard fetch handler pattern** for advanced routing control, following the same conventions as Cloudflare Workers, Deno, and Bun. ### Key Points - Define a `src/fetch.ts` file to take full control of the request pipeline - Compose individual pieces: `i18n()`, `actions()`, `middleware()`, `pages()` - Full control over request pipeline order - Compatible with Hono for complex routing scenarios ### Basic Routing ```ts // src/fetch.ts import { astro, FetchState } from 'astro/fetch'; export default astro((request: Request, state: FetchState) => { // Compose the pipeline in your preferred order return state.pipeline( i18n(), middleware(), actions(), pages() ); }); ``` ### Hono Integration ```ts // src/fetch.ts import { astro } from 'astro/hono'; import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { logger } from 'hono/logger'; const app = new Hono(); app.use('*', logger()); app.use('/api/*', cors()); app.get('/api/health', (c) => c.json({ status: 'ok' })); // Hand off to Astro for everything else export default astro(app); ``` ### Composing Middleware ```ts // src/fetch.ts import { astro, FetchState } from 'astro/fetch'; import { i18n, actions, middleware, pages } from 'astro/fetch'; export default astro((request: Request, state: FetchState) => { const url = new URL(request.url); // Custom routing logic if (url.pathname.startsWith('/api/')) { return state.pipeline( actions() ); } // Full pipeline for pages return state.pipeline( i18n(), middleware(), actions(), pages() ); }); ``` --- ## 6. Route Caching (Stable) Route-level caching is now **stable** in Astro v7, providing fine-grained control over page caching with tag-based invalidation. ### Key Points - In-memory cache available out of the box - Per-page caching with `Astro.cache.set()` - Declarative `routeRules` in config - Tag-based invalidation - Integration with live content collections ### Config-Level Setup ```js // astro.config.mjs import { defineConfig } from 'astro/config'; import { memoryCache } from 'astro/config'; export default defineConfig({ cache: memoryCache(), routeRules: { '/blog/**': { cache: { maxAge: 3600, swr: 86400, tags: ['blog'] } }, '/products/**': { cache: { maxAge: 600, tags: ['products'] } }, '/about': { cache: { maxAge: 86400 } }, } }); ``` ### Per-Page Caching ```astro --- // src/pages/blog/[slug].astro const { slug } = Astro.params; const post = await getEntry('blog', slug); Astro.cache.set({ maxAge: 3600, // 1 hour swr: 86400, // stale-while-revalidate: 24 hours tags: ['blog', `post:${slug}`] }); --- <article> <h1>{post.data.title}</h1> <Content /> </article> ``` ### Webhook Invalidation ```ts // src/pages/api/revalidate.ts import type { APIRoute } from 'astro'; import { cache } from 'astro:cache'; export const POST: APIRoute = async ({ request }) => { const { secret, tags, path } = await request.json(); if (secret !== import.meta.env.REVALIDATION_SECRET) { return new Response('Unauthorized', { status: 401 }); } // Invalidate by tags if (tags) { await cache.invalidate({ tags }); } // Invalidate by path if (path) { await cache.invalidate({ path }); } return new Response(JSON.stringify({ revalidated: true }), { status: 200, headers: { 'Content-Type': 'application/json' } }); }; ``` ### Live Content Collections Integration ```js // astro.config.mjs import { defineConfig } from 'astro/config'; import { memoryCache } from 'astro/config'; export default defineConfig({ cache: memoryCache(), content: { collections: { blog: { // When content changes, invalidate matching cache tags onUpdate: (entry) => cache.invalidate({ tags: ['blog', `post:${entry.slug}`] }) } } } }); ``` --- ## 7. CDN Cache Providers (Experimental) CDN-level cache providers push cache directives to the edge, allowing cached responses to be served **without invoking the server**. ### Key Points - Edge-level caching — hits never reach your application server - Platform-specific providers for Netlify, Vercel, and Cloudflare - Works with the same `routeRules` and `Astro.cache.set()` API ### Providers ```js // Netlify import { cacheNetlify } from '@astrojs/netlify/cache'; export default defineConfig({ cache: cacheNetlify(), }); ``` ```js // Vercel import { cacheVercel } from '@astrojs/vercel/cache'; export default defineConfig({ cache: cacheVercel(), }); ``` ```js // Cloudflare (private beta) import { cacheCloudflare } from '@astrojs/cloudflare/cache'; export default defineConfig({ cache: cacheCloudflare(), }); ``` ### How It Works 1. On first request: server renders the page, cache provider stores response at the edge 2. On subsequent requests: CDN serves cached response directly (no server invocation) 3. On invalidation: cache is purged via provider API, next request triggers fresh render --- ## 8. AI Enhancements Astro v7 includes first-class support for AI-assisted development workflows. ### Key Points - **Background dev server**: auto-detects when running inside AI agents and optimizes output - **JSON logging**: configurable, composable log output for machine consumption - See `ai-dev-server.md` for full details ### Background Dev Server When Astro detects it's running inside an AI agent environment, it automatically: - Switches to structured JSON log output - Suppresses interactive UI elements (progress bars, spinners) - Provides machine-readable error messages with file/line references - Exposes a lightweight status API for agent polling ### JSON Logging ```js // astro.config.mjs export default defineConfig({ devToolbar: { enabled: false }, logging: { format: 'json', // 'pretty' | 'json' | 'minimal' level: 'info', } }); ``` --- ## 9. Performance Benchmarks Real-world build time improvements measured on production sites (Astro v6 → v7): | Site | v6 | v7 | Improvement | |------|----|----|-------------| | docs.astro.build | 114s | 73s | **36% faster** | | astro.build | 62s | 24s | **61% faster** | | biomejs.dev | 176s | 150s | **15% faster** | | developers.cloudflare.com | 387s | 262s | **32% faster** | ### Contributing Factors - **Rolldown bundler**: 10-30x faster than Rollup for the bundling phase - **Rust compiler**: eliminates Go→WASM overhead, native binary execution - **Sätteri**: Markdown/MDX processing in Rust vs. JavaScript-based unified pipeline - **Queued rendering**: 2.4x faster for expression-dense templates ### Impact by Project Size - Small sites (< 100 pages): 15-25% faster builds - Medium sites (100-1000 pages): 30-45% faster builds - Large sites (1000+ pages): 40-60% faster builds The largest gains are seen in content-heavy sites with extensive Markdown processing and complex component trees. -
validation-checklist.md 6.2 KB
# Validation Checklist Complete checklist for validating an Astro installation/upgrade. Run these checks in the project root. --- ## 1. Build Validation ```bash # Full production build — must exit 0 with no errors npx astro build # Type checking (requires @astrojs/check) npx astro check # Verify no unclosed tags (Rust compiler is strict about this) find src -name "*.astro" -exec grep -Pn '<(img|br|hr|input|meta|link|source|area|base|col|embed|param|track|wbr)[^/]*[^/]>' {} + # Check for HTML nesting issues (div/section/article inside p) grep -rPn '<p[^>]*>[\s\S]*?<(div|section|article|ul|ol|table|blockquote|h[1-6])' src/**/*.astro ``` **Expected:** All commands pass with no errors or matches. --- ## 2. Breaking Pattern Detection ### Unclosed HTML tags ```bash # Find self-closing tags that are NOT void elements (common breakage) grep -rPn '<(div|span|p|a|section|main|footer|header|nav|ul|li)\s[^>]*/>' src/ --include="*.astro" ``` ### Block elements inside `<p>` ```bash grep -rPn '<p[^>]*>[\s\S]*?<(div|section|article|ul|ol|dl|table|blockquote|pre|h[1-6]|form|fieldset|hr)' src/ --include="*.astro" ``` ### Whitespace-dependent inline layouts ```bash # Look for adjacent inline elements that rely on whitespace rendering grep -rPn '</(span|a|strong|em|code)>\s*<(span|a|strong|em|code)' src/ --include="*.astro" ``` ### src/fetch.ts conflict ```bash # Astro reserves src/fetch.ts — check if it exists find src -maxdepth 1 -name "fetch.ts" -o -name "fetch.js" ``` ### @astrojs/db usage (removed in Astro 5+) ```bash grep -rn "@astrojs/db" package.json src/ --include="*.{ts,js,astro}" ``` ### Deprecated transition imports ```bash # TRANSITION_* named exports removed grep -rPn 'TRANSITION_[A-Z_]+' src/ --include="*.{ts,js,astro}" # isTransition*() helpers removed grep -rPn 'isTransition\w+\(' src/ --include="*.{ts,js,astro}" ``` ### getContainerRenderer() from package root ```bash # Must now import from /container subpath grep -rn "getContainerRenderer" src/ --include="*.{ts,js}" | grep -v "/container" ``` ### Experimental flags that should be removed ```bash # Check astro.config for experimental flags that graduated to stable grep -A 20 'experimental:' astro.config.{mjs,ts,js} 2>/dev/null | grep -P '(contentLayer|serverIslands|actions|env|fonts|responsiveImages|svg)' ``` --- ## 3. Deprecated Pattern Detection | Pattern | grep command | Fix | |---------|-------------|-----| | `Astro.glob()` | `grep -rn "Astro.glob" src/ --include="*.astro"` | Replace with `import.meta.glob()` or Content Collections | | `Astro.fetchContent()` | `grep -rn "Astro.fetchContent" src/ --include="*.astro"` | Replace with Content Collections | | `getStaticPaths` without `paginate` import | `grep -rn "getStaticPaths" src/ --include="*.astro"` | Verify using new pagination API | | Legacy content collections (`src/content/config.ts` with `defineCollection` using `schema` only) | `grep -rn "defineCollection" src/content/config.ts` | Migrate to `type: 'content_layer'` or new loader API | | `@astrojs/image` | `grep -rn "@astrojs/image" package.json` | Use built-in `astro:assets` | | `integrations: [image()]` | `grep -rn "image()" astro.config.*` | Remove — use built-in `<Image>` component | | `<Markdown>` component | `grep -rn "<Markdown" src/ --include="*.astro"` | Use MDX or Content Collections | | `set:html` on component | `grep -rPn 'set:html' src/ --include="*.astro"` | Verify it's on HTML elements only | | `class:list` with nested arrays | `grep -rPn 'class:list=\{.*\[.*\[' src/ --include="*.astro"` | Flatten to single array | --- ## 4. Markdown/MDX Validation ### Remark/Rehype plugin migration ```bash # Check if custom remark/rehype plugins are configured grep -Pn '(remarkPlugins|rehypePlugins)' astro.config.{mjs,ts,js} 2>/dev/null # If found, verify @astrojs/markdown-remark is installed grep -n "@astrojs/markdown-remark" package.json ``` **Fix:** If custom plugins exist but `@astrojs/markdown-remark` is missing: ```bash npx astro add @astrojs/markdown-remark ``` ### Shiki (syntax highlighting) compatibility ```bash # Check for custom Shiki config — API may have changed grep -A 10 'shikiConfig' astro.config.{mjs,ts,js} 2>/dev/null ``` ### GFM features (tables, strikethrough, autolinks) ```bash # GFM is built-in — check there's no redundant remark-gfm grep -rn "remark-gfm" package.json astro.config.{mjs,ts,js} 2>/dev/null ``` **Fix:** Remove `remark-gfm` from plugins — GFM is included by default. ### Test MDX rendering ```bash # Verify MDX integration is present if .mdx files exist find src -name "*.mdx" | head -1 && grep -n "@astrojs/mdx" package.json ``` --- ## 5. Performance Validation ### Compare build times ```bash # Time the build (run before and after upgrade) time npx astro build 2>&1 | tail -5 ``` ### Verify queued rendering is active ```bash # Queued rendering should be default in Astro 5+ — check it's not disabled grep -n "queuedRendering" astro.config.{mjs,ts,js} 2>/dev/null ``` **Expected:** No results (uses default) or `true`. If set to `false`, remove it. ### Check Vite 6+ bundle output ```bash # Verify build output structure ls -la dist/ 2>/dev/null || ls -la dist/_astro/ 2>/dev/null # Check chunk sizes find dist -name "*.js" -exec wc -c {} + | sort -n | tail -10 # Verify no duplicate framework chunks find dist -name "*.js" | xargs grep -l "react" 2>/dev/null | wc -l ``` ### Verify no dev-only code in production build ```bash grep -rn "import.meta.env.DEV" dist/ 2>/dev/null ``` --- ## Quick Full Validation Script ```bash #!/usr/bin/env bash set -e echo "=== Astro Validation ===" echo "[1/5] Build..." npx astro build echo "[2/5] Type check..." npx astro check || echo "WARN: astro check failed" echo "[3/5] Breaking patterns..." grep -rn "@astrojs/db" src/ --include="*.{ts,js,astro}" && echo "FAIL: @astrojs/db found" || true grep -rPn 'TRANSITION_[A-Z_]+' src/ --include="*.{ts,js,astro}" && echo "FAIL: deprecated transitions" || true find src -maxdepth 1 -name "fetch.ts" -o -name "fetch.js" | grep . && echo "FAIL: src/fetch conflict" || true echo "[4/5] Deprecated APIs..." grep -rn "Astro.glob\|Astro.fetchContent\|@astrojs/image" src/ package.json && echo "FAIL: deprecated APIs" || true echo "[5/5] Performance..." time npx astro build 2>&1 | tail -3 echo "=== Done ===" ```
-
-
SKILL.md 9.4 KB
--- name: astro-sites-manager description: > Comprehensive skill for building, migrating, and maintaining Astro v7 projects. Covers best practices from the official AGENTS.md, the v6→v7 migration path, validation of breaking/deprecated patterns, AI-enhanced dev server usage (background mode, JSON logging), advanced routing with src/fetch.ts, route caching, Sätteri Markdown, and the Rust compiler. Use when the user mentions 'Astro', '.astro files', 'astro dev', 'astro build', 'islands architecture', 'content collections', 'SSG', 'SSR adapter', 'upgrade to Astro 7', 'migrate Astro', 'Astro v7', 'Astro v6', 'Sätteri', 'route caching', 'Astro.cache', 'astro dev --background', 'src/fetch.ts', 'advanced routing', 'Hono + Astro', 'Rolldown', 'Vite 8', 'queued rendering', 'CDN cache provider', 'Astro AI', 'related content', 'related posts', 'vector embeddings Astro', 'astro-related-content', or asks about static site generation with Astro. metadata: author: ft.ia.br version: "1.0.0" date: 2026-06-22 repository: https://github.com/fabricioctelles/skills license: Apache-2.0 category: ci-cd-and-deployment --- # Astro Framework — v7 ## MCP Documentation Access This skill works alongside the **Astro Docs MCP server**. Before answering Astro questions, check if the `astro-docs` MCP tool is available and query it for the latest documentation. The MCP server provides real-time access to docs.astro.build and is the single source of truth for current APIs. ``` MCP Server: astro-docs Tool: search_astro_docs ``` If the MCP server is unavailable, fall back to the reference material in this skill and https://docs.astro.build. --- ## Best Practices ### Component Design - One `.astro` component per file. Keep components small and focused. - Use frontmatter (`---`) for data fetching and logic; template below for markup only. - Prefer Astro components over framework components unless client interactivity is needed. - Use `client:*` directives sparingly — each adds JavaScript to the bundle. - Directive hierarchy: `client:idle` > `client:visible` > `client:load` (prefer lazy). ### Routing & Pages - Use file-based routing in `src/pages/`. Dynamic routes: `[slug].astro`, `[...path].astro`. - Always export `getStaticPaths()` for prerendered dynamic routes. - For SSR pages: `export const prerender = false` at the top. - Use `src/fetch.ts` (v7) only when you need control beyond middleware — don't use it for simple auth. ### Content Collections - Define schemas in `content.config.ts` with Zod — never trust untyped content. - Use `getCollection()` for lists, `getEntry()` for single items. - Prefer `glob()` loader for local files, custom loaders for CMS data. ### Performance - Default to static (`prerender = true`). Use SSR only for personalized/dynamic content. - Use `<Image />` from `astro:assets` — never raw `<img>` for local images. - Prefer Sätteri (default v7) over unified for Markdown — it's significantly faster. - Use Server Islands (`server:defer`) for mixing static shells with dynamic fragments. ### Styling - Scoped `<style>` in `.astro` files is default and preferred. - Use `is:global` only when truly needed (third-party component styling). - Tailwind: install with `astro add tailwind`, don't configure manually. ### TypeScript - Run `astro sync` after changing content schemas or env variables. - Run `astro check` before committing — catches template type errors other tools miss. - Use `astro:env/server` and `astro:env/client` for typed env variables (never `process.env` directly). ### Development Workflow - Use `astro dev` for HMR. Never use `python -m http.server` or other static servers. - Use `astro add` for official integrations — don't manually edit config for them. - Use `astro build && astro preview` to test production behavior locally. - In AI agent workflows: use `astro dev --background` and validate via `/_astro/status`. --- ## CLI Commands ```bash npx astro dev # Dev server (foreground) npx astro dev --background # Dev server (detached, for AI agents) npx astro dev --json # Dev server with JSON structured logs npx astro build # Production build npx astro preview # Serve production build locally npx astro check # Type checking and diagnostics npx astro sync # Generate TypeScript types npx astro add <integration># Install and configure integration ``` ### Background Dev Server (AI Agents) When working as an AI agent, use background mode: ```bash # Start (blocks until ready, then detaches) astro dev --background # → Dev server running at http://localhost:4321 (pid 12345) # Check status astro dev status # Read logs astro dev logs # Stop astro dev stop # Health check endpoint (JSON) curl http://localhost:4321/_astro/status # → {"ok": true} ``` **Key behaviors:** - Lockfile prevents duplicate instances — starting again returns existing instance - All commands are idempotent (stop when not running = silent success) - Auto-detected when running inside an AI agent (no flag needed) - Opt out: `ASTRO_DEV_BACKGROUND=0 astro dev` --- ## Project Structure ``` src/ ├── pages/ # File-based routing (.astro, .md, .mdx) ├── layouts/ # Reusable page layouts ├── components/ # Astro & framework components ├── content/ # Content collections (type-safe) ├── middleware.ts # Request middleware ├── fetch.ts # Advanced routing (v7, optional) ├── styles/ # Global CSS ├── assets/ # Optimized assets (images, fonts) ├── actions/ # Server actions └── env.d.ts # Environment type declarations astro.config.mjs # Main configuration content.config.ts # Content collection schemas tsconfig.json # TypeScript config ``` --- ## Configuration (v7) ```typescript import { defineConfig, memoryCache, logHandlers } from 'astro/config'; export default defineConfig({ // Output mode output: 'static', // or configure per-page with server adapter // Route caching (stable in v7) cache: { provider: memoryCache(), }, routeRules: { '/blog/[...path]': { maxAge: 300, swr: 60 }, }, // Logger (stable in v7) logger: logHandlers.json(), // or .console(), or .compose(...) // Markdown (Sätteri is default in v7) markdown: { // No config needed for defaults (GFM, smartypants, heading IDs) // For extra features: // processor: satteri({ features: { directive: true, math: true } }) }, // Advanced routing file (default: src/fetch.ts) // fetchFile: null, // disable if src/fetch.ts is used for other purposes // Whitespace (v7 default: 'jsx') compressHTML: 'jsx', // or true (v6 behavior), or false (preserve all) }); ``` --- ## Islands Architecture (client:* directives) ```astro <!-- Load immediately (interactive above the fold) --> <Counter client:load /> <!-- Load when browser is idle (non-critical interactivity) --> <Newsletter client:idle /> <!-- Load when scrolled into viewport (below the fold) --> <Comments client:visible /> <!-- Load on media query match (mobile-only widget) --> <MobileMenu client:media="(max-width: 768px)" /> <!-- Client-only, skip SSR entirely (browser APIs needed) --> <MapWidget client:only="react" /> <!-- Server Island: static shell, fetched at request time (v6+) --> <UserGreeting server:defer /> ``` **Decision guide:** No directive (default) = zero JS, static HTML. Add directive only when user interaction is required. --- ## Image Optimization ```astro --- import { Image } from 'astro:assets'; import heroImage from '../assets/hero.jpg'; --- <!-- Local image (optimized, lazy-loaded, responsive) --> <Image src={heroImage} alt="Hero" width={1200} /> <!-- Remote image (must allowlist domain in config) --> <Image src="https://cdn.example.com/photo.jpg" alt="Photo" width={800} height={600} /> ``` Config for remote images: ```typescript // astro.config.mjs image: { domains: ['cdn.example.com'], remotePatterns: [{ protocol: 'https', hostname: '**.cloudinary.com' }], } ``` --- ## Detailed References - [Install MCP Server](references/install-mcp.md) — Setup Astro Docs MCP for any AI tool (Kiro, Claude, Cursor, VS Code, etc.) - [Migration Guide v6→v7](references/migration-v6-to-v7.md) — Step-by-step upgrade plan with breaking changes checklist - [Validation Checklist](references/validation-checklist.md) — Verify installation, detect breaking/deprecated patterns - [AI Dev Server](references/ai-dev-server.md) — Background mode, JSON logging, agent detection - [Astro v7 Features](references/v7-features.md) — Rust compiler, Sätteri, Advanced Routing, Route Caching, CDN providers - [Astro v6 Features](references/v6-features.md) — Content Collections v2, Actions, Sessions, Server Islands, env module - [Related Content](references/related-content.md) — Vector embeddings para posts relacionados, deploy leve no Coolify sem modelo - [Testing](references/testing.md) — Vitest components, Playwright E2E, link checking, CI pipeline - [SEO Full Stack](references/seo-full-stack.md) — JSON-LD graph, agent discovery, IndexNow, OG images, build-time validation, performance - [Starlight & Patterns](references/starlight-and-patterns.md) — Docs sites, Pagefind search, i18n, pagination, RSS - [Deployment](references/deployment.md) — Cloudflare, Vercel, Netlify, Firebase, GitHub Pages, Docker/Coolify, Azure - [Coolify Deploy](references/coolify-deploy.md) — Self-hosted deploy on Coolify (Dockerfile, API, gotchas, recommended stack)
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.