From dd6890e905b142bca7681585fd695ecb8c48418a Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 4 Nov 2026 10:40:50 +0100 Subject: [PATCH] docs(33-02): complete Journal headings, editor and content facts plan --- .planning/WINDOWS.md | 20 +- .../33-02-SUMMARY.md | 346 ++++++++++++++++++ .../deferred-items.md | 7 + 3 files changed, 370 insertions(+), 3 deletions(-) create mode 100644 .planning/phases/33-seo-output-audit-and-content/33-02-SUMMARY.md diff --git a/.planning/WINDOWS.md b/.planning/WINDOWS.md index 2ebfbf49..bd97338c 100644 --- a/.planning/WINDOWS.md +++ b/.planning/WINDOWS.md @@ -1,10 +1,10 @@ --- schema_version: 1 -open_count: 20 +open_count: 21 waived_count: 0 fixed_count: 7 -total_count: 27 -last_updated: 2026-10-22T14:44:56.679Z +total_count: 28 +last_updated: 2026-11-04T09:39:12.905Z --- # Broken Windows Ledger @@ -42,6 +42,7 @@ last_updated: 2026-10-22T14:44:56.679Z | 25 | 19.7 | deviation | modules/cabana | | 19.7-01: admin route inventories (phase09Routes, tokenRouteClasses, CSRF walk 35, phase10 want list, conformCases) extended for the two account contribution routes | open | | 2026-10-22T14:44:56.218Z | | | 26 | 19.7 | deviation | modules/cabana | | 19.7-01: TestRecordActionPromptRun added on Postgres to prove Run receives the trimmed prompt value | open | | 2026-10-22T14:44:56.449Z | | | 27 | 19.7 | unrun-verify | scripts/check-phase19.6.1.sh | | 19.7-01: check-phase19.6.1.sh --all stopped by the host (memory pressure) after the cabana and surf Postgres suites passed; re-run on the Docker host | open | | 2026-10-22T14:44:56.679Z | | +| 28 | 33 | deviation | modules/lagoon/validate.go | | 33-02: lagoon.Validate rejects the string and url rule tokens, so the Journal admin settings save answers 500 (pre-existing); the Headings and links switch is saved through the settings API/MCP until fixed | open | | 2026-11-04T09:39:12.905Z | | ````json [ @@ -395,6 +396,19 @@ last_updated: 2026-10-22T14:44:56.679Z "recorded_at": "2026-10-22T14:44:56.679Z", "resolved_at": null, "milestone": "v1.0" + }, + { + "id": 28, + "kind": "deviation", + "phase": "33", + "file": "modules/lagoon/validate.go", + "line": null, + "description": "33-02: lagoon.Validate rejects the string and url rule tokens, so the Journal admin settings save answers 500 (pre-existing); the Headings and links switch is saved through the settings API/MCP until fixed", + "status": "open", + "reason": "", + "recorded_at": "2026-11-04T09:39:12.905Z", + "resolved_at": null, + "milestone": "v1.0" } ] ```` diff --git a/.planning/phases/33-seo-output-audit-and-content/33-02-SUMMARY.md b/.planning/phases/33-seo-output-audit-and-content/33-02-SUMMARY.md new file mode 100644 index 00000000..2ad48ae8 --- /dev/null +++ b/.planning/phases/33-seo-output-audit-and-content/33-02-SUMMARY.md @@ -0,0 +1,346 @@ +--- +phase: 33-seo-output-audit-and-content +plan: 02 +subsystem: journal +tags: [sm-journal-plugin, headings, h1-setting, blog-editor, tiptap, content-descriptor, mcp, media-names, bluemonday] + +requires: + - phase: 33-seo-output-audit-and-content + provides: "33-01 seams: pact content facts (Kind, Tools, PublishedAt, Parents, Authors, TopHeading, Images, Videos, Headings) and pact.FormFieldType.RequestOptions" + - phase: 32-seo-foundation + provides: "Journal content descriptors, journal-tag, RecordSaved emission on API writes" +provides: + - "Journal H1 setting content_h1_to_h2 (settings row, config key golem15.journal.content_h1_to_h2 as fallback) with H1 to H2 conversion on every Journal rich-text write path (post content and category pillar text)" + - "content_h1_to_h2 flag on GET /posts/{slug} and every GET /categories row; notices key on API write answers" + - "journal:fix-headings and POST /_journal/api/v1/fix-headings over stored post and category content" + - "GET/PUT /_journal/api/v1/settings and POST /_journal/api/v1/images/alt" + - "MCP tools journal_get_settings, journal_update_settings, journal_fix_headings, journal_set_image_alt" + - "Descriptor facts for posts, categories and tags, and the journal-author content type (golem15.journal.author_pages)" + - "Blog Editor J1-J4 on RequestOptions (heading levels, outline panel, link rel, generic-anchor warning, image naming)" + - "HTML policy keeps rel=nofollow|sponsored|ugc on a; classes.MediaFileName slugs uploaded image names" +affects: [33-04, 33-06, 33-09, 33-10, 33-11, 33-13, 33-14] + +actuals: + tokens: 55440 + tasks: 4 + commits: 9 +plan_head_before: f7bfb3313b718e31cc754c87e4e0f9b0dcf95c0a +plan_head_after: 0edf6e1b67d4f7d8f62980e30d52292cce94faf3 + +tech-stack: + added: [] + patterns: + - "Byte-preserving HTML rewrites on the x/net/html tokenizer (ConvertH1 rewrites only the tag name; SetImageAlt rewrites only the matching img from its token)" + - "One setting reader (EffectiveH1ToH2): settings row, else config key, else true" + - "Per-request field options from settings (RequestOptions), read by the element from its options attribute" + - "A parse rule carried by a never-created node type (HeadingOneAsTwo) to map h1 to the heading node at level 2" + +key-files: + created: + - ../sm-journal-plugin/updates/202610280001_add_golem15_journal_settings_headings.go + - ../sm-journal-plugin/config/config.yaml + - ../sm-journal-plugin/classes/headings.go + - ../sm-journal-plugin/classes/heading_fix.go + - ../sm-journal-plugin/classes/journal_settings.go + - ../sm-journal-plugin/classes/image_alt.go + - ../sm-journal-plugin/classes/content_facts.go + - ../sm-journal-plugin/classes/media_names.go + - ../sm-journal-plugin/console/fix_headings.go + - ../sm-journal-plugin/controllers/api/settings.go + - ../sm-journal-plugin/controllers/api/maintenance.go + - ../sm-journal-plugin/assets/src/blogeditor/outline.ts + - ../sm-journal-plugin/assets/src/blogeditor/linkform.ts + - ../sm-journal-plugin/assets/src/blogeditor/imageform.ts + - ../sm-journal-plugin/headings_test.go + - ../sm-journal-plugin/journal_descriptor_fields_test.go + - ../sm-journal-plugin/classes/media_names_test.go + - ../sm-journal-plugin/tests/blogeditor/additions.test.ts + modified: + - ../sm-journal-plugin/models/settings.go + - ../sm-journal-plugin/models/settings/fields.yaml + - ../sm-journal-plugin/plugin.go + - ../sm-journal-plugin/routes.go + - ../sm-journal-plugin/mcp.go + - ../sm-journal-plugin/classes/blog_editor.go + - ../sm-journal-plugin/classes/format_html.go + - ../sm-journal-plugin/classes/media_store.go + - ../sm-journal-plugin/classes/content_descriptors.go + - ../sm-journal-plugin/classes/menu_item_types.go + - ../sm-journal-plugin/controllers/api/posts.go + - ../sm-journal-plugin/controllers/api/posts_helpers.go + - ../sm-journal-plugin/console/import_posts.go + - ../sm-journal-plugin/assets/src/blogeditor/element.ts + - ../sm-journal-plugin/assets/src/blogeditor/extensions.ts + - ../sm-journal-plugin/assets/src/blogeditor/toolbar.ts + - ../sm-journal-plugin/assets/src/blogeditor/labels.ts + - ../sm-journal-plugin/assets/src/blogeditor/blogeditor.css + - ../sm-journal-plugin/assets/dist/blogeditor.js + - ../sm-journal-plugin/assets/dist/blogeditor.css + - ../sm-journal-plugin/tests/blogeditor/fixtures/editor-corpus.json + - ../sm-journal-plugin/lang/en/lang.yaml + - ../sm-journal-plugin/lang/pl/lang.yaml + - ../sm-journal-plugin/README.md + +key-decisions: + - "Version mapping (Jakub's 33-01 ruling): the framework target is SummerCMS v0.6.1 (base v0.6.0); sm-journal-plugin is at v0.3.1, its next version is read from git tag in 33-14. go.mod still requires v0.6.0 with the existing replace to ../summercms.go; the require bump waits for the v0.6.1 tag" + - "P33-04 as planned: EffectiveH1ToH2 reads the settings row, then golem15.journal.content_h1_to_h2, then true" + - "API H1 conversion converts content and content_html together (default and translated); ConvertH1 is byte-preserving, so markdown content changes only where it holds raw

