skilldrop-cli 0.16.35 → 0.16.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/agents/README.md +4 -1
- package/agents/docs-reviewer.md +47 -0
- package/agents/finding-adjudicator.md +49 -0
- package/agents/test-reviewer.md +52 -0
- package/catalogue.json +17 -7
- package/model-routing.json +40 -0
- package/package.json +1 -1
- package/packs/ai-engineering/skills/agent-config-review/SKILL.md +116 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/eval_queries.json +12 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/evals.json +75 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/agents/db-migrator.md +8 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/agents/reviewer.md +9 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/settings.json +48 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/settings.local.json +5 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.mcp.json +22 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/package.json +12 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/2/.cursor/mcp.json +17 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/2/config.toml +21 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/my-user-settings.json +11 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.claude/agents/release-notes.md +13 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.claude/settings.json +39 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.mcp.json +18 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/package.json +14 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/5/.github/workflows/codex-triage.yml +29 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/5/AGENTS.md +16 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/5/codex/config.toml +34 -0
- package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/.claude/settings.json +22 -0
- package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/.mcp.json +9 -0
- package/packs/ai-engineering/skills/agent-config-review/examples/python-repo-tighten.md +81 -0
- package/packs/ai-engineering/skills/agent-config-review/manifest.json +60 -0
- package/packs/ai-engineering/skills/agent-config-review/reference.md +205 -0
- package/packs/ai-engineering/skills/agent-config-review/scripts/agent_config_scan.py +989 -0
- package/packs/ai-engineering/skills/agent-incident-response/SKILL.md +175 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/eval_queries.json +14 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/evals.json +107 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/agent-session-excerpt.txt +11 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/ci-build-log.txt +24 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/incident-notes.md +19 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/2/mcp-config.json +20 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/2/session-transcript.txt +8 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/4/agent-session.log +70 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/4/infra/staging-db.tf +25 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/jobs/nightly-triage/job.yaml +19 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/jobs/nightly-triage/run.log +13 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/usage-export-2026-10-20.csv +13 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/.mcp.json +20 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/git-log.txt +12 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/notesync-excerpt.txt +30 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/notesync.log +15 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/ssh-config.txt +16 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/.github/workflows/ci.yml +28 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/SECURITY.md +8 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/issue-479.md +35 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/package.json +15 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/pr-482.diff +20 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/run-log.txt +30 -0
- package/packs/ai-engineering/skills/agent-incident-response/examples/injected-issue-comment.md +57 -0
- package/packs/ai-engineering/skills/agent-incident-response/manifest.json +55 -0
- package/packs/ai-engineering/skills/agent-incident-response/reference.md +152 -0
- package/packs/ai-engineering/skills/agent-incident-response/scripts/find_secret_exposure.py +239 -0
- package/packs/ai-engineering/skills/agent-incident-response/scripts/usage_split.py +194 -0
- package/packs/converters/skills/file-to-markdown/SKILL.md +2 -1
- package/packs/converters/skills/file-to-markdown/evals/eval_queries.json +4 -0
- package/packs/converters/skills/file-to-markdown/manifest.json +2 -2
- package/packs/converters/skills/file-to-markdown/scripts/to_markdown.py +253 -3
- package/packs/converters/skills/md-to-confluence/SKILL.md +139 -0
- package/packs/converters/skills/md-to-confluence/evals/eval_queries.json +42 -0
- package/packs/converters/skills/md-to-confluence/evals/evals.json +89 -0
- package/packs/converters/skills/md-to-confluence/evals/files/1/img/latency-dashboard.png +0 -0
- package/packs/converters/skills/md-to-confluence/evals/files/1/payments-runbook.md +60 -0
- package/packs/converters/skills/md-to-confluence/evals/files/2/deploy-checklist.md +33 -0
- package/packs/converters/skills/md-to-confluence/evals/files/3/img/after/screen.png +0 -0
- package/packs/converters/skills/md-to-confluence/evals/files/3/img/before/screen.png +0 -0
- package/packs/converters/skills/md-to-confluence/evals/files/3/release-notes-4.2.md +49 -0
- package/packs/converters/skills/md-to-confluence/evals/files/4/docs/img/topology.png +0 -0
- package/packs/converters/skills/md-to-confluence/evals/files/4/docs/ingest-pipeline-design.md +65 -0
- package/packs/converters/skills/md-to-confluence/evals/files/5/support/troubleshooting-sso.md +57 -0
- package/packs/converters/skills/md-to-confluence/examples/inputs/img/queue-depth.png +0 -0
- package/packs/converters/skills/md-to-confluence/examples/inputs/queue-runbook.fixed.md +21 -0
- package/packs/converters/skills/md-to-confluence/examples/inputs/queue-runbook.md +21 -0
- package/packs/converters/skills/md-to-confluence/examples/queue-runbook.md +57 -0
- package/packs/converters/skills/md-to-confluence/manifest.json +52 -0
- package/packs/converters/skills/md-to-confluence/reference.md +92 -0
- package/packs/converters/skills/md-to-confluence/scripts/md_to_confluence.py +821 -0
- package/packs/converters/skills/md-to-pptx/SKILL.md +131 -0
- package/packs/converters/skills/md-to-pptx/evals/eval_queries.json +12 -0
- package/packs/converters/skills/md-to-pptx/evals/evals.json +84 -0
- package/packs/converters/skills/md-to-pptx/evals/files/1/img/defect-burndown.png +0 -0
- package/packs/converters/skills/md-to-pptx/evals/files/1/status-report.md +75 -0
- package/packs/converters/skills/md-to-pptx/evals/files/3/brand.json +12 -0
- package/packs/converters/skills/md-to-pptx/evals/files/3/night-shift-induction.md +53 -0
- package/packs/converters/skills/md-to-pptx/evals/files/4/charts/ontime.png +0 -0
- package/packs/converters/skills/md-to-pptx/evals/files/4/qbr-q3.md +85 -0
- package/packs/converters/skills/md-to-pptx/evals/files/5/vendor-sync-notes.md +28 -0
- package/packs/converters/skills/md-to-pptx/examples/support-weekly-input.md +48 -0
- package/packs/converters/skills/md-to-pptx/examples/support-weekly.md +75 -0
- package/packs/converters/skills/md-to-pptx/examples/tickets-by-week.png +0 -0
- package/packs/converters/skills/md-to-pptx/manifest.json +49 -0
- package/packs/converters/skills/md-to-pptx/reference.md +139 -0
- package/packs/converters/skills/md-to-pptx/scripts/md_to_pptx.py +1841 -0
- package/packs/dev-team/loops/modernize/LOOP.md +124 -0
- package/packs/dev-team/loops/modernize/evals/eval_queries.json +9 -0
- package/packs/dev-team/loops/modernize/evals/evals.json +27 -0
- package/packs/dev-team/loops/modernize/loop.json +60 -0
- package/packs/dev-team/skills/behaviour-equivalence/SKILL.md +155 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/eval_queries.json +11 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/evals.json +76 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/cases.jsonl +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/new.jsonl +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/notes.md +15 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/old.jsonl +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/old_rerun.jsonl +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/pricing_v1.py +45 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/pricing_v2.py +65 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/rules.json +10 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/3/diff-summary.txt +16 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/diff.txt +13 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/fee_calc_py2.py +16 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/fee_calc_py3.py +24 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/input.csv +13 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/new_output.csv +13 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/old_output.csv +13 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/TEAM_NOTES.md +19 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/normalisation.yaml +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/new_run.jsonl +8 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/old_run1.jsonl +8 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/old_run2.jsonl +8 -0
- package/packs/dev-team/skills/behaviour-equivalence/examples/delivery-date.md +78 -0
- package/packs/dev-team/skills/behaviour-equivalence/manifest.json +33 -0
- package/packs/dev-team/skills/behaviour-equivalence/reference.md +97 -0
- package/packs/dev-team/skills/behaviour-equivalence/scripts/golden_diff.py +524 -0
- package/packs/dev-team/skills/behaviour-equivalence/templates/accepted.json +10 -0
- package/packs/dev-team/skills/behaviour-equivalence/templates/rules.json +7 -0
- package/packs/frontend/pack.json +55 -0
- package/packs/frontend/skills/rendering-strategy/SKILL.md +149 -0
- package/packs/frontend/skills/rendering-strategy/evals/eval_queries.json +38 -0
- package/packs/frontend/skills/rendering-strategy/evals/evals.json +77 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/1/fernhill-app.md +40 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/3/routes.md +33 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/4/build-notes.txt +5 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/4/nuxt.config.ts +19 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/4/pages/recipes/[slug].vue +37 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/4/routes.md +15 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/routes.txt +18 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/src/hooks.server.ts +14 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/src/routes/(marketing)/pricing/+page.server.ts +10 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/src/routes/+layout.server.ts +10 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/svelte.config.js +10 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/traffic.md +18 -0
- package/packs/frontend/skills/rendering-strategy/examples/saas-site.md +42 -0
- package/packs/frontend/skills/rendering-strategy/manifest.json +50 -0
- package/packs/frontend/skills/rendering-strategy/reference.md +116 -0
- package/packs/frontend/skills/rendering-strategy/scripts/render_load.py +181 -0
- package/packs/frontend/skills/web-performance-review/SKILL.md +106 -0
- package/packs/frontend/skills/web-performance-review/evals/eval_queries.json +38 -0
- package/packs/frontend/skills/web-performance-review/evals/evals.json +76 -0
- package/packs/frontend/skills/web-performance-review/evals/files/1/psi.json +1065 -0
- package/packs/frontend/skills/web-performance-review/evals/files/2/after.json +359 -0
- package/packs/frontend/skills/web-performance-review/evals/files/2/before.json +375 -0
- package/packs/frontend/skills/web-performance-review/evals/files/4/psi-pdp-mobile-2026-10-19.json +171 -0
- package/packs/frontend/skills/web-performance-review/evals/files/4/rum-inp-attribution-oct14-19.csv +6 -0
- package/packs/frontend/skills/web-performance-review/evals/files/4/rum-inp-daily.csv +15 -0
- package/packs/frontend/skills/web-performance-review/evals/files/5/crux-home-phone.json +35 -0
- package/packs/frontend/skills/web-performance-review/evals/files/5/lighthouse-home-mobile.json +173 -0
- package/packs/frontend/skills/web-performance-review/examples/quayside-article-lhr.json +480 -0
- package/packs/frontend/skills/web-performance-review/examples/quayside-article.md +103 -0
- package/packs/frontend/skills/web-performance-review/manifest.json +48 -0
- package/packs/frontend/skills/web-performance-review/reference.md +105 -0
- package/packs/frontend/skills/web-performance-review/scripts/lh_summary.py +743 -0
- package/packs/product-manager/skills/lean-canvas/SKILL.md +162 -0
- package/packs/product-manager/skills/lean-canvas/evals/eval_queries.json +42 -0
- package/packs/product-manager/skills/lean-canvas/evals/evals.json +82 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/1/pawpass-notes.md +54 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/3/lunchloop-pitch.md +30 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/4/clubkit-notes.md +44 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/5/cost-model.csv +5 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/5/proposal.md +40 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/5/stakeholder-emails.txt +18 -0
- package/packs/product-manager/skills/lean-canvas/examples/bike-repair.md +72 -0
- package/packs/product-manager/skills/lean-canvas/manifest.json +34 -0
- package/packs/product-manager/skills/lean-canvas/reference.md +102 -0
- package/packs/product-manager/skills/lean-canvas/scripts/unit_economics.py +211 -0
- package/packs/solution-architect/skills/api-contract-draft/SKILL.md +2 -2
- package/packs/solution-architect/skills/api-contract-draft/manifest.json +28 -6
- package/packs/solution-architect/skills/event-contract-draft/SKILL.md +167 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/eval_queries.json +38 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/evals.json +76 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/files/1/order-events-brief.md +45 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/files/4/NOTES.md +23 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/files/4/schemas/shipment-update.json +40 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/files/5/invoice.example.json +25 -0
- package/packs/solution-architect/skills/event-contract-draft/examples/order-service.asyncapi.json +246 -0
- package/packs/solution-architect/skills/event-contract-draft/examples/order-service.md +92 -0
- package/packs/solution-architect/skills/event-contract-draft/manifest.json +25 -0
- package/packs/solution-architect/skills/event-contract-draft/reference.md +113 -0
- package/packs/solution-architect/skills/event-contract-draft/scripts/check_asyncapi.py +502 -0
- package/packs/solution-architect/skills/event-contract-draft/templates/asyncapi-skeleton.json +144 -0
- package/packs/sre-oncall/skills/slo-spec/SKILL.md +161 -0
- package/packs/sre-oncall/skills/slo-spec/evals/eval_queries.json +38 -0
- package/packs/sre-oncall/skills/slo-spec/evals/evals.json +75 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/1/order-service.md +51 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/3/search-slo-status.md +30 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/4/alerts/payments-api.rules.yaml +16 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/4/docs/dependencies.md +8 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/4/metrics/summary-90d.md +51 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/5/ops-notes.md +34 -0
- package/packs/sre-oncall/skills/slo-spec/examples/ticket-booking.md +88 -0
- package/packs/sre-oncall/skills/slo-spec/manifest.json +34 -0
- package/packs/sre-oncall/skills/slo-spec/reference.md +113 -0
- package/packs/sre-oncall/skills/slo-spec/scripts/slo_math.py +449 -0
- package/packs/sre-oncall/skills/slo-spec/templates/slo-spec.md +35 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rendering-strategy
|
|
3
|
+
description: Decide route by route how each page of a web app is rendered — static at build (SSG), static with revalidation (ISR, stale-while-revalidate), server-rendered per request (SSR, streaming), client-rendered (CSR, SPA), at the edge, or a mix with islands and server components — from how fresh each route's data must be, whether it is personalised, whether it must be indexed or shared, what it costs per request at the stated traffic and how much JavaScript it needs, with the cache keys, invalidation events and failure modes of each choice. Use when the user asks "should this be SSR or static", "SSR vs SSG vs ISR", "how should each page be rendered", "do we need SSR for SEO", "which pages can we cache", or is choosing rendering modes in Next.js, Nuxt, Remix, React Router, SvelteKit or Astro.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# rendering-strategy
|
|
7
|
+
|
|
8
|
+
Picks a rendering mode for every route of a web app and records why: what the route needs, the
|
|
9
|
+
mode that meets it at the lowest cost, what invalidates it, and what the user sees when its data
|
|
10
|
+
source fails. It decides rendering, not the whole system: the system design is `design-doc`,
|
|
11
|
+
latency and availability targets are `nfr-spec`, and recording the chosen strategy as a
|
|
12
|
+
decision record is `adr-generator`.
|
|
13
|
+
|
|
14
|
+
## How to respond
|
|
15
|
+
|
|
16
|
+
**Four rules come before the steps and outrank them:**
|
|
17
|
+
|
|
18
|
+
- **Answer what was asked, first.** Open with the answer, the decision or the artifact, in plain words. Scores, matrices, frameworks and tags come after it, and anything that doesn't change the answer is cut.
|
|
19
|
+
- **Use only what you were given.** Don't add facts, names, numbers, incidents, history, steps or sections the input doesn't contain. What you need and don't have is left out of the artifact and listed once at the end under "To confirm".
|
|
20
|
+
- **Deliver from what you have.** When the request gives you something to work on, state your assumptions in a line and produce the result. When it gives you nothing to work on, ask for it in one or two plain sentences and say what you will do once you have it.
|
|
21
|
+
- **Write for someone who has never heard of this skill.** No skill names, no paths or scripts from this folder, no internal terms, and nothing about how the run was set up. A next step is one plain sentence at the end that describes the work.
|
|
22
|
+
- **For this skill:** Decide per route, never per app. A page that is the same for everyone and can be minutes stale is cached, and the personalised or seconds-fresh part of a page is rendered on its own, never used as the reason to render the whole page per request.
|
|
23
|
+
|
|
24
|
+
1. **List the routes and what is known about each.** One row per route or route group: how
|
|
25
|
+
fresh its data must be, whether it differs per user, whether it must be indexed or shared,
|
|
26
|
+
its traffic, and what on it is interactive. Take these from the input only. Traffic,
|
|
27
|
+
freshness and framework version are never invented: ask at most two questions (the
|
|
28
|
+
freshness of the most volatile data, then traffic), or tag the gap `[assumption]`.
|
|
29
|
+
|
|
30
|
+
2. **Ask the five questions for each route, in this order.** The first answer that settles the
|
|
31
|
+
mode wins; later questions refine it.
|
|
32
|
+
1. **How fresh must the data be?** "Minutes stale is fine" → cacheable. "Within seconds" →
|
|
33
|
+
only that field is fresh, the page still caches (step 3).
|
|
34
|
+
2. **Is it the same for everyone?** Same → static or revalidated. Personalised in one corner →
|
|
35
|
+
cached page plus a personal fragment. Personalised throughout → per request, never cached.
|
|
36
|
+
3. **Must it be indexed or shared with a preview?** Then the content and the preview meta
|
|
37
|
+
tags are in the HTML at first byte: static, revalidated or server-rendered, never
|
|
38
|
+
client-only.
|
|
39
|
+
4. **What does it cost per request at the stated traffic?** Count renders (step 5).
|
|
40
|
+
5. **How much JavaScript does it need?** Read-only content ships none or islands; a dense
|
|
41
|
+
app screen can be client-rendered.
|
|
42
|
+
|
|
43
|
+
✅ *"Blog post: changes on publish, same for everyone, must be indexed → static, rebuilt on
|
|
44
|
+
the CMS publish event."* — ❌ *"Blog post: SSR, for SEO."*
|
|
45
|
+
|
|
46
|
+
3. **Split the page, not the mode.** A personalised header, a cart count or a stock figure is a
|
|
47
|
+
fragment: fetched on the client after a cached page loads, rendered at the edge, or streamed
|
|
48
|
+
into a static shell (islands, server islands, partial prerendering).
|
|
49
|
+
✅ *"Home: revalidated every 10 min; the 'Hi, Sam' greeting and cart badge load client-side
|
|
50
|
+
from `/api/me`."* — ❌ *"Home must be SSR because the header shows the user's name."*
|
|
51
|
+
A value that must be fresh within seconds is never left to time-based revalidation: a
|
|
52
|
+
stale-while-revalidate cache serves the old page to the first visitor after the window
|
|
53
|
+
ends. ✅ *"Product page cached; stock fetched from a 5-second-TTL endpoint; checkout
|
|
54
|
+
re-checks stock."* — ❌ *"Product page ISR with revalidate: 5 so stock is fresh."* A
|
|
55
|
+
CDN TTL on that endpoint also turns a traffic spike into one origin call per TTL per item.
|
|
56
|
+
|
|
57
|
+
4. **Treat SEO as an HTML question, not an SSR question.** Crawlers and link-preview bots need
|
|
58
|
+
the content and `og:` tags in the first response. A static page has them; so does a
|
|
59
|
+
revalidated one. CSR is wrong for any page that must be indexed or shared, and right for
|
|
60
|
+
logged-in screens nobody searches for.
|
|
61
|
+
✅ *"Marketing pages prerendered; the logged-in app stays a client-rendered SPA."* —
|
|
62
|
+
❌ *"Move the whole app to SSR so Google can see it."*
|
|
63
|
+
|
|
64
|
+
5. **Put a number on cost.** When the input gives traffic, write the routes as a CSV (columns
|
|
65
|
+
in the script header: `route, mode, views_month, pages, revalidate_s, changes_month,
|
|
66
|
+
builds_month`) and count origin renders under the proposed mode against per-request SSR:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Claude Code
|
|
70
|
+
python3 "${CLAUDE_SKILL_DIR}/scripts/render_load.py" routes.csv
|
|
71
|
+
# Other IDEs (from the skill folder)
|
|
72
|
+
python3 scripts/render_load.py routes.csv
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The count also shows when time-based revalidation saves nothing: a long-tail route whose
|
|
76
|
+
pages each get fewer views than revalidation windows renders nearly once per view, so
|
|
77
|
+
on-demand revalidation on the change event is the saving. If no script can run, do the
|
|
78
|
+
same arithmetic by hand and show it. Without traffic, state the comparison as "one render
|
|
79
|
+
per view vs one per change" and list traffic under To confirm.
|
|
80
|
+
|
|
81
|
+
6. **Name the cache key and the invalidation event for every cached route.** What event
|
|
82
|
+
invalidates which pages, and what must never reach a shared cache.
|
|
83
|
+
✅ *"Product updated in the PIM → webhook revalidates `/p/[slug]` and its category pages.
|
|
84
|
+
Cart and account: `Cache-Control: private, no-store`."* — ❌ *"Cache with a 60-second
|
|
85
|
+
TTL."* Filter and sort query strings multiply cache entries: name the parameters that are
|
|
86
|
+
part of the key and drop the rest.
|
|
87
|
+
|
|
88
|
+
7. **Name the failure mode of each row.** What the user sees when the data source is slow or
|
|
89
|
+
down. SSR ties time to first byte to the slowest call: say the timeout and the fallback, or
|
|
90
|
+
stream the slow part behind a placeholder. Revalidated pages serve the last good copy on
|
|
91
|
+
failure. CSR shows a spinner, and nothing without JavaScript. ✅ *"Stock call times out at
|
|
92
|
+
800 ms → page shows 'Check availability' and the add-to-cart button still works."*
|
|
93
|
+
|
|
94
|
+
8. **Use framework terms only for the framework the user named.** Then give the exact setting
|
|
95
|
+
per route from [`reference.md`](reference.md), flag anything that depends on the version in
|
|
96
|
+
`package.json`, and catch the framework trap (a cookie read in a shared layout that makes
|
|
97
|
+
every route dynamic; a server cache rule on a personalised path). No framework named →
|
|
98
|
+
neutral terms only.
|
|
99
|
+
|
|
100
|
+
9. **Write it up, lean.** First line: the answer, including whether the team's proposal
|
|
101
|
+
stands. Then the route table: route, mode, why (freshness / personalised / indexed),
|
|
102
|
+
invalidation, failure mode. Then the traps found, each in one or two lines, the cost
|
|
103
|
+
numbers if traffic was given, and To confirm. No section for a question nobody asked.
|
|
104
|
+
|
|
105
|
+
**Non-interactive runs** (subagent, CI, headless): missing traffic, freshness tolerances or
|
|
106
|
+
framework version become `[assumption]` rows listed at the top under To confirm. A request
|
|
107
|
+
that names no routes, pages or app at all emits `BLOCKED: need the app's routes and what each
|
|
108
|
+
page shows`, because a route table of invented pages decides nothing.
|
|
109
|
+
|
|
110
|
+
## Useful references in this skill
|
|
111
|
+
|
|
112
|
+
- [`reference.md`](reference.md) — the mode-by-mode cost and failure table, cache headers that are safe for personalised pages, and per-framework settings for Next.js, Nuxt, Remix / React Router, SvelteKit and Astro
|
|
113
|
+
- [`scripts/render_load.py`](scripts/render_load.py) — counts origin renders per month per route against per-request SSR (stdlib only)
|
|
114
|
+
- [`examples/saas-site.md`](examples/saas-site.md) — worked example: marketing site, docs, status page and app in SvelteKit
|
|
115
|
+
|
|
116
|
+
## Quality bar
|
|
117
|
+
|
|
118
|
+
- **One row per route or route group,** each with a mode and the deciding reason. One mode for the whole app fails.
|
|
119
|
+
- **Every cached route names its invalidation event** and the pages it touches; time alone is not an invalidation plan for data that changes on an event.
|
|
120
|
+
- **Every personalised response is named as never shared-cached** (`private, no-store`, or kept off the cached page as a fragment).
|
|
121
|
+
- **Seconds-fresh data is a fragment with its own short-lived source,** not the page's revalidation window.
|
|
122
|
+
- **Every row has a failure mode:** what the user sees when its data source is slow or down.
|
|
123
|
+
- **Cost is a computed number when traffic is given,** from the input's figures only.
|
|
124
|
+
- **Framework settings appear only for the named framework,** and version-dependent behaviour is flagged.
|
|
125
|
+
- **Nothing invented:** traffic, freshness and versions come from the input or carry `[assumption]`.
|
|
126
|
+
|
|
127
|
+
## When to use this skill
|
|
128
|
+
|
|
129
|
+
- ✅ "Should these pages be SSR, static or ISR?" for an app with several kinds of page
|
|
130
|
+
- ✅ A team proposing SSR (or an SPA) everywhere and wanting it checked
|
|
131
|
+
- ✅ "Our pages don't show up in Google / link previews are blank" on a client-rendered site
|
|
132
|
+
- ✅ Setting route rules, revalidation and cache headers in a named framework
|
|
133
|
+
|
|
134
|
+
## When NOT to use this skill
|
|
135
|
+
|
|
136
|
+
- ❌ Designing the whole system (services, data stores, rollout) — `design-doc`
|
|
137
|
+
- ❌ Writing the chosen strategy up as a numbered decision record — `adr-generator`
|
|
138
|
+
- ❌ Setting latency or availability targets for the app — `nfr-spec`
|
|
139
|
+
- ❌ Diagnosing why a page is slow from a Lighthouse report — that is a page-speed review, not a rendering decision
|
|
140
|
+
|
|
141
|
+
## Anti-patterns to avoid
|
|
142
|
+
|
|
143
|
+
- ❌ **SSR everywhere "for SEO."** It pays a render per view for pages that are the same for everyone and could be served from a CDN.
|
|
144
|
+
- ❌ **A personalised header forcing the site dynamic.** A name and a cart count are a fragment, not a rendering mode.
|
|
145
|
+
- ❌ **Short revalidation as a freshness guarantee.** Stale-while-revalidate serves the stale copy first; a 5-second window across thousands of pages is also thousands of regenerations nobody needed.
|
|
146
|
+
- ❌ **CSR for a page that must be indexed or shared.** Preview bots don't run JavaScript; the card is blank.
|
|
147
|
+
- ❌ **Caching a logged-in page at the CDN.** One user's account page served to the next visitor is a data leak, not a performance win.
|
|
148
|
+
- ❌ **SSR with no timeout.** The page is as slow as the slowest API it calls, and fails with it.
|
|
149
|
+
- ❌ **Framework advice for a framework nobody named,** or version-specific settings stated without the version.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "Should our product pages be SSR or static with ISR? Stock changes a lot.",
|
|
4
|
+
"should_trigger": true
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"query": "The team wants to server-render the whole site for SEO. Is that actually needed?",
|
|
8
|
+
"should_trigger": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"query": "Help me pick SSG, ISR, SSR or CSR for each route of our Nuxt app",
|
|
12
|
+
"should_trigger": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"query": "Our SPA's pages don't show up in Google and link previews are blank. What should we render on the server?",
|
|
16
|
+
"should_trigger": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"query": "Which of our SvelteKit pages can we prerender and cache at the CDN, and which have to be dynamic because they're logged in?",
|
|
20
|
+
"should_trigger": true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"query": "Write the design doc for the new storefront: services, data stores and rollout plan",
|
|
24
|
+
"should_trigger": false
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"query": "Write an ADR for our decision to move from Create React App to Next.js",
|
|
28
|
+
"should_trigger": false
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"query": "Set p95 latency and availability targets for the checkout API",
|
|
32
|
+
"should_trigger": false
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"query": "Here's our Lighthouse report: LCP is 4.1s on mobile. What's slowing the page down?",
|
|
36
|
+
"should_trigger": false
|
|
37
|
+
}
|
|
38
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "rendering-strategy",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "We're rebuilding our outdoor gear shop in Next.js and the team wants SSR on every page. Everything we know is in fernhill-app.md. How should each route be rendered? Keep it to what we need to decide.",
|
|
7
|
+
"assertions": [
|
|
8
|
+
"Opens with the answer, including that SSR on every page is not needed, and gives a decision for each route (home, listing, product detail, cart, account, blog) rather than one mode for the app",
|
|
9
|
+
"Says SEO needs the content in the HTML of the first response, which static or revalidated pages provide, so the home, listing, blog and product content do not need per-request SSR",
|
|
10
|
+
"Keeps the personalised header off the rendering decision: the greeting and cart count load as a client or edge fragment on cached pages, and it flags that reading the session cookie in the root layout (or force-dynamic there) makes every route dynamic",
|
|
11
|
+
"Does not rely on time-based revalidation for the 10-second stock requirement: keeps the product page cached, fetches stock separately from a short-lived source, and says checkout or add-to-cart must re-check stock",
|
|
12
|
+
"Names what the product page shows when the inventory API is slow or times out during a drop (a timeout and a fallback), rather than letting the whole page wait",
|
|
13
|
+
"Names invalidation events and the pages they touch: the PIM webhook revalidates the product page and its category pages, the CMS publish webhook revalidates the post and the blog index; and states that cart and account are never stored in a shared cache",
|
|
14
|
+
"Compares renders or server load under SSR everywhere with the proposed mix using the file's traffic figures, with arithmetic that matches them",
|
|
15
|
+
"Does not invent traffic, freshness requirements, versions or services not in the file; anything else is tagged [assumption] or listed under To confirm"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": 2,
|
|
20
|
+
"prompt": "Our product is one React SPA built with Vite: marketing pages, pricing, docs and the logged-in app all in the same bundle. Marketing says our pages barely show up in Google and when people share our pricing page on LinkedIn the preview is blank. Someone wants to move the whole thing to server-side rendering. Is that the right call?",
|
|
21
|
+
"assertions": [
|
|
22
|
+
"Answers in the first lines that the whole app does not need server rendering: the public pages need HTML at first byte and the logged-in app can stay client-rendered",
|
|
23
|
+
"Splits the decision by route group (marketing, pricing, docs, logged-in app) rather than one mode for everything",
|
|
24
|
+
"Explains the blank link preview as the Open Graph tags not being in the initial HTML, because preview bots do not run JavaScript, and that prerendering the public pages fixes it",
|
|
25
|
+
"Recommends prerendering (static at build) or revalidation for marketing, pricing and docs rather than per-request SSR, since they are the same for every visitor",
|
|
26
|
+
"Says how the prerendered pages get updated (rebuild on deploy or on a content-change event)",
|
|
27
|
+
"Stays framework-neutral or offers options, and does not present one specific meta-framework's API as required",
|
|
28
|
+
"Does not invent traffic, page counts or update frequencies; anything it needs is asked for or tagged [assumption]",
|
|
29
|
+
"Is a chat-length answer, not a long design document"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": 3,
|
|
34
|
+
"prompt": "Election night is in two weeks and I'm worried our Nuxt config won't hold. routes.md has our routes, traffic and the draft routeRules. What would you change, route by route?",
|
|
35
|
+
"assertions": [
|
|
36
|
+
"Flags the swr rule on /account/** as caching personalised pages in a shared server cache, so one reader's account page can be served to another, and replaces it with no shared caching (ssr: false or private, no-store)",
|
|
37
|
+
"Does not keep per-request SSR for the results pages: they are the same for everyone and a minute of staleness is acceptable, so they get a short swr or isr window (or a cached shell with a short-TTL results endpoint)",
|
|
38
|
+
"Uses the election-night traffic to show what per-request SSR on results would cost against a cached window, with arithmetic that matches the file",
|
|
39
|
+
"Keeps article pages cached and moves the paywall meter to a client-side (or edge) fragment read from the cookie, instead of rendering every article per request",
|
|
40
|
+
"Flags that prerender: true on topic pages freezes them until the next deploy while 150 articles a day are published, and replaces it with revalidation or on-demand invalidation on the CMS webhook",
|
|
41
|
+
"Names invalidation events: article publish or correction purges that article plus its topic pages and the home page",
|
|
42
|
+
"Names what results pages show if the results feed stalls (the last good copy, with an updated-at time)",
|
|
43
|
+
"Gives settings in Nuxt routeRules terms only, and does not invent traffic, versions or hosting details beyond the file"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": 4,
|
|
48
|
+
"held_out": "2026-10-20",
|
|
49
|
+
"prompt": "our recipe site (nuxt) is a spa and google barely indexes it, also when people share a recipe on whatsapp the preview is just blank. routes.md has the page list, nuxt.config.ts and pages/recipes/[slug].vue are in here too, and build-notes.txt has what we tried. what should each page actually be - ssr, static, something else? we're on a static bucket + cdn right now if that matters",
|
|
50
|
+
"assertions": [
|
|
51
|
+
"Answers the question directly with a rendering mode for each of the seven routes in routes.md (/, /recipes/[slug], /category/[name], /search?q=, /account/saved, /account/settings, /print/[slug]), e.g. as a table, before or alongside the explanation.",
|
|
52
|
+
"Flags that the current setup (nitro preset 'static', files in a bucket behind a CDN, no servers) cannot run SSR or ISR, so any per-request or revalidating route needs a server/edge runtime; either recommends that hosting change explicitly or gives a static-only path (prerender plus rebuild on the Larder webhook) and states the trade-off.",
|
|
53
|
+
"Uses build-notes.txt to estimate that prerendering all ~42,000 recipes at the observed rate (1,200 in 6 min) takes about 3.5 hours (~210 min), and therefore recommends prerendering a subset and generating the rest on demand/incrementally rather than a full rebuild per CMS edit.",
|
|
54
|
+
"Keeps /recipes/[slug] shared-cacheable (static or revalidated) and moves the per-user Save heart to a client-side island/fetch; does not make the whole recipe page per-request SSR or vary its cache on the user's login just for the heart.",
|
|
55
|
+
"Explains that WhatsApp and similar link-preview crawlers do not run JavaScript, so title, description and og:image must be in the initial HTML, and points out that [slug].vue fetches the recipe and calls useHead inside onMounted, so that fetch must move to server-side data loading (useAsyncData/useFetch) - turning ssr on alone will not fix previews.",
|
|
56
|
+
"Ties invalidation to real events from the input: the Larder publish/edit/unpublish webhook for recipe (and affected category) pages and the daily 06:00 editor swap for the home page, and treats the ~3,000 reviews/day rating as tolerating a short revalidation window or a client fetch rather than requiring SSR.",
|
|
57
|
+
"Marks /search and /account/* as not for indexing or shared caching (client-rendered or private per-request), and gives /print/[slug] a canonical to /recipes/[slug] or noindex as duplicate content.",
|
|
58
|
+
"Invents nothing not in the input: no traffic figures, costs, recipe counts, build times, hosting providers, or Larder/FindIt API features beyond what the files state; anything needing the network (e.g. testing a URL with a preview debugger) is given as a command or step for the user to run."
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": 5,
|
|
63
|
+
"held_out": "2026-10-20",
|
|
64
|
+
"prompt": "Our tech lead wants to ship the whole SvelteKit app (Tallyhub) as SSR on Stratus Functions next sprint - 'one mode, less to think about.' Marketing wants pricing and docs to be fast, and finance is asking what SSR-everything will cost us. The relevant bits are in the repo: traffic.md, routes.txt, svelte.config.js, src/hooks.server.ts, src/routes/+layout.server.ts and src/routes/(marketing)/pricing/+page.server.ts. Can you give me a rendering plan per route and tell me straight whether SSR-everything is actually a cost problem? I'm bringing this to Thursday's architecture review, so keep it to something I can put on a slide or two plus notes.",
|
|
65
|
+
"assertions": [
|
|
66
|
+
"Answers both questions directly: gives a rendering mode for each route or route group in routes.txt (/, /pricing, /customers/:slug, /docs/*, /blog/:slug, /app/* pages, api/*) and gives a clear yes/no on whether SSR-everything is a cost problem.",
|
|
67
|
+
"Computes cost from traffic.md: public pages 2.4M x 0.22 s x 1 GB = ~528,000 GB-s = ~$9.50 plus ~$0.96 invocations, about $10-11/month (and/or the app's ~2.52M navigations adding roughly another $11), and concludes compute cost is not a strong argument against SSR - the stronger reasons are cold-start latency (p95 900 ms) and cacheability.",
|
|
68
|
+
"Flags as the top-priority issue that src/hooks.server.ts sets 'cache-control: public, max-age=300' on every response, which would let the CDN serve one customer's authenticated /app pages (dashboard, billing) to another; says app and API responses must be private/no-store and the header must be set per route, not globally.",
|
|
69
|
+
"Notices that the root src/routes/+layout.server.ts returns locals.user for every page, so public pages depend on the session cookie and cannot be prerendered or shared-cached; recommends moving the user load into the app layout and rendering the nav's Log in/Open dashboard state on the client (or from a cheap check) for public pages.",
|
|
70
|
+
"Handles /pricing's per-country currency (x-country header -> USD/EUR/GBP): prerender or cache one variant per currency with country/currency in the cache key, resolve at the edge, or pick currency on the client - and warns that a single prerendered or shared-cached pricing page would show one currency to everyone.",
|
|
71
|
+
"Recommends prerendering docs (~900 repo markdown pages), blog (~250) and customers (40) at build, e.g. with SvelteKit's per-route `export const prerender = true`, with deploy as the invalidation event since the content lives in the repo.",
|
|
72
|
+
"Keeps /app/dashboard (live usage metrics) and other /app pages per-request or client-fetched behind login, never shared-cached, and notes that SvelteKit client navigations still hit functions for load data.",
|
|
73
|
+
"Invents nothing not in the input: no Stratus features, prices, traffic numbers, adapters or routes beyond the files (adapter-auto is noted as-is; any platform capability it can't verify offline is stated as something to check), and the answer is sized for a slide or two plus notes rather than a long essay."
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Fernhill Outdoor — web shop rebuild
|
|
2
|
+
|
|
3
|
+
We sell hiking and climbing gear online. The current site is a client-rendered single-page app
|
|
4
|
+
and we lost a lot of Google traffic after launching it. We are rebuilding on **Next.js 15
|
|
5
|
+
(App Router)**, self-hosted on two Node containers behind a CDN that honours `Cache-Control`.
|
|
6
|
+
|
|
7
|
+
## The team's proposal
|
|
8
|
+
|
|
9
|
+
> Render every page with SSR (`export const dynamic = 'force-dynamic'` in the root layout).
|
|
10
|
+
> SSR is what fixes SEO, and it means every page always shows the right header and stock.
|
|
11
|
+
|
|
12
|
+
## Routes
|
|
13
|
+
|
|
14
|
+
| Route | What it shows | Pages | Page views / month |
|
|
15
|
+
|---|---|---:|---:|
|
|
16
|
+
| `/` home | hero, featured products, seasonal banners | 1 | 900,000 |
|
|
17
|
+
| `/c/[category]` product listing | product grid, filters as query params (`?size=`, `?colour=`, `?brand=`, `?sort=`) | 60 | 1,400,000 |
|
|
18
|
+
| `/p/[slug]` product detail | photos, description, price, reviews, **stock level** ("Only 3 left") | 4,000 | 2,600,000 |
|
|
19
|
+
| `/cart` | the user's basket | 1 | 380,000 |
|
|
20
|
+
| `/account` | orders, addresses, saved items | 1 | 150,000 |
|
|
21
|
+
| `/blog/[slug]` and `/blog` | trip reports and gear guides | 300 | 220,000 |
|
|
22
|
+
|
|
23
|
+
## What changes, and how fast
|
|
24
|
+
|
|
25
|
+
- **Product content and prices** live in our PIM. It sends a webhook on every product update.
|
|
26
|
+
Prices change in a nightly batch plus a few flash sales a month. About 2,000 product updates
|
|
27
|
+
a month in total.
|
|
28
|
+
- **Stock** comes from the inventory API. Twice a month we run a limited "drop": one product,
|
|
29
|
+
a few hundred units, sold out in minutes. During a drop, the stock shown on the product page
|
|
30
|
+
must be right to within 10 seconds or we get angry customers who saw "in stock" and then
|
|
31
|
+
couldn't buy.
|
|
32
|
+
- The inventory API answers in about 180 ms normally; during drops it slows to 2–3 seconds and
|
|
33
|
+
sometimes times out.
|
|
34
|
+
- **Blog** posts come from a headless CMS that sends a webhook on publish. About 12 posts a month.
|
|
35
|
+
- **Header:** every page shows "Hi, {first name}" and the cart count when the visitor is
|
|
36
|
+
logged in. The design has it in the root layout, reading the session cookie.
|
|
37
|
+
|
|
38
|
+
## Ask
|
|
39
|
+
|
|
40
|
+
How should each route be rendered? We need to decide before the sprint starts.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Valley Courier — election night
|
|
2
|
+
|
|
3
|
+
Local news site on **Nuxt 3**, deployed on our own Node servers (Nitro) behind a CDN.
|
|
4
|
+
|
|
5
|
+
## Routes
|
|
6
|
+
|
|
7
|
+
| Route | What it shows | Pages | Traffic |
|
|
8
|
+
|---|---|---:|---|
|
|
9
|
+
| `/` | top stories, results ticker | 1 | 600,000 views / month |
|
|
10
|
+
| `/news/[slug]` | articles; metered paywall: anonymous readers get 5 free articles a month, with a "3 free articles left" bar | 12,000 | 4,000,000 views / month |
|
|
11
|
+
| `/topics/[topic]` | latest articles on one topic | 200 | 300,000 views / month |
|
|
12
|
+
| `/results/[race]` | live election results by race | 40 | expected 1,800,000 views between 21:00 and 03:00 on election night |
|
|
13
|
+
| `/account/**` | subscription, saved articles, newsletter settings | 3 | 40,000 views / month |
|
|
14
|
+
|
|
15
|
+
- The results feed updates about every 30 seconds on the night. Readers expect results no more
|
|
16
|
+
than a minute old.
|
|
17
|
+
- The newsroom publishes about 150 articles a day and corrects some after publishing. The CMS
|
|
18
|
+
sends a webhook on publish and on edit.
|
|
19
|
+
- The paywall meter is counted from a cookie.
|
|
20
|
+
|
|
21
|
+
## Draft config
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
export default defineNuxtConfig({
|
|
25
|
+
routeRules: {
|
|
26
|
+
'/': { swr: 300 },
|
|
27
|
+
'/news/**': { ssr: true }, // per request, so the meter is right
|
|
28
|
+
'/topics/**': { prerender: true },
|
|
29
|
+
'/results/**': { ssr: true }, // must be live
|
|
30
|
+
'/account/**': { swr: 60 }, // takes load off the servers
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Crumbly Kitchen - Nuxt 3
|
|
2
|
+
export default defineNuxtConfig({
|
|
3
|
+
ssr: false,
|
|
4
|
+
nitro: {
|
|
5
|
+
preset: 'static',
|
|
6
|
+
},
|
|
7
|
+
app: {
|
|
8
|
+
head: {
|
|
9
|
+
title: 'Crumbly Kitchen',
|
|
10
|
+
meta: [{ name: 'description', content: 'Recipes for every day' }],
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
runtimeConfig: {
|
|
14
|
+
public: {
|
|
15
|
+
larderApiBase: 'https://cms.example.invalid/api',
|
|
16
|
+
findItKey: 'PLACEHOLDER_SEARCH_KEY',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const route = useRoute()
|
|
3
|
+
const recipe = ref<any>(null)
|
|
4
|
+
const saved = ref(false)
|
|
5
|
+
|
|
6
|
+
onMounted(async () => {
|
|
7
|
+
recipe.value = await $fetch(`/recipes/${route.params.slug}`, {
|
|
8
|
+
baseURL: useRuntimeConfig().public.larderApiBase,
|
|
9
|
+
})
|
|
10
|
+
useHead({
|
|
11
|
+
title: recipe.value.title,
|
|
12
|
+
meta: [
|
|
13
|
+
{ property: 'og:title', content: recipe.value.title },
|
|
14
|
+
{ property: 'og:image', content: recipe.value.heroImage },
|
|
15
|
+
{ name: 'description', content: recipe.value.summary },
|
|
16
|
+
],
|
|
17
|
+
})
|
|
18
|
+
const token = localStorage.getItem('ck_token')
|
|
19
|
+
if (token) {
|
|
20
|
+
const res: any = await $fetch(`/api/saved/${route.params.slug}`, {
|
|
21
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
22
|
+
})
|
|
23
|
+
saved.value = res.saved
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<div v-if="recipe">
|
|
30
|
+
<h1>{{ recipe.title }}</h1>
|
|
31
|
+
<SaveHeart :saved="saved" />
|
|
32
|
+
<RatingStars :avg="recipe.ratingAvg" :count="recipe.reviewCount" />
|
|
33
|
+
<IngredientList :items="recipe.ingredients" />
|
|
34
|
+
<MethodSteps :steps="recipe.steps" />
|
|
35
|
+
</div>
|
|
36
|
+
<Spinner v-else />
|
|
37
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Crumbly Kitchen routes
|
|
2
|
+
|
|
3
|
+
| Route | What's on it | Notes |
|
|
4
|
+
|---|---|---|
|
|
5
|
+
| `/` | featured recipes, seasonal picks | editors swap the featured set every day at 06:00 |
|
|
6
|
+
| `/recipes/[slug]` | the recipe: title, hero image, ingredients, steps, avg rating + review count, "Save" heart | ~42,000 recipes. Recipe text rarely changes after publish. ~3,000 reviews posted per day across the site. Heart is filled if the logged-in user saved it |
|
|
7
|
+
| `/category/[name]` | paginated list of recipes in a category, 24 per page | 180 categories |
|
|
8
|
+
| `/search?q=` | search results from FindIt | |
|
|
9
|
+
| `/account/saved` | the logged-in user's saved recipes | login required |
|
|
10
|
+
| `/account/settings` | email, password, newsletter prefs | login required |
|
|
11
|
+
| `/print/[slug]` | printer-friendly version of a recipe | same content as /recipes/[slug] |
|
|
12
|
+
|
|
13
|
+
Content comes from Larder CMS. Larder can send a webhook on publish, edit and unpublish.
|
|
14
|
+
|
|
15
|
+
Hosting: static files in a storage bucket behind a CDN. No servers.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Tallyhub - SvelteKit route map (src/routes)
|
|
2
|
+
|
|
3
|
+
PUBLIC
|
|
4
|
+
(marketing)/+page.svelte / landing page, edited by marketing ~2x/month
|
|
5
|
+
(marketing)/pricing/ /pricing 3 plans, price shown in USD/EUR/GBP by visitor country (x-country header)
|
|
6
|
+
(marketing)/customers/[slug]/ /customers/:slug 40 case studies
|
|
7
|
+
docs/[...path]/ /docs/* ~900 pages, markdown files in the repo (docs/content/)
|
|
8
|
+
blog/[slug]/ /blog/:slug ~250 posts, markdown in the repo
|
|
9
|
+
|
|
10
|
+
BEHIND LOGIN
|
|
11
|
+
app/+layout.server.ts redirects to /login if no user
|
|
12
|
+
app/dashboard/ /app/dashboard live usage metrics for the customer's org
|
|
13
|
+
app/projects/[id]/ /app/projects/:id
|
|
14
|
+
app/settings/ /app/settings
|
|
15
|
+
app/billing/ /app/billing invoices, card on file (last 4)
|
|
16
|
+
|
|
17
|
+
API
|
|
18
|
+
api/* JSON endpoints used by the app
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Handle } from '@sveltejs/kit';
|
|
2
|
+
import { getUserBySession } from '$lib/server/auth';
|
|
3
|
+
|
|
4
|
+
export const handle: Handle = async ({ event, resolve }) => {
|
|
5
|
+
const sid = event.cookies.get('sid');
|
|
6
|
+
event.locals.user = sid ? await getUserBySession(sid) : null;
|
|
7
|
+
|
|
8
|
+
const response = await resolve(event);
|
|
9
|
+
|
|
10
|
+
// let the CDN take some load off the functions (added by Priya, sprint 41)
|
|
11
|
+
response.headers.set('cache-control', 'public, max-age=300');
|
|
12
|
+
|
|
13
|
+
return response;
|
|
14
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PageServerLoad } from './$types';
|
|
2
|
+
import { priceTable } from '$lib/pricing';
|
|
3
|
+
|
|
4
|
+
const CURRENCY: Record<string, 'USD' | 'EUR' | 'GBP'> = { GB: 'GBP', DE: 'EUR', FR: 'EUR', NL: 'EUR' };
|
|
5
|
+
|
|
6
|
+
export const load: PageServerLoad = async ({ request }) => {
|
|
7
|
+
const country = request.headers.get('x-country') ?? 'US';
|
|
8
|
+
const currency = CURRENCY[country] ?? 'USD';
|
|
9
|
+
return { currency, plans: priceTable(currency) };
|
|
10
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LayoutServerLoad } from './$types';
|
|
2
|
+
|
|
3
|
+
// root layout: every page gets the user so the nav can show "Log in" or "Open dashboard"
|
|
4
|
+
export const load: LayoutServerLoad = async ({ locals }) => {
|
|
5
|
+
return {
|
|
6
|
+
user: locals.user
|
|
7
|
+
? { name: locals.user.name, org: locals.user.orgName, plan: locals.user.plan }
|
|
8
|
+
: null
|
|
9
|
+
};
|
|
10
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Traffic and platform numbers (from Dan, Oct 2026)
|
|
2
|
+
|
|
3
|
+
Monthly, last 90 days averaged:
|
|
4
|
+
- Public pageviews: 2.4M total
|
|
5
|
+
- marketing (/, /pricing, /customers): 0.9M
|
|
6
|
+
- docs: 1.3M
|
|
7
|
+
- blog: 0.2M
|
|
8
|
+
- App: 180k sessions/month, avg 14 page navigations per session
|
|
9
|
+
|
|
10
|
+
Stratus Functions pricing (our plan):
|
|
11
|
+
- $0.40 per 1M invocations
|
|
12
|
+
- $0.000018 per GB-second
|
|
13
|
+
- functions configured at 1024 MB
|
|
14
|
+
- static/CDN-served requests: no per-request charge, bandwidth included up to 1 TB/month
|
|
15
|
+
|
|
16
|
+
Measured on staging with SSR on:
|
|
17
|
+
- avg SSR duration: 220 ms per page
|
|
18
|
+
- cold start p95: 900 ms
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Worked example: SaaS marketing site, docs, status page and app (SvelteKit)
|
|
2
|
+
|
|
3
|
+
## Input
|
|
4
|
+
|
|
5
|
+
> We're moving Tallyhook (invoicing SaaS) to one SvelteKit app on Node behind a CDN. Routes:
|
|
6
|
+
> `/` and `/pricing` (marketing, edited a few times a month, the pricing table shows the
|
|
7
|
+
> visitor's currency from their country); `/docs/**` (about 500 pages, markdown in the repo,
|
|
8
|
+
> deployed on merge); `/changelog` (one entry a week); `/status` (must show an incident within
|
|
9
|
+
> 60 seconds, and has to work when the app is down); `/app/**` (the logged-in product).
|
|
10
|
+
> Traffic a month: marketing and changelog 300,000 views, docs 450,000, `/app` 900,000. We were
|
|
11
|
+
> going to set `ssr = true` everywhere and be done. How should each route render?
|
|
12
|
+
|
|
13
|
+
## Output
|
|
14
|
+
|
|
15
|
+
Don't render everything per request: only `/app` needs per-user pages, and even there the
|
|
16
|
+
choice is client- or server-rendered behind no shared cache. Everything public is prerendered
|
|
17
|
+
or cached, and the status page must not depend on the app at all.
|
|
18
|
+
|
|
19
|
+
| Route | Mode | Why | Invalidated by | When its source fails |
|
|
20
|
+
|---|---|---|---|---|
|
|
21
|
+
| `/`, `/pricing` | prerendered; currency picked by a CDN-geo cache key or a small client fragment | same for everyone in a country, indexed, shared | deploy (content is in the repo) `[assumption]` | page serves; currency falls back to USD `[assumption]` |
|
|
22
|
+
| `/docs/**` | prerendered (`export const prerender = true` on the docs layout), `csr = false` on pages with no widgets | same for everyone, indexed, changes only on merge | deploy on merge | served from the CDN |
|
|
23
|
+
| `/changelog` | prerendered | weekly change, indexed | deploy | served from the CDN |
|
|
24
|
+
| `/status` | static page on separate hosting, polling a status JSON with a 30-second TTL | must work when the app is down; 60-second freshness | the status JSON updates; the page never needs a rebuild | page loads; shows the last status with its time |
|
|
25
|
+
| `/app/**` | `ssr = false` (client-rendered SPA) or SSR with `private, no-store` | per user, never indexed | — | spinner, then an error state with retry |
|
|
26
|
+
|
|
27
|
+
**Traps found**
|
|
28
|
+
|
|
29
|
+
- `/status` inside the same SvelteKit app goes down with it. Host it apart from the app.
|
|
30
|
+
- Currency from geo makes `/pricing` vary per country: put the country in the CDN cache key
|
|
31
|
+
(a few entries), not `Vary: Cookie` (one entry per visitor).
|
|
32
|
+
- `prerender = true` on the root layout would reach `/app` too; set it on the public layouts only.
|
|
33
|
+
|
|
34
|
+
**Cost.** SSR on every route: 1,650,000 renders a month. This plan: public pages render at
|
|
35
|
+
build only (500-odd pages per deploy); `/app` at most 900,000 if server-rendered, 0 if
|
|
36
|
+
client-rendered.
|
|
37
|
+
|
|
38
|
+
**To confirm**
|
|
39
|
+
|
|
40
|
+
- How often marketing pages are edited outside a deploy (if a CMS edits them, add an on-demand
|
|
41
|
+
rebuild on its publish event).
|
|
42
|
+
- The fallback currency.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rendering-strategy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Decide route by route how each page of a web app is rendered — static at build (SSG), static with revalidation (ISR, stale-while-revalidate), server-rendered per request (SSR, streaming), client-rendered (CSR, SPA), at the edge, or a mix with islands and server components — from how fresh each route's data must be, whether it is personalised, whether it must be indexed or shared, what it costs per request at the stated traffic and how much JavaScript it needs, with the cache keys, invalidation events and failure modes of each choice. Use when the user asks \"should this be SSR or static\", \"SSR vs SSG vs ISR\", \"how should each page be rendered\", \"do we need SSR for SEO\", \"which pages can we cache\", or is choosing rendering modes in Next.js, Nuxt, Remix, React Router, SvelteKit or Astro.",
|
|
5
|
+
"entrypoint": "SKILL.md",
|
|
6
|
+
"deps": {
|
|
7
|
+
"npm": [],
|
|
8
|
+
"pip": []
|
|
9
|
+
},
|
|
10
|
+
"env": {
|
|
11
|
+
"required": [],
|
|
12
|
+
"optional": []
|
|
13
|
+
},
|
|
14
|
+
"related": [
|
|
15
|
+
"adr-generator",
|
|
16
|
+
"design-doc",
|
|
17
|
+
"nfr-spec"
|
|
18
|
+
],
|
|
19
|
+
"handoff": [
|
|
20
|
+
{
|
|
21
|
+
"to": "nfr-spec",
|
|
22
|
+
"when": "the routes have no stated freshness, latency or availability targets to decide against",
|
|
23
|
+
"purpose": "Pin the freshness and time-to-first-byte targets each route's mode must meet.",
|
|
24
|
+
"fallback": "If nfr-spec is absent, state each route's freshness tolerance and TTFB target inline, tagged [assumption] where the user gave none."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"to": "adr-generator",
|
|
28
|
+
"when": "the route table is agreed and the team wants it on record",
|
|
29
|
+
"purpose": "Record the rendering strategy, the options rejected and why, as a numbered decision.",
|
|
30
|
+
"fallback": "If adr-generator is absent, add a short decision note under the table: the decision, the alternative rejected (such as SSR everywhere) and the reason."
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"tags": [
|
|
34
|
+
"frontend",
|
|
35
|
+
"rendering",
|
|
36
|
+
"ssr",
|
|
37
|
+
"caching",
|
|
38
|
+
"seo"
|
|
39
|
+
],
|
|
40
|
+
"permissions": {
|
|
41
|
+
"network": [],
|
|
42
|
+
"commands": [],
|
|
43
|
+
"files": "named-paths",
|
|
44
|
+
"notes": "scripts/render_load.py reads a routes CSV from a named path or stdin and writes a Markdown report to stdout or the path given with -o. No network."
|
|
45
|
+
},
|
|
46
|
+
"model": {
|
|
47
|
+
"tier": "standard",
|
|
48
|
+
"rationale": "Choosing static, server, client or incremental rendering per route follows fixed trade-offs (freshness, personalisation, SEO, cost); bounded judgment."
|
|
49
|
+
}
|
|
50
|
+
}
|