Files
summercms/.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-01-PLAN.md

337 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 11.2-ready-to-share-summercms-io-website-and-newsletter-plugin
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- ../sm-summercmsio-app/vue-summercmsio-app/package.json
- ../sm-summercmsio-app/vue-summercmsio-app/pnpm-lock.yaml
- ../sm-summercmsio-app/vue-summercmsio-app/pnpm-workspace.yaml
- ../sm-summercmsio-app/vue-summercmsio-app/.nvmrc
- ../sm-summercmsio-app/vue-summercmsio-app/.gitignore
- ../sm-summercmsio-app/vue-summercmsio-app/README.md
- ../sm-summercmsio-app/vue-summercmsio-app/tsconfig.json
- ../sm-summercmsio-app/vue-summercmsio-app/nuxt.config.ts
- ../sm-summercmsio-app/vue-summercmsio-app/app/app.config.ts
- ../sm-summercmsio-app/vue-summercmsio-app/app/app.vue
- ../sm-summercmsio-app/vue-summercmsio-app/app/pages/index.vue
- ../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue
- ../sm-summercmsio-app/vue-summercmsio-app/app/components/HeroSection.vue
- ../sm-summercmsio-app/vue-summercmsio-app/app/components/WhySection.vue
- ../sm-summercmsio-app/vue-summercmsio-app/app/components/FeaturesSection.vue
- ../sm-summercmsio-app/vue-summercmsio-app/app/components/WinterSection.vue
- ../sm-summercmsio-app/vue-summercmsio-app/app/components/StartSection.vue
- ../sm-summercmsio-app/vue-summercmsio-app/app/components/TerminalCard.vue
- ../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteFooter.vue
- ../sm-summercmsio-app/vue-summercmsio-app/app/assets/css/tokens.css
- ../sm-summercmsio-app/vue-summercmsio-app/app/assets/css/base.css
- ../sm-summercmsio-app/vue-summercmsio-app/app/data/terminal.json
- ../sm-summercmsio-app/vue-summercmsio-app/app/utils/terminal.ts
- ../sm-summercmsio-app/vue-summercmsio-app/app/utils/scrollSpy.ts
- ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json
- ../sm-summercmsio-app/vue-summercmsio-app/assets-src/logo.png
- ../sm-summercmsio-app/vue-summercmsio-app/scripts/derive-images.sh
- ../sm-summercmsio-app/vue-summercmsio-app/public/sun-logo.webp
- ../sm-summercmsio-app/vue-summercmsio-app/public/sun-badge.webp
- ../sm-summercmsio-app/vue-summercmsio-app/public/favicon.ico
- ../sm-summercmsio-app/vue-summercmsio-app/public/apple-touch-icon.png
- ../sm-summercmsio-app/vue-summercmsio-app/public/og-image.png
- ../sm-summercmsio-app/vue-summercmsio-app/tests/output.test.ts
- ../sm-summercmsio-app/vue-summercmsio-app/tests/terminal.test.ts
- ../sm-summercmsio-app/vue-summercmsio-app/tests/scrollSpy.test.ts
autonomous: true
requirements: []
assumption_delta_decision: no-change
specless_probe_fallback: "skipped: phase has no requirement IDs to probe (visible skip); ROADMAP SC1-SC5 and the D-IDs are the acceptance contract"
user_setup: []
estimate:
tokens: 120000
raw_tokens: 120000
tasks: 3
confidence: low
must_haves:
truths:
- "Per D-04 and SC1, `pnpm -C ../sm-summercmsio-app/vue-summercmsio-app run generate` exits 0 and writes `.output/public/index.html` as real prerendered HTML holding, in order, the sticky header, the hero `#top`, `#why`, `#features`, `#winter`, `#start` and the footer."
- "Per D-23, colors, type, spacing, breakpoints, copy and the two interactions match design/README.md and `SummerCMS Landing.dc.html`; the only copy deviations are the D-12 credit, the D-39 clone step and the D-26 `PostgreSQL 15+` chip."
- "Per D-33, every visible string on the page comes from `i18n/locales/en.json` through @nuxtjs/i18n with the single locale `en` served at `/` (strategy `prefix_except_default`), so Phase 11.3 adds `pl.json` without touching component markup."
- "Per D-34, Roboto 300/400/500/700 and Roboto Mono 400/500 are self-hosted by @nuxt/fonts under `/_fonts/` and the generated HTML and CSS reference no Google font host."
- "Per D-35, styles are plain CSS: the handoff tokens are custom properties on `:root` in `tokens.css`, used by scoped component styles; no CSS framework package is installed."
- "Per D-36, every sun image (header logo, hero badge, favicons, OG image) is derived from the original `logo.png` by a committed ImageMagick script, and the handoff `sun-crop.png` placeholder is not shipped."
- "Per D-37, @nuxtjs/seo emits title, description, canonical, Open Graph, Twitter and schema.org tags, a flat `sitemap.xml` and `robots.txt` at generate time; `og:image` is the absolute `https://summercms.io/og-image.png`; nothing is rendered at runtime."
- "Per D-45, `app.config.ts` has `landing.showRays` and `landing.scrollSpy`, both `true`; setting either to false removes the hero rays layer or the scroll-spy listener."
- "Per D-39 and D-40, the terminal card shows three groups and six commands read from the single source `app/data/terminal.json`, and Copy writes exactly the six command lines joined by newlines (no `$`, no comments), then shows `Copied` for 1500 ms."
- "Per D-47 and D-38, the eight feature tiles, the concept-map link and the Installation guide keep the handoff's extension-less `/docs/...` hrefs, the Docs links point to `/docs`, and both Source links point to `https://git.golem15.com/golem15/summercms`."
- "Per D-09, the From WinterCMS section carries the handoff copy and concept table unchanged, with no added seasons copy; per D-12, the footer has a `Built by Golem15` link to `https://golem15.com` styled as a footer link."
- "Per SC1, at viewport widths of 720px or less the header section links are hidden by a CSS media query (rendered in SSR, no width-dependent markup), and every grid collapses to one column on phone widths."
- "Scroll-spy marks the last of why, features, winter and start whose top is less than 140px from the viewport top; above `#why` no link is active."
- statement: "Visual fidelity at 1280, 721, 720 and 375px, the sticky header, and the Copy label change are confirmed against the handoff in the end-of-phase UAT."
verification: backstop
artifacts:
- path: "../sm-summercmsio-app/vue-summercmsio-app/nuxt.config.ts"
provides: "Nuxt 4 SSG config: fonts, i18n, seo, prerender ignore for /docs"
contains: "prerender"
- path: "../sm-summercmsio-app/vue-summercmsio-app/app/app.config.ts"
provides: "D-45 landing flags"
contains: "scrollSpy"
- path: "../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json"
provides: "all page copy (D-33)"
contains: "A new dawn in content management"
- path: "../sm-summercmsio-app/vue-summercmsio-app/app/data/terminal.json"
provides: "single source of the six terminal commands (D-40)"
contains: "./bin/hello greeter:hello"
- path: "../sm-summercmsio-app/vue-summercmsio-app/app/components/TerminalCard.vue"
provides: "terminal card with Copy"
contains: "copyPayload"
- path: "../sm-summercmsio-app/vue-summercmsio-app/app/utils/scrollSpy.ts"
provides: "pure scroll-spy selection"
contains: "export function activeSection"
- path: "../sm-summercmsio-app/vue-summercmsio-app/app/utils/terminal.ts"
provides: "pure copy payload builder"
contains: "export function copyPayload"
- path: "../sm-summercmsio-app/vue-summercmsio-app/tests/output.test.ts"
provides: "SC1 assertions on the generated output"
contains: "index.html"
- path: "../sm-summercmsio-app/vue-summercmsio-app/scripts/derive-images.sh"
provides: "D-36 image derivation from the original sun"
contains: "magick"
key_links:
- from: "../sm-summercmsio-app/vue-summercmsio-app/app/components/TerminalCard.vue"
to: "../sm-summercmsio-app/vue-summercmsio-app/app/data/terminal.json"
via: "static JSON import; the card renders and copies only these commands"
pattern: "data/terminal\\.json"
- from: "../sm-summercmsio-app/vue-summercmsio-app/app/components/TerminalCard.vue"
to: "../sm-summercmsio-app/vue-summercmsio-app/app/utils/terminal.ts"
via: "copyPayload(groups) builds the clipboard text"
pattern: "copyPayload\\("
- from: "../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue"
to: "../sm-summercmsio-app/vue-summercmsio-app/app/utils/scrollSpy.ts"
via: "scroll listener calls activeSection with section tops"
pattern: "activeSection\\("
- from: "../sm-summercmsio-app/vue-summercmsio-app/nuxt.config.ts"
to: "/docs links on the page"
via: "nitro.prerender.ignore keeps generate from crawling /docs, which the Go binary serves"
pattern: "ignore:.*'/docs'"
---
<objective>
Build `vue-summercmsio-app`, the Nuxt 4 static site for summercms.io, matching the claude.ai/design handoff exactly (D-23) with the decided deviations (D-12, D-26, D-39). `nuxt generate` produces real prerendered HTML (D-04) with self-hosted fonts (D-34), plain-CSS tokens (D-35), the original sun artwork (D-36), static SEO output (D-37), a single `en` locale ready for Phase 11.3 (D-33), the app-config flags (D-45), and a terminal card fed from one command list (D-39, D-40). Docs links stay extension-less (D-47). The superseded decisions apply in their superseded form: D-06 English only (Polish is Phase 11.3, via D-33), D-08 the handoff's section list, D-10 the "Alpha 0.1 is out" tone, and D-11 Source links on the page (D-38).
Purpose: SC1. The Go binary in plan 11.2-02 embeds this output, so the generate output contract here (file layout, `_nuxt/`, `_fonts/`, `_i18n/`, flat `sitemap.xml`) is what the site handler serves.
Output: a new git repository at `/media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app` (it becomes a submodule of `sm-summercmsio-app` in plan 11.2-02), with node:test assertions over the generated output and the two pure utilities.
</objective>
<execution_context>
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@CLAUDE.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-CONTEXT.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-PATTERNS.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/design/README.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/design/SummerCMS Landing.dc.html
@/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/nuxt.config.ts
@/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/package.json
@/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/pnpm-workspace.yaml
**Paths.** Commands run from the summercms.go root. `SITE` = `../sm-summercmsio-app/vue-summercmsio-app` (absolute: `/media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app`). Every file in this plan is written to the SITE repository, which this plan creates. Nothing in summercms.go changes in this plan. Do not run `git add` in the meta repository `summercms/` or in summercms.go; `sm-summercmsio-app/` stays a plain directory until plan 11.2-02 makes it a repository.
**Commits.** Commit inside SITE only, one logical change per commit, conventional messages such as `feat(site): ...`, and never a co-author tag.
**Design source of truth.** `design/SummerCMS Landing.dc.html` is authoritative for punctuation and details not in design/README.md (RESEARCH "Details found in SummerCMS Landing.dc.html"): typographic apostrophes in "each plugin’s YAML" and "the caller’s transaction", the straight apostrophe in "WHAT'S IN THE BOX", gaps, paddings and the terminal layout. Ignore its `support.js` runtime and its JS width state.
<interfaces>
Pure utilities consumed by components and by node:test (use only erasable TypeScript syntax so Node 22 runs the files without a build: no enums, no namespaces, no parameter properties; type-only imports use `import type`):
- `app/utils/terminal.ts`: `export interface TerminalGroup { comment: string; commands: string[] }` and `export function copyPayload(groups: readonly TerminalGroup[]): string` returning every command in order joined by `\n`, with no trailing newline, no `$` and no comment text.
- `app/utils/scrollSpy.ts`: `export const SPY_SECTIONS` = `['why', 'features', 'winter', 'start']`, `export const SPY_THRESHOLD = 140`, and `export function activeSection(tops: ReadonlyArray<{ id: string; top: number }>, threshold?: number): string` returning the id of the last entry whose `top` is strictly less than the threshold, or `''` when none is.
- `app/data/terminal.json`: `{ "groups": [ { "comment": "<i18n key>", "commands": [ ... ] } ] }` with exactly these groups in order: `terminal.getFramework` → `git clone https://git.golem15.com/golem15/summercms`, `cd summercms`; `terminal.installCli` → `go install ./cmd/summer`; `terminal.buildExample` → `cd examples/hello`, `summer build`, `./bin/hello greeter:hello`. Plan 11.2-02 reads this file from Go (D-40 check and the HTML drift guard), so the key names and shape are a contract.
- `app/app.config.ts`: `defineAppConfig({ landing: { showRays: true, scrollSpy: true } })`.
</interfaces>
</context>
<tasks>
<task type="tracer">
<name>Task 1: Tracer: `nuxt generate` turns en.json into a prerendered page with the sticky header and hero, self-hosted fonts, the real sun and static SEO output</name>
<precondition>`node --version` prints v22.18 or newer, `pnpm --version` prints 11.x, and `magick -version` succeeds with WEBP and ICO delegates (RESEARCH Environment Availability).</precondition>
<files>../sm-summercmsio-app/vue-summercmsio-app/package.json, ../sm-summercmsio-app/vue-summercmsio-app/pnpm-lock.yaml, ../sm-summercmsio-app/vue-summercmsio-app/pnpm-workspace.yaml, ../sm-summercmsio-app/vue-summercmsio-app/.nvmrc, ../sm-summercmsio-app/vue-summercmsio-app/.gitignore, ../sm-summercmsio-app/vue-summercmsio-app/README.md, ../sm-summercmsio-app/vue-summercmsio-app/tsconfig.json, ../sm-summercmsio-app/vue-summercmsio-app/nuxt.config.ts, ../sm-summercmsio-app/vue-summercmsio-app/app/app.config.ts, ../sm-summercmsio-app/vue-summercmsio-app/app/app.vue, ../sm-summercmsio-app/vue-summercmsio-app/app/pages/index.vue, ../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue, ../sm-summercmsio-app/vue-summercmsio-app/app/components/HeroSection.vue, ../sm-summercmsio-app/vue-summercmsio-app/app/assets/css/tokens.css, ../sm-summercmsio-app/vue-summercmsio-app/app/assets/css/base.css, ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json, ../sm-summercmsio-app/vue-summercmsio-app/assets-src/logo.png, ../sm-summercmsio-app/vue-summercmsio-app/scripts/derive-images.sh, ../sm-summercmsio-app/vue-summercmsio-app/public/sun-logo.webp, ../sm-summercmsio-app/vue-summercmsio-app/public/sun-badge.webp, ../sm-summercmsio-app/vue-summercmsio-app/public/favicon.ico, ../sm-summercmsio-app/vue-summercmsio-app/public/apple-touch-icon.png, ../sm-summercmsio-app/vue-summercmsio-app/public/og-image.png, ../sm-summercmsio-app/vue-summercmsio-app/tests/output.test.ts</files>
<read_first>
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/design/README.md (Global, 1. Header, 2. Hero, Design Tokens, Assets)
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/design/SummerCMS Landing.dc.html (header and hero markup and inline styles)
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md sections "Standard Stack", "Package Legitimacy Audit", "Nuxt 4 (question 4)", "Pattern 1", "Design Handoff Inventory", Pitfalls 1-4 and 6, Open Questions 4-5
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-PATTERNS.md section "SITE/nuxt.config.ts, package.json, pnpm-workspace.yaml, i18n/locales/en.json"
- /media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/nuxt.config.ts (fonts, i18n and site blocks; copy shapes only)
- /media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/package.json and pnpm-workspace.yaml (scripts, engines, allowBuilds)
</read_first>
<action>
Create the repository and the thinnest production-quality slice through every layer the site has: package config, Nuxt modules, i18n copy, tokens, components, static assets and the generate output, checked by a test over the output.
1. Repository (per D-01 and D-43): `mkdir -p /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app`, then `git init -b master` inside `vue-summercmsio-app` only. Add `.gitignore` (`node_modules/`, `.nuxt/`, `.output/`, `.data/`, `dist`, `.env`), `.nvmrc` with `22.22.0`, and a short `README.md` (what the site is, `pnpm install --frozen-lockfile`, `pnpm generate`, `pnpm test`, `scripts/derive-images.sh`, and that the root app embeds `.output/public`).
2. Packages (T-11.2-SC): `package.json` with `"name": "vue-summercmsio-app"`, `"private": true`, `"type": "module"`, `"engines": { "node": ">=22.6" }`, scripts `build` (`nuxt build`), `dev` (`nuxt dev`), `generate` (`nuxt generate`), `preview` (`nuxt preview`), `prepare` (`nuxt prepare`), `test` (`node --test tests/*.test.ts`), and no install-time lifecycle script. Install with exact pins via `pnpm add --save-exact nuxt@4.4.8 @nuxt/fonts@0.14.0 @nuxtjs/i18n@10.4.0 @nuxtjs/seo@5.2.1 vue@3.5.35` and `pnpm add -D --save-exact typescript@5.9.3`. These are the versions running in vue-fonoteka-app and approved in RESEARCH "Package Legitimacy Audit". If pnpm cannot resolve any of these exact versions, stop and return a `checkpoint:human-verify` with `gate="blocking-human"` naming the package and version instead of installing another version. Copy fonoteka's `pnpm-workspace.yaml` `allowBuilds` entries for `@parcel/watcher`, `esbuild` and `vue-demi` only (no protobufjs, no sharp, no patchedDependencies). Commit `pnpm-lock.yaml`. Add `tsconfig.json` extending `./.nuxt/tsconfig.json` as fonoteka does.
3. `nuxt.config.ts` (the spike-verified config in RESEARCH Pattern 1, per D-04, D-33, D-34, D-37): `compatibilityDate: '2025-07-15'`, `devtools: { enabled: false }`, `modules: ['@nuxt/fonts', '@nuxtjs/i18n', '@nuxtjs/seo']`, `css` loading `~/assets/css/tokens.css` then `~/assets/css/base.css`; `fonts.families` Roboto weights 300/400/500/700 and Roboto Mono weights 400/500, each with `styles: ['normal']`, `subsets: ['latin', 'latin-ext']`, `global: true` (Roboto 900 from the prototype is not loaded, per D-34); `i18n` with `strategy: 'prefix_except_default'`, `defaultLocale: 'en'`, one locale `{ code: 'en', language: 'en-US', file: 'en.json', name: 'English' }`, `langDir: 'locales/'`, `baseUrl: 'https://summercms.io'`, `experimental: { prerenderMessages: true }`; `site: { url: 'https://summercms.io', name: 'SummerCMS', description: <the hero description>, defaultLocale: 'en' }`; `ogImage: { enabled: false }`; `sitemap: { autoI18n: false }` (Pitfall 2: one flat file); `linkChecker: { excludeLinks: ['/docs', '/docs/**'] }`; `nitro: { prerender: { ignore: ['/docs'] } }` (Pitfall 1: generate otherwise exits 1 on /docs links); `app.head.link` with `/favicon.ico`, `/apple-touch-icon.png` (sizes 180x180).
4. `app/app.config.ts` per D-45 (both flags `true`). `app/app.vue` renders `<NuxtPage />`. `app/pages/index.vue` renders `SiteHeader` and `HeroSection` inside a wrapper whose horizontal overflow is clipped with `overflow-x: clip`, never `hidden` (Pitfall 3: a hidden-overflow ancestor breaks the sticky header), and sets the page head with `useSeoMeta`: title `SummerCMS: A new dawn in content management` with a `titleTemplate` that does not append the site name again (spike finding 4), `description` = the hero description, `ogImage` `/og-image.png` with width 1200, height 630 and alt text, `twitterCard: 'summary_large_image'`. Title, description and alt live in `en.json` under `meta.*`.
5. `i18n/locales/en.json` (D-33): nested keys for `meta`, `header` (logo aria label, wordmark parts "Summer" and "CMS", pill `ALPHA 0.1`, nav labels Why, Features, From WinterCMS, Get started, `Docs →`), `hero` (tagline "A new dawn in content management", description "A content management framework for Go, inspired by WinterCMS. Plugins, the admin SPA and console commands compile into a single binary.", CTA labels "Read the docs" and "Alpha 0.1 is out"). Copy every string verbatim from the handoff. Use no vue-i18n special characters (`@`, `|`, `{`, `}`) in values except deliberate slot placeholders (Pitfall 6).
6. Styles per D-35: `tokens.css` defines on `:root` the fourteen color tokens from RESEARCH "Tokens → :root custom properties" (`--navy-900` `#18223a` through `--text-4` `#8a94a3`), white border alphas 6/7/8/10/12/14% as `--border-06` … `--border-14`, radii `--radius-sm` 6px, `--radius-card` 16px, `--radius-pill` 999px, and font stacks `--font-sans` (Roboto, system-ui, sans-serif) and `--font-mono` (Roboto Mono, ui-monospace, monospace). `base.css` holds the globals from design/README.md "Global": page background `#233148`, text `#e9edf3`, `html { scroll-behavior: smooth; scroll-padding-top: 72px }` plus `@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto } }` (discretion choice), selection colors, link colors and hover, `.container` (max-width 1120px, padding 0 24px, auto margins), the shared section heading pattern (`.eyebrow`, `.section-title`), and the shared button shapes (primary, secondary, outline) and chip shape used by later sections. Component styles are `<style scoped>` and reference the tokens. No CSS framework package.
7. `SiteHeader.vue` per design/README.md "1. Header" and the dc.html details: sticky 64px bar, translucent navy with blur, logo link to `#top` (30px sun image `/sun-logo.webp` sized to match the handoff visually, wordmark "Summer" white plus "CMS" `#fbd77e`), the `ALPHA 0.1` pill, the section links in a `flex: 1; min-width: 0; overflow-x: auto; scrollbar-width: none` wrapper with the inner row pushed right, and the `Docs →` button linking `/docs` with the arrow in its own span. Section links are always rendered and hidden by `@media (max-width: 720px)` (Pitfall 4). Give each nav link a `data-section` attribute; active styling is wired in Task 3.
8. `HeroSection.vue` per "2. Hero": `<section id="top">`, the rays layer rendered only when `useAppConfig().landing.showRays` is true (D-45), the glow, the 180px sun badge holding `/sun-badge.webp` at about 136px (RESEARCH Open Question 5), the H1 with the gradient "CMS", tagline, description, and the two CTAs (`/docs` and `#start`, the secondary with its 8px dot).
9. Images per D-36 with `scripts/derive-images.sh` (bash, `set -euo pipefail`, ImageMagick `magick` only, run from the SITE root): download the original once with `curl -fsSL https://summercms.io/logo.png -o assets-src/logo.png` and commit it (the live URL disappears at cutover), check it is 746x744 (`magick identify`), then write `public/sun-logo.webp` (60x60, 2x of 30px), `public/sun-badge.webp` (272x272, 2x of 136px), `public/favicon.ico` (16, 32 and 48 px layers), `public/apple-touch-icon.png` (180x180 on `#233148`), and `public/og-image.png` (1200x630 on `#233148` with the sun, the wordmark "Summer" white and "CMS" `#fbd77e`, and the tagline in `#c5ccd6`, set in Roboto from `/usr/share/fonts/TTF/`). Commit the script and every output. Do not copy `design/assets/sun-crop.png`.
10. `tests/output.test.ts` (node:test; every test file resolves paths from `import.meta.url`, never from the working directory, so it runs from any cwd; reads files under `.output/public` and fails with a clear message when `index.html` is missing): `index.html` contains `<section id="top"`, the hero tagline and description, `href="/docs"`, a `<link rel="canonical" href="https://summercms.io/"`, `og:image` with content `https://summercms.io/og-image.png`, `twitter:card` `summary_large_image`, `<html lang="en` and a JSON-LD `@graph`; `_fonts/` holds at least one `.woff2`; `robots.txt` contains `Sitemap: https://summercms.io/sitemap.xml`; `sitemap.xml` is a regular file containing `<loc>https://summercms.io/</loc>`; at least one `_i18n/**/en/messages.json` exists (Pitfall 7 contract for plan 11.2-02); no generated `.html` or `.css` file mentions either Google font host named in the acceptance criteria. Build the en.json leaf check as a helper reused by later tasks: every leaf string without a `{` placeholder, HTML-escaped the way Vue escapes text (`&`, `<`, `>`, `"`, `'`), appears in `index.html`.
Commit as `feat(site): scaffold the Nuxt 4 landing page with header, hero, fonts and SEO`.
</action>
<verify>
<automated>pnpm -C ../sm-summercmsio-app/vue-summercmsio-app install --frozen-lockfile && pnpm -C ../sm-summercmsio-app/vue-summercmsio-app run generate && pnpm -C ../sm-summercmsio-app/vue-summercmsio-app test</automated>
<fails_when>non-zero exit, "Exiting due to prerender errors" in the generate log, a "not ok" line in the node:test TAP output, or "# fail" with a count above 0</fails_when>
</verify>
<acceptance_criteria>
- `node -p "const p=require('/media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/package.json'); [p.dependencies.nuxt,p.dependencies['@nuxt/fonts'],p.dependencies['@nuxtjs/i18n'],p.dependencies['@nuxtjs/seo'],p.dependencies.vue,p.devDependencies.typescript].join(' ')"` prints `4.4.8 0.14.0 10.4.0 5.2.1 3.5.35 5.9.3`.
- `test -f ../sm-summercmsio-app/vue-summercmsio-app/.output/public/index.html && test -f ../sm-summercmsio-app/vue-summercmsio-app/.output/public/sitemap.xml` succeeds (a regular file, not a directory).
- `ls ../sm-summercmsio-app/vue-summercmsio-app/.output/public/_fonts/*.woff2 | wc -l` prints at least 1.
- `grep -c "fonts.googleapis.com\|fonts.gstatic.com" ../sm-summercmsio-app/vue-summercmsio-app/.output/public/index.html` prints 0.
- `grep -n "ignore: \['/docs'\]" ../sm-summercmsio-app/vue-summercmsio-app/nuxt.config.ts` finds a match.
- `magick identify ../sm-summercmsio-app/vue-summercmsio-app/public/og-image.png` reports `1200x630`, and `magick identify ../sm-summercmsio-app/vue-summercmsio-app/assets-src/logo.png` reports `746x744`.
- `git -C ../sm-summercmsio-app/vue-summercmsio-app log --format='%(trailers:key=Co-authored-by,valueonly)' master` prints only empty lines.
</acceptance_criteria>
<done>A new vue-summercmsio-app repository generates a prerendered page with the handoff header and hero from en.json, self-hosted Roboto fonts, the original sun in every derived image, static SEO tags, flat sitemap and robots, and its output test passes.</done>
</task>
<task type="auto">
<name>Task 2: Visitors read the Why, Features and From WinterCMS sections and the footer, with every docs, source and credit link from the handoff</name>
<files>../sm-summercmsio-app/vue-summercmsio-app/app/components/WhySection.vue, ../sm-summercmsio-app/vue-summercmsio-app/app/components/FeaturesSection.vue, ../sm-summercmsio-app/vue-summercmsio-app/app/components/WinterSection.vue, ../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteFooter.vue, ../sm-summercmsio-app/vue-summercmsio-app/app/pages/index.vue, ../sm-summercmsio-app/vue-summercmsio-app/app/assets/css/base.css, ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json, ../sm-summercmsio-app/vue-summercmsio-app/tests/output.test.ts</files>
<read_first>
- ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json, app/pages/index.vue, app/assets/css/base.css and tests/output.test.ts (as written in Task 1)
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/design/README.md sections "Section heading pattern", "3. Why", "4. Features", "5. From WinterCMS", "7. Footer"
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/design/SummerCMS Landing.dc.html (the same sections: grids, gaps, concept-row borders, footer spacer)
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md "Every link on the page" table and Pitfall 6
</read_first>
<action>
Add the three content sections and the footer as components, all copy in `en.json` (D-33), styled with the tokens (D-35), matching the handoff exactly (D-23).
1. `WhySection.vue` (`<section id="why">`): eyebrow `WHY SUMMER`, H2 "What you tested is exactly what you deploy." (max-width 720px), and the three cards (number in Roboto Mono, title, body) on a `repeat(auto-fit, minmax(260px, 1fr))` grid. Card 2's body has inline code for `fields.yaml` and `columns.yaml` (Roboto Mono 13px, `#e9edf3`): render it with the `<i18n-t>` component and named slots for the two file names (Pitfall 6). Do not use Vue's raw-HTML binding directive anywhere in the project (T-11.2-01).
2. `FeaturesSection.vue` (`<section id="features">`, full-bleed `#1f2b42` band with top and bottom borders): eyebrow `WHAT'S IN THE BOX`, H2 "The services a content app needs.", and the eight tiles as links on the 1px-divider grid (`gap: 1px` over the border color, outer border, radius 16px, `overflow: hidden`). Tile hrefs per D-47, exactly as the handoff writes them, extension-less: `/docs/backend/admin-spa`, `/docs/database/models`, `/docs/services/routing`, `/docs/services/jobs`, `/docs/services/realtime`, `/docs/services/search`, `/docs/services/mail`, `/docs/console/introduction`. Keep tile titles and bodies verbatim, including "the caller’s transaction" with its typographic apostrophe. Keep the hrefs in the component (or a small typed array next to it) and the copy in en.json.
3. `WinterSection.vue` (`<section id="winter">`), per D-09 with no added seasons copy: two columns `repeat(auto-fit, minmax(300px, 1fr))`, gap 48px; left column eyebrow `COMING FROM WINTERCMS`, H2 "Same shape. Compiled and typed.", the paragraph, and "See the full concept map →" linking `/docs/setup/coming-from-wintercms` (`display: inline-flex; margin-top: 24px`); right column the concept table with header `WINTERCMS | SUMMERCMS` and the seven rows from design/README.md (`minmax(0,1fr) minmax(0,1.3fr)` columns, Roboto Mono 13px, every row including the last with a bottom divider).
4. `SiteFooter.vue`: top border, 32px 24px padding, 13px `#8a94a3`; left "© 2026 SummerCMS. Something bright is here."; a `flex: 1` spacer; the link group (gap 20px, `#a3adbd`, hover `#f5c55a`) with Docs → `/docs`, Source → `https://git.golem15.com/golem15/summercms` (D-38), the D-12 credit "Built by Golem15" → `https://golem15.com` styled as a footer link, and "Back to top ↑" → `#top`.
5. `index.vue` renders header, hero, Why, Features, Winter, then the footer (the Get started section is added in Task 3 between Winter and the footer).
6. Extend `tests/output.test.ts`: section ids `why`, `features`, `winter` present after `top`; the eight tile hrefs plus `/docs/setup/coming-from-wintercms` each appear as `href="…"`; the Source href and `https://golem15.com` appear; the en.json leaf check passes for the new keys; the seven concept rows (both columns) appear.
Commit as `feat(site): add the Why, Features and From WinterCMS sections and the footer`.
</action>
<verify>
<automated>pnpm -C ../sm-summercmsio-app/vue-summercmsio-app run generate && pnpm -C ../sm-summercmsio-app/vue-summercmsio-app test</automated>
<fails_when>non-zero exit, "Exiting due to prerender errors" in the generate log, or a "not ok" line in the TAP output</fails_when>
</verify>
<acceptance_criteria>
- `grep -o 'href="/docs/[a-z/-]*"' ../sm-summercmsio-app/vue-summercmsio-app/.output/public/index.html | sort -u | wc -l` prints at least 9 (eight tiles plus the concept map).
- `grep -c 'href="https://golem15.com' ../sm-summercmsio-app/vue-summercmsio-app/.output/public/index.html` prints at least 1.
- `grep -c 'caller’s transaction' ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json` prints 1.
- `grep -rn 'v-html' ../sm-summercmsio-app/vue-summercmsio-app/app` prints nothing.
- `grep -c '<i18n-t' ../sm-summercmsio-app/vue-summercmsio-app/app/components/WhySection.vue` prints at least 1.
</acceptance_criteria>
<done>The Why, Features and From WinterCMS sections and the footer render from en.json with the handoff's links (extension-less docs hrefs, Source, Golem15 credit), and the output test covers them.</done>
</task>
<task type="auto" tdd="true">
<name>Task 3: Visitors copy six working commands from the Get started terminal card, and the header highlights the section they are reading</name>
<files>../sm-summercmsio-app/vue-summercmsio-app/app/components/StartSection.vue, ../sm-summercmsio-app/vue-summercmsio-app/app/components/TerminalCard.vue, ../sm-summercmsio-app/vue-summercmsio-app/app/data/terminal.json, ../sm-summercmsio-app/vue-summercmsio-app/app/utils/terminal.ts, ../sm-summercmsio-app/vue-summercmsio-app/app/utils/scrollSpy.ts, ../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue, ../sm-summercmsio-app/vue-summercmsio-app/app/pages/index.vue, ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json, ../sm-summercmsio-app/vue-summercmsio-app/tests/terminal.test.ts, ../sm-summercmsio-app/vue-summercmsio-app/tests/scrollSpy.test.ts, ../sm-summercmsio-app/vue-summercmsio-app/tests/output.test.ts</files>
<read_first>
- ../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue, app/pages/index.vue, i18n/locales/en.json and tests/output.test.ts (as written in Tasks 1-2)
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/design/README.md sections "6. Get started", "Interactions & Behavior", "State"
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/design/SummerCMS Landing.dc.html (terminal title bar, line blocks, group spacing, chips row, buttons row)
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md "Pattern 3: Single-source terminal commands", "Breakpoints/interactions", Pitfall 5
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-CONTEXT.md D-26, D-38, D-39, D-40, D-45
</read_first>
<behavior>
- copyPayload over the three terminal.json groups returns exactly `git clone https://git.golem15.com/golem15/summercms\ncd summercms\ngo install ./cmd/summer\ncd examples/hello\nsummer build\n./bin/hello greeter:hello` (six lines, no trailing newline, no `$`, no `#`).
- copyPayload over an empty list returns `''`; over a group with no commands contributes nothing.
- terminal.json has three groups, six commands, and each group's `comment` key resolves to a string in en.json.
- activeSection([]) is `''`; when every top is at or above 140 it is `''`; with why at 100 and features at 300 it is `why`; with why -900, features -200, winter 120, start 600 it is `winter`; a top of exactly 140 is not active; a custom threshold is honoured.
- The generated index.html contains `id="start"`, the chips `Go 1.27` and `PostgreSQL 15+`, each of the six commands, the three comment texts, `href="/docs/setup/installation"`, and the Source href.
</behavior>
<action>
Write the tests from the behavior block first (they fail), then implement until they pass.
1. `app/data/terminal.json` exactly as the interfaces block defines it (D-39: the clone step comes first; D-40: this file is the single source the page renders and plan 11.2-02's Go check reads). The comment keys resolve in `en.json` to `get the framework`, `install the summer CLI` and `build and run the example app`; the component prints them after `# `.
2. `app/utils/terminal.ts` and `app/utils/scrollSpy.ts` exactly as the interfaces block defines them: pure functions, no Vue or Nuxt imports, erasable TypeScript only, so `node --test` imports them directly with explicit `.ts` specifiers.
3. `TerminalCard.vue` per "6. Get started → terminal card": title bar (`terminal` label, Copy button 500 12px Roboto, radius 6px), body padding 22px 20px, Roboto Mono 14px, line-height 1.9, `overflow-x: auto`, flex column; each line is its own block with `white-space: pre`; comment lines `#8a94a3`; the `$ ` prompt `#f5c55a` in its own span so the command text stays contiguous in the HTML; each group's comment after the first has `margin-top: 16px`. Copy calls `navigator.clipboard?.writeText(copyPayload(groups))` and falls back to a hidden textarea plus `document.execCommand('copy')` when the Clipboard API is unavailable (Pitfall 5); on success the label shows the en.json `Copied` text for 1500 ms, then `Copy`; a failed copy leaves the label unchanged.
4. `StartSection.vue` (`<section id="start">`, full-bleed `#1f2b42` band with a top border): two columns as in the Winter section with `align-items: center`; left column eyebrow `GET STARTED`, H2 "Build your first app.", the chips row (`margin-top: 24px; gap: 8px`) with `Go 1.27` and `PostgreSQL 15+` (D-26, following D-25; plan 11.2-02 re-runs the PG 15 suites and stops if they fail), the note "Alpha 0.1 is for early adopters. APIs may change before 1.0." (`margin-top: 20px`, 16px/1.65), and the buttons row (`margin-top: 28px`): "Installation guide" primary → `/docs/setup/installation` (padding 13px 24px; extension-less per D-47) and "Source" outline → `https://git.golem15.com/golem15/summercms` (D-38; padding 12px 22px, border 14% white, hover background 8% white and text `#fff`). Right column the TerminalCard. Insert it in `index.vue` between Winter and the footer.
5. Scroll-spy in `SiteHeader.vue` (D-45): when `useAppConfig().landing.scrollSpy` is true, `onMounted` computes the active id from `getBoundingClientRect().top` of the four sections via `activeSection`, then on every `scroll` event with `{ passive: true }`; `onBeforeUnmount` removes the listener. The active link gets `#f5c55a` text and `rgba(245,197,90,0.1)` background. SSR renders no active link (Pitfall 4). With the flag false no listener is attached.
6. Extend `tests/output.test.ts` with the last behavior bullet (it reads the six commands and three comment keys from `terminal.json` and `en.json`, never from a second hard-coded list) and with the order check `top` < `why` < `features` < `winter` < `start` < footer.
Commit the tests and the implementation as `feat(site): add the Get started terminal card, copy button and scroll-spy`.
</action>
<verify>
<automated>pnpm -C ../sm-summercmsio-app/vue-summercmsio-app run generate && pnpm -C ../sm-summercmsio-app/vue-summercmsio-app test</automated>
<fails_when>non-zero exit, a "not ok" line in the TAP output, or the TAP summary missing the terminal and scrollSpy test files</fails_when>
<automated>node --test ../sm-summercmsio-app/vue-summercmsio-app/tests/terminal.test.ts ../sm-summercmsio-app/vue-summercmsio-app/tests/scrollSpy.test.ts</automated>
<fails_when>non-zero exit, "# pass 0", or "# fail" with a count above 0</fails_when>
<human-check>Run `pnpm -C ../sm-summercmsio-app/vue-summercmsio-app run preview` and open the printed URL next to `design/SummerCMS Landing.dc.html` in a browser. At 1280px, 721px, 720px and 375px wide: layout, colors, type and spacing match; the header stays stuck while scrolling; section links are visible at 721px and hidden at 720px; the active link follows the section in view and none is active above Why; Copy changes to "Copied" for about 1.5s and the clipboard holds six lines without `$` or comments; the hero rays and the sun badge match the handoff.</human-check>
</verify>
<acceptance_criteria>
- `node -e "const t=require('/media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/app/data/terminal.json'); console.log(t.groups.length, t.groups.flatMap(g=>g.commands).length)"` prints `3 6`.
- `grep -c 'PostgreSQL 15+' ../sm-summercmsio-app/vue-summercmsio-app/.output/public/index.html` prints at least 1.
- `grep -c './bin/hello greeter:hello' ../sm-summercmsio-app/vue-summercmsio-app/.output/public/index.html` prints at least 1.
- `grep -n "@media (max-width: 720px)" ../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue` finds a match.
- `grep -n 'passive: true' ../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue` finds a match.
- `grep -n 'scrollSpy' ../sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue` and `grep -n 'showRays' ../sm-summercmsio-app/vue-summercmsio-app/app/components/HeroSection.vue` both find a match.
</acceptance_criteria>
<done>The Get started section shows the D-26 chips, the D-38 links and the D-39 terminal card fed from terminal.json; Copy writes the six commands; scroll-spy follows the handoff rule behind the D-45 flag; unit and output tests pass.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| npm registry → build machine | Third-party packages execute during install and generate |
| en.json / terminal.json → rendered HTML | Repository copy becomes public HTML; authors are trusted, mistakes are expected |
| page → visitor clipboard | The Copy button writes text the visitor will paste into a shell |
| page → third-party origins | Any runtime request to another origin leaks visitor data |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-11.2-01 | Tampering (XSS) | components rendering en.json | medium | mitigate | Text interpolation only; inline code through `<i18n-t>` slots; Task 2 criterion greps that no raw-HTML binding exists under app/ |
| T-11.2-02 | Information disclosure (privacy) | font loading | medium | mitigate | @nuxt/fonts downloads at build and serves from `/_fonts/` (D-34); the output test and a criterion assert no Google font host in the generated HTML or CSS |
| T-11.2-03 | Tampering | Copy button payload | low | accept | Payload is the bundled terminal.json, identical to the visible lines; the page only writes to the clipboard and never reads it |
| T-11.2-SC | Tampering | pnpm installs | high | mitigate | Exact pins equal to vue-fonoteka-app's installed versions, approved in RESEARCH Package Legitimacy Audit; committed pnpm-lock.yaml and `--frozen-lockfile`; allowBuilds limited to three packages; any other version stops at a blocking-human checkpoint |
</threat_model>
<verification>
- `pnpm -C ../sm-summercmsio-app/vue-summercmsio-app install --frozen-lockfile && pnpm -C ../sm-summercmsio-app/vue-summercmsio-app run generate && pnpm -C ../sm-summercmsio-app/vue-summercmsio-app test` green.
- `git -C ../sm-summercmsio-app/vue-summercmsio-app status --porcelain` prints nothing after the last commit.
- `git -C . status --porcelain` in summercms.go shows no change from this plan.
</verification>
<success_criteria>
- SC1: `nuxt generate` produces the landing page matching the handoff with sticky header and scroll-spy, hero, Why, Features, From WinterCMS, Get started, a working Copy button and the footer; responsive to phone width with section links hidden at 720px or less (visual parts confirmed in UAT).
- Inputs for SC2 and SC3: the `.output/public` tree and `app/data/terminal.json` that plan 11.2-02 embeds, link-checks and runs.
</success_criteria>
## Artifacts this phase produces
- Repository `vue-summercmsio-app` at `/media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app` (branch `master`, no remote yet; D-43).
- Config: `nuxt.config.ts` (modules `@nuxt/fonts`, `@nuxtjs/i18n`, `@nuxtjs/seo`; `nitro.prerender.ignore`, `sitemap.autoI18n: false`, `ogImage.enabled: false`, `linkChecker.excludeLinks`, `i18n.experimental.prerenderMessages`), `app/app.config.ts` keys `landing.showRays`, `landing.scrollSpy`, `package.json` scripts `build`, `dev`, `generate`, `preview`, `prepare`, `test`.
- Components: `SiteHeader`, `HeroSection`, `WhySection`, `FeaturesSection`, `WinterSection`, `StartSection`, `TerminalCard`, `SiteFooter`; page `app/pages/index.vue`.
- TypeScript: `TerminalGroup`, `copyPayload` (`app/utils/terminal.ts`); `SPY_SECTIONS`, `SPY_THRESHOLD`, `activeSection` (`app/utils/scrollSpy.ts`).
- Data and copy: `app/data/terminal.json` (`groups[].comment`, `groups[].commands`), `i18n/locales/en.json` (keys `meta.*`, `header.*`, `hero.*`, `why.*`, `features.*`, `winter.*`, `start.*`, `terminal.*`, `footer.*`).
- Styles: `app/assets/css/tokens.css` (`--navy-900` … `--text-4`, `--border-06` … `--border-14`, `--radius-sm`, `--radius-card`, `--radius-pill`, `--font-sans`, `--font-mono`), `app/assets/css/base.css`.
- Images: `assets-src/logo.png`, `public/sun-logo.webp`, `public/sun-badge.webp`, `public/favicon.ico`, `public/apple-touch-icon.png`, `public/og-image.png`; script `scripts/derive-images.sh`.
- Tests: `tests/output.test.ts`, `tests/terminal.test.ts`, `tests/scrollSpy.test.ts`.
<output>
Create `.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-01-SUMMARY.md` when done
</output>