Skip to content

Commit f978a9f

Browse files
fix/site: Fix broken tables of contents (#1859)
Linear [FE-499: Fix doc site issues](https://linear.app/sourcegraph/issue/FE-499/fix-doc-site-issues) ## Problem The right-hand TOC (`headings` computed field in `contentlayer.config.ts`) strips fenced code blocks with the non-greedy regex `/```[\s\S]*?```/g`, then treats any remaining `#` line as a heading. Any inline triple-backtick run in prose is taken as a fence opener and flips every later fence pairing. `docs/batch-changes/batch-spec-yaml-reference.mdx` has two (`` `"true``` `` on line 376, `` `"*``` `` on line 709), so from there on the "inside/outside a fence" state is inverted: YAML `# comment` lines leak into the TOC as headings whose anchors don't exist, and real headings are dropped. Live repro: https://sourcegraph.com/docs/batch-changes/batch-spec-yaml-reference has a TOC entry linking to `#do-not-meddle-in-the-affairs-of-wizards-for-they-are-subtle-and-quick-to-anger` (line 572 of the MDX, a YAML comment inside a fence). No element with that id exists. This page alone accounted for 14 of the broken anchors found by lychee in the investigation behind #1858. ## Before / after TOC on `/batch-changes/batch-spec-yaml-reference`, local `next dev`, 1600px viewport. | Before (`main`) | After (this PR) | | --- | --- | | <img src="https://ampcode.com/user-content/artifacts/ee9ba290aa34b608e52fa272b2abfdc0cf6484f93120fa42b2c1c31441aab30f-file.png" width="400"> | <img src="https://ampcode.com/user-content/artifacts/96b96b558b53a28b6f65f786485818bb28dce5812a8c32995a1725242f1ad6dd-file.png" width="400"> | Before: 12 YAML comments (`if: is true, step always executes.`, `Mount a Python script and run the script`, `Do not meddle in the affairs of wizards…`) render as TOC entries with dead anchors, and every `changesetTemplate.*` heading except `.fork` is missing. After: the comments are gone and `steps.mount`, `importChangesets*`, `changesetTemplate*`, `Publishing only specific changesets` are back. ## Fix Walk the body line by line: a fence opens on a line starting with 3+ backticks or tildes and closes on a line of the same character at least as long, matching how the MDX renderer treats fences. Also handles the ```` ```` ```` four-backtick fences in `cody/troubleshooting.mdx` and `code-navigation/writing-an-indexer.mdx` that contain literal ```` ``` ```` text. ## Verification - Compared old vs new heading output across every `.mdx` under `docs/`: only `batch-spec-yaml-reference.mdx` changes — 12 bogus comment entries removed, 13 real headings restored. - `npx contentlayer build`: generated `Post` for that page has 66 headings, 0 bogus, all `changesetTemplate.*` ids present. - `npx tsc --noEmit` clean, `next lint` clean. Follow-up from the link-check work in #1858. Co-authored-by: Amp <amp@ampcode.com>
1 parent e5c8190 commit f978a9f

1 file changed

Lines changed: 30 additions & 8 deletions

File tree

contentlayer.config.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,35 @@ import {searchMetadata} from './src/data/search';
99
import GithubSlugger from 'github-slugger';
1010
import {visit} from 'unist-util-visit';
1111

12+
// A fence opener/closer is a run of 3+ backticks or tildes at the start of a line.
13+
const regXFenceLine = /^\s*(`{3,}|~{3,})/;
14+
15+
// Remove fenced code blocks so `# comment` lines inside them are not mistaken for
16+
// headings. Walks line by line: a naive /```[\s\S]*?```/ regex also matches inline
17+
// backtick runs in prose (e.g. `"true```), which flips every later fence pairing.
18+
function stripFencedCodeBlocks(markdown: string): string {
19+
const keptLines: string[] = [];
20+
let openFence: string | undefined;
21+
for (const line of markdown.split('\n')) {
22+
const fence = line.match(regXFenceLine)?.[1];
23+
if (openFence) {
24+
const closesOpenFence =
25+
fence !== undefined &&
26+
fence[0] === openFence[0] &&
27+
fence.length >= openFence.length &&
28+
line.trim() === fence;
29+
if (closesOpenFence) {
30+
openFence = undefined;
31+
}
32+
} else if (fence) {
33+
openFence = fence;
34+
} else {
35+
keptLines.push(line);
36+
}
37+
}
38+
return keptLines.join('\n');
39+
}
40+
1241
export const Post = defineDocumentType(() => ({
1342
name: 'Post',
1443
filePathPattern: `**/*.mdx`,
@@ -28,17 +57,10 @@ export const Post = defineDocumentType(() => ({
2857
type: 'json',
2958
resolve: async doc => {
3059
const regXHeader = /^ *(?<flag>#{1,6})\s+(?<content>.+)/gm;
31-
const regXCodeBlock = /```[\s\S]*?```/g;
3260
const slugger = new GithubSlugger();
3361

34-
// Ignore content within code blocks – No headings there
35-
const bodyWithoutCodeBlocks = doc.body.raw.replace(
36-
regXCodeBlock,
37-
''
38-
);
39-
4062
const headings = Array.from(
41-
bodyWithoutCodeBlocks.matchAll(regXHeader)
63+
stripFencedCodeBlocks(doc.body.raw).matchAll(regXHeader)
4264
).map(({groups}) => {
4365
const flag = groups?.flag;
4466
// Handles headings with links eg:

0 commit comments

Comments
 (0)