From 13ec1f4adabad8389696929249a31c02f1119c7c Mon Sep 17 00:00:00 2001 From: "Dylan Mordaunt (ISLHD)" Date: Mon, 10 Aug 2026 23:58:36 +1000 Subject: [PATCH 1/2] Fix title and subtitle body duplication --- README.md | 5 + conductor/archive/T18-01/contracts.json | 7 + conductor/archive/T18-01/design.md | 18 +++ conductor/archive/T18-01/implementation.md | 7 + conductor/archive/T18-01/index.md | 9 ++ conductor/archive/T18-01/metadata.json | 12 ++ conductor/archive/T18-01/plan.md | 23 ++++ conductor/archive/T18-01/requirements.md | 21 +++ conductor/archive/T18-01/risks.md | 5 + conductor/archive/T18-01/spec.md | 19 +++ conductor/archive/T18-01/verification.md | 11 ++ .../substack-cli-maturity.contract.json | 127 ++++++++++++++++++ conductor/programme-state.json | 38 ++++++ conductor/traceability.json | 82 +++++++++++ conductor/tracks.md | 3 + docs/examples/content-features.md | 8 +- examples/basic.md | 2 - src/publish/prepare.test.ts | 123 +++++++++++++++++ src/publish/prepare.ts | 60 +++++++-- 19 files changed, 563 insertions(+), 17 deletions(-) create mode 100644 conductor/archive/T18-01/contracts.json create mode 100644 conductor/archive/T18-01/design.md create mode 100644 conductor/archive/T18-01/implementation.md create mode 100644 conductor/archive/T18-01/index.md create mode 100644 conductor/archive/T18-01/metadata.json create mode 100644 conductor/archive/T18-01/plan.md create mode 100644 conductor/archive/T18-01/requirements.md create mode 100644 conductor/archive/T18-01/risks.md create mode 100644 conductor/archive/T18-01/spec.md create mode 100644 conductor/archive/T18-01/verification.md diff --git a/README.md b/README.md index 048373c8..a9c6d9ee 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,11 @@ audience: everyone section: original-essays comments: enabled --- + +Start the article body here. Do not repeat the front-matter title or subtitle as +the first heading or paragraph; those values are written to Substack's dedicated +header fields. The CLI defensively removes exact leading repetitions, but users +and authoring agents should emit each title and subtitle only once. ``` Media examples: diff --git a/conductor/archive/T18-01/contracts.json b/conductor/archive/T18-01/contracts.json new file mode 100644 index 00000000..10e4818b --- /dev/null +++ b/conductor/archive/T18-01/contracts.json @@ -0,0 +1,7 @@ +{ + "schemaVersion": 1, + "trackId": "T18-01", + "contractIds": ["PARSER-AST-006", "FEATURE-FIDELITY-008", "UX-CLI-CONTRACT-005"], + "programmeIssue": 184, + "project": 38 +} diff --git a/conductor/archive/T18-01/design.md b/conductor/archive/T18-01/design.md new file mode 100644 index 00000000..075c4528 --- /dev/null +++ b/conductor/archive/T18-01/design.md @@ -0,0 +1,18 @@ +# Design T18-01 + +```mermaid +flowchart LR + A["Markdown source"] --> B["Parse front matter and body"] + B --> C["Resolve title"] + C --> D{"Leading block exactly matches title?"} + D -->|Yes| E["Remove title block"] + D -->|No| F["Preserve block"] + E --> G{"Next block exactly matches subtitle?"} + F --> G + G -->|Yes| H["Remove subtitle block"] + G -->|No| I["Preserve body"] + H --> J["Synchronized prepared post"] + I --> J +``` + +Matching uses rendered block text with surrounding whitespace trimmed. Removal is limited to the first eligible title block and immediately leading subtitle block. diff --git a/conductor/archive/T18-01/implementation.md b/conductor/archive/T18-01/implementation.md new file mode 100644 index 00000000..2f7d05fe --- /dev/null +++ b/conductor/archive/T18-01/implementation.md @@ -0,0 +1,7 @@ +# Implementation T18-01 + +- Exact leading explicit title headings or paragraphs are removed. +- Heading-derived title fallback remains supported when front matter has no title. +- Exact leading subtitle headings or paragraphs are removed after title normalization. +- Markdown, HTML, and ProseMirror representations remain synchronized. +- Canonical examples and agent/user guidance no longer repeat header metadata. diff --git a/conductor/archive/T18-01/index.md b/conductor/archive/T18-01/index.md new file mode 100644 index 00000000..c7827afc --- /dev/null +++ b/conductor/archive/T18-01/index.md @@ -0,0 +1,9 @@ +# T18-01 Metadata/body de-duplication correctness + +- [Specification](./spec.md) +- [Requirements](./requirements.md) +- [Design](./design.md) +- [Plan](./plan.md) +- [Verification](./verification.md) +- [Metadata](./metadata.json) +- [GitHub issue #537](https://github.com/edithatogo/substack-cli-ts/issues/537) diff --git a/conductor/archive/T18-01/metadata.json b/conductor/archive/T18-01/metadata.json new file mode 100644 index 00000000..9fab7760 --- /dev/null +++ b/conductor/archive/T18-01/metadata.json @@ -0,0 +1,12 @@ +{ + "trackId": "T18-01", + "title": "Metadata/body de-duplication correctness", + "type": "bug", + "status": "archived", + "parentIssue": 538, + "issue": 537, + "project": 38, + "createdAt": "2026-08-10T23:30:00+10:00", + "updatedAt": "2026-08-10T23:55:00+10:00", + "archivedAt": "2026-08-10T23:55:00+10:00" +} diff --git a/conductor/archive/T18-01/plan.md b/conductor/archive/T18-01/plan.md new file mode 100644 index 00000000..64200fb9 --- /dev/null +++ b/conductor/archive/T18-01/plan.md @@ -0,0 +1,23 @@ +# Plan T18-01 + +## Phase 1: Reproduction and contract + +- [x] Task: Reproduce title/subtitle metadata duplication and identify all shared preparation paths. +- [x] Task: Define MoSCoW requirements, risks, and Mermaid design. +- [x] Task: Register P18/T18-01 and nested task issues. +- [x] Task: Phase verification and checkpoint per `conductor/workflow.md`. + +## Phase 2: Test-driven correction + +- [x] Task: Add exact title/subtitle and formatting regression tests. +- [x] Task: Add mismatch, later-match, and fallback preservation tests. +- [x] Task: Normalize leading metadata blocks at `preparePost()`. +- [x] Task: Phase verification and checkpoint per `conductor/workflow.md`. + +## Phase 3: Guidance and closeout + +- [x] Task: Correct checked-in examples and document canonical agent/user input. +- [x] Task: Apply review finding for explicit paragraph title duplication. +- [x] Task: Run focused and required hosted verification. +- [x] Task: Review Actions and comments, merge, archive, and clean up. +- [x] Task: Phase verification and checkpoint per `conductor/workflow.md`. diff --git a/conductor/archive/T18-01/requirements.md b/conductor/archive/T18-01/requirements.md new file mode 100644 index 00000000..f250b4f2 --- /dev/null +++ b/conductor/archive/T18-01/requirements.md @@ -0,0 +1,21 @@ +# Requirements T18-01 + +## Must + +- Remove exact leading metadata repetitions at the shared preparation boundary. +- Keep Markdown, HTML, and ProseMirror representations synchronized. +- Add deterministic regression and preservation tests. +- Correct examples and document the agent authoring contract. + +## Should + +- Treat inline formatting as presentation rather than a text mismatch. +- Preserve title fallback when title front matter is absent. + +## Could + +- Add future diagnostics for near-matching title/deck content. + +## Won't + +- Remove content based on fuzzy, semantic, or LLM judgment. diff --git a/conductor/archive/T18-01/risks.md b/conductor/archive/T18-01/risks.md new file mode 100644 index 00000000..572a7c16 --- /dev/null +++ b/conductor/archive/T18-01/risks.md @@ -0,0 +1,5 @@ +# Risks T18-01 + +- Over-broad deletion could remove legitimate editorial content; exact rendered-text equality and leading-position constraints mitigate this. +- Divergence between Markdown, HTML, and ProseMirror forms could affect validation; all three are normalized together and tested. +- Agent guidance alone is insufficient; the shared runtime boundary remains defensive. diff --git a/conductor/archive/T18-01/spec.md b/conductor/archive/T18-01/spec.md new file mode 100644 index 00000000..4ed5102a --- /dev/null +++ b/conductor/archive/T18-01/spec.md @@ -0,0 +1,19 @@ +# Track T18-01: Metadata/body de-duplication correctness + +## Overview + +Prevent front-matter title and subtitle values from appearing again at the start of the article body while preserving legitimate editorial content and heading-derived title fallback. + +## Acceptance criteria + +- Exact leading title headings and subtitle paragraphs/headings are absent from prepared Markdown, HTML, and ProseMirror content. +- Formatting does not prevent exact rendered subtitle matching. +- Non-matching headings, standfirsts, and later matching text remain untouched. +- Title fallback from a leading heading without front matter remains supported. +- Examples and agent-facing guidance use one canonical metadata/body shape. + +## Out of scope + +- Fuzzy or semantic deletion of similar content. +- Editing already-created Substack drafts. +- Changing Substack title or subtitle field selectors. diff --git a/conductor/archive/T18-01/verification.md b/conductor/archive/T18-01/verification.md new file mode 100644 index 00000000..48b76679 --- /dev/null +++ b/conductor/archive/T18-01/verification.md @@ -0,0 +1,11 @@ +# Verification T18-01 + +- Issue reproduction: confirmed. +- Focused regression tests: 14 passed. +- Full test suite: 117 files and 933 tests passed. +- TypeScript typecheck: passed. +- Contract generation check: passed. +- Programme drift check before closeout: passed. +- Conductor review: one paragraph-title gap found, fixed, and reverified. +- Required hosted checks: pending PR. +- Review comments: pending PR. diff --git a/conductor/contracts/substack-cli-maturity.contract.json b/conductor/contracts/substack-cli-maturity.contract.json index 013ad822..d9e13e21 100644 --- a/conductor/contracts/substack-cli-maturity.contract.json +++ b/conductor/contracts/substack-cli-maturity.contract.json @@ -1701,6 +1701,15 @@ "T11-03" ], "status": "planned" + }, + { + "phaseId": "P18", + "title": "Metadata/body de-duplication correctness", + "releaseTarget": "next patch", + "tracks": [ + "T18-01" + ], + "status": "contract_verified" } ], "tracks": [ @@ -2571,6 +2580,26 @@ "url": "https://github.com/edithatogo/substack-cli-ts/issues/457", "title": "T11-03 Prompt retirement and canonical-system handover" } + }, + { + "id": "T18-01", + "phaseId": "P18", + "title": "Metadata/body de-duplication correctness", + "status": "contract_verified", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "requiredTaskCount": 3, + "existingIssues": [ + 537 + ], + "issue": { + "number": 537, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/537", + "title": "Prevent title and subtitle duplication in published post bodies" + } } ], "tasks": [ @@ -7095,6 +7124,75 @@ "GOV-PROMPT-RETIREMENT-006" ], "release": "1.0 and beyond" + }, + { + "id": "T18-01-TASK-01", + "title": "Add metadata-prefix regression coverage", + "trackId": "T18-01", + "phaseId": "P18", + "status": "contract_verified", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "issue": { + "number": 539, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/539", + "title": "Add metadata-prefix regression coverage" + }, + "mustContractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "release": "next patch" + }, + { + "id": "T18-01-TASK-02", + "title": "Normalize exact title and subtitle prefixes", + "trackId": "T18-01", + "phaseId": "P18", + "status": "contract_verified", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "issue": { + "number": 540, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/540", + "title": "Normalize exact title and subtitle prefixes" + }, + "mustContractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "release": "next patch" + }, + { + "id": "T18-01-TASK-03", + "title": "Correct examples and agent authoring guidance", + "trackId": "T18-01", + "phaseId": "P18", + "status": "contract_verified", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "issue": { + "number": 541, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/541", + "title": "Correct examples and agent authoring guidance" + }, + "mustContractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "release": "next patch" } ], "issueMap": { @@ -7175,6 +7273,11 @@ "url": "https://github.com/edithatogo/substack-cli-ts/issues/446", "title": "P11 1.0 governance and post-1.0 roadmap", "parentPhaseLink": "already" + }, + "P18": { + "number": 538, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/538", + "title": "P18 Metadata/body de-duplication correctness" } }, "trackIssues": { @@ -7453,6 +7556,12 @@ "url": "https://github.com/edithatogo/substack-cli-ts/issues/457", "title": "T11-03 Prompt retirement and canonical-system handover", "parentTrackLink": "already" + }, + "T18-01": { + "number": 537, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/537", + "title": "Prevent title and subtitle duplication in published post bodies", + "parentTrackLink": "already" } }, "taskIssues": { @@ -8601,6 +8710,24 @@ "url": "https://github.com/edithatogo/substack-cli-ts/issues/461", "title": "T11-03 T11-03-TASK-04", "parentTaskLink": "already" + }, + "T18-01-TASK-01": { + "number": 539, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/539", + "title": "Add metadata-prefix regression coverage", + "parentTaskLink": "already" + }, + "T18-01-TASK-02": { + "number": 540, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/540", + "title": "Normalize exact title and subtitle prefixes", + "parentTaskLink": "already" + }, + "T18-01-TASK-03": { + "number": 541, + "url": "https://github.com/edithatogo/substack-cli-ts/issues/541", + "title": "Correct examples and agent authoring guidance", + "parentTaskLink": "already" } } }, diff --git a/conductor/programme-state.json b/conductor/programme-state.json index 1cc02f4f..99114be9 100644 --- a/conductor/programme-state.json +++ b/conductor/programme-state.json @@ -84,6 +84,12 @@ "issue": 446, "state": "OPEN", "status": "Todo" + }, + { + "id": "P18", + "issue": 538, + "state": "CLOSED", + "status": "Done" } ], "tracks": [ @@ -454,6 +460,14 @@ "state": "OPEN", "status": "Todo", "evidence": "conductor/tracks/T11-03/verification.md" + }, + { + "id": "T18-01", + "phase": "P18", + "issue": 537, + "state": "CLOSED", + "status": "Done", + "evidence": "conductor/archive/T18-01/verification.md" } ], "tasks": [ @@ -1984,6 +1998,30 @@ "state": "OPEN", "status": "Todo", "evidence": "conductor/tracks/T11-03/verification.md" + }, + { + "id": "T18-01-TASK-01", + "track": "T18-01", + "issue": 539, + "state": "CLOSED", + "status": "Done", + "evidence": "conductor/archive/T18-01/verification.md" + }, + { + "id": "T18-01-TASK-02", + "track": "T18-01", + "issue": 540, + "state": "CLOSED", + "status": "Done", + "evidence": "conductor/archive/T18-01/verification.md" + }, + { + "id": "T18-01-TASK-03", + "track": "T18-01", + "issue": 541, + "state": "CLOSED", + "status": "Done", + "evidence": "conductor/archive/T18-01/verification.md" } ], "supplemental": [ diff --git a/conductor/traceability.json b/conductor/traceability.json index 19fcfab4..69591b71 100644 --- a/conductor/traceability.json +++ b/conductor/traceability.json @@ -151,6 +151,15 @@ "T11-03" ], "status": "planned" + }, + "P18": { + "phaseId": "P18", + "title": "Metadata/body de-duplication correctness", + "releaseTarget": "next patch", + "tracks": [ + "T18-01" + ], + "status": "contract_verified" } }, "tracks": { @@ -837,6 +846,22 @@ "requiredTaskCount": 4, "existingIssues": [], "issue": null + }, + "T18-01": { + "id": "T18-01", + "phaseId": "P18", + "title": "Metadata/body de-duplication correctness", + "status": "contract_verified", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "requiredTaskCount": 3, + "existingIssues": [ + 537 + ], + "issue": null } }, "tasks": { @@ -4597,6 +4622,63 @@ "GOV-PROMPT-RETIREMENT-006" ], "release": "1.0 and beyond" + }, + "T18-01-TASK-01": { + "id": "T18-01-TASK-01", + "title": "Add metadata-prefix regression coverage", + "trackId": "T18-01", + "phaseId": "P18", + "status": "contract_verified", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "issue": null, + "mustContractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "release": "next patch" + }, + "T18-01-TASK-02": { + "id": "T18-01-TASK-02", + "title": "Normalize exact title and subtitle prefixes", + "trackId": "T18-01", + "phaseId": "P18", + "status": "contract_verified", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "issue": null, + "mustContractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "release": "next patch" + }, + "T18-01-TASK-03": { + "id": "T18-01-TASK-03", + "title": "Correct examples and agent authoring guidance", + "trackId": "T18-01", + "phaseId": "P18", + "status": "contract_verified", + "contractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "issue": null, + "mustContractIds": [ + "PARSER-AST-006", + "FEATURE-FIDELITY-008", + "UX-CLI-CONTRACT-005" + ], + "release": "next patch" } }, "contractMap": {}, diff --git a/conductor/tracks.md b/conductor/tracks.md index 59e92f93..58c788eb 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -87,3 +87,6 @@ Canonical status is evidence-gated by [programme-state.json](./programme-state.j ## P17 Supplemental phase - [x] **T17-01** ([#498](https://github.com/edithatogo/substack-cli-ts/issues/498)) ([evidence](./archive/T17-01/index.md)) + +## P18 Metadata/body de-duplication correctness +- [x] **T18-01 Metadata/body de-duplication correctness** ([#537](https://github.com/edithatogo/substack-cli-ts/issues/537)) ([evidence](./archive/T18-01/index.md)) diff --git a/docs/examples/content-features.md b/docs/examples/content-features.md index 8fa74403..63fa9faf 100644 --- a/docs/examples/content-features.md +++ b/docs/examples/content-features.md @@ -16,6 +16,12 @@ section: my-section-slug --- ``` +The body starts after the closing delimiter. Do not repeat `title` or `subtitle` +as the first body heading or paragraph. This is the canonical format for both +human-authored files and files generated by agents. Exact leading repetitions +are removed defensively during preparation; non-matching headings and standfirsts +remain body content. + | Field | Required | Description | |-------|----------|-------------| | `title` | yes | Post title displayed in the header | @@ -173,8 +179,6 @@ tags: [demo, features, markdown] audience: everyone --- -# Welcome - This is a **demonstration** of everything substack-cli supports. ## Text diff --git a/examples/basic.md b/examples/basic.md index d9b6e478..70b0fb69 100644 --- a/examples/basic.md +++ b/examples/basic.md @@ -5,8 +5,6 @@ tags: [example, markdown] audience: everyone --- -# Example Substack Draft - This is a local Markdown draft with **bold text** and a [link](https://substack.com). {{paywall}} diff --git a/src/publish/prepare.test.ts b/src/publish/prepare.test.ts index b69e4a1a..78c413fe 100644 --- a/src/publish/prepare.test.ts +++ b/src/publish/prepare.test.ts @@ -85,6 +85,129 @@ Body. } }); + it("removes matching leading title and subtitle blocks", async () => { + const file = await writeTempMarkdown(`--- +title: Same Title +subtitle: Separate subtitle +--- +# Same Title + +Separate subtitle + +Body. +`); + + try { + const prepared = await preparePost(file); + + assert.equal(prepared.post.markdown, "Body.\n"); + assert.doesNotMatch(prepared.post.html, /Same Title|Separate subtitle/); + assert.equal(prepared.post.document.content?.length, 1); + assert.equal(prepared.post.document.content?.[0]?.content?.[0]?.text, "Body."); + } finally { + await cleanup(file); + } + }); + + it("removes a formatted subtitle that exactly matches its rendered metadata", async () => { + const file = await writeTempMarkdown(`--- +title: Post Title +subtitle: Formatted subtitle +--- +**Formatted subtitle** + +Body. +`); + + try { + const prepared = await preparePost(file); + + assert.equal(prepared.post.markdown, "Body.\n"); + assert.equal(prepared.post.document.content?.[0]?.content?.[0]?.text, "Body."); + } finally { + await cleanup(file); + } + }); + + it("removes a formatted paragraph title when explicit title metadata matches", async () => { + const file = await writeTempMarkdown(`--- +title: Formatted title +subtitle: Header subtitle +--- +**Formatted title** + +Header subtitle + +Body. +`); + + try { + const prepared = await preparePost(file); + + assert.equal(prepared.post.markdown, "Body.\n"); + assert.equal(prepared.post.document.content?.[0]?.content?.[0]?.text, "Body."); + } finally { + await cleanup(file); + } + }); + + it("does not promote a leading paragraph to a missing title", async () => { + const file = await writeTempMarkdown(`Opening paragraph + +Body. +`); + + try { + const prepared = await preparePost(file); + + assert.equal(prepared.post.metadata.title, undefined); + assert.match(prepared.post.markdown, /^Opening paragraph/); + } finally { + await cleanup(file); + } + }); + + it("preserves leading body text that differs from the subtitle", async () => { + const file = await writeTempMarkdown(`--- +title: Post Title +subtitle: Header subtitle +--- +Editorial standfirst + +Body. +`); + + try { + const prepared = await preparePost(file); + + assert.match(prepared.post.markdown, /^Editorial standfirst/); + assert.equal(prepared.post.document.content?.[0]?.content?.[0]?.text, "Editorial standfirst"); + } finally { + await cleanup(file); + } + }); + + it("preserves a non-matching leading heading and matching text later in the body", async () => { + const file = await writeTempMarkdown(`--- +title: Post Title +subtitle: Header subtitle +--- +# Section heading + +Header subtitle +`); + + try { + const prepared = await preparePost(file); + + assert.equal(prepared.post.document.content?.[0]?.type, "heading"); + assert.match(prepared.post.markdown, /^# Section heading/); + assert.match(prepared.post.markdown, /Header subtitle/); + } finally { + await cleanup(file); + } + }); + it("preserves a leading heading that differs from the frontmatter title", async () => { const file = await writeTempMarkdown(`--- title: Post Title diff --git a/src/publish/prepare.ts b/src/publish/prepare.ts index ce096755..17b0204f 100644 --- a/src/publish/prepare.ts +++ b/src/publish/prepare.ts @@ -11,7 +11,7 @@ export async function preparePost( filePath: string, options: PreparePostOptions = {}, ): Promise { - const post = promoteLeadingTitle(await parseMarkdownFile(filePath)); + const post = normalizeLeadingMetadata(await parseMarkdownFile(filePath)); const mode = options.mode ?? "draft"; const scheduleAt = options.scheduleAt ?? post.metadata.scheduleAt; @@ -30,22 +30,52 @@ export async function preparePost( }; } -function promoteLeadingTitle(post: ParsedPost): ParsedPost { - const firstBlock = post.document.content?.[0]; - if (firstBlock?.type !== "heading") return post; - +function normalizeLeadingMetadata(post: ParsedPost): ParsedPost { const title = resolvePostTitle(post); - if (collectText(firstBlock).trim() !== title.trim()) return post; + let normalized = post; + const firstBlock = normalized.document.content?.[0]; + + const isExplicitTitleBlock = + Boolean(post.metadata.title) && + (firstBlock?.type === "heading" || firstBlock?.type === "paragraph"); + const isFallbackTitleBlock = !post.metadata.title && firstBlock?.type === "heading"; + if ( + (isExplicitTitleBlock || isFallbackTitleBlock) && + firstBlock && + collectText(firstBlock).trim() === title.trim() + ) { + normalized = removeLeadingBlock( + normalized, + post.metadata.title ? post.metadata : { ...post.metadata, title }, + ); + } + + const subtitle = normalized.metadata.subtitle?.trim(); + const nextBlock = normalized.document.content?.[0]; + if ( + subtitle && + (nextBlock?.type === "paragraph" || nextBlock?.type === "heading") && + collectText(nextBlock).trim() === subtitle + ) { + normalized = removeLeadingBlock(normalized, normalized.metadata); + } + + return normalized; +} + +function removeLeadingBlock(post: ParsedPost, metadata: ParsedPost["metadata"]): ParsedPost { + const firstBlock = post.document.content?.[0]; + if (!firstBlock) return post; return { ...post, - metadata: post.metadata.title ? post.metadata : { ...post.metadata, title }, - markdown: stripLeadingMarkdownHeading(post.markdown), - html: post.html.replace(/^\s*]*)?>[\s\S]*?<\/h[1-6]>\s*/i, ""), - document: { - ...post.document, - content: post.document.content?.slice(1), - }, + metadata, + markdown: + firstBlock.type === "heading" + ? stripLeadingMarkdownHeading(post.markdown) + : stripLeadingMarkdownParagraph(post.markdown), + html: post.html.replace(/^\s*<(h[1-6]|p)(?:\s[^>]*)?>[\s\S]*?<\/\1>\s*/i, ""), + document: { ...post.document, content: post.document.content?.slice(1) }, }; } @@ -65,3 +95,7 @@ function stripLeadingMarkdownHeading(markdown: string): string { "", ); } + +function stripLeadingMarkdownParagraph(markdown: string): string { + return markdown.replace(/^(?:[ \t]*\r?\n)*(?:[^\r\n]+(?:\r?\n(?![ \t]*\r?$))?)+(?:(?:\r?\n)?[ \t]*\r?\n)?/, ""); +} From 3d5a71c57ba578da73638f648bf35d83843db44e Mon Sep 17 00:00:00 2001 From: "Dylan Mordaunt (ISLHD)" Date: Tue, 11 Aug 2026 00:01:45 +1000 Subject: [PATCH 2/2] Fix T18-01 CI closeout --- biome.json | 2 +- src/publish/prepare.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/biome.json b/biome.json index 879e785f..537984fe 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.3/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.7/schema.json", "assist": { "actions": { "source": { "organizeImports": "off" } } }, "linter": { "enabled": true, diff --git a/src/publish/prepare.ts b/src/publish/prepare.ts index 17b0204f..c5759d3a 100644 --- a/src/publish/prepare.ts +++ b/src/publish/prepare.ts @@ -97,5 +97,8 @@ function stripLeadingMarkdownHeading(markdown: string): string { } function stripLeadingMarkdownParagraph(markdown: string): string { - return markdown.replace(/^(?:[ \t]*\r?\n)*(?:[^\r\n]+(?:\r?\n(?![ \t]*\r?$))?)+(?:(?:\r?\n)?[ \t]*\r?\n)?/, ""); + return markdown.replace( + /^(?:[ \t]*\r?\n)*(?:[^\r\n]+(?:\r?\n(?![ \t]*\r?$))?)+(?:(?:\r?\n)?[ \t]*\r?\n)?/, + "", + ); }