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:
172
.github/skills/astro-sites-manager/references/ai-dev-server.md
vendored
Normal file
172
.github/skills/astro-sites-manager/references/ai-dev-server.md
vendored
Normal 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.
|
||||
415
.github/skills/astro-sites-manager/references/coolify-deploy.md
vendored
Normal file
415
.github/skills/astro-sites-manager/references/coolify-deploy.md
vendored
Normal 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) |
|
||||
396
.github/skills/astro-sites-manager/references/deployment.md
vendored
Normal file
396
.github/skills/astro-sites-manager/references/deployment.md
vendored
Normal 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
|
||||
190
.github/skills/astro-sites-manager/references/install-mcp.md
vendored
Normal file
190
.github/skills/astro-sites-manager/references/install-mcp.md
vendored
Normal 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
|
||||
557
.github/skills/astro-sites-manager/references/migration-v6-to-v7.md
vendored
Normal file
557
.github/skills/astro-sites-manager/references/migration-v6-to-v7.md
vendored
Normal 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
|
||||
```
|
||||
394
.github/skills/astro-sites-manager/references/related-content.md
vendored
Normal file
394
.github/skills/astro-sites-manager/references/related-content.md
vendored
Normal 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.
|
||||
712
.github/skills/astro-sites-manager/references/seo-full-stack.md
vendored
Normal file
712
.github/skills/astro-sites-manager/references/seo-full-stack.md
vendored
Normal 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 (5–120 chars) and description (15–160 chars) — build fails if outside limits
|
||||
- `imageSchema` requires `alt` — image without alt won't compile
|
||||
|
||||
---
|
||||
|
||||
## 6. Build-Time Validation
|
||||
|
||||
```typescript
|
||||
// astro.config.mjs
|
||||
import seoGraph from '@jdevalk/astro-seo-graph/integration';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
seoGraph({
|
||||
// All enabled by default:
|
||||
validateH1: true, // 0 or >1 H1 = warning
|
||||
validateUniqueMetadata: true, // Duplicate title/desc across pages
|
||||
validateImageAlt: true, // <img> without alt
|
||||
validateMetadataLength: { // SERP-safe bounds
|
||||
title: { min: 30, max: 65 },
|
||||
description: { min: 70, max: 200 },
|
||||
},
|
||||
validateInternalLinks: { // Broken internal links or missing trailing slash
|
||||
skip: (href) => href.startsWith('/api/'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### What each validation catches:
|
||||
|
||||
- **H1**: Templates with duplicate or missing H1
|
||||
- **Duplicates**: Paginated pages sharing the same title (corpus-level bug)
|
||||
- **Alt text**: Images missed over the years
|
||||
- **Meta length**: Titles truncated in SERP or invisible descriptions
|
||||
- **Internal links**: `/about-me` without trailing slash that works via 301 but wastes a round-trip
|
||||
|
||||
### CI: External Broken Link Checker
|
||||
|
||||
For external links (internal validation doesn't cover), use [lychee](https://github.com/lycheeverse/lychee-action) in GitHub Actions:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/links.yml
|
||||
name: Check Links
|
||||
on:
|
||||
push:
|
||||
paths: ['src/content/**']
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Weekly for link rot
|
||||
|
||||
jobs:
|
||||
links:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: lycheeverse/lychee-action@v2
|
||||
with:
|
||||
args: --verbose --no-progress 'src/content/**/*.md'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Advanced Sitemaps
|
||||
|
||||
### Per-Collection with chunks
|
||||
|
||||
```typescript
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
|
||||
sitemap({
|
||||
entryLimit: 1000,
|
||||
chunks: {
|
||||
posts: (item) => {
|
||||
if (/^\/blog\/[^/]+/.test(new URL(item.url).pathname)) return item;
|
||||
},
|
||||
pages: (item) => item, // default bucket
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Produces: `sitemap-posts-0.xml`, `sitemap-pages-0.xml` — makes debugging easier in Google Search Console.
|
||||
|
||||
### Git-based lastmod
|
||||
|
||||
```typescript
|
||||
import { gitLastmod } from '@jdevalk/astro-seo-graph';
|
||||
|
||||
// In the sitemap serialize callback:
|
||||
serialize(item) {
|
||||
const filePath = urlToFilePath(item.url); // your logic
|
||||
const lastmod = gitLastmod(filePath, {
|
||||
excludeCommits: ['abc1234'], // bulk imports that don't count
|
||||
});
|
||||
return { ...item, lastmod: lastmod ?? item.lastmod };
|
||||
}
|
||||
```
|
||||
|
||||
`gitLastmod` uses `git log` for the real timestamp of the last commit that touched the file — doesn't depend on filesystem `mtime` (which resets on CI).
|
||||
|
||||
---
|
||||
|
||||
## 8. IndexNow — Active Notification
|
||||
|
||||
IndexNow notifies Bing, Yandex, and others that URLs changed, instead of waiting for passive crawl.
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
// astro.config.mjs
|
||||
seoGraph({
|
||||
indexNow: {
|
||||
key: process.env.INDEXNOW_KEY!,
|
||||
host: 'example.com',
|
||||
siteUrl: 'https://example.com',
|
||||
filter: (url) => !/^\/blog\/\d+\/$/.test(new URL(url).pathname), // Exclude pagination
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Key Route (ownership verification)
|
||||
|
||||
```typescript
|
||||
// src/pages/[your-key-here].txt.ts
|
||||
import { createIndexNowKeyRoute } from '@jdevalk/astro-seo-graph';
|
||||
|
||||
export const GET = createIndexNowKeyRoute({ key: 'your-key-here' });
|
||||
```
|
||||
|
||||
### Deploy order matters
|
||||
|
||||
1. Deploy the key route first
|
||||
2. Confirm `https://example.com/your-key.txt` returns 200
|
||||
3. Only then enable `indexNow` in the integration
|
||||
|
||||
> Submissions before the key is reachable = HTTP 403 and key permanently invalidated.
|
||||
|
||||
### Direct IndexNow API
|
||||
|
||||
For manual or custom submission:
|
||||
|
||||
```bash
|
||||
# Single URL
|
||||
curl "https://api.indexnow.org/indexnow?url=https://example.com/new-post/&key=YOUR_KEY"
|
||||
|
||||
# Batch (up to 10,000 URLs per POST)
|
||||
curl -X POST https://api.indexnow.org/indexnow \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"host": "example.com",
|
||||
"key": "YOUR_KEY",
|
||||
"urlList": [
|
||||
"https://example.com/post-1/",
|
||||
"https://example.com/post-2/"
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Auto-Generated OG Images
|
||||
|
||||
Pipeline: **satori** (JSX → SVG) → **sharp** (SVG → JPEG)
|
||||
|
||||
```typescript
|
||||
// src/pages/og/[...slug].jpg.ts
|
||||
import satori from 'satori';
|
||||
import sharp from 'sharp';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog');
|
||||
return posts.map((p) => ({ params: { slug: p.id } }));
|
||||
}
|
||||
|
||||
export async function GET({ params }) {
|
||||
const posts = await getCollection('blog');
|
||||
const post = posts.find((p) => p.id === params.slug);
|
||||
if (!post) return new Response('Not found', { status: 404 });
|
||||
|
||||
const fontData = await fetch('https://cdn.example.com/fonts/Inter-Bold.ttf')
|
||||
.then((r) => r.arrayBuffer());
|
||||
|
||||
const svg = await satori(
|
||||
{
|
||||
type: 'div',
|
||||
props: {
|
||||
style: {
|
||||
width: '100%', height: '100%',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
justifyContent: 'center', padding: '60px',
|
||||
background: 'linear-gradient(135deg, #1a1a2e, #16213e)',
|
||||
color: '#ffffff', fontFamily: 'Inter',
|
||||
},
|
||||
children: [
|
||||
{ type: 'div', props: { style: { fontSize: '48px', fontWeight: 700, lineHeight: 1.2 }, children: post.data.title } },
|
||||
{ type: 'div', props: { style: { fontSize: '24px', marginTop: '20px', opacity: 0.8 }, children: 'example.com' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ width: 1200, height: 675, fonts: [{ name: 'Inter', data: fontData, weight: 700 }] },
|
||||
);
|
||||
|
||||
const jpeg = await sharp(Buffer.from(svg)).jpeg({ quality: 80 }).toBuffer();
|
||||
|
||||
return new Response(jpeg, {
|
||||
headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'public, max-age=31536000, immutable' },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Why JPEG and not WebP/AVIF?** Social platforms don't reliably support modern formats yet.
|
||||
|
||||
**Size: 1200×675** — Google Discover requires ≥1200px width, and 16:9 works well cross-platform.
|
||||
|
||||
The `<Seo>` component derives the OG image URL from the slug automatically:
|
||||
|
||||
```typescript
|
||||
const slug = Astro.url.pathname.replace(/^\/|\/$/g, '');
|
||||
const ogImage = new URL(`/og/${slug || 'index'}.jpg`, SITE_URL).toString();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Agent Discovery
|
||||
|
||||
### Schema Endpoints (corpus-wide JSON-LD)
|
||||
|
||||
```typescript
|
||||
// src/pages/schema/post.json.ts
|
||||
import { getCollection } from 'astro:content';
|
||||
import { createSchemaEndpoint } from '@jdevalk/astro-seo-graph';
|
||||
import { buildArticle, buildWebPage, makeIds } from '@jdevalk/seo-graph-core';
|
||||
|
||||
const ids = makeIds({ siteUrl: 'https://example.com' });
|
||||
|
||||
export const GET = createSchemaEndpoint({
|
||||
entries: () => getCollection('blog'),
|
||||
mapper: (post) => {
|
||||
const url = `https://example.com/${post.id}/`;
|
||||
return [
|
||||
buildWebPage({ url, name: post.data.title, isPartOf: { '@id': ids.website }, datePublished: post.data.publishDate }, ids),
|
||||
buildArticle({ url, isPartOf: { '@id': ids.webPage(url) }, author: { '@id': ids.person }, headline: post.data.title, description: post.data.description ?? '', datePublished: post.data.publishDate }, ids, 'BlogPosting'),
|
||||
];
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Schema Map (`/schemamap.xml`)
|
||||
|
||||
```typescript
|
||||
// src/pages/schemamap.xml.ts
|
||||
import { createSchemaMap } from '@jdevalk/astro-seo-graph';
|
||||
|
||||
export const GET = createSchemaMap({
|
||||
siteUrl: 'https://example.com',
|
||||
entries: [
|
||||
{ path: '/schema/post.json', lastModified: new Date() },
|
||||
{ path: '/schema/page.json', lastModified: new Date() },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### API Catalog (RFC 9727)
|
||||
|
||||
```typescript
|
||||
// src/pages/.well-known/api-catalog.ts
|
||||
import { createApiCatalog } from '@jdevalk/astro-seo-graph';
|
||||
|
||||
export const GET = createApiCatalog({
|
||||
siteUrl: 'https://example.com',
|
||||
schemaEndpoints: [
|
||||
{ path: '/schema/post.json', schemaType: 'BlogPosting', serviceDoc: '/about/' },
|
||||
],
|
||||
schemaMap: { path: '/schemamap.xml' },
|
||||
});
|
||||
```
|
||||
|
||||
### Markdown Alternates
|
||||
|
||||
Serve a `.md` version of every page so agents can consume content without HTML parsing:
|
||||
|
||||
```typescript
|
||||
// src/pages/blog/[...slug].md.ts
|
||||
import { getCollection } from 'astro:content';
|
||||
import { createMarkdownEndpoint } from '@jdevalk/astro-seo-graph';
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const posts = await getCollection('blog');
|
||||
return posts.map((p) => ({ params: { slug: p.id } }));
|
||||
};
|
||||
|
||||
export const GET = createMarkdownEndpoint({
|
||||
entries: () => getCollection('blog'),
|
||||
mapper: (post, slug) =>
|
||||
post.id !== slug ? null : {
|
||||
frontmatter: { title: post.data.title, canonical: `https://example.com/blog/${post.id}/`, pubDate: post.data.publishDate },
|
||||
body: post.body ?? '',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Enable the discovery link:
|
||||
|
||||
```typescript
|
||||
// astro.config.mjs
|
||||
seoGraph({ markdownAlternate: true });
|
||||
```
|
||||
|
||||
Emits `<link rel="alternate" type="text/markdown" href="…">` on every page.
|
||||
|
||||
### Content Negotiation via Cloudflare (no SSR)
|
||||
|
||||
Transform Rule in the dashboard (works on free plan):
|
||||
|
||||
```
|
||||
When: http.request.headers["accept"][0] contains "text/markdown"
|
||||
AND ends_with(http.request.uri.path, "/")
|
||||
AND NOT starts_with(http.request.uri.path, "/_")
|
||||
|
||||
Rewrite URI path (dynamic): wildcard_replace(http.request.uri.path, "*/", "${1}.md")
|
||||
```
|
||||
|
||||
Turns `/blog/post/` → `/blog/post.md` before cache lookup. No need for `Vary: Accept` header — Cloudflare strips custom Vary values.
|
||||
|
||||
### llms.txt
|
||||
|
||||
```typescript
|
||||
seoGraph({
|
||||
llmsTxt: {
|
||||
title: 'My Site',
|
||||
siteUrl: 'https://example.com',
|
||||
summary: 'A blog about web development, Astro, and SEO.',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Generates `/llms.txt` automatically at build time listing all pages.
|
||||
|
||||
### NLWeb Discovery
|
||||
|
||||
`<link>` tag for conversational endpoint (Microsoft protocol):
|
||||
|
||||
```html
|
||||
<link rel="nlweb" href="https://example.com/api/nlweb" />
|
||||
```
|
||||
|
||||
NLWeb allows AI agents to make conversational queries against site content via schema.org structured data. Still early days but the setup is trivial.
|
||||
|
||||
---
|
||||
|
||||
## 11. Performance SEO
|
||||
|
||||
### No-Vary-Search
|
||||
|
||||
UTM params break caching: `?utm_source=linkedin` and `?utm_source=email` are different resources to the browser. Header that fixes it:
|
||||
|
||||
```
|
||||
No-Vary-Search: key-order, params=("utm_source" "utm_medium" "utm_campaign" "utm_content" "utm_term")
|
||||
```
|
||||
|
||||
**Status:** IETF draft (`draft-ietf-httpbis-no-vary-search`), supported in Chrome, degrades gracefully elsewhere.
|
||||
|
||||
Configure in `_headers` (Cloudflare Pages / Netlify):
|
||||
|
||||
```
|
||||
/*
|
||||
No-Vary-Search: key-order, params=("utm_source" "utm_medium" "utm_campaign" "utm_content" "utm_term")
|
||||
```
|
||||
|
||||
### CDN Cache Headers
|
||||
|
||||
```
|
||||
# _headers (Cloudflare Pages)
|
||||
/_astro/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/og/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
```
|
||||
|
||||
Hashed assets under `/_astro/` never need revalidation — the filename changes when content changes.
|
||||
|
||||
### View Transitions Prefetch
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/layouts/Base.astro
|
||||
import { ClientRouter } from 'astro:transitions';
|
||||
---
|
||||
<head>
|
||||
<ClientRouter defaultStrategy="viewport" />
|
||||
</head>
|
||||
```
|
||||
|
||||
`defaultStrategy: 'viewport'` prefetches links as they scroll into view, making navigation feel instant while keeping initial load minimal.
|
||||
|
||||
### Font Preloading
|
||||
|
||||
```html
|
||||
<link rel="preload" href="/fonts/Inter.woff2" as="font" type="font/woff2" crossorigin />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Redirects
|
||||
|
||||
### Per platform
|
||||
|
||||
| Platform | File | Format |
|
||||
|---|---|---|
|
||||
| Cloudflare Pages | `public/_redirects` | `/old /new 301` |
|
||||
| Netlify | `public/_redirects` or `netlify.toml` | Same format |
|
||||
| Vercel | `vercel.json` | `{ "source": "/old", "destination": "/new", "permanent": true }` |
|
||||
|
||||
### FuzzyRedirect on 404
|
||||
|
||||
Safety net for URLs that slip through redirect tables:
|
||||
|
||||
```astro
|
||||
---
|
||||
// src/pages/404.astro
|
||||
import FuzzyRedirect from '@jdevalk/astro-seo-graph/FuzzyRedirect.astro';
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head><title>Page not found</title></head>
|
||||
<body>
|
||||
<h1>Page not found</h1>
|
||||
<p>The page you're looking for doesn't exist.</p>
|
||||
<FuzzyRedirect />
|
||||
<p><a href="/">Go to the homepage</a></p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Behavior:
|
||||
- Fetches `/sitemap-index.xml`, computes Levenshtein similarity
|
||||
- **0.6–0.85 similarity**: shows "Did you mean /correct-path/?"
|
||||
- **>0.85**: auto-redirects with `window.location.replace`
|
||||
- **<0.6**: does nothing
|
||||
|
||||
---
|
||||
|
||||
## 13. RSS with Full Content
|
||||
|
||||
```typescript
|
||||
// src/pages/rss.xml.ts
|
||||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function GET(context) {
|
||||
const posts = await getCollection('blog');
|
||||
return rss({
|
||||
title: 'My Blog',
|
||||
description: 'Latest posts',
|
||||
site: context.site,
|
||||
items: posts.map((post) => ({
|
||||
title: post.data.title,
|
||||
pubDate: post.data.publishDate,
|
||||
description: post.data.description,
|
||||
link: `/blog/${post.id}/`,
|
||||
content: post.body, // Full content, not excerpts
|
||||
})),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Include full content in the feed** — truncated feeds frustrate readers and give AI systems less to work with.
|
||||
|
||||
---
|
||||
|
||||
## 14. Dynamic robots.txt
|
||||
|
||||
```typescript
|
||||
// src/pages/robots.txt.ts
|
||||
export function GET() {
|
||||
return new Response(
|
||||
`User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://example.com/sitemap-index.xml
|
||||
Schemamap: https://example.com/schemamap.xml
|
||||
`,
|
||||
{ headers: { 'Content-Type': 'text/plain' } },
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The `Schemamap:` directive points agents to the schema map — similar to `Sitemap:` but for structured data.
|
||||
|
||||
---
|
||||
|
||||
## 15. Implementation Checklist
|
||||
|
||||
- [ ] `@jdevalk/astro-seo-graph` installed and `<Seo>` in all layouts
|
||||
- [ ] JSON-LD `@graph` with full entities (WebSite, Person, WebPage, Article, BreadcrumbList)
|
||||
- [ ] Trust signals: `publishingPrinciples`, `knowsAbout`, `SearchAction`
|
||||
- [ ] `seoSchema` in content collection with title/desc validation
|
||||
- [ ] `seoGraph()` integration with all validations enabled
|
||||
- [ ] Per-collection sitemaps with `gitLastmod`
|
||||
- [ ] IndexNow configured and key route deployed
|
||||
- [ ] Auto-generated OG images (1200×675 JPEG)
|
||||
- [ ] Schema endpoints + `/schemamap.xml`
|
||||
- [ ] Markdown alternates with `<link rel="alternate" type="text/markdown">`
|
||||
- [ ] `llms.txt` generated automatically
|
||||
- [ ] `<link rel="nlweb">` (when endpoint available)
|
||||
- [ ] `No-Vary-Search` header for UTM params
|
||||
- [ ] CDN cache: immutable for `/_astro/*`
|
||||
- [ ] View Transitions with viewport prefetch
|
||||
- [ ] FuzzyRedirect on 404
|
||||
- [ ] RSS with full content
|
||||
- [ ] `robots.txt` with Sitemap + Schemamap
|
||||
- [ ] Lychee in CI for broken external links
|
||||
- [ ] `/.well-known/api-catalog` (RFC 9727)
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [astro-seo-graph README](https://github.com/jdevalk/seo-graph/tree/main/packages/astro-seo-graph)
|
||||
- [astro-seo-graph AGENTS.md](https://github.com/jdevalk/seo-graph/blob/main/AGENTS.md) — 3000+ lines with recipes for 14 site types
|
||||
- [seo-graph-core](https://github.com/jdevalk/seo-graph/tree/main/packages/seo-graph-core)
|
||||
- [IndexNow documentation](https://www.indexnow.org/documentation)
|
||||
- [NLWeb protocol](https://github.com/nlweb-ai/NLWeb)
|
||||
- [satori](https://github.com/vercel/satori) — JSX → SVG
|
||||
- [sharp](https://sharp.pixelplumbing.com/) — SVG → JPEG/PNG
|
||||
- [No-Vary-Search (MDN)](https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/No-Vary-Search)
|
||||
- [RFC 9727 — API Catalog](https://www.rfc-editor.org/rfc/rfc9727)
|
||||
- [llms.txt standard](https://llmstxt.org)
|
||||
- [Joost: Astro SEO definitive guide](https://joost.blog/astro-seo-complete-guide/)
|
||||
- [Joost: Agent-ready static blog](https://joost.blog/agent-ready/)
|
||||
604
.github/skills/astro-sites-manager/references/starlight-and-patterns.md
vendored
Normal file
604
.github/skills/astro-sites-manager/references/starlight-and-patterns.md
vendored
Normal 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.
|
||||
387
.github/skills/astro-sites-manager/references/testing.md
vendored
Normal file
387
.github/skills/astro-sites-manager/references/testing.md
vendored
Normal 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` |
|
||||
271
.github/skills/astro-sites-manager/references/v6-features.md
vendored
Normal file
271
.github/skills/astro-sites-manager/references/v6-features.md
vendored
Normal 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.
|
||||
491
.github/skills/astro-sites-manager/references/v7-features.md
vendored
Normal file
491
.github/skills/astro-sites-manager/references/v7-features.md
vendored
Normal 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.
|
||||
210
.github/skills/astro-sites-manager/references/validation-checklist.md
vendored
Normal file
210
.github/skills/astro-sites-manager/references/validation-checklist.md
vendored
Normal 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 ==="
|
||||
```
|
||||
Reference in New Issue
Block a user