From 2c4e6085f21496f5d2fa8a7a87e1e80a9d145dd1 Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Thu, 6 Aug 2026 13:07:04 +0100 Subject: [PATCH] DOC-6939 Stop publishing HTML-commented content to the AI outputs Hugo runs with unsafe = true, so an HTML comment passes through to the rendered page and is invisible to readers, which is how authors park prose that should not be published yet. Nothing in the AI output path knew that, so commented text reached the JSON feed as real section content and the Markdown output as real body text. We were publishing to AI consumers exactly what we withhold from readers, which is worse than a formatting defect: an assistant can cite a feature as documented when we have deliberately not documented it. The case that surfaced it was develop/clients/observability, where a comment holds an entire "Tracing overview" section, kept because the explanation is good but the clients do not support tracing yet. The feed published it as a section with 1,610 characters of prose and the table of contents listed it, pointing at an anchor that does not exist because Hugo renders no heading for commented-out text. Across the corpus, 56 files carry HTML comments totalling about 22,000 characters. The important thing to know before touching the pattern: HTML comments legitimately appear inside code examples. A Maven snippet on the Lettuce pages carries " " mid-line, and stripping that would damage the sample a reader sees. Requiring the open delimiter to sit at the start of a line is what separates an author's parked block from a comment inside a code sample. Measured over the whole corpus, the line-anchored form removes 48 comments and about 19,200 characters and never matches inside a fenced code block, where the unanchored form matches 9 times. I nearly rejected the right pattern on a bad measurement, which is worth recording. My first safety test asked whether a match overlapped a fenced code block, and reported 8 overlaps even for the line-anchored form. Those 8 were comments that *contain* a code block, which is exactly what should be removed -- the opposite of the dangerous case, a comment sitting *inside* a code block. Asking the right question, whether the match is fully contained by a code span, gives 0 for the anchored form and 9 for the unanchored one. Verified by building before and after and comparing every page's content field: 5,706 pages byte-identical, 27 shrank, every one of the 27 explained by having a line-anchored comment, and none grew. No line-anchored comment now survives into either sections[] or the Markdown output. Duplicate section and example ids stay at zero and content_hash still verifies for all 5,687 content pages. Deliberately not fixed by editing the content. The comment is a legitimate authoring choice, and removing it would have fixed one page and left 55. Learned: HTML comments legitimately appear inside code examples, so comment stripping has to be line-anchored -- an unanchored pattern damages a Maven snippet on the Lettuce pages and 8 other sites Learned: a safety check asking whether a match "overlaps" a code block conflates two opposite situations, a comment that contains a code block against a comment inside one, and it wrongly condemned the correct pattern until the test asked about containment instead Constraint: the comment strip must keep its line-anchored open delimiter in both places, or it starts eating comments out of code samples Constraint: the strip is implemented twice, in layouts/partials/process-markdown-content.html and layouts/partials/toc-from-markdown.html, and must be changed together Gaps: inline comments, about 2,800 characters, still reach the outputs -- almost all of it is part of a code sample and belongs there; and a block comment written at column 0 inside a fenced code block would still be stripped, though none exists today Ticket: DOC-6939 Co-Authored-By: Claude Opus 5 (1M context) --- .../partials/process-markdown-content.html | 24 +++++++++++++++++++ layouts/partials/toc-from-markdown.html | 9 +++++++ 2 files changed, 33 insertions(+) diff --git a/layouts/partials/process-markdown-content.html b/layouts/partials/process-markdown-content.html index 5e47e1eef7..dd0a7297d9 100644 --- a/layouts/partials/process-markdown-content.html +++ b/layouts/partials/process-markdown-content.html @@ -16,6 +16,30 @@ {{- $content := .RawContent -}} {{- $visited := .Visited | default (slice) -}} +{{- /* Drop HTML-comment blocks before anything else looks at the content. + + Hugo is configured with unsafe = true, so a comment passes through to the rendered + HTML and is invisible to readers -- which is how authors park prose that should not + be published yet. Nothing downstream of here knew that, so the commented text was + reaching the JSON feed as real section content and the Markdown output as real body + text: we were publishing to AI consumers exactly what we withhold from readers. On + develop/clients/observability that was a whole "Tracing overview" section, kept + because the explanation is good but the clients do not support tracing yet. + + The open delimiter must be at the start of a line. That is what separates an author's + block comment from a comment inside a code example -- a Maven snippet carries + " " mid-line, and stripping that would + damage the sample. Measured over the whole corpus, the line-anchored form removes 48 + comments totalling about 19,200 characters and never matches inside a fenced code + block, where the unanchored form would have hit 9. The residual is inline comments, + which are small and usually part of a code sample anyway. + + Known limit: a block comment written at column 0 *inside* a fenced code block would + still be stripped. None exists today. Both the literal and entity-escaped forms are + matched, because RawContent arrives escaped in some contexts. */ -}} +{{- $content = $content | replaceRE "(?ms)^" "" -}} +{{- $content = $content | replaceRE "(?ms)^<!--.*?-->" "" -}} + {{- /* Expand embed-md shortcodes before other transforms so embedded content can be processed too. */ -}} {{- $content = partial "markdown-embed-md.html" (dict "RawContent" $content "Page" .Page "Visited" $visited) -}} {{- /* Split wide table-scrollable blocks for AI-facing Markdown output only. */ -}} diff --git a/layouts/partials/toc-from-markdown.html b/layouts/partials/toc-from-markdown.html index f59a6e6be5..45b7b5e79a 100644 --- a/layouts/partials/toc-from-markdown.html +++ b/layouts/partials/toc-from-markdown.html @@ -25,6 +25,15 @@ what counts as code and their ids stay in step. */ -}} {{- $content = $content | replaceRE "(?s)```.*?```" "" -}} +{{- /* Drop HTML-comment blocks too, so prose an author has parked does not appear in the + navigation. develop/clients/observability comments out a whole "Tracing overview" + section, which was published here as a real entry pointing at an anchor that does + not exist on the page, because Hugo renders no heading for commented-out text. + Line-anchored open delimiter, matching process-markdown-content.html -- change the + two together. Safe to run after the code strip above, which has already removed any + comment living inside a code sample. */ -}} +{{- $content = $content | replaceRE "(?ms)^" "" -}} + {{- /* Find all ## and ### headers in the raw markdown */ -}} {{- /* Pattern matches lines starting with ## or ### followed by space and title */ -}} {{- $headerPattern := `(?m)^(#{2,3}) +(.+)$` -}}