Skip to content

Commit 449c822

Browse files
HiDeooEveeifyeveflorian-lefebvreematipicoMoustaphaDev
authored
Prevent heading anchor links on non-Starlight content (#3181)
Co-authored-by: Eveeifyeve <[email protected]> Co-authored-by: florian-lefebvre <[email protected]> Co-authored-by: ematipico <[email protected]> Co-authored-by: MoustaphaDev <[email protected]>
1 parent be00286 commit 449c822

File tree

8 files changed

+99
-6
lines changed

8 files changed

+99
-6
lines changed

.changeset/pink-cameras-perform.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@astrojs/starlight': patch
3+
---
4+
5+
Fixes an issue where all headings in Markdown and MDX content were rendered with a [clickable anchor link](https://starlight.astro.build/reference/configuration/#headinglinks), even in non-Starlight pages.

packages/starlight/__e2e__/components.test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,21 @@ test.describe('anchor headings', () => {
391391

392392
expect(markdownHtml).toEqual(componentHtml);
393393
});
394+
395+
test('does not render headings anchor links for individual Markdown pages and entries not part of the `docs` collection', async ({
396+
getProdServer,
397+
page,
398+
}) => {
399+
const starlight = await getProdServer();
400+
401+
// Individual Markdown page
402+
await starlight.goto('/markdown-page');
403+
await expect(page.locator('.sl-anchor-link')).not.toBeAttached();
404+
405+
// Content entry from the `reviews` content collection
406+
await starlight.goto('/reviews/alice');
407+
await expect(page.locator('.sl-anchor-link')).not.toBeAttached();
408+
});
394409
});
395410

396411
test.describe('head propagation', () => {
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { defineCollection } from 'astro:content';
1+
import { defineCollection, z } from 'astro:content';
22
import { docsLoader } from '@astrojs/starlight/loaders';
33
import { docsSchema } from '@astrojs/starlight/schema';
4+
import { glob } from 'astro/loaders';
45

56
export const collections = {
67
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
8+
// A collection not handled by Starlight.
9+
reviews: defineCollection({
10+
loader: glob({ base: './src/content/reviews', pattern: `**/[^_]*.{md,mdx}` }),
11+
schema: z.object({ title: z.string() }),
12+
}),
713
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: Review from Alice
3+
---
4+
5+
# Review from Alice
6+
7+
This is a review from Alice.
8+
9+
## Description
10+
11+
This content collection entry is not part of the Starlight `docs` collection.
12+
It is used to test that anchor links for headings are not generated for non-docs collection entries.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
title: Individual Markdown Page
3+
---
4+
5+
# Individual Markdown Page
6+
7+
## Description
8+
9+
This page is an [individual Markdown page](https://docs.astro.build/en/guides/markdown-content/#individual-markdown-pages).
10+
It is used to test that anchor links for headings are not generated for individual Markdown pages.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
import { getEntry, render } from 'astro:content';
3+
4+
/**
5+
* This route is used to test that anchor links for headings are not generated for non-docs
6+
* collection entries.
7+
*/
8+
9+
export function getStaticPaths() {
10+
return [{ params: { review: 'alice' } }];
11+
}
12+
13+
// @ts-expect-error - we don't generate types for this test fixture before type-checking the entire
14+
// project.
15+
const entry = await getEntry('reviews', 'alice');
16+
if (!entry) throw new Error('Could not find Alice review entry.');
17+
18+
const { Content } = await render(entry);
19+
---
20+
21+
<Content />

packages/starlight/__tests__/remark-rehype/anchor-links.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ const processor = await createMarkdownProcessor({
3030
rehypePlugins: [
3131
...starlightAutolinkHeadings({
3232
starlightConfig,
33-
astroConfig: { experimental: { headingIdCompat: false } },
33+
astroConfig: {
34+
srcDir: astroConfig.srcDir,
35+
experimental: { headingIdCompat: false },
36+
},
3437
useTranslations,
3538
absolutePathToLang,
3639
}),

packages/starlight/integrations/heading-links.ts

+25-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { h } from 'hastscript';
66
import type { Transformer } from 'unified';
77
import { SKIP, visit } from 'unist-util-visit';
88
import type { HookParameters, StarlightConfig } from '../types';
9+
import { resolveCollectionPath } from '../utils/collection';
910

1011
const AnchorLinkIcon = h(
1112
'span',
@@ -24,10 +25,14 @@ const AnchorLinkIcon = h(
2425
* Add anchor links to headings.
2526
*/
2627
export default function rehypeAutolinkHeadings(
27-
useTranslationsForLang: HookParameters<'config:setup'>['useTranslations'],
28-
absolutePathToLang: HookParameters<'config:setup'>['absolutePathToLang']
28+
docsCollectionPath: string,
29+
useTranslationsForLang: AutolinkHeadingsOptions['useTranslations'],
30+
absolutePathToLang: AutolinkHeadingsOptions['absolutePathToLang']
2931
) {
3032
const transformer: Transformer<Root> = (tree, file) => {
33+
// If the document is not part of the Starlight docs collection, skip it.
34+
if (!normalizePath(file.path).startsWith(docsCollectionPath)) return;
35+
3136
const pageLang = absolutePathToLang(file.path);
3237
const t = useTranslationsForLang(pageLang);
3338

@@ -67,7 +72,9 @@ export default function rehypeAutolinkHeadings(
6772

6873
interface AutolinkHeadingsOptions {
6974
starlightConfig: Pick<StarlightConfig, 'markdown'>;
70-
astroConfig: { experimental: Pick<AstroConfig['experimental'], 'headingIdCompat'> };
75+
astroConfig: Pick<AstroConfig, 'srcDir'> & {
76+
experimental: Pick<AstroConfig['experimental'], 'headingIdCompat'>;
77+
};
7178
useTranslations: HookParameters<'config:setup'>['useTranslations'];
7279
absolutePathToLang: HookParameters<'config:setup'>['absolutePathToLang'];
7380
}
@@ -85,10 +92,24 @@ export const starlightAutolinkHeadings = ({
8592
rehypeHeadingIds,
8693
{ experimentalHeadingIdCompat: astroConfig.experimental?.headingIdCompat },
8794
],
88-
rehypeAutolinkHeadings(useTranslations, absolutePathToLang),
95+
rehypeAutolinkHeadings(
96+
normalizePath(resolveCollectionPath('docs', astroConfig.srcDir)),
97+
useTranslations,
98+
absolutePathToLang
99+
),
89100
]
90101
: [];
91102

103+
/**
104+
* File path separators seems to be inconsistent on Windows when the rehype plugin is used on
105+
* Markdown vs MDX files.
106+
* For the time being, we normalize the path to unix style path.
107+
*/
108+
const backSlashRegex = /\\/g;
109+
function normalizePath(path: string) {
110+
return path.replace(backSlashRegex, '/');
111+
}
112+
92113
// This utility is inlined from https://github.com/syntax-tree/hast-util-heading-rank
93114
// Copyright (c) 2020 Titus Wormer <[email protected]>
94115
// MIT License: https://github.com/syntax-tree/hast-util-heading-rank/blob/main/license

0 commit comments

Comments
 (0)