Astro standing desk review site with comparison tables
Static Astro site with Markdown desk reviews, specs, stability scores, price history charts, comparison tables, and affiliate links.
astrotypescriptmarkdownzodvitest
Built 1 time, downloaded 0 times.
Install from your terminal
Run this in your project folder. It signs you in, shows what changes and asks before it overwrites a file.
npx claude-lab add astro-standing-desk-review-site-with-comparison-tables-2d93a5CLAUDE.md# Standing desk review site
This is a niche review site for standing desks. A solo author writes desk reviews in Markdown. The site builds to static HTML with fast page loads and earns affiliate revenue. Shoppers use it to compare desks on specs, stability, and price history.
## Commands
Placeholders. Confirm them against package.json after the Phase 1 scaffold.
- `npm run dev`: local dev server
- `npm run build`: static build to dist/
- `npm run preview`: serve dist/ locally
- `npx astro check`: types and content schema
- `npm run lint`: lint (if defined)
- `npm test`: Vitest for helpers and the price script
## Stack
- Astro, static output, zero client JS by default
- Content collections, schema in `src/content.config.ts` (Zod)
- TypeScript strict
## Folder map
- `src/content/desks/*.md`: one review per desk
- `src/data/price-history/<slug>.json`: price entries per desk
- `src/components/`: UI pieces, including AffiliateLink
- `src/pages/`: routes
- `src/layouts/`: page shells
- `public/`: static assets
## Desk frontmatter
Fields: `title`, `brand`, `slug`, `price`, `affiliateUrl`, `specs` (heightMin, heightMax, weightCapacity, motorType, memory presets), `stability` (score 0-10, method, testedOn), `pros`, `cons`, `verdict`, `updatedAt`, `draft`.
## Rules
- Specs and stability scores come only from frontmatter or data files. Never invent or estimate numbers. If a value is missing, ask the user.
- Every affiliate link goes through the AffiliateLink component with `rel="sponsored nofollow noopener"`. Show the disclosure on every page that contains an affiliate link.
- Use `YOUR_AFFILIATE_TAG` placeholders. Never commit real affiliate IDs.
- Performance: no client framework islands unless the user approves. Images go through astro:assets with width and height. The price chart is a build-time inline SVG, not a JS library. Keep pages under the size budget in SPEC.md.
- Never edit the body prose of review Markdown files unless asked. The user writes the reviews. Claude may fix frontmatter, build components, and suggest prose edits in chat.
## Workflow
Explore, plan, code, then run the verify skill before saying a task is done. Give Claude a way to check its work.
## Gotchas
- Collection schema errors fail the build.
- Desks with `draft: true` must be excluded from lists, the sitemap and comparison pages.
- Slugs are permanent URLs. Don't rename a slug without adding a redirect.
## Review checklist
@REVIEW.md
## Token habits
- Run `/clear` between unrelated tasks.
- Use the explorer subagent for wide searches.
.claude/agents/explorer.md---
name: explorer
description: Read-only search agent for wide codebase and content searches. Use to find where a field is used, list desks missing a value, or locate components, returning a short summary instead of raw results.
model: haiku
tools: Read, Grep, Glob
maxTurns: 15
---
You are a read-only explorer for an Astro standing desk review site.
Rules:
- Never edit, create or delete files. You have no write tools; do not suggest running commands that change files.
- Return file paths with line numbers where they matter, then a concise answer.
- Keep your final reply under 200 words. Summarise; do not paste long file contents.
- If a search finds nothing, say so and name the paths and patterns you checked.
Typical uses:
- Find where a spec field such as `motorType` or `weightCapacity` is used across `src/`.
- List desks in `src/content/desks/` that are missing a stability score, or that still contain TODO markers.
- Find components or layouts that load external scripts, iframes or third-party fonts.
Search the project folders (`src/`, `public/`, `.claude/`) first and avoid `node_modules/` and `dist/` unless asked.
.claude/settings.json{
"permissions": {
"allow": [
"Bash(npm run dev:*)",
"Bash(npm run build:*)",
"Bash(npm run lint:*)",
"Bash(npm test:*)",
"Bash(npx astro check:*)",
"Bash(npm run preview:*)",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)"
],
"ask": [
"Bash(npm install:*)",
"Bash(git commit:*)",
"Bash(git push:*)"
],
"deny": [
"Bash(rm -rf:*)",
"Bash(git push --force:*)",
"Read(./.env)",
"Read(./.env.*)"
],
"blockWritesOutsideWorkingDirectories": true
},
"env": {
"CLAUDE_CODE_SUBAGENT_MODEL": "haiku"
},
"subagentPromptCacheTtl": "1h",
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{const p=JSON.parse(d).tool_input.file_path;require('child_process').execFileSync('npx',['--no-install','prettier','--write',p],{stdio:'ignore'})}catch(e){}})\" || true"
}
]
}
]
}
}
.claude/skills/add-comparison/SKILL.md---
name: add-comparison
description: Use when the user wants a head-to-head or multi-desk comparison page. Adds a curated set of 2 to 4 desks to the comparison config and checks the generated page.
disable-model-invocation: true
argument-hint: [slug-a slug-b ...]
allowed-tools: Read Write Edit Bash(npx:*) Bash(npm:*)
---
# Add a comparison
Input: `$ARGUMENTS` lists 2 to 4 desk slugs. If the count is outside that range, stop and ask.
## Steps
1. For each slug, confirm `src/content/desks/<slug>.md` exists.
2. For each slug, confirm its frontmatter has `draft: false`. If any desk is still a draft, stop and list which ones.
3. Find the comparison config file that the page generator reads. Check `PLAN.md` Phase 3 and `src/data/` for it, usually `src/data/comparisons.json`. If it is not found, stop and ask the user where it is.
4. Check that the set is not already in the config. Compare the sorted slugs.
5. Append the set to the config. Use the URL form `/compare/<slug-a>-vs-<slug-b>/`, with slugs in the order given. Keep the file's existing format.
6. Run `npm run build` (or `npx astro build` if no `build` script exists). Confirm the generated page exists at `dist/compare/<slug-a>-vs-<slug-b>/index.html`.
7. Open that HTML file and check:
- A table row exists for every spec field the desks share.
- Each desk has a column.
- The best-value highlight appears on the expected row or rows.
8. If a check fails, report which row or column is missing and where the data comes from. Do not edit desk data to make the check pass.
## Rules
- Do not change desk files.
- Do not invent missing spec values. A missing value in the table is a data problem for the user to fix.
## Finish
Run the `verify` skill. Report the config change, the generated page path, the table check result and the verify verdict.
.claude/skills/add-desk/SKILL.md---
name: add-desk
description: Use when the user wants to add a new standing desk to the site. Scaffolds the review file and price history file without writing review prose or guessing values.
disable-model-invocation: true
argument-hint: [brand model]
allowed-tools: Read Write Edit Bash(npx:*) Bash(npm:*)
---
# Add a desk
Input: `$ARGUMENTS` gives the brand and model. Derive the slug as lowercase words joined by hyphens, for example `example-desk-one`. Slugs are permanent URLs, so confirm the slug with the user before creating files.
## Steps
1. Check that `src/content/desks/<slug>.md` does not already exist. If it does, stop and ask.
2. Read `src/content.config.ts` to get the current `desks` schema. Match its field names and types exactly.
3. Create `src/content/desks/<slug>.md` with complete frontmatter:
- `title`, `brand`, `slug`, `price`, `affiliateUrl`, `updatedAt`, `pros`, `cons`, `verdict`, `specs`, `stability`
- Set `draft: true`.
- Use `affiliateUrl` with the `YOUR_AFFILIATE_TAG` placeholder, never a real ID.
- Mark unknown values with a YAML comment: `# TODO: from manufacturer spec sheet`.
- If the schema rejects a placeholder for a required field, do not loosen the schema. Tell the user which field blocks the draft and ask how to proceed.
4. Under the frontmatter, add a heading skeleton only, for example `## Overview`, `## Specs`, `## Stability`, `## Verdict`. Leave each section empty or with a `TODO` line. The user writes all prose.
5. Create `src/data/price-history/<slug>.json`:
- If the user gave a first price, write `[{"date": "<today ISO date>", "price": <number>}]`.
- Otherwise write `[]`.
6. List every missing field for the user to fill, grouped by section: specs, stability (score, method, testedOn), price, affiliate URL.
## Rules
- Never guess specs, stability scores or prices. Never estimate them.
- Keep `draft: true` until the user changes it.
- Do not edit other desk files.
- Do not write review prose.
## Finish
Run the `verify` skill. Report the files created, the missing fields and the verify verdict.
.claude/skills/verify/SKILL.md---
name: verify
description: Use after any change and before claiming done. Runs the real checks for this Astro site and reports pass or fail per step.
allowed-tools: Bash(npm:*) Bash(npx:*) Read
---
# Verify the Astro desk review site
Run these steps in order. Stop at the first failing step, report its first error, and give the verdict.
Before starting, read `package.json` and note which scripts exist: `lint`, `test`, `build`, `preview`. Do not invent a command that is not defined there.
## Step 1: Types and content schema
Run `npx astro check`.
- Pass: exit code 0 with no errors.
- Fail: report the first error with its file and line.
## Step 2: Lint
If `lint` exists in `package.json`, run `npm run lint`.
If it does not exist, report "lint: not defined, skipped" and continue.
## Step 3: Tests
If `test` exists in `package.json`, run `npm test`. This covers helpers and the price script (`scripts/add-price.mjs`).
If it does not exist, report "test: not defined, skipped" and continue.
## Step 4: Build and expected output
Run `npm run build`.
After a successful build, confirm these exist in `dist/`:
- `index.html`
- At least one desk page under `dist/desks/<slug>/index.html`
- At least one compare page under `dist/compare/`
- `sitemap-index.xml`
Fail if any are missing. Drafts (`draft: true`) must not have pages, sitemap entries or list entries.
## Step 5: Smoke test the preview server
Run `npm run preview` in the background and read the port from its output. If `preview` is not defined, report "smoke: preview not defined, skipped".
Then request each of these with curl and check for HTTP 200 and the expected `<h1>`:
- The home page `/`
- One desk page from `dist/desks/`
- One compare page from `dist/compare/`
Stop the preview server before reporting.
## Step 6: Static checks on dist/
- Count `<script` tags in each HTML file under `dist/`. Report any script that is not expected. Zero is the target for review and compare pages.
- Find every `<a` with an `href` pointing to an external domain. Confirm each one has a `rel` attribute containing `sponsored`. Report any that do not.
## Report format
Give one line per step:
`Step N <name>: PASS | FAIL | SKIPPED`
For a failure, show only the first error for that step.
End with one line:
`Verdict: PASS` or `Verdict: FAIL (step N)`
INSTALL.md# Install and setup
This guide gets the standing desk review site running on your machine with Claude Code.
## Prerequisites
- Node 20 or newer
- git
- Claude Code 2.1.296 or newer (run `claude --version` to check)
## Create the project
1. Make a project folder and open it in a terminal.
2. Run `git init`.
3. Copy `CLAUDE.md`, `SPEC.md`, `PLAN.md`, `REVIEW.md`, `INSTALL.md` and the `.claude` folder into it.
4. If Phase 1 has not scaffolded Astro yet, run `npm create astro@latest` and choose the minimal template with strict TypeScript.
## Add the plugins
1. Add the extra marketplace:
`claude plugin marketplace add anthropics/skills`
2. Install the plugins:
- `claude plugin install code-review@claude-plugins-official`
- `claude plugin install commit-commands@claude-plugins-official`
- `claude plugin install frontend-design@anthropic-agent-skills`
- `claude plugin install webapp-testing@anthropic-agent-skills`
## Fill in your details
1. Open `SPEC.md` and fill in your desks, affiliate programs and hosting choice.
2. Keep your affiliate tag out of files. Use the placeholder `YOUR_AFFILIATE_TAG` and set the real value in an environment variable or a local config file that is not committed.
## Start Claude Code
1. Run `claude` in the project folder.
2. Run `/memory` and check that it lists `CLAUDE.md` and `REVIEW.md`.
3. Run `/init` only if `CLAUDE.md` is missing.
## Run the workflow
1. Open `PLAN.md` and copy the Phase 1 `/goal` prompt.
2. Paste it into Claude Code and let it run.
3. Run `/clear` before each new phase.
4. Commit after each phase passes.
## Add a desk
1. Run `/add-desk brand model`.
2. Claude creates the desk file and an empty price history file. It does not guess values.
3. Fill in the TODO markers in the frontmatter.
4. Write the review prose yourself in the Markdown body.
5. Set `draft: false` when the desk is ready.
## Cost tips
- Run `/usage` to see where tokens went.
- Use `/effort low` for small edits.
- Ask for the explorer subagent for wide searches.
- Run `/clear` between unrelated tasks.
## Troubleshooting
- **Schema errors in the build:** the error names the file and field. Fix the frontmatter to match `src/content.config.ts`.
- **Drafts not showing:** check that `draft` is `false`. Drafts are hidden on purpose.
- **Commands missing from package.json:** the verify skill skips undefined commands. Add the script or update CLAUDE.md.
PLAN.md# Build plan: standing desk review site
Run one phase at a time. Each phase ends with a `/goal` prompt.
Run `/clear` between phases and commit after each phase passes.
Start each phase in plan mode and approve the plan before coding.
## Phase 1: Scaffold
Scope: Astro project, TypeScript strict, `desks` collection schema, three placeholder desks, base layout, home page, desk page.
Files: `package.json`, `astro.config.mjs`, `tsconfig.json`, `src/content.config.ts`, `src/content/desks/*.md`, `src/layouts/`, `src/pages/index.astro`, `src/pages/desks/[slug].astro`.
First step: list the files you will create and wait for approval.
```
/goal Phase 1: scaffold the Astro standing desk review site. Read CLAUDE.md, SPEC.md and PLAN.md first. End state: (1) Astro project with TypeScript strict and static output in this folder; (2) src/content.config.ts defines a desks collection with a Zod schema matching SPEC.md; (3) three sample desks in src/content/desks/ labelled PLACEHOLDER, affiliate URLs using YOUR_AFFILIATE_TAG; (4) base layout, home page listing non-draft desks, dynamic page at /desks/[slug]/ rendering the Markdown body and a specs table; (5) package.json scripts dev, build, preview, check. Check: run the verify skill; npx astro check and npm run build exit 0; dist/ contains index.html and three desk pages. Show the verify output in the conversation. Constraints: no client framework or UI library; no new dependencies beyond Astro defaults without asking; write no review prose beyond placeholders; never invent specs or scores; no secrets or real affiliate IDs; touch only this folder. Turn bound: stop after 30 turns or when the check passes, then report files created, verify summary and anything left undone.
```
## Phase 2: Review pages
Scope: review layout, specs table, stability display, pros and cons, `AffiliateLink` with disclosure, images through `astro:assets`.
Files: `src/components/AffiliateLink.astro`, `src/components/SpecsTable.astro`, `src/components/StabilityScore.astro`, `src/pages/desks/[slug].astro`, `src/layouts/`.
First step: propose the component list and wait for approval.
```
/goal Phase 2: build the desk review pages. Read CLAUDE.md, SPEC.md, REVIEW.md and PLAN.md first. End state: (1) AffiliateLink component renders rel="sponsored nofollow noopener" and a visible disclosure line; every affiliate link on the site uses it; (2) desk pages show specs table, stability score with method and test date, pros and cons; (3) images use astro:assets with width and height; (4) a test, using Vitest only if I approve adding it, asserts every affiliate href in dist has rel containing sponsored and nofollow. Check: run the verify skill; build passes; show verify output in the conversation. Constraints: ask before adding any dependency; do not edit review prose in src/content; no client JS framework; no real affiliate IDs. Turn bound: stop after 40 turns and report what is left.
```
## Phase 3: Comparison tables
Scope: comparison page generator from curated pairs, accessible responsive table, best-value highlight, brand and best-of listing pages.
Files: `src/data/comparisons.json`, `src/components/CompareTable.astro`, `src/pages/compare/[pair].astro`, `src/pages/brands/[brand].astro`, `src/pages/best/[list].astro`.
First step: show the table markup plan and the pairs file format, then wait for approval.
```
/goal Phase 3: build comparison tables and listing pages. Read SPEC.md and PLAN.md first. End state: (1) src/data/comparisons.json lists curated pairs of 2 to 4 non-draft desks; (2) a static page is generated for every configured pair at /compare/<a>-vs-<b>/; (3) the table has a caption, th scope on headers, a text label for best value in each row, and scrolls horizontally inside a labelled container on mobile; (4) brand and best-of listing pages show only non-draft desks; (5) tests, if Vitest is approved, assert each compare page's row count equals the number of spec fields. Check: run the verify skill; build produces a compare page for every pair; show verify output in the conversation. Constraints: ask before adding dependencies; no JavaScript needed for the table; do not edit review prose; no real affiliate IDs. Turn bound: stop after 40 turns and report what is left.
```
## Phase 4: Price history
Scope: price JSON format, build-time inline SVG sparkline, min, max and current stats, append script.
Files: `src/data/price-history/<slug>.json`, `src/components/PriceChart.astro`, `src/lib/prices.ts`, `scripts/add-price.mjs`, `tests/add-price.test.ts`.
First step: show the JSON schema and script arguments, then wait for approval.
```
/goal Phase 4: add price history. Read SPEC.md and PLAN.md first. End state: (1) each desk with a price-history JSON file renders min, max and current price and an inline SVG sparkline built at build time with no JS library; (2) scripts/add-price.mjs takes a slug, a date and a price, validates them, checks the slug exists, and appends one entry sorted by date; it refuses to write invalid input; (3) tests cover valid append, bad date, bad price and unknown slug, and pass. Check: run the verify skill; build passes; show verify output in the conversation. Constraints: ask before adding dependencies; no chart library; no live scraping; never invent prices; do not edit review prose. Turn bound: stop after 35 turns and report what is left.
```
## Phase 5: SEO and speed
Scope: sitemap, JSON-LD, meta tags, OG images, draft exclusion, JS budget script, Lighthouse run against preview.
Files: `astro.config.mjs`, `src/components/SEO.astro`, `scripts/js-budget.mjs`, `scripts/lighthouse.sh` or equivalent config.
First step: list which tools need approval (sitemap integration, Lighthouse) and wait.
```
/goal Phase 5: SEO and speed. Read SPEC.md, REVIEW.md and PLAN.md first. End state: (1) sitemap-index.xml lists every non-draft page and no draft URLs; (2) every page has a unique title, meta description and canonical URL; (3) desk pages include valid Product and Review JSON-LD; (4) OG image per desk; (5) scripts/js-budget.mjs scans dist and reports script count and bytes, failing above a stated budget; (6) Lighthouse run against npm run preview reports mobile performance at or above 100 on review pages, or lists what fails. Check: run the verify skill, then the budget script; show outputs in the conversation. Constraints: ask before adding dependencies such as a sitemap integration or Lighthouse; no client JS framework; do not edit review prose. Turn bound: stop after 40 turns and report what is left, including any metric below target.
```
## Phase 6: Deploy
Scope: host choice, build settings, redirects file, 404 page, launch checklist.
Files: `public/_redirects` or `vercel.json` (per host), `src/pages/404.astro`, `docs/launch-checklist.md`.
First step: ask me to choose the host from the open questions in SPEC.md, then wait.
```
/goal Phase 6: prepare deployment. Read SPEC.md, INSTALL.md and PLAN.md first. End state: (1) host chosen by me and recorded in SPEC.md; (2) build settings documented in INSTALL.md for that host; (3) redirects file exists with a placeholder entry for any slug change; (4) src/pages/404.astro exists and is in the build; (5) docs/launch-checklist.md lists sitemap submitted, redirects tested, affiliate disclosure present, no placeholder text left, drafts excluded, budget script passing, and each item marked verified or open with evidence. Check: run the verify skill and npm run build; show output in the conversation. Constraints: never deploy, push or submit anything without my approval; no real affiliate IDs; no secrets committed. Turn bound: stop after 30 turns and report what is left.
```
## Notes
- Keep the phase order. Phase 5 depends on comparison and review pages existing.
- Commit with the commit-commands plugin after each passing phase.
- Use `/usage` after each phase to check token use.
REVIEW.md# Review checklist
Used by `/code-review`. Imported from CLAUDE.md with `@REVIEW.md`.
Flag any item that fails. Quote the file and line.
## Content integrity
- No invented specs, stability scores or prices.
- Sample data is labelled PLACEHOLDER.
- Missing values are TODO markers, not guesses.
## Affiliate compliance
- Every outbound affiliate link uses the AffiliateLink component.
- The link has `rel="sponsored nofollow noopener"`.
- The disclosure is visible on every page that contains an affiliate link.
## Performance
- No new client-side JavaScript or UI framework.
- Images go through astro:assets and have width and height.
- No render-blocking third-party scripts.
## Accessibility
- Tables have a caption and `th` with `scope`.
- Colour is not the only signal, for example best-value highlights also have text.
- Images have alt text.
- Text contrast meets WCAG AA.
## SEO
- Each page has a unique title and meta description.
- Each page has a canonical URL.
- JSON-LD is valid.
- Draft desks are excluded from the sitemap.
## Schema safety
- Frontmatter changes match the Zod schema in `src/content.config.ts`.
- A slug change includes a redirect.
## Security
- No secrets committed.
- No real affiliate IDs. Use `YOUR_AFFILIATE_TAG`.
## Scope
- The diff touches only what the task asked for.
- Review prose in `src/content/` is unchanged unless requested.
## Verification
- Output of the verify skill is shown.
- All steps pass.
SPEC.md# Standing desk review site: product spec
## Goal and audience
Shoppers comparing standing desks who want fast, trustworthy numbers.
The site is a static Astro build. The author writes reviews in Markdown.
Revenue comes from affiliate links.
## Pages
- Home: latest non-draft desks, links to best-of lists and comparisons.
- Desk review: `/desks/<slug>/` with specs, stability score, pros and cons, verdict, price history and affiliate link.
- Comparison: `/compare/<slug-a>-vs-<slug-b>/` for 2 to 4 desks.
- Brand listing: `/brands/<brand>/`.
- Best-of lists: `/best/<list>/`.
- About and how we test: `/about/`, `/how-we-test/`.
- Affiliate disclosure: `/disclosure/`.
## Desk data model
File: `src/content/desks/<slug>.md`. Example frontmatter (placeholder values only):
```yaml
---
title: "PLACEHOLDER Desk One"
brand: "PLACEHOLDER Brand"
slug: "placeholder-desk-one"
price: 499
affiliateUrl: "https://example.com/?tag=YOUR_AFFILIATE_TAG"
specs:
heightMin: 62 # cm (confirm units)
heightMax: 127 # cm
weightCapacity: 120 # kg
motorType: "dual"
memoryPresets: 4
stability:
score: 7.5
method: "PLACEHOLDER lateral wobble test"
testedOn: 2026-01-01
pros: ["PLACEHOLDER pro"]
cons: ["PLACEHOLDER con"]
verdict: "PLACEHOLDER verdict"
updatedAt: 2026-01-01
draft: true
---
```
Zod types in `src/content.config.ts`:
- `title`, `brand`, `verdict`: `z.string()`
- `slug`: `z.string().regex(/^[a-z0-9-]+$/)`
- `price`: `z.number().positive()`
- `affiliateUrl`: `z.string().url()`
- `specs.heightMin`, `specs.heightMax`, `specs.weightCapacity`: positive numbers
- `specs.motorType`: `z.enum(["single", "dual", "triple"])`
- `specs.memoryPresets`: integer 0 to 10
- `stability.score`: number 0 to 10
- `stability.method`: string; `stability.testedOn`: date
- `pros`, `cons`: string arrays
- `updatedAt`: date; `draft`: boolean, default false
Values missing from a review are left as TODO. Claude asks instead of guessing.
## Comparison tables
- Default row order is the order in the curated pairs file.
- Each spec row highlights the best value with a text label, not color alone.
- Columns: price, height range, weight capacity, memory presets, stability score.
- Works without JavaScript.
- Horizontally scrollable on mobile inside a labelled container.
- Table has a caption and `th scope` on headers.
## Comparison URLs
Static pages are generated for curated pairs listed in `src/data/comparisons.json`.
No runtime query strings. Adding a pair means adding one entry and rebuilding.
## Price history
- One file per desk: `src/data/price-history/<slug>.json`.
- Entries: `[{ "date": "YYYY-MM-DD", "price": 499 }]`.
- Updated by hand or with `scripts/add-price.mjs` (added in Phase 4).
- Page shows min, max and current price, plus an inline SVG sparkline built at build time.
## Affiliate links
- All outbound affiliate links use the `AffiliateLink` component.
- The component sets `rel="sponsored nofollow noopener"`.
- Disclosure text appears on every page that contains an affiliate link.
- Tracking placeholders: `YOUR_AFFILIATE_TAG`. Real IDs live outside the repo.
## Performance targets
- Lighthouse performance 100 on mobile for review pages.
- LCP under 1.0 s on review pages.
- Total JavaScript near zero. No client framework islands unless approved.
- Images through `astro:assets` with width and height set.
- Each review page has a stated size budget, checked by script.
## SEO
- Unique titles and meta descriptions per page.
- Canonical URL on every page.
- Sitemap at `sitemap-index.xml`. Drafts excluded.
- Product and Review JSON-LD on desk pages.
- OG image per desk.
## Non-goals (v1)
- User accounts, comments, forms that store data.
- Runtime database.
- Live price scraping.
## Open questions for the author
1. Hosting target: Cloudflare Pages, Netlify or Vercel?
2. Domain name and brand name?
3. Which affiliate programs, and what tracking parameter format?
4. Test methodology wording for the "how we test" page.
5. Height units: cm or inches? Weight: kg or lb?
6. Which best-of lists to launch with?
## Acceptance criteria
- Home: lists only non-draft desks; build fails on schema errors.
- Desk page: specs table, stability score with method and date, pros and cons, price stats, AffiliateLink with disclosure, JSON-LD valid.
- Compare page: row count equals number of spec fields; best-value label present per row; no JS in output.
- Listing pages: include only non-draft desks; link to each desk once.
- Sitemap: contains every non-draft page; contains no draft URLs.
- Build output: no unexpected `<script>` tags; every affiliate href has rel containing `sponsored`.