Neue Skills, Referenzen & OpenWiki-Doku integriert

Umfangreiche Erweiterung der Skill-Bibliothek: Neue Skills für Humanisierung (Englisch/PT-BR), Design-Validierung, AI-SEO und Coolify-Deployment inkl. Regelwerke, Presets, Pattern-Referenzen, Testfälle und Automatisierungsskripte. Zusätzliche Skills für Revenue-Centric Design, Pier Cloud, OKF, Lebenslauf- und LinkedIn-Optimierung sowie zahlreiche Referenzdateien, Checklisten und YAML/JSON/Markdown-Templates. Einführung einer vollständigen OpenWiki-Dokumentation mit Architektur-, Domain- und Workflow-Beschreibungen, zentralem Index und automatisierten Updates. Modularer Aufbau, restriktive Lizenzen und umfassende Qualitäts- und Evaluationsmechanismen für alle neuen Inhalte.
This commit is contained in:
Tim Krampitz
2026-07-26 14:00:58 +02:00
parent 070727d5cd
commit 01046b01e4
202 changed files with 31290 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
---
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)

View File

@@ -0,0 +1,172 @@
# 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.

View File

@@ -0,0 +1,415 @@
# 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) |

View File

@@ -0,0 +1,396 @@
# 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

View File

@@ -0,0 +1,190 @@
# 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

View File

@@ -0,0 +1,557 @@
# 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
```

View File

@@ -0,0 +1,394 @@
# 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.

View File

@@ -0,0 +1,712 @@
# 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 (5120 chars) and description (15160 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.60.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/)

View File

@@ -0,0 +1,604 @@
# 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.

View File

@@ -0,0 +1,387 @@
# 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` |

View File

@@ -0,0 +1,271 @@
# 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.

View File

@@ -0,0 +1,491 @@
# 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.

View File

@@ -0,0 +1,210 @@
# 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 ==="
```