tags. An update that does not send content converts nothing" + - "API notices: one {message, count} per converted locale, with a locale key on a translation's notice (additive)" + - "POST /fix-headings defaults to dry_run true (mirrors the CLI); POST /images/alt defaults to dry_run false (an explicit per-change edit); set_image_alt also checks post.CanEdit per post (403)" + - "The stored-heading fix rewrites translate rows directly (attribute_data JSON, translate's own encoding) so a translation in a since-disabled locale is converted too and SetTranslated's enabled-locale check cannot abort the batch" + - "Editor schema keeps H5 and H6 (levels 2-6 while the setting is on, 1-6 while off); the toolbar offers only H2-H4 or H1-H4. A strict [2,3,4] schema would drop stored H5/H6 on the first edit" + - "Post Parents are the post's categories by name (the order :category takes its first category in)" + - "journal-author: Controller empty, Model Backend\\Models\\User, Published = has a published post, LastMod = newest update of their published posts" + - "MediaFileName applies to both the editor upload and POST /_journal/api/v1/media/upload (D-85 server backstop); the response shape is unchanged, the stored name is now slugged" + +patterns-established: + - "Content plugins answer their own fixing tools in ContentDescriptor.Tools (journal_update_post, journal_fix_headings, journal_set_image_alt)" + - "Editor options arrive per request through RequestOptions; the element parses them from its options attribute with defaults" + +requirements-completed: [SC-2, SC-3, D-50, D-51, D-81, D-83, D-85, D-96, D-43, D-93, D-10] + +coverage: + - id: D1 + description: "H1 setting (column, config key, Headings and links tab) with admin save conversion and the post-title notice; GET /posts/{slug} reports content_h1_to_h2; setting off stores H1 unchanged; switching never rewrites stored content" + requirement: "D-50" + verification: + - kind: integration + ref: "../sm-journal-plugin/headings_test.go#TestHeadingConversionTracer" + status: pass + - kind: unit + ref: "../sm-journal-plugin/classes/headings_test.go#TestConvertH1" + status: pass + - kind: integration + ref: "../sm-journal-plugin/updates/settings_headings_migration_test.go#TestSettingsHeadingsMigration" + status: pass + human_judgment: false + - id: D2 + description: "Conversion on the API (default and translated), markdown, import and category admin paths with notices; GET /categories rows carry content_h1_to_h2" + requirement: "D-96" + verification: + - kind: integration + ref: "../sm-journal-plugin/headings_test.go#TestHeadingConversion" + status: pass + human_judgment: false + - id: D3 + description: "journal:fix-headings (dry run, --apply, nothing to do, refusal exit 1) and POST /fix-headings over posts and categories with RecordSaved per changed record; settings API with manage_settings (401/403/422)" + requirement: "D-10" + verification: + - kind: integration + ref: "../sm-journal-plugin/headings_test.go#TestFixHeadingsCommand" + status: pass + - kind: integration + ref: "../sm-journal-plugin/headings_test.go#TestJournalSettingsTools" + status: pass + human_judgment: false + - id: D4 + description: "POST /images/alt and MCP journal_set_image_alt: tokenizer-escaped alt, all-or-nothing 422, dry run, decorative alt, translated content, access_posts; four new MCP tools listed and bound to routes" + requirement: "D-85" + verification: + - kind: integration + ref: "../sm-journal-plugin/headings_test.go#TestSetImageAlt" + status: pass + - kind: unit + ref: "../sm-journal-plugin/classes/image_alt_test.go#TestSetImageAlt" + status: pass + - kind: unit + ref: "../sm-journal-plugin/mcp_test.go#TestJournalMCP" + status: pass + - kind: unit + ref: "../sm-journal-plugin/mcp_smoke_test.go#TestJournalMCPSmoke" + status: pass + human_judgment: false + - id: D5 + description: "Descriptor facts for posts (PublishedAt, Parents, Authors, Images, Videos, Headings, TopHeading, Kind, Tools), categories (Parents, Headings, TopHeading, Kind, Tools) and tags (Kind)" + requirement: "D-93" + verification: + - kind: integration + ref: "../sm-journal-plugin/journal_descriptor_fields_test.go#TestJournalDescriptorFields" + status: pass + human_judgment: false + - id: D6 + description: "journal-author content type with author_pages on and off" + requirement: "D-43" + verification: + - kind: integration + ref: "../sm-journal-plugin/journal_descriptor_fields_test.go#TestJournalAuthorType" + status: pass + human_judgment: false + - id: D7 + description: "Stored HTML keeps rel with only nofollow/sponsored/ugc; MediaFileName slug rule on uploads; corpus pins a rel link and a decorative image" + requirement: "D-83" + verification: + - kind: unit + ref: "../sm-journal-plugin/classes/editor_policy_test.go#TestEditorOutputPassesPolicy" + status: pass + - kind: unit + ref: "../sm-journal-plugin/classes/media_names_test.go#TestMediaFileName" + status: pass + human_judgment: false + - id: D8 + description: "Blog Editor J1-J4 behaviour: heading levels per setting via RequestOptions, outline flags and counts with CLDR plurals, rel checkboxes and generic warning, slugged file name and decorative alt; dist matches a fresh build" + requirement: "D-51" + verification: + - kind: automated_ui + ref: "../sm-journal-plugin/tests/blogeditor/additions.test.ts" + status: pass + - kind: integration + ref: "../sm-journal-plugin/headings_test.go#TestBlogEditorRequestOptions" + status: pass + - kind: other + ref: "bash ../sm-journal-plugin/scripts/check-dist.sh" + status: pass + human_judgment: false + - id: D9 + description: "J2 outline look and wrapping in the real admin (long headings wrap with a hanging indent at 360 px, 12px indent per level, icon and colours in light and dark), J3/J4 form layout" + requirement: "D-81" + verification: [] + human_judgment: true + rationale: "Visual layout in the embedded admin SPA; happy-dom tests cover structure and behaviour, not rendering" + - id: D10 + description: "The H1 setting switch saved from Settings › Journal in the admin" + verification: [] + human_judgment: true + rationale: "Blocked by a pre-existing framework issue (lagoon.Validate rejects the string/url rule tokens; the Journal settings screen answers 500 on save, deferred-items.md); the API and MCP path is tested" + +duration: 60min +completed: 2026-11-04 +status: complete +--- + +# Phase 33 Plan 02: Journal headings, editor additions and content facts Summary + +**Per-site Journal H1 setting with byte-preserving H1→H2 conversion on every rich-text write path (admin, API/MCP, markdown, import, category pillar text), journal:fix-headings, the settings and bulk image-alt routes with four new MCP tools, descriptor facts plus a journal-author content type, and Blog Editor J1–J4 (heading levels, outline, link rel, slugged image names)** + +## Read first: version mapping and things to decide + +- **Versions (Jakub's 33-01 ruling).** Read "v0.5.1" in the plan as SummerCMS v0.6.0 and "v0.5.2" as v0.6.1. The Journal changes need v0.6.1 (the content facts and `RequestOptions` are new there). `sm-journal-plugin/go.mod` still requires `v0.6.0` and resolves the framework through its existing `replace => ../summercms.go`; I did not touch that replace. The require bump to v0.6.1 happens when 33-14 tags the framework. The Journal was at v0.3.1 before this plan, and its next version is read from `git tag` in 33-14. Nothing was pushed or tagged. +- **No breaking change to an existing Journal consumer.** Every API change adds something: `content_h1_to_h2` on `GET /posts/{slug}` and on the category rows, `notices` on write answers, and new routes and tools. Two behaviour changes need a line in the release notes: + 1. The H1 setting defaults to **on** (D-50, Jakub 2026-10-28). After the upgrade, new saves on a site turn H1 into H2 until someone switches the setting off. Stored content is never rewritten unless `journal:fix-headings --apply` runs. + 2. Uploaded image names are now slugged on the server (D-85). This covers `POST /_journal/api/v1/media/upload` too: `My Photo.JPG` is stored as `my-photo.jpg`. The response shape is the same, and clients use the returned URL. +- **Pre-existing framework issue (deferred, needs a decision).** Saving Settings › Journal in the admin answers **500**, and it did before this plan too. `lagoon.Validate` does not know the `string` and `url` rule tokens that `Settings.Rules()` has used for a long time. So until the framework is fixed, the new switch can only be changed through `PUT /_journal/api/v1/settings` or MCP `journal_update_settings`. Details are in `deferred-items.md`, and there is a WINDOWS.md entry. The fix belongs in lagoon (v0.6.1). + +## Performance + +- **Duration:** about 60 min +- **Started:** 2026-11-04T08:38:29Z +- **Completed:** 2026-11-04T09:38:48Z +- **Tasks:** 4 (9 commits in sm-journal-plugin) +- **Files modified:** 63 in sm-journal-plugin + +## Accomplishments + +- The Journal H1 setting (`content_h1_to_h2`, Headings and links tab, config fallback) converts H1 to H2 on every Journal rich-text write path and sends a notice. Post and category answers expose the setting. Stored content changes only through `journal:fix-headings --apply` or `journal_fix_headings`. +- Journal fixes its own content through MCP: `journal_get_settings`, `journal_update_settings`, `journal_fix_headings` and `journal_set_image_alt` (D-10, D-85, P33-05). +- Descriptors give the SEO, sitemap and audit plugins publication dates, parents, authors, images (with alt and the decorative flag), YouTube videos, the heading outline, the top heading level, the kind and the fixing tools. Author pages are a content type (`journal-author`). +- Blog Editor J1–J4: the heading levels follow the setting (per request through `RequestOptions`). There is a read-only outline that flags skipped, empty, link-only and Source-mode H1 headings and generic link texts. External links get rel checkboxes, a generic link text gets a non-blocking warning, and an image gets a slugged file name and a required alt (or is marked decorative) before upload. +- On the server, the HTML policy keeps `rel` when it holds only `nofollow`, `sponsored` and `ugc`, and `MediaFileName` applies the same slug rule as the editor. + +## Task Commits (sm-journal-plugin, branch main, not pushed) + +1. **Task 1: H1 setting with the admin save conversion (tracer)**: `305f94a` (feat) +2. **Task 2: other write paths, fix-headings, settings API, MCP tools**: `faaa9b0` (group A), `5210b64` (group B), `af25eaa` (group C) +3. **Task 3: descriptor facts and journal-author**: `e8e77a1` (group A), `805bfdb` (group B) +4. **Task 4: Blog Editor J1–J4, rel policy, media names, README**: `391435b` (group A), `4b09bd9` (groups B and C together), `0edf6e1` (group D, docs) + +**Plan metadata:** the SUMMARY commit in summercms.go (docs), then ROADMAP alone. + +## Files Created/Modified (sm-journal-plugin) + +- `updates/202610280001_add_golem15_journal_settings_headings.go`: `content_h1_to_h2 BOOLEAN NOT NULL DEFAULT TRUE`, `generic_anchors TEXT NULL`, with rollback. +- `config/config.yaml`, `plugin.go`: `ConfigFS` (`pact.HasConfig`) and the `journal:fix-headings` registration. +- `classes/journal_settings.go`: `EffectiveH1ToH2`, `GenericAnchors`, `DefaultGenericAnchors`, `H1ToH2Key`. +- `classes/headings.go`: `ConvertH1`, `CountH1`, `ConvertContentH1`, `ConvertTranslationH1`. +- `classes/heading_fix.go`: `FixStoredHeadings`, `HeadingFix` and the refusal text. They are shared by the command and the API. +- `classes/image_alt.go`: `SetImageAlt` on the tokenizer. +- `classes/content_facts.go`: the image, video and heading parse, category refs and the author projection. +- `classes/content_descriptors.go`, `classes/menu_item_types.go`: the facts, `AuthorPagesKey`, and the `journal-author` type (`MenuTypeAuthor`, `DefaultAuthorPattern`). +- `classes/blog_editor.go`: the Decode conversion with the post or category notice, and `RequestOptions` (`headingLevels`, `genericAnchors`). `safeUploadName` now uses `MediaFileName`. +- `classes/format_html.go`: the rel rule. `classes/media_names.go`: `MediaFileName`. `classes/media_store.go`: applies it. +- `controllers/api/posts.go`, `posts_helpers.go`: conversion inside the write transaction, the `notices` answer and the category flag. +- `controllers/api/settings.go`: `ShowSettings`, `UpdateSettings`. `controllers/api/maintenance.go`: `FixHeadings`, `SetImageAlt`. `routes.go`: four routes. +- `console/fix_headings.go`. `console/import_posts.go`: `ImportFromDirConfig`, `ImportStats.H1Converted`. `ImportFromDir` keeps its signature. +- `mcp.go`: four tools. +- `assets/src/blogeditor/{element,extensions,toolbar,labels,outline,linkform,imageform}.ts`, `blogeditor.css`, `assets/dist/*`: J1–J4. +- `lang/{en,pl}/lang.yaml`: the C27 keys with the P33-54 wording, `editor.h1_converted` and `editor.h1_converted_category` (CLDR plurals), and `menuitem.journal_author`. +- `README.md`: the setting and its precedence, the write paths, the command, the routes, the MCP tools, `journal-author`, the content facts, the editor additions and the rel and name rules. + +## Decisions Made + +See `key-decisions` in the frontmatter. The main ones: +- The API converts `content` and `content_html` together. +- A PUT without `content` converts nothing. +- `fix-headings` is a dry run by default. `images/alt` is not, because it is an explicit edit, and it checks edit rights per post. +- The editor schema keeps H5 and H6 so that stored headings survive an edit. +- Post parents are ordered by category name. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug, deferred root cause] Settings screen save fails in the admin (pre-existing)** +- **Found during:** Task 2 (settings API) +- **Issue:** `lagoon.Validate` rejects the `string` and `url` tokens in `Settings.Rules()`. The admin settings PUT answers 500, and my API handler hit the same error. +- **Fix:** `UpdateSettings` checks `string` and `url` itself (`settingsRule`) and passes the other tokens to `lagoon.Validate`. The admin screen is not fixed, because that needs a framework change; it is logged in `deferred-items.md` and WINDOWS.md. +- **Files modified:** controllers/api/settings.go +- **Verification:** TestJournalSettingsTools (422 for a bad URL and for a non-string value) +- **Committed in:** 5210b64 + +**2. [Rule 3 - Blocking] The category-content migration test assumed it was the last migration** +- **Found during:** Task 1 +- **Fix:** It now rolls back to the step before its own (`RollbackTo`). I added `TestSettingsHeadingsMigration` for the new step. +- **Committed in:** 305f94a + +**3. [Rule 1 - Data loss] The heading schema keeps H5 and H6** +- **Found during:** Task 4 +- **Issue:** TipTap levels of exactly `[2, 3, 4]` (UI-SPEC C28) would turn stored H5 and H6 into paragraphs on the first edit of their block. +- **Fix:** The schema levels are 2–6 while the setting is on and 1–6 while it is off. The toolbar offers H2–H4 or H1–H4. A dedicated parse rule (`HeadingOneAsTwo`) maps `

