docs(16.2-04): complete Journal embeds layer release plan
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
---
|
||||
phase: 16.2-plugin-provided-field-types-and-the-blog-editor-formwidget
|
||||
plan: 04
|
||||
subsystem: journal-frontend
|
||||
tags: [vue-journal-layer, nuxt, sanitize-html, embeds, youtube-nocookie, release, pnpm, vue-grzybyfunkcjonalne-app]
|
||||
|
||||
requires:
|
||||
- phase: 16.2-02
|
||||
provides: tests/blogeditor/fixtures/editor-corpus.json (the Blog Editor's output corpus) and the placeholder format <figure data-embed="youtube" data-id="ID"></figure>
|
||||
- phase: 16.1
|
||||
provides: sanitize-html 2.18.0 mirror (htmlGate.ts, decision note 0001-sanitize-html.md), layer tag v0.1.1, app pin #v0.1.1
|
||||
provides:
|
||||
- "vue-journal-layer htmlGate.ts mirror of the Go editor-output extension: figure data-embed ^youtube$, figure data-id ^[A-Za-z0-9_-]{11}$, img alt free text, ol start INTEGER"
|
||||
- "vue-journal-layer app/utils/embeds.ts: EMBED_PLACEHOLDER, EMBED_PROVIDERS (youtube only), expandEmbeds(html)"
|
||||
- "JournalPostBody and JournalCategoryHeader render expandEmbeds(sanitizeJournalHtml(html)); expansion is last, payload transforms only sanitize"
|
||||
- "CSS classes .journal-embed and .journal-embed--youtube (16 / 9 frame)"
|
||||
- "tests/fixtures/editor-corpus.json, a byte copy of plan 02's corpus"
|
||||
- "Annotated tag vue-journal-layer v0.2.0 (tag object 042b65d0, commit 759e684e) on origin, installable anonymously over git+https"
|
||||
- "vue-grzybyfunkcjonalne-app main 465286a pins #v0.2.0 with a lockfile resolving to 759e684e"
|
||||
affects: [16.2-05, 16.2-06, vue-journal-layer, vue-grzybyfunkcjonalne-app]
|
||||
|
||||
actuals:
|
||||
tokens: 9700
|
||||
tasks: 3
|
||||
commits: 3
|
||||
plan_head_before: 294a8d8b19b999d54cc3b1266d0f42968159f5f2
|
||||
plan_head_after: 759e684e862bd8a2f34e2bbcdf258f66aeddad4f
|
||||
commits_repo: ../vue-journal-layer
|
||||
app_head_before: ada03b202bdfce2c09812dc81c58b0890cf4b86b
|
||||
app_head_after: 465286a880f0aae6118c2915aa1475a5da353295
|
||||
app_commits_repo: ../sm-grzybyfunkcjonalne-app/vue-grzybyfunkcjonalne-app
|
||||
app_commits: 1
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Sanitize first, expand last: the v-html sink runs sanitizeJournalHtml and then expandEmbeds; nothing sanitizes after expansion, and payload transforms never expand"
|
||||
- "Exact-markup placeholder expansion: one global RegExp matching the sanitizer's serialization, a provider allowlist with an id pattern, and an iframe src built from a fixed host plus the validated id; any other match is returned unchanged"
|
||||
- "Mirror pinned by the editor's corpus: the layer keeps a byte copy of the Go side's corpus and every entry must be safe under gateJournalHtml"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- ../vue-journal-layer/app/utils/embeds.ts
|
||||
- ../vue-journal-layer/tests/embeds.test.ts
|
||||
- ../vue-journal-layer/tests/fixtures/editor-corpus.json
|
||||
modified:
|
||||
- ../vue-journal-layer/app/utils/htmlGate.ts
|
||||
- ../vue-journal-layer/app/components/Journal/PostBody.vue
|
||||
- ../vue-journal-layer/app/components/Journal/CategoryHeader.vue
|
||||
- ../vue-journal-layer/app/assets/css/journal-base.css
|
||||
- ../vue-journal-layer/tests/htmlGate.test.ts
|
||||
- ../vue-journal-layer/testing/fixtures/acme/posts.json
|
||||
- ../vue-journal-layer/testing/expect-acme.txt
|
||||
- ../vue-journal-layer/docs/decisions/0001-sanitize-html.md
|
||||
- ../vue-journal-layer/README.md
|
||||
- ../vue-journal-layer/package.json
|
||||
- ../sm-grzybyfunkcjonalne-app/vue-grzybyfunkcjonalne-app/package.json
|
||||
- ../sm-grzybyfunkcjonalne-app/vue-grzybyfunkcjonalne-app/pnpm-lock.yaml
|
||||
|
||||
key-decisions:
|
||||
- "The layer mirror gained exactly the four editor-output rules of the Go policy (figure data-embed ^youtube$ and data-id ^[A-Za-z0-9_-]{11}$, img alt free text, ol start INTEGER); no fifth rule was needed for the corpus"
|
||||
- "Embed expansion runs only at the v-html sinks, after sanitizeJournalHtml; the payload (toPostView, CategoryPage) carries the placeholder, never the iframe"
|
||||
- "Human approved the one-way release at the Task 2 blocking-human checkpoint with the reply \"push\"; vue-journal-layer v0.2.0 is published and any later defect gets v0.2.1"
|
||||
- "The release commit also moved the README install line from #v0.1.0 to #v0.2.0 so the documented pin matches the published tag"
|
||||
|
||||
patterns-established:
|
||||
- "A layer release is a separate 'chore: release vX.Y.Z' commit (package.json version plus README install line), tagged annotated, pushed after a blocking-human checkpoint, proven by scripts/check-git-install.sh, then pinned in the consuming app with a regenerated lockfile"
|
||||
|
||||
requirements-completed: [D-05, D-17]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "htmlGate.ts mirrors the Go editor-output extension and nothing more; every corpus entry is safe, the placeholder survives two sanitizer passes, negative cases (other provider, 10/12-char id, data-id on div, other data attributes, iframe, ol start=x) are stripped"
|
||||
requirement: "D-05"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "../vue-journal-layer/tests/htmlGate.test.ts (npm test: 136 pass, 0 fail)"
|
||||
status: pass
|
||||
- kind: other
|
||||
ref: "cmp ../vue-journal-layer/tests/fixtures/editor-corpus.json ../sm-journal-plugin/tests/blogeditor/fixtures/editor-corpus.json (exit 0)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "expandEmbeds turns only the exact allowlisted placeholder into a youtube-nocookie iframe, last at the post and category sinks; a raw stored iframe is stripped before expansion"
|
||||
requirement: "D-17"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "../vue-journal-layer/tests/embeds.test.ts (npm test: 136 pass, 0 fail)"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "node testing/smoke-ssr.mjs against the built playground: smoke-ssr: ok (29 checks), including 200 /blog/hello-acme contains=https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "vue-journal-layer v0.2.0 is an annotated tag on origin and installs anonymously; the scratch lockfile pins the tag commit and the acme SSR expectations pass"
|
||||
requirement: "D-17"
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "bash ../vue-journal-layer/scripts/check-git-install.sh v0.2.0 -> check-git-install: ok v0.2.0 759e684e862bd8a2f34e2bbcdf258f66aeddad4f"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "vue-grzybyfunkcjonalne-app main pins #v0.2.0, its lockfile resolves to 759e684e, it builds, its unit and SSR tests pass, and main equals origin/main"
|
||||
requirement: "D-17"
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "Task 3 app verify command -> # pass 26, # fail 0, app-pin-ok; pnpm run smoke:ssr -> smoke-ssr: ok (37 checks)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D5
|
||||
description: "A Blog Editor YouTube embed plays on the deployed grzybyfunkcjonalne.pl UAT page"
|
||||
requirement: "D-17"
|
||||
verification: []
|
||||
human_judgment: true
|
||||
rationale: "The UAT deploy and the CSP frame-src https://www.youtube-nocookie.com addition belong to plan 05; a real browser on the deployed site is needed to confirm the frame loads"
|
||||
|
||||
duration: "about 50 min across two executor runs (Task 3 continuation 7 min)"
|
||||
completed: 2026-10-10
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 16.2 Plan 04: Journal Embeds in the Nuxt Layer Summary
|
||||
|
||||
**vue-journal-layer v0.2.0 keeps the Blog Editor's markup through its sanitize-html mirror (four rules matching the Go policy) and expands `<figure data-embed="youtube" data-id="…">` placeholders into youtube-nocookie iframes as the last step before v-html. The tag is published and installs anonymously, and vue-grzybyfunkcjonalne-app main pins it.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** about 50 min over two executor runs. The first run covered Task 1, the release commit and the Task 2 checkpoint (layer commits at 10:19Z). This continuation ran Task 3 from 2026-10-10T11:01:53Z to about 11:08Z.
|
||||
- **Started:** about 2026-10-10T10:14Z, after plan 03 closed. The prior run did not record an exact start time.
|
||||
- **Completed:** 2026-10-10T11:10Z
|
||||
- **Tasks:** 3/3 (Task 1 tracer, Task 2 blocking-human decision, Task 3 release)
|
||||
- **Files modified:** 15 (13 in vue-journal-layer, 2 in vue-grzybyfunkcjonalne-app)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- **Mirror (D-05 amended).** `htmlGate.ts` gained exactly the four editor-output rules that plan 03 added to the Go bluemonday policy: figure `data-embed` `^youtube$`, figure `data-id` `^[A-Za-z0-9_-]{11}$`, img `alt` free text and ol `start` INTEGER. Every entry of the byte-copied corpus is safe. The Policy section of decision note 0001 lists the four additions.
|
||||
- **Embeds (D-17).** `app/utils/embeds.ts` exports `EMBED_PLACEHOLDER`, `EMBED_PROVIDERS` (YouTube only) and `expandEmbeds`. The iframe src is the fixed host `https://www.youtube-nocookie.com/embed/` plus the validated id, and the iframe gets lazy loading, a strict-origin-when-cross-origin referrer policy and a restricted `allow` list.
|
||||
- **Order.** `JournalPostBody` and `JournalCategoryHeader` render `expandEmbeds(sanitizeJournalHtml(html))`. `toPostView` and `CategoryPage.vue` only sanitize. A raw iframe in stored HTML is stripped before expansion.
|
||||
- **SSR proof.** The acme fixture post `hello-acme` carries a placeholder, and the built playground renders the iframe for it.
|
||||
- **Release.** Annotated tag `v0.2.0` is on origin and `check-git-install.sh v0.2.0` passes.
|
||||
- **App pin.** The site app pins `#v0.2.0` on its pushed main, with a lockfile that resolves to the tag commit.
|
||||
|
||||
## Task Commits
|
||||
|
||||
| Task | Name | Repo | Commit |
|
||||
|---|---|---|---|
|
||||
| 1 | Tracer: a stored placeholder renders as a privacy-enhanced YouTube iframe | vue-journal-layer | `a6ee443` (feat) |
|
||||
| 2 | Approve pushing vue-journal-layer v0.2.0 and the site app pin (prep: release commit) | vue-journal-layer | `759e684` (chore: release v0.2.0) |
|
||||
| 3 | Push the layer tag, prove the git install, pin v0.2.0 in the site app | vue-grzybyfunkcjonalne-app | `465286a` (chore(deps)) |
|
||||
|
||||
Task 3 also created the annotated tag `v0.2.0` (tag object `042b65d072f276f7ee6f1d8bc6f66f518411161e`, peeled commit `759e684e862bd8a2f34e2bbcdf258f66aeddad4f`).
|
||||
|
||||
**Pushed heads:**
|
||||
- vue-journal-layer `main`: `294a8d8..759e684` (`759e684e862bd8a2f34e2bbcdf258f66aeddad4f`) and tag `v0.2.0`, pushed to git@git.golem15.com:golem15/vue-journal-layer.git.
|
||||
- vue-grzybyfunkcjonalne-app `main`: `ada03b2..465286a` (`465286a880f0aae6118c2915aa1475a5da353295`), pushed to git@git.golem15.com:golem15/vue-grzybyfunkcjonalne-app.git.
|
||||
|
||||
The summercms.go host gitlink and the sm-grzybyfunkcjonalne-app host gitlink were not touched. Plan 05 bumps them.
|
||||
|
||||
## Evidence
|
||||
|
||||
**Task 1 (tracer):**
|
||||
- `npm test`: 136 pass, 0 fail.
|
||||
- `pnpm install --frozen-lockfile` and `build:playground`: OK.
|
||||
- `smoke-ssr: ok (29 checks)`, including `200 /blog/hello-acme contains=https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ`.
|
||||
- `cmp tests/fixtures/editor-corpus.json ../sm-journal-plugin/tests/blogeditor/fixtures/editor-corpus.json`: exit 0.
|
||||
- Exactly four mirror rules were added: figure data-embed `^youtube$`, figure data-id `^[A-Za-z0-9_-]{11}$`, img alt FREE_TEXT, ol start INTEGER.
|
||||
|
||||
**Task 2 (checkpoint:decision, gate blocking-human):**
|
||||
- The verify command printed `push-precheck-ok`.
|
||||
- Release sheet:
|
||||
- Layer commits to push: `a6ee443` feat, `759e684` chore: release v0.2.0.
|
||||
- Tag `v0.2.0`, message "v0.2.0: Blog Editor markup kept by the sanitizer; Journal embed placeholders expand into privacy-enhanced YouTube iframes".
|
||||
- App change: pin `#v0.1.1` to `#v0.2.0`, plus the regenerated lockfile.
|
||||
- Rollback: a defect means v0.2.1, and the app pin can go back to `#v0.1.1`.
|
||||
- Human reply, verbatim: "push"
|
||||
|
||||
**Task 3 (release):**
|
||||
- Re-check before tagging: `git ls-remote --tags origin refs/tags/v0.2.0` was empty, the layer and app trees were clean, and neither was behind origin/main.
|
||||
- `git ls-remote --tags origin`: `042b65d0… refs/tags/v0.2.0` and `759e684e… refs/tags/v0.2.0^{}`.
|
||||
- `bash scripts/check-git-install.sh v0.2.0`: `smoke-ssr: ok (29 checks)`, then `check-git-install: ok v0.2.0 759e684e862bd8a2f34e2bbcdf258f66aeddad4f`.
|
||||
- App `pnpm install`: `- vue-journal-layer 0.1.0` / `+ vue-journal-layer 0.2.0`. The lockfile diff only touches the layer's specifier, version and resolution `commit: 759e684e862bd8a2f34e2bbcdf258f66aeddad4f`.
|
||||
- App verify (`grep pin && pnpm install --frozen-lockfile && pnpm build && pnpm test && HEAD == origin/main`): `# pass 26`, `# fail 0`, `app-pin-ok`.
|
||||
- App `pnpm run smoke:ssr` (extra check): `smoke-ssr: ok (37 checks)`.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `../vue-journal-layer/app/utils/embeds.ts`: placeholder RegExp, provider allowlist and `expandEmbeds`.
|
||||
- `../vue-journal-layer/app/utils/htmlGate.ts`: figure and ol entries and the widened img alt rule.
|
||||
- `../vue-journal-layer/app/components/Journal/PostBody.vue` and `CategoryHeader.vue`: sanitize, then expand, then v-html.
|
||||
- `../vue-journal-layer/app/assets/css/journal-base.css`: `.journal-embed` frame at a 16 / 9 aspect ratio.
|
||||
- `../vue-journal-layer/tests/embeds.test.ts`, `tests/htmlGate.test.ts` and `tests/fixtures/editor-corpus.json`: transform, pipeline and mirror tests, plus the corpus copy.
|
||||
- `../vue-journal-layer/testing/fixtures/acme/posts.json` and `testing/expect-acme.txt`: the SSR fixture and its expectation.
|
||||
- `../vue-journal-layer/docs/decisions/0001-sanitize-html.md` and `README.md`: the Policy additions and the Embeds section.
|
||||
- `../vue-journal-layer/package.json`: version 0.2.0.
|
||||
- `../sm-grzybyfunkcjonalne-app/vue-grzybyfunkcjonalne-app/package.json` and `pnpm-lock.yaml`: the `#v0.2.0` pin, resolving to 759e684e.
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- The mirror has exactly four rules, matching the Go policy. The corpus needed no fifth rule.
|
||||
- Expansion happens only at the sinks and comes last, so the payload carries the placeholder and never an iframe.
|
||||
- The release is a separate commit, and the README install line moves with the version.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 2 - Correctness] README install line bumped with the version**
|
||||
- **Found during:** Task 2 (release prep, prior run)
|
||||
- **Issue:** The plan's release commit changes only the package.json version, but the README install snippet still pinned `#v0.1.0`. A consumer copying it would install an outdated layer.
|
||||
- **Fix:** The release commit also changes the README install line from `#v0.1.0` to `#v0.2.0`.
|
||||
- **Files modified:** ../vue-journal-layer/README.md
|
||||
- **Committed in:** `759e684` (chore: release v0.2.0)
|
||||
|
||||
**2. [Rule 3 - Blocking] check-git-install could not write the shared /tmp pnpm store**
|
||||
- **Found during:** Task 3
|
||||
- **Issue:** The script's scratch app lives under `$TMPDIR` (/tmp), where pnpm picks the store `/tmp/.pnpm-store/v11`. On this machine another local user (`jin`) owns that store, so `pnpm install` failed with `ERR_SQLITE_ERROR attempt to write a readonly database`. The tag was not the cause.
|
||||
- **Fix:** I re-ran the unchanged script with `TMPDIR` and `npm_config_store_dir`/`pnpm_config_store_dir` pointing into the session scratchpad. The script, the tag and the remote were not touched.
|
||||
- **Files modified:** none
|
||||
- **Verification:** `check-git-install: ok v0.2.0 759e684e862bd8a2f34e2bbcdf258f66aeddad4f`
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 2 (1 correctness, 1 environment blocker).
|
||||
**Impact on plan:** No change to the shipped code. The tag was created and pushed exactly as planned, with no moved tag and no force-push.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- On this machine, `scripts/check-git-install.sh` fails under the default `/tmp` because the store there belongs to another user (see deviation 2). This is a machine-local environment issue, not a layer defect. Anyone re-running the script here should set `npm_config_store_dir`, or a `TMPDIR` whose pnpm store they own.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None. No external service configuration is required.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- Plan 05 can bump the sm-grzybyfunkcjonalne-app host gitlink to app `465286a`. It also needs to add `frame-src https://www.youtube-nocookie.com` to the report-only CSP, then deploy to UAT.
|
||||
- The plan 06 phase gate can `cmp` the layer corpus copy against `../sm-journal-plugin/tests/blogeditor/fixtures/editor-corpus.json`.
|
||||
|
||||
---
|
||||
*Phase: 16.2-plugin-provided-field-types-and-the-blog-editor-formwidget*
|
||||
*Completed: 2026-10-10*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: ../vue-journal-layer/app/utils/embeds.ts, tests/embeds.test.ts, tests/fixtures/editor-corpus.json
|
||||
- FOUND commits: a6ee443, 759e684 (vue-journal-layer), 465286a (vue-grzybyfunkcjonalne-app)
|
||||
- expandEmbeds count 0 in postView.ts and CategoryPage.vue; app pin #v0.2.0 present; corpus cmp exit 0
|
||||
Reference in New Issue
Block a user