docs(11.2): create phase plan

This commit is contained in:
Jakub Zych
2026-10-01 15:40:27 +02:00
parent b6013a81ed
commit 88f21af923
7 changed files with 1128 additions and 13 deletions

View File

@@ -586,10 +586,17 @@ Plans:
**Context:** `.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-CONTEXT.md` (D-01 to D-05, D-07, D-09 and D-12 still apply; see its supersession note)
**Plans:** 0 plans
**Plans:** 3 plans
Plans:
- [ ] TBD (run /gsd-plan-phase 11.2 to break down)
**Wave 1**
- [ ] 11.2-01-PLAN.md — vue-summercms-app: Nuxt 4 landing page matching the handoff (en-only i18n, @nuxt/fonts, @nuxtjs/seo with static OG, sun assets from logo.png, single-source terminal commands, Copy, scroll-spy, app config flags)
**Wave 2** *(blocked on Wave 1 completion)*
- [ ] 11.2-02-PLAN.md — Framework PG15 verification and site_url/site_label (D-25, D-41, D-46), v0.1.0 tag checkpoint (D-42), sm-summercms-plugin static serving at / and /docs (D-07, D-47), sm-summercms-app wiring, build and smoke scripts, link and terminal checks (SC3, D-40), DEPLOY.md with nginx, supervisor and rollback
**Wave 3** *(blocked on Wave 2 completion)*
- [ ] 11.2-03-PLAN.md — Unit tests last: plugin rule table at 90%+ coverage, site_url/site_label branches, terminal-check helpers, check-phase11.2.sh gate, validated VALIDATION.md
### Phase 11.3: Newsletter plugin and signup on summercms.io (INSERTED)

View File

@@ -1,18 +1,18 @@
---
gsd_state_version: "1.0"
milestone: v1.0
current_phase: "11.1"
current_phase_name: SummerCMS documentation for humans and AI agents (INSERTED)
status: verifying
current_phase: "11.2"
current_phase_name: summercms.io Alpha 0.1 landing page on SummerCMS
status: executing
stopped_at: Phase 11.2 context gathered
last_updated: "2026-10-01T12:33:55.152Z"
last_updated: "2026-10-01T13:40:22.087Z"
last_activity: 2026-10-01
last_activity_desc: Phase 11.1 execution started
state_head: 3cbc8d32d15ca53ad9c81b8a202137bfb39bd133
state_head: b6013a81edf0fd2c7c9254c865b359065b44652d
progress:
total_phases: 20
completed_phases: 9
total_plans: 93
total_plans: 96
completed_plans: 93
milestone_name: milestone
---
@@ -28,9 +28,9 @@ See: .planning/PROJECT.md (updated 2026-09-16)
## Current Position
Phase: 11.1 (SummerCMS documentation for humans and AI agents (INSERTED)) — EXECUTING
Phase: 11.2 (summercms.io Alpha 0.1 landing page on SummerCMS) — READY TO EXECUTE
Plan: 7 of 7
Status: Phase complete — ready for verification
Status: Ready to execute
Last activity: 2026-10-01 - Completed quick task 261001-ddh: Replace lagoon hardcoded ICU pl-PL database locale with per-query COLLATE option
Progress: [██████░░░░] 60%

View File

@@ -0,0 +1,336 @@
---
phase: 11.2-ready-to-share-summercms-io-website-and-newsletter-plugin
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- ../sm-summercms-app/vue-summercms-app/package.json
- ../sm-summercms-app/vue-summercms-app/pnpm-lock.yaml
- ../sm-summercms-app/vue-summercms-app/pnpm-workspace.yaml
- ../sm-summercms-app/vue-summercms-app/.nvmrc
- ../sm-summercms-app/vue-summercms-app/.gitignore
- ../sm-summercms-app/vue-summercms-app/README.md
- ../sm-summercms-app/vue-summercms-app/tsconfig.json
- ../sm-summercms-app/vue-summercms-app/nuxt.config.ts
- ../sm-summercms-app/vue-summercms-app/app/app.config.ts
- ../sm-summercms-app/vue-summercms-app/app/app.vue
- ../sm-summercms-app/vue-summercms-app/app/pages/index.vue
- ../sm-summercms-app/vue-summercms-app/app/components/SiteHeader.vue
- ../sm-summercms-app/vue-summercms-app/app/components/HeroSection.vue
- ../sm-summercms-app/vue-summercms-app/app/components/WhySection.vue
- ../sm-summercms-app/vue-summercms-app/app/components/FeaturesSection.vue
- ../sm-summercms-app/vue-summercms-app/app/components/WinterSection.vue
- ../sm-summercms-app/vue-summercms-app/app/components/StartSection.vue
- ../sm-summercms-app/vue-summercms-app/app/components/TerminalCard.vue
- ../sm-summercms-app/vue-summercms-app/app/components/SiteFooter.vue
- ../sm-summercms-app/vue-summercms-app/app/assets/css/tokens.css
- ../sm-summercms-app/vue-summercms-app/app/assets/css/base.css
- ../sm-summercms-app/vue-summercms-app/app/data/terminal.json
- ../sm-summercms-app/vue-summercms-app/app/utils/terminal.ts
- ../sm-summercms-app/vue-summercms-app/app/utils/scrollSpy.ts
- ../sm-summercms-app/vue-summercms-app/i18n/locales/en.json
- ../sm-summercms-app/vue-summercms-app/assets-src/logo.png
- ../sm-summercms-app/vue-summercms-app/scripts/derive-images.sh
- ../sm-summercms-app/vue-summercms-app/public/sun-logo.webp
- ../sm-summercms-app/vue-summercms-app/public/sun-badge.webp
- ../sm-summercms-app/vue-summercms-app/public/favicon.ico
- ../sm-summercms-app/vue-summercms-app/public/apple-touch-icon.png
- ../sm-summercms-app/vue-summercms-app/public/og-image.png
- ../sm-summercms-app/vue-summercms-app/tests/output.test.ts
- ../sm-summercms-app/vue-summercms-app/tests/terminal.test.ts
- ../sm-summercms-app/vue-summercms-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-summercms-app/vue-summercms-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-summercms-app/vue-summercms-app/nuxt.config.ts"
provides: "Nuxt 4 SSG config: fonts, i18n, seo, prerender ignore for /docs"
contains: "prerender"
- path: "../sm-summercms-app/vue-summercms-app/app/app.config.ts"
provides: "D-45 landing flags"
contains: "scrollSpy"
- path: "../sm-summercms-app/vue-summercms-app/i18n/locales/en.json"
provides: "all page copy (D-33)"
contains: "A new dawn in content management"
- path: "../sm-summercms-app/vue-summercms-app/app/data/terminal.json"
provides: "single source of the six terminal commands (D-40)"
contains: "./bin/hello greeter:hello"
- path: "../sm-summercms-app/vue-summercms-app/app/components/TerminalCard.vue"
provides: "terminal card with Copy"
contains: "copyPayload"
- path: "../sm-summercms-app/vue-summercms-app/app/utils/scrollSpy.ts"
provides: "pure scroll-spy selection"
contains: "export function activeSection"
- path: "../sm-summercms-app/vue-summercms-app/app/utils/terminal.ts"
provides: "pure copy payload builder"
contains: "export function copyPayload"
- path: "../sm-summercms-app/vue-summercms-app/tests/output.test.ts"
provides: "SC1 assertions on the generated output"
contains: "index.html"
- path: "../sm-summercms-app/vue-summercms-app/scripts/derive-images.sh"
provides: "D-36 image derivation from the original sun"
contains: "magick"
key_links:
- from: "../sm-summercms-app/vue-summercms-app/app/components/TerminalCard.vue"
to: "../sm-summercms-app/vue-summercms-app/app/data/terminal.json"
via: "static JSON import; the card renders and copies only these commands"
pattern: "data/terminal\\.json"
- from: "../sm-summercms-app/vue-summercms-app/app/components/TerminalCard.vue"
to: "../sm-summercms-app/vue-summercms-app/app/utils/terminal.ts"
via: "copyPayload(groups) builds the clipboard text"
pattern: "copyPayload\\("
- from: "../sm-summercms-app/vue-summercms-app/app/components/SiteHeader.vue"
to: "../sm-summercms-app/vue-summercms-app/app/utils/scrollSpy.ts"
via: "scroll listener calls activeSection with section tops"
pattern: "activeSection\\("
- from: "../sm-summercms-app/vue-summercms-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-summercms-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-summercms-app/vue-summercms-app` (it becomes a submodule of `sm-summercms-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-summercms-app/vue-summercms-app` (absolute: `/media/nvme/dev/golem15/summercms.io/summercms/sm-summercms-app/vue-summercms-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-summercms-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-summercms-app/vue-summercms-app/package.json, ../sm-summercms-app/vue-summercms-app/pnpm-lock.yaml, ../sm-summercms-app/vue-summercms-app/pnpm-workspace.yaml, ../sm-summercms-app/vue-summercms-app/.nvmrc, ../sm-summercms-app/vue-summercms-app/.gitignore, ../sm-summercms-app/vue-summercms-app/README.md, ../sm-summercms-app/vue-summercms-app/tsconfig.json, ../sm-summercms-app/vue-summercms-app/nuxt.config.ts, ../sm-summercms-app/vue-summercms-app/app/app.config.ts, ../sm-summercms-app/vue-summercms-app/app/app.vue, ../sm-summercms-app/vue-summercms-app/app/pages/index.vue, ../sm-summercms-app/vue-summercms-app/app/components/SiteHeader.vue, ../sm-summercms-app/vue-summercms-app/app/components/HeroSection.vue, ../sm-summercms-app/vue-summercms-app/app/assets/css/tokens.css, ../sm-summercms-app/vue-summercms-app/app/assets/css/base.css, ../sm-summercms-app/vue-summercms-app/i18n/locales/en.json, ../sm-summercms-app/vue-summercms-app/assets-src/logo.png, ../sm-summercms-app/vue-summercms-app/scripts/derive-images.sh, ../sm-summercms-app/vue-summercms-app/public/sun-logo.webp, ../sm-summercms-app/vue-summercms-app/public/sun-badge.webp, ../sm-summercms-app/vue-summercms-app/public/favicon.ico, ../sm-summercms-app/vue-summercms-app/public/apple-touch-icon.png, ../sm-summercms-app/vue-summercms-app/public/og-image.png, ../sm-summercms-app/vue-summercms-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-summercms-app/vue-summercms-app`, then `git init -b master` inside `vue-summercms-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-summercms-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-summercms-app/vue-summercms-app install --frozen-lockfile && pnpm -C ../sm-summercms-app/vue-summercms-app run generate && pnpm -C ../sm-summercms-app/vue-summercms-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-summercms-app/vue-summercms-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-summercms-app/vue-summercms-app/.output/public/index.html && test -f ../sm-summercms-app/vue-summercms-app/.output/public/sitemap.xml` succeeds (a regular file, not a directory).
- `ls ../sm-summercms-app/vue-summercms-app/.output/public/_fonts/*.woff2 | wc -l` prints at least 1.
- `grep -c "fonts.googleapis.com\|fonts.gstatic.com" ../sm-summercms-app/vue-summercms-app/.output/public/index.html` prints 0.
- `grep -n "ignore: \['/docs'\]" ../sm-summercms-app/vue-summercms-app/nuxt.config.ts` finds a match.
- `magick identify ../sm-summercms-app/vue-summercms-app/public/og-image.png` reports `1200x630`, and `magick identify ../sm-summercms-app/vue-summercms-app/assets-src/logo.png` reports `746x744`.
- `git -C ../sm-summercms-app/vue-summercms-app log --format='%(trailers:key=Co-authored-by,valueonly)' master` prints only empty lines.
</acceptance_criteria>
<done>A new vue-summercms-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-summercms-app/vue-summercms-app/app/components/WhySection.vue, ../sm-summercms-app/vue-summercms-app/app/components/FeaturesSection.vue, ../sm-summercms-app/vue-summercms-app/app/components/WinterSection.vue, ../sm-summercms-app/vue-summercms-app/app/components/SiteFooter.vue, ../sm-summercms-app/vue-summercms-app/app/pages/index.vue, ../sm-summercms-app/vue-summercms-app/app/assets/css/base.css, ../sm-summercms-app/vue-summercms-app/i18n/locales/en.json, ../sm-summercms-app/vue-summercms-app/tests/output.test.ts</files>
<read_first>
- ../sm-summercms-app/vue-summercms-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-summercms-app/vue-summercms-app run generate && pnpm -C ../sm-summercms-app/vue-summercms-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-summercms-app/vue-summercms-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-summercms-app/vue-summercms-app/.output/public/index.html` prints at least 1.
- `grep -c 'caller’s transaction' ../sm-summercms-app/vue-summercms-app/i18n/locales/en.json` prints 1.
- `grep -rn 'v-html' ../sm-summercms-app/vue-summercms-app/app` prints nothing.
- `grep -c '<i18n-t' ../sm-summercms-app/vue-summercms-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-summercms-app/vue-summercms-app/app/components/StartSection.vue, ../sm-summercms-app/vue-summercms-app/app/components/TerminalCard.vue, ../sm-summercms-app/vue-summercms-app/app/data/terminal.json, ../sm-summercms-app/vue-summercms-app/app/utils/terminal.ts, ../sm-summercms-app/vue-summercms-app/app/utils/scrollSpy.ts, ../sm-summercms-app/vue-summercms-app/app/components/SiteHeader.vue, ../sm-summercms-app/vue-summercms-app/app/pages/index.vue, ../sm-summercms-app/vue-summercms-app/i18n/locales/en.json, ../sm-summercms-app/vue-summercms-app/tests/terminal.test.ts, ../sm-summercms-app/vue-summercms-app/tests/scrollSpy.test.ts, ../sm-summercms-app/vue-summercms-app/tests/output.test.ts</files>
<read_first>
- ../sm-summercms-app/vue-summercms-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-summercms-app/vue-summercms-app run generate && pnpm -C ../sm-summercms-app/vue-summercms-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-summercms-app/vue-summercms-app/tests/terminal.test.ts ../sm-summercms-app/vue-summercms-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-summercms-app/vue-summercms-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-summercms-app/vue-summercms-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-summercms-app/vue-summercms-app/.output/public/index.html` prints at least 1.
- `grep -c './bin/hello greeter:hello' ../sm-summercms-app/vue-summercms-app/.output/public/index.html` prints at least 1.
- `grep -n "@media (max-width: 720px)" ../sm-summercms-app/vue-summercms-app/app/components/SiteHeader.vue` finds a match.
- `grep -n 'passive: true' ../sm-summercms-app/vue-summercms-app/app/components/SiteHeader.vue` finds a match.
- `grep -n 'scrollSpy' ../sm-summercms-app/vue-summercms-app/app/components/SiteHeader.vue` and `grep -n 'showRays' ../sm-summercms-app/vue-summercms-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-summercms-app/vue-summercms-app install --frozen-lockfile && pnpm -C ../sm-summercms-app/vue-summercms-app run generate && pnpm -C ../sm-summercms-app/vue-summercms-app test` green.
- `git -C ../sm-summercms-app/vue-summercms-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-summercms-app` at `/media/nvme/dev/golem15/summercms.io/summercms/sm-summercms-app/vue-summercms-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>

