From e032a65fc922f35996efa97933d0c229ac90f671 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 1 Oct 2026 15:08:12 +0200 Subject: [PATCH] docs(11.2): research the landing page phase and add validation strategy --- .../11.2-RESEARCH.md | 972 ++++++++++++++++++ .../11.2-VALIDATION.md | 91 ++ 2 files changed, 1063 insertions(+) create mode 100644 .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md create mode 100644 .planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md diff --git a/.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md b/.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md new file mode 100644 index 0000000..db2b2f4 --- /dev/null +++ b/.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-RESEARCH.md @@ -0,0 +1,972 @@ +# Phase 11.2: summercms.io Alpha 0.1 landing page on SummerCMS - Research + +**Researched:** 2026-10-01 +**Domain:** Nuxt 4 static site embedded in a SummerCMS Go binary (surf routing, go:embed, docsite build), Postgres 15 verification, nginx + supervisor deploy +**Confidence:** HIGH for the framework surface, the docs build, PG15 and the Nuxt build behaviour (all exercised in this session). MEDIUM for the rome deploy details, because the server was not reachable. + + +## User Constraints (from CONTEXT.md) + +The 11.2 planner applies D-01 to D-05, D-07, D-09, D-12 and D-23 to D-45. D-06, D-08, D-10 and D-11 are superseded, and D-13 to D-22 belong to Phase 11.3. Both groups are left out below. + +### Locked Decisions + +**Repos and layout (2026-09-29, still in force)** +- **D-01:** Three new repos follow the `sm-` naming convention (`oc-`/`wn-` in October/Winter): + - `sm-summercms-app` is the root app. It builds the binary, holds the other pieces and wires the plugins. + - `vue-summercms-app` is the Nuxt 4 site, held inside the root app as `vue-fonoteka-app` is in the fonoteka project. + - `sm-summercms-plugin` is the site plugin. It holds everything specific to summercms.io: serving the embedded Nuxt build at `/`, mounting the 11.1 docs output at `/docs`, and any site-specific data or routes. This is the proven WinterCMS pattern from `figs.org.pl` (`figs/website`) and golem15.com. + + Public static-site serving goes in `sm-summercms-plugin`, not in a new framework module. (The fourth repo, `sm-newsletter-plugin`, moved to 11.3.) — **Reversibility:** costly — repo names and module paths are referenced by the go.work/replace wiring and every import. +- **D-02:** Go module paths live under `git.golem15.com/golem15/`, like the framework (`git.golem15.com/golem15/summercms`): for example `git.golem15.com/golem15/sm-summercms-plugin`. — **Reversibility:** one-way — a published Go module path is baked into every importer. +- **D-03:** The root app wires the framework and the site plugin the way `fonoteka.go` does today (a go.work workspace with a local replace during development). + +**Website build and hosting (2026-09-29, still in force)** +- **D-04:** The site is Nuxt 4 with `nuxt generate` (static output), the same stack as `vue-fonoteka-app`. Every route is real prerendered HTML, so link previews and search engines see content. +- **D-05:** The generated site and the 11.1 docs output are embedded in the Go binary with `go:embed`. One binary serves `/` and `/docs`. The build order (nuxt generate, then `summer docs:build`, then `go build`) is documented and scripted in the root app. +- **D-07:** The site's served responses must be indexable. The admin-only `boardwalk` behaviour (`noindex, nofollow`, admin CSP, base-path rewrite) must not apply to the public site. The site plugin sets its own cache headers: immutable for hashed assets, no-cache for HTML. + +**Landing page content (2026-09-29)** +- **D-09:** The "From WinterCMS" section carries the seasons lineage: it started in October (OctoberCMS), went through Winter (WinterCMS), and now it's time for Summer. The handoff's copy is final. The theme lives in the brand (sun, "A new dawn", "Something bright is here") and is not added as extra copy. +- **D-12:** A "Built by Golem15" credit links to golem15.com. It is an addition to the handoff's footer, styled as a footer link. + +**Design handoff** +- **D-23:** The handoff in `design/` (`README.md`, `SummerCMS Landing.dc.html`) is high fidelity. Colors, type, spacing, copy, breakpoints and the two interactions (scroll-spy and copy-to-clipboard) are matched exactly. The only copy deviations are those decided here: D-12 (credit), D-39 (clone line) and D-26 (the Postgres chip). + +**Database at runtime** +- **D-24:** The binary keeps the stock `summer serve` path, which opens Postgres unconditionally (`modules/surf/serve.go:40`). There is no framework change to make the DB optional. `rome` already runs PostgreSQL 15.19 (Debian 12). +- **D-25:** Verify the framework on PostgreSQL 15. Run the framework's database test suites (lagoon, conga and the rest that use `postgres:16-alpine`) against a `postgres:15` image. If they pass, change the documented requirement from "PostgreSQL 16" to "PostgreSQL 15 or newer" in the root `README.md`, `docs/setup/installation.md` and any other page that states it (the docs checker must stay green). **If any test fails on 15, stop and ask the user.** Do not quietly work around it or upgrade rome. +- **D-26:** The "PostgreSQL 16" chip in the Get started section follows D-25 and reads "PostgreSQL 15+". +- **D-27:** The app uses a dedicated Postgres role (`summercms`) that owns a dedicated database (`summercms_io`). The password lives in the app's config or env on the server, never in git. DEPLOY.md documents `createuser`/`createdb`, and every deploy runs the app's `migrate` command before restarting. + +**Server and deploy flow** +- **D-28:** The binary is built locally for linux/amd64 by a script in `sm-summercms-app` (nuxt generate, then `summer docs:build --base-url /docs`, then `go build`), then rsynced to rome with its config. The server needs neither Go nor Node. +- **D-29:** `/backend` and the admin API are not exposed publicly. nginx proxies only `/` and `/docs` to the binary and denies the admin paths. Nothing needs administering until 11.3. +- **D-30:** TLS uses rome's existing certbot / Let's Encrypt setup. DEPLOY.md shows the nginx server block with the certbot-managed certificate paths, HTTP to HTTPS redirect, gzip and proxy headers. +- **D-31:** Cutover replaces the existing summercms.io nginx server block in place. The current "Under construction" block is saved in DEPLOY.md (or alongside it) as the rollback. +- **D-32:** The binary runs under supervisor as a dedicated system user on a localhost-only port. The user, port and paths are Claude's discretion and are written down in DEPLOY.md and the supervisor program config. + +**Design to Nuxt** +- **D-33:** The site is i18n-ready. `@nuxtjs/i18n` is installed and configured with the single locale `en`, and all page copy lives in `locales/en.json`. 11.3 adds `pl.json` and a switcher without touching component markup. +- **D-34:** Roboto (300/400/500/700) and Roboto Mono (400/500) are self-hosted through `@nuxt/fonts`, the same module as `vue-fonoteka-app`. They are downloaded at build time and embedded, so visitors make no request to Google. +- **D-35:** Styles are plain CSS. The handoff tokens are CSS custom properties on `:root`, used by scoped component styles. No Tailwind. +- **D-36:** The sun artwork is the original from the live site, `http://summercms.io/logo.png` (746×744 transparent RGBA PNG, 513 KB). It replaces the handoff's `sun-crop.png` placeholder. Resized and compressed copies are made for the hero badge (180px), the header logo (30px), the favicon set and the OG image. +- **D-37:** SEO uses the full `@nuxtjs/seo` module, as `vue-fonoteka-app` does: meta, Open Graph and Twitter tags, OG image, schema.org, sitemap and robots. Whatever it generates must work with `nuxt generate` and be embedded (no runtime OG rendering in the Go binary). +- **D-45:** The prototype's `showRays` and `scrollSpy` flags become Nuxt app config, both defaulting to `true`. + +**Get started accuracy** +- **D-38:** Both Source links point to `https://git.golem15.com/golem15/summercms`. The repo currently requires login, so making it public is a launch checklist item in DEPLOY.md, done manually by the user before cutover. The link check (roadmap criterion 3) verifies anonymous access at cutover time. +- **D-39:** The terminal card gets a clone step so it works when pasted into a fresh shell: a `# get the framework` comment, then `$ git clone https://git.golem15.com/golem15/summercms` and `$ cd summercms`, before the existing `go install ./cmd/summer` group. The Copy button copies every command line in order (now six), without `$` or comments. +- **D-40:** A scripted check proves the terminal commands work verbatim. It runs them in a temp clone with a temp `GOBIN`, ending with `./bin/hello greeter:hello`. If that command needs a database or other setup, the check provides it, and the copy is not changed again without asking. The command list lives in one place shared by the page and the check, or the check asserts that the two match, so they cannot drift. + +**Docs and release** +- **D-41:** Framework change: an optional `site_url` key in `docs/site.yaml` adds a link back to the main site in the docs header (`internal/docsite/theme/templates/header.html`), for example "← summercms.io". When the key is unset, the output is unchanged. The change also updates the docs page that describes `site.yaml`, and `summer docs:build --check` and `TestDocsTree` stay green. summercms.io sets it to `/`. +- **D-42:** The framework is tagged `v0.1.0` (Alpha 0.1). `sm-summercms-app` requires `git.golem15.com/golem15/summercms v0.1.0` (with the local replace during development), and the embedded docs are built from that tag, so the page, the docs and the code agree. The user confirms before the tag is created and pushed. — **Reversibility:** one-way — a pushed Go module version tag is cached by proxies and cannot be reused. +- **D-43:** The new repos live as siblings of `summercms.go` in the meta repo directory (`summercms/sm-summercms-app/`, like `fonoteka.go`). `vue-summercms-app` and `sm-summercms-plugin` are submodules inside `sm-summercms-app`. Creating the remotes on git.golem15.com is a manual step the plan lists for the user. +- **D-44:** Every link on the page is verified against the built docs output (roadmap criterion 3), not assumed. As of 2026-10-01 all ten `/docs/...` targets in the handoff exist as pages in `docs/`. + +### Claude's Discretion +- The system user, port and filesystem paths on rome (D-32). +- Whether the Nuxt build and the docs are embedded by `sm-summercms-plugin` or by the root app, and how the plugin mounts `/docs`. +- The 404 handling for unknown paths (for example Nuxt's generated `404.html` with a 404 status). +- Image formats and sizes derived from the sun artwork, and the OG image layout within the brand. +- Where the D-40 command check lives (root app script or test) and how it shares the command list with the page. +- How `prefers-reduced-motion` affects smooth scrolling. + +### Deferred Ideas (OUT OF SCOPE) +- Composing and sending newsletters, templates and the campaign CLI, ported from the PHP plugin in a later phase. That phase also merges confirmed subscribers with the user-based audience. +- CSV export of subscribers. It waits for a framework export or toolbar capability. +- Renaming `fonoteka.go` to `sm-fonoteka-app` is a separate task. +- Translating the docs into Polish. +- `go install git.golem15.com/golem15/summercms/cmd/summer@latest` as the install command. This needs the module path to be go-gettable (a public repo plus go-import meta). +- A CI pipeline that builds and publishes the binary, instead of local build and rsync. +- Making the database optional in `summer serve`, for apps without data. +- (Also out of scope: newsletter signup, the Polish locale and the subscriber admin, all in Phase 11.3.) + + + +## Choices made in this planning session (orchestrator-provided, binding for the planner) + +- **Plan count is fixed at 3 plans:** + - **01**: the Nuxt site (`vue-summercms-app`). + - **02**: framework tweaks (PG15 verification and docs, the optional `site_url`, the v0.1.0 tag checkpoint), then `sm-summercms-plugin`, `sm-summercms-app`, the build script, the D-40 command check, the link check, and DEPLOY.md with the nginx and supervisor configs. + - **03**: unit tests. +- **No UI-SPEC.md.** The `design/` handoff (`README.md` and `SummerCMS Landing.dc.html`) is the design contract (D-23). + + + +## Phase Requirements + +No requirement IDs are mapped (ROADMAP says "Requirements: TBD"). The five ROADMAP success criteria are the acceptance contract: + +| ID | Success criterion (ROADMAP § Phase 11.2) | Research support | +|----|------------------------------------------|------------------| +| SC1 | `nuxt generate` in `vue-summercms-app` produces the landing page matching the handoff, with a sticky header and scroll-spy, a hero, the Why, Features, From WinterCMS and Get started sections, a terminal card with a working Copy button, and a footer. It is responsive down to phone width, and the section links hide at 720px or less. | §Design handoff inventory; Nuxt pattern 1; spike-verified `nuxt.config.ts` (Pitfalls 1–6) | +| SC2 | One `sm-summercms-app` binary embeds the Nuxt output and the 11.1 docs build, and serves `/` and `/docs` with indexable responses (no admin `noindex`/CSP). Cache headers are immutable for hashed assets and no-cache for HTML. | §Framework surface; Pattern 2 (site handler); cache table; Pitfalls 7–11 | +| SC3 | Every link on the page resolves, including the eight feature-tile `/docs/...` links and the WinterCMS concept-map link, verified against the built docs. | All ten targets verified against a real `docs:build --base-url /docs`; the `.html` URL finding; link-check design | +| SC4 | A scripted build (`nuxt generate`, `summer docs:build`, `go build`) and a `DEPLOY.md` cover building, uploading, a supervisor program config and an nginx site config (TLS, proxy, gzip). Following them on a clean server brings the site up. | §Build script; §DEPLOY.md content requirements; boot config requirements (Pitfall 12) | +| SC5 | The new Go code has unit tests, delivered in the last plan. | §Validation Architecture | + + +## Project Constraints (from CLAUDE.md) + +- Standard library first (net/http ServeMux, html/template, encoding/json). A dependency is added only when the research doc or a phase decision names it. **This phase needs no new Go dependency.** The site plugin is stdlib plus framework modules. +- Plugins are compiled Go modules registered at build time. No runtime plugin loading. +- `go vet` and `go test ./...` stay green at every commit, in every repo (framework, app, plugin). +- Unit tests go in the last plan of the phase (plan 03). Earlier plans may carry smoke tests. +- Commits: never add co-author tags. One logical change per commit. Planning docs and code go in separate commits. +- Docs rule: a change to exported API, config keys or CLI commands also updates the module README and the affected `docs/` pages in the same change. `go test ./cmd/summer -run TestDocsTree` and `summer docs:build --check` must stay green. D-41 adds a `site.yaml` key (and, as recommended below, a `docs:build` flag), so `docs/console/utilities.md` changes in the same commit. +- Framework docs never name a consuming application. The D-41 docs example uses a neutral URL (`https://acme.example/`), not summercms.io. The forbidden-name checker is `(?i)fonoteka|p[lł][yý]tarium` [VERIFIED: internal/docsite/check_forbidden.go:14], so "summercms.io" would pass the checker, but the CLAUDE.md rule still applies. +- Config keys named in docs are not checked automatically (deferred in 11.1). Review the new `site_url` key mentions by hand. +- Core plugin contracts (user, blog, pages, payment) are unaffected by this phase. +- Global user rule: core plugins must not get breaking changes. This phase touches none. + +## Summary + +Everything the phase needs already exists in the framework, apart from the two small D-41 and D-25 edits. A site plugin registers `GET /` and `GET /docs/{path...}` in a raw group on the surf router. These were exercised against the real `surf.Assemble`. Go's ServeMux gives the catch-all `/` the lowest precedence, so it does not conflict with any cabana admin pattern. `/docs` is redirected to `/docs/` automatically. `POST /` returns 405. When the site plugin declares no admin controllers, cabana does not activate, so no `/backend` routes exist at all and no `admin.jwt.secret` is needed. `summer serve` still opens Postgres unconditionally. It also needs `app.key`, `storage.uploads.bucket_url` and numeric `http.body_limits`, so the app ships those configs, and the database password goes in a server-side `.env`. + +Five findings change the plan: + +1. **Docs pages are `.html` files, not directories.** The handoff's `/docs/backend/admin-spa` resolves only if the site handler maps extension-less paths to `.html`. The page links should use the canonical `.html` form. +2. **`nuxt generate` fails** (exit 1, "Exiting due to prerender errors") on any `/docs` link unless `nitro.prerender.ignore: ['/docs']` is set. This was reproduced and the fix confirmed. +3. **Nuxt writes `_nuxt/`, `_fonts/`, `_i18n/` and `_payload.json`.** `go:embed` drops names that start with `_` unless the pattern uses `all:`. +4. **Docs assets are not content-hashed** (`/docs/assets/site.css`), and neither is `/_nuxt/builds/latest.json`. "Immutable for hashed assets" therefore means only `/_nuxt/*` (excluding `builds/`) and `/_fonts/*`. Everything else gets no-cache with an ETag. +5. **The DB suites pass on PostgreSQL 15.** All nine testcontainers harnesses were run against `postgres:15` (15.16) from a scratch export of HEAD. Every package passed, so D-25's stop-and-ask branch is not expected to trigger. Plan 02 still re-runs the check formally. + +The D-40 terminal commands were run verbatim in a fresh local clone with a temp `GOBIN`. `./bin/hello greeter:hello` needs no database and printed `name=hello-app ... handled=true`, exit 0. The only blocker for running the commands exactly as written is the `git clone` URL. `https://git.golem15.com/golem15/summercms` returns 404 to anonymous users and `git ls-remote` asks for credentials (verified). So the check needs a clone-source override until the repo is public, and it runs verbatim at cutover. + +**Primary recommendation:** +- Embed `all:public` in `sm-summercms-plugin`. The build script fills `public/site` (Nuxt) and `public/docs` (docs from a `git archive v0.1.0` export), and a placeholder `public/README.md` is committed. +- Serve both trees with one stdlib handler that has its own MIME table, extension-less `.html` resolution, 404 pages with a 404 status, and per-path cache rules. +- Keep the Nuxt config exactly as spike-verified below, with versions pinned to `vue-fonoteka-app`'s lockfile. + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| Landing page markup, copy, styles, scroll-spy, copy-to-clipboard | Browser / Client (Nuxt SSG output) | Build time (`nuxt generate` prerender) | Static HTML prerendered at build time; the two interactions are client-only JS. | +| SEO metadata, sitemap, robots, schema.org | Build time (`@nuxtjs/seo` during prerender) | — | Emitted as static files and tags; nothing renders at runtime (D-37). | +| Fonts | Build time (`@nuxt/fonts` downloads to `/_fonts`) | CDN/Static (served by the binary) | No Google request at runtime (D-34). | +| Docs site | Build time (`summer docs:build` from the v0.1.0 tag) | Static (served by the binary) | Deterministic output: two builds were byte-identical. | +| Serving `/` and `/docs`, cache headers, MIME types, 404s | API / Backend (Go binary, `sm-summercms-plugin` handler on the surf mux) | — | One binary (D-05); the plugin owns site specifics (D-01). | +| TLS, gzip, HTTP→HTTPS, www→apex, denying `/backend`, method restriction | Reverse proxy (nginx on rome) | — | D-29/D-30; the binary listens on loopback only. | +| Process lifecycle | OS (supervisor) | — | D-32. | +| Persistence | Database (Postgres 15 `summercms_io`) | — | Required by `serve` (D-24); only framework tables (system_files, backend admin, queue) are migrated. | + +## Standard Stack + +### Core (frontend: `vue-summercms-app`) + +Versions are pinned to what `vue-fonoteka-app` runs today. Read from its `node_modules` this session, and the same versions were installed and built in the spike. + +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| nuxt | 4.4.8 (latest 4.5.2) | SSG via `nuxt generate` | D-04; fonoteka-proven [VERIFIED: fonoteka node_modules + spike build] | +| @nuxt/fonts | 0.14.0 (= latest) | Self-host Roboto / Roboto Mono | D-34 [VERIFIED: spike downloaded fonts to `/_fonts`, no googleapis/gstatic in HTML] | +| @nuxtjs/i18n | 10.4.0 (latest 10.6.0) | Single `en` locale, i18n-ready | D-33 [VERIFIED: spike] | +| @nuxtjs/seo | 5.2.1 (latest 5.3.16) | meta/OG/Twitter, schema.org, sitemap, robots, link checker | D-37 [VERIFIED: spike output] | +| vue | 3.5.35 | — | Peer of nuxt | +| typescript (dev) | ~5.9.0 | Type checking | Mirror fonoteka. Do **not** take TS 7.0.2 (published 2026-10-01, the Go-native compiler). Nuxt/vue-tsc compatibility is unverified [ASSUMED risk]. | + +Sub-modules resolved inside `@nuxtjs/seo` 5.2.1 in the spike: `@nuxtjs/sitemap` 8.6.1, `nuxt-og-image` 6.5.x, `nuxt-schema-org` 6.2.0, `nuxt-seo-utils` 8.2.1, `nuxt-site-config` 4.0.8, `@nuxtjs/robots` 6.0.9, `nuxt-link-checker` 5.0.10. `@nuxtjs/seo` uses caret ranges, so only a committed `pnpm-lock.yaml` pins them. Use `pnpm install --frozen-lockfile` in the build script. + +### Supporting + +| Tool | Version (local) | Purpose | When to Use | +|------|-----------------|---------|-------------| +| pnpm | 11.3.0 | Package manager (fonoteka uses pnpm, lockfile v9) | Always. Copy fonoteka's `pnpm-workspace.yaml` `allowBuilds` (minus `protobufjs`, unused here). | +| Node | 22.23.2 (fonoteka `.nvmrc` 22.22.0, engines `>=22.6`) | Build only | `node --test` runs `.ts` tests natively. Verified: `# pass 1` with no flags. | +| ImageMagick | 7.1.2 (`magick`, WEBP/ICO/AVIF delegates present) | One-off derivation of sun/favicon/OG images | Commit the derived images plus the script. No `sharp` dependency. | +| Go | 1.27.0 | App/plugin build, all tests | — | +| Docker + `postgres:15` (15.16) and `postgres:15-alpine` images | 29.7.2 | D-25 verification and local boot smoke | Images already pulled locally. | + +### Alternatives Considered + +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| Static `public/og-image.png` + `ogImage: { enabled: false }` | `nuxt-og-image` prerendered (zero-runtime) | Adds satori/resvg or Chromium build deps and a component. A static PNG is what fonoteka does, and it satisfies D-37 trivially. | +| Embedding in the plugin | Embedding in the root app plus an init-time setter | Keeps the app 100% generated + config, as fonoteka is, but needs a global setter and couples init order. Plugin embed is simpler. | +| `node --test` for TS logic | vitest | vitest is another dependency (latest is 5.x and fonoteka is on 3.x). Built-in `node:test` is enough for two pure functions. | +| ImageMagick one-off | sharp in devDependencies | sharp is flagged SUS (too-new, 0.35.5 published 2026-09-27) and has native binaries. It is not needed when the outputs are committed. | + +**Installation (vue-summercms-app):** +```bash +pnpm add nuxt@4.4.8 @nuxt/fonts@0.14.0 @nuxtjs/i18n@10.4.0 @nuxtjs/seo@5.2.1 vue@3.5.35 +pnpm add -D typescript@~5.9.0 +``` + +## Package Legitimacy Audit + +| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition | +|---------|----------|-----|-----------|-------------|---------|-------------| +| nuxt | npm | 10+ yrs | 2.44M/wk | github.com/nuxt/nuxt | OK | Approved | +| @nuxt/fonts | npm | 2+ yrs | 967k/wk | github.com/nuxt/fonts | OK | Approved | +| @nuxtjs/i18n | npm | 7+ yrs | 776k/wk | github.com/nuxt-modules/i18n | OK | Approved | +| @nuxtjs/seo | npm | 2+ yrs | 127k/wk | github.com/harlan-zw/nuxt-seo | SUS (reason: `too-new`, its *latest* 5.3.16 published 2026-09-12) | Approved at the pinned 5.2.1, the version already running in vue-fonoteka-app. Planner: no extra checkpoint is needed if 5.2.1 is pinned, but add one if a newer version is taken. | +| vue | npm | 10+ yrs | — | github.com/vuejs/core | SUS (reason: `too-new`, latest version recently published) | Approved at the pinned 3.5.35 (fonoteka-proven) | +| typescript | npm | 12+ yrs | — | github.com/microsoft/TypeScript | OK | Approved at ~5.9.0 | +| sharp | npm | — | 123M/wk | github.com/lovell/sharp | SUS (`too-new`) | **Not used.** ImageMagick one-off instead. | + +No `postinstall` script on nuxt, @nuxt/fonts, @nuxtjs/i18n, @nuxtjs/seo or vue (`npm view scripts.postinstall` printed nothing). +**Packages removed due to [SLOP] verdict:** none. +**Packages flagged as suspicious [SUS]:** @nuxtjs/seo and vue. Both are flagged only because a recent *latest* release exists. The recommended pinned versions are the ones already in production use in vue-fonoteka-app. If the planner takes newer versions, add a `checkpoint:human-verify` before install. +**Go:** no new modules. The plugin and app import only stdlib and `git.golem15.com/golem15/summercms/modules/...`. + +## Framework Surface (question 1) + +### How a plugin registers routes on the surf mux + +- Plugins implement `pact.HasRoutes`, `Routes(Router) error` [VERIFIED: modules/pact/capabilities.go:76-78]. `pact.Router` has `Group`, `GroupRaw`, `Get`, `Post`, `Put`, `Patch`, `Delete`, `Where` and `WhereIn` [VERIFIED: capabilities.go:63-73]. +- Each route is registered on a stdlib ServeMux as `mux.Handle(rt.method+" "+rt.path, h)` [VERIFIED: modules/surf/router.go:366]. Every route therefore has a method pattern, and Go 1.22+ precedence rules apply. A ServeMux conflict panic is turned into a boot error, `surf: route conflict for ...` (router.go:359-368). +- Non-raw routes are wrapped in `recoverJSON` (house JSON 500). Raw groups are wrapped in `recoverBare` [VERIFIED: router.go:418-422]. **Use `GroupRaw`** for static serving. Both buffer the whole response before committing it (`bufferedResponse`), which is fine for files of a few hundred KB. +- Body limits come from config: `http.body_limits.default_bytes` and `http.body_limits.upload_bytes` must be present and numeric whenever `app.Config` is non-nil, or boot fails with `surf: config %s is required` [VERIFIED: router.go:466-476, 561-565]. + +### Catch-all conflicts (verified by running code) + +A probe plugin registered `GroupRaw("", nil, ...)` with `Get("/")` and `Get("/docs/{path...}")` through the real `surf.Assemble(nil, plugins)`. Results: + +| Request | Result | +|---------|--------| +| `GET /` | 200 → site handler, `r.URL.Path="/"` | +| `GET /x/y.css` | 200 → site handler (catch-all) | +| `GET /docs` | **307**, `Location: /docs/` (ServeMux trailing-slash redirect) | +| `GET /docs/` | 200 → docs handler, `PathValue("path")=""` | +| `GET /docs/setup/installation.html` | 200 → docs handler | +| `HEAD /` | 200 (GET patterns also match HEAD) | +| `POST /` | 405 Method Not Allowed | +| `GET /backend` | 200 → **site** catch-all, because no admin is active | + +The same probe registered `GET /`, `GET /docs/{path...}` and cabana's real patterns (`GET /backend`, `GET /backend/{path...}`, `GET /backend/assets/{vendor}/{plugin}/{file...}`, `POST /backend/api/v1/auth/login`, `GET /backend/api/v1/{vendor}/{plugin}/{controller}`) on one ServeMux. **No conflict panic.** `GET /` is the least specific pattern [VERIFIED: probe test run this session]. + +- cabana activates only when some plugin declares admin controllers. `if len(items) == 0 { return nil, nil }` means no admin routes and no secret check [VERIFIED: modules/cabana/http.go:65-74]. **The site plugin must not implement `pact.HasAdminControllers`** (the `make:plugin` stub does, so do not use the stub as-is). +- `checkAdminPrefix` fails boot when a non-cabana route sits at or under `backend.uri`. It runs only when admin is active (router.go:526-534, 546-559). A catch-all `/` is not "under" `/backend`, so it would pass in 11.3 too. +- A `/docs` → `/docs/` hop: the handoff links `/docs` from three places. Either register an explicit `GET /docs` that 301s to `/docs/`, or let ServeMux's 307 stand. Recommend the explicit 301 (permanent, cacheable), or link `/docs/` directly. Both resolve. + +### boardwalk: what to copy and what not to (D-07) + +Copy the idea, not the code: +- Serve from an `fs.FS` with `http.ServeContent` (boardwalk.go:132, 147). +- Use an explicit content-type table before the `mime` fallback (boardwalk.go:34-43, 153-162). +- Set immutable cache only on a known hashed prefix: `if strings.HasPrefix(name, "assets/") { "public, max-age=31536000, immutable" } else { "no-cache" }` [VERIFIED: modules/boardwalk/boardwalk.go:142-146]. + +Do **not** copy: +- `SetSecurityHeaders` sets `X-Robots-Tag: noindex, nofollow`, `Content-Security-Policy: frame-ancestors 'none'; base-uri 'none'; object-src 'none'; script-src 'self'` and `X-Frame-Options: DENY` [VERIFIED: boardwalk.go:32, 167-173]. Nuxt emits inline scripts, so `script-src 'self'` would break hydration. +- `RewriteIndex` (`__SUMMER_ADMIN_BASE__` token). +- The SPA fallback to index.html for unknown paths. The public site wants real 404s. +- `serveIndex`'s `no-store`. + +### `summer build` and `summer.yaml` + +- `summer.yaml` has `module`, `binary` and an ordered `plugins: [{id, module}]` list [VERIFIED: internal/build/manifest.go:18-28; examples/hello/summer.yaml]. Plugin IDs must match `^[a-z][a-z0-9]*\.[a-z][a-z0-9]*$` [VERIFIED: manifest.go:16]. **`golem15.summercms` is valid.** Binary names allow letters, digits, `.`, `_` and `-` (manifest.go:149-165). **`summercms-io` is valid.** +- `summer build` writes `main.go` and `plugins.gen.go` (only when changed), checks the models leaf, then runs `go build -o bin/ .` with `GOWORK` set to the nearest `go.work` (build.go:19-77; scaffold.go:519-541). It inherits the environment, so `GOOS`/`GOARCH` pass through. Rome and the local host are both linux/amd64. +- The generated `main.go` registers `lagoon.RuntimeCommands` (`migrate`, `migrate:rollback`, `migrate:status`, `key:generate`), `conga.RuntimeCommands`, `surf.ServeCommand`, `surf.RouteListCommand`, `cabana.RuntimeCommands` and plugin `HasCommands` [VERIFIED: build.go:114-123; lagoon/commands.go:16-75]. **D-27's migrate command is `./bin/summercms-io migrate`** (`summer migrate` delegates to the same binary, cmd/summer/runtime.go:16-26). +- `compass.Load("config")` is relative to the **working directory** (build.go:105). Supervisor's `directory=` must therefore be the app dir. `SUMMER_ENV` defaults to `production`. A `.env` next to `config/` supplies `KEY=VALUE` lines not already in the environment [VERIFIED: modules/compass/README.md:90-97]. +- `summer plugin:add ` adds the plugin to `summer.yaml`, runs `go mod edit -require=@v0.0.0 -replace==./` and `go work use ` (scaffold.go:106-170, 318-370; docs/console/scaffolding.md:34). It reads the plugin ID from the plugin's `ID()` return literal (scaffold.go:284-316). Use it after creating the plugin by hand, because `make:plugin` would set the module path to `/plugins/` (scaffold.go:75), not `git.golem15.com/golem15/sm-summercms-plugin`. + +### `fonoteka.go` reference wiring (D-03) + +- `go.work`: `go 1.27.0`, `toolchain go1.27.0`, `use ( . ./plugins/golem15/user ./plugins/golem15/fonoteka )`. It does **not** `use` the framework. +- App `go.mod`: `replace git.golem15.com/golem15/summercms => ../summercms.go`, `require git.golem15.com/golem15/summercms v0.0.0`, plus replaces for the in-tree plugins. +- Plugin `go.mod`: `replace git.golem15.com/golem15/summercms => ../../../../summercms.go`. That is four levels up from `fonoteka.go/plugins/golem15/user`. +- Config: `config/{app,http,storage,database,queue,...}.yaml`. `database.yaml` has `dsn: ""` (env-provided). `app.yaml` has `key: ""` with the comment "Set SUMMER_APP__KEY". `http.yaml` has `body_limits`. `storage.yaml` has `uploads.bucket_url: "file://./storage/app/uploads"`. `queue.yaml` has `work_in_serve: true`. +- `.gitignore`: `/bin/`, `/tmp/`, `go.work.sum`. + +For `sm-summercms-app` with the plugin submodule at `plugins/golem15/summercms`, the plugin's framework replace is `../../../../summercms.go`. That is the same depth as fonoteka, and it resolves to the meta-repo sibling `summercms/summercms.go`. + +### Postgres-at-boot config the app must ship (D-24) + +`serve` runs `lagoon.OpenFromApp`, which needs `database.dsn` and a valid `app.key` (base64 of exactly 32 bytes; empty or invalid fails) [VERIFIED: lagoon/connection.go:77-90; lagoon/encrypted.go:179-207]. It then runs `publishUploads`, which fails on an empty `storage.uploads.bucket_url` [VERIFIED: lagoon/attach/bucket.go:66-69], then `Assemble` (body limits), then `conga.StartServeWorker`. The worker defaults to `workInServe: true` [VERIFIED: modules/conga/client.go:39, 55-57] and opens a LISTEN pool. **Set `queue.work_in_serve: false`**, because the site has no jobs. `migrate` creates system_files, backend admin and queue tables (lagoon/migrations.go:63-91). That is harmless and is what D-27's "migrate before restart" runs. + +## Docs Build (question 2) + +- **Output layout** (real build: `go run ./cmd/summer docs:build --base-url /docs --out ` wrote 72 pages, 164 files, 3.4 MB in 2.5 s): `index.html`, `index.md`, `404.html`, `llms.txt`, `llms-full.txt`, `search-index.json`, `assets/{site.css,site.js,search.js,theme-init.js,fonts/,LICENSE-lucide.txt}`, `
/.html` and `
/.md`, `api/.html`, plus the marker file `.summer-docs` [VERIFIED: build run; docsite.go:24 `const MarkerFile = ".summer-docs"`]. +- **Every internal link is absolute with the base**, for example `href="/docs/api/backpack.html"`, `/docs/assets/site.css` and `src="/docs/assets/site.js"`. The URL rule is `s.base + "/" + p`, and nav/pager links use `p.URL + ".html"` [VERIFIED: internal/docsite/emit.go:23-27, 87-95, 187-195]. +- **All ten handoff targets exist as `.html`:** backend/admin-spa, database/models, services/routing, services/jobs, services/realtime, services/search, services/mail, console/introduction, setup/coming-from-wintercms and setup/installation [VERIFIED: `test -f` on the build output]. **They do not exist without the extension.** The built-in `docsite.Handler` does not resolve extension-less paths either (it serves files by path, a directory's index.html, otherwise 404.html with status 404) [VERIFIED: internal/docsite/serve.go:236-290]. +- **`--out` handling:** a non-empty out dir without `.summer-docs` is refused. With the marker it is emptied, then written (docsite.go:189-211). Never commit a placeholder **inside** the docs out dir. `--out` must not be inside `--src` or equal to the root (docsite.go:180-187). +- **Determinism:** building from `git archive HEAD | tar -x` produced output byte-identical to the working tree (`diff -r` clean). **The docs build can run against a tag export** [VERIFIED: this session]. +- **`site.yaml` parsing:** the `Site` struct has the fields `title`, `description`, `base_url`, `edit_url`, `source_url`, `llms_notes` and `sections`, decoded with `yaml.DisallowUnknownField()` [VERIFIED: internal/docsite/load.go:21-31, 88-93]. **An unset new key needs a struct field, or decode fails.** The CLI `--base-url` overrides `base_url` (`s.base = strings.TrimRight(cfg.BaseURL, "/")`, then `if opts.BaseURL != "" {...}`) [VERIFIED: load.go:217-221]. +- **Header template** (whole file) [VERIFIED: internal/docsite/theme/templates/header.html:1-9]: menu button, ``, `
`, search trigger, theme toggle. `pageView` carries `HomeURL` etc. and is filled by `baseView` (emit.go:62-95). The 404 page also uses `baseView(nil)` (emit.go:144). **D-41 needs:** `Site.SiteURL string \`yaml:"site_url"\`` → a `pageView.SiteURL` field set in `baseView` → `{{if .SiteURL}}
…{{end}}` in header.html → a `.site-link` rule in `theme/assets/site.css`. The theme already inlines `icon-chevron-left` (11.1 UI-SPEC icon list). +- **Which docs page documents site.yaml:** none fully. `docs/console/utilities.md:31-46` documents `docs:build` / `docs:serve` flags in a table (`--root`, `--src`, `--out`, `--base-url`, `--check`). Add the `site_url` key (and the recommended `--site-url` flag) there, preferably with a short "site.yaml keys" paragraph. +- **Checker requirements for a new key:** `TestDocsTree` (cmd/summer/docs_test.go:25-33) runs `docsite.Check` on the real tree. Command words are checked against the real command list, but flags are stripped (cobra stripFlags), so a new flag is not validated. `TestParseSite` (internal/docsite/load_test.go:11-50) has an `unknown key` case. Add a `site_url` positive case and a render assertion that the link is absent when unset (output unchanged) and present when set. +- **How summercms.io sets `site_url: /` without changing the framework's own `docs/site.yaml`:** the docs are built from the framework tag's `docs/site.yaml`, which must not name the site, and setting `/` there would also make `summer docs:serve` link to itself. **Recommend a `--site-url` flag on `docs:build` (and `docs:serve`) that overrides `site_url`, mirroring `--base-url`.** The build script then passes `--base-url /docs --site-url /`. This is a small extension of D-41 (see Open Question 1). + +## PostgreSQL 15 (question 3) + +**Every pinned image** (9 Go harnesses plus 1 shell gate). The value `"postgres:16-alpine"` was read at modules/lagoon/postgres_test.go:52-58 (`postgres.Run(ctx, "postgres:16-alpine", postgres.WithDatabase("lagoon"), ...)`). The rest were grep-located: + +| File:line | Kind | +|-----------|------| +| modules/lagoon/postgres_test.go:53 | TestMain harness | +| modules/lagoon/attach/lifecycle_test.go:273 | harness | +| modules/cabana/query_test.go:275 | harness | +| modules/cabana/auth_test.go:329 | harness | +| modules/beachcomber/postgres_test.go:56 | harness | +| modules/lighthouse/postgres_test.go:56 | harness | +| modules/bouncer/phase07_coverage_test.go:67 | harness | +| modules/conga/postgres_test.go:59 | TestMain harness | +| docs/examples/blog/postgres_test.go:68 | harness | +| scripts/check-phase8.sh:257 | fonoteka phase-8 gate (shell). Out of scope. | + +The image is a hard-coded literal everywhere. **There is no env var or parameter.** + +**Docs stating the version:** +- `README.md:15` "PostgreSQL 16 for any application that uses the data layer" +- `README.md:35` "Create a PostgreSQL 16 database:" +- `docs/setup/installation.md:14` "PostgreSQL 16 for any application that uses the data layer." + +These three describe only the **test image**, not a requirement, so leave them or add a "also verified on 15" sentence: +- `docs/plugins/testing.md:34` +- `modules/lagoon/README.md:201` +- `modules/conga/README.md:207` + +**How to run on 15 without permanent edits (done this session):** +```bash +S=$(mktemp -d); git -C summercms.go archive HEAD | tar -x -C "$S" +cd "$S" && grep -rl 'postgres:16-alpine' --include=*.go . | xargs sed -i 's#postgres:16-alpine#postgres:15#g' +go test -count=1 -p 4 ./modules/lagoon/... ./modules/cabana/... ./modules/beachcomber/... \ + ./modules/lighthouse/... ./modules/bouncer/... ./modules/conga/... ./docs/examples/blog/... +``` + +The result was all `ok`: lagoon, lagoon/attach, cabana, beachcomber (+typesense), lighthouse (+centrifugo), bouncer, conga, docs/examples/blog (+console, controllers, models, updates), with `EXIT=0`. Verbose confirmation: `🐳 Creating container for image postgres:15` and `Waiting for container ... image: postgres:15` [VERIFIED: this session; `docker run --rm postgres:15 postgres --version` → `PostgreSQL 15.16 (Debian 15.16-1.pgdg13+1)`]. Rome runs 15.19, a later patch of the same major. + +- No PG16-only SQL was found in non-test code. A grep for `ANY_VALUE`, SQL/JSON constructors, `IS JSON`, `daticulocale`, `datlocale`, `SYSTEM_USER` and `pg_stat_io` found nothing. The ICU database-locale check was removed by quick task 261001-ddh (`037dc53`). `pl-x-icu` per-query collation needs an ICU-enabled server, and Debian's postgresql-15 is ICU-enabled [ASSUMED]. The site never uses Polish ordering. +- Docker 29.7.2 with a live daemon is available, and `postgres:15` and `postgres:15-alpine` are already pulled. +- Recommendation: plan 02 re-runs exactly this scripted, throwaway procedure, records the `go test` output in its SUMMARY, then edits the three doc lines. Prefer `postgres:15` (Debian, like rome) over `-alpine`. An env-var override for the test image (for example `SUMMER_TEST_POSTGRES_IMAGE`) would make this repeatable, but it is a framework change outside D-25's wording. Offer it as optional (Open Question 3). + +## Nuxt 4 (question 4): spike-verified configuration + +A minimal project with the fonoteka versions was installed (13 s, warm pnpm store) and generated (19 s) in scratch. + +**Output of `nuxt generate`** (`.output/public`, plus a `dist` symlink to it in the project root): +``` +index.html 200.html 404.html _payload.json robots.txt sitemap.xml og-image.png +_nuxt/.js _nuxt/entry..css _nuxt/error-404..css _nuxt/error-500..css +_nuxt/builds/latest.json _nuxt/builds/meta/.json <- NOT content-hashed names +_fonts/.woff2 (4 files with the subset/style config below) +_i18n//en/messages.json <- fetched at hydration +__sitemap__/style.xsl +``` + +The HTML preloads `/_payload.json?` and modulepreloads `/_nuxt/*.js`. + +**Spike results that drive the config:** +1. **Prerender fails on `/docs` links.** `nuxt generate` exit 1: `[404] Page not found: /docs ... Linked from /`, `ERROR Exiting due to prerender errors.` **Fix (verified, exit 0):** `nitro: { prerender: { ignore: ['/docs'] } }` and `linkChecker: { excludeLinks: ['/docs', '/docs/**'] }`. +2. **With i18n, the sitemap becomes an index.** It produced `sitemap_index.xml`, `__sitemap__/en-US.xml` and a **directory** `sitemap.xml/index.html` meta-refresh, and robots pointed at `sitemap_index.xml`. **Fix (verified):** `sitemap: { autoI18n: false }` gives one flat `sitemap.xml`, and robots says `Sitemap: https://summercms.io/sitemap.xml`. +3. **Fonts:** with only `weights`, @nuxt/fonts fetched 26 files (normal+italic × every subset). With `styles: ['normal'], subsets: ['latin', 'latin-ext']` it fetched **4 files**. The `@font-face` rules are inlined in `entry..css` with `url(../_fonts/...)`. No googleapis/gstatic reference appears in the HTML. +4. **Title duplication:** page title "SummerCMS" + `site.name` "SummerCMS" rendered `og:title "SummerCMS | SummerCMS"`. Set the home title so the template does not duplicate (for example a full title plus `titleTemplate: '%s'` for the index page). The handoff names no `` text, so the copy is planner/user discretion; the live page uses "SummerCMS - Coming Soon". +5. **Generated head (good defaults):** canonical `https://summercms.io/`, `og:url`, `og:site_name`, `og:locale en_US`, `twitter:card summary_large_image`, an absolute `og:image` from `site.url`, `robots index, follow, max-image-preview:large`, schema.org `WebSite` + `WebPage` JSON-LD and `<html lang="en-US">`. +6. **i18n v10 single locale:** with `strategy: 'prefix_except_default'` and `defaultLocale: 'en'`, `en` is served at `/`, and 11.3 adds `pl` at `/pl/` without moving `/`. Messages are emitted to `_i18n/<hash>/en/messages.json` and fetched on hydration, so the Go handler must serve `_i18n/` (Pitfall 7). Locale files live at **`i18n/locales/en.json`** (`restructureDir` default `i18n/` + `langDir: 'locales/'`, the same as fonoteka's `i18n/locales/*.json`). CONTEXT's "`locales/en.json`" means that file. +7. `ogImage: { enabled: false }` plus a static `public/og-image.png` worked with no warnings. Fonoteka also ships static OG PNGs. +8. The harmless warning `[nuxt-seo-utils] treeShakeUseSeoMeta requires Unhead v3` appears. Ignore it. + +**Fonoteka's static-build i18n note (`_i18n/**/*.json`):** that comment concerns its PWA precache manifest. On `ssr: true` builds, i18n v10 fetches `/_i18n/<hash>/<locale>/messages.json` at runtime, and the file must be reachable. Fonoteka sets `experimental.prerenderMessages: true`. The spike emitted the file without that flag. Setting it anyway makes emission a contract rather than an accident (recommended). + +**Package manager / runtime:** pnpm (fonoteka has `pnpm-lock.yaml` lockfileVersion 9.0 and `pnpm-workspace.yaml` `allowBuilds`). Node 22.23.2 locally, and fonoteka `.nvmrc` is 22.22.0. Fonoteka also patches `nuxtseo-shared@5.2.2` (`patches/`), but only to stop a devtools crash in `nuxt dev`. Set `devtools: { enabled: false }` and do not copy the patch unless `pnpm dev` crashes. + +## Design Handoff Inventory (question 5) + +**Sections in order:** sticky header → hero `#top` → `#why` → `#features` (full-bleed band) → `#winter` → `#start` (full-bleed band) → footer. + +**Tokens → `:root` custom properties (D-35):** + +| Token | Value | +|-------|-------| +| `--navy-900` | `#18223a` | +| `--navy-850` | `#1d2940` | +| `--navy-800` | `#1f2b42` | +| `--navy-700` | `#233148` | +| `--navy-650` | `#253350` | +| `--navy-600` | `#29344a` | +| `--yellow-500` | `#f5c55a` | +| `--yellow-400` | `#fbd77e` | +| `--yellow-200` | `#fbe3a6` | +| `--yellow-100` | `#fde9b4` | +| `--text` | `#e9edf3` | +| `--text-2` | `#c5ccd6` | +| `--text-3` | `#a3adbd` | +| `--text-4` | `#8a94a3` | + +- Borders: white at 6, 7, 8, 10, 12 and 14%. +- Radii: 6px, 16px and 999px. +- Spacing scale: 4, 6, 8, 12, 16, 20, 24, 28, 32, 40, 48, 96 and 120px. +- Container: `max-width:1120px; padding:0 24px`. +- Section padding: 96px. +- `html{scroll-behavior:smooth; scroll-padding-top:72px}`. +- Selection: `#f5c55a` on `#1d2940`. +- Body: `font-family: Roboto, system-ui, sans-serif; -webkit-font-smoothing: antialiased`. + +**Details found in `SummerCMS Landing.dc.html` that are not in README.md (match these):** +- Header nav row `gap:4px`, inside `flex:1; min-width:0; overflow-x:auto; scrollbar-width:none`. +- Docs button `gap:8px`, with the arrow in its own `<span>`. +- Header logo `<img>` is 30×30 with `mask-image: radial-gradient(circle,#000 52%,transparent 70%)` and `transform: scale(1.35)`, logo row `gap:10px`. The mask was there to crop the screenshot placeholder. With the transparent original, match the visual size by screenshot comparison rather than copying the mask blindly. +- Hero tagline `letter-spacing: 0.01em`. `text-wrap: pretty` on the description, H2s and card bodies. +- Secondary CTA text `#e9edf3`, hover `#fff`, `gap:10px`. Primary CTA hover text stays `#1d2940`. +- Feature tile hover text `#fff`. +- Concept rows: `grid-template-columns: minmax(0,1fr) minmax(0,1.3fr); gap:12px; line-height:1.5`. Every row has a bottom border, including the last. +- "See the full concept map →": `display:inline-flex; margin-top:24px`. +- Get started: + - Chips row `margin-top:24px; gap:8px`. + - Note `margin-top:20px`, 16px/1.65. + - Buttons row `margin-top:28px`. + - Installation guide padding `13px 24px`. Source outline padding `12px 22px`, border `rgba(255,255,255,0.14)`, hover bg `rgba(255,255,255,0.08)` with text `#fff`. +- Terminal: + - Title bar `padding:12px 18px`, bottom border `rgba(255,255,255,0.07)`. + - Copy button font `500 12px Roboto`. + - Body `overflow-x:auto; display:flex; flex-direction:column`. Each line is a `white-space:pre` block. Each group's comment line after the first has `margin-top:16px`. +- Footer: row `gap:24px; flex-wrap:wrap`, then a `flex:1` spacer, then the link group `gap:20px`. +- Copy uses typographic apostrophes: "each plugin’s YAML" and "the caller’s transaction". +- The prototype loads Roboto 900 as well. D-34 says 300/400/500/700. 900 is unused, so follow D-34. + +**Breakpoints/interactions:** +- Section links are hidden at ≤720px. **Use CSS `@media (max-width: 720px)`**, not the prototype's JS `wide` state (Pitfall 4). +- Scroll-spy: the active section is the last of `why`, `features`, `winter`, `start` whose `getBoundingClientRect().top < 140`. Before `#why`, nothing is active. Listen to `scroll` with `{passive:true}` and also compute on mount. Gate it on `appConfig.scrollSpy` (D-45). +- Copy: `navigator.clipboard.writeText(lines.join('\n'))`, label "Copied" for 1500ms, then "Copy". Gate the rays on `appConfig.showRays`. +- Transitions: optional 150ms ease on background/color. Reduced motion: `@media (prefers-reduced-motion: reduce){ html{scroll-behavior:auto} }` (discretion, recommended). + +**Terminal command list after D-39 (single source; six commands, three groups):** +``` +# get the framework +$ git clone https://git.golem15.com/golem15/summercms +$ cd summercms + +# install the summer CLI +$ go install ./cmd/summer + +# build and run the example app +$ cd examples/hello +$ summer build +$ ./bin/hello greeter:hello +``` +Copy payload: `git clone https://git.golem15.com/golem15/summercms\ncd summercms\ngo install ./cmd/summer\ncd examples/hello\nsummer build\n./bin/hello greeter:hello`. + +**Every link on the page (verified against the real docs build this session):** + +| Where | href (handoff) | Resolves to | Status | +|-------|----------------|-------------|--------| +| Header logo, footer "Back to top ↑" | `#top` | hero section id | in-page | +| Header section links | `#why` `#features` `#winter` `#start` | section ids | in-page | +| Header "Docs →", hero "Read the docs", footer "Docs" | `/docs` | `/docs/` → `index.html` | ✓ (via redirect) | +| Hero "Alpha 0.1 is out" | `#start` | section id | in-page | +| Feature tiles (8) | `/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` | `<same>.html` | ✓ files exist **as `.html` only** | +| WinterCMS "See the full concept map →" | `/docs/setup/coming-from-wintercms` | `.html` | ✓ | +| Get started "Installation guide" | `/docs/setup/installation` | `.html` | ✓ | +| Get started "Source", footer "Source" | `https://git.golem15.com/golem15/summercms` | **404 anonymously today** (Gitea, private) | ✗ until made public (D-38 launch item) | +| Footer credit (D-12) | `https://golem15.com` | external | check at cutover | + +**Recommendation:** +- Write page hrefs in the canonical `.html` form (`/docs/backend/admin-spa.html`). The handoff itself says "Doc URLs were derived from the docs folder structure. Confirm them against the built docs site." This keeps one URL per page and makes the link check an exact file check. +- Make the docs handler also 301 extension-less paths to `.html` when that file exists, so typed or shared pretty URLs work. + +**Identifiers named on the page exist:** `party.Plugin`, `pact.HasMigrations`, `pact.HasRoutes`, `festival.Bus` and `bonfire.Command` all resolved with `go doc` [VERIFIED: this session]. + +**D-40: does `./bin/hello greeter:hello` need a database? No.** This was run verbatim in a fresh `git clone` of the local repo, with `GOBIN=<tmp>/gobin` and `PATH=<tmp>/gobin:$PATH` set and no `SUMMER_*` env: +1. `go install ./cmd/summer` +2. `cd examples/hello` +3. `summer build` printed `built hello in 2.185s`. +4. `./bin/hello greeter:hello </dev/null` printed the table `golem15.greeter active` and `name=hello-app posts_per_page=10 debug=false extra=hello-from-optional events=ok collected=greeter handled=true`, `EXIT=0`. +5. The tree was clean afterwards (no `main.go` drift), so the README "Known issues" note about a stale `main.go` is itself stale. + +`greeter:hello` calls `out.Confirm("show greeting?", true)`. In non-interactive mode `Confirm` returns the default (`if !c.interactive { return def, nil }`) [VERIFIED: modules/bonfire/prompts.go:29-32], so the check needs no stdin. Requirements for the check: +- (a) a temp `GOBIN` placed **first** on PATH, because a stale `summer` may already be on the user's PATH; +- (b) a clone source override until the repo is public, because `git ls-remote https://git.golem15.com/golem15/summercms` fails with "could not read Username" (verified). + +## Deploy (question 6) + +**Live site observations (fetched this session):** +- `http://summercms.io/` → `301 https://summercms.io/` (nginx). +- `https://summercms.io/` → HTTP/2 200 `text/html`, 9816 bytes, `last-modified: Wed, 04 Feb 2026`. Title "SummerCMS - Coming Soon", gzip on. +- `https://summercms.io/logo.png` → 200, `image/png`, 512916 bytes, PNG 746×744 RGBA interlaced. This matches D-36. `http://summercms.io/logo.png` 301s to HTTPS. +- `robots.txt` and `favicon.ico` return 404. The page uses `logo.png` as its favicon and OG image. +- The Let's Encrypt cert (issuer YE1) has SAN `summercms.io, www.summercms.io`. +- `http://www.summercms.io/` 301s to `https://summercms.io/`, but `https://www.summercms.io/` **fails the TLS handshake** ("unrecognized name"): there is no 443 server block for www. DEPLOY.md can fix this with a 443 www→apex redirect block. +- The current site is a static docroot (one `index.html` plus `logo.png`). The rollback is the saved server block **plus** that docroot. A copy of the live HTML was captured in this session's scratch dir. + +**DEPLOY.md content requirements (discretion values, marked [ASSUMED] until confirmed on rome):** +1. **Launch checklist:** + - Create the remotes on git.golem15.com for the three repos (D-43). + - Make `golem15/summercms` public (D-38). + - Confirm the `v0.1.0` tag exists (D-42). + - Run the external link check anonymously. +2. **One-time server setup:** + - `adduser --system --group --home /srv/summercms-io --shell /usr/sbin/nologin summercms` [ASSUMED path/user]. + - `sudo -u postgres createuser --pwprompt summercms`. + - `sudo -u postgres createdb -O summercms -E UTF8 summercms_io` (D-27). + - Create `/srv/summercms-io/{bin,config,storage}`. + - Server-only `/srv/summercms-io/.env` (owner summercms, mode 0600) with `SUMMER_DATABASE__DSN=postgres://summercms:<pw>@127.0.0.1:5432/summercms_io?sslmode=disable` and `SUMMER_APP__KEY=<from ./bin/summercms-io key:generate>`. (Peer auth over the unix socket is an alternative that needs no password. D-27 says password.) +3. **Build (local):** `scripts/build.sh` (see Code Examples) produces `bin/summercms-io` (linux/amd64, `CGO_ENABLED=0`). +4. **Upload:** + - `rsync -av --chmod=F644,D755 config/ rome:/srv/summercms-io/config/`. Exclude any server-only files; `.env` lives outside `config/`. + - `rsync -av bin/summercms-io rome:/srv/summercms-io/bin/summercms-io.new`. +5. **Release (every deploy):** + - `cd /srv/summercms-io && sudo -u summercms ./bin/summercms-io.new migrate`. The cwd matters because config is read relative to it. + - `cp -p bin/summercms-io bin/summercms-io.prev` (if present). + - `mv bin/summercms-io.new bin/summercms-io`. + - `supervisorctl restart summercms-io`. + - `curl -sI http://127.0.0.1:8095/`. +6. **Supervisor program** `/etc/supervisor/conf.d/summercms-io.conf`: + - `command=/srv/summercms-io/bin/summercms-io serve --addr 127.0.0.1:8095` [ASSUMED port; check with `ss -ltnp` on rome] + - `directory=/srv/summercms-io` + - `user=summercms` + - `environment=SUMMER_ENV="production"` + - `autostart=true`, `autorestart=true`, `startsecs=3` + - `stopsignal=TERM`, `stopwaitsecs=15` (serve shuts down within 10 s on SIGTERM, surf/serve.go:38, 70-77) + - `redirect_stderr=true`, `stdout_logfile=/var/log/supervisor/summercms-io.log` with size/backups + - Then `supervisorctl reread && supervisorctl update`. +7. **nginx server blocks** (replace the existing summercms.io block in place, D-31): + - port 80 → 301 to `https://summercms.io$request_uri` + - 443 www → 301 to apex + - 443 apex with the certbot paths `ssl_certificate /etc/letsencrypt/live/summercms.io/fullchain.pem`, `ssl_certificate_key .../privkey.pem`, `include /etc/letsencrypt/options-ssl-nginx.conf`, `ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem`. Copy the exact lineage name from the existing block; it may not be `summercms.io` [ASSUMED]. + - `gzip on; gzip_vary on; gzip_proxied any; gzip_types text/css text/plain text/xml application/xml application/json text/javascript application/javascript image/svg+xml text/markdown;` (woff2 and png are already compressed). + - `location ^~ /backend { return 404; }` (D-29). + - `location / { limit_except GET HEAD { deny all; } proxy_pass http://127.0.0.1:8095; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }`. + - Use `listen 443 ssl http2;`. That is Debian 12's nginx 1.22 syntax [ASSUMED version]. It is deprecated but still valid on ≥1.25, and local nginx 1.30.4 can `nginx -t` it. + - Then `nginx -t && systemctl reload nginx`. +8. **Rollback:** + - App: `mv bin/summercms-io.prev bin/summercms-io && supervisorctl restart summercms-io`. Migrations are not reversed; 11.2 has no plugin migrations. + - Cutover: restore the saved "Under construction" server block and its docroot, then `nginx -t && systemctl reload nginx`. +9. **Verification after cutover:** curl checks for status, `Cache-Control`, no `X-Robots-Tag: noindex`, `/docs/` 200, `/backend` 404, `POST /` 403 (nginx), and HTTPS www→apex. Then run the link check against `https://summercms.io`. + +## Architecture Patterns + +### System Architecture Diagram + +``` + BUILD (local, scripts/build.sh in sm-summercms-app) + ┌──────────────────────────┐ pnpm install --frozen-lockfile + nuxt generate + │ vue-summercms-app (sub) │──────────────► .output/public/ ──rsync -a --delete──┐ + └──────────────────────────┘ ▼ + ┌──────────────────────────┐ git archive v0.1.0 → tmp; go run ./cmd/summer plugins/golem15/summercms/public/site/ + │ ../summercms.go @ v0.1.0 │ docs:build --base-url /docs --site-url / ────────► plugins/golem15/summercms/public/docs/ + └──────────────────────────┘ │ + link check + terminal-list drift test (go test, REQUIRE_BUILD=1) + ▼ + summer build (main.go/plugins.gen.go) + GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build + ▼ + bin/summercms-io (one file) + RUNTIME (rome) + Browser ──HTTPS──► nginx :443 ──┬─ /backend* ─► 404 + (TLS, gzip, ├─ non-GET/HEAD ─► 403 + www→apex) └─ everything else ─proxy─► 127.0.0.1:8095 summercms-io serve + │ boot: config/ + .env → Postgres (summercms_io) + ▼ + surf ServeMux (GroupRaw, plugin golem15.summercms) + ├─ GET /docs → 301 /docs/ + ├─ GET /docs/{path...} ─► docs fs: file │ x→x.html (301) │ dir/index.html │ 404.html(404) + └─ GET / (catch-all) ──► site fs: file │ dir/index.html │ 404.html(404) + headers: Content-Type (own table), Cache-Control by path, ETag +``` + +### Recommended Project Structure +``` +summercms/ # meta repo dir (siblings) +├── summercms.go/ # framework (D-25, D-41, D-42 changes only) +└── sm-summercms-app/ # root app, module git.golem15.com/golem15/sm-summercms-app + ├── go.mod go.work summer.yaml # replace summercms => ../summercms.go; use . ./plugins/golem15/summercms + ├── main.go plugins.gen.go # generated by summer build (committed, like fonoteka) + ├── config/{app,http,storage,database,queue}.yaml # no secrets; work_in_serve: false + ├── scripts/build.sh # D-05/D-28 build; flags --release (tag required) / dev + ├── deploy/{nginx-summercms.io.conf, supervisor-summercms-io.conf, rollback/} # referenced by DEPLOY.md + ├── DEPLOY.md README.md .gitignore (/bin/ /tmp/ go.work.sum .env) + ├── terminal_check_test.go # D-40 (gated by SUMMERCMS_TERMINAL_CHECK=1) + ├── vue-summercms-app/ # submodule (Nuxt 4) + │ ├── nuxt.config.ts app/app.config.ts app/app.vue app/pages/index.vue + │ ├── app/components/{SiteHeader,HeroSection,WhySection,FeaturesSection,WinterSection,StartSection,TerminalCard,SiteFooter}.vue + │ ├── app/assets/css/{tokens.css,base.css} + │ ├── app/data/terminal.json # SINGLE SOURCE of the six commands (+ comment i18n keys) + │ ├── app/utils/{scrollSpy.ts,terminal.ts} # pure functions, node --test + │ ├── i18n/locales/en.json # all copy (D-33) + │ ├── public/{og-image.png,favicon.ico,favicon-32x32.png,apple-touch-icon.png,sun-*.png|webp} + │ ├── scripts/derive-images.sh # ImageMagick one-off from logo.png + │ └── tests/*.test.ts # node --test + └── plugins/golem15/summercms/ # submodule sm-summercms-plugin, module git.golem15.com/golem15/sm-summercms-plugin + ├── go.mod (replace summercms => ../../../../summercms.go) + ├── plugin.go (ID golem15.summercms, Routes, embed all:public) + ├── static.go (handler: resolve, MIME, cache, ETag, 404) + ├── public/README.md (committed placeholder; site/ and docs/ gitignored) + └── *_test.go +``` + +### Pattern 1: Nuxt config (spike-verified core) +```ts +// Source: spike in this session (nuxt 4.4.8, @nuxtjs/i18n 10.4.0, @nuxtjs/seo 5.2.1, @nuxt/fonts 0.14.0) +export default defineNuxtConfig({ + compatibilityDate: '2025-07-15', + devtools: { enabled: false }, + modules: ['@nuxt/fonts', '@nuxtjs/i18n', '@nuxtjs/seo'], + css: ['~/assets/css/tokens.css', '~/assets/css/base.css'], + fonts: { + families: [ + { name: 'Roboto', weights: [300, 400, 500, 700], styles: ['normal'], subsets: ['latin', 'latin-ext'], global: true }, + { name: 'Roboto Mono', weights: [400, 500], styles: ['normal'], subsets: ['latin', 'latin-ext'], global: true }, + ], + }, + i18n: { + strategy: 'prefix_except_default', + defaultLocale: 'en', + locales: [{ code: 'en', language: 'en-US', file: 'en.json', name: 'English' }], + langDir: 'locales/', + baseUrl: 'https://summercms.io', + experimental: { prerenderMessages: true }, // fonoteka precedent; makes _i18n emission explicit + }, + site: { url: 'https://summercms.io', name: 'SummerCMS', description: 'A new dawn in content management', defaultLocale: 'en' }, + ogImage: { enabled: false }, // static public/og-image.png (D-37: nothing at runtime) + sitemap: { autoI18n: false }, // one flat sitemap.xml (spike) + linkChecker: { excludeLinks: ['/docs', '/docs/**'] }, + nitro: { prerender: { ignore: ['/docs'] } }, // REQUIRED: else generate exits 1 on /docs links +}) +// app/app.config.ts (D-45): export default defineAppConfig({ landing: { showRays: true, scrollSpy: true } }) +``` + +### Pattern 2: Site plugin with an embedded static handler (stdlib only) +```go +// Source: patterns from modules/boardwalk/boardwalk.go (fs.FS + ServeContent + cache by prefix) and +// modules/surf/router.go (GroupRaw), adapted for a public, indexable site. [ASSUMED code; APIs VERIFIED above] +package summercms + +//go:embed all:public // all: is required: _nuxt/, _fonts/, _i18n/, _payload.json start with '_' +var publicFS embed.FS + +type Plugin struct{ site, docs http.Handler } + +func (p *Plugin) ID() string { return "golem15.summercms" } +func (p *Plugin) Requires() []string { return nil } +func (p *Plugin) Register(*backpack.App) error { return nil } +func (p *Plugin) Boot(*backpack.App) error { return nil } + +func (p *Plugin) Routes(r pact.Router) error { + site, docs, err := handlers(publicFS) // fs.Sub "public/site", "public/docs"; fail closed if index.html missing + if err != nil { + return err // e.g. "summercms: public/site/index.html missing; run scripts/build.sh" + } + r.GroupRaw("", nil, func(g pact.Router) { + g.Get("/docs", redirect("/docs/")) // 301 instead of ServeMux's 307 + g.Get("/docs/{path...}", docs.ServeHTTP) // prefix "/docs/" stripped inside + g.Get("/", site.ServeHTTP) // catch-all, lowest precedence + }) + return nil +} + +func init() { party.Register(&Plugin{}) } +``` +Handler rules: +- Clean the path with `path.Clean("/"+rel)`. Refuse any segment starting with `.`, which hides `.summer-docs`. +- Resolution order: exact regular file, then `<p>/index.html`, then (docs only) `<p>.html` → 301 to `/docs/<p>.html`, built from the cleaned path only. Otherwise serve `404.html` with status 404. +- Set `Content-Type` from an own table before `mime.TypeByExtension`. +- Set `Cache-Control` per the table below. +- Precompute a strong `ETag` (sha256 prefix) per file at construction, so `no-cache` revalidates to 304 through `http.ServeContent`. +- No `X-Robots-Tag` and no CSP. `X-Content-Type-Options: nosniff` and `Referrer-Policy: strict-origin-when-cross-origin` are fine. + +**Cache-Control by path (D-07, adjusted to what is actually hashed):** + +| Path | Hashed? | Cache-Control | +|------|---------|---------------| +| `/_nuxt/*` except `/_nuxt/builds/*` | yes (`B_P4Zhpw.js`, `entry.UH8Ffg1R.css`) | `public, max-age=31536000, immutable` | +| `/_fonts/*` | yes (content-hash names) | `public, max-age=31536000, immutable` | +| `/_nuxt/builds/latest.json`, `/_nuxt/builds/meta/*` | **no** (polled for new deploys) | `no-cache` | +| `*.html`, `/`, `/_payload.json`, `/_i18n/**`, `robots.txt`, `sitemap.xml`, `og-image.png`, favicons, sun images | no | `no-cache` (+ETag) | +| `/docs/**` including `/docs/assets/*` | **no** (`/docs/assets/site.css` is a fixed name) | `no-cache` (+ETag) | + +**MIME table the plugin must own.** Go 1.27's builtin table [VERIFIED: $(go env GOROOT)/src/mime/type.go builtinTypesLower] has `.css .html .js .json .mjs .png .svg .txt .ico .xml .xsl .webp` but **not** `.woff2`, `.woff`, `.md` or `.webmanifest`. Without `/etc/mime.types` on rome, fonts and `/docs/*.md` would be served as `application/octet-stream`. Own entries: +- `.woff2` → `font/woff2` +- `.woff` → `font/woff` +- `.md` → `text/markdown; charset=utf-8` +- `.webmanifest` → `application/manifest+json` +- `.json` → `application/json` +- `.xml`/`.xsl` → `application/xml; charset=utf-8` (or keep the builtin `text/xml`) +- `.txt` → `text/plain; charset=utf-8` +- `.html` → `text/html; charset=utf-8` +- `.css` → `text/css; charset=utf-8` +- `.js`/`.mjs` → `text/javascript; charset=utf-8` + +### Pattern 3: Single-source terminal commands (D-40) +`vue-summercms-app/app/data/terminal.json` (imported by `TerminalCard.vue`; comments are i18n keys): +```json +{ "groups": [ + { "comment": "terminal.getFramework", "commands": ["git clone https://git.golem15.com/golem15/summercms", "cd summercms"] }, + { "comment": "terminal.installCli", "commands": ["go install ./cmd/summer"] }, + { "comment": "terminal.buildExample", "commands": ["cd examples/hello", "summer build", "./bin/hello greeter:hello"] } +] } +``` +- **Check (Go test in sm-summercms-app, `TestTerminalCommands`):** + - Skip unless `SUMMERCMS_TERMINAL_CHECK=1`. + - Read the JSON and make a temp dir and a temp `GOBIN`, with `PATH=$GOBIN:$PATH`, `GOWORK` unset and no `SUMMER_*` env. + - Run `bash -euo pipefail -c "<commands joined by \n>"` in the temp dir with stdin `/dev/null`. + - If `SUMMERCMS_CLONE_URL` is set, substitute only the clone URL (pre-launch, for example the local `../summercms.go`). Unset means verbatim (cutover). + - Assert exit 0 and `handled=true` in the output. +- **Drift guard (plugin test after build):** assert the embedded `public/site/index.html` contains each command string and each of the three comment texts. The page and the check then cannot diverge. + +### Anti-Patterns to Avoid +- **`//go:embed public`** without `all:`. Silently drops `_nuxt/`, `_fonts/`, `_i18n/` and `_payload.json`, so the page renders unstyled and hydration fails [CITED: pkg.go.dev/embed]. +- **Embedding the `dist` symlink.** `nuxt generate` creates `dist -> .output/public`, and embed refuses symlinks [CITED: pkg.go.dev/embed]. Copy with `rsync -a --delete .output/public/ <plugin>/public/site/`. +- **Reusing `boardwalk.Handler` or `boardwalk.SetSecurityHeaders`.** These bring noindex, CSP and the SPA fallback (D-07). +- **Implementing `pact.HasAdminControllers`** (as the `make:plugin` stub does). It would activate cabana, require `admin.jwt.secret` and mount `/backend`. +- **Committing a placeholder file inside `public/docs/`.** `docs:build` refuses to clean a dir without the `.summer-docs` marker. +- **Copying the prototype's JS `wide` state.** SSR cannot know the width, which causes a hydration mismatch and layout shift. Use a CSS media query. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Range/HEAD/If-None-Match handling | Manual header logic | `http.ServeContent` with a preset `ETag` | Handles HEAD, 304, ranges and Content-Length correctly | +| Path normalisation / traversal | String hacks | `path.Clean` + `fs.Sub` over `embed.FS` + dot-segment refusal | embed.FS cannot escape its root, and Clean defeats `..` | +| Route precedence | Own prefix router | surf `GroupRaw` → stdlib ServeMux | Verified conflict-free alongside cabana | +| Sitemap/robots/schema.org/OG tags | Hand-written XML/JSON-LD | `@nuxtjs/seo` (spike output above) | Already decided (D-37) | +| Font self-hosting | Manually downloaded woff2 + `@font-face` | `@nuxt/fonts` | Hashed names, subsets, fallback metrics | +| Docs site | Anything | `summer docs:build` from the tag | Deterministic, checker-guarded | +| Image derivation | A Go/Node image pipeline | One-off `magick` script, outputs committed | No build-time dependency; sharp is SUS | + +**Key insight:** the only genuinely new code is a ~150-line static handler plus wiring. Everything else is configuration of tools already verified in this repo or in fonoteka. + +## Common Pitfalls + +### Pitfall 1: `nuxt generate` fails on `/docs` links +**What goes wrong:** exit 1, `[404] Page not found: /docs/setup/installation ... Linked from /`. **Why:** the nitro prerender crawler follows every `<a href>` and `/docs` is not a Nuxt route. **Avoid:** `nitro.prerender.ignore: ['/docs']` plus `linkChecker.excludeLinks`. **Warning sign:** "Errors prerendering" in the generate log. [VERIFIED: spike] + +### Pitfall 2: Sitemap index plus a `sitemap.xml/` directory under i18n +**What goes wrong:** `sitemap.xml` becomes a directory holding a meta-refresh HTML, which the Go handler would serve as HTML. **Avoid:** `sitemap: { autoI18n: false }`. [VERIFIED: spike] + +### Pitfall 3: `position: sticky` header inside an `overflow-x: hidden` wrapper +**What goes wrong:** the prototype wraps everything in `<div style="min-height:100vh;overflow-x:hidden">`. A non-visible overflow makes that div the sticky containing scroll box, and since the window scrolls instead, the header stops sticking. **Avoid:** use `overflow-x: clip` on the wrapper (or none, relying on the hero's `overflow:hidden` for the rays). **Warning sign:** the header scrolls away in the browser UAT. [ASSUMED: CSS spec behaviour; verify in browser] + +### Pitfall 4: Hydration mismatch from width-dependent rendering +The prototype renders nav links only when `wide`. Render them always and hide them with `@media (max-width: 720px) { .nav-links { display: none } }`. Scroll-spy state starts as `''` on SSR and is computed in `onMounted`. + +### Pitfall 5: The Clipboard API needs a secure context +`navigator.clipboard` is undefined on plain-HTTP non-localhost origins (for example testing via a LAN IP). Guard with `navigator.clipboard?.writeText(...)` and fall back to a hidden-textarea `document.execCommand('copy')`, or skip it. HTTPS production and localhost are fine. [ASSUMED] + +### Pitfall 6: vue-i18n message syntax in copy +`@`, `|`, `{` and `}` are special in vue-i18n messages (linked messages, plurals, interpolation). None appear in the current copy, but a future `@` (an email) needs `{'@'}`. Inline `<code>` inside the Why card body (`fields.yaml`, `columns.yaml`) should use `<i18n-t keypath>` with slots, or split keys, not `v-html`. [ASSUMED] + +### Pitfall 7: go:embed drops `_`-prefixed files +Use `//go:embed all:public`. Then refuse dot-segments in the handler, because `all:` also embeds `.summer-docs`. [CITED: pkg.go.dev/embed] + +### Pitfall 8: Immutable caching of non-hashed files +`/docs/assets/site.css` and `/_nuxt/builds/latest.json` have fixed names. Marking them immutable pins a stale CSS or a stale build manifest in browsers for a year. Use the cache table above. [VERIFIED: docs output and spike output] + +### Pitfall 9: Extension-less docs URLs 404 +Docs pages are `x.html` files and the docs index is `index.html`. Resolve or redirect `x` → `x.html`, and use `.html` hrefs on the page. [VERIFIED: docs build] + +### Pitfall 10: Missing MIME types on a minimal server +`.woff2` and `.md` are not in Go's builtin table, so ship an explicit table. [VERIFIED: GOROOT mime/type.go] + +### Pitfall 11: Open redirect in the `.html` redirect +Build the `Location` only from `"/docs/" + cleanedRel + ".html"`, never from the raw `r.URL`. ServeMux already redirects `//host` paths to cleaned ones, but do not rely on it. + +### Pitfall 12: `serve` boot requirements +Missing `http.body_limits` (numeric, YAML only, because the env overlay gives strings), `app.key` (32-byte base64), `storage.uploads.bucket_url`, `database.dsn`, or a running worker LISTEN pool all fail boot. Ship: +- `config/http.yaml` with `body_limits: {default_bytes: 1048576, upload_bytes: 1048576}` +- `config/storage.yaml` with `uploads.bucket_url: "file://./storage/app/uploads"` (or `mem://`) +- `config/queue.yaml` with `work_in_serve: false` +- `config/app.yaml` with `key: ""` and the secrets in `.env` + +[VERIFIED: code reads above] + +### Pitfall 13: Tag ordering (D-42 is one-way) +The D-25 doc edits and the D-41 code, docs and smoke tests must be committed and green **before** `v0.1.0` is created, or the embedded docs (built from the tag) will lack the PG15 wording and the header link. The tag checkpoint therefore comes after the framework tasks in plan 02. Any plan-03 framework tests land after the tag, which is acceptable because tests do not change v0.1.0 behaviour. + +### Pitfall 14: The README quick-start is stale for `http.body_limits` only +It also claims `main.go` drifts, which was not reproduced. Irrelevant to the page copy, which is final per D-40. + +## Code Examples + +### Build script outline (`sm-summercms-app/scripts/build.sh`) +```bash +#!/usr/bin/env bash +# Source: composed from verified steps in this session. [ASSUMED script; each command VERIFIED individually] +set -euo pipefail +APP="$(cd "$(dirname "$0")/.." && pwd)"; FW="$APP/../summercms.go"; PLUG="$APP/plugins/golem15/summercms" +TAG="$(cd "$APP" && go list -m -f '{{.Version}}' git.golem15.com/golem15/summercms)" # v0.1.0 from require +MODE="${1:-release}" # release: docs from $TAG export; dev: docs from working tree (HEAD export) +# 1) Nuxt +(cd "$APP/vue-summercms-app" && pnpm install --frozen-lockfile && pnpm generate) +rsync -a --delete "$APP/vue-summercms-app/.output/public/" "$PLUG/public/site/" +# 2) Docs from the tag (D-42) +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT +REF="$TAG"; [ "$MODE" = dev ] && REF=HEAD +git -C "$FW" archive "$REF" | tar -x -C "$TMP" +(cd "$TMP" && go run ./cmd/summer docs:build --base-url /docs --site-url / --out "$PLUG/public/docs") +# 3) Checks against the built tree +(cd "$PLUG" && SUMMERCMS_REQUIRE_BUILD=1 go test ./...) +# 4) Binary +(cd "$APP" && summer build && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o bin/summercms-io .) +``` +In release mode, to make "code agrees with tag" true for the binary as well, either assert `git -C "$FW" describe --exact-match --tags HEAD` equals `$TAG` and that the tree is clean, or build with a temporary `GOWORK` file that adds `replace git.golem15.com/golem15/summercms => $TMP` ("`replace` directives in `go.work` files override any replaces of the same module or module version in workspace modules" [CITED: go.dev/ref/mod#go-work-file-replace]). Recommend the temporary go.work. It needs no checkout switching, but the `summer build` step must then also run with `GOWORK=$TMP/go.work`. Note that `summer build` itself sets `GOWORK` to the nearest `go.work` (scaffold.go:519-525), so in release mode call `go build` directly after `summer build` has generated the sources. + +### D-41 header change +```html +<!-- internal/docsite/theme/templates/header.html (after the wordmark) --> +{{- if .SiteURL}}<a class="site-link" href="{{.SiteURL}}">{{template "icon-chevron-left"}}<span>{{.SiteLabel}}</span></a>{{end}} +``` +- `SiteLabel` is derived (see Open Question 1). +- `html/template` escapes the href attribute. +- Validate `site_url` in `ParseSite`: it must be absolute `http(s)://` or root-relative `/...`. Reject `javascript:`. + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| nuxt-og-image runtime rendering | Static OG PNG or zero-runtime prerender | — | No runtime rendering in Go (D-37) | +| i18n sitemap auto-split | `sitemap.autoI18n: false` for single-locale sites | @nuxtjs/sitemap 8.x | One flat sitemap | +| Go ServeMux trailing-slash redirect 301 | Observed **307** in Go 1.27 for `/docs` → `/docs/` | — | Register an explicit 301 if a permanent redirect is wanted | +| TypeScript 5.x | TS 7.0.2 (Go-native) published today | 2026-10-01 | Stay on ~5.9 for Nuxt tooling | + +## Assumptions Log + +| # | Claim | Section | Risk if Wrong | +|---|-------|---------|---------------| +| A1 | Rome paths `/srv/summercms-io`, user `summercms`, port `127.0.0.1:8095` | Deploy | Port clash. Check `ss -ltnp` on rome before writing configs. | +| A2 | The certbot lineage is `/etc/letsencrypt/live/summercms.io/` with `options-ssl-nginx.conf` and `ssl-dhparams.pem` | Deploy | nginx -t fails. Copy the lines from the existing block. | +| A3 | Rome's nginx is Debian 12's 1.22 (`listen 443 ssl http2` syntax) | Deploy | Only a deprecation warning on ≥1.25 | +| A4 | Debian postgresql-15 is ICU-enabled | PG15 | Only `lagoon` Polish OrderBy is affected; the site does not use it | +| A5 | The sticky header breaks under `overflow-x:hidden` | Pitfall 3 | Low. Verify in browser and use `clip`. | +| A6 | Clipboard secure-context behaviour; vue-i18n special chars | Pitfalls 5–6 | Low | +| A7 | TS 7 incompatibility with Nuxt typecheck | Stack | Low (pinned to 5.9) | +| A8 | The static handler code skeleton (Pattern 2) | Patterns | The executor writes the real code; the APIs it uses are verified | + +## Open Questions + +1. **D-41 link label and how summercms.io sets `site_url`.** + - What we know: D-41 names one key, `site_url`, with summercms.io setting `/`. The docs are built from the framework tag's own `docs/site.yaml`, which must stay neutral. With a root-relative `/` there is no host to derive the "summercms.io" label from. + - Recommendation: add the `site_url` key **and** a `--site-url` flag on `docs:build`/`docs:serve` (mirrors `--base-url`). Derive the label from the URL host when absolute, else use a fixed "Home" text. Alternatively add an optional `site_label` key / `--site-label` flag so summercms.io shows "← summercms.io". Ask the user at the plan checkpoint, since both extend D-41's literal wording. +2. **Landing hrefs `.html` vs pretty.** Recommend `.html` hrefs plus a handler 301 for pretty URLs. This touches only hrefs, not copy, and the handoff asked for confirmation against the built docs. The user may prefer the pretty form, which still works via the redirect. +3. **A repeatable PG15 test switch.** The one-off export plus sed procedure is enough for D-25. A `SUMMER_TEST_POSTGRES_IMAGE` env override is a nicer but extra framework change. Default: do not add it. +4. **`<title>`, meta description and OG image layout.** The handoff gives no `<title>`. Proposal: title "SummerCMS: A new dawn in content management", description from the hero paragraph, OG 1200×630 on navy `#233148` with the sun, wordmark and tagline (an ImageMagick script, Roboto from `/usr/share/fonts/TTF`). Planner discretion within the brand. +5. **Sun sizing in the badge.** In the placeholder the sun fills about 75% of the 180px badge on navy. With the transparent original, render it at about 136px centred in the badge (2x asset 360px, webp 28 KB / png 113 KB measured). Confirm visually against the handoff screenshot. + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| Go | all Go builds/tests | ✓ | go1.27.0 linux/amd64 | — | +| Node | nuxt generate, node --test | ✓ | v22.23.2 | — | +| pnpm | Nuxt deps | ✓ | 11.3.0 | npm (not recommended; lockfile parity with fonoteka) | +| Docker daemon | D-25 PG15 run, local boot smoke | ✓ | 29.7.2 | — | +| postgres:15 / postgres:15-alpine images | D-25 | ✓ (pulled) | 15.16 | `docker pull postgres:15` | +| ImageMagick (`magick`, WEBP/ICO) | image derivation | ✓ | 7.1.2-30 | — | +| Roboto TTF (for the OG render) | OG image | ✓ `/usr/share/fonts/TTF/Roboto-*.ttf` | — | — | +| rsync | build copy + upload | ✓ | 3.5.0 | `cp -a` locally | +| nginx (local, for `nginx -t` of the DEPLOY config) | config validation | ✓ | 1.30.4 | — | +| supervisor (local) | config sanity | ✓ | 4.3.0 | — | +| psql client | local smoke | ✓ | 18.6 | docker exec psql | +| Anonymous access to git.golem15.com/golem15/summercms | D-38/D-40 verbatim clone, external link check | ✗ (404 / auth prompt) | Gitea | `SUMMERCMS_CLONE_URL` override until the user makes it public | +| rome (nginx, supervisor, certbot, PG 15.19) | deploy | not reachable from here | — | User executes DEPLOY.md, a manual checkpoint | + +**Missing dependencies with no fallback:** none for build and test. Rome access is a human step. +**Missing with fallback:** public repo access, via the clone override until cutover. + +## Validation Architecture + +### Test Framework +| Property | Value | +|----------|-------| +| Framework | Go `testing` (stdlib, `net/http/httptest`, `testing/fstest`), plus `node:test` (built-in, runs `.ts` natively on Node 22.23) | +| Config file | none. Go tests per repo. `vue-summercms-app/package.json` script `"test": "node --test tests/*.test.ts"` | +| Quick run command | plugin: `go test ./...` (in `plugins/golem15/summercms`); framework: `go test ./internal/docsite/ ./cmd/summer -run 'TestParseSite\|TestDocsTree\|TestBuildSiteMarkers'` | +| Full suite command | framework: `go vet ./... && go test ./...`; app: `scripts/build.sh dev` (runs the plugin tests with `SUMMERCMS_REQUIRE_BUILD=1`) then `go vet ./... && go test ./...`; site: `pnpm generate && pnpm test` | + +### Phase Requirements → Test Map +| Req | Behavior | Test Type | Automated Command | File Exists? | +|-----|----------|-----------|-------------------|-------------| +| SC1 | generate succeeds; sections/ids `top why features winter start` present; copy strings from en.json; no `fonts.googleapis`/`gstatic`; `/_fonts/*.woff2` present; robots.txt + flat sitemap.xml; og:image absolute; six commands in HTML | output assertion | `pnpm generate && node --test tests/output.test.ts` (reads `.output/public/index.html`) | ❌ Wave 0 | +| SC1 | scroll-spy picks the last section with top < 140, '' above `#why`; copy payload = six lines joined by `\n` | unit (pure TS) | `node --test tests/scrollSpy.test.ts tests/terminal.test.ts` | ❌ Wave 0 | +| SC1 | visual fidelity at 1280/721/720/375px; sticky header; Copy → "Copied" 1.5s; nav hidden ≤720 | manual UAT (browser) | — (manual-only: pixel/interaction fidelity) | n/a | +| SC2 | `/` 200 text/html no-cache no X-Robots-Tag; `/_nuxt/x.js` immutable; `/_nuxt/builds/latest.json` no-cache; `/_fonts/*.woff2` font/woff2 immutable; `/docs` 301; `/docs/` 200; `/docs/assets/site.css` no-cache; `/docs/x.md` text/markdown; `/missing` 404 with 404.html body; `/docs/missing` 404 docs page; dot-path 404; ETag → 304; HEAD ok; `..` traversal contained | unit (handler with fstest.MapFS) | `go test ./... -run TestStatic` (plugin) | ❌ Wave 0 | +| SC2 | plugin routes assemble alongside a fake admin-controller plugin without conflict; no admin routes when alone | unit | `go test ./... -run TestRoutesAssemble` (plugin, `surf.Assemble`) | ❌ Wave 0 | +| SC2 | binary boots on PG15 and serves both trees | integration smoke | `scripts/smoke.sh` (docker postgres:15, migrate, serve, curl assertions) | ❌ Wave 0 | +| SC3 | every href in the built index.html resolves: internal via the real handler (200, or 301→200), anchors to existing ids | integration (built tree) | `SUMMERCMS_REQUIRE_BUILD=1 go test ./... -run TestLandingLinks` (plugin) | ❌ Wave 0 | +| SC3 | external links 200 anonymously (Source, golem15.com) | network check at cutover | `SUMMERCMS_CHECK_EXTERNAL=1 go test ./... -run TestExternalLinks` | ❌ Wave 0 | +| SC4 | terminal commands run verbatim and end in `handled=true` | scripted e2e | `SUMMERCMS_TERMINAL_CHECK=1 [SUMMERCMS_CLONE_URL=../summercms.go] go test -run TestTerminalCommands .` (app) | ❌ Wave 0 | +| SC4 | build script produces a linux/amd64 binary with both trees embedded | scripted | `scripts/build.sh dev && file bin/summercms-io` | ❌ Wave 0 | +| SC4 | nginx config is syntactically valid | scripted (local nginx, temp self-signed certs at substituted paths) | `nginx -t -p $TMP -c $TMP/nginx.conf` | ❌ Wave 0 | +| SC4 | clean-server bring-up | manual (rome) | DEPLOY.md walk-through, a human checkpoint | n/a | +| D-25 | DB suites green on PG 15 | scripted throwaway | export + sed + `go test` (see §PostgreSQL 15) | ✅ procedure verified | +| D-41 | `site_url` parsed; header link rendered only when set; unset output byte-identical; bad schemes rejected; `--site-url` override | unit | `go test ./internal/docsite -run 'TestParseSite\|TestSiteURL'` and `go test ./cmd/summer -run TestDocsTree` | partial (extend `load_test.go`, `theme_test.go`) | +| SC5 | coverage of the new Go code | unit | `go test -cover ./...` in plugin and app | ❌ Wave 0 | + +### Sampling Rate +- **Per task commit:** the touched repo's `go vet ./... && go test ./...` (framework: add `-short` for speed except on D-25/D-41 tasks). Site: `pnpm generate` when Nuxt files change. +- **Per plan merge:** framework full `go test ./...` (Docker); `scripts/build.sh dev`; plugin tests with `SUMMERCMS_REQUIRE_BUILD=1`; `node --test`. +- **Phase gate:** all of the above, plus `SUMMERCMS_TERMINAL_CHECK=1` with a local clone override, `scripts/smoke.sh` on postgres:15, `nginx -t` of the shipped config, and the manual browser UAT. External links and verbatim clone are checked at cutover after D-38. + +### Wave 0 Gaps +- [ ] `vue-summercms-app/tests/{output,scrollSpy,terminal}.test.ts` and the `"test"` script +- [ ] `plugins/golem15/summercms/{static_test.go,routes_test.go,links_test.go}`, with fixtures via `fstest.MapFS` +- [ ] `sm-summercms-app/terminal_check_test.go`, `scripts/smoke.sh`, `scripts/check-nginx.sh` (optional) +- [ ] framework: extend `internal/docsite/load_test.go` (`site_url` cases) and `theme_test.go` (header link present/absent) + +## Security Domain + +### Applicable ASVS Categories +| ASVS Category | Applies | Standard Control | +|---------------|---------|-----------------| +| V2 Authentication | no (no auth surface; admin not activated) | — | +| V3 Session Management | no | — | +| V4 Access Control | yes (keep the admin unreachable) | No admin controllers means no `/backend` routes. nginx `location ^~ /backend { return 404; }` and `limit_except GET HEAD`. | +| V5 Input Validation | yes (request paths; `site_url` value) | `path.Clean` + `fs.Sub(embed.FS)` + dot-segment refusal; `site_url` scheme allow-list; `html/template` attribute escaping | +| V6 Cryptography | yes (TLS only) | certbot / Let's Encrypt at nginx. The app key is generated with `key:generate`. | +| V7 Error Handling/Logging | yes | `recoverBare` (no stack to client); supervisor log file | +| V9 Communications | yes | HTTPS-only with an HTTP→HTTPS 301; HSTS optional at nginx | +| V10 Malicious Code / supply chain | yes | Pinned npm versions + committed `pnpm-lock.yaml` + `--frozen-lockfile`; `allowBuilds` limited; no new Go deps | +| V14 Configuration | yes | Secrets only in server `.env` (0600, owner summercms), never rsynced from git. Dedicated PG role owning a single DB. Loopback-only listen. | + +### Known Threat Patterns +| Pattern | STRIDE | Standard Mitigation | +|---------|--------|---------------------| +| Path traversal / dotfile disclosure (`/docs/.summer-docs`, `/../`) | Information disclosure | Clean path, `fs.Sub` root, refuse `.` segments | +| Open redirect via the `.html` / `/docs` redirects | Spoofing | Location built from the cleaned relative path with a fixed `/docs/` prefix | +| MIME sniffing of user-agent-supplied content | Tampering | Explicit Content-Type + `X-Content-Type-Options: nosniff` (no user content is served) | +| Admin exposure | Elevation of privilege | Admin never activated + nginx deny | +| Secret leakage into git/rsync | Information disclosure | `.env` outside `config/`, `.gitignore`d, mode 0600 | +| Clickjacking of a static marketing page | Tampering | Low value. Optional `X-Frame-Options: SAMEORIGIN`. Do not add boardwalk's CSP (it breaks Nuxt inline scripts). | +| Dependency compromise in the npm tree | Tampering | Lockfile, frozen install, versions already in production use | + +## Sources + +### Primary (HIGH confidence) +- Framework code read this session: + - `modules/surf/{serve.go,router.go,clientip.go}` + - `modules/cabana/{http.go,prefix.go}` + - `modules/boardwalk/boardwalk.go` + - `modules/pact/capabilities.go` + - `modules/party/registry.go` + - `modules/lagoon/{commands.go,connection.go,encrypted.go,migrations.go,postgres_test.go}` + - `modules/lagoon/attach/bucket.go` + - `modules/conga/{worker.go,client.go}` + - `modules/bonfire/prompts.go` + - `modules/compass/README.md` + - `internal/build/{build.go,manifest.go,scaffold.go,leaf.go}` + - `internal/docsite/{load.go,emit.go,serve.go,docsite.go,check_forbidden.go}` + - `internal/docsite/theme/templates/header.html` + - `cmd/summer/{docs.go,runtime.go,main.go,docs_test.go}` + - `docs/site.yaml`, `docs/console/utilities.md`, `docs/setup/installation.md` + - `README.md` + - `examples/hello/*` +- Executed this session: + - PG15 suite run + - surf catch-all probe + - real `docs:build --base-url /docs` (+ git-archive determinism) + - D-40 command run in a temp clone + - Nuxt 4 spike (4 generate runs) + - live-site fetch + - git.golem15.com anonymous probe + - GOROOT mime table +- `../fonoteka.go/{go.work,go.mod,summer.yaml,config/*}`; `/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/{package.json,nuxt.config.ts,pnpm-workspace.yaml,.nvmrc,i18n/,patches/}` and installed versions. +- [CITED: go.dev/ref/mod#go-work-file-replace]: go.work replace overrides workspace module replaces. +- [CITED: pkg.go.dev/embed]: `_`/`.` exclusion, the `all:` prefix, no symlinks, a pattern must match files. + +### Secondary (MEDIUM confidence) +- npm registry (`npm view`) for latest versions; gsd package-legitimacy seam verdicts. + +### Tertiary (LOW confidence) +- Rome server specifics (nginx version, cert lineage, free port): not observable from here. + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH. Versions are installed in fonoteka and were rebuilt in the spike. +- Architecture: HIGH. Routing, docs layout and boot requirements were exercised against real code. +- Pitfalls: HIGH for 1, 2, 7–12 (reproduced or read). MEDIUM for 3, 5, 6 (spec knowledge, needs browser UAT). +- Deploy: MEDIUM. The live site was observed, but rome internals are assumed. + +**Research date:** 2026-10-01 +**Valid until:** 2026-10-31. The Nuxt ecosystem moves fast: @nuxtjs/i18n and nuxt-og-image published on 2026-09-30. Re-check if versions are unpinned. diff --git a/.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md b/.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md new file mode 100644 index 0000000..983de58 --- /dev/null +++ b/.planning/phases/11.2-ready-to-share-summercms-io-website-and-newsletter-plugin/11.2-VALIDATION.md @@ -0,0 +1,91 @@ +--- +phase: "11.2" +slug: "ready-to-share-summercms-io-website-and-newsletter-plugin" +# status lifecycle: draft (seeded by plan-phase) → validated (set by validate-phase §6) +# audit-milestone §5.5 distinguishes NOT-VALIDATED (draft) from PARTIAL (validated + nyquist_compliant: false) (#2117) +status: draft +nyquist_compliant: false +wave_0_complete: false +created: "2026-10-01" +--- + +# Phase 11.2 — Validation Strategy + +> Per-phase validation contract for feedback sampling during execution. Source: `11.2-RESEARCH.md` § Validation Architecture. + +--- + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | Go `testing` (stdlib, `httptest`, `testing/fstest`) in summercms.go, sm-summercms-plugin and sm-summercms-app; `node:test` in vue-summercms-app | +| **Config file** | none; `vue-summercms-app/package.json` script `"test": "node --test tests/*.test.ts"` (Wave 0) | +| **Quick run command** | touched repo: `go vet ./... && go test ./...`; site: `pnpm generate && pnpm test` | +| **Full suite command** | framework `go vet ./... && go test ./...`; app `scripts/build.sh dev` then `go vet ./... && go test ./...` (plugin with `SUMMERCMS_REQUIRE_BUILD=1`); site `pnpm generate && pnpm test` | +| **Estimated runtime** | ~180 seconds (framework DB suites dominate) | + +--- + +## Sampling Rate + +- **After every task commit:** the touched repo's `go vet ./... && go test ./...` (framework may use `-short` except on D-25/D-41 tasks); `pnpm generate` when Nuxt files change +- **After every plan wave:** framework full `go test ./...`; `scripts/build.sh dev`; plugin tests with `SUMMERCMS_REQUIRE_BUILD=1`; `pnpm test` +- **Before `/gsd-verify-work`:** full suite green, plus `SUMMERCMS_TERMINAL_CHECK=1` with a local clone override, `scripts/smoke.sh` on postgres:15, `nginx -t` of the shipped config +- **Max feedback latency:** 180 seconds + +--- + +## Per-Task Verification Map + +Filled from the PLAN.md files once written. Requirement → check map: + +| Req | Behavior | Test Type | Automated Command | File Exists | Status | +|-----|----------|-----------|-------------------|-------------|--------| +| SC1 | generate succeeds; section ids, en.json copy, self-hosted fonts, robots + flat sitemap, absolute og:image, six commands | output assertion | `pnpm generate && node --test tests/output.test.ts` | ❌ W0 | ⬜ pending | +| SC1 | scroll-spy selection; copy payload = six lines | unit (TS) | `node --test tests/scrollSpy.test.ts tests/terminal.test.ts` | ❌ W0 | ⬜ pending | +| SC2 | status, content type and cache headers for `/`, `/_nuxt`, `/_fonts`, `/docs`, 404s, ETag/304, traversal | unit | `go test ./... -run TestStatic` (plugin) | ❌ W0 | ⬜ pending | +| SC2 | plugin routes assemble without conflict; no admin routes | unit | `go test ./... -run TestRoutesAssemble` (plugin) | ❌ W0 | ⬜ pending | +| SC2 | binary boots on PG15 and serves both trees | integration smoke | `scripts/smoke.sh` (app) | ❌ W0 | ⬜ pending | +| SC3 | every href in the built index.html resolves | integration | `SUMMERCMS_REQUIRE_BUILD=1 go test ./... -run TestLandingLinks` (plugin) | ❌ W0 | ⬜ pending | +| SC3 | external links 200 anonymously | network, at cutover | `SUMMERCMS_CHECK_EXTERNAL=1 go test ./... -run TestExternalLinks` | ❌ W0 | ⬜ pending | +| SC4 | terminal commands run verbatim | scripted e2e | `SUMMERCMS_TERMINAL_CHECK=1 go test -run TestTerminalCommands .` (app) | ❌ W0 | ⬜ pending | +| SC4 | linux/amd64 binary with both trees embedded | scripted | `scripts/build.sh dev && file bin/summercms-io` | ❌ W0 | ⬜ pending | +| SC4 | nginx config syntactically valid | scripted | `nginx -t -p $TMP -c $TMP/nginx.conf` | ❌ W0 | ⬜ pending | +| D-25 | DB suites green on PG 15 | scripted throwaway | export + sed + `go test` (RESEARCH § PostgreSQL 15) | ✅ | ⬜ pending | +| D-41 | `site_url` parsed; header link only when set; unset output unchanged | unit | `go test ./internal/docsite -run 'TestParseSite\|TestSiteURL' && go test ./cmd/summer -run TestDocsTree` | partial | ⬜ pending | +| SC5 | coverage of new Go code | unit | `go test -cover ./...` (plugin, app) | ❌ W0 | ⬜ pending | + +*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* + +--- + +## Wave 0 Requirements + +- [ ] `vue-summercms-app/tests/{output,scrollSpy,terminal}.test.ts` and the `"test"` script +- [ ] sm-summercms-plugin `static_test.go`, `routes_test.go`, `links_test.go` with `fstest.MapFS` fixtures +- [ ] sm-summercms-app `terminal_check_test.go`, `scripts/smoke.sh`, optional `scripts/check-nginx.sh` +- [ ] framework: extend `internal/docsite/load_test.go` and `theme_test.go` for `site_url` + +--- + +## Manual-Only Verifications + +| Behavior | Requirement | Why Manual | Test Instructions | +|----------|-------------|------------|-------------------| +| Visual fidelity at 1280/721/720/375px, sticky header, Copy → "Copied" 1.5s, nav hidden ≤720px | SC1 | pixel and interaction fidelity against the handoff | open the generated site next to `design/SummerCMS Landing.dc.html` at each width | +| Clean-server bring-up on rome | SC4 | server not reachable from the dev machine | follow DEPLOY.md end to end; human checkpoint | +| Source link anonymous access | SC3 / D-38 | repo must be made public by the user first | run `TestExternalLinks` at cutover | + +--- + +## Validation Sign-Off + +- [ ] All tasks have `<automated>` verify or Wave 0 dependencies +- [ ] Sampling continuity: no 3 consecutive tasks without automated verify +- [ ] Wave 0 covers all MISSING references +- [ ] No watch-mode flags +- [ ] Feedback latency < 180s +- [ ] `nyquist_compliant: true` set in frontmatter + +**Approval:** pending