`. (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 `/index.html`, then (docs only) `
.html` → 301 to `/docs/
.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 ""` 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/ /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 `` 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 ``. 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 `
` inside the Why card body (`fields.yaml`, `columns.yaml`) should use `` 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
+
+{{- if .SiteURL}}{{template "icon-chevron-left"}}{{.SiteLabel}}{{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. **``, meta description and OG image layout.** The handoff gives no ``. 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 `` 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