View File

@@ -0,0 +1,489 @@
---
phase: 11.2-ready-to-share-summercms-io-website-and-newsletter-plugin
plan: 02
type: execute
wave: 2
depends_on: ["11.2-01"]
files_modified:
- README.md
- docs/setup/installation.md
- internal/docsite/load.go
- internal/docsite/docsite.go
- internal/docsite/emit.go
- internal/docsite/theme/templates/header.html
- internal/docsite/theme/assets/site.css
- internal/docsite/load_test.go
- internal/docsite/theme_test.go
- cmd/summer/docs.go
- cmd/summer/docs_test.go
- docs/console/utilities.md
- ../sm-summercms-app/plugins/golem15/summercms/go.mod
- ../sm-summercms-app/plugins/golem15/summercms/go.sum
- ../sm-summercms-app/plugins/golem15/summercms/plugin.go
- ../sm-summercms-app/plugins/golem15/summercms/static.go
- ../sm-summercms-app/plugins/golem15/summercms/smoke_test.go
- ../sm-summercms-app/plugins/golem15/summercms/links_test.go
- ../sm-summercms-app/plugins/golem15/summercms/public/README.md
- ../sm-summercms-app/plugins/golem15/summercms/.gitignore
- ../sm-summercms-app/plugins/golem15/summercms/README.md
- ../sm-summercms-app/go.mod
- ../sm-summercms-app/go.sum
- ../sm-summercms-app/go.work
- ../sm-summercms-app/summer.yaml
- ../sm-summercms-app/main.go
- ../sm-summercms-app/plugins.gen.go
- ../sm-summercms-app/config/app.yaml
- ../sm-summercms-app/config/http.yaml
- ../sm-summercms-app/config/storage.yaml
- ../sm-summercms-app/config/queue.yaml
- ../sm-summercms-app/config/database.yaml
- ../sm-summercms-app/.gitignore
- ../sm-summercms-app/.gitmodules
- ../sm-summercms-app/scripts/build.sh
- ../sm-summercms-app/scripts/smoke.sh
- ../sm-summercms-app/scripts/check-deploy.sh
- ../sm-summercms-app/terminal_check_test.go
- ../sm-summercms-app/README.md
- ../sm-summercms-app/DEPLOY.md
- ../sm-summercms-app/deploy/nginx/summercms.io.conf
- ../sm-summercms-app/deploy/supervisor/summercms-io.conf
- ../sm-summercms-app/deploy/env.example
- ../sm-summercms-app/deploy/rollback/under-construction/index.html
- ../sm-summercms-app/deploy/rollback/under-construction/logo.png
autonomous: false
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: 200000
raw_tokens: 200000
tasks: 5
confidence: low
must_haves:
truths:
- "Per D-01, D-02 and D-43, `sm-summercms-app` (module `git.golem15.com/golem15/sm-summercms-app`) is a git repository next to summercms.go, holding `vue-summercms-app` and `plugins/golem15/summercms` (module `git.golem15.com/golem15/sm-summercms-plugin`, plugin ID `golem15.summercms`) as submodules whose URLs are `git@git.golem15.com:golem15/<repo>.git`; creating those remotes is a listed user step."
- "Per D-03, the app is a go.work workspace (`use . ./plugins/golem15/summercms`) that requires the framework with `replace git.golem15.com/golem15/summercms => ../summercms.go`, the plugin requires it with `=> ../../../../summercms.go`, and `go list ./...` in the app lists only the app's own package."
- "Per D-05 and SC2, one `summercms-io` binary embeds the Nuxt output and the docs build through `//go:embed all:public` in the site plugin, and `scripts/build.sh` runs nuxt generate, then `summer docs:build --base-url /docs --site-url / --site-label summercms.io`, then the plugin tests, then `summer build` and a linux/amd64 `go build`."
- "Per D-07 and SC2, responses for `/` and `/docs/` carry no robots-blocking header and no Content-Security-Policy; `/_nuxt/*` (except `/_nuxt/builds/*`) and `/_fonts/*` get `public, max-age=31536000, immutable`; every other file, including `/docs/assets/*`, gets `no-cache` with a strong ETag, and a matching If-None-Match returns 304."
- "Per D-47, `GET /docs/<p>` answers 301 to `/docs/<p>.html` when that page exists, built only from the cleaned path; `GET /docs` answers 301 to `/docs/`; an unknown path gets its tree's `404.html` with status 404, and any dot-segment path (for example `/docs/.summer-docs`) is 404."
- "Per D-24, D-27 and D-29, the binary boots through the stock `serve` command against PostgreSQL 15 with `queue.work_in_serve: false`, numeric `http.body_limits`, a file uploads bucket and secrets only from a server-side `.env`; the site plugin declares no admin controllers, so `/backend` is a 404 and `POST /` is a 405 (smoke script)."
- "Per D-25, the framework's database suites pass against `postgres:15` (output recorded in the SUMMARY) before README.md and docs/setup/installation.md say `PostgreSQL 15 or newer`; if any suite fails on 15 the plan stops and asks the user."
- "Per D-41 and D-46, `docs/site.yaml` accepts optional `site_url` and `site_label`, `summer docs:build` and `summer docs:serve` accept `--site-url` and `--site-label` overrides, the docs header shows a link back to the main site only when a site URL is set (label: explicit, else the URL host, else `Home`), the framework's own docs output is byte-identical when nothing is set, and `docs/console/utilities.md` documents both keys and flags with a neutral example."
- "Per D-42, the framework is tagged `v0.1.0` only after the user confirms at a blocking-human checkpoint, at a commit where the D-25 and D-41/D-46 changes are green; the app and the plugin require `git.golem15.com/golem15/summercms v0.1.0`, and the release build takes the docs and the compiled framework from the tag export."
- "Per SC3 and D-44, `TestLandingLinks` resolves every href and src in the built index.html through the real assembled handler (one 301 followed, final 200), checks every in-page anchor, and requires all ten handoff `/docs/...` targets plus `/docs`; external links are checked anonymously by `TestExternalLinks` at cutover."
- "Per D-40, `TestTerminalCommands` runs the six commands from `vue-summercms-app/app/data/terminal.json` in a temp directory with a temp GOBIN first on PATH and no SUMMER_* or GOWORK variables, and sees `handled=true`; only the clone URL may be overridden (SUMMERCMS_CLONE_URL) until D-38 makes the repository public, and a drift test proves every command and comment appears in the built page."
- "Per SC4 and D-27 to D-32, DEPLOY.md and the committed nginx and supervisor configs cover the launch checklist, one-time server setup (system user, Postgres role `summercms` owning `summercms_io`, a 0600 `.env`), local build, rsync upload, migrate before restart, nginx with certbot TLS, HTTP to HTTPS and www to apex redirects, gzip, proxy headers and denied admin paths, the supervisor program on 127.0.0.1:8095, verification and rollback; `scripts/check-deploy.sh` passes `nginx -t`."
- statement: "Following DEPLOY.md on rome brings the site up at https://summercms.io (manual, at cutover)."
verification: backstop
artifacts:
- path: "../sm-summercms-app/plugins/golem15/summercms/plugin.go"
provides: "site plugin: ID, embed, routes for /, /docs and /docs/{path...}"
contains: "go:embed all:public"
- path: "../sm-summercms-app/plugins/golem15/summercms/static.go"
provides: "embedded static handler: resolution, MIME table, cache rules, ETag, 301s, 404s"
contains: "http.ServeContent"
- path: "../sm-summercms-app/plugins/golem15/summercms/links_test.go"
provides: "SC3 link check, D-40 drift guard, D-46 header check, external link check"
contains: "TestLandingLinks"
- path: "../sm-summercms-app/scripts/build.sh"
provides: "D-05/D-28 scripted build, release from the tag and dev"
contains: "docs:build --base-url /docs --site-url / --site-label summercms.io"
- path: "../sm-summercms-app/scripts/smoke.sh"
provides: "boot on postgres:15 and HTTP assertions"
contains: "postgres:15"
- path: "../sm-summercms-app/terminal_check_test.go"
provides: "D-40 verbatim terminal command check"
contains: "TestTerminalCommands"
- path: "../sm-summercms-app/DEPLOY.md"
provides: "SC4 deploy, launch checklist and rollback"
contains: "supervisorctl restart summercms-io"
- path: "../sm-summercms-app/deploy/nginx/summercms.io.conf"
provides: "nginx server blocks"
contains: "proxy_pass http://127.0.0.1:8095"
- path: "../sm-summercms-app/deploy/supervisor/summercms-io.conf"
provides: "supervisor program"
contains: "user=summercms"
- path: "internal/docsite/load.go"
provides: "Site.SiteURL, Site.SiteLabel, checkSiteURL, siteLabel"
contains: "yaml:\"site_url\""
- path: "internal/docsite/theme/templates/header.html"
provides: "conditional link back to the main site"
contains: "site-link"
- path: "cmd/summer/docs.go"
provides: "--site-url and --site-label on docs:build and docs:serve"
contains: "site-label"
key_links:
- from: "../sm-summercms-app/plugins.gen.go"
to: "../sm-summercms-app/plugins/golem15/summercms/plugin.go"
via: "generated blank import registers the plugin through its init"
pattern: "sm-summercms-plugin"
- from: "../sm-summercms-app/plugins/golem15/summercms/plugin.go"
to: "modules/surf router"
via: "Routes registers a raw group with GET /docs, GET /docs/{path...} and GET /"
pattern: "GroupRaw\\("
- from: "../sm-summercms-app/scripts/build.sh"
to: "../sm-summercms-app/plugins/golem15/summercms/public"
via: "rsync of .output/public into public/site and docs:build --out public/docs before go build"
pattern: "public/(site|docs)"
- from: "../sm-summercms-app/scripts/build.sh"
to: "summercms.go tag v0.1.0"
via: "git archive of the required tag feeds docs:build and a temporary go.work replace"
pattern: "git -C \"\\$FW\" archive"
- from: "cmd/summer/docs.go"
to: "internal/docsite/docsite.go"
via: "docsOptions copies --site-url and --site-label into Options"
pattern: "SiteURL"
- from: "../sm-summercms-app/terminal_check_test.go"
to: "../sm-summercms-app/vue-summercms-app/app/data/terminal.json"
via: "reads the single command list the page renders"
pattern: "app/data/terminal\\.json"
---
<objective>
Make summercms.io shippable: the small framework changes the site needs (D-25 PostgreSQL 15, D-41/D-46 docs link back to the site), the `v0.1.0` tag behind a user confirmation (D-42), the `sm-summercms-plugin` site plugin that serves the embedded Nuxt build at `/` and the docs at `/docs` with indexable responses and correct cache headers (D-01, D-05, D-07, D-47), the `sm-summercms-app` root app with its workspace, config, build and smoke scripts (D-02, D-03, D-24, D-28), the link and terminal-command checks (SC3, D-40, D-44), and DEPLOY.md with nginx, supervisor and rollback (SC4, D-27 to D-32, D-38, D-43).
Purpose: SC2, SC3 and SC4. Plan 11.2-03 adds the full unit-test coverage on top of the interfaces fixed here.
Output: framework commits in summercms.go (two, then the tag), and two new repositories, `sm-summercms-app` and its submodule `sm-summercms-plugin` (the plan 11.2-01 repository becomes the second submodule).
Task count: five tasks including the tag checkpoint, above the usual three, because the user fixed this phase at three plans (CLAUDE.md lean rule) and placed all of this scope in plan 02.
</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/11.2-01-SUMMARY.md
@modules/boardwalk/boardwalk.go
@internal/docsite/serve.go
@internal/docsite/load.go
@internal/docsite/emit.go
@internal/docsite/theme/templates/header.html
@cmd/summer/docs.go
@examples/hello/plugins/base/plugin.go
@../fonoteka.go/go.work
@../fonoteka.go/summer.yaml
**Paths.** Commands run from the summercms.go root (`FW`). `APP` = `../sm-summercms-app` (absolute `/media/nvme/dev/golem15/summercms.io/summercms/sm-summercms-app`), `PLUG` = `APP/plugins/golem15/summercms`, `SITE` = `APP/vue-summercms-app` (created by plan 11.2-01; this plan only reads it). Each task names the repository it commits to. Never `git add` in the meta repository `summercms/`.
**Commits.** One logical change per commit, conventional messages, never a co-author tag. In summercms.go, stage only the files a task lists (never `git add -A`); planning docs are committed separately by the orchestrator.
**Tools.** A `summer` binary for the app is installed into a temporary GOBIN, for example `GOBIN=$(mktemp -d) go -C <framework tree> install ./cmd/summer`; never rely on a `summer` already on PATH.
<interfaces>
Site plugin, package `summercms`, module `git.golem15.com/golem15/sm-summercms-plugin` (plan 11.2-03 tests these names; keep them stable):
- `//go:embed all:public` into `var publicFS embed.FS` (the `all:` prefix is required: Nuxt writes `_nuxt/`, `_fonts/`, `_i18n/` and `_payload.json`).
- `type Plugin struct { fsys fs.FS }`; a nil `fsys` means `fs.Sub(publicFS, "public")`. Methods `ID() string` (returns the literal `"golem15.summercms"`), `Requires() []string` (nil), `Register(*backpack.App) error`, `Boot(*backpack.App) error`, `Routes(r pact.Router) error`. Compile-time assertion `var _ pact.HasRoutes = (*Plugin)(nil)`. `func init() { party.Register(&Plugin{}) }`.
- `func newHandlers(public fs.FS) (site, docs http.Handler, err error)`: subtrees `site` and `docs` of `public`; error text `summercms: public/site/index.html missing; run scripts/build.sh` (or `public/docs/...`) when a tree has no `index.html`.
- `type tree struct` with fields `files map[string]*file`, `mount string` (`"/"` or `"/docs/"`), `immutable func(name string) bool` (nil means never), `htmlRedirect bool` (true for docs); `type file struct { body []byte; etag, ctype string }`.
- `func newTree(fsys fs.FS, mount string, immutable func(string) bool, htmlRedirect bool) (*tree, error)`: loads every regular file whose path has no dot-segment, precomputes `ctype` and a strong ETag `"` + first 16 hex chars of sha256 + `"`; returns `errMissingIndex` when `index.html` is absent.
- `func (t *tree) ServeHTTP(w http.ResponseWriter, r *http.Request)`: rel = `r.URL.Path` without `t.mount`; name = `path.Clean("/"+rel)` without the leading `/` (empty means `index.html`); any segment starting with `.` → not found; exact file → serve; `name + "/index.html"` → serve; `htmlRedirect` and `name + ".html"` exists → 301 to `t.mount + name + ".html"`; otherwise not found = the tree's `404.html` with status 404 (or `http.NotFound` without one). Serving sets `Content-Type` (from `ctype`), `Cache-Control` (`cacheImmutable` when `t.immutable(name)`, else `cacheNoCache`), `ETag`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, then calls `http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(body))`.
- `const cacheImmutable = "public, max-age=31536000, immutable"`, `const cacheNoCache = "no-cache"`.
- `func siteImmutable(name string) bool`: true for `_nuxt/` paths except `_nuxt/builds/`, and for `_fonts/` paths.
- `var contentTypes map[string]string` and `func contentType(name string) string`: own table first (`.html` `text/html; charset=utf-8`, `.css` `text/css; charset=utf-8`, `.js` and `.mjs` `text/javascript; charset=utf-8`, `.json` `application/json`, `.md` `text/markdown; charset=utf-8`, `.txt` `text/plain; charset=utf-8`, `.xml` and `.xsl` `application/xml; charset=utf-8`, `.svg` `image/svg+xml`, `.png` `image/png`, `.webp` `image/webp`, `.ico` `image/x-icon`, `.woff2` `font/woff2`, `.woff` `font/woff`, `.webmanifest` `application/manifest+json`), then `mime.TypeByExtension`, then `application/octet-stream`.
- `func redirectTo(location string) http.HandlerFunc`: 301 to a constant location.
- Test helpers in `links_test.go`: `func requireBuild(t *testing.T)` (skips unless `SUMMERCMS_REQUIRE_BUILD=1`; when set, fails if either tree has no index.html), `func pageLinks(html []byte) []string`, `func resolve(t *testing.T, h http.Handler, target string) (status int, final string)`.
Framework, package `internal/docsite` and `cmd/summer`:
- `Site.SiteURL string` (`yaml:"site_url"`) and `Site.SiteLabel string` (`yaml:"site_label"`); `Options.SiteURL` and `Options.SiteLabel` override them when non-empty, as `Options.BaseURL` overrides `base_url`.
- `func checkSiteURL(raw string) error`: accepts `http://` or `https://` URLs with a host and no user info, or a path starting with exactly one `/`; rejects every other value (`javascript:`, `data:`, `//host`, relative paths, whitespace or control characters).
- `func siteLabel(siteURL, label string) string`: `label` (trimmed) when set, else the host of an absolute URL (the parsed URL's `Host` field, port included when present), else `Home`.
- Unexported `site` fields `siteURL`, `siteLabel`; `pageView.SiteURL`, `pageView.SiteLabel` filled in `baseView` (so the 404 page has the link too), never passed through `s.url()`.
- CLI flags `site-url` and `site-label` on `docs:build` and `docs:serve`, read in `docsOptions`.
App, package `main` in `sm-summercms-app` (test file only, never shipped in the binary):
- `type terminalGroup struct { Comment string \`json:"comment"\`; Commands []string \`json:"commands"\` }`, `func loadTerminal(path string) ([]terminalGroup, error)`, `const terminalCloneURL = "https://git.golem15.com/golem15/summercms"`, `func terminalScript(groups []terminalGroup, cloneURL string) string` (commands joined by `\n`; a non-empty cloneURL replaces only `terminalCloneURL` inside the `git clone` command), `func terminalEnv(base []string, gobin string) []string` (drops every `SUMMER_*` and `GOWORK` entry, sets `GOBIN=gobin`, prefixes `PATH` with gobin).
</interfaces>
<assumption_delta_decision>
Detector signal: pluralization ("a SummerCMS binary that also serves the Phase 11.1 docs"). Primary noun: the docs site stays a self-contained static tree addressed by `base_url`. Decision: no-change. The site plugin serves two independent embedded trees by path prefix, and `site_url` is an optional outbound link, not a second identity for the docs; no data model, primary key or contract changes.
</assumption_delta_decision>
</context>
<tasks>
<task type="tracer">
<name>Task 1: Tracer: one `summercms-io` binary, booted on PostgreSQL 15, serves the Nuxt site at / and the docs at /docs</name>
<precondition>`docker info` exits 0 and `docker image inspect postgres:15` succeeds; `test -f ../sm-summercms-app/vue-summercms-app/app/data/terminal.json` succeeds (plan 11.2-01 is complete).</precondition>
<reversibility rating="costly">D-02/D-01 module paths and repo names are referenced by go.mod, go.work, summer.yaml, the generated imports and the submodule URLs; nothing is published in this task (remotes are created at cutover), so a rename now touches many files but needs no migration. They become one-way when pushed at cutover, a step the user already chose in CONTEXT.</reversibility>
<files>../sm-summercms-app/plugins/golem15/summercms/go.mod, ../sm-summercms-app/plugins/golem15/summercms/go.sum, ../sm-summercms-app/plugins/golem15/summercms/plugin.go, ../sm-summercms-app/plugins/golem15/summercms/static.go, ../sm-summercms-app/plugins/golem15/summercms/smoke_test.go, ../sm-summercms-app/plugins/golem15/summercms/public/README.md, ../sm-summercms-app/plugins/golem15/summercms/.gitignore, ../sm-summercms-app/go.mod, ../sm-summercms-app/go.sum, ../sm-summercms-app/go.work, ../sm-summercms-app/summer.yaml, ../sm-summercms-app/main.go, ../sm-summercms-app/plugins.gen.go, ../sm-summercms-app/config/app.yaml, ../sm-summercms-app/config/http.yaml, ../sm-summercms-app/config/storage.yaml, ../sm-summercms-app/config/queue.yaml, ../sm-summercms-app/config/database.yaml, ../sm-summercms-app/.gitignore, ../sm-summercms-app/.gitmodules, ../sm-summercms-app/scripts/build.sh, ../sm-summercms-app/scripts/smoke.sh</files>
<read_first>
- modules/boardwalk/boardwalk.go lines 24-48 and 132-173 (embed, fs.Sub, content-type table, cache by prefix; the security headers and SPA fallback there are NOT to be copied, D-07)
- internal/docsite/serve.go lines 236-290 (public static semantics: dot-segment refusal, dir index.html, 404.html with status 404)
- examples/hello/plugins/base/plugin.go (plugin skeleton, init registration) and examples/hello/plugins/greeter/plugin.go lines 59-76 (Routes)
- modules/pact/capabilities.go lines 60-80 (Router, GroupRaw, HasRoutes) and modules/surf/router.go lines 350-430 and 460-480 (route registration, recoverBare, body limits)
- modules/cabana/http.go lines 64-74 (no admin controllers means no admin routes)
- internal/build/scaffold.go lines 104-170 and 318-370 (plugin:add edits go.mod, go.work and summer.yaml; ID read from plugin.go)
- ../fonoteka.go/go.work, ../fonoteka.go/go.mod (header and replaces), ../fonoteka.go/summer.yaml, ../fonoteka.go/.gitignore, ../fonoteka.go/config/{app,http,storage,queue,database}.yaml
- modules/surf/clientip.go lines 45-70 (http.trusted_proxies format)
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md sections "Framework Surface", "Postgres-at-boot config", "Docs Build", "Pattern 2", "Cache-Control by path", "MIME table", "Build script outline", Pitfalls 7-12
</read_first>
<action>
Wire one request path through every layer: embedded files → static handler → surf raw group → generated app binary → stock `serve` on Postgres 15 → HTTP.
1. Plugin repository (writes to sm-summercms-plugin): `mkdir -p` PLUG and `git init -b master` inside it. `go.mod`: `module git.golem15.com/golem15/sm-summercms-plugin`, `go 1.27.0`, `require git.golem15.com/golem15/summercms v0.1.0` (D-42; the directory replace resolves it before the tag exists), `replace git.golem15.com/golem15/summercms => ../../../../summercms.go` (same depth as fonoteka's plugins). `.gitignore`: `/public/site/` and `/public/docs/`. `public/README.md`: build.sh fills `site/` (Nuxt output) and `docs/` (docs build); nothing else may be placed in `public/docs`, because docs:build refuses to clean a directory without its marker file; the file exists so the embed pattern always matches.
2. `plugin.go` exactly as the interfaces block defines it. The plugin implements only the party lifecycle and `pact.HasRoutes`: it must not implement the pact admin-controllers capability or any other capability, so cabana never activates and no `/backend` route exists (D-29, T-11.2-07). `Routes` builds both handlers via `newHandlers` (returning its error, so `serve` fails closed with `summercms: public/site/index.html missing; run scripts/build.sh` on an unbuilt tree) and registers, inside `r.GroupRaw("", nil, …)`: `GET /docs` → `redirectTo("/docs/")` (a permanent 301 instead of ServeMux's 307), `GET /docs/{path...}` → docs tree, `GET /` → site tree (the least specific pattern, verified conflict-free alongside cabana's patterns).
3. `static.go` exactly as the interfaces block defines it (D-07, D-47; Pitfalls 8-11). Copy the ideas of the admin SPA embed handler (fs.FS, own content-type table, cache by path, ServeContent) and of the docsite preview handler (dot refusal, dir index, 404 page), but add no robots-blocking header, no CSP, no frame-deny header, no index-token rewrite and no SPA fallback: the public site wants indexable pages and real 404s. Build the 301 Location only from `t.mount` plus the cleaned name plus `.html`, and only when that file is in the map (T-11.2-05). Stdlib only.
4. `smoke_test.go` (smoke level; coverage is plan 11.2-03): `TestStaticSmoke` builds handlers from a `fstest.MapFS` with `site/index.html`, `site/404.html`, `docs/index.html`, `docs/404.html`, `docs/setup/installation.html` and asserts `GET /` 200 `text/html; charset=utf-8`, docs `GET /docs/setup/installation` 301 to `/docs/setup/installation.html`, and `GET /nope` 404 with the 404 body. Run `go -C PLUG mod tidy`, then commit in PLUG as `feat: serve the embedded site at / and the docs at /docs`.
5. App repository (writes to sm-summercms-app): `git init -b master` in APP. Register the two existing repositories as submodules without cloning (git reports "Adding existing repo"): `git submodule add git@git.golem15.com:golem15/vue-summercms-app.git vue-summercms-app` and `git submodule add git@git.golem15.com:golem15/sm-summercms-plugin.git plugins/golem15/summercms` (D-43; the remotes are created by the user later, see DEPLOY.md). `.gitignore`: `/bin/`, `/tmp/`, `*.exe`, `go.work.sum`, `.env`, `/storage/`.
6. `go.mod`: `module git.golem15.com/golem15/sm-summercms-app`, `go 1.27.0`, `toolchain go1.27.0`, `replace git.golem15.com/golem15/summercms => ../summercms.go`, `require git.golem15.com/golem15/summercms v0.1.0`. `summer.yaml`: `module: git.golem15.com/golem15/sm-summercms-app`, `binary: summercms-io`, `plugins:` with `id: golem15.summercms`, `module: git.golem15.com/golem15/sm-summercms-plugin`. Then, with a summer binary installed from summercms.go into a temp GOBIN, run `summer plugin:add plugins/golem15/summercms` in APP (it adds the plugin require and `replace … => ./plugins/golem15/summercms`, and writes `go.work` with `use ( . ./plugins/golem15/summercms )`; confirm `go 1.27.0` and `toolchain go1.27.0` as in fonoteka.go/go.work, and that go.work does not `use` the framework), then `go -C APP mod tidy`.
7. `config/` (no secrets; Pitfall 12, D-24, D-27): `app.yaml` (`name: summercms-io`, `debug: false`, `locale: en`, `fallback_locale: en`, `key: ""` with the comment "Set SUMMER_APP__KEY to a 32-byte base64 value (key:generate)"); `http.yaml` (`body_limits.default_bytes: 1048576`, `body_limits.upload_bytes: 1048576`, `trusted_proxies` holding `127.0.0.1/32` because nginx proxies on loopback, using the CIDR list format clientip.go reads); `storage.yaml` (`uploads.bucket_url: "file://./storage/app/uploads"`, `uploads.public_path_prefix: "/storage/uploads"`); `queue.yaml` (`work_in_serve: false` with a comment that the site has no jobs, plus fonoteka's `max_attempts`, `job_timeout` and `queues.default: 1`); `database.yaml` (`dsn: ""` with the comment "Set SUMMER_DATABASE__DSN").
8. `scripts/build.sh` (bash, `set -euo pipefail`, `ROOT` from `BASH_SOURCE` as in fonoteka.go/scripts/check-openapi.sh, explicit guards that print `build: …` to stderr and exit 1). This task implements the `dev` mode; Task 4 adds `release`, which needs the tag, so until then any argument other than `dev` exits 2 with a usage line. Steps: check `pnpm`, `go`, `rsync`, `git`, `tar` exist; `FW="${SUMMERCMS_FRAMEWORK:-$ROOT/../summercms.go}"`; (a) in SITE `pnpm install --frozen-lockfile` and `pnpm run generate`, require `.output/public/index.html`, then `rsync -a --delete "$SITE/.output/public/" "$PLUG/public/site/"` (never the `dist` symlink, which embed refuses); (b) `git -C "$FW" archive HEAD | tar -x -C "$TMP/fw"` (TMP from mktemp with an EXIT trap), install summer from the export into `$TMP/bin`, run `docs:build --base-url /docs --out "$PLUG/public/docs"` inside the export, require `public/docs/index.html` and `public/docs/.summer-docs`; (c) `SUMMERCMS_REQUIRE_BUILD=1 go -C "$PLUG" test ./...`; (d) in APP run `"$TMP/bin/summer" build` (writes main.go and plugins.gen.go), then `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go -C "$ROOT" build -trimpath -o bin/summercms-io .`; print `build: bin/summercms-io (dev) ready`.
9. `scripts/smoke.sh` (bash, `set -euo pipefail`, trap cleanup of the container, the serve process and the temp dir; binary path argument defaulting to `bin/summercms-io`): start `postgres:15` with `docker run -d --rm` (user `summercms`, password `smoke`, database `summercms_io`, port `127.0.0.1::5432`, read back with `docker port`), wait for `pg_isready`; copy `config/` into a temp work dir and write a `.env` there with `SUMMER_DATABASE__DSN` and `SUMMER_APP__KEY` (32 random bytes, base64); from the work dir run the binary's `migrate`, then `serve --addr 127.0.0.1:${SMOKE_PORT:-18095}` in the background and wait for it. Assert with curl: `/` 200, `Content-Type` starting `text/html`, `Cache-Control: no-cache`, an `ETag`, and neither an `X-Robots-Tag` nor a `Content-Security-Policy` header; the same `/` with `If-None-Match: <etag>` is 304; `/docs` 301 with `Location: /docs/`; `/docs/` 200 and no `X-Robots-Tag`; `/docs/setup/installation` 301 to `/docs/setup/installation.html`, which is 200; the first `/_nuxt/*.js` referenced by index.html has `immutable` and `text/javascript`; the first file under `public/site/_fonts/` is `font/woff2` and `immutable`; `/missing-page` 404; `/backend` 404; `/docs/.summer-docs` 404; `POST /` 405. Print `smoke: ok`.
10. Run `scripts/build.sh dev` and `scripts/smoke.sh`, then commit in APP (including `.gitmodules` and both gitlinks) as `feat: wire the summercms.io app with build and smoke scripts`.
</action>
<verify>
<automated>../sm-summercms-app/scripts/build.sh dev && ../sm-summercms-app/scripts/smoke.sh</automated>
<fails_when>non-zero exit, a line starting "build:" on stderr, or no "smoke: ok" line</fails_when>
<automated>go -C ../sm-summercms-app vet ./... && go -C ../sm-summercms-app/plugins/golem15/summercms vet ./... && go -C ../sm-summercms-app/plugins/golem15/summercms test ./... -run '^TestStaticSmoke$' -count=1 -v</automated>
<fails_when>non-zero exit, a "--- FAIL" line, or no "--- PASS: TestStaticSmoke" line</fails_when>
</verify>
<acceptance_criteria>
- `go -C ../sm-summercms-app list ./...` prints exactly `git.golem15.com/golem15/sm-summercms-app` (pnpm's dot-directory layout keeps node_modules out of the module).
- `git -C ../sm-summercms-app submodule status` lists `plugins/golem15/summercms` and `vue-summercms-app`, and `git -C ../sm-summercms-app config -f .gitmodules --get-regexp url` prints `git@git.golem15.com:golem15/sm-summercms-plugin.git` and `git@git.golem15.com:golem15/vue-summercms-app.git`.
- `grep -n 'go:embed all:public' ../sm-summercms-app/plugins/golem15/summercms/plugin.go` and `grep -n 'return "golem15.summercms"' ../sm-summercms-app/plugins/golem15/summercms/plugin.go` both find a match.
- `grep -c 'HasAdminControllers' ../sm-summercms-app/plugins/golem15/summercms/plugin.go` prints 0.
- Every import path printed by `go -C ../sm-summercms-app/plugins/golem15/summercms list -f '{{join .Imports "\n"}}' .` is either a standard-library path or starts with `git.golem15.com/golem15/summercms/modules/` (no new third-party dependency).
- `grep -n 'work_in_serve: false' ../sm-summercms-app/config/queue.yaml` finds a match, and `grep -n 'key: ""' ../sm-summercms-app/config/app.yaml` finds a match.
- `file ../sm-summercms-app/bin/summercms-io` reports an ELF 64-bit x86-64 executable.
</acceptance_criteria>
<done>The app and plugin repositories exist with the submodule layout, the binary embeds both trees and boots on postgres:15 through the stock serve command, and the smoke script proves status codes, content types, cache headers, 301s, 404s and the absence of robots-blocking and CSP headers.</done>
</task>
<task type="auto">
<name>Task 2: Framework for v0.1.0: verified and documented on PostgreSQL 15, and the docs header links back to the main site</name>
<files>README.md, docs/setup/installation.md, internal/docsite/load.go, internal/docsite/docsite.go, internal/docsite/emit.go, internal/docsite/theme/templates/header.html, internal/docsite/theme/assets/site.css, internal/docsite/load_test.go, internal/docsite/theme_test.go, cmd/summer/docs.go, cmd/summer/docs_test.go, docs/console/utilities.md</files>
<read_first>
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md sections "PostgreSQL 15 (question 3)", "Docs Build (question 2)", "D-41 header change", Open Question 1
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-CONTEXT.md D-25, D-41, D-46
- README.md lines 10-40, docs/setup/installation.md lines 1-20
- internal/docsite/load.go (Site, ParseSite, load and the base_url override at lines 217-221), internal/docsite/docsite.go (Options, normalize), internal/docsite/emit.go lines 60-95 (pageView, baseView)
- internal/docsite/theme/templates/header.html (whole file), internal/docsite/theme/templates/icons.html line 8 (icon-chevron-left), internal/docsite/theme/assets/site.css lines 250-285 (.wordmark, .header-spacer) and its mobile media query
- cmd/summer/docs.go (flag lists of docs:build and docs:serve, docsOptions), cmd/summer/docs_test.go (TestDocsTree, TestDocsBuildRealTree)
- internal/docsite/load_test.go lines 11-50 (TestParseSite table) and internal/docsite/theme_test.go lines 1-80 (themeTree, buildTheme)
- docs/console/utilities.md lines 28-50 (Documentation commands table)
</read_first>
<action>
Two commits in summercms.go, in this order. Stage only the listed files.
A. D-25, PostgreSQL 15 (no repository edits until the run passes). Export HEAD to a scratch directory, retarget the test image and run the database suites exactly as RESEARCH "How to run on 15 without permanent edits" does: `S=$(mktemp -d)`, `git archive HEAD | tar -x -C "$S"`, replace `postgres:16-alpine` with `postgres:15` in every `*.go` file under `$S` with sed, then `go -C "$S" test -count=1 -p 4 ./modules/lagoon/... ./modules/cabana/... ./modules/beachcomber/... ./modules/lighthouse/... ./modules/bouncer/... ./modules/conga/... ./docs/examples/blog/...`, and confirm with `-v` output or the testcontainers log that the image was `postgres:15`. Record the package result lines in the SUMMARY. If any package fails on 15, stop here: do not edit the docs, do not continue to the tag, and return a `checkpoint:decision` (`gate="blocking-human"`) to the user with the failing output (D-25 says stop and ask; do not work around it and do not upgrade rome). When every package passes, change `README.md` line 15 to "PostgreSQL 15 or newer for any application that uses the data layer ([lagoon](modules/lagoon/README.md))." and line 35 to "Create a database on PostgreSQL 15 or newer:", and `docs/setup/installation.md` line 14 to "PostgreSQL 15 or newer for any application that uses the data layer." Leave `docs/plugins/testing.md` and the lagoon and conga READMEs unchanged: they name the test image, not a requirement. Commit as `docs: require PostgreSQL 15 or newer (verified on postgres:15)`.
B. D-41/D-46, the link back to the main site. Before editing, build the real tree from a HEAD export into a scratch directory `pre` (for the byte-identical check).
1. `load.go`: add `SiteURL` (`yaml:"site_url"`) and `SiteLabel` (`yaml:"site_label"`) to `Site` with doc comments (strict decoding needs the fields). In `ParseSite`, when `site_url` is set validate it with `checkSiteURL` and report `docsite: site config: site_url must be an http(s) URL with a host or a path starting with a single /`; when `site_label` is set without `site_url` report `docsite: site config: site_label needs site_url`; a label that is blank after trimming or holds a line break is `docsite: site config: site_label must be one non-empty line`. Add `checkSiteURL` and `siteLabel` exactly as the interfaces block defines them (`net/url` for absolute URLs; T-11.2-09).
2. `docsite.go`: add `Options.SiteURL` ("overrides site.yaml site_url when non-empty") and `Options.SiteLabel` ("overrides site.yaml site_label when non-empty"). In `load`, next to the base_url override: start from the config values, apply the option overrides, validate an overriding URL with `checkSiteURL` (error `docsite: --site-url: must be an http(s) URL with a host or a path starting with a single /`), refuse a label without any site URL (`docsite: --site-label needs --site-url or site.yaml site_url`), then store `s.siteURL` and `s.siteLabel = siteLabel(s.siteURL, label)`.
3. `emit.go`: add `SiteURL` and `SiteLabel` to `pageView` and set them in `baseView` from the site fields, not through `s.url()` (they point at another site).
4. `header.html`: append to the end of the wordmark anchor's line a conditional anchor `{{if .SiteURL}}<a class="site-link" href="{{.SiteURL}}" aria-label="{{.SiteLabel}}">{{template "icon-chevron-left"}}<span class="site-link-label">{{.SiteLabel}}</span></a>{{end}}`, on the same line so the unset output keeps the exact bytes it has today. html/template escapes the attribute.
5. `site.css`: a `.site-link` rule next to `.wordmark` (inline-flex, centered, small gap, a left margin, the theme's muted text color and the existing accent on hover, 14px), and in the theme's existing narrow-screen media query hide `.site-link-label` so the search trigger keeps its room (the aria-label keeps the link named).
6. `cmd/summer/docs.go`: add `{Name: "site-url", Description: "Main site URL linked from the docs header (overrides site.yaml site_url)"}` and `{Name: "site-label", Description: "Label of the main site link (overrides site.yaml site_label; default: the URL host, or Home)"}` to both `docs:build` and `docs:serve`, and read both in `docsOptions`.
7. `docs/console/utilities.md`: add `--site-url` and `--site-label` to the Flags cells of the `docs:build` and `docs:serve` rows, and below the table a short paragraph: `docs/site.yaml` accepts two optional keys, `site_url` and `site_label`; with `site_url: https://acme.example/` every page header links back to the main site as "acme.example"; without `site_label` the label is the URL's host, or Home for a path such as `/`; the two flags override the keys the way `--base-url` overrides `base_url`. Use only the neutral `acme.example` example (CLAUDE.md: framework docs never name a consuming application). `docs/site.yaml` itself stays unchanged (D-46).
8. Smoke tests (coverage is plan 11.2-03): `TestParseSite` rows for an accepted `https://acme.example/`, an accepted `/`, a rejected `javascript:alert(1)`, a rejected `//acme.example`, and a rejected `site_label` without `site_url`; `TestSiteLink` in `theme_test.go` builds `themeTree` three ways and checks `index.html` and `404.html`: no options → no `site-link`; `Options{SiteURL: "/", SiteLabel: "acme.example"}` → `<a class="site-link" href="/"` and `acme.example`; `Options{SiteURL: "https://acme.example/docs"}` → label `acme.example`; `Options{SiteURL: "/"}` → label `Home`. `TestDocsBuildSiteFlags` in `cmd/summer/docs_test.go`: `docs:build --root ../.. --out <tmp> --site-url / --site-label example.org` writes `class="site-link" href="/"` into `index.html`, and `--site-url javascript:alert(1)` returns an error containing `--site-url`.
9. Byte-identical check: build the working tree with no site flags into a scratch directory `post` and require `diff -r pre post` to print nothing; record that in the SUMMARY.
Commit as `feat(docsite): optional site_url and site_label link back to the main site`.
</action>
<verify>
<automated>go vet ./internal/docsite/... ./cmd/summer && go test ./internal/docsite ./cmd/summer -run '^(TestDocsTree|TestDocsBuildRealTree|TestToolCommandNames|TestParseSite|TestSiteLink|TestDocsBuildSiteFlags)$' -count=1 -v</automated>
<fails_when>non-zero exit, a "--- FAIL" line, "no tests to run", or fewer than six "--- PASS" lines for the named tests</fails_when>
<automated>go vet ./... && go test ./internal/docsite ./cmd/summer -count=1</automated>
<fails_when>non-zero exit or a "FAIL" line</fails_when>
<automated>scripts/check-phase11.1.sh --docs && scripts/check-phase11.1.sh --forbidden</automated>
<fails_when>non-zero exit or a line starting "refuse:"</fails_when>
</verify>
<acceptance_criteria>
- The SUMMARY lists an `ok` line for each of lagoon, lagoon/attach, cabana, beachcomber, lighthouse, bouncer, conga and docs/examples/blog run against `postgres:15`, and no `FAIL` line.
- `grep -c 'PostgreSQL 15 or newer' README.md` prints 2 and `grep -c 'PostgreSQL 15 or newer' docs/setup/installation.md` prints 1.
- `grep -n 'yaml:"site_url"' internal/docsite/load.go` and `grep -n 'yaml:"site_label"' internal/docsite/load.go` both find a match.
- `grep -c 'site-url\|site-label' cmd/summer/docs.go` prints at least 6 (two flags on two commands plus two reads).
- `grep -n 'acme.example' docs/console/utilities.md` finds a match, and `git diff --stat "$D25_SHA^..$SITE_SHA" -- docs/site.yaml` prints nothing, where `D25_SHA` and `SITE_SHA` hold the two commit shas of this task recorded in the SUMMARY (the framework's own site.yaml is unchanged, D-46).
- `diff -r` of the pre-change and post-change real-tree builds without site flags prints nothing (recorded in the SUMMARY).
- `git log --format=%s "$D25_SHA^..$SITE_SHA"` in summercms.go prints exactly the two commit subjects above, and `git log --format='%(trailers:key=Co-authored-by,valueonly)' "$D25_SHA^..$SITE_SHA"` prints only empty lines.
</acceptance_criteria>
<done>The database suites are proven on postgres:15 and the docs say 15 or newer; site_url and site_label (keys and flags) add a validated link back to the main site in every docs page header, leaving the framework's own output unchanged; the docs checker stays green.</done>
</task>
<task type="checkpoint:decision" gate="blocking-human">
<name>Task 3: Confirm the v0.1.0 tag (one-way, D-42)</name>
<decision>Create and push the `v0.1.0` tag of `git.golem15.com/golem15/summercms` now, at the commit produced by Task 2?</decision>
<context>D-42 requires the user to confirm before the tag is created and pushed. A pushed Go module version tag is cached by module proxies and cannot be reused, so a wrong commit means burning v0.1.0. Before asking, show: the summercms.go HEAD sha and subject (it must be the Task 2 docsite commit, with the D-25 docs commit before it), `git status --porcelain` (must be empty), the Task 2 test results, the D-25 postgres:15 result lines, and `git log --oneline` since the last pushed commit. The release build in Task 4 takes the docs and the compiled framework from this tag, so the page, the docs and the code agree. Plan 11.2-03 adds framework test commits after the tag, which is acceptable because tests do not change v0.1.0 behaviour.</context>
<options>
<option id="tag-now">
<name>Tag and push v0.1.0 now</name>
<pros>The release build, the embedded docs and DEPLOY.md all reference a real published tag; nothing is left for cutover.</pros>
<cons>One-way: if a defect is found before launch, the fix ships as v0.1.1.</cons>
</option>
<option id="defer-tag">
<name>Defer the tag to cutover</name>
<pros>Keeps v0.1.0 unspent until the site is reviewed; Task 4 still proves the release path against a scratch clone carrying a local tag.</pros>
<cons>The real release build waits for the tag; DEPLOY.md lists creating and pushing it as a launch step.</cons>
</option>
</options>
<resume-signal>Type "tag-now" or "defer-tag".</resume-signal>
</task>
<task type="auto">
<name>Task 4: Release build from the v0.1.0 tag, with every landing link and every terminal command proven against the built site</name>
<precondition>Task 3 returned "tag-now" or "defer-tag". For "tag-now": `git ls-remote origin` in summercms.go succeeds (SSH access to git.golem15.com). For "defer-tag": `SUMMERCMS_FRAMEWORK` is exported to the scratch clone (step 1) before any verify command below runs, so the same commands exercise the release path.</precondition>
<reversibility rating="one-way">Pushing v0.1.0 publishes a module version that proxies cache forever (D-42); done only after the Task 3 confirmation.</reversibility>
<files>../sm-summercms-app/scripts/build.sh, ../sm-summercms-app/scripts/smoke.sh, ../sm-summercms-app/terminal_check_test.go, ../sm-summercms-app/plugins/golem15/summercms/links_test.go</files>
<read_first>
- ../sm-summercms-app/scripts/build.sh and scripts/smoke.sh (as written in Task 1)
- ../sm-summercms-app/plugins/golem15/summercms/plugin.go and static.go (as written in Task 1)
- ../sm-summercms-app/vue-summercms-app/app/data/terminal.json and i18n/locales/en.json (the command list and comment texts)
- modules/surf/example_test.go lines 114-140 (surf.Assemble with backpack.New(nil))
- modules/bonfire/prompts.go lines 25-35 (non-interactive Confirm returns the default)
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md "Pattern 3", "Every link on the page", "D-40: does ./bin/hello greeter:hello need a database? No.", "Build script outline" (the temporary go.work note)
</read_first>
<action>
1. Tag (summercms.go). For "tag-now": confirm `git status --porcelain` is empty and HEAD is the Task 2 docsite commit, run `git tag -a v0.1.0 -m "SummerCMS Alpha 0.1"` and `git push origin v0.1.0`, then confirm `git ls-remote --tags origin v0.1.0` prints it. If the push fails on authentication, return a `checkpoint:human-action` asking the user to run the same push. For "defer-tag": create nothing in summercms.go; make a scratch clone (`git clone` of the local summercms.go into a temp dir), add a local `v0.1.0` tag there, and use it through `SUMMERCMS_FRAMEWORK=<clone>` for every release-mode run below. Record which path was taken in the SUMMARY.
2. `scripts/build.sh` release mode (now the default when no argument is given; `dev` stays): read the framework version the app requires (`go -C "$ROOT" list -m -f '{{.Version}}' git.golem15.com/golem15/summercms`, which prints `v0.1.0`); require `git -C "$FW" rev-parse -q --verify "refs/tags/$TAG^{commit}"`, else exit 1 with `build: tag $TAG not found in $FW; run 'scripts/build.sh dev' or create the tag (DEPLOY.md)`; `git -C "$FW" archive "$TAG"` into `$TMP/fw`; install summer from that export; run docs:build there with `--base-url /docs --site-url / --site-label summercms.io --out "$PLUG/public/docs"` (D-46; dev mode now passes the same two site flags); run the plugin tests with `SUMMERCMS_REQUIRE_BUILD=1`; run `summer build` in APP, then fail if `git -C "$ROOT" status --porcelain -- main.go plugins.gen.go` shows drift (generated sources must be committed); write `$TMP/go.work` (`go 1.27.0`, `use` the absolute APP and PLUG directories, `replace git.golem15.com/golem15/summercms => $TMP/fw`) and build with `GOWORK="$TMP/go.work" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go -C "$ROOT" build -trimpath -o bin/summercms-io .` so the compiled framework is the tag (go.work replaces override module replaces); print `build: bin/summercms-io (release v0.1.0) ready`.
3. `scripts/smoke.sh`: add the D-46 assertion that `/docs/` contains `class="site-link" href="/"` and the text `summercms.io`.
4. `links_test.go` in PLUG (package `summercms`, stdlib only) with the helpers from the interfaces block:
- `TestLandingLinks` (`requireBuild`): assemble the real plugin with `surf.Assemble(backpack.New(nil), []party.Plugin{&Plugin{}})`; collect every `href` and `src` value from `public/site/index.html`, plus the `og:image` content; for `#id` require `id="id"` in the page; for a root-relative path, or an absolute `https://summercms.io/` URL mapped to its path, request it through the handler, follow at most one 301 whose Location must start with `/`, and require a final 200; collect other `http(s)` URLs for the external test. Require that the hrefs include `/docs` and the ten D-44 targets: `/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`, `/docs/setup/coming-from-wintercms`, `/docs/setup/installation`.
- `TestTerminalCommandsInPage` (`requireBuild`; the D-40 drift guard): read `../../../vue-summercms-app/app/data/terminal.json` and `../../../vue-summercms-app/i18n/locales/en.json` (overridable with `SUMMERCMS_SITE_DIR`) and require every command and every resolved comment text in the built index.html.
- `TestDocsHeaderSiteLink` (`requireBuild`): `public/docs/index.html` contains `class="site-link" href="/"` and `summercms.io`.
- `TestExternalLinks` (skips unless `SUMMERCMS_CHECK_EXTERNAL=1`): GET each external URL with a fresh client (no cookies, 15 s timeout, redirects followed) and require 200; log each URL with its status. Before D-38 the Source link is expected to fail (private repository); this test runs at cutover.
Commit in PLUG as `test: verify the landing links and terminal commands against the built site`.
5. `terminal_check_test.go` in APP (package `main`) with the helpers from the interfaces block. `TestTerminalCommands` skips unless `SUMMERCMS_TERMINAL_CHECK=1`; it loads `vue-summercms-app/app/data/terminal.json`, creates a temp work dir and a temp GOBIN, runs `bash -euo pipefail -c <terminalScript(groups, os.Getenv("SUMMERCMS_CLONE_URL"))>` in the work dir with stdin from /dev/null, `terminalEnv(os.Environ(), gobin)` and a 10-minute context, then requires exit 0 and `handled=true` in the combined output (logged on failure). With `SUMMERCMS_CLONE_URL` unset the commands run verbatim (cutover, after D-38). If a command needs setup that the check cannot provide without changing the page copy, stop and ask the user (D-40) instead of editing terminal.json.
6. Run the release build (for "defer-tag", with `SUMMERCMS_FRAMEWORK` pointing at the scratch clone), then `scripts/smoke.sh`, then `SUMMERCMS_TERMINAL_CHECK=1 SUMMERCMS_CLONE_URL=/media/nvme/dev/golem15/summercms.io/summercms/summercms.go go -C ../sm-summercms-app test -run '^TestTerminalCommands$' -count=1 -v .`. Commit in APP (including the updated plugin gitlink) as `feat: release builds from the v0.1.0 tag and the terminal command check`.
</action>
<verify>
<automated>../sm-summercms-app/scripts/build.sh && ../sm-summercms-app/scripts/smoke.sh</automated>
<fails_when>non-zero exit, a line starting "build:" on stderr, no "(release v0.1.0) ready" line, or no "smoke: ok" line</fails_when>
<automated>SUMMERCMS_REQUIRE_BUILD=1 go -C ../sm-summercms-app/plugins/golem15/summercms test ./... -run '^(TestLandingLinks|TestTerminalCommandsInPage|TestDocsHeaderSiteLink)$' -count=1 -v</automated>
<fails_when>non-zero exit, a "--- FAIL" or "--- SKIP" line, or fewer than three "--- PASS" lines</fails_when>
<automated>SUMMERCMS_TERMINAL_CHECK=1 SUMMERCMS_CLONE_URL=/media/nvme/dev/golem15/summercms.io/summercms/summercms.go go -C ../sm-summercms-app test -run '^TestTerminalCommands$' -count=1 -v .</automated>
<fails_when>non-zero exit, a "--- SKIP" line, or no "--- PASS: TestTerminalCommands" line</fails_when>
</verify>
<acceptance_criteria>
- For "tag-now": `git ls-remote --tags origin v0.1.0` in summercms.go prints one line ending in `refs/tags/v0.1.0`. For "defer-tag": `git tag -l v0.1.0` in summercms.go prints nothing and the SUMMARY names the scratch clone used.
- `go version -m ../sm-summercms-app/bin/summercms-io | grep 'git.golem15.com/golem15/summercms'` shows version `v0.1.0`.
- `grep -n 'docs:build --base-url /docs --site-url / --site-label summercms.io' ../sm-summercms-app/scripts/build.sh` finds a match.
- `grep -c 'class="site-link" href="/"' ../sm-summercms-app/plugins/golem15/summercms/public/docs/index.html` prints 1.
- `go -C ../sm-summercms-app test ./... -count=1` passes with the gated tests skipped (no env vars set), and `go -C ../sm-summercms-app vet ./...` is clean.
</acceptance_criteria>
<done>The release binary is built from the confirmed v0.1.0 tag (or a scratch tag when deferred), every landing link resolves against the built site with redirects followed, the page and the terminal check share one command list, and the six commands run from a fresh shell to handled=true.</done>
</task>
<task type="auto">
<name>Task 5: An operator can deploy, verify and roll back summercms.io on rome by following DEPLOY.md</name>
<files>../sm-summercms-app/DEPLOY.md, ../sm-summercms-app/README.md, ../sm-summercms-app/deploy/nginx/summercms.io.conf, ../sm-summercms-app/deploy/supervisor/summercms-io.conf, ../sm-summercms-app/deploy/env.example, ../sm-summercms-app/deploy/rollback/under-construction/index.html, ../sm-summercms-app/deploy/rollback/under-construction/logo.png, ../sm-summercms-app/scripts/check-deploy.sh, ../sm-summercms-app/plugins/golem15/summercms/README.md</files>
<read_first>
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md section "Deploy (question 6)" items 1-9, "Live site observations", Assumptions A1-A3, "Security Domain"
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-CONTEXT.md D-27 to D-32, D-38, D-42, D-43
- modules/surf/serve.go lines 20-80 (`serve --addr`, 10 s shutdown on SIGTERM), modules/lagoon/keygen.go (key:generate prints a key), modules/compass/README.md lines 85-100 (config dir and the dotenv file next to it)
- ../sm-summercms-app/scripts/build.sh and scripts/smoke.sh (as written in Tasks 1 and 4)
</read_first>
<action>
Write the deploy contract for rome (D-28 to D-32) and the user's launch steps (D-38, D-42, D-43). Values marked [ASSUMED] in RESEARCH are written as defaults with a "check on rome" note.
1. `deploy/nginx/summercms.io.conf` (D-29, D-30, D-31): a port-80 server for `summercms.io` and `www.summercms.io` answering `301 https://summercms.io$request_uri`; a 443 server for `www.summercms.io` answering 301 to the apex (rome has no 443 www block today, so https://www.summercms.io fails its TLS handshake); the 443 apex server with `listen 443 ssl http2`, `ssl_certificate /etc/letsencrypt/live/summercms.io/fullchain.pem`, `ssl_certificate_key /etc/letsencrypt/live/summercms.io/privkey.pem`, `include /etc/letsencrypt/options-ssl-nginx.conf`, `ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem` (comment: copy the lineage lines from the existing block if they differ), `gzip on; gzip_vary on; gzip_proxied any;` with types `text/css text/plain text/xml application/xml application/json text/javascript application/javascript image/svg+xml text/markdown`, `location ^~ /backend { return 404; }`, and `location /` with `limit_except GET HEAD { deny all; }`, `proxy_pass http://127.0.0.1:8095`, `proxy_http_version 1.1` and the `Host`, `X-Real-IP`, `X-Forwarded-For` and `X-Forwarded-Proto` headers.
2. `deploy/supervisor/summercms-io.conf` (D-32): `[program:summercms-io]` with `command=/srv/summercms-io/bin/summercms-io serve --addr 127.0.0.1:8095`, `directory=/srv/summercms-io` (config is read relative to the working directory), `user=summercms`, `environment=SUMMER_ENV="production"`, `autostart=true`, `autorestart=true`, `startsecs=3`, `stopsignal=TERM`, `stopwaitsecs=15`, `redirect_stderr=true`, `stdout_logfile=/var/log/supervisor/summercms-io.log` with size and backup limits.
3. `deploy/env.example`: the two variable names with no secret values (`SUMMER_DATABASE__DSN=postgres://summercms:<password>@127.0.0.1:5432/summercms_io?sslmode=disable`, `SUMMER_APP__KEY=`) and a comment that the real file lives at `/srv/summercms-io/.env`, owner summercms, mode 0600, never in git (T-11.2-08).
4. Rollback copy of today's site (D-31): download `https://summercms.io/` and `https://summercms.io/logo.png` with `curl -fsSL` into `deploy/rollback/under-construction/index.html` and `logo.png` (the live "Under construction" docroot is one HTML file plus the logo).
5. `DEPLOY.md` with these sections:
- Overview: what runs where (nginx :443 → 127.0.0.1:8095 summercms-io → Postgres 15 `summercms_io`), the user, port and paths (`summercms`, `127.0.0.1:8095`, `/srv/summercms-io/{bin,config,storage,.env}`), marked "check on rome" where assumed.
- Launch checklist (once, before cutover, done by the user): create the three repositories `golem15/sm-summercms-app`, `golem15/vue-summercms-app` and `golem15/sm-summercms-plugin` on git.golem15.com, then in each local repository `git remote add origin git@git.golem15.com:golem15/<name>.git` and `git push -u origin master`, submodules first (`ssu` can push submodules that are ahead) (D-43); make `golem15/summercms` public (D-38); confirm `git ls-remote --tags origin v0.1.0` in summercms.go, or create and push the tag now if it was deferred (D-42); run `SUMMERCMS_CHECK_EXTERNAL=1 go -C plugins/golem15/summercms test -run TestExternalLinks -count=1 -v ./...` and `SUMMERCMS_TERMINAL_CHECK=1 go test -run TestTerminalCommands -count=1 -v .` with no clone override; on rome check that port 8095 is free with `ss -ltnp`.
- One-time server setup: `adduser --system --group --home /srv/summercms-io --shell /usr/sbin/nologin summercms`; `sudo -u postgres createuser --pwprompt summercms`; `sudo -u postgres createdb -O summercms -E UTF8 summercms_io` (D-27); create the directories; write `/srv/summercms-io/.env` from `deploy/env.example` with the DSN and a key printed by `./bin/summercms-io key:generate`, `chown summercms:summercms` and `chmod 0600`; install the supervisor program and run `supervisorctl reread && supervisorctl update`.
- Build (local, D-28): `scripts/build.sh` (release from v0.1.0; `scripts/build.sh dev` builds from the framework working tree for previews); the server needs neither Go nor Node.
- Upload: `rsync -av --chmod=F644,D755 config/ rome:/srv/summercms-io/config/` and `rsync -av bin/summercms-io rome:/srv/summercms-io/bin/summercms-io.new`; `.env` is never rsynced.
- Release (every deploy): `cd /srv/summercms-io && sudo -u summercms ./bin/summercms-io.new migrate` (D-27, before restart; cwd matters), keep the old binary as `bin/summercms-io.prev`, move `.new` into place, `supervisorctl restart summercms-io`, `curl -sI http://127.0.0.1:8095/`.
- Cutover (D-31): first save the existing summercms.io server block from rome into `deploy/rollback/nginx-under-construction.conf` in this repository and commit it (it cannot be read from the build machine), keep the old docroot, then replace the block in place with `deploy/nginx/summercms.io.conf` and run `nginx -t && systemctl reload nginx`.
- Verify after cutover: curl checks for `https://summercms.io/` 200 with `Cache-Control: no-cache` and no robots-blocking header, a `/_nuxt/` asset with `immutable`, `/docs/` 200, `/docs/setup/installation` 301 to `.html`, `/backend` 404, `POST /` 403, `http://summercms.io/` 301 to https, `https://www.summercms.io/` 301 to the apex; then the external link check against the live site.
- Rollback: app (`mv bin/summercms-io.prev bin/summercms-io && supervisorctl restart summercms-io`; 11.2 adds no plugin migrations) and cutover (restore `deploy/rollback/nginx-under-construction.conf` and the docroot from `deploy/rollback/under-construction/`, then `nginx -t && systemctl reload nginx`).
6. `scripts/check-deploy.sh` (bash, `set -euo pipefail`): in a temp prefix, generate a self-signed certificate with openssl, copy the nginx site config with the certbot paths, the options include and the dhparam line replaced by temp equivalents, wrap it in a minimal `nginx.conf` (events block, an http block with temp `*_temp_path` entries and `pid` in the prefix), and run `nginx -t -p "$TMP" -c "$TMP/nginx.conf" -e "$TMP/error.log"`; parse the supervisor file with `python3` configparser and require `[program:summercms-io]` with `command`, `directory`, `user`, `autostart`, `autorestart`, `stopsignal`; print `check-deploy: ok`.
7. `README.md` in APP: what the app is, the submodule layout and `git submodule update --init`, `scripts/build.sh` and `dev`, the test commands and their environment gates (`SUMMERCMS_REQUIRE_BUILD`, `SUMMERCMS_TERMINAL_CHECK`, `SUMMERCMS_CLONE_URL`, `SUMMERCMS_CHECK_EXTERNAL`), and a pointer to DEPLOY.md. `README.md` in PLUG: what it serves (`/`, `/docs`, `/docs/{path...}`), the cache and content-type rules, the 301 for extension-less docs URLs, why it declares no admin controllers, and how `public/` is filled.
Commit in PLUG as `docs: describe the site plugin`, then in APP (with the updated plugin gitlink) as `docs: add DEPLOY.md with nginx, supervisor and rollback configs`.
</action>
<verify>
<automated>../sm-summercms-app/scripts/check-deploy.sh</automated>
<fails_when>non-zero exit, "test failed" or "[emerg]" in the nginx output, or no "check-deploy: ok" line</fails_when>
<human-check>At cutover, follow DEPLOY.md on rome from the launch checklist to "Verify after cutover": the site answers at https://summercms.io with the landing page, /docs serves the docs with the "summercms.io" header link, /backend is 404, and https://www.summercms.io redirects to the apex.</human-check>
</verify>
<acceptance_criteria>
- `grep -n 'location ^~ /backend' ../sm-summercms-app/deploy/nginx/summercms.io.conf` and `grep -n 'limit_except GET HEAD' ../sm-summercms-app/deploy/nginx/summercms.io.conf` both find a match.
- `grep -n 'user=summercms' ../sm-summercms-app/deploy/supervisor/summercms-io.conf` and `grep -n '127.0.0.1:8095' ../sm-summercms-app/deploy/supervisor/summercms-io.conf` both find a match.
- `grep -c 'migrate' ../sm-summercms-app/DEPLOY.md` prints at least 1, and `grep -n 'createdb -O summercms' ../sm-summercms-app/DEPLOY.md`, `grep -n 'chmod 0600' ../sm-summercms-app/DEPLOY.md` and `grep -n 'git remote add origin' ../sm-summercms-app/DEPLOY.md` each find a match.
- `grep -n 'Rollback' ../sm-summercms-app/DEPLOY.md` finds a match and `test -s ../sm-summercms-app/deploy/rollback/under-construction/index.html` succeeds.
- `git -C ../sm-summercms-app ls-files -- .env deploy/.env` prints nothing (secrets are never committed).
</acceptance_criteria>
<done>DEPLOY.md, the nginx and supervisor configs, the env template and the rollback copy cover build, upload, release, cutover, verification and rollback on rome; the nginx config passes `nginx -t`; the user's launch steps (remotes, public repository, tag, external checks) are listed.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| internet → nginx (rome) | Untrusted HTTP requests; TLS terminates here |
| nginx → summercms-io on 127.0.0.1:8095 | Proxied requests; path and method filtered by nginx |
| request path → embedded file map | Untrusted path selects a file and may trigger a redirect |
| site.yaml / CLI flags → docs header HTML | Configured URL is written into every docs page |
| operator machine → rome | Binary and config upload; secrets stay on the server |
| summercms.go → module proxy | A pushed version tag becomes immutable public state |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-11.2-04 | Information disclosure | PLUG static.go path resolution | high | mitigate | `path.Clean` on a fixed root, lookups only in the in-memory map built from the embedded tree, dot-segment paths refused (hides `.summer-docs`); smoke asserts `/docs/.summer-docs` 404 |
| T-11.2-05 | Spoofing (open redirect) | `/docs` and extension-less docs redirects | medium | mitigate | Location is a constant (`/docs/`) or `t.mount` + cleaned name + `.html`, only when that file exists; never from the raw URL |
| T-11.2-06 | Tampering (MIME sniffing) | static.go headers | low | mitigate | Own content-type table before `mime`, `X-Content-Type-Options: nosniff`; only build output is served |
| T-11.2-07 | Elevation of privilege | admin exposure | high | mitigate | The plugin declares no admin controllers, so cabana never mounts `/backend`; nginx `location ^~ /backend { return 404; }`; smoke asserts `/backend` 404 |
| T-11.2-08 | Information disclosure | DB password and app key | high | mitigate | Config YAML keeps `key` and `dsn` empty; secrets only in `/srv/summercms-io/.env` (0600, owner summercms), gitignored and never rsynced; `deploy/env.example` holds names only |
| T-11.2-09 | Tampering (XSS) | docs header `site_url` | medium | mitigate | `checkSiteURL` allow-list (http/https with host, or a single-slash path) for both the key and the flag; html/template escaping; TestParseSite rejects `javascript:` and `//host` |
| T-11.2-10 | Tampering (clickjacking) | public pages | low | accept | Static marketing and docs pages with no state-changing actions; the admin CSP is deliberately not reused because it blocks Nuxt's inline scripts (D-07) |
| T-11.2-11 | Information disclosure (transport) | nginx | high | mitigate | HTTPS only with certbot certificates, HTTP→HTTPS 301, a 443 www→apex block; the binary listens on loopback only |
| T-11.2-12 | Denial of service | nginx and binary | medium | mitigate | `limit_except GET HEAD` at nginx, GET-only routes (405 otherwise) and in-memory static files in the binary; supervisor restarts on exit |
| T-11.2-13 | Tampering / Repudiation | v0.1.0 tag | medium | mitigate | Blocking-human checkpoint before creation and push; annotated tag at a clean, green commit whose sha is shown to the user |
| T-11.2-14 | Elevation of privilege | runtime account | medium | mitigate | Dedicated nologin system user, dedicated Postgres role owning only `summercms_io` (D-27), app on 127.0.0.1 |
| T-11.2-SC | Tampering | dependency installs | high | mitigate | No new Go module (plugin and app import only stdlib and framework modules, checked by an acceptance criterion); build.sh installs npm packages only with `--frozen-lockfile` from plan 11.2-01's lockfile |
</threat_model>
<verification>
- summercms.go: `go vet ./... && go test ./internal/docsite ./cmd/summer -count=1` green; `scripts/check-phase11.1.sh --docs && scripts/check-phase11.1.sh --forbidden` green.
- APP: `scripts/build.sh` (release) or the scratch-clone equivalent, `scripts/smoke.sh`, `scripts/check-deploy.sh` green; `go -C ../sm-summercms-app vet ./... && go -C ../sm-summercms-app test ./... -count=1` green.
- PLUG: `SUMMERCMS_REQUIRE_BUILD=1 go -C ../sm-summercms-app/plugins/golem15/summercms test ./... -count=1` green.
- D-40: `TestTerminalCommands` green with the local clone override.
</verification>
<success_criteria>
- SC2: one binary embeds and serves `/` and `/docs` with indexable responses and the cache rules (smoke and plugin tests).
- SC3: every link on the page resolves against the built docs (TestLandingLinks); external links are checked at cutover.
- SC4: scripted build and DEPLOY.md with supervisor and nginx configs (TLS, proxy, gzip); the clean-server bring-up is the cutover UAT item.
- D-25, D-41, D-42, D-46 framework changes landed before the tag; the tag is created only after confirmation.
</success_criteria>
## User steps at cutover (D-38, D-42, D-43)
Creating the three remotes and pushing, making `golem15/summercms` public, pushing `v0.1.0` if it was deferred, running the external-link and verbatim terminal checks, saving rome's current server block, and the rome deploy itself. DEPLOY.md's launch checklist lists them; the SUMMARY repeats them.
## Artifacts this phase produces
- Repositories: `sm-summercms-app` (module `git.golem15.com/golem15/sm-summercms-app`, binary `summercms-io`) and `sm-summercms-plugin` (module `git.golem15.com/golem15/sm-summercms-plugin`, package `summercms`, plugin ID `golem15.summercms`) at `/media/nvme/dev/golem15/summercms.io/summercms/sm-summercms-app` and its `plugins/golem15/summercms`; submodule URLs `git@git.golem15.com:golem15/vue-summercms-app.git` and `git@git.golem15.com:golem15/sm-summercms-plugin.git`.
- Plugin symbols: `Plugin` (field `fsys`; methods `ID`, `Requires`, `Register`, `Boot`, `Routes`), `publicFS`, `newHandlers`, `tree` (fields `files`, `mount`, `immutable`, `htmlRedirect`; method `ServeHTTP`), `file` (fields `body`, `etag`, `ctype`), `newTree`, `errMissingIndex`, `siteImmutable`, `contentTypes`, `contentType`, `redirectTo`, `cacheImmutable`, `cacheNoCache`. Routes `GET /docs`, `GET /docs/{path...}`, `GET /`.
- Plugin tests: `TestStaticSmoke`, `TestLandingLinks`, `TestTerminalCommandsInPage`, `TestDocsHeaderSiteLink`, `TestExternalLinks`; helpers `requireBuild`, `pageLinks`, `resolve`.
- App test symbols: `terminalGroup`, `loadTerminal`, `terminalCloneURL`, `terminalScript`, `terminalEnv`, `TestTerminalCommands`.
- Environment variables: `SUMMERCMS_FRAMEWORK`, `SUMMERCMS_REQUIRE_BUILD`, `SUMMERCMS_TERMINAL_CHECK`, `SUMMERCMS_CLONE_URL`, `SUMMERCMS_CHECK_EXTERNAL`, `SUMMERCMS_SITE_DIR`, `SMOKE_PORT`; runtime `SUMMER_DATABASE__DSN`, `SUMMER_APP__KEY`, `SUMMER_ENV`.
- App config keys: `app.key`, `http.body_limits.default_bytes`, `http.body_limits.upload_bytes`, `http.trusted_proxies`, `storage.uploads.bucket_url`, `storage.uploads.public_path_prefix`, `queue.work_in_serve`, `database.dsn`.
- Scripts and deploy files: `scripts/build.sh` (`release`, `dev`), `scripts/smoke.sh`, `scripts/check-deploy.sh`, `DEPLOY.md`, `deploy/nginx/summercms.io.conf`, `deploy/supervisor/summercms-io.conf`, `deploy/env.example`, `deploy/rollback/under-construction/`.
- Framework: `docsite.Site.SiteURL`, `docsite.Site.SiteLabel`, `docsite.Options.SiteURL`, `docsite.Options.SiteLabel`, unexported `checkSiteURL`, `siteLabel`, `pageView.SiteURL`, `pageView.SiteLabel`; `site.yaml` keys `site_url`, `site_label`; CLI flags `--site-url`, `--site-label` on `summer docs:build` and `summer docs:serve`; CSS classes `.site-link`, `.site-link-label`; tests `TestSiteLink`, `TestDocsBuildSiteFlags` and new `TestParseSite` rows; git tag `v0.1.0` (after confirmation).
<output>
Create `.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-02-SUMMARY.md` when done
</output>

View File

@@ -0,0 +1,275 @@
---
phase: 11.2-ready-to-share-summercms-io-website-and-newsletter-plugin
plan: 03
type: execute
wave: 3
depends_on: ["11.2-01", "11.2-02"]
files_modified:
- scripts/check-phase11.2.sh
- internal/docsite/load_test.go
- internal/docsite/theme_test.go
- cmd/summer/docs_test.go
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md
- ../sm-summercms-app/plugins/golem15/summercms/static_test.go
- ../sm-summercms-app/plugins/golem15/summercms/routes_test.go
- ../sm-summercms-app/plugins/golem15/summercms/links_test.go
- ../sm-summercms-app/terminal_check_test.go
- ../sm-summercms-app/vue-summercms-app/tests/terminal.test.ts
- ../sm-summercms-app/vue-summercms-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 SC5 and the CLAUDE.md rule that unit tests are the last plan, the site plugin package `git.golem15.com/golem15/sm-summercms-plugin` reaches at least 90.0% statement coverage from tests that need no build output (fstest fixtures), and `internal/docsite` stays at or above 85.0%."
- "Every serving rule from D-07 and D-47 has a test that fails when the rule breaks: content types from the own table, immutable cache only for `_nuxt/` (not `_nuxt/builds/`) and `_fonts/`, no-cache with a strong ETag elsewhere including `/docs/assets/`, 304 on a matching If-None-Match, HEAD and Range handled, dot-segment and traversal paths 404, extension-less docs paths 301 to `.html`, `/docs` 301 to `/docs/`, tree 404 pages with status 404, and no robots-blocking, CSP or frame-deny header on any response."
- "No redirect the plugin emits has a Location outside `/docs/`, and no Location starts with `//` (T-11.2-05)."
- "The plugin assembles alone through `surf.Assemble` with GET-only routes (POST 405, `/backend` falls to the site 404), fails closed with `public/site/index.html missing` on an empty tree, registers itself through init under `golem15.summercms`, declares no admin controllers, and its three patterns coexist with cabana's admin patterns on one ServeMux (ready for Phase 11.3)."
- "Per D-41 and D-46, every branch of `checkSiteURL`, `siteLabel`, the `site_url`/`site_label` parsing rules and the flag override precedence is tested, the header escapes the label, the link appears on the 404 page, and the unset header keeps its exact bytes."
- "Per D-40, the terminal-check helpers `loadTerminal`, `terminalScript` and `terminalEnv` have ungated tests, and the TypeScript utilities cover their edge cases."
- "`scripts/check-phase11.2.sh --all` runs the framework, plugin, app, site, built-tree, smoke, deploy and terminal stages, requires every named test to PASS (a SKIP, FAIL or zero-match fails), and prints `phase11.2 all passed`."
- "11.2-VALIDATION.md has every per-task row filled with its command and a green status, `status: validated`, `nyquist_compliant: true` and `wave_0_complete: true`, and keeps the manual-only rows (visual UAT, rome bring-up, external links and the verbatim clone at cutover)."
- "No production code changes in this plan except fixes for defects the new tests expose; each fix lands with its failing-then-passing test in the same commit and is listed in the SUMMARY. summercms.go commits here are tests and the gate only, after v0.1.0 (RESEARCH Pitfall 13)."
- statement: "External link reachability and the verbatim terminal clone are re-run at cutover after the repository is made public (D-38)."
verification: backstop
artifacts:
- path: "../sm-summercms-app/plugins/golem15/summercms/static_test.go"
provides: "table-driven tests of every static serving rule"
contains: "fstest.MapFS"
- path: "../sm-summercms-app/plugins/golem15/summercms/routes_test.go"
provides: "assembly, fail-closed, coexistence and identity tests"
contains: "surf.Assemble"
- path: "scripts/check-phase11.2.sh"
provides: "phase gate across the four repositories"
contains: "phase11.2 all passed"
- path: "../sm-summercms-app/terminal_check_test.go"
provides: "ungated tests of the D-40 helpers"
contains: "TestTerminalScript"
- path: "internal/docsite/load_test.go"
provides: "site_url and site_label branch tests"
contains: "TestCheckSiteURL"
- path: ".planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md"
provides: "validated per-task verification map"
contains: "nyquist_compliant: true"
key_links:
- from: "scripts/check-phase11.2.sh"
to: "../sm-summercms-app/plugins/golem15/summercms"
via: "go test -json with a detector that requires each named test to PASS"
pattern: "go -C .*summercms test"
- from: "../sm-summercms-app/plugins/golem15/summercms/routes_test.go"
to: "modules/surf router"
via: "surf.Assemble(backpack.New(nil), []party.Plugin{...})"
pattern: "surf\\.Assemble\\("
- from: "scripts/check-phase11.2.sh"
to: "../sm-summercms-app/scripts/smoke.sh"
via: "--smoke stage boots the binary on postgres:15"
pattern: "smoke\\.sh"
---
<objective>
Bring full unit test coverage to the phase's new code and close the phase with a gate (SC5, CLAUDE.md "unit tests are always the last plan"). The site plugin's static handler and routes get table-driven tests over `fstest.MapFS` fixtures (D-07, D-47, T-11.2-04, T-11.2-05), the framework's `site_url`/`site_label` work gets every branch tested (D-41, D-46, T-11.2-09), the D-40 terminal-check helpers and the TypeScript utilities get their edge cases, and `scripts/check-phase11.2.sh` runs all four repositories' checks fail-closed. VALIDATION.md is filled and validated.
Purpose: plans 11.2-01 and 11.2-02 shipped smoke tests only; this plan pins every rule so a regression fails `go test`.
Output: test files in sm-summercms-plugin, sm-summercms-app, vue-summercms-app and summercms.go, the phase gate script in summercms.go, and a validated 11.2-VALIDATION.md.
</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-VALIDATION.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-01-SUMMARY.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-02-SUMMARY.md
@.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-02-PLAN.md
@scripts/check-phase11.1.sh
@modules/boardwalk/boardwalk_test.go
@modules/surf/example_test.go
**Paths.** Commands run from the summercms.go root. `APP` = `../sm-summercms-app`, `PLUG` = `APP/plugins/golem15/summercms`, `SITE` = `APP/vue-summercms-app`. Each task names the repositories it commits to.
**Interfaces under test** are the ones plan 11.2-02 fixed in its `<interfaces>` block (`Plugin`, `newHandlers`, `tree`, `newTree`, `errMissingIndex`, `siteImmutable`, `contentTypes`, `contentType`, `redirectTo`, `cacheImmutable`, `cacheNoCache`; `checkSiteURL`, `siteLabel`, `Options.SiteURL`, `Options.SiteLabel`; `terminalGroup`, `loadTerminal`, `terminalScript`, `terminalEnv`, `terminalCloneURL`) and plan 11.2-01's TypeScript utilities (`copyPayload`, `activeSection`, `SPY_THRESHOLD`). Read the 11.2-02 SUMMARY for any name that changed during execution and test the shipped name.
**Conventions.** Stdlib `testing` only (no testify in these packages), table-driven where natural, `t.TempDir()` for files, `t.Fatalf("x = %v, want %v")`. Commit per repository, one logical change per commit, never a co-author tag. In summercms.go stage only the listed files; the VALIDATION.md update is a separate planning-docs commit. If a test exposes a production defect, fix it in the same commit as its test and list it in the SUMMARY; make no other production change.
</context>
<tasks>
<task type="tracer">
<name>Task 1: Tracer: the phase gate runs the site plugin's full rule table, and the plugin package reaches 90% coverage</name>
<files>../sm-summercms-app/plugins/golem15/summercms/static_test.go, ../sm-summercms-app/plugins/golem15/summercms/routes_test.go, ../sm-summercms-app/plugins/golem15/summercms/links_test.go, scripts/check-phase11.2.sh</files>
<read_first>
- ../sm-summercms-app/plugins/golem15/summercms/plugin.go, static.go, smoke_test.go and links_test.go (as shipped by plan 11.2-02)
- modules/boardwalk/boardwalk_test.go lines 1-44 (fstest fixture and `get` helper)
- modules/surf/example_test.go lines 114-140 (surf.Assemble with backpack.New(nil))
- modules/party/registry.go lines 29-60 (Register, Activate)
- modules/cabana/http.go and modules/cabana/prefix.go (the admin route patterns to mirror in the coexistence test)
- scripts/check-phase11.1.sh lines 1-60 and 171-240 (mode layout, detect_json over go test -json)
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md "Catch-all conflicts", "Cache-Control by path", "MIME table", Pitfalls 8-11
</read_first>
<behavior>
- Fixture site tree: `index.html`, `404.html`, `200.html`, `_payload.json`, `_nuxt/entry.abc.js`, `_nuxt/entry.abc.css`, `_nuxt/builds/latest.json`, `_nuxt/builds/meta/x.json`, `_fonts/r.woff2`, `_i18n/h/en/messages.json`, `robots.txt`, `sitemap.xml`, `og-image.png`, `favicon.ico`, `sun-logo.webp`, `.hidden`. Fixture docs tree: `index.html`, `404.html`, `.summer-docs`, `assets/site.css`, `assets/site.js`, `backend/admin-spa.html`, `backend/admin-spa.md`, `guide/index.html`, `llms.txt`, `search-index.json`.
- Site: `/` 200 `text/html; charset=utf-8` `no-cache`; `/_nuxt/entry.abc.js` `text/javascript; charset=utf-8` immutable; `/_nuxt/entry.abc.css` `text/css; charset=utf-8` immutable; `/_nuxt/builds/latest.json` and `/_nuxt/builds/meta/x.json` `application/json` no-cache; `/_fonts/r.woff2` `font/woff2` immutable; `/_i18n/h/en/messages.json`, `/_payload.json`, `/robots.txt`, `/sitemap.xml`, `/og-image.png`, `/favicon.ico`, `/sun-logo.webp` no-cache with their table types; `/.hidden`, `/_nuxt/../.hidden`, `/missing` 404 with the site 404 body, `text/html; charset=utf-8`, `no-cache`.
- Docs: `/docs/` and `/docs/index.html` 200; `/docs/backend/admin-spa` and `/docs/backend/admin-spa/` 301 to `/docs/backend/admin-spa.html`; `/docs/backend/admin-spa.md` `text/markdown; charset=utf-8`; `/docs/assets/site.css` no-cache (never immutable); `/docs/guide` and `/docs/guide/` serve `guide/index.html`; `/docs/.summer-docs` and `/docs/assets/../.summer-docs` 404 with the docs 404 body; `/docs/missing` 404; a request whose path is `/docs//evil.example/x` never yields a Location starting with `//`.
- ETag is `"` + 16 lowercase hex + `"`; equal content gives equal ETags, different content different ones; If-None-Match with the ETag gives 304 and no body; HEAD gives 200, a Content-Length and no body; `Range: bytes=0-3` gives 206.
- Every response (200, 301, 304, 404) carries no X-Robots-Tag, Content-Security-Policy or X-Frame-Options header; served files carry `X-Content-Type-Options: nosniff`.
- A tree without `404.html` answers a miss with Go's plain 404; `newHandlers` on a fixture without `site/index.html` or without `docs/index.html` returns an error naming `public/site/index.html missing` or `public/docs/index.html missing`.
- `contentType` returns the table value for every listed extension, lower-cases the extension (`X.HTML`), falls back to `mime` for an unlisted known type (`.pdf`), and to `application/octet-stream` for an unknown one; `siteImmutable` is true for `_nuxt/a.js` and `_fonts/x.woff2`, false for `_nuxt/builds/latest.json`, `index.html` and `_i18n/x.json`.
- Routes: `surf.Assemble(backpack.New(nil), []party.Plugin{&Plugin{fsys: fixture}})` gives `GET /` 200, `HEAD /` 200, `GET /docs` 301 to `/docs/`, `GET /docs/` 200, `POST /` 405, `GET /backend` 404 with the site 404 body; an empty fixture makes Assemble fail with `public/site/index.html missing`; a fresh ServeMux holding the plugin's three patterns plus `GET /backend`, `GET /backend/{path...}`, `GET /backend/assets/{vendor}/{plugin}/{file...}`, `POST /backend/api/v1/auth/login` and `GET /backend/api/v1/{vendor}/{plugin}/{controller}` registers without a panic and routes `/backend/x` to the admin handler and `/` to the site; `ID()` is `golem15.summercms`, `Requires()` is nil, `Register` and `Boot` return nil, the plugin does not satisfy `pact.HasAdminControllers`, and `party.Activate(backpack.New(nil), []string{"golem15.summercms"})` finds the init-registered plugin; `&Plugin{}` (embedded tree) either assembles (tree built) or fails with the missing-index error (tree not built), never panics.
- `pageLinks` extracts every `href` and `src` value, including duplicates removed and fragment-only links kept.
</behavior>
<action>
1. `static_test.go` in PLUG (package `summercms`): one fixture builder returning the `fstest.MapFS` from the behavior block (distinct bodies per file so ETags differ), a `do(h, method, target, header)` helper, and table-driven tests `TestStaticSite`, `TestStaticDocs`, `TestStaticConditionalAndRange`, `TestStaticNoBlockingHeaders`, `TestStaticRedirectLocations`, `TestStaticMissing404Page`, `TestNewHandlersMissingIndex`, `TestContentType` and `TestSiteImmutable` covering every bullet. For paths that `httptest.NewRequest` would normalise, set `r.URL.Path` directly so the handler's own cleaning is what is tested. Assert exact header values, not substrings, for Content-Type and Cache-Control.
2. `routes_test.go` in PLUG: `TestRoutesAssemble`, `TestRoutesFailClosed`, `TestRoutesCoexistWithAdminPatterns` (mirror cabana's patterns as literal strings, register with a `recover` guard), `TestPluginIdentity` and `TestPluginEmbeddedTree` per the behavior block.
3. `links_test.go` in PLUG: add the ungated `TestPageLinks` for the `pageLinks` helper; leave the build-gated tests as shipped.
4. Coverage: `go -C PLUG test -count=1 -coverprofile=<tmp> ./...` then `go tool cover -func` total at least 90.0%. Add cases until it is, or record in the SUMMARY any line that is unreachable without a build and why.
5. `scripts/check-phase11.2.sh` in summercms.go (bash, `set -euo pipefail`, structure and `go test -json` detector modelled on `scripts/check-phase11.1.sh` `detect_json`: fail on a build failure, any FAIL, any SKIP of a named test, zero matched tests or "no tests to run"). This task implements `--plugin`: `go -C "$PLUG" vet ./...`, the named tests from steps 1-3 run with `-json` through the detector, and the coverage threshold; it prints `phase11.2 plugin passed`. Paths resolve from the script location (`APP="$ROOT/../sm-summercms-app"`). Unknown modes print usage and exit 2. Later tasks add the other modes.
Commit in PLUG as `test: cover every static serving rule and the plugin routes`, and in summercms.go as `test(11.2): add the phase gate with the plugin stage`.
</action>
<verify>
<automated>go -C ../sm-summercms-app/plugins/golem15/summercms vet ./... && go -C ../sm-summercms-app/plugins/golem15/summercms test ./... -run '^(TestStatic|TestNewHandlersMissingIndex|TestContentType|TestSiteImmutable|TestRoutes|TestPlugin|TestPageLinks)' -count=1 -v</automated>
<fails_when>non-zero exit, a "--- FAIL" line, or "no tests to run"</fails_when>
<automated>scripts/check-phase11.2.sh --plugin</automated>
<fails_when>non-zero exit, a coverage line below 90.0%, or no "phase11.2 plugin passed" line</fails_when>
</verify>
<acceptance_criteria>
- `go -C ../sm-summercms-app/plugins/golem15/summercms test -cover ./... -count=1` prints a `coverage:` value of at least 90.0%.
- `go -C ../sm-summercms-app/plugins/golem15/summercms test ./... -run '^(TestStatic.*|TestRoutes.*|TestPlugin.*)$' -count=1 -v` prints at least 10 `--- PASS` lines (subtests included).
- Changing `siteImmutable` to also return true for `_nuxt/builds/` paths in a scratch copy makes `TestStaticSite` fail (checked once by hand during execution and recorded in the SUMMARY), proving the cache test fails when broken.
- `bash -n scripts/check-phase11.2.sh` exits 0 and `scripts/check-phase11.2.sh --bogus` exits 2.
</acceptance_criteria>
<done>Every static serving rule and route behaviour of the site plugin is pinned by a test that fails when the rule breaks, the plugin package is at 90% or more, and the phase gate runs that stage fail-closed.</done>
</task>
<task type="auto">
<name>Task 2: Every new framework and app branch is pinned: site_url and site_label rules, override precedence, header rendering, CLI flags, terminal-check helpers and the TypeScript edge cases</name>
<files>internal/docsite/load_test.go, internal/docsite/theme_test.go, cmd/summer/docs_test.go, ../sm-summercms-app/terminal_check_test.go, ../sm-summercms-app/vue-summercms-app/tests/terminal.test.ts, ../sm-summercms-app/vue-summercms-app/tests/scrollSpy.test.ts, scripts/check-phase11.2.sh</files>
<read_first>
- internal/docsite/load.go, internal/docsite/docsite.go, internal/docsite/emit.go and internal/docsite/theme/templates/header.html (as shipped by plan 11.2-02: checkSiteURL, siteLabel, the override block in load)
- internal/docsite/load_test.go (TestParseSite table), internal/docsite/theme_test.go (themeTree, buildTheme, TestSiteLink), cmd/summer/docs_test.go (TestDocsBuildSiteFlags, TestToolCommandNames helpWants)
- ../sm-summercms-app/terminal_check_test.go (helpers as shipped)
- ../sm-summercms-app/vue-summercms-app/tests/terminal.test.ts, tests/scrollSpy.test.ts, app/utils/terminal.ts, app/utils/scrollSpy.ts (as shipped by plan 11.2-01)
</read_first>
<behavior>
- checkSiteURL accepts `https://acme.example`, `https://acme.example/`, `http://acme.example:8080/x`, `/` and `/home`; rejects the empty string, `javascript:alert(1)`, `JavaScript:alert(1)`, `data:text/html,x`, `//acme.example`, `acme.example`, `docs/x`, `https://`, `https://user:pw@acme.example`, `ftp://acme.example`, `/ x`, a value with a newline and a value with a leading tab.
- siteLabel returns a trimmed explicit label; else `acme.example` for `https://acme.example/docs`, `acme.example:8080` for `http://acme.example:8080/`; else `Home` for `/` and `/home`.
- ParseSite: `site_url` alone is accepted; `site_label` without `site_url`, a blank label and a two-line label are rejected with their messages.
- Load precedence: a yaml `site_url` plus an `Options.SiteURL` uses the option; a yaml `site_label` plus `Options.SiteLabel` uses the option; `Options.SiteLabel` with no URL anywhere fails with the `--site-label needs` message; an invalid `Options.SiteURL` fails with the `--site-url:` message; a yaml URL with no label derives the label.
- Rendering: a label `<b>&` appears as `&lt;b&gt;&amp;` in the header; the link is on `index.html`, a section page and `404.html`; with nothing set, the header contains the wordmark's closing tag immediately followed by a newline and `<div class="header-spacer">`.
- CLI: `summer help docs:build` and `summer help docs:serve` (or the command's flag listing the existing help test uses) list `--site-url` and `--site-label`; `docs:build --site-label x` with no site URL returns an error.
- loadTerminal reads the real `vue-summercms-app/app/data/terminal.json` as 3 groups and 6 commands, and returns an error for a missing file and for malformed JSON.
- terminalScript with no override returns the six commands joined by `\n`; with an override `/tmp/fw` the first command becomes `git clone /tmp/fw` and the other five are unchanged.
- terminalEnv drops `SUMMER_ENV`, `SUMMER_DATABASE__DSN` and `GOWORK`, keeps `HOME`, sets `GOBIN`, makes `PATH` start with the gobin directory followed by `:`, and sets `PATH` to the gobin directory when the base has no `PATH`.
- copyPayload: three groups give the six-line payload; an empty list gives `''`; a group with no commands adds nothing; activeSection: the exact-140 boundary is inactive, a custom threshold is honoured, and negative tops (sections scrolled past) still count.
</behavior>
<action>
1. summercms.go: add `TestCheckSiteURL`, `TestSiteLabel` and `TestSiteURLPrecedence` to `internal/docsite/load_test.go` and new `TestParseSite` rows; extend `TestSiteLink` in `theme_test.go` with the escaping, 404-page, section-page and exact-unset-bytes cases; add `TestDocsSiteFlagsInHelp` and the label-without-URL error case to `cmd/summer/docs_test.go`. Keep `go test -cover ./internal/docsite` at or above 85.0%.
2. APP `terminal_check_test.go`: add the ungated `TestLoadTerminal`, `TestTerminalScript` and `TestTerminalEnv` per the behavior block; `TestTerminalCommands` stays gated.
3. SITE tests: add only the behavior cases that the plan 11.2-01 tests do not already cover (check first; if all are covered, leave the files unchanged and say so in the SUMMARY).
4. `scripts/check-phase11.2.sh`: add `--framework` (`go vet ./...`; `go test ./internal/docsite ./cmd/summer -count=1 -json` through the detector with the named tests `TestDocsTree`, `TestDocsBuildRealTree`, `TestParseSite`, `TestCheckSiteURL`, `TestSiteLabel`, `TestSiteURLPrecedence`, `TestSiteLink`, `TestDocsBuildSiteFlags`, `TestDocsSiteFlagsInHelp`; docsite coverage at least 85.0%; then `scripts/check-phase11.1.sh --docs` and `--forbidden`), `--app` (`go -C "$APP" vet ./...`; `go -C "$APP" list ./...` must print only the app package; named `TestLoadTerminal`, `TestTerminalScript`, `TestTerminalEnv`), and `--site` (`pnpm -C "$SITE" install --frozen-lockfile`, `pnpm -C "$SITE" run generate`, `pnpm -C "$SITE" test` with a `# fail 0` line required).
Commit per repository: summercms.go `test(11.2): cover site_url and site_label and gate the framework, app and site stages`; APP `test: cover the terminal check helpers`; SITE `test: cover terminal and scroll-spy edge cases` (only if changed).
</action>
<verify>
<automated>go vet ./... && go test ./internal/docsite ./cmd/summer -run '^(TestParseSite|TestCheckSiteURL|TestSiteLabel|TestSiteURLPrecedence|TestSiteLink|TestDocsBuildSiteFlags|TestDocsSiteFlagsInHelp|TestDocsTree)$' -count=1 -v</automated>
<fails_when>non-zero exit, a "--- FAIL" line, "no tests to run", or fewer than eight top-level "--- PASS" lines</fails_when>
<automated>go -C ../sm-summercms-app test -run '^(TestLoadTerminal|TestTerminalScript|TestTerminalEnv)$' -count=1 -v .</automated>
<fails_when>non-zero exit, a "--- FAIL" or "--- SKIP" line, or fewer than three "--- PASS" lines</fails_when>
<automated>scripts/check-phase11.2.sh --framework && scripts/check-phase11.2.sh --app && scripts/check-phase11.2.sh --site</automated>
<fails_when>non-zero exit, a docsite coverage value below 85.0%, or a missing "phase11.2 framework passed", "phase11.2 app passed" or "phase11.2 site passed" line</fails_when>
</verify>
<acceptance_criteria>
- `go test -cover ./internal/docsite -count=1` prints a `coverage:` value of at least 85.0%.
- `go test ./internal/docsite -run '^TestCheckSiteURL$' -count=1 -v` prints at least 18 `--- PASS: TestCheckSiteURL/` subtest lines (five accepted, thirteen rejected values).
- Making `checkSiteURL` accept `//host` URLs in a scratch copy makes `TestCheckSiteURL` fail (checked once by hand and recorded in the SUMMARY).
- `pnpm -C ../sm-summercms-app/vue-summercms-app test` prints `# fail 0`.
</acceptance_criteria>
<done>Every new framework, app-helper and TypeScript branch has a test, docsite coverage holds at 85% or more, and the gate's framework, app and site stages pass fail-closed.</done>
</task>
<task type="auto">
<name>Task 3: The whole phase passes one gate end to end, and VALIDATION.md is validated</name>
<files>scripts/check-phase11.2.sh, .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md</files>
<read_first>
- scripts/check-phase11.2.sh (as written in Tasks 1-2)
- ../sm-summercms-app/scripts/build.sh, scripts/smoke.sh, scripts/check-deploy.sh (as shipped by plan 11.2-02)
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md (draft map and manual-only rows)
- .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-0{1,2}-PLAN.md verify blocks (the commands to list per task)
- .planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-VALIDATION.md (a validated example of the format)
</read_first>
<action>
1. `scripts/check-phase11.2.sh`: add the remaining stages. `--built`: run `"$APP/scripts/build.sh"` in release mode when `git -C "$ROOT" rev-parse -q --verify refs/tags/v0.1.0` succeeds, else `build.sh dev` (and say which); then run the plugin with `SUMMERCMS_REQUIRE_BUILD=1` through the detector requiring `TestLandingLinks`, `TestTerminalCommandsInPage` and `TestDocsHeaderSiteLink` to PASS (a SKIP fails the stage). `--smoke`: `"$APP/scripts/smoke.sh"` and require `smoke: ok`. `--deploy`: `"$APP/scripts/check-deploy.sh"` and require `check-deploy: ok`. `--terminal`: `SUMMERCMS_TERMINAL_CHECK=1 SUMMERCMS_CLONE_URL="$ROOT"` with `TestTerminalCommands` required to PASS; with `--verbatim` the clone override is left unset (the cutover run after D-38). `--full`: `go vet ./... && go test ./... -count=1` in summercms.go (Docker suites included). `--all`: framework, plugin, app, site, built, smoke, deploy, terminal and full, in that order, then `phase11.2 all passed`. Every stage prints `phase11.2 <stage> passed`.
2. Run `scripts/check-phase11.2.sh --all` until it passes. Fix only test or gate defects; a production defect gets its fix plus a failing-then-passing test in the same commit and a SUMMARY entry.
3. `11.2-VALIDATION.md`: fill the per-task verification map with one row per task of plans 11.2-01, 11.2-02 and 11.2-03 (task id, the SC or D-ID it proves, test type, the exact automated command from the plan, file exists, status green), set the frontmatter to `status: validated`, `nyquist_compliant: true`, `wave_0_complete: true`, tick the Wave 0 and sign-off checklists, and keep the manual-only rows (visual fidelity at 1280/721/720/375px, rome bring-up, external links via `TestExternalLinks`, and the verbatim clone via `--terminal --verbatim`, both at cutover after D-38).
Commit the gate in summercms.go as `test(11.2): complete the phase gate`, then VALIDATION.md separately as `docs(11.2): validate the phase verification map`.
</action>
<verify>
<automated>scripts/check-phase11.2.sh --all</automated>
<fails_when>non-zero exit, a line starting "refuse:" or "FAIL", or no "phase11.2 all passed" line</fails_when>
</verify>
<acceptance_criteria>
- `scripts/check-phase11.2.sh --all` prints `phase11.2 all passed` and one `phase11.2 <stage> passed` line for each of framework, plugin, app, site, built, smoke, deploy, terminal and full.
- `grep -n 'nyquist_compliant: true' .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md` and `grep -n 'status: validated' .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md` both find a match.
- `grep -c '⬜ pending' .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md` prints 0 for automated rows (manual-only rows sit in their own table).
- `grep -n 'TestExternalLinks' .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md` finds the manual-at-cutover row.
</acceptance_criteria>
<done>One command proves the whole phase across the four repositories, and VALIDATION.md records every automated check as green with the cutover-only checks kept as manual rows.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| test results → phase sign-off | A gate that misreads skipped or filtered tests certifies behaviour it never measured |
| environment gates → integration tests | Build-dependent tests skip when their variable is unset |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-11.2-15 | Repudiation (false green) | scripts/check-phase11.2.sh | medium | mitigate | `go test -json` detector requires every named test to PASS; a SKIP, FAIL, zero match or "no tests to run" fails the stage; `--bogus` exits 2 |
| T-11.2-16 | Tampering (silent skip) | build-gated plugin tests and the terminal check | medium | mitigate | The gate sets `SUMMERCMS_REQUIRE_BUILD=1` and `SUMMERCMS_TERMINAL_CHECK=1` itself and requires PASS; `requireBuild` fails rather than skips when the variable is set and the tree is missing |
| T-11.2-SC | Tampering | dependency installs | low | accept | Tests use the stdlib and the existing node:test runner; no package is added; `pnpm install --frozen-lockfile` reuses plan 11.2-01's lockfile |
</threat_model>
<verification>
- `scripts/check-phase11.2.sh --all` prints `phase11.2 all passed`.
- `go -C ../sm-summercms-app/plugins/golem15/summercms test -cover ./...` at least 90.0%; `go test -cover ./internal/docsite` at least 85.0%.
- `git status --porcelain` is empty in summercms.go, sm-summercms-app, sm-summercms-plugin and vue-summercms-app after the last commit.
</verification>
<success_criteria>
- SC5: the new Go code has unit tests delivered in the phase's last plan (plugin at 90% or more, docsite at 85% or more, app helpers covered).
- The gate proves SC1 to SC4 automatically where they are automatable; the manual rows (visual UAT, rome bring-up, cutover link and clone checks) are recorded in VALIDATION.md.
</success_criteria>
## Artifacts this phase produces
- Plugin tests: `TestStaticSite`, `TestStaticDocs`, `TestStaticConditionalAndRange`, `TestStaticNoBlockingHeaders`, `TestStaticRedirectLocations`, `TestStaticMissing404Page`, `TestNewHandlersMissingIndex`, `TestContentType`, `TestSiteImmutable`, `TestRoutesAssemble`, `TestRoutesFailClosed`, `TestRoutesCoexistWithAdminPatterns`, `TestPluginIdentity`, `TestPluginEmbeddedTree`, `TestPageLinks`.
- Framework tests: `TestCheckSiteURL`, `TestSiteLabel`, `TestSiteURLPrecedence`, extended `TestParseSite` and `TestSiteLink`, `TestDocsSiteFlagsInHelp`.
- App tests: `TestLoadTerminal`, `TestTerminalScript`, `TestTerminalEnv`.
- Gate: `scripts/check-phase11.2.sh` with modes `--framework`, `--plugin`, `--app`, `--site`, `--built`, `--smoke`, `--deploy`, `--terminal` (with `--verbatim`), `--full`, `--all`.
- Planning: validated `11.2-VALIDATION.md`.
<output>
Create `.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-03-SUMMARY.md` when done
</output>

View File

@@ -0,0 +1,3 @@
# Phase 11.2 API coverage
No external API integration: phase builds a static Nuxt landing page, a static-file site plugin that embeds it with the docs build, and nginx/supervisor deploy configs; the browser Clipboard API and the links to git.golem15.com and golem15.com are not service integrations.

View File

@@ -68,6 +68,11 @@
"name": "summercms.io Alpha 0.1 landing page on SummerCMS",
"status": "pending"
},
{
"number": "11.3",
"name": "Newsletter plugin and signup on summercms.io",
"status": "pending"
},
{
"number": "12",
"name": "Płytarium API — Collections and Albums",
@@ -91,8 +96,8 @@
],
"next": {
"command": "/gsd:progress --next",
"label": "Advance to the next step (verify)",
"reason": "Phase 11.1 of 19 · ready to verify"
"label": "Advance to the next step",
"reason": "Phase 11.2 of 20 · executing"
},
"updated_at": "2026-10-01T11:49:17.622Z"
"updated_at": "2026-10-01T13:40:22.112Z"
}