From 0e5bb0c44fe599d7d0b792c6350c14697fb11d9c Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 29 Oct 2026 00:31:21 +0100 Subject: [PATCH] docs(quick-261028-wqu): summercms.io changelog dropdown with Framework and Plugins streams --- .../261028-wqu-CONTEXT.md | 8 + .../261028-wqu-PLAN.md | 384 ++++++++++++++++++ .../261028-wqu-SUMMARY.md | 356 ++++++++++++++++ 3 files changed, 748 insertions(+) create mode 100644 .planning/quick/261028-wqu-summercms-io-changelog-dropdown-framewor/261028-wqu-CONTEXT.md create mode 100644 .planning/quick/261028-wqu-summercms-io-changelog-dropdown-framewor/261028-wqu-PLAN.md create mode 100644 .planning/quick/261028-wqu-summercms-io-changelog-dropdown-framewor/261028-wqu-SUMMARY.md diff --git a/.planning/quick/261028-wqu-summercms-io-changelog-dropdown-framewor/261028-wqu-CONTEXT.md b/.planning/quick/261028-wqu-summercms-io-changelog-dropdown-framewor/261028-wqu-CONTEXT.md new file mode 100644 index 00000000..2edef6ab --- /dev/null +++ b/.planning/quick/261028-wqu-summercms-io-changelog-dropdown-framewor/261028-wqu-CONTEXT.md @@ -0,0 +1,8 @@ +# Quick 261028-wqu — Context (locked decisions) + +Request: summercms.io site -> make Changelog a dropdown with two links: Framework and Plugins. Plugins page looks exactly the same as the main changelog but filtered: /changelog shows only framework posts, /changelog/plugins only plugin posts. + +## Decisions (user, 2026-10-28) +- D1: Filtering uses Journal categories `framework` and `plugins` (the public API already supports `GET /posts?category=`). +- D2: Plugin release posts (`scripts/changelog-post.py publish ... --repo `) now get category `plugins` and are PUBLISHED normally (no longer forced draft; `--draft` stays available). Framework posts get category `framework`. Update sm-summercmsio-app CLAUDE.md, DEPLOY.md, README and the framework repo CLAUDE.md changelog line to match. +- D3: Backfill is a changelog-post.py subcommand (creates both categories if missing, assigns `framework` to existing framework posts, `plugins` to existing `sm-*-plugin` posts). Tested locally / in smoke only. The user runs it against production. Do NOT run it against summercms.io; no pushes or deploys. diff --git a/.planning/quick/261028-wqu-summercms-io-changelog-dropdown-framewor/261028-wqu-PLAN.md b/.planning/quick/261028-wqu-summercms-io-changelog-dropdown-framewor/261028-wqu-PLAN.md new file mode 100644 index 00000000..4d8a5f7a --- /dev/null +++ b/.planning/quick/261028-wqu-summercms-io-changelog-dropdown-framewor/261028-wqu-PLAN.md @@ -0,0 +1,384 @@ +--- +phase: quick-261028-wqu +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/app/components/SiteFooter.vue + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/nuxt.config.ts + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/app/utils/streams.ts + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/app/composables/useChangelogStream.ts + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/app/components/ChangelogIndex.vue + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/app/pages/changelog/index.vue + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/app/pages/changelog/plugins.vue + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/app/components/Journal/IndexHeader.vue + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/app/components/Journal/RssLink.vue + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/app/layouts/default.vue + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/app/error.vue + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/i18n/locales/en.json + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/server/utils/feed.ts + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/server/utils/changelogFeed.ts + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/server/routes/changelog/rss.xml.ts + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/server/routes/changelog/plugins/rss.xml.ts + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/testing/fixtures/summercms/posts.json + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/testing/fixtures/summercms/categories.json + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/testing/fixtures/summercms/rss.xml + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/testing/expect.txt + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/package.json + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/README.md + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/tests/changelog.test.ts + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/scripts/changelog-post.py + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/scripts/test_changelog_post.py + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/scripts/smoke.sh + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/CLAUDE.md + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/README.md + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/DEPLOY.md + - /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app (submodule pointer) + - /media/nvme/dev/golem15/summercms.io/summercms/summercms.go/CLAUDE.md +autonomous: true +requirements: + - quick-261028-wqu + - D1 + - D2 + - D3 + +estimate: + tokens: 150000 + raw_tokens: 150000 + tasks: 3 + confidence: low + +must_haves: + truths: + - "D1: GET /changelog lists only published Journal posts in category `framework`, 10 per page, with ?page=N pagination (out of range or non-numeric page is a 404)" + - "D1: GET /changelog/plugins renders the same layout (hero, entry list, pagination, RSS link) listing only posts in category `plugins`, with its own ?page=N pagination; the path is not handled by the /changelog/:slug post route" + - "The header's Changelog entry is a disclosure menu with two links, Framework (/changelog) and Plugins (/changelog/plugins), on both the embedded landing and the changelog SSR app; it opens and closes without JavaScript (details/summary), by keyboard and by tap, and is not clipped by the scrolling links strip" + - "Phones can reach both changelogs: the menu stays in the header while it fits, and the footer links both /changelog and /changelog/plugins" + - "GET /changelog/rss.xml carries only `framework` items and GET /changelog/plugins/rss.xml only `plugins` items; every link, guid and atom self link points under https://summercms.io/changelog" + - "D2: changelog-post.py publish sets category `framework` for the framework repo and `plugins` for --repo , and publishes plugin posts (no forced draft); --draft still saves unpublished" + - "D3: changelog-post.py backfill creates the `framework` and `plugins` categories when missing, assigns `framework` to summercms-* posts and `plugins` to sm-*-plugin-* posts, is idempotent, previews with --dry-run and refuses to write without --yes; it is exercised only in scripts/smoke.sh against the local smoke binary" + - "No request from this work writes to https://summercms.io; nothing is pushed, tagged or deployed; vue-journal-layer, sm-journal-plugin and deploy/ configs are not edited" + artifacts: + - path: /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/app/utils/streams.ts + provides: "CHANGELOG_STREAMS (framework/plugins: category slug, index path, feed path), type ChangelogStream, streamForPath(path)" + contains: "export function streamForPath" + - path: /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/app/components/ChangelogIndex.vue + provides: "Shared stream index view: category-filtered listPosts, 404 rules, header/list/pagination/RSS" + contains: "category" + - path: /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/app/pages/changelog/plugins.vue + provides: "The /changelog/plugins page" + contains: "plugins" + - path: /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/server/utils/feed.ts + provides: "rewriteChangelogFeed with per-stream index/self paths, filterFeedItems, findCategoryName (pure)" + contains: "export function filterFeedItems" + - path: /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/server/routes/changelog/plugins/rss.xml.ts + provides: "GET /changelog/plugins/rss.xml" + contains: "plugins" + - path: /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app/changelog/testing/fixtures/summercms/posts.json + provides: "App-owned fixture: 12 published framework posts, 11 published plugin posts, an uncategorised post, a draft" + - path: /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/scripts/changelog-post.py + provides: "publish with categories (D2), backfill subcommand (D3), __main__ guard" + contains: "def cmd_backfill" + - path: /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/scripts/test_changelog_post.py + provides: "stdlib unittest for the pure helpers of changelog-post.py, no network" + contains: "unittest" + key_links: + - from: "changelog/nuxt.config.ts journal.routes.index: false" + to: "changelog/app/pages/changelog/index.vue and plugins.vue" + via: "the layer no longer registers its unfiltered index page; the app's pages own /changelog and /changelog/plugins" + pattern: "index: false" + - from: "ChangelogIndex.vue" + to: "Journal API GET /posts?category=" + via: "useJournalApi().listPosts({ category, page, perPage, sort })" + pattern: "listPosts" + - from: "changelog/app/layouts/default.vue back link" + to: "CHANGELOG_STREAMS[stream].index" + via: "useChangelogStream() (indexLink() is null once the layer index route is off)" + pattern: "useChangelogStream" + - from: "changelogFeed.ts" + to: "Journal API GET /rss and GET /categories?include_empty=1" + via: "category slug -> name, then filterFeedItems on text" + pattern: "include_empty" + - from: "scripts/changelog-post.py publish/backfill" + to: "Journal categories with slugs framework and plugins" + via: "GET /categories?include_empty=1; PUT /posts/{id} {categories}; POST /golem15/journal/categories" + pattern: "framework" +--- + + +Split the summercms.io changelog into two streams backed by Journal categories (D1): `/changelog` for framework releases (category `framework`) and `/changelog/plugins` for plugin releases (category `plugins`). The plugins page looks exactly like the main changelog. The header's Changelog link becomes a dropdown with Framework and Plugins. Each stream has its own RSS feed. `scripts/changelog-post.py` publishes plugin posts into `plugins` (D2) and gains a `backfill` subcommand for the existing posts (D3). The docs in the app and the framework follow (D2). + +Purpose: plugin releases get their own public changelog instead of hidden drafts, and the framework changelog stays framework-only. + +Output: changelog streams and the header menu in vue-summercmsio-app (submodule commits first), script, smoke and docs in sm-summercmsio-app, then the submodule pointer bump, and one docs commit in summercms.go. Nothing is pushed, tagged or deployed. + +Repos and order of commits (stage files by explicit path, never `git add -A`; no co-author tags; conventional prefixes; keep existing D-xx comments): +1. /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app (submodule `golem15/vue-summercmsio-app`): code, fixture and README (Task 1), unit tests (Task 3). +2. /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app: script and smoke (Task 2), docs (Task 2), script unit tests (Task 3), and last the submodule pointer bump as its own commit (Task 3). Leave the untracked `scripts/__pycache__/` alone (not ours); run Python with `-B`. +3. /media/nvme/dev/golem15/summercms.io/summercms/summercms.go: the CLAUDE.md changelog line only (Task 2), its own docs commit, staged by path (other sessions have uncommitted STATE.md, state.json and G15OFFICE.md there). + +HARD SAFETY RULE (D3): never run `changelog-post.py publish` or `backfill` without SUMMERCMS_JOURNAL_API pointing at a loopback smoke binary, SUMMERCMS_JOURNAL_TOKEN set to the smoke token, and HOME set to a temp dir, so `~/.config/summercms/journal-token` (a real production token on this machine) is never read. Unit tests must not open sockets. A read-only `curl` GET against production is allowed for inspection; any write is not. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@/media/nvme/dev/golem15/summercms.io/summercms/summercms.go/.planning/quick/261028-wqu-summercms-io-changelog-dropdown-framewor/261028-wqu-CONTEXT.md +@/media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/CLAUDE.md +@/media/nvme/dev/golem15/summercms.io/summercms/summercms.go/CLAUDE.md + +Facts observed at planning time (verified against the live tree, 2026-10-28): + +- Layer: vue-journal-layer v0.6.0 is installed at vue-summercmsio-app/node_modules/vue-journal-layer. Its module (lib/journal-module.ts) registers PostIndexPage at journal.routes.index. PostIndexPage calls `api.listPosts({ page, perPage, sort })` without a category, so the layer index cannot be filtered. `normalizeJournalRoutes` accepts `index: false`. `useJournalLinks().indexLink()` then returns null. Its only user in the app is the back link in changelog/app/layouts/default.vue (line 27), which must be replaced. Nothing else (sitemap source, cache purge) depends on routes.index. JournalPagination builds its links from `route.path` plus `?page`, so it works on any path. Importable pure helpers: `parsePageParam` and `pageOutOfRange` in 'vue-journal-layer/app/utils/pagination.ts', `toListPost` in 'vue-journal-layer/app/utils/view.ts'. `useJournalApi()` is auto-imported. `listPosts` accepts `category`. +- Journal API (sm-journal-plugin v0.2.2 in plugins/golem15/journal): `GET /posts?category=` filters, and an unknown slug returns an empty list, not every post. `GET /rss` has NO category filter and returns the latest RSSPostsPerFeed (default 20) published posts. Each item carries one `NAME` element per category, with XML-escaped names. `GET /categories?include_empty=1` returns `{data:[tree]}`. `previous_post`/`next_post` are computed over ALL published posts, not per category. `PUT /posts/{id}` is a partial update; `categories` replaces the post's whole category set and accepts existing ids only. The Journal API has NO endpoint that creates a category. A token-authenticated editor lists drafts too with `GET /posts` (no `published` param). per_page is capped at 30. The write throttle allows 120 requests per minute, and a 429 answers `{"message":"Too Many Attempts."}`. +- Category creation is only possible through the cabana admin API: `POST /backend/api/v1/golem15/journal/categories` with a JSON object of form fields (`name` and `slug`, both `mltext`). A Bearer `g15_` token skips the CSRF check (cabana csrf.go), and the token scope `journal` covers the `golem15.journal.*` permissions. Production nginx refuses `/backend`, so creating categories in production needs an SSH tunnel to the binary (127.0.0.1:8095 on rome). Once the categories exist, the assignment and publish steps work through the public Journal API without a tunnel. +- Production state (read-only GET): 43 published posts, all slugged `summercms-x-y-z`, none with a category. Any plugin posts are drafts. +- Fixture harness: `pnpm test:changelog` runs node_modules/vue-journal-layer/testing/smoke-ssr.mjs with `--fixtures`. fixture-api.mjs implements category filtering (with children) and serves `rss.xml` from the fixture dir verbatim. It also requires every `/categories` request to carry `include_empty=1` and at least one API request to carry the forwarded X-Forwarded-For. The acme fixture has only categories `news`/`quiet`, so the app needs its own fixture. +- Landing contracts: tests/output.test.ts `assertLeavesRendered(readIndex(), ['meta','header','hero'])` requires EVERY `header.*` string in i18n/locales/en.json to appear in the generated index.html (attributes count). The landing prerender ignores the '/changelog' prefix, and linkChecker excludes '/changelog/**'. The site plugin's nodeServed treats '/changelog/...' as Node-served. nginx `location ^~ /changelog/` already routes the new paths. No deploy config change is needed. +- SiteHeader: `.links-wrap` has `overflow-x: auto`, so an absolutely positioned panel inside it would be clipped or would scroll. At max-width 720px `.links` and `.links-wrap` are hidden and `.docs` gets `margin-left: auto`. The footer already links /changelog. +- scripts/changelog-post.py calls `main()` at module level (no `__main__` guard), so it cannot be imported by tests yet. `publish` currently refuses a non-framework --repo without --draft. No Python tests exist. +- scripts/smoke.sh creates a superuser and a `journal` token (`TOKEN`), posts "SummerCMS 9.9.9" with slug `smoke-9-9-9`, and defines `purge` (it clears Nitro's whole route cache), `changelog_get`, `expect`, `expect_contains` and `expect_prefix`. The binary in smoke has no site_cache purge URL, so list pages cached earlier need the `purge` call before re-checking. The tool check loop covers docker, curl and node. + + + + + + Task 1: Framework and plugins streams end to end in the changelog SSR, plus the header menu (vue-summercmsio-app) + + vue-summercmsio-app/app/components/SiteHeader.vue, vue-summercmsio-app/app/components/SiteFooter.vue, vue-summercmsio-app/i18n/locales/en.json, + vue-summercmsio-app/changelog/nuxt.config.ts, changelog/app/utils/streams.ts, changelog/app/composables/useChangelogStream.ts, + changelog/app/components/ChangelogIndex.vue, changelog/app/pages/changelog/index.vue, changelog/app/pages/changelog/plugins.vue, + changelog/app/components/Journal/IndexHeader.vue, changelog/app/components/Journal/RssLink.vue, changelog/app/layouts/default.vue, changelog/app/error.vue, + changelog/i18n/locales/en.json, changelog/server/utils/feed.ts, changelog/server/utils/changelogFeed.ts, + changelog/server/routes/changelog/rss.xml.ts, changelog/server/routes/changelog/plugins/rss.xml.ts, + changelog/testing/fixtures/summercms/{posts.json,categories.json,rss.xml}, changelog/testing/expect.txt, package.json, README.md + (all under /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app) + + + vue-summercmsio-app/app/components/SiteHeader.vue, app/components/SiteFooter.vue, changelog/nuxt.config.ts, changelog/app/layouts/default.vue, changelog/app/error.vue, changelog/app/components/Journal/*.vue, changelog/server/utils/feed.ts, changelog/server/routes/changelog/rss.xml.ts, changelog/testing/expect.txt, node_modules/vue-journal-layer/app/journal-pages/PostIndexPage.vue, node_modules/vue-journal-layer/testing/fixtures/acme/posts.json (entry shape only: the first entry and hello-acme / unsafe-sample content_html), app/assets/css/tokens.css (header colours and radii) + + +Implements D1 (filtering by the Journal categories `framework` and `plugins`). The layer stays untouched: everything is done in the app, reusing the layer's components. Work in /media/nvme/dev/golem15/summercms.io/summercms/sm-summercmsio-app/vue-summercmsio-app. + +Streams (single source of truth): +- New changelog/app/utils/streams.ts, pure TypeScript importable by `node --test` (no Nuxt globals, no enums). Export type `ChangelogStream` ('framework' | 'plugins'). Export `CHANGELOG_STREAMS`, a frozen record mapping each stream to `{ category, index, feed }`: framework → 'framework', '/changelog', '/changelog/rss.xml'; plugins → 'plugins', '/changelog/plugins', '/changelog/plugins/rss.xml'. Export `PLUGIN_POST_SLUG` = /^sm-[a-z0-9-]*-plugin-\d/ (the slug changelog-post.py builds for a plugin tag) and `streamForPath(path)`. `streamForPath` drops the query, hash and trailing slash. It returns 'plugins' for '/changelog/plugins' and anything below it, and for '/changelog/' when the slug matches PLUGIN_POST_SLUG. Otherwise it returns 'framework'. Comment that the backfill in scripts/changelog-post.py uses the same slug rule. +- New changelog/app/composables/useChangelogStream.ts: `useChangelogStream()` returns `computed(() => streamForPath(useRoute().path))`. It is derived from the route only, so it is deterministic during SSR. A layout renders before the page's data loads, so the post's categories cannot be used there without a hydration mismatch. + +Pages (per D1): +- changelog/nuxt.config.ts: set `journal.routes.index: false`, with a comment explaining that the layer's index page lists every post unfiltered, so the app's own pages own /changelog and /changelog/plugins. Keep perPage.index 10, post '/changelog/:slug', and category/tag/author false. Update the header comment block where it mentions the index. +- New changelog/app/components/ChangelogIndex.vue with prop `stream` (ChangelogStream, required). It mirrors the layer's PostIndexPage. It calls `parsePageParam(route.query.page)` and throws a fatal 404 on null. It calls `useAsyncData(\`changelog-${stream}-${page}\`, () => api.listPosts({ category: CHANGELOG_STREAMS[stream].category, page, perPage: cfg.perPage.index, sort: cfg.sort }), { transform: res => ({ ...res, data: res.data.map(toListPost) }) })`, where cfg = useRuntimeConfig().public.journal. It uses the same error status propagation, and throws a fatal 404 when pageOutOfRange. It renders section.journal-index with JournalIndexHeader :page, JournalPostList :posts and the stream's empty text, JournalPagination :current/:last, and JournalRssLink. Use the layer's relative-import style with .ts extensions ('vue-journal-layer/app/utils/pagination.ts', '.../view.ts'). An unknown category gives an empty list from the API, so a missing `plugins` category renders the empty state, never framework posts. +- New changelog/app/pages/changelog/index.vue and changelog/app/pages/changelog/plugins.vue. Each calls `definePageMeta({ key: (route) => route.fullPath })` (same as the layer, so client-side pagination re-runs setup) and renders `` or `stream="plugins"`. Vue Router ranks the static /changelog/plugins above /changelog/:slug, so a post slug `plugins` is reserved; note that in a comment. + +Stream-aware chrome: +- changelog/app/components/Journal/IndexHeader.vue: read `useChangelogStream()`. The framework stream keeps the existing `changelog.*` keys. The plugins stream uses new keys under `changelog.plugins` (meta.title, meta.description, eyebrow, title, lead) for the heading and useSeoMeta. Keep the page suffix logic. +- changelog/app/components/Journal/RssLink.vue: href and head link come from CHANGELOG_STREAMS[stream].feed (keep key 'journal-rss'). On a post page this follows the slug rule too. +- changelog/app/layouts/default.vue: replace `links.indexLink()` (null now). The back link shows on post pages (`isPost` unchanged) and points at CHANGELOG_STREAMS[stream].index, with text `changelog.back` for framework or `changelog.plugins.back` for plugins. Pass the header `current` as 'changelog' for framework and 'changelog-plugins' for plugins. +- changelog/app/error.vue: header `current` from the same composable. "Read the changelog" goes to the stream's index path. +- changelog/i18n/locales/en.json: add `changelog.plugins` with meta.title "Plugin changelog", meta.description, eyebrow, title (for example "What changed in the SummerCMS plugins"), lead, empty ("No plugin release notes yet. …") and back ("← All plugin changes"). Copy is Claude's discretion; match the tone of the existing strings. Use plain English, no em dashes, and no planning IDs in user-facing copy. + +Feeds (framework-only /changelog/rss.xml, plus a plugins feed so the plugins page's RSS link is not misleading): +- changelog/server/utils/feed.ts (pure; keep its current exports and behaviour): + - Give `rewriteChangelogFeed(xml, siteUrl, paths?)` an optional third argument `{ index, self }`, defaulting to '/changelog' and '/changelog/rss.xml'. The channel/root link goes to `${site}${index}` and the atom self link to `${site}${self}`; item links stay `${site}/changelog/`. Existing calls and tests must behave the same. + - Add `filterFeedItems(xml, categoryName)`. It keeps only `` blocks that contain a `` element whose XML-unescaped, trimmed text equals categoryName. A null categoryName removes every item. CDATA sections must be masked before matching and restored after, using the same split approach as the rewrite, so a literal closing item tag inside a description cannot cut an item. The channel elements stay untouched. + - Add `findCategoryName(tree, slug)`. It flattens `children`, matches `slug` and returns the `name`, or null. +- New changelog/server/utils/changelogFeed.ts: `changelogFeed(event, stream)` holds the current handler's logic: runtime apiBase, the forwarded X-Forwarded-For chain built the same way, and a 502 'The feed is unavailable.' text response when an API call fails. It fetches '/rss' (text) and '/categories' with query include_empty=1 in parallel, both with the forwarded header. It resolves the stream's category name, filters, rewrites with the stream's index/self paths and sets the content type as now. Import CHANGELOG_STREAMS relatively from '../../app/utils/streams.ts'. +- changelog/server/routes/changelog/rss.xml.ts becomes a thin `defineEventHandler(e => changelogFeed(e, 'framework'))` (keep the header comment, updated). Add changelog/server/routes/changelog/plugins/rss.xml.ts for 'plugins'. A feed holds the latest N published posts (Journal setting, default 20) filtered to its stream, so it may carry fewer than N items. Say so in the comment. + +Header menu (vue-summercmsio-app/app/components/SiteHeader.vue, used by the landing and the changelog): +- Replace the plain Changelog anchor with a disclosure: `