90 KiB
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>
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-summercmsio-appis the root app. It builds the binary, holds the other pieces and wires the plugins.vue-summercmsio-appis the Nuxt 4 site, held inside the root app asvue-fonoteka-appis in the fonoteka project.sm-summercmsio-pluginis 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 fromfigs.org.pl(figs/website) and golem15.com.
Public static-site serving goes in
sm-summercmsio-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 examplegit.golem15.com/golem15/sm-summercmsio-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.godoes 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 asvue-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, thensummer docs:build, thengo build) is documented and scripted in the root app. - D-07: The site's served responses must be indexable. The admin-only
boardwalkbehaviour (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 servepath, which opens Postgres unconditionally (modules/surf/serve.go:40). There is no framework change to make the DB optional.romealready 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 apostgres:15image. If they pass, change the documented requirement from "PostgreSQL 16" to "PostgreSQL 15 or newer" in the rootREADME.md,docs/setup/installation.mdand 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 documentscreateuser/createdb, and every deploy runs the app'smigratecommand before restarting.
Server and deploy flow
- D-28: The binary is built locally for linux/amd64 by a script in
sm-summercmsio-app(nuxt generate, thensummer docs:build --base-url /docs, thengo build), then rsynced to rome with its config. The server needs neither Go nor Node. - D-29:
/backendand the admin API are not exposed publicly. nginx proxies only/and/docsto 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/i18nis installed and configured with the single localeen, and all page copy lives inlocales/en.json. 11.3 addspl.jsonand 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 asvue-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'ssun-crop.pngplaceholder. 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/seomodule, asvue-fonoteka-appdoes: meta, Open Graph and Twitter tags, OG image, schema.org, sitemap and robots. Whatever it generates must work withnuxt generateand be embedded (no runtime OG rendering in the Go binary). - D-45: The prototype's
showRaysandscrollSpyflags become Nuxt app config, both defaulting totrue.
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 frameworkcomment, then$ git clone https://git.golem15.com/golem15/summercmsand$ cd summercms, before the existinggo install ./cmd/summergroup. 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_urlkey indocs/site.yamladds 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 describessite.yaml, andsummer docs:build --checkandTestDocsTreestay green. summercms.io sets it to/. - D-42: The framework is tagged
v0.1.0(Alpha 0.1).sm-summercmsio-apprequiresgit.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.goin the meta repo directory (summercms/sm-summercmsio-app/, likefonoteka.go).vue-summercmsio-appandsm-summercmsio-pluginare submodules insidesm-summercmsio-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 indocs/.
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-summercmsio-pluginor by the root app, and how the plugin mounts/docs. - The 404 handling for unknown paths (for example Nuxt's generated
404.htmlwith 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-motionaffects 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.gotosm-fonoteka-appis a separate task. - Translating the docs into Polish.
go install git.golem15.com/golem15/summercms/cmd/summer@latestas 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.) </user_constraints>
<session_choices>
Choices made in this planning session (orchestrator-provided, binding for the planner)
- Plan count is fixed at 3 plans:
- 01: the Nuxt site (
vue-summercmsio-app). - 02: framework tweaks (PG15 verification and docs, the optional
site_url, the v0.1.0 tag checkpoint), thensm-summercmsio-plugin,sm-summercmsio-app, the build script, the D-40 command check, the link check, and DEPLOY.md with the nginx and supervisor configs. - 03: unit tests.
- 01: the Nuxt site (
- No UI-SPEC.md. The
design/handoff (README.mdandSummerCMS Landing.dc.html) is the design contract (D-23). </session_choices>
<phase_requirements>
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-summercmsio-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-summercmsio-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 |
| </phase_requirements> |
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 vetandgo 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 TestDocsTreeandsummer docs:build --checkmust stay green. D-41 adds asite.yamlkey (and, as recommended below, adocs:buildflag), sodocs/console/utilities.mdchanges 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_urlkey 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:
- Docs pages are
<path>.htmlfiles, not directories. The handoff's/docs/backend/admin-sparesolves only if the site handler maps extension-less paths to.html. The page links should use the canonical.htmlform. nuxt generatefails (exit 1, "Exiting due to prerender errors") on any/docslink unlessnitro.prerender.ignore: ['/docs']is set. This was reproduced and the fix confirmed.- Nuxt writes
_nuxt/,_fonts/,_i18n/and_payload.json.go:embeddrops names that start with_unless the pattern usesall:. - 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/*(excludingbuilds/) and/_fonts/*. Everything else gets no-cache with an ETag. - 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:publicinsm-summercmsio-plugin. The build script fillspublic/site(Nuxt) andpublic/docs(docs from agit archive v0.1.0export), and a placeholderpublic/README.mdis committed. - Serve both trees with one stdlib handler that has its own MIME table, extension-less
.htmlresolution, 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-summercmsio-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-summercmsio-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-summercmsio-app):
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 <pkg> 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.RouterhasGroup,GroupRaw,Get,Post,Put,Patch,Delete,WhereandWhereIn[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 inrecoverBare[VERIFIED: router.go:418-422]. UseGroupRawfor 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_bytesandhttp.body_limits.upload_bytesmust be present and numeric wheneverapp.Configis non-nil, or boot fails withsurf: 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 implementpact.HasAdminControllers(themake:pluginstub does, so do not use the stub as-is). checkAdminPrefixfails boot when a non-cabana route sits at or underbackend.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/docsfrom three places. Either register an explicitGET /docsthat 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.FSwithhttp.ServeContent(boardwalk.go:132, 147). - Use an explicit content-type table before the
mimefallback (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:
SetSecurityHeaderssetsX-Robots-Tag: noindex, nofollow,Content-Security-Policy: frame-ancestors 'none'; base-uri 'none'; object-src 'none'; script-src 'self'andX-Frame-Options: DENY[VERIFIED: boardwalk.go:32, 167-173]. Nuxt emits inline scripts, soscript-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'sno-store.
summer build and summer.yaml
summer.yamlhasmodule,binaryand an orderedplugins: [{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.summercmsis valid. Binary names allow letters, digits,.,_and-(manifest.go:149-165).summercms-iois valid.summer buildwritesmain.goandplugins.gen.go(only when changed), checks the models leaf, then runsgo build -o bin/<binary> .withGOWORKset to the nearestgo.work(build.go:19-77; scaffold.go:519-541). It inherits the environment, soGOOS/GOARCHpass through. Rome and the local host are both linux/amd64.- The generated
main.goregisterslagoon.RuntimeCommands(migrate,migrate:rollback,migrate:status,key:generate),conga.RuntimeCommands,surf.ServeCommand,surf.RouteListCommand,cabana.RuntimeCommandsand pluginHasCommands[VERIFIED: build.go:114-123; lagoon/commands.go:16-75]. D-27's migrate command is./bin/summercms-io migrate(summer migratedelegates to the same binary, cmd/summer/runtime.go:16-26). compass.Load("config")is relative to the working directory (build.go:105). Supervisor'sdirectory=must therefore be the app dir.SUMMER_ENVdefaults toproduction. A.envnext toconfig/suppliesKEY=VALUElines not already in the environment [VERIFIED: modules/compass/README.md:90-97].summer plugin:add <dir>adds the plugin tosummer.yaml, runsgo mod edit -require=<mod>@v0.0.0 -replace=<mod>=./<rel>andgo work use <dir>(scaffold.go:106-170, 318-370; docs/console/scaffolding.md:34). It reads the plugin ID from the plugin'sID()return literal (scaffold.go:284-316). Use it after creating the plugin by hand, becausemake:pluginwould set the module path to<app module>/plugins/<name>(scaffold.go:75), notgit.golem15.com/golem15/sm-summercmsio-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 notusethe 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 fromfonoteka.go/plugins/golem15/user. - Config:
config/{app,http,storage,database,queue,...}.yaml.database.yamlhasdsn: ""(env-provided).app.yamlhaskey: ""with the comment "Set SUMMER_APP__KEY".http.yamlhasbody_limits.storage.yamlhasuploads.bucket_url: "file://./storage/app/uploads".queue.yamlhaswork_in_serve: true. .gitignore:/bin/,/tmp/,go.work.sum.
For sm-summercmsio-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 <tmp>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},<section>/<page>.htmland<section>/<page>.md,api/<module>.html, plus the marker file.summer-docs[VERIFIED: build run; docsite.go:24const MarkerFile = ".summer-docs"]. - Every internal link is absolute with the base, for example
href="/docs/api/backpack.html",/docs/assets/site.cssandsrc="/docs/assets/site.js". The URL rule iss.base + "/" + p, and nav/pager links usep.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 -fon the build output]. They do not exist without the extension. The built-indocsite.Handlerdoes 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]. --outhandling: a non-empty out dir without.summer-docsis refused. With the marker it is emptied, then written (docsite.go:189-211). Never commit a placeholder inside the docs out dir.--outmust not be inside--srcor equal to the root (docsite.go:180-187).- Determinism: building from
git archive HEAD | tar -xproduced output byte-identical to the working tree (diff -rclean). The docs build can run against a tag export [VERIFIED: this session]. site.yamlparsing: theSitestruct has the fieldstitle,description,base_url,edit_url,source_url,llms_notesandsections, decoded withyaml.DisallowUnknownField()[VERIFIED: internal/docsite/load.go:21-31, 88-93]. An unset new key needs a struct field, or decode fails. The CLI--base-urloverridesbase_url(s.base = strings.TrimRight(cfg.BaseURL, "/"), thenif opts.BaseURL != "" {...}) [VERIFIED: load.go:217-221].- Header template (whole file) [VERIFIED: internal/docsite/theme/templates/header.html:1-9]: menu button,
<a class="wordmark" href="{{.HomeURL}}" ...>,<div class="header-spacer"></div>, search trigger, theme toggle.pageViewcarriesHomeURLetc. and is filled bybaseView(emit.go:62-95). The 404 page also usesbaseView(nil)(emit.go:144). D-41 needs:Site.SiteURL string \yaml:"site_url"`→ apageView.SiteURLfield set inbaseView→{{if .SiteURL}}…{{end}}in header.html → a.site-linkrule intheme/assets/site.css. The theme already inlinesicon-chevron-left` (11.1 UI-SPEC icon list). - Which docs page documents site.yaml: none fully.
docs/console/utilities.md:31-46documentsdocs:build/docs:serveflags in a table (--root,--src,--out,--base-url,--check). Add thesite_urlkey (and the recommended--site-urlflag) there, preferably with a short "site.yaml keys" paragraph. - Checker requirements for a new key:
TestDocsTree(cmd/summer/docs_test.go:25-33) runsdocsite.Checkon 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 anunknown keycase. Add asite_urlpositive 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 owndocs/site.yaml: the docs are built from the framework tag'sdocs/site.yaml, which must not name the site, and setting/there would also makesummer docs:servelink to itself. Recommend a--site-urlflag ondocs:build(anddocs:serve) that overridessite_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:34modules/lagoon/README.md:201modules/conga/README.md:207
How to run on 15 without permanent edits (done this session):
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_USERandpg_stat_iofound nothing. The ICU database-locale check was removed by quick task 261001-ddh (037dc53).pl-x-icuper-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:15andpostgres:15-alpineare already pulled. - Recommendation: plan 02 re-runs exactly this scripted, throwaway procedure, records the
go testoutput in its SUMMARY, then edits the three doc lines. Preferpostgres:15(Debian, like rome) over-alpine. An env-var override for the test image (for exampleSUMMER_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/<hash>.js _nuxt/entry.<hash>.css _nuxt/error-404.<hash>.css _nuxt/error-500.<hash>.css
_nuxt/builds/latest.json _nuxt/builds/meta/<buildId>.json <- NOT content-hashed names
_fonts/<hash>.woff2 (4 files with the subset/style config below)
_i18n/<hash>/en/messages.json <- fetched at hydration
__sitemap__/style.xsl
The HTML preloads /_payload.json?<buildId> and modulepreloads /_nuxt/*.js.
Spike results that drive the config:
- Prerender fails on
/docslinks.nuxt generateexit 1:[404] Page not found: /docs ... Linked from /,ERROR Exiting due to prerender errors.Fix (verified, exit 0):nitro: { prerender: { ignore: ['/docs'] } }andlinkChecker: { excludeLinks: ['/docs', '/docs/**'] }. - With i18n, the sitemap becomes an index. It produced
sitemap_index.xml,__sitemap__/en-US.xmland a directorysitemap.xml/index.htmlmeta-refresh, and robots pointed atsitemap_index.xml. Fix (verified):sitemap: { autoI18n: false }gives one flatsitemap.xml, and robots saysSitemap: https://summercms.io/sitemap.xml. - Fonts: with only
weights, @nuxt/fonts fetched 26 files (normal+italic × every subset). Withstyles: ['normal'], subsets: ['latin', 'latin-ext']it fetched 4 files. The@font-facerules are inlined inentry.<hash>.csswithurl(../_fonts/...). No googleapis/gstatic reference appears in the HTML. - Title duplication: page title "SummerCMS" +
site.name"SummerCMS" renderedog:title "SummerCMS | SummerCMS". Set the home title so the template does not duplicate (for example a full title plustitleTemplate: '%s'for the index page). The handoff names no<title>text, so the copy is planner/user discretion; the live page uses "SummerCMS - Coming Soon". - Generated head (good defaults): canonical
https://summercms.io/,og:url,og:site_name,og:locale en_US,twitter:card summary_large_image, an absoluteog:imagefromsite.url,robots index, follow, max-image-preview:large, schema.orgWebSite+WebPageJSON-LD and<html lang="en-US">. - i18n v10 single locale: with
strategy: 'prefix_except_default'anddefaultLocale: 'en',enis served at/, and 11.3 addsplat/pl/without moving/. Messages are emitted to_i18n/<hash>/en/messages.jsonand fetched on hydration, so the Go handler must serve_i18n/(Pitfall 7). Locale files live ati18n/locales/en.json(restructureDirdefaulti18n/+langDir: 'locales/', the same as fonoteka'si18n/locales/*.json). CONTEXT's "locales/en.json" means that file. ogImage: { enabled: false }plus a staticpublic/og-image.pngworked with no warnings. Fonoteka also ships static OG PNGs.- The harmless warning
[nuxt-seo-utils] treeShakeUseSeoMeta requires Unhead v3appears. 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:
#f5c55aon#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, insideflex: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 withmask-image: radial-gradient(circle,#000 52%,transparent 70%)andtransform: scale(1.35), logo rowgap: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: prettyon 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 padding12px 22px, borderrgba(255,255,255,0.14), hover bgrgba(255,255,255,0.08)with text#fff.
- Chips row
- Terminal:
- Title bar
padding:12px 18px, bottom borderrgba(255,255,255,0.07). - Copy button font
500 12px Roboto. - Body
overflow-x:auto; display:flex; flex-direction:column. Each line is awhite-space:preblock. Each group's comment line after the first hasmargin-top:16px.
- Title bar
- Footer: row
gap:24px; flex-wrap:wrap, then aflex:1spacer, then the link groupgap: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 JSwidestate (Pitfall 4). - Scroll-spy: the active section is the last of
why,features,winter,startwhosegetBoundingClientRect().top < 140. Before#why, nothing is active. Listen toscrollwith{passive:true}and also compute on mount. Gate it onappConfig.scrollSpy(D-45). - Copy:
navigator.clipboard.writeText(lines.join('\n')), label "Copied" for 1500ms, then "Copy". Gate the rays onappConfig.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
.htmlform (/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
.htmlwhen 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:
go install ./cmd/summercd examples/hellosummer buildprintedbuilt hello in 2.185s../bin/hello greeter:hello </dev/nullprinted the tablegolem15.greeter activeandname=hello-app posts_per_page=10 debug=false extra=hello-from-optional events=ok collected=greeter handled=true,EXIT=0.- The tree was clean afterwards (no
main.godrift), so the README "Known issues" note about a stalemain.gois 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
GOBINplaced first on PATH, because a stalesummermay 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/summercmsfails 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 200text/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.png301s to HTTPS.robots.txtandfavicon.icoreturn 404. The page useslogo.pngas its favicon and OG image.- The Let's Encrypt cert (issuer YE1) has SAN
summercms.io, www.summercms.io. http://www.summercms.io/301s tohttps://summercms.io/, buthttps://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.htmlpluslogo.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):
- Launch checklist:
- Create the remotes on git.golem15.com for the three repos (D-43).
- Make
golem15/summercmspublic (D-38). - Confirm the
v0.1.0tag exists (D-42). - Run the external link check anonymously.
- 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) withSUMMER_DATABASE__DSN=postgres://summercms:<pw>@127.0.0.1:5432/summercms_io?sslmode=disableandSUMMER_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.)
- Build (local):
scripts/build.sh(see Code Examples) producesbin/summercms-io(linux/amd64,CGO_ENABLED=0). - Upload:
rsync -av --chmod=F644,D755 config/ rome:/srv/summercms-io/config/. Exclude any server-only files;.envlives outsideconfig/.rsync -av bin/summercms-io rome:/srv/summercms-io/bin/summercms-io.new.
- 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/.
- 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 withss -ltnpon rome]directory=/srv/summercms-iouser=summercmsenvironment=SUMMER_ENV="production"autostart=true,autorestart=true,startsecs=3stopsignal=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.logwith size/backups- Then
supervisorctl reread && supervisorctl update.
- 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 besummercms.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 cannginx -tit. - Then
nginx -t && systemctl reload nginx.
- port 80 → 301 to
- 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.
- App:
- Verification after cutover: curl checks for status,
Cache-Control, noX-Robots-Tag: noindex,/docs/200,/backend404,POST /403 (nginx), and HTTPS www→apex. Then run the link check againsthttps://summercms.io.
Architecture Patterns
System Architecture Diagram
BUILD (local, scripts/build.sh in sm-summercmsio-app)
┌──────────────────────────┐ pnpm install --frozen-lockfile + nuxt generate
│ vue-summercmsio-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-summercmsio-app/ # root app, module git.golem15.com/golem15/sm-summercmsio-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-summercmsio-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-summercmsio-plugin, module git.golem15.com/golem15/sm-summercmsio-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)
// 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)
// 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 serve404.htmlwith status 404. - Set
Content-Typefrom an own table beforemime.TypeByExtension. - Set
Cache-Controlper the table below. - Precompute a strong
ETag(sha256 prefix) per file at construction, sono-cacherevalidates to 304 throughhttp.ServeContent. - No
X-Robots-Tagand no CSP.X-Content-Type-Options: nosniffandReferrer-Policy: strict-origin-when-cross-originare 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 builtintext/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-summercmsio-app/app/data/terminal.json (imported by TerminalCard.vue; comments are i18n keys):
{ "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-summercmsio-app,
TestTerminalCommands):- Skip unless
SUMMERCMS_TERMINAL_CHECK=1. - Read the JSON and make a temp dir and a temp
GOBIN, withPATH=$GOBIN:$PATH,GOWORKunset and noSUMMER_*env. - Run
bash -euo pipefail -c "<commands joined by \n>"in the temp dir with stdin/dev/null. - If
SUMMERCMS_CLONE_URLis set, substitute only the clone URL (pre-launch, for example the local../summercms.go). Unset means verbatim (cutover). - Assert exit 0 and
handled=truein the output.
- Skip unless
- Drift guard (plugin test after build): assert the embedded
public/site/index.htmlcontains each command string and each of the three comment texts. The page and the check then cannot diverge.
Anti-Patterns to Avoid
//go:embed publicwithoutall:. Silently drops_nuxt/,_fonts/,_i18n/and_payload.json, so the page renders unstyled and hydration fails [CITED: pkg.go.dev/embed].- Embedding the
distsymlink.nuxt generatecreatesdist -> .output/public, and embed refuses symlinks [CITED: pkg.go.dev/embed]. Copy withrsync -a --delete .output/public/ <plugin>/public/site/. - Reusing
boardwalk.Handlerorboardwalk.SetSecurityHeaders. These bring noindex, CSP and the SPA fallback (D-07). - Implementing
pact.HasAdminControllers(as themake:pluginstub does). It would activate cabana, requireadmin.jwt.secretand mount/backend. - Committing a placeholder file inside
public/docs/.docs:buildrefuses to clean a dir without the.summer-docsmarker. - Copying the prototype's JS
widestate. 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.yamlwithbody_limits: {default_bytes: 1048576, upload_bytes: 1048576}config/storage.yamlwithuploads.bucket_url: "file://./storage/app/uploads"(ormem://)config/queue.yamlwithwork_in_serve: falseconfig/app.yamlwithkey: ""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-summercmsio-app/scripts/build.sh)
#!/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-summercmsio-app" && pnpm install --frozen-lockfile && pnpm generate)
rsync -a --delete "$APP/vue-summercmsio-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
<!-- 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}}
SiteLabelis derived (see Open Question 1).html/templateescapes the href attribute.- Validate
site_urlinParseSite: it must be absolutehttp(s)://or root-relative/.... Rejectjavascript:.
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
- 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 owndocs/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_urlkey and a--site-urlflag ondocs:build/docs:serve(mirrors--base-url). Derive the label from the URL host when absolute, else use a fixed "Home" text. Alternatively add an optionalsite_labelkey /--site-labelflag so summercms.io shows "← summercms.io". Ask the user at the plan checkpoint, since both extend D-41's literal wording.
- What we know: D-41 names one key,
- Landing hrefs
.htmlvs pretty. Recommend.htmlhrefs 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. - A repeatable PG15 test switch. The one-off export plus sed procedure is enough for D-25. A
SUMMER_TEST_POSTGRES_IMAGEenv override is a nicer but extra framework change. Default: do not add it. <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#233148with the sun, wordmark and tagline (an ImageMagick script, Roboto from/usr/share/fonts/TTF). Planner discretion within the brand.- 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-summercmsio-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-shortfor speed except on D-25/D-41 tasks). Site:pnpm generatewhen Nuxt files change. - Per plan merge: framework full
go test ./...(Docker);scripts/build.sh dev; plugin tests withSUMMERCMS_REQUIRE_BUILD=1;node --test. - Phase gate: all of the above, plus
SUMMERCMS_TERMINAL_CHECK=1with a local clone override,scripts/smoke.shon postgres:15,nginx -tof the shipped config, and the manual browser UAT. External links and verbatim clone are checked at cutover after D-38.
Wave 0 Gaps
vue-summercmsio-app/tests/{output,scrollSpy,terminal}.test.tsand the"test"scriptplugins/golem15/summercms/{static_test.go,routes_test.go,links_test.go}, with fixtures viafstest.MapFSsm-summercmsio-app/terminal_check_test.go,scripts/smoke.sh,scripts/check-nginx.sh(optional)- framework: extend
internal/docsite/load_test.go(site_urlcases) andtheme_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/, .gitignored, 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.gomodules/pact/capabilities.gomodules/party/registry.gomodules/lagoon/{commands.go,connection.go,encrypted.go,migrations.go,postgres_test.go}modules/lagoon/attach/bucket.gomodules/conga/{worker.go,client.go}modules/bonfire/prompts.gomodules/compass/README.mdinternal/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.htmlcmd/summer/{docs.go,runtime.go,main.go,docs_test.go}docs/site.yaml,docs/console/utilities.md,docs/setup/installation.mdREADME.mdexamples/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, theall: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.