` to level 2 without importing `@tiptap/extension-heading`, so no package was added. +- **Committed in:** 4b09bd9 + +**4. [Rule 3 - Blocking] Existing tests pinned the behaviour this plan changes** +- Tool lists (TestJournalMCP, TestJournalMCPSmoke), menu type lists (TestMenuItemTypesPatterns, TestContentDescriptors), the category key list (categories_test.go), and the corpus test's "no rel" assertion. The upload-name expectations changed too: `zdj-cie-1.png` is now `zdjecie-1.png`, `ok-adka.png` is now `okladka.png`, and `b.JPEG` is now `b.jpeg`. The old upload-on-pick tests (smoke.test.ts, embed.test.ts) were rewritten to the J4 flow, which is pick, then the form, then Insert uploads, then the image is inserted on the result. I updated each test to the new behaviour; I did not remove any. +- **Committed in:** 5210b64, af25eaa, 805bfdb, 391435b, 4b09bd9 + +**5. [Plan shape] Task 4 groups B and C are one commit** +- **Issue:** J1–J4 share `element.ts`, `labels.ts` and the bundle. If B were committed without C, the import graph would not build. +- **Fix:** One commit, `feat: editor heading levels, outline, link rel, generic-anchor warning and image naming`. + +**6. [Plan shape] Helper and test files the plan did not list** +- `classes/heading_fix.go` holds the logic the command and the API share. The tests not in the plan are `classes/headings_test.go`, `classes/image_alt_test.go`, `updates/settings_headings_migration_test.go`, `tests/blogeditor/additions.test.ts` and `TestBlogEditorRequestOptions`. The slug helpers live in `imageform.ts`. I added `controllers.PermissionManageSettings` and `console.ImportFromDirConfig`; both are additive. +- For the acceptance greps, the C27 console copy appears in `console/fix_headings.go` (doc comment and long help). The runtime refusal string is `classes.H1ConversionOffText`. + +**7. [TDD note] RED evidence** +- Task 2 group A tests were written right after the code, so no RED was captured for them. For Task 2 groups B and C and for Task 3, I ran the tests against stubs first. Each failed on its target assertion: TestFixHeadingsCommand had dry-run output `""`, TestJournalSettingsTools got 404, TestSetImageAlt got 404, the TestJournalMCP tool list mismatched, and the descriptor kinds were empty and journal-author was nil. `check tdd-red-evidence` only reads TAP, so I converted the Go output to TAP, and it then returned RED_EVIDENCE_OK. + +**8. [Cosmetic] Attribute order of new editor links** +- TipTap renders `rel` before `href` (``). The policy and the layer do not depend on attribute order. Stored links keep their own bytes. + +--- + +**Total deviations:** 8 (1 deferred pre-existing bug, 1 blocking test fix, 1 data-loss guard, 1 test update set, 2 plan-shape, 1 TDD note, 1 cosmetic) +**Impact on plan:** Every must-have truth is met through the API, MCP, editor and descriptors. The one exception is the admin switch save, which a pre-existing framework issue blocks. + +## Issues Encountered + +- `go test ./... -run X` prints "no tests to run" for the subpackages. The plan's `fails_when` lists that phrase, but each named root test passes: the line is from the other packages. +- vue-journal-layer holds a copy of `editor-corpus.json`. Its policy mirror (htmlGate) has to accept `rel` with the three tokens and `alt=""`, which is 33-09's work. + +## User Setup Required + +None. No external service configuration is required. + +## Next Phase Readiness + +- 33-04 (image and News sitemaps), 33-06 (JSON-LD, breadcrumbs), 33-09 (layer title tag and htmlGate following `content_h1_to_h2`) and 33-10 (CMS checks through `Tools`, `TopHeading`, `Images`, `Headings`) can read what this plan provides. +- 33-14: bump the Journal `go.mod` to the v0.6.1 tag, tag the Journal from `git tag` (after v0.3.1), and include the release notes about the default-on setting and slugged upload names. +- Open: the framework fix for `lagoon.Validate` `string`/`url` (deferred-items.md). + +## Self-Check: PASSED + +- FOUND: the 18 created files listed in key-files.created (checked with `[ -f ]`). +- FOUND commits in sm-journal-plugin: 305f94a, faaa9b0, 5210b64, af25eaa, e8e77a1, 805bfdb, 391435b, 4b09bd9, 0edf6e1. `git rev-list --count f7bfb33..HEAD` = 9. +- Verification: `go vet ./...` and `go test -short ./...` are green. The full Docker `go test ./...` is green (root, classes, controllers, api, models, updates). `npm run test` passed 244 of 244. `check-dist.sh` matches. `check-phase33.sh --plugins` passed. + +--- +*Phase: 33-seo-output-audit-and-content* +*Completed: 2026-11-04* diff --git a/.planning/phases/33-seo-output-audit-and-content/deferred-items.md b/.planning/phases/33-seo-output-audit-and-content/deferred-items.md index 0a563fe6..e0c8c745 100644 --- a/.planning/phases/33-seo-output-audit-and-content/deferred-items.md +++ b/.planning/phases/33-seo-output-audit-and-content/deferred-items.md @@ -7,3 +7,10 @@ Out-of-scope discoveries logged during execution; not fixed in the plan that fou - **Path canonicals under swr become `http://localhost/...` (pre-existing since v0.1.1).** A route-cached (swr) render runs on a copy of the request without the Host header. `seoAbsolute(value, useRequestURL().origin)` then makes a canonical, og:url or og:image that the backend answers as a path (a backend without `golem15.seo.site_url` or `app.url`) absolute on `http://localhost`. Sites with the site URL set on the backend are not affected; the layer README already requires it. - **Possible fix:** record the request origin in `event.context` in the `request` hook, as `seo-client-address.ts` does for the visitor address, and use it in `seoHeadInput`. - **Why deferred:** it changes v0.1.1 output, so it needs a decision. The 33-07 playground keeps `'/blog/**'` swr instead of `'/**'` so the v0.1.1 `/relative` check stays meaningful. + +## From 33-02 (sm-journal-plugin) + +- **Saving Settings › Journal in the admin answers 500 (pre-existing, framework).** The admin settings save (`cabana` `settingsPut`) runs `lagoon.Validate` over every rule in `models.Settings.Rules()`. `lagoon.Validate` does not know the `string` and `url` rule tokens and returns `lagoon: unrecognized validation rule "string"`. The Journal rules have used `string` (`rss_title`) and `url` (`rss_image_url`) since before Phase 33, so the screen could not be saved before 33-02 either. The same tokens appear in sm-feedback-plugin and sm-paymentgateway-plugin models. Because of this, the new Headings and links switch cannot be saved from the admin until the framework is fixed. `PUT /_journal/api/v1/settings` and MCP `journal_update_settings` work: the 33-02 handler checks `string` and `url` itself. + status: open + - **Possible fix:** teach `lagoon.Validate` the `string` and `url` tokens (`lagoon.ValidateRequest` already knows `string`). This is a framework change and belongs in v0.6.1 or a follow-up patch. + - **Why deferred:** 33-02 commits only to sm-journal-plugin, and the fix belongs in the framework. Dropping the tokens from Journal's `Rules()` would loosen validation in a core plugin.