docs(11.2): map analog patterns for the landing page phase
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
# Phase 11.2: summercms.io Alpha 0.1 landing page on SummerCMS - Pattern Map
|
||||
|
||||
**Mapped:** 2026-10-01
|
||||
**Files analyzed:** 27 (new or modified, across 4 repos)
|
||||
**Analogs found:** 22 / 27
|
||||
|
||||
Paths: `FW` = `summercms/summercms.go` (framework), `APP` = `summercms/sm-summercms-app` (new), `PLUG` = `APP/plugins/golem15/summercms` (new submodule, module `git.golem15.com/golem15/sm-summercms-plugin`), `SITE` = `APP/vue-summercms-app` (new submodule), `FON` = `summercms/fonoteka.go`, `VFON` = `/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app`. All framework analog paths below are git-tracked source (checked with `git ls-files`); none are mirrors.
|
||||
|
||||
## File Classification
|
||||
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
|---|---|---|---|---|
|
||||
| `FW/internal/docsite/load.go` (Site.SiteURL, SiteLabel, validation) | config/model | transform | itself: `Site.BaseURL` + `ParseSite` (load.go:21-31, 88-100) | exact |
|
||||
| `FW/internal/docsite/emit.go` (pageView.SiteURL/SiteLabel, baseView) | utility | transform | itself: `pageView.HomeURL` / `baseView` (emit.go ~62-95) | exact |
|
||||
| `FW/internal/docsite/theme/templates/header.html` | template | transform | itself (wordmark anchor, line 4) | exact |
|
||||
| `FW/internal/docsite/theme/assets/site.css` (`.site-link`) | style | - | existing `.wordmark` / `.search-trigger` rules | exact |
|
||||
| `FW/cmd/summer/docs.go` (`--site-url`, `--site-label` on build and serve) | controller (CLI) | request-response | itself: `base-url` flag (docs.go:31, 93, 128) | exact |
|
||||
| `FW/internal/docsite/load.go` Options + override in `load()` | config | transform | `opts.BaseURL` override (load.go:217-221) | exact |
|
||||
| `FW/docs/console/utilities.md` | docs | - | itself, the flags table (lines ~33-37) | exact |
|
||||
| `FW/README.md`, `FW/docs/setup/installation.md` (PG 15+) | docs | - | in-place text edit | n/a |
|
||||
| `FW/internal/docsite/load_test.go`, `theme_test.go` | test | - | `TestParseSite` table (load_test.go:11-50) | exact |
|
||||
| `PLUG/plugin.go` | plugin (route registrar) | request-response | `FW/examples/hello/plugins/base/plugin.go` + `greeter/plugin.go` Routes | role-match |
|
||||
| `PLUG/static.go` | handler (static file server) | file-I/O | `FW/modules/boardwalk/boardwalk.go` + `FW/internal/docsite/serve.go:236-290` | exact (idea), with deliberate deviations |
|
||||
| `PLUG/public/README.md` | placeholder | - | none (embed needs a matching file) | no analog |
|
||||
| `PLUG/go.mod` | config | - | `FON/plugins/golem15/user/go.mod` | exact |
|
||||
| `PLUG/static_test.go` | test | - | `FW/modules/boardwalk/boardwalk_test.go` (fstest + httptest) | exact |
|
||||
| `PLUG/routes_test.go` | test | - | `FW/modules/surf/example_test.go:114-140` (`surf.Assemble`) and `FW/examples/hello/hello_test.go:201` | role-match |
|
||||
| `PLUG/links_test.go` | test (integration, built tree) | file-I/O | `boardwalk_test.go` `assetRef` regex walk | partial |
|
||||
| `APP/go.mod`, `APP/go.work` | config | - | `FON/go.mod`, `FON/go.work` | exact |
|
||||
| `APP/summer.yaml` | config | - | `FON/summer.yaml` | exact |
|
||||
| `APP/main.go`, `APP/plugins.gen.go` | generated | - | `FON/main.go`, `FON/plugins.gen.go` (`summer build` output) | exact (generated, do not hand-write) |
|
||||
| `APP/config/{app,http,storage,database,queue}.yaml` | config | - | `FON/config/*.yaml` | exact |
|
||||
| `APP/.gitignore` | config | - | `FON/.gitignore` | exact |
|
||||
| `APP/scripts/build.sh` | script | batch | `FON/scripts/check-openapi.sh` (bash skeleton) + RESEARCH build outline | role-match |
|
||||
| `APP/terminal_check_test.go` | test (e2e) | batch | `FW/examples/hello/hello_test.go` | partial |
|
||||
| `APP/scripts/smoke.sh`, `APP/deploy/*.conf`, `APP/DEPLOY.md`, `APP/README.md` | script/config/docs | - | none in repo | no analog (use RESEARCH Deploy section) |
|
||||
| `SITE/nuxt.config.ts`, `package.json`, `pnpm-workspace.yaml` | config | - | `VFON/nuxt.config.ts`, `VFON/package.json`, `VFON/pnpm-workspace.yaml` | exact (subset) |
|
||||
| `SITE/i18n/locales/en.json` | config (copy) | - | `VFON/i18n/locales/en.json` | exact (location) |
|
||||
| `SITE/app/components/*.vue`, `app/utils/*.ts`, `app/data/terminal.json`, `tests/*.test.ts`, `scripts/derive-images.sh` | component/utility/test | event-driven (scroll, clipboard) | none suitable (VFON uses Tailwind + shadcn; D-35 forbids) | no analog (use design handoff + RESEARCH Pattern 3) |
|
||||
|
||||
## Pattern Assignments
|
||||
|
||||
### `FW/internal/docsite/load.go` (config, transform) — D-41/D-46
|
||||
|
||||
**Analog:** itself. Add fields next to `BaseURL` (load.go, struct `Site`):
|
||||
```go
|
||||
type Site struct {
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
BaseURL string `yaml:"base_url"`
|
||||
// EditURL and SourceURL carry a {path} token ...
|
||||
EditURL string `yaml:"edit_url"`
|
||||
SourceURL string `yaml:"source_url"`
|
||||
LLMSNotes []string `yaml:"llms_notes"`
|
||||
Sections []Section `yaml:"sections"`
|
||||
}
|
||||
```
|
||||
A struct field is mandatory: `ParseSite` decodes with `yaml.DisallowUnknownField()`. Validation goes with the existing checks in `ParseSite`, same error style:
|
||||
```go
|
||||
if s.Title == "" {
|
||||
return Site{}, fmt.Errorf("docsite: site config: title is required")
|
||||
}
|
||||
```
|
||||
(add e.g. `"docsite: site config: site_url must be http(s):// or start with /"`; reject `javascript:` and `//host`).
|
||||
|
||||
**CLI override pattern** (load.go:217-221), mirror for site_url/site_label from `Options`:
|
||||
```go
|
||||
s.cfg, s.cfgRaw = cfg, raw
|
||||
s.base = strings.TrimRight(cfg.BaseURL, "/")
|
||||
if opts.BaseURL != "" {
|
||||
s.base = strings.TrimRight(opts.BaseURL, "/")
|
||||
}
|
||||
```
|
||||
Validate the flag value through the same function as the yaml value. Label rule (D-46): explicit label, else URL host, else "Home" for relative.
|
||||
|
||||
### `FW/internal/docsite/emit.go` (utility, transform)
|
||||
|
||||
Add `SiteURL, SiteLabel string` to `pageView` and fill them in `baseView`, which the 404 page also uses (`baseView(nil)`), so the link appears on every page:
|
||||
```go
|
||||
func (s *site) baseView(current *Page) pageView {
|
||||
return pageView{
|
||||
Assets: s.url("assets"),
|
||||
HomeURL: s.url("index.html"),
|
||||
SearchIndex: s.url("search-index.json"),
|
||||
LLMS: s.url("llms.txt"),
|
||||
LLMSFull: s.url("llms-full.txt"),
|
||||
Nav: s.nav(current),
|
||||
}
|
||||
}
|
||||
```
|
||||
Do not pass SiteURL through `s.url()` (that prefixes base_url); it is an absolute or root-relative link to a different site.
|
||||
|
||||
### `FW/internal/docsite/theme/templates/header.html`
|
||||
|
||||
Current line 4 (insert after it, before `header-spacer`, so unset output is byte-identical):
|
||||
```html
|
||||
<a class="wordmark" href="{{.HomeURL}}" aria-label="SummerCMS documentation home"><span class="wordmark-sun">{{template "icon-sun"}}</span><span class="wordmark-text">Summer<span class="wordmark-cms">CMS</span></span></a>
|
||||
```
|
||||
Insert: `{{- if .SiteURL}}<a class="site-link" href="{{.SiteURL}}">{{template "icon-chevron-left"}}<span>{{.SiteLabel}}</span></a>{{end}}`. Use `{{-` trimming carefully so the unset branch adds no whitespace; assert byte-identical output in the test.
|
||||
|
||||
### `FW/cmd/summer/docs.go` (CLI)
|
||||
|
||||
Flag declaration (docs.go:31, repeat at :93 for docs:serve):
|
||||
```go
|
||||
{Name: "base-url", Description: "Base URL for site links (overrides site.yaml base_url)"},
|
||||
```
|
||||
Flag read (docs.go:124-130):
|
||||
```go
|
||||
func docsOptions(in bonfire.Input) docsite.Options {
|
||||
opts := docsite.Options{Commands: docsCommands()}
|
||||
opts.Root, _ = in.Flag("root")
|
||||
opts.Src, _ = in.Flag("src")
|
||||
opts.Out, _ = in.Flag("out")
|
||||
opts.BaseURL, _ = in.Flag("base-url")
|
||||
return opts
|
||||
}
|
||||
```
|
||||
Add `site-url` / `site-label` the same way. Check whether docs:serve builds options through `docsOptions` too; if not, add the read there.
|
||||
|
||||
### `FW/docs/console/utilities.md`
|
||||
|
||||
Extend the `Flags` cells of the `docs:build` and `docs:serve` rows (`--root` (default `.`), `--src`, `--out`, `--base-url`, `--check`) and add a short "site.yaml keys" paragraph. Example URL must be neutral (`https://acme.example/`), never summercms.io (CLAUDE.md docs rule).
|
||||
|
||||
### `FW/internal/docsite/load_test.go` (test)
|
||||
|
||||
Analog `TestParseSite` (load_test.go:11-50): `const valid = "title: Acme\ndescription: Acme docs.\nsections:\n - name: setup\n title: Setup\n"` then a `[]struct{ name, raw, want string }` table checked with `strings.Contains(err.Error(), tc.want)`. Add rows `valid + "site_url: javascript:alert(1)\n"` etc., and a positive case. Header present/absent assertions go in `theme_test.go` next to existing render tests.
|
||||
|
||||
---
|
||||
|
||||
### `PLUG/plugin.go` (plugin, request-response)
|
||||
|
||||
**Analog:** `FW/examples/hello/plugins/base/plugin.go` (lines 1-54) for the skeleton, `greeter/plugin.go:59-76` for `Routes`.
|
||||
|
||||
Imports + interface assertions + embed (base/plugin.go:1-25):
|
||||
```go
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
)
|
||||
|
||||
var (
|
||||
_ pact.HasConfig = (*Plugin)(nil)
|
||||
...
|
||||
)
|
||||
|
||||
//go:embed config
|
||||
var configFS embed.FS
|
||||
```
|
||||
Use `var _ pact.HasRoutes = (*Plugin)(nil)` only. Do NOT add `pact.HasAdminControllers` (activates cabana, needs `admin.jwt.secret`). Embed must be `//go:embed all:public` (boardwalk.go:24 uses `//go:embed all:dist` for the same reason).
|
||||
|
||||
Lifecycle methods (base/plugin.go:30-34, 52-54):
|
||||
```go
|
||||
func (p *Plugin) ID() string { return "golem15.hello" }
|
||||
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 init() {
|
||||
party.Register(&Plugin{})
|
||||
}
|
||||
```
|
||||
ID becomes `"golem15.summercms"` (must be a string literal: `summer plugin:add` reads it from source).
|
||||
|
||||
Routes (greeter/plugin.go:59-68) — plain `r.Get(path, func(w, req))`; for the site wrap in `r.GroupRaw("", nil, func(g pact.Router){ g.Get("/docs", ...); g.Get("/docs/{path...}", ...); g.Get("/", ...) })` per RESEARCH Pattern 2. Return the handler-construction error from `Routes` (fail closed when `public/site/index.html` is missing), as boardwalk's `newHandler` does:
|
||||
```go
|
||||
raw, err := fs.ReadFile(root, "index.html")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("boardwalk: dist/index.html: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
### `PLUG/static.go` (handler, file-I/O)
|
||||
|
||||
**Analogs:** `FW/modules/boardwalk/boardwalk.go` (embed.FS + ServeContent + content-type table + cache by prefix) and `FW/internal/docsite/serve.go:236-290` (public static: dot-segment refusal, dir index.html, 404.html with 404 status). The docsite handler is the closer semantic match (public site, real 404s); boardwalk gives the embed/cache mechanics.
|
||||
|
||||
Content-type table + fallback (boardwalk.go:34-43, 153-162) — copy and extend with `.md`, `.webmanifest`, `.txt`, `.xml`/`.xsl`:
|
||||
```go
|
||||
var contentTypes = map[string]string{
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".mjs": "text/javascript; charset=utf-8",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".html": "text/html; charset=utf-8",
|
||||
".woff2": "font/woff2",
|
||||
".woff": "font/woff",
|
||||
".svg": "image/svg+xml",
|
||||
".json": "application/json",
|
||||
}
|
||||
|
||||
func ContentType(name string) string {
|
||||
ext := strings.ToLower(path.Ext(name))
|
||||
if ct, ok := contentTypes[ext]; ok {
|
||||
return ct
|
||||
}
|
||||
if ct := mime.TypeByExtension(ext); ct != "" {
|
||||
return ct
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
```
|
||||
Sub-tree (boardwalk.go:46-48): `fs.Sub(distFS, "dist")` → `fs.Sub(publicFS, "public/site")` and `"public/docs"`.
|
||||
|
||||
Serve-file + cache (boardwalk.go:135-148) — replace the single `assets/` prefix with RESEARCH's cache table (`_nuxt/` except `_nuxt/builds/`, and `_fonts/` immutable; everything else `no-cache`), and preset an `ETag` header before `ServeContent`:
|
||||
```go
|
||||
w.Header().Set("Content-Type", ContentType(name))
|
||||
if strings.HasPrefix(name, "assets/") {
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||
} else {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
}
|
||||
http.ServeContent(w, r, path.Base(name), time.Time{}, bytes.NewReader(body))
|
||||
```
|
||||
Path cleaning + dot refusal + dir index + 404 page (serve.go:250-283):
|
||||
```go
|
||||
clean := path.Clean("/" + r.URL.Path)
|
||||
for _, seg := range strings.Split(clean, "/") {
|
||||
if strings.HasPrefix(seg, ".") {
|
||||
notFound(w, root)
|
||||
return
|
||||
}
|
||||
}
|
||||
...
|
||||
if err == nil && info.IsDir() {
|
||||
name = filepath.Join(name, "index.html")
|
||||
...
|
||||
func notFound(w http.ResponseWriter, root string) {
|
||||
body, err := os.ReadFile(filepath.Join(root, "404.html"))
|
||||
if err != nil {
|
||||
http.NotFound(w, nil)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = io.Copy(w, bytes.NewReader(body))
|
||||
}
|
||||
```
|
||||
Port to `fs.Stat`/`fs.ReadFile` on the embedded `fs.FS`. Add the D-47 step: for docs, if `<p>` is not found and `<p>.html` is a regular file, `http.Redirect(w, r, "/docs/"+strings.TrimPrefix(clean,"/")+".html", http.StatusMovedPermanently)` built only from the cleaned path. Method check is already done by ServeMux (GET/HEAD patterns), so serve.go's 405 block is not needed.
|
||||
|
||||
**Do NOT copy:** `SetSecurityHeaders` (boardwalk.go:167-173: noindex, CSP `script-src 'self'`, DENY), `RewriteIndex`, the SPA fallback (boardwalk.go:118-126), `serveIndex`'s `no-store`. Optional headers allowed: `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`.
|
||||
|
||||
### `PLUG/static_test.go` (test)
|
||||
|
||||
**Analog:** `FW/modules/boardwalk/boardwalk_test.go:1-44`:
|
||||
```go
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
func get(h http.Handler, target string) *httptest.ResponseRecorder {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, target, nil))
|
||||
return rec
|
||||
}
|
||||
```
|
||||
Build fixtures with `fstest.MapFS` and call the unexported constructor (boardwalk splits `Handler` / `newHandler(root fs.FS, ...)` exactly for this — copy that split). Cases: RESEARCH Validation table row SC2.
|
||||
|
||||
### `PLUG/routes_test.go` (test)
|
||||
|
||||
**Analog:** `FW/modules/surf/example_test.go:114-140`:
|
||||
```go
|
||||
app := backpack.New(nil)
|
||||
plugin := &BlogPlugin{}
|
||||
if err := plugin.Register(app); err != nil { ... }
|
||||
h, err := surf.Assemble(app, []party.Plugin{plugin})
|
||||
...
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
```
|
||||
`backpack.New(nil)` (nil config) skips the body_limits requirement. Add a fake plugin implementing admin controllers to prove no conflict; assert `/backend` hits the site catch-all when alone, `/docs` → 301, `POST /` → 405.
|
||||
|
||||
### `PLUG/links_test.go` (integration on built tree, gated `SUMMERCMS_REQUIRE_BUILD=1`)
|
||||
|
||||
Reuse boardwalk_test.go's href extractor idea (line 17): `assetRef = regexp.MustCompile(`(?:src|href)="([^"]+)"`)`; resolve each internal href through the real handler (follow one 301, require 200) and each `#id` against `id="..."` in index.html. Also the drift guard: each command in `SITE/app/data/terminal.json` appears in `public/site/index.html`.
|
||||
|
||||
### `PLUG/go.mod`
|
||||
|
||||
**Analog:** `FON/plugins/golem15/user/go.mod`: `module ...`, `go 1.27.0`, `require git.golem15.com/golem15/summercms v0.0.0` (v0.1.0 after the tag), plus `replace git.golem15.com/golem15/summercms => ../../../../summercms.go` (same depth as fonoteka). No third-party requires (stdlib only).
|
||||
|
||||
---
|
||||
|
||||
### `APP/go.work`, `APP/go.mod`, `APP/summer.yaml`, `APP/.gitignore`
|
||||
|
||||
**Analog:** `FON/go.work`:
|
||||
```
|
||||
go 1.27.0
|
||||
|
||||
toolchain go1.27.0
|
||||
|
||||
use (
|
||||
.
|
||||
./plugins/golem15/user
|
||||
./plugins/golem15/fonoteka
|
||||
)
|
||||
```
|
||||
→ `use ( . ./plugins/golem15/summercms )`. Does not `use` the framework.
|
||||
|
||||
`FON/go.mod` header:
|
||||
```
|
||||
module git.golem15.com/golem15/fonoteka
|
||||
|
||||
go 1.27.0
|
||||
toolchain go1.27.0
|
||||
|
||||
replace git.golem15.com/golem15/summercms => ../summercms.go
|
||||
|
||||
require (
|
||||
git.golem15.com/golem15/fonoteka/plugins/golem15/fonoteka v0.0.0
|
||||
...
|
||||
git.golem15.com/golem15/summercms v0.0.0
|
||||
...
|
||||
replace git.golem15.com/golem15/fonoteka/plugins/golem15/user => ./plugins/golem15/user
|
||||
```
|
||||
→ module `git.golem15.com/golem15/sm-summercms-app`, require `summercms v0.1.0` (D-42) and `sm-summercms-plugin v0.0.0` with `replace ... => ./plugins/golem15/summercms`. Prefer generating these with `summer plugin:add plugins/golem15/summercms`.
|
||||
|
||||
`FON/summer.yaml`:
|
||||
```yaml
|
||||
module: git.golem15.com/golem15/fonoteka
|
||||
binary: fonoteka
|
||||
plugins:
|
||||
- id: golem15.user
|
||||
module: git.golem15.com/golem15/fonoteka/plugins/golem15/user
|
||||
```
|
||||
→ `binary: summercms-io`, one plugin `golem15.summercms` / `git.golem15.com/golem15/sm-summercms-plugin`.
|
||||
|
||||
`FON/.gitignore`: `/bin/`, `/tmp/`, `*.exe`, `go.work.sum` → add `.env`, `/storage/`.
|
||||
|
||||
`main.go` / `plugins.gen.go`: generated by `summer build`; commit them as fonoteka does, never hand-edit.
|
||||
|
||||
### `APP/config/*.yaml`
|
||||
|
||||
Analogs from `FON/config/` (copy shape, change values):
|
||||
- `app.yaml`: `name`, `debug: false`, `locale: en`, `fallback_locale: en`, `key: ""` with the comment "Set SUMMER_APP__KEY to a 32-byte base64 value (summer key:generate)".
|
||||
- `http.yaml`: keep only `body_limits: {default_bytes: 1048576, upload_bytes: 1048576}` (numeric, YAML-only) and `trusted_proxies: []` (consider `127.0.0.1/32` for nginx); drop fonoteka's CORS block.
|
||||
- `storage.yaml`: `uploads.bucket_url: "file://./storage/app/uploads"` (empty fails boot).
|
||||
- `queue.yaml`: `work_in_serve: false` (fonoteka has `true`; the site has no jobs).
|
||||
- `database.yaml`: `dsn: ""` (env `SUMMER_DATABASE__DSN`).
|
||||
|
||||
### `APP/scripts/build.sh` (script, batch)
|
||||
|
||||
**Analog skeleton:** `FON/scripts/check-openapi.sh:1-9`:
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# <purpose>
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
```
|
||||
plus explicit `if [[ ! -f ... ]]; then echo "build: ... " >&2; exit 1; fi` guards between steps. Body: RESEARCH §Code Examples "Build script outline" (nuxt generate → rsync `.output/public/` → `public/site/`; `git archive $TAG` → `docs:build --base-url /docs --site-url / --site-label summercms.io --out $PLUG/public/docs`; plugin tests with `SUMMERCMS_REQUIRE_BUILD=1`; `summer build` then `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build`). Never rsync the `dist` symlink.
|
||||
|
||||
### `APP/terminal_check_test.go` (e2e, gated `SUMMERCMS_TERMINAL_CHECK=1`)
|
||||
|
||||
Partial analog `FW/examples/hello/hello_test.go` (exercises the hello binary). Use `os/exec` with `bash -euo pipefail -c`, temp `GOBIN` first on PATH, `GOWORK` unset, stdin `/dev/null`, optional `SUMMERCMS_CLONE_URL` substitution; read commands from `vue-summercms-app/app/data/terminal.json` (RESEARCH Pattern 3).
|
||||
|
||||
---
|
||||
|
||||
### `SITE/nuxt.config.ts`, `package.json`, `pnpm-workspace.yaml`, `i18n/locales/en.json`
|
||||
|
||||
**Analog:** `VFON/nuxt.config.ts` — take only the module trio and their config shapes; drop Tailwind, pinia, shadcn, PWA, runtimeConfig proxies.
|
||||
|
||||
Modules + fonts (VFON lines ~64-87):
|
||||
```ts
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: false },
|
||||
modules: [ ..., '@nuxt/fonts', '@nuxtjs/i18n', '@nuxtjs/seo', ... ],
|
||||
fonts: {
|
||||
processCSSVariables: true,
|
||||
families: [
|
||||
{ name: 'IBM Plex Mono', weights: [400, 500], global: true },
|
||||
],
|
||||
},
|
||||
```
|
||||
i18n (VFON lines ~182-203): `strategy: 'prefix_except_default'`, `locales: [{ code: 'en', language: 'en-US', file: 'en.json', name: 'English' }]`, `langDir: 'locales/'`, `baseUrl`, `experimental: { prerenderMessages: true }`. Drop `detectBrowserLanguage` (single locale).
|
||||
site (VFON lines ~220-225): `site: { url, name, description, defaultLocale }`.
|
||||
Then add the spike-verified extras from RESEARCH Pattern 1: `styles: ['normal'], subsets: ['latin','latin-ext']`, `ogImage: { enabled: false }`, `sitemap: { autoI18n: false }`, `linkChecker.excludeLinks`, `nitro.prerender.ignore: ['/docs']`.
|
||||
|
||||
`package.json` scripts (VFON lines 5-15): keep `build`, `dev`, `generate`, `preview`, `prepare`, `typecheck`; add `"test": "node --test tests/*.test.ts"`. Pin versions per RESEARCH Standard Stack.
|
||||
|
||||
`pnpm-workspace.yaml` (VFON): copy `allowBuilds` minus `protobufjs` (and `sharp`, unused); do not copy `patchedDependencies` unless `pnpm dev` crashes.
|
||||
|
||||
`i18n/locales/en.json`: same location as `VFON/i18n/locales/en.json`.
|
||||
|
||||
## Shared Patterns
|
||||
|
||||
### Plugin skeleton (compiled, init-registered)
|
||||
**Source:** `FW/examples/hello/plugins/base/plugin.go:27-54`
|
||||
**Apply to:** `PLUG/plugin.go`. Interface compile-time assertions block, `ID/Requires/Register/Boot`, `func init() { party.Register(&Plugin{}) }`.
|
||||
|
||||
### Static serving with embed.FS
|
||||
**Source:** `FW/modules/boardwalk/boardwalk.go:24-48, 135-162` (mechanics) + `FW/internal/docsite/serve.go:236-290` (public semantics).
|
||||
**Apply to:** `PLUG/static.go`. `all:` embed, `fs.Sub`, own MIME table first, `http.ServeContent`, cache by path prefix, dot-segment refusal, 404.html with status 404.
|
||||
|
||||
### Error message style
|
||||
**Source:** boardwalk.go:63-71, load.go `ParseSite`.
|
||||
**Apply to:** all new Go code. `fmt.Errorf("<pkg>: <what>: %w", err)` with a lowercase package prefix (`summercms: public/site/index.html missing; run scripts/build.sh`).
|
||||
|
||||
### Tests
|
||||
**Source:** `boardwalk_test.go` (httptest + fstest.MapFS, `get` helper), `load_test.go` (table of `{name, raw, want}` with `strings.Contains`).
|
||||
**Apply to:** plugin and framework tests. Plain `func TestX(t *testing.T)`, stdlib only; env-gated integration tests (`SUMMERCMS_REQUIRE_BUILD`, `SUMMERCMS_TERMINAL_CHECK`, `SUMMERCMS_CHECK_EXTERNAL`) call `t.Skip` when unset.
|
||||
|
||||
### App wiring
|
||||
**Source:** `FON/go.work`, `FON/go.mod`, `FON/summer.yaml`, `FON/config/*.yaml`.
|
||||
**Apply to:** all `APP/` config files. Local replace to `../summercms.go`, plugin replace to `./plugins/...`, secrets empty in YAML and supplied by `.env` on the server.
|
||||
|
||||
## No Analog Found
|
||||
|
||||
| File | Role | Data Flow | Reason |
|
||||
|---|---|---|---|
|
||||
| `SITE/app/components/*.vue`, `app/assets/css/{tokens,base}.css` | component/style | event-driven | VFON uses Tailwind/shadcn (forbidden by D-35); follow `design/README.md` + `SummerCMS Landing.dc.html` and RESEARCH Design Handoff Inventory |
|
||||
| `SITE/app/utils/{scrollSpy,terminal}.ts`, `app/data/terminal.json`, `tests/*.test.ts` | utility/test | transform | No `node:test` usage anywhere; use RESEARCH Pattern 3 and Validation table |
|
||||
| `SITE/scripts/derive-images.sh` | script | batch | ImageMagick one-off; no precedent |
|
||||
| `APP/DEPLOY.md`, `APP/deploy/nginx-summercms.io.conf`, `APP/deploy/supervisor-summercms-io.conf`, `APP/scripts/smoke.sh` | docs/config | - | No nginx/supervisor configs in any repo; use RESEARCH §Deploy items 1-9 |
|
||||
| `PLUG/public/README.md` | placeholder | - | Required so `//go:embed all:public` matches before the first build; never place anything inside `public/docs/` (docs:build refuses to clean it without `.summer-docs`) |
|
||||
|
||||
## Metadata
|
||||
|
||||
**Analog search scope:** `summercms.go/{modules/boardwalk,modules/surf,internal/docsite,cmd/summer,examples/hello,docs/console}`, `fonoteka.go/{go.work,go.mod,summer.yaml,.gitignore,config,scripts,plugins/golem15/user/go.mod}`, `vue-fonoteka-app/{nuxt.config.ts,package.json,pnpm-workspace.yaml,i18n/locales}`
|
||||
**Files scanned:** ~25
|
||||
**Pattern extraction date:** 2026-10-01
|
||||
Reference in New Issue
Block a user