From 6d36ffaef0427a03ca5934d9f6332ea4128a443e Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 9 Oct 2026 11:44:39 +0200 Subject: [PATCH] docs(16): add pattern map --- .../16-PATTERNS.md | 278 ++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 .planning/phases/16-grzybyfunkcjonalne-pl-on-reusable-blog-views/16-PATTERNS.md diff --git a/.planning/phases/16-grzybyfunkcjonalne-pl-on-reusable-blog-views/16-PATTERNS.md b/.planning/phases/16-grzybyfunkcjonalne-pl-on-reusable-blog-views/16-PATTERNS.md new file mode 100644 index 0000000..9e32892 --- /dev/null +++ b/.planning/phases/16-grzybyfunkcjonalne-pl-on-reusable-blog-views/16-PATTERNS.md @@ -0,0 +1,278 @@ +# Phase 16: grzybyfunkcjonalne.pl on reusable blog views - Pattern Map + +**Mapped:** 2026-10-09 +**Files analyzed:** 26 (grouped where they share an analog) +**Analogs found:** 21 / 26 + +Paths are relative to `/media/nvme/dev/golem15/summercms.io/summercms/`. All analogs are git-tracked source (no `.msd` mirrors). D-20 (author endpoint) and D-21 (category content field) are pre-execution quick tasks in `sm-journal-plugin` and are **not** mapped here; only their consumption by the layer is. + +## File Classification + +| New/Modified File | Plan | Role | Data Flow | Closest Analog | Match | +|---|---|---|---|---|---| +| `vue-journal-layer/package.json` | 1 | config | build | `sm-summercmsio-app/vue-summercmsio-app/package.json` | exact (move nuxt/vue to devDeps) | +| `vue-journal-layer/nuxt.config.ts` | 1 | config | build | `vue-summercmsio-app/nuxt.config.ts` | role-match (layer: absolute paths) | +| `vue-journal-layer/modules/journal.ts` | 1 | module | build-time route registration | none in repo; RESEARCH Pattern 1 | no analog | +| `vue-journal-layer/app/composables/useJournalApi.ts`, `app/utils/client.ts`, `types.ts`, `query.ts` | 1 | service/client | request-response | none (summercms.io has no data fetch); RESEARCH Pattern 2 | no analog | +| `vue-journal-layer/app/utils/{pagination,htmlGate,routes}.ts` | 1 | utility | transform | `vue-summercmsio-app/app/utils/scrollSpy.ts` | role-match (pure TS, node --test) | +| `vue-journal-layer/app/journal-pages/*.vue` (PostIndex, Post, Category, Tag, Author) | 1 | page | request-response (SSR) | `vue-summercmsio-app/app/pages/index.vue` | partial | +| `vue-journal-layer/app/components/Journal/*.vue` | 1 | component | render | `vue-summercmsio-app/app/components/*` | role-match | +| `vue-journal-layer/app/assets/css/journal-tokens.css`, `journal-base.css` | 1 | config (style) | — | `vue-summercmsio-app/app/assets/css/tokens.css`, `base.css` | exact | +| `vue-journal-layer/i18n/locales/{pl,en}.json` | 1 | config (i18n) | — | `vue-summercmsio-app/i18n/locales/en.json` | exact | +| `vue-journal-layer/server/api/__sitemap__/journal.ts` | 1 | server route | request-response | none; RESEARCH Pattern 5 | no analog | +| `vue-journal-layer/README.md` | 1 | docs | — | `vue-summercmsio-app/README.md` (neutral names: `blog`/`acme`) | role-match | +| `vue-grzybyfunkcjonalne-app/package.json` | 2 | config | build | `vue-summercmsio-app/package.json` | exact (+ git dep) | +| `vue-grzybyfunkcjonalne-app/nuxt.config.ts` | 2 | config | build | `vue-summercmsio-app/nuxt.config.ts` | exact (SSR instead of generate) | +| `vue-grzybyfunkcjonalne-app/app/pages/{index,kontakt,trwaja-prace}.vue`, `app/error.vue` | 2 | page | render | `vue-summercmsio-app/app/pages/index.vue` | exact | +| `vue-grzybyfunkcjonalne-app/app/components/*` (Hero, About, Cta, Services, Faq, SiteHeader, SiteFooter, Journal overrides) | 2 | component | render | `vue-summercmsio-app/app/components/*` | exact | +| `vue-grzybyfunkcjonalne-app/app/data/{menu.ts,faq.pl.json}` | 2 | config/data | — | `vue-summercmsio-app/app/data/terminal.json` | role-match | +| `vue-grzybyfunkcjonalne-app/app/assets/css/tokens.css` | 2 | config (style) | — | `vue-summercmsio-app/app/assets/css/tokens.css` | exact | +| `vue-grzybyfunkcjonalne-app/i18n/locales/pl.json` | 2 | config (i18n) | — | `vue-summercmsio-app/i18n/locales/en.json` | exact | +| `sm-grzybyfunkcjonalne-app/.gitmodules` (add submodule) | 2/3 | config | — | `sm-summercmsio-app/.gitmodules` | exact | +| `sm-grzybyfunkcjonalne-app/config/backend.yaml` | 3 | config | — | itself (currently `uri: /backend`) | exact | +| app-local plugin `sm-grzybyfunkcjonalne-app/plugins/golem15/grzyby/plugin.go` (hosts the command) + `summer.yaml` entry | 3 | plugin | — | `sm-journal-plugin/plugin.go:60-65` (`Commands()`), `sm-summercmsio-app/plugins/golem15/summercms/plugin.go` | role-match | +| `.../console/import_featured_images.go` (D-22) | 3 | command | file-I/O + CRUD | `sm-journal-plugin/console/import_posts.go` | exact | +| `sm-grzybyfunkcjonalne-app/deploy/nginx/*.conf` | 3 | config | request routing | `sm-summercmsio-app/deploy/nginx/summercms.io.conf` | role-match (two upstreams) | +| `sm-grzybyfunkcjonalne-app/deploy/supervisor/*.conf` (Go + Nuxt) | 3 | config | process | `sm-summercmsio-app/deploy/supervisor/summercms-io.conf` | exact (x2) | +| `sm-grzybyfunkcjonalne-app/deploy/env.example` | 3 | config | — | `sm-summercmsio-app/deploy/env.example` | exact (+ `SUMMER_URL`, `NUXT_*`) | +| `sm-grzybyfunkcjonalne-app/scripts/{build.sh,smoke.sh,check-deploy.sh,urls.txt,migrate.sh}` | 3/4 | script | batch | `sm-summercmsio-app/scripts/{build.sh,smoke.sh,check-deploy.sh}` | role-match | +| Tests: layer/app `tests/*.test.ts`, Go `console/*_test.go` | 5 | test | — | `vue-summercmsio-app/tests/scrollSpy.test.ts`, `sm-grzybyfunkcjonalne-app/boot_test.go` | exact | + +## Pattern Assignments + +### `vue-journal-layer/package.json` and `vue-grzybyfunkcjonalne-app/package.json` + +**Analog:** `sm-summercmsio-app/vue-summercmsio-app/package.json` (whole file) +```json +{ + "name": "vue-summercmsio-app", + "private": true, + "type": "module", + "scripts": { + "build": "nuxt build", "dev": "nuxt dev", "generate": "nuxt generate", + "preview": "nuxt preview", "prepare": "nuxt prepare", + "test": "node --test --test-reporter=tap tests/*.test.ts" + }, + "engines": { "node": ">=22.18" }, + "dependencies": { + "@nuxt/fonts": "0.14.0", "@nuxtjs/i18n": "10.4.0", "@nuxtjs/seo": "5.2.1", + "nuxt": "4.4.8", "vue": "3.5.35" + }, + "devDependencies": { "typescript": "5.9.3" } +} +``` +Adapt: layer keeps the three modules in `dependencies`, moves `nuxt`/`vue` to `devDependencies` (RESEARCH Installation). App adds `"vue-journal-layer": "git+https://git.golem15.com/golem15/vue-journal-layer.git#v0.1.0"` and does not re-list modules at other versions (Pitfall 4). Drop `generate` from the app (SSR, D-07). Copy `.nvmrc` (22.22.0), `pnpm-workspace.yaml`, `tsconfig.json` from the same directory. + +### `nuxt.config.ts` (layer and app) + +**Analog:** `vue-summercmsio-app/nuxt.config.ts` (lines 1-66) +```ts +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 } ] }, + i18n: { + strategy: 'prefix_except_default', + defaultLocale: 'en', + locales: [{ code: 'en', language: 'en-US', file: 'en.json', name: 'English' }], + langDir: 'locales/', + baseUrl: 'https://summercms.io', + }, + site: { url: 'https://summercms.io', name: 'SummerCMS', description: '...', defaultLocale: 'en' }, + ogImage: { enabled: false }, + sitemap: { autoI18n: false }, +}) +``` +- **Layer:** keep `modules`, replace `~/` css paths with `fileURLToPath(new URL('./app/assets/css/journal-tokens.css', import.meta.url))` (anti-pattern: relative/`~` paths resolve to the consumer); add `resolve('./modules/journal')` to modules; locales pl+en; `sitemap: { sources: ['/api/__sitemap__/journal'] }`; declare `runtimeConfig.journal.apiBase` and `runtimeConfig.public.journal.apiBase` (`/_journal/api/v1`). +- **App:** `extends: ['vue-journal-layer']`, `journal.routes` (RESEARCH Pattern 1 app excerpt: `category: '/:category'`, `author: '/autor/:author'`, `tag: false`), `defaultLocale: 'pl'` with pl only, `site.url` grzyby, `routeRules: { '/**': { swr: 300 } }`, `runtimeConfig.public.gaId` empty (D-23 `NUXT_PUBLIC_GA_ID`). Fonts: Open Sans/Jost/Poppins family list in the same shape. Remove the `nitro.prerender` / `/docs` link-checker blocks; add `linkChecker.excludeLinks` for `/gfadmin`, `/_journal/**`, `/storage/**`. +- Comment style: short explanatory comments above each block, as the analog does. + +### `vue-journal-layer/modules/journal.ts`, `app/composables/useJournalApi.ts`, `server/api/__sitemap__/journal.ts` + +**No codebase analog.** Use RESEARCH.md Pattern 1 (`defineNuxtModule` + `extendPages`, route names `journal-`, `path === false` skips), Pattern 2 (server `apiBase` + forwarded `x-forwarded-for`), Pattern 3 (category resolves against `GET /categories?include_empty=1`, 404 via `createError({ statusCode: 404, fatal: true })`, D-24; render D-21 category `content` above the list), Pattern 5 (sitemap source paging `per_page=30` until `meta.last_page`). Author page consumes the D-20 endpoint; plan 1 checkpoints if absent. Put the pure logic (query building, page parsing, route-path validation, html gate, sitemap loc building from `journal.routes`) in `app/utils/*.ts` so it is `node --test`-able like `scrollSpy.ts`. + +### `app/utils/*.ts` (pure TS, both repos) + +**Analog:** `vue-summercmsio-app/app/utils/scrollSpy.ts` paired with `tests/scrollSpy.test.ts`. Exported constants + pure functions, no Nuxt imports, so tests import with an explicit `.ts` extension: +```ts +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { activeSection, SPY_SECTIONS, SPY_THRESHOLD } from '../app/utils/scrollSpy.ts' + +test('no sections means no active link', () => { + assert.equal(activeSection([]), '') +}) +``` + +### CSS tokens (layer `journal-tokens.css`, app `tokens.css`) + +**Analog:** `vue-summercmsio-app/app/assets/css/tokens.css` (lines 1-20): single `:root { ... }` block, grouped by family with a trailing comment per variable naming its use: +```css +/* Design tokens from the summercms.io handoff. Component styles use these. */ +:root { + --navy-900: #18223a; /* terminal background */ + --yellow-500: #f5c55a; /* accent */ + --text: #e9edf3; /* body text */ +``` +Layer uses the `--journal-color-*`, `--journal-font-*`, `--journal-space-*`, `--journal-radius-*` prefix (D-03). App overrides on `:root:root` or `html` (RESEARCH Pattern 4), seeded with `#0D705D/#27AE60/#138C74/#1AAC8F/#444444`. + +### Pages and components (both repos) + +**Analog:** `vue-summercmsio-app/app/pages/index.vue` and `app/components/*` (SFCs consuming i18n keys from `i18n/locales/*.json`, tokens only, no hard-coded colors). Layer pages live in `app/journal-pages/` (not `app/pages/`, anti-pattern); layer components under `app/components/Journal/` so the app overrides by the same name. Links via `useLocalePath()` with route names. + +### i18n locale files + +**Analog:** `vue-summercmsio-app/i18n/locales/en.json`, plus the leaf-walk check in `tests/output.test.ts` (lines 34-45: `type Messages = { [key: string]: string | Messages }`, `loadMessages()`, `leaves(tree, prefix)`). Reuse that helper in plan 5 to assert pl/en key parity in the layer. + +### `sm-grzybyfunkcjonalne-app/.gitmodules` + +**Analog:** `sm-summercmsio-app/.gitmodules` lines 1-3: +``` +[submodule "vue-summercmsio-app"] + path = vue-summercmsio-app + url = git@git.golem15.com:golem15/vue-summercmsio-app.git +``` +Submodule rule: commit and push inside the submodule first, then bump the gitlink as a separate commit. + +### `sm-grzybyfunkcjonalne-app/config/backend.yaml` + +Current content: +```yaml +# Admin mount path. Override with SUMMER_BACKEND__URI. +uri: /backend +``` +Change to `uri: /gfadmin` (D-09). Also review `config/http.yaml` (trusted proxy `127.0.0.1/32` at lines 15-16) - keep as is. + +### App-local plugin + `import_featured_images.go` (D-22) + +**Command analog:** `sm-journal-plugin/console/import_posts.go` + +Imports (lines 3-21) - stdlib first, then framework modules, then gorm: +```go +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + + "git.golem15.com/golem15/summercms/modules/backpack" + "git.golem15.com/golem15/summercms/modules/bonfire" + "gorm.io/gorm" +) +``` +Command declaration (lines 38-63): a constructor returning `bonfire.Command` that delegates to a testable function taking `*gorm.DB`: +```go +func ImportPosts(app *backpack.App) bonfire.Command { + return bonfire.Command{ + Name: "journal:import-posts", + Description: "Import journal posts with translations from JSON files.", + Flags: []bonfire.Flag{ + {Name: "path", Description: "Import directory (default: journal-export)"}, + {Name: "force", Description: "Update existing posts (default: skip existing)", Bare: true}, + {Name: "dry-run", Description: "Show what would be imported without making changes", Bare: true}, + }, + Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { + path := flagOrDefault(in, "path", defaultExportPath) + ... + stats, err := ImportFromDir(ctx, mustDB(app), path, force, dry, out, nil) + ... + out.Info(fmt.Sprintf("Results: %d created, %d updated, %d skipped, %d failed", ...)) + return nil + }, + } +} +``` +Core loop + error handling (lines 66-120): nil-DB guard, `os.Stat` dir check, per-item failures counted into a stats struct with `out.Error(...)` and `continue`, fatal DB errors returned; whole run in one transaction (`work := func(tx *gorm.DB) error`). Copy this shape for `grzyby:import-featured-images --map map.json --source [--dry-run]` with `Created/Skipped/Failed` stats; idempotency = skip when an `attach.File` row with the same `disk_name` + `attachment_id` + `field` exists. Helpers `flagOrDefault`, `flagOn`, `mustDB` are unexported in the journal console package - re-declare locally. + +**Attachment API** (`summercms.go/modules/lagoon/attach`): +- `attach.BlobKey(diskName)` = `PartitionDirectory(diskName) + diskName` (thumb.go:54-69) - the blob key under bucket `file://./storage/app/uploads/public` (host `config/storage.yaml`). +- `attach.File` struct (file.go:26-45): set `DiskName, FileName, FileSize, ContentType, Field: "featured_images", AttachmentID: strconv(postID), AttachmentType: "Golem15\\Journal\\Models\\Post"`; `IsPublic` is `*bool` (nil = DEFAULT TRUE). +- Look up post id by host `slug` (which is the pl slug after D-25). + +**Plugin host analog:** `sm-journal-plugin/plugin.go:60-65`: +```go +func (p *Plugin) Commands() []bonfire.Command { + return []bonfire.Command{ + console.ExportPosts(p.app), + console.ImportPosts(p.app), + } +} +``` +Plugin skeleton: `sm-summercmsio-app/plugins/golem15/summercms/plugin.go` lines 9-40 and 76-78 (`ID()`, `Requires()`, `Register`, `Boot`, `func init() { party.Register(&Plugin{}) }`). Here implement `pact.HasCommands` instead of `HasRoutes`; store `app` in `Register`. Register in `summer.yaml` `plugins:` list (`id: golem15.grzyby`, module path) and regenerate `plugins.gen.go`/`main.go` with `summer build` (main.go is generated - do not hand-edit; it already appends `pact.HasCommands` commands). Whether the plugin lives as an in-repo directory (no separate module) is a planner decision; `go.work` must include it. + +### `deploy/supervisor/*.conf` + +**Analog:** `sm-summercmsio-app/deploy/supervisor/summercms-io.conf` (whole file): +```ini +[program:summercms-io] +command=/srv/summercms-io/bin/summercms-io serve --addr 127.0.0.1:8095 +directory=/srv/summercms-io +user=summercms +environment=SUMMER_ENV="production" +autostart=true +autorestart=true +startsecs=3 +stopsignal=TERM +stopwaitsecs=15 +redirect_stderr=true +stdout_logfile=/var/log/supervisor/summercms-io.log +stdout_logfile_maxbytes=10MB +stdout_logfile_backups=5 +``` +Duplicate for `[program:grzyby-nuxt]` with `command=node .output/server/index.mjs`, `environment=NITRO_HOST="127.0.0.1",NITRO_PORT="...",NUXT_JOURNAL_API_BASE="http://127.0.0.1:/_journal/api/v1"`. Keep the header comment style (path on server, why `directory=` matters). + +### `deploy/nginx/*.conf` + +**Analog:** `sm-summercmsio-app/deploy/nginx/summercms.io.conf` - copy the HTTP→HTTPS + ACME block, the ssl/HSTS/gzip lines, and the per-path `location ^~` style (analog uses `location ^~ /backend { return 404; }`). New: `location ^~ /gfadmin` and `^~ /_journal` → Go upstream, `= /rss.xml` → Go `/_journal/api/v1/rss`, `^~ /storage/app/uploads/public/` and `^~ /storage/app/media/` → `alias` disk with `nosniff`, `/` → Nuxt upstream; `proxy_set_header Host $host` and `X-Forwarded-For` (Pitfalls 5, 6). Staging `server_name`, not the apex (D-10). + +### `deploy/env.example` + +**Analog:** `sm-summercmsio-app/deploy/env.example` (whole file: header with owner/mode, one commented line per key, `SUMMER_DATABASE__DSN`, `SUMMER_APP__KEY`, `SUMMER_ADMIN__JWT__SECRET`, `SUMMER_GOLEM15__USER__JWT__SECRET`). Add `SUMMER_URL=https://` and the Nuxt keys (`NUXT_JOURNAL_API_BASE`, `NUXT_PUBLIC_GA_ID`), either in this file or a second `nuxt.env.example`. + +### `scripts/build.sh`, `scripts/smoke.sh`, `scripts/check-deploy.sh` + +**Analog:** `sm-summercmsio-app/scripts/*`. Conventions to copy: +- Header usage comment, `set -euo pipefail`, `ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"`, `die()`/`fail()` to stderr, tool presence loop (`for tool in pnpm go ...; do command -v ...`), submodule presence check (`[[ -f "$SITE/package.json" ]] || die "... run git submodule update --init"`), `SUMMERCMS_FRAMEWORK` override, `release|dev` mode (build.sh lines 1-50). +- smoke.sh (lines 1-60): postgres:15 via `docker run -d --rm -p 127.0.0.1::5432`, `pg_isready` loop, `trap cleanup EXIT` killing PID and container, env-overridable port. Extend: start Nuxt too, run migrate → set pl default (D-25 SQL `UPDATE golem15_translate_locales SET is_default = (code = 'pl')`) → import, then curl every line of `scripts/urls.txt` (71 sitemap URLs + static routes) for expected status. +- build.sh adapts `nuxt generate` + embed to `pnpm build` producing `.output/` shipped next to the binary. + +### Tests (plan 5) + +- Layer/app TS: `node --test` per `tests/scrollSpy.test.ts` pattern above. +- Go: plain `func TestX(t *testing.T)` per `sm-grzybyfunkcjonalne-app/boot_test.go` (lines 1-30: `compass.Load("config")`, `cfg.Set("admin.jwt.secret", ...)`, `backpack.New(cfg)`, `party.Activate(app, PluginIDs)`). Extend boot test to assert `/gfadmin` mount and the new command is in the catalog. Featured-image command tests use a temp source dir and an in-memory/`mem://` bucket (comment in `config/storage.yaml`: "mem:// in unit tests") and assert idempotency on second run. + +## Shared Patterns + +### Pinned toolchain +**Source:** `vue-summercmsio-app/package.json`, `.nvmrc`. **Apply to:** both Nuxt repos. Exact versions, no carets. + +### node --test with `.ts` imports +**Source:** `vue-summercmsio-app/tests/*.test.ts`. **Apply to:** all TS unit tests; logic lives in `app/utils/*.ts` with no Nuxt auto-imports. + +### Bonfire command + testable core +**Source:** `sm-journal-plugin/console/import_posts.go:38-120`. **Apply to:** any app-local Go command. + +### Deploy file headers +**Source:** `sm-summercmsio-app/deploy/*`. **Apply to:** all deploy files: first comment names the server path and the reason for non-obvious settings; secrets never committed. + +### Neutral README +**Apply to:** `vue-journal-layer/README.md` - never name grzybyfunkcjonalne; use `blog`/`acme`. + +## No Analog Found + +| File | Role | Data Flow | Reason | +|---|---|---|---| +| `vue-journal-layer/modules/journal.ts` | Nuxt module | build-time | No Nuxt module or layer exists in the workspace; use RESEARCH Pattern 1 | +| `useJournalApi.ts` / `client.ts` | client | request-response | summercms.io app is static and fetches nothing; RESEARCH Pattern 2 | +| `server/api/__sitemap__/journal.ts` | server route | request-response | No Nitro server routes exist; RESEARCH Pattern 5 | +| `app/utils/htmlGate.ts` | utility | transform | Port of Go `FormatHTML` gate regexes from `sm-journal-plugin/classes`; no TS precedent | +| `scripts/migrate.sh` / runbook (D-25 order) | script | batch | No migration runbook script precedent; build from smoke.sh conventions | + +## Metadata + +**Analog search scope:** `sm-summercmsio-app/` (vue app, deploy, scripts, site plugin), `sm-grzybyfunkcjonalne-app/`, `sm-journal-plugin/console` and `plugin.go`, `summercms.go/modules/lagoon/attach` +**Files scanned:** ~25 +**Pattern extraction date:** 2026-10-09