summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Swithinbank2025-04-16 11:37:52 +0200
committerGitHub2025-04-16 11:37:52 +0200
commit8c19678e57c0270d3d80d4678f23a6fc287ebf12 (patch)
tree698fc83a5729254f34fe89bb358e13ed8abce922
parenteda22726299e8771be790444ea86a6dd7deab153 (diff)
downloadIT.starlight-8c19678e57c0270d3d80d4678f23a6fc287ebf12.tar.gz
IT.starlight-8c19678e57c0270d3d80d4678f23a6fc287ebf12.tar.bz2
IT.starlight-8c19678e57c0270d3d80d4678f23a6fc287ebf12.zip
Add built-in heading anchor link support (#3033)
Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com>
-rw-r--r--.changeset/late-onions-sparkle.md26
-rw-r--r--.changeset/quick-items-dream.md24
-rw-r--r--.prettierignore3
-rw-r--r--docs/src/content/docs/guides/authoring-content.mdx19
-rw-r--r--docs/src/content/docs/guides/pages.mdx59
-rw-r--r--docs/src/content/docs/reference/configuration.mdx23
-rw-r--r--package.json2
-rw-r--r--packages/markdoc/components.ts1
-rw-r--r--packages/markdoc/index.mjs24
-rw-r--r--packages/markdoc/package.json2
-rw-r--r--packages/starlight/__e2e__/components.test.ts19
-rw-r--r--packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading-component.mdx9
-rw-r--r--packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading.md7
-rw-r--r--packages/starlight/__tests__/basics/config-errors.test.ts3
-rw-r--r--packages/starlight/__tests__/remark-rehype/anchor-links.test.ts85
-rw-r--r--packages/starlight/__tests__/remark-rehype/snapshots/generates-anchor-link-markup.html1
-rw-r--r--packages/starlight/components/AnchorHeading.astro53
-rw-r--r--packages/starlight/components/Page.astro1
-rw-r--r--packages/starlight/index.ts12
-rw-r--r--packages/starlight/integrations/heading-links.ts104
-rw-r--r--packages/starlight/integrations/virtual-user-config.ts9
-rw-r--r--packages/starlight/package.json9
-rw-r--r--packages/starlight/schemas/i18n.ts2
-rw-r--r--packages/starlight/style/anchor-links.css125
-rw-r--r--packages/starlight/translations/ar.json3
-rw-r--r--packages/starlight/translations/ca.json3
-rw-r--r--packages/starlight/translations/cs.json3
-rw-r--r--packages/starlight/translations/da.json3
-rw-r--r--packages/starlight/translations/de.json3
-rw-r--r--packages/starlight/translations/en.json3
-rw-r--r--packages/starlight/translations/es.json3
-rw-r--r--packages/starlight/translations/fa.json3
-rw-r--r--packages/starlight/translations/fr.json3
-rw-r--r--packages/starlight/translations/gl.json3
-rw-r--r--packages/starlight/translations/he.json3
-rw-r--r--packages/starlight/translations/hi.json3
-rw-r--r--packages/starlight/translations/id.json3
-rw-r--r--packages/starlight/translations/it.json3
-rw-r--r--packages/starlight/translations/ja.json3
-rw-r--r--packages/starlight/translations/ko.json3
-rw-r--r--packages/starlight/translations/lv.json3
-rw-r--r--packages/starlight/translations/nb.json3
-rw-r--r--packages/starlight/translations/nl.json3
-rw-r--r--packages/starlight/translations/pl.json3
-rw-r--r--packages/starlight/translations/pt.json3
-rw-r--r--packages/starlight/translations/ro.json3
-rw-r--r--packages/starlight/translations/ru.json3
-rw-r--r--packages/starlight/translations/sk.json3
-rw-r--r--packages/starlight/translations/sv.json3
-rw-r--r--packages/starlight/translations/tr.json3
-rw-r--r--packages/starlight/translations/uk.json3
-rw-r--r--packages/starlight/translations/vi.json3
-rw-r--r--packages/starlight/translations/zh-CN.json3
-rw-r--r--packages/starlight/translations/zh-TW.json3
-rw-r--r--packages/starlight/utils/user-config.ts14
-rw-r--r--packages/starlight/virtual-internal.d.ts2
-rw-r--r--pnpm-lock.yaml9
57 files changed, 692 insertions, 45 deletions
diff --git a/.changeset/late-onions-sparkle.md b/.changeset/late-onions-sparkle.md
new file mode 100644
index 00000000..a70a0ef0
--- /dev/null
+++ b/.changeset/late-onions-sparkle.md
@@ -0,0 +1,26 @@
+---
+'@astrojs/starlight': minor
+---
+
+Adds support for generating clickable anchor links for headings.
+
+By default, Starlight now renders an anchor link beside headings in Markdown and MDX content. A new `<AnchorHeading>` component is available to achieve the same thing in custom pages built using `<StarlightPage>`.
+
+If you want to disable this new Markdown processing set the `markdown.headingLinks` option in your Starlight config to `false`:
+
+```js
+starlight({
+ title: 'My docs',
+ markdown: {
+ headingLinks: false,
+ },
+}),
+```
+
+⚠️ **BREAKING CHANGE:** The minimum supported version of Astro is now v5.5.0.
+
+Please update Starlight and Astro together:
+
+```sh
+npx @astrojs/upgrade
+```
diff --git a/.changeset/quick-items-dream.md b/.changeset/quick-items-dream.md
new file mode 100644
index 00000000..cb127773
--- /dev/null
+++ b/.changeset/quick-items-dream.md
@@ -0,0 +1,24 @@
+---
+'@astrojs/starlight-markdoc': minor
+---
+
+Adds support for generating clickable anchor links for headings.
+
+By default, the Starlight Markdoc preset now includes a default `heading` node, which renders an anchor link beside headings in your Markdoc content.
+
+If you want to disable this new feature, pass `headingLinks: false` in your Markdoc config:
+
+```js
+export default defineMarkdocConfig({
+ // Disable the default heading anchor link support
+ extends: [starlightMarkdoc({ headingLinks: false })],
+});
+```
+
+⚠️ **BREAKING CHANGE:** The minimum supported peer version of Starlight is now v0.34.0.
+
+Please update Starlight and the Starlight Markdoc preset together:
+
+```sh
+npx @astrojs/upgrade
+```
diff --git a/.prettierignore b/.prettierignore
index 79270814..5cbeb0c1 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -18,5 +18,8 @@ pnpm-lock.yaml
# https://github.com/withastro/prettier-plugin-astro/issues/337
packages/starlight/user-components/Tabs.astro
+# Prettier forces whitespace between elements we want to avoid for consistency with the rehype version
+packages/starlight/components/AnchorHeading.astro
+
# Malformed YAML file used for testing
packages/starlight/__tests__/i18n/malformed-yaml-src/content/i18n/*.yml
diff --git a/docs/src/content/docs/guides/authoring-content.mdx b/docs/src/content/docs/guides/authoring-content.mdx
index 15334b24..be27ca80 100644
--- a/docs/src/content/docs/guides/authoring-content.mdx
+++ b/docs/src/content/docs/guides/authoring-content.mdx
@@ -650,3 +650,22 @@ If you already have a Starlight site and want to add Markdoc, follow these steps
</Steps>
To learn more about the Markdoc syntax and features, see the [Markdoc documentation](https://markdoc.dev/docs/syntax) or the [Astro Markdoc integration guide](https://docs.astro.build/en/guides/integrations-guide/markdoc/).
+
+### Configuring the Markdoc preset
+
+The `starlightMarkdoc()` preset accepts the following configuration options:
+
+#### `headingLinks`
+
+**type:** `boolean`
+**default:** `true`
+
+Controls whether or not headings are rendered with a clickable anchor link.
+Equivalent to the [`markdown.headingLinks`](/reference/configuration/#markdown) option, which applies to Markdown and MDX files.
+
+```js "headingLinks: false"
+export default defineMarkdocConfig({
+ // Disable the default heading anchor link support
+ extends: [starlightMarkdoc({ headingLinks: false })],
+});
+```
diff --git a/docs/src/content/docs/guides/pages.mdx b/docs/src/content/docs/guides/pages.mdx
index 663bea68..5f9a2eca 100644
--- a/docs/src/content/docs/guides/pages.mdx
+++ b/docs/src/content/docs/guides/pages.mdx
@@ -3,6 +3,8 @@ title: Pages
description: Learn how to create and manage your documentation site’s pages with Starlight.
sidebar:
order: 1
+tableOfContents:
+ maxHeadingLevel: 4
---
Starlight generates your site’s HTML pages based on your content, with flexible options provided via Markdown frontmatter.
@@ -73,28 +75,49 @@ Read more in the [“Pages” guide in the Astro docs](https://docs.astro.build/
### Using Starlight’s design in custom pages
-To use the Starlight layout in custom pages, wrap your page content with the `<StarlightPage />` component.
+To use the Starlight layout in custom pages, wrap your page content with the [`<StarlightPage>` component](#starlightpage-component).
This can be helpful if you are generating content dynamically but still want to use Starlight’s design.
+To add anchor links to headings that match Starlight’s Markdown anchor link styles, you can use the [`<AnchorHeading>` component](#anchorheading-component) in your custom pages.
+
```astro
---
// src/pages/custom-page/example.astro
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
+import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro';
import CustomComponent from './CustomComponent.astro';
---
<StarlightPage frontmatter={{ title: 'My custom page' }}>
<p>This is a custom page with a custom component:</p>
<CustomComponent />
+
+ <AnchorHeading level="2" id="learn-more">Learn more</AnchorHeading>
+ <p>
+ <a href="https://starlight.astro.build/">Read more in the Starlight docs</a>
+ </p>
</StarlightPage>
```
-#### Props
+#### `<StarlightPage>` component
+
+The `<StarlightPage />` component renders a full page of content using Starlight’s layout and styles.
+
+```astro
+---
+import StarlightPage from '@astrojs/starlight/components/AnchorHeading.astro';
+---
+
+<StarlightPage frontmatter={{ title: 'My custom page' }}>
+ <!-- Custom page content -->
+</StarlightPage>
+```
The `<StarlightPage />` component accepts the following props.
-##### `frontmatter` (required)
+##### `frontmatter`
+**required**
**type:** `StarlightPageFrontmatter`
Set the [frontmatter properties](/reference/frontmatter/) for this page, similar to frontmatter in Markdown pages.
@@ -173,3 +196,33 @@ Set the BCP-47 language tag for this page’s content, e.g. `en`, `zh-CN`, or `p
**default:** `false`
Indicate if this page is using [fallback content](/guides/i18n/#fallback-content) because there is no translation for the current language.
+
+#### `<AnchorHeading>` component
+
+The `<AnchorHeading />` component renders an HTML heading element with a clickable anchor link that matches Starlight’s Markdown styles.
+
+```astro
+---
+import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro';
+---
+
+<AnchorHeading level="2" id="sub-heading">Sub heading</AnchorHeading>
+```
+
+It accepts the following props as well as any other valid [global HTML attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes).
+
+##### `level`
+
+**required**
+**type:** `1 | 2 | 3 | 4 | 5 | 6`
+
+The heading level to render.
+For example, `level="1"` would render an `<h1>` element.
+
+##### `id`
+
+**required**
+**type:** `string`
+
+The unique ID for this heading.
+This will be used as the `id` attribute of the rendered heading and the anchor icon will link to it.
diff --git a/docs/src/content/docs/reference/configuration.mdx b/docs/src/content/docs/reference/configuration.mdx
index dbdb277a..7166da59 100644
--- a/docs/src/content/docs/reference/configuration.mdx
+++ b/docs/src/content/docs/reference/configuration.mdx
@@ -372,6 +372,29 @@ starlight({
});
```
+### `markdown`
+
+**type:** `{ headingLinks?: boolean }`
+**default:** `{ headingLinks: true }`
+
+Configure Starlight’s Markdown processing.
+
+#### `headingLinks`
+
+**type:** `boolean`
+**default:** `true`
+
+Controls whether or not headings are rendered with a clickable anchor link.
+
+```js
+starlight({
+ markdown: {
+ // Disable Starlight’s clickable heading anchor links.
+ headingLinks: false,
+ },
+}),
+```
+
### `expressiveCode`
**type:** `StarlightExpressiveCodeOptions | boolean`
diff --git a/package.json b/package.json
index cf8d314d..1ff32e1d 100644
--- a/package.json
+++ b/package.json
@@ -42,7 +42,7 @@
"examples/basics/dist/_astro/*.css",
"!examples/basics/dist/_astro/print.*.css"
],
- "limit": "14.5 kB",
+ "limit": "14.75 kB",
"gzip": true
}
],
diff --git a/packages/markdoc/components.ts b/packages/markdoc/components.ts
index 62b21695..2eaa0b47 100644
--- a/packages/markdoc/components.ts
+++ b/packages/markdoc/components.ts
@@ -1 +1,2 @@
export { default as Code } from './Code.astro';
+export { default as Heading } from '@astrojs/starlight/components/AnchorHeading.astro';
diff --git a/packages/markdoc/index.mjs b/packages/markdoc/index.mjs
index a2ac12cf..82a472f4 100644
--- a/packages/markdoc/index.mjs
+++ b/packages/markdoc/index.mjs
@@ -1,4 +1,4 @@
-import { component } from '@astrojs/markdoc/config';
+import { component, nodes } from '@astrojs/markdoc/config';
import { WellKnownElementAttributes, WellKnownAnchorAttributes } from './html.mjs';
/**
@@ -287,7 +287,23 @@ export const StarlightMarkdocPreset = {
},
};
-/** @return {import('@astrojs/markdoc/config').AstroMarkdocConfig} */
-export default function starlightMarkdoc() {
- return StarlightMarkdocPreset;
+/**
+ * Markdoc preset that configures Starlight’s built-in components.
+ * @return {import('@astrojs/markdoc/config').AstroMarkdocConfig}
+ */
+export default function starlightMarkdoc({ headingLinks = true } = {}) {
+ return {
+ ...StarlightMarkdocPreset,
+ nodes: {
+ ...StarlightMarkdocPreset.nodes,
+ ...(headingLinks
+ ? {
+ heading: {
+ ...nodes.heading,
+ render: component('@astrojs/starlight-markdoc/components', 'Heading'),
+ },
+ }
+ : {}),
+ },
+ };
}
diff --git a/packages/markdoc/package.json b/packages/markdoc/package.json
index 3902ac97..962d354d 100644
--- a/packages/markdoc/package.json
+++ b/packages/markdoc/package.json
@@ -23,7 +23,7 @@
},
"peerDependencies": {
"@astrojs/markdoc": ">=0.12.1",
- "@astrojs/starlight": ">=0.30.0"
+ "@astrojs/starlight": ">=0.34.0"
},
"publishConfig": {
"provenance": true
diff --git a/packages/starlight/__e2e__/components.test.ts b/packages/starlight/__e2e__/components.test.ts
index b4021c47..6a1b7e5b 100644
--- a/packages/starlight/__e2e__/components.test.ts
+++ b/packages/starlight/__e2e__/components.test.ts
@@ -374,6 +374,25 @@ test.describe('whitespaces', () => {
});
});
+test.describe('anchor headings', () => {
+ test('renders the same content for Markdown headings and Astro component', async ({
+ getProdServer,
+ page,
+ }) => {
+ const starlight = await getProdServer();
+
+ await starlight.goto('/anchor-heading');
+ const markdownContent = page.locator('.sl-markdown-content');
+ const markdownHtml = await markdownContent.innerHTML();
+
+ await starlight.goto('/anchor-heading-component');
+ const componentContent = page.locator('.sl-markdown-content');
+ const componentHtml = await componentContent.innerHTML();
+
+ expect(markdownHtml).toEqual(componentHtml);
+ });
+});
+
async function expectSelectedTab(tabs: Locator, label: string, panel?: string) {
expect(
(
diff --git a/packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading-component.mdx b/packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading-component.mdx
new file mode 100644
index 00000000..245ce62c
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading-component.mdx
@@ -0,0 +1,9 @@
+---
+title: Anchor Headings Component
+---
+
+import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro';
+
+<AnchorHeading level="2" id="an-anchor-heading">An anchor heading</AnchorHeading>
+
+<AnchorHeading level="3" id="another-anchor-heading">Another anchor heading</AnchorHeading>
diff --git a/packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading.md b/packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading.md
new file mode 100644
index 00000000..ce659560
--- /dev/null
+++ b/packages/starlight/__e2e__/fixtures/basics/src/content/docs/anchor-heading.md
@@ -0,0 +1,7 @@
+---
+title: Anchor Headings
+---
+
+## An anchor heading
+
+### Another anchor heading
diff --git a/packages/starlight/__tests__/basics/config-errors.test.ts b/packages/starlight/__tests__/basics/config-errors.test.ts
index 6f9dad71..c64cdcc4 100644
--- a/packages/starlight/__tests__/basics/config-errors.test.ts
+++ b/packages/starlight/__tests__/basics/config-errors.test.ts
@@ -63,6 +63,9 @@ test('parses bare minimum valid config successfully', () => {
"isUsingBuiltInDefaultLocale": true,
"lastUpdated": false,
"locales": undefined,
+ "markdown": {
+ "headingLinks": true,
+ },
"pagefind": {
"ranking": {
"pageLength": 0.1,
diff --git a/packages/starlight/__tests__/remark-rehype/anchor-links.test.ts b/packages/starlight/__tests__/remark-rehype/anchor-links.test.ts
new file mode 100644
index 00000000..f7b8bb5c
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/anchor-links.test.ts
@@ -0,0 +1,85 @@
+import { createMarkdownProcessor, type MarkdownProcessor } from '@astrojs/markdown-remark';
+import { expect, test } from 'vitest';
+import { createTranslationSystemFromFs } from '../../utils/translations-fs';
+import { StarlightConfigSchema, type StarlightUserConfig } from '../../utils/user-config';
+import { absolutePathToLang as getAbsolutePathFromLang } from '../../integrations/shared/absolutePathToLang';
+import { starlightAutolinkHeadings } from '../../integrations/heading-links';
+
+const starlightConfig = StarlightConfigSchema.parse({
+ title: 'Anchor Links Tests',
+ locales: { en: { label: 'English' }, fr: { label: 'French' } },
+ defaultLocale: 'en',
+} satisfies StarlightUserConfig);
+
+const astroConfig = {
+ root: new URL(import.meta.url),
+ srcDir: new URL('./_src/', import.meta.url),
+};
+
+const useTranslations = createTranslationSystemFromFs(
+ starlightConfig,
+ // Using non-existent `_src/` to ignore custom files in this test fixture.
+ { srcDir: new URL('./_src/', import.meta.url) }
+);
+
+function absolutePathToLang(path: string) {
+ return getAbsolutePathFromLang(path, { astroConfig, starlightConfig });
+}
+
+const processor = await createMarkdownProcessor({
+ rehypePlugins: [
+ ...starlightAutolinkHeadings({
+ starlightConfig,
+ astroConfig: { experimental: { headingIdCompat: false } },
+ useTranslations,
+ absolutePathToLang,
+ }),
+ ],
+});
+
+function renderMarkdown(
+ content: string,
+ options: { fileURL?: URL; processor?: MarkdownProcessor } = {}
+) {
+ return (options.processor ?? processor).render(
+ content,
+ // @ts-expect-error fileURL is part of MarkdownProcessor's options
+ { fileURL: options.fileURL ?? new URL(`./_src/content/docs/index.md`, import.meta.url) }
+ );
+}
+
+test('generates anchor link markup', async () => {
+ const res = await renderMarkdown(`
+## Some text
+`);
+ await expect(res.code).toMatchFileSnapshot('./snapshots/generates-anchor-link-markup.html');
+});
+
+test('generates an accessible link label', async () => {
+ const res = await renderMarkdown(`
+## Some text
+`);
+ expect(res.code).includes('<span class="sr-only">Section titled “Some text”</span>');
+});
+
+test('strips HTML markup in accessible link label', async () => {
+ const res = await renderMarkdown(`
+## Some _important nested \`HTML\`_
+`);
+ // Heading renders HTML
+ expect(res.code).includes('Some <em>important nested <code>HTML</code></em>');
+ // Visually hidden label renders plain text
+ expect(res.code).includes(
+ '<span class="sr-only">Section titled “Some important nested HTML”</span>'
+ );
+});
+
+test('localizes accessible label for the current language', async () => {
+ const res = await renderMarkdown(
+ `
+## Some text
+`,
+ { fileURL: new URL('./_src/content/docs/fr/index.md', import.meta.url) }
+ );
+ expect(res.code).includes('<span class="sr-only">Section intitulée « Some text »</span>');
+});
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/generates-anchor-link-markup.html b/packages/starlight/__tests__/remark-rehype/snapshots/generates-anchor-link-markup.html
new file mode 100644
index 00000000..7a3b3aae
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/generates-anchor-link-markup.html
@@ -0,0 +1 @@
+<div class="sl-heading-wrapper level-h2"><h2 id="some-text">Some text</h2><a class="sl-anchor-link" href="#some-text"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentcolor" d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"></path></svg></span><span class="sr-only">Section titled “Some text”</span></a></div> \ No newline at end of file
diff --git a/packages/starlight/components/AnchorHeading.astro b/packages/starlight/components/AnchorHeading.astro
new file mode 100644
index 00000000..c8b10402
--- /dev/null
+++ b/packages/starlight/components/AnchorHeading.astro
@@ -0,0 +1,53 @@
+---
+import type { HTMLAttributes } from 'astro/types';
+import { transform } from 'ultrahtml';
+import sanitize from 'ultrahtml/transformers/sanitize';
+// These styles are included globally by default, but can be removed when `markdown.headingLinks` is
+// set to `false`. We import them here to ensure they are included if the component is used with the
+// global Markdown option disabled.
+import '../style/anchor-links.css';
+import { AstroError } from 'astro/errors';
+
+const headingLevels = [1, 2, 3, 4, 5, 6, '1', '2', '3', '4', '5', '6'] as const;
+interface Props extends HTMLAttributes<'h1'> {
+ level: 1 | 2 | 3 | 4 | 5 | 6 | `${1 | 2 | 3 | 4 | 5 | 6}`;
+ id: string;
+}
+
+const { level, id, ...attrs } = Astro.props;
+
+if (!id) {
+ throw new AstroError(
+ 'Missing `id` attribute passed to `<AnchorHeading>` component',
+ `The \`<AnchorHeading>\` component requires an \`id\` attribute, but received \`${typeof id === 'string' ? '""' : id}\`.`
+ );
+}
+if (!headingLevels.includes(level)) {
+ throw new AstroError(
+ 'Invalid `level` attribute passed to `<AnchorHeading>` component',
+ `The \`<AnchorHeading>\` component expects a \`level\` attribute of \`1 | 2 | 3 | 4 | 5 | 6\`, but received \`${level}\`.`
+ );
+}
+
+const HeadingElement = `h${level}` as const;
+const headingHTML = await Astro.slots.render('default');
+const headingString = await transform(headingHTML, [sanitize({ unblockElements: [] })]);
+const accessibleLabel = Astro.locals.t('heading.anchorLabel', {
+ title: headingString,
+ interpolation: { escapeValue: false },
+})
+---
+
+{/* The spacing in this component is a little awkward to ensure whitespace matches what the rehype plugin produces. */}
+<div class={`sl-heading-wrapper level-h${level}`}
+ ><HeadingElement {id} {...attrs} set:html={headingHTML} /><a class="sl-anchor-link" href={`#${id}`}
+ ><span aria-hidden="true" class="sl-anchor-icon"
+ ><svg width="16" height="16" viewBox="0 0 24 24"
+ ><path
+ fill="currentcolor"
+ d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z"
+ ></path></svg
+ ></span
+ ><span class="sr-only" set:text={accessibleLabel} /></a
+ ></div
+>
diff --git a/packages/starlight/components/Page.astro b/packages/starlight/components/Page.astro
index 0076e4b8..7edf4217 100644
--- a/packages/starlight/components/Page.astro
+++ b/packages/starlight/components/Page.astro
@@ -10,6 +10,7 @@ import '../style/props.css';
import '../style/reset.css';
import '../style/asides.css';
import '../style/util.css';
+import 'virtual:starlight/optional-css';
import Banner from 'virtual:starlight/components/Banner';
import ContentPanel from 'virtual:starlight/components/ContentPanel';
diff --git a/packages/starlight/index.ts b/packages/starlight/index.ts
index fb80b2a5..96cd63de 100644
--- a/packages/starlight/index.ts
+++ b/packages/starlight/index.ts
@@ -26,6 +26,7 @@ import {
} from './utils/plugins';
import { processI18nConfig } from './utils/i18n';
import type { StarlightConfig } from './types';
+import { starlightAutolinkHeadings } from './integrations/heading-links';
export default function StarlightIntegration(
userOpts: StarlightUserConfigWithPlugins
@@ -127,7 +128,16 @@ export default function StarlightIntegration(
absolutePathToLang,
}),
],
- rehypePlugins: [rehypeRtlCodeSupport()],
+ rehypePlugins: [
+ rehypeRtlCodeSupport(),
+ // Process headings and add anchor links.
+ ...starlightAutolinkHeadings({
+ starlightConfig,
+ astroConfig: config,
+ useTranslations,
+ absolutePathToLang,
+ }),
+ ],
},
scopedStyleStrategy: 'where',
// If not already configured, default to prefetching all links on hover.
diff --git a/packages/starlight/integrations/heading-links.ts b/packages/starlight/integrations/heading-links.ts
new file mode 100644
index 00000000..f5914486
--- /dev/null
+++ b/packages/starlight/integrations/heading-links.ts
@@ -0,0 +1,104 @@
+import { rehypeHeadingIds } from '@astrojs/markdown-remark';
+import type { AstroConfig, AstroUserConfig } from 'astro';
+import type { Nodes, Root } from 'hast';
+import { toString } from 'hast-util-to-string';
+import { h } from 'hastscript';
+import type { Transformer } from 'unified';
+import { SKIP, visit } from 'unist-util-visit';
+import type { HookParameters, StarlightConfig } from '../types';
+
+const AnchorLinkIcon = h(
+ 'span',
+ { ariaHidden: 'true', class: 'sl-anchor-icon' },
+ h(
+ 'svg',
+ { width: 16, height: 16, viewBox: '0 0 24 24' },
+ h('path', {
+ fill: 'currentcolor',
+ d: 'm12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z',
+ })
+ )
+);
+
+/**
+ * Add anchor links to headings.
+ */
+export default function rehypeAutolinkHeadings(
+ useTranslationsForLang: HookParameters<'config:setup'>['useTranslations'],
+ absolutePathToLang: HookParameters<'config:setup'>['absolutePathToLang']
+) {
+ const transformer: Transformer<Root> = (tree, file) => {
+ const pageLang = absolutePathToLang(file.path);
+ const t = useTranslationsForLang(pageLang);
+
+ visit(tree, 'element', function (node, index, parent) {
+ if (!headingRank(node) || !node.properties.id || typeof index !== 'number' || !parent) {
+ return;
+ }
+
+ const accessibleLabel = t('heading.anchorLabel', {
+ title: toString(node),
+ interpolation: { escapeValue: false },
+ });
+
+ // Wrap the heading in a div and append the anchor link.
+ parent.children[index] = h(
+ 'div',
+ { class: `sl-heading-wrapper level-${node.tagName}` },
+ // Heading
+ node,
+ // Anchor link
+ {
+ type: 'element',
+ tagName: 'a',
+ properties: { class: 'sl-anchor-link', href: '#' + node.properties.id },
+ children: [AnchorLinkIcon, h('span', { class: 'sr-only' }, accessibleLabel)],
+ }
+ );
+
+ return SKIP;
+ });
+ };
+
+ return function attacher() {
+ return transformer;
+ };
+}
+
+interface AutolinkHeadingsOptions {
+ starlightConfig: Pick<StarlightConfig, 'markdown'>;
+ astroConfig: { experimental: Pick<AstroConfig['experimental'], 'headingIdCompat'> };
+ useTranslations: HookParameters<'config:setup'>['useTranslations'];
+ absolutePathToLang: HookParameters<'config:setup'>['absolutePathToLang'];
+}
+type RehypePlugins = NonNullable<NonNullable<AstroUserConfig['markdown']>['rehypePlugins']>;
+
+export const starlightAutolinkHeadings = ({
+ starlightConfig,
+ astroConfig,
+ useTranslations,
+ absolutePathToLang,
+}: AutolinkHeadingsOptions): RehypePlugins =>
+ starlightConfig.markdown.headingLinks
+ ? [
+ [
+ rehypeHeadingIds,
+ { experimentalHeadingIdCompat: astroConfig.experimental?.headingIdCompat },
+ ],
+ rehypeAutolinkHeadings(useTranslations, absolutePathToLang),
+ ]
+ : [];
+
+// This utility is inlined from https://github.com/syntax-tree/hast-util-heading-rank
+// Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com>
+// MIT License: https://github.com/syntax-tree/hast-util-heading-rank/blob/main/license
+/**
+ * Get the rank (`1` to `6`) of headings (`h1` to `h6`).
+ * @param node Node to check.
+ * @returns Rank of the heading or `undefined` if not a heading.
+ */
+function headingRank(node: Nodes): number | undefined {
+ const name = node.type === 'element' ? node.tagName.toLowerCase() : '';
+ const code = name.length === 2 && name.charCodeAt(0) === 104 /* `h` */ ? name.charCodeAt(1) : 0;
+ return code > 48 /* `0` */ && code < 55 /* `7` */ ? code - 48 /* `0` */ : undefined;
+}
diff --git a/packages/starlight/integrations/virtual-user-config.ts b/packages/starlight/integrations/virtual-user-config.ts
index 22c86ccc..47685cbf 100644
--- a/packages/starlight/integrations/virtual-user-config.ts
+++ b/packages/starlight/integrations/virtual-user-config.ts
@@ -84,6 +84,15 @@ export function vitePluginStarlightUserConfig(
: `import { makeAPI } from ${resolveLocalPath('../utils/gitInlined.ts')};` +
`const api = makeAPI(${JSON.stringify(getAllNewestCommitDate(rootPath, docsPath))});`) +
'export const getNewestCommitDate = api.getNewestCommitDate;',
+ /**
+ * Module containing styles for features that can be toggled on or off such as heading anchor links.
+ */
+ 'virtual:starlight/optional-css': opts.markdown.headingLinks
+ ? `import ${resolveLocalPath('../style/anchor-links.css')};`
+ : '',
+ /**
+ * Module containing imports of user-specified custom CSS files.
+ */
'virtual:starlight/user-css': opts.customCss.map((id) => `import ${resolveId(id)};`).join(''),
'virtual:starlight/user-images': opts.logo
? 'src' in opts.logo
diff --git a/packages/starlight/package.json b/packages/starlight/package.json
index 2848606c..39544e83 100644
--- a/packages/starlight/package.json
+++ b/packages/starlight/package.json
@@ -109,6 +109,10 @@
"types": "./components/StarlightPage.astro.tsx",
"import": "./components/StarlightPage.astro"
},
+ "./components/AnchorHeading.astro": {
+ "types": "./components/AnchorHeading.astro.tsx",
+ "import": "./components/AnchorHeading.astro"
+ },
"./components/Footer.astro": {
"types": "./components/Footer.astro.tsx",
"import": "./components/Footer.astro"
@@ -179,10 +183,9 @@
"./style/markdown.css": "./style/markdown.css"
},
"peerDependencies": {
- "astro": "^5.1.5"
+ "astro": "^5.5.0"
},
"devDependencies": {
- "@astrojs/markdown-remark": "^6.3.1",
"@playwright/test": "^1.45.0",
"@types/node": "^18.16.19",
"@vitest/coverage-v8": "^3.0.5",
@@ -191,6 +194,7 @@
"vitest": "^3.0.5"
},
"dependencies": {
+ "@astrojs/markdown-remark": "^6.3.1",
"@astrojs/mdx": "^4.2.3",
"@astrojs/sitemap": "^3.3.0",
"@pagefind/default-ui": "^1.3.0",
@@ -213,6 +217,7 @@
"rehype": "^13.0.1",
"rehype-format": "^5.0.0",
"remark-directive": "^3.0.0",
+ "ultrahtml": "^1.6.0",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.2"
diff --git a/packages/starlight/schemas/i18n.ts b/packages/starlight/schemas/i18n.ts
index a4c7c8b2..62047d9e 100644
--- a/packages/starlight/schemas/i18n.ts
+++ b/packages/starlight/schemas/i18n.ts
@@ -158,6 +158,8 @@ function starlightI18nSchema() {
.describe(
'Label for the “Built with Starlight” badge optionally displayed in the site footer.'
),
+
+ 'heading.anchorLabel': z.string().describe('Label for anchor links in Markdown content.'),
})
.partial();
}
diff --git a/packages/starlight/style/anchor-links.css b/packages/starlight/style/anchor-links.css
new file mode 100644
index 00000000..04749443
--- /dev/null
+++ b/packages/starlight/style/anchor-links.css
@@ -0,0 +1,125 @@
+/*
+How does anchor link placement work?
+
+Because anchor links need to placed inline at the end of a heading, but are not children of the
+heading element itself, positioning them to behave in a desirable way is a tiny bit tricky. Here’s
+how we do it.
+
+1. We wrap the heading and anchor link in a div and make the heading element inline:
+ <div class="sl-heading-wrapper">
+ <h2>...</h2><a class="sl-anchor-link">...</a>
+ </a>
+
+2. We need to avoid the anchor link wrapping onto a new line by itself like this because it looks
+ broken:
+
+ Some heading text
+ ⛓
+
+3. To achieve this we add an area of padding to the end of the heading and move the link over this
+ padding using negative margin:
+
+ padding-inline-end creates space at the end of the line
+ ↓
+ Some heading text[ ]⛓
+
+ margin-inline-start then pulls the anchor link into that space
+ ↓
+ Some heading text[ ⛓ ]
+
+ This ensures that when the anchor link wraps, the final word in the heading will wrap with it.
+
+*/
+@layer starlight.content {
+ /* ======================================================
+ WRAPPER
+ ====================================================== */
+ .sl-markdown-content .sl-heading-wrapper {
+ /* The size of the SVG icon. */
+ --sl-anchor-icon-size: 0.8275em;
+ /* The horizontal space between the SVG icon and the end of the heading text. */
+ --sl-anchor-icon-gap: 0.25em;
+ /* The end of line space required to accommodate the anchor link. */
+ --sl-anchor-icon-space: calc(var(--sl-anchor-icon-size) + var(--sl-anchor-icon-gap));
+
+ line-height: var(--sl-line-height-headings);
+ }
+
+ /* We need to apply the same rule we use for heading spacing to the parent wrapper. */
+ .sl-markdown-content
+ :not(h1, h2, h3, h4, h5, h6, .sl-heading-wrapper)
+ + :is(.sl-heading-wrapper) {
+ margin-top: 1.5em;
+ }
+
+ /* These font sizes are set in `markdown.css` for heading elements, but we need them one level higher on the wrapper. */
+ .sl-markdown-content .sl-heading-wrapper.level-h1 {
+ font-size: var(--sl-text-h1);
+ }
+ .sl-markdown-content .sl-heading-wrapper.level-h2 {
+ font-size: var(--sl-text-h2);
+ }
+ .sl-markdown-content .sl-heading-wrapper.level-h3 {
+ font-size: var(--sl-text-h3);
+ }
+ .sl-markdown-content .sl-heading-wrapper.level-h4 {
+ font-size: var(--sl-text-h4);
+ }
+ .sl-markdown-content .sl-heading-wrapper.level-h5 {
+ font-size: var(--sl-text-h5);
+ }
+ .sl-markdown-content .sl-heading-wrapper.level-h6 {
+ font-size: var(--sl-text-h6);
+ }
+
+ /* ======================================================
+ HEADING
+ ====================================================== */
+ .sl-markdown-content .sl-heading-wrapper > :first-child {
+ display: inline;
+ /* Apply end-of-line padding to the heading element. */
+ padding-inline-end: var(--sl-anchor-icon-space);
+ }
+
+ /* ======================================================
+ LINK
+ ====================================================== */
+ .sl-markdown-content .sl-anchor-link {
+ position: relative;
+ /* Move the anchor link over the heading element’s end-of-line padding. */
+ margin-inline-start: calc(-1 * var(--sl-anchor-icon-size));
+ }
+
+ /* Increase clickable area for anchor links with a pseudo element that doesn’t impact layout. */
+ .sl-markdown-content .sl-anchor-link::after {
+ content: '';
+ position: absolute;
+ /* While most icon spacing is done with `em` to be relative to the heading font-size, increasing
+ the touch area is most important for smaller headings like h5/h6, so we use absolute units,
+ which have a diminishing impact at larger font-sizes. */
+ inset: -0.25rem -0.5rem;
+ }
+
+ /* Size and position the SVG icon inside the link. */
+ .sl-markdown-content .sl-anchor-icon > svg {
+ display: inline;
+ width: var(--sl-anchor-icon-size);
+ /* Center the link icon SVG vertically in the line. */
+ vertical-align: top;
+ transform: translateY(
+ calc((var(--sl-line-height-headings) * 1em - var(--sl-anchor-icon-size)) / 2)
+ );
+ }
+
+ /* On devices with hover capability, hide the anchor link icons and show only show them when focused
+ or when the heading is hovered. */
+ @media (hover: hover) {
+ .sl-markdown-content .sl-anchor-link {
+ opacity: 0;
+ }
+ .sl-markdown-content .sl-anchor-link:focus,
+ .sl-markdown-content .sl-heading-wrapper:hover .sl-anchor-link {
+ opacity: 1;
+ }
+ }
+}
diff --git a/packages/starlight/translations/ar.json b/packages/starlight/translations/ar.json
index 5e84413d..5d028817 100644
--- a/packages/starlight/translations/ar.json
+++ b/packages/starlight/translations/ar.json
@@ -25,5 +25,6 @@
"aside.caution": "تنبيه",
"aside.danger": "تحذير",
"fileTree.directory": "Directory",
- "builtWithStarlight.label": "طوِّر بواسطة Starlight"
+ "builtWithStarlight.label": "طوِّر بواسطة Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/ca.json b/packages/starlight/translations/ca.json
index 2b2d6f67..ebd3f2da 100644
--- a/packages/starlight/translations/ca.json
+++ b/packages/starlight/translations/ca.json
@@ -38,5 +38,6 @@
"pagefind.one_result": "[COUNT] resultat per a: [SEARCH_TERM]",
"pagefind.alt_search": "Cap resultat per a [SEARCH_TERM]. Mostrant resultats per a: [DIFFERENT_TERM]",
"pagefind.search_suggestion": "Cap resultat per a [SEARCH_TERM]. Prova alguna d’aquestes cerques:",
- "pagefind.searching": "Cercant [SEARCH_TERM]..."
+ "pagefind.searching": "Cercant [SEARCH_TERM]...",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/cs.json b/packages/starlight/translations/cs.json
index 7ec5014c..8b2e3210 100644
--- a/packages/starlight/translations/cs.json
+++ b/packages/starlight/translations/cs.json
@@ -38,5 +38,6 @@
"pagefind.one_result": "[COUNT] výsledek pro: [SEARCH_TERM]",
"pagefind.alt_search": "Žádné výsledky pro [SEARCH_TERM]. Namísto toho zobrazuji výsledky pro: [DIFFERENT_TERM]",
"pagefind.search_suggestion": "Žádný výsledek pro [SEARCH_TERM]. Zkus nějaké z těchto hledání:",
- "pagefind.searching": "Hledám [SEARCH_TERM]..."
+ "pagefind.searching": "Hledám [SEARCH_TERM]...",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/da.json b/packages/starlight/translations/da.json
index 7aeb4e48..6c8d9af1 100644
--- a/packages/starlight/translations/da.json
+++ b/packages/starlight/translations/da.json
@@ -25,5 +25,6 @@
"aside.caution": "Caution",
"aside.danger": "Danger",
"fileTree.directory": "Directory",
- "builtWithStarlight.label": "Built with Starlight"
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/de.json b/packages/starlight/translations/de.json
index 55d60eab..83f57a27 100644
--- a/packages/starlight/translations/de.json
+++ b/packages/starlight/translations/de.json
@@ -25,5 +25,6 @@
"aside.caution": "Achtung",
"aside.danger": "Gefahr",
"fileTree.directory": "Directory",
- "builtWithStarlight.label": "Erstellt mit Starlight"
+ "builtWithStarlight.label": "Erstellt mit Starlight",
+ "heading.anchorLabel": "Abschnitt betitelt „{{title}}“"
}
diff --git a/packages/starlight/translations/en.json b/packages/starlight/translations/en.json
index 186cbe1f..5e968f8c 100644
--- a/packages/starlight/translations/en.json
+++ b/packages/starlight/translations/en.json
@@ -25,5 +25,6 @@
"aside.caution": "Caution",
"aside.danger": "Danger",
"fileTree.directory": "Directory",
- "builtWithStarlight.label": "Built with Starlight"
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/es.json b/packages/starlight/translations/es.json
index 0bc851f3..ee807a6a 100644
--- a/packages/starlight/translations/es.json
+++ b/packages/starlight/translations/es.json
@@ -38,5 +38,6 @@
"pagefind.one_result": "[COUNT] resultado para: [SEARCH_TERM]",
"pagefind.alt_search": "Ningún resultado para [SEARCH_TERM]. Mostrando resultados para: [DIFFERENT_TERM]",
"pagefind.search_suggestion": "Ningún resultado para [SEARCH_TERM]. Prueba alguna de estas búsquedas:",
- "pagefind.searching": "Buscando [SEARCH_TERM]..."
+ "pagefind.searching": "Buscando [SEARCH_TERM]...",
+ "heading.anchorLabel": "Sección titulada «{{title}}»"
}
diff --git a/packages/starlight/translations/fa.json b/packages/starlight/translations/fa.json
index 2ce584e8..22db252d 100644
--- a/packages/starlight/translations/fa.json
+++ b/packages/starlight/translations/fa.json
@@ -25,5 +25,6 @@
"aside.caution": "احتیاط",
"aside.danger": "خطر",
"fileTree.directory": "فهرست",
- "builtWithStarlight.label": "Built with Starlight"
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/fr.json b/packages/starlight/translations/fr.json
index 3ab18641..25e6cea8 100644
--- a/packages/starlight/translations/fr.json
+++ b/packages/starlight/translations/fr.json
@@ -28,5 +28,6 @@
"expressiveCode.copyButtonTooltip": "Copier dans le presse-papiers",
"expressiveCode.terminalWindowFallbackTitle": "Fenêtre de terminal",
"fileTree.directory": "Répertoire",
- "builtWithStarlight.label": "Créé avec Starlight"
+ "builtWithStarlight.label": "Créé avec Starlight",
+ "heading.anchorLabel": "Section intitulée « {{title}} »"
}
diff --git a/packages/starlight/translations/gl.json b/packages/starlight/translations/gl.json
index c61d5990..13c1c861 100644
--- a/packages/starlight/translations/gl.json
+++ b/packages/starlight/translations/gl.json
@@ -25,5 +25,6 @@
"aside.caution": "Caution",
"aside.danger": "Danger",
"fileTree.directory": "Directory",
- "builtWithStarlight.label": "Built with Starlight"
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/he.json b/packages/starlight/translations/he.json
index 936ef3be..07281034 100644
--- a/packages/starlight/translations/he.json
+++ b/packages/starlight/translations/he.json
@@ -25,5 +25,6 @@
"aside.caution": "Caution",
"aside.danger": "Danger",
"fileTree.directory": "Directory",
- "builtWithStarlight.label": "Built with Starlight"
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/hi.json b/packages/starlight/translations/hi.json
index 7556b350..6cd11a0c 100644
--- a/packages/starlight/translations/hi.json
+++ b/packages/starlight/translations/hi.json
@@ -25,5 +25,6 @@
"aside.caution": "सावधानी",
"aside.danger": "खतरा",
"fileTree.directory": "Directory",
- "builtWithStarlight.label": "Starlight द्वारा निर्मित"
+ "builtWithStarlight.label": "Starlight द्वारा निर्मित",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/id.json b/packages/starlight/translations/id.json
index 88af6f39..b42d3f3b 100644
--- a/packages/starlight/translations/id.json
+++ b/packages/starlight/translations/id.json
@@ -25,5 +25,6 @@
"aside.caution": "Perhatian",
"aside.danger": "Bahaya",
"fileTree.directory": "Directory",
- "builtWithStarlight.label": "Built with Starlight"
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/it.json b/packages/starlight/translations/it.json
index 221c9bb1..65c32f15 100644
--- a/packages/starlight/translations/it.json
+++ b/packages/starlight/translations/it.json
@@ -25,5 +25,6 @@
"aside.caution": "Attenzione",
"aside.danger": "Pericolo",
"fileTree.directory": "Directory",
- "builtWithStarlight.label": "Built with Starlight"
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Sezione intitolata “{{title}}”"
}
diff --git a/packages/starlight/translations/ja.json b/packages/starlight/translations/ja.json
index 17a502b8..2e1fd806 100644
--- a/packages/starlight/translations/ja.json
+++ b/packages/starlight/translations/ja.json
@@ -25,5 +25,6 @@
"aside.caution": "注意",
"aside.danger": "危険",
"fileTree.directory": "ディレクトリ",
- "builtWithStarlight.label": "Starlightで作成"
+ "builtWithStarlight.label": "Starlightで作成",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/ko.json b/packages/starlight/translations/ko.json
index 7298c8f0..b5ff6bbd 100644
--- a/packages/starlight/translations/ko.json
+++ b/packages/starlight/translations/ko.json
@@ -25,5 +25,6 @@
"aside.caution": "주의",
"aside.danger": "위험",
"fileTree.directory": "디렉터리",
- "builtWithStarlight.label": "Starlight로 제작됨"
+ "builtWithStarlight.label": "Starlight로 제작됨",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/lv.json b/packages/starlight/translations/lv.json
index 4b8481e3..43190fc9 100644
--- a/packages/starlight/translations/lv.json
+++ b/packages/starlight/translations/lv.json
@@ -25,5 +25,6 @@
"aside.caution": "Uzmanību",
"aside.danger": "Bīstamība",
"fileTree.directory": "Direktorija",
- "builtWithStarlight.label": "Veidots ar Starlight"
+ "builtWithStarlight.label": "Veidots ar Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/nb.json b/packages/starlight/translations/nb.json
index 14099cda..892fc435 100644
--- a/packages/starlight/translations/nb.json
+++ b/packages/starlight/translations/nb.json
@@ -25,5 +25,6 @@
"aside.caution": "Advarsel",
"aside.danger": "Fare",
"fileTree.directory": "Mappe",
- "builtWithStarlight.label": "Laget med Starlight"
+ "builtWithStarlight.label": "Laget med Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/nl.json b/packages/starlight/translations/nl.json
index 37393933..e2a6cbe7 100644
--- a/packages/starlight/translations/nl.json
+++ b/packages/starlight/translations/nl.json
@@ -25,5 +25,6 @@
"aside.caution": "Opgepast",
"aside.danger": "Gevaar",
"fileTree.directory": "Directory",
- "builtWithStarlight.label": "Built with Starlight"
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/pl.json b/packages/starlight/translations/pl.json
index 157a9610..8a0c4759 100644
--- a/packages/starlight/translations/pl.json
+++ b/packages/starlight/translations/pl.json
@@ -28,5 +28,6 @@
"expressiveCode.copyButtonCopied": "Skopiowane!",
"expressiveCode.copyButtonTooltip": "Skopiuj do schowka",
"expressiveCode.terminalWindowFallbackTitle": "Okno terminala",
- "builtWithStarlight.label": "Built with Starlight"
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Dział zatytułowany „{{title}}”"
}
diff --git a/packages/starlight/translations/pt.json b/packages/starlight/translations/pt.json
index 8ce20a92..25b57b2f 100644
--- a/packages/starlight/translations/pt.json
+++ b/packages/starlight/translations/pt.json
@@ -25,5 +25,6 @@
"aside.caution": "Cuidado",
"aside.danger": "Perigo",
"fileTree.directory": "Directory",
- "builtWithStarlight.label": "Feito com Starlight"
+ "builtWithStarlight.label": "Feito com Starlight",
+ "heading.anchorLabel": "Seção intitulada “{{title}}”"
}
diff --git a/packages/starlight/translations/ro.json b/packages/starlight/translations/ro.json
index f9d4b334..e7083649 100644
--- a/packages/starlight/translations/ro.json
+++ b/packages/starlight/translations/ro.json
@@ -25,5 +25,6 @@
"aside.caution": "Atenție",
"aside.danger": "Pericol",
"fileTree.directory": "Directory",
- "builtWithStarlight.label": "Built with Starlight"
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/ru.json b/packages/starlight/translations/ru.json
index 967a807f..1d97ae79 100644
--- a/packages/starlight/translations/ru.json
+++ b/packages/starlight/translations/ru.json
@@ -28,5 +28,6 @@
"expressiveCode.copyButtonCopied": "Скопировано!",
"expressiveCode.copyButtonTooltip": "Копировать",
"expressiveCode.terminalWindowFallbackTitle": "Окно терминала",
- "builtWithStarlight.label": "Сделано с помощью Starlight"
+ "builtWithStarlight.label": "Сделано с помощью Starlight",
+ "heading.anchorLabel": "Заголовок раздела «{{title}}»"
}
diff --git a/packages/starlight/translations/sk.json b/packages/starlight/translations/sk.json
index f1f9e008..a54c83fb 100644
--- a/packages/starlight/translations/sk.json
+++ b/packages/starlight/translations/sk.json
@@ -25,5 +25,6 @@
"aside.caution": "Upozornenie",
"aside.danger": "Nebezpečenstvo",
"fileTree.directory": "Adresár",
- "builtWithStarlight.label": "Postavené so Starlight"
+ "builtWithStarlight.label": "Postavené so Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/sv.json b/packages/starlight/translations/sv.json
index d6b09ea8..a834ea96 100644
--- a/packages/starlight/translations/sv.json
+++ b/packages/starlight/translations/sv.json
@@ -25,5 +25,6 @@
"aside.caution": "Caution",
"aside.danger": "Danger",
"fileTree.directory": "Directory",
- "builtWithStarlight.label": "Built with Starlight"
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/tr.json b/packages/starlight/translations/tr.json
index 9b645a35..9ea48573 100644
--- a/packages/starlight/translations/tr.json
+++ b/packages/starlight/translations/tr.json
@@ -25,5 +25,6 @@
"aside.caution": "Dikkat",
"aside.danger": "Tehlike",
"fileTree.directory": "Dizin",
- "builtWithStarlight.label": "Built with Starlight"
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/uk.json b/packages/starlight/translations/uk.json
index eba7051d..5bcbd924 100644
--- a/packages/starlight/translations/uk.json
+++ b/packages/starlight/translations/uk.json
@@ -25,5 +25,6 @@
"aside.caution": "Обережно",
"aside.danger": "Небезпечно",
"fileTree.directory": "Каталог",
- "builtWithStarlight.label": "Створено з Starlight"
+ "builtWithStarlight.label": "Створено з Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/vi.json b/packages/starlight/translations/vi.json
index 3d544ac1..20de8148 100644
--- a/packages/starlight/translations/vi.json
+++ b/packages/starlight/translations/vi.json
@@ -25,5 +25,6 @@
"aside.caution": "Thận trọng",
"aside.danger": "Nguy hiểm",
"fileTree.directory": "Danh mục",
- "builtWithStarlight.label": "Tạo với Starlight"
+ "builtWithStarlight.label": "Tạo với Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/zh-CN.json b/packages/starlight/translations/zh-CN.json
index 90dd5c13..17c0f4e9 100644
--- a/packages/starlight/translations/zh-CN.json
+++ b/packages/starlight/translations/zh-CN.json
@@ -25,5 +25,6 @@
"aside.caution": "警告",
"aside.danger": "危险",
"fileTree.directory": "文件夹",
- "builtWithStarlight.label": "基于 Starlight 构建"
+ "builtWithStarlight.label": "基于 Starlight 构建",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/translations/zh-TW.json b/packages/starlight/translations/zh-TW.json
index e57658a6..001e1d77 100644
--- a/packages/starlight/translations/zh-TW.json
+++ b/packages/starlight/translations/zh-TW.json
@@ -25,5 +25,6 @@
"aside.caution": "警告",
"aside.danger": "危險",
"fileTree.directory": "目錄",
- "builtWithStarlight.label": "Built with Starlight"
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”"
}
diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts
index c1d40749..56955515 100644
--- a/packages/starlight/utils/user-config.ts
+++ b/packages/starlight/utils/user-config.ts
@@ -244,6 +244,20 @@ const UserConfigSchema = z.object({
}
})
.describe('Add middleware to process Starlight’s route data for each page.'),
+
+ /** Configure features that impact Starlight’s Markdown processing. */
+ markdown: z
+ .object({
+ /** Define whether headings in content should be rendered with clickable anchor links. Default: `true`. */
+ headingLinks: z
+ .boolean()
+ .default(true)
+ .describe(
+ 'Define whether headings in content should be rendered with clickable anchor links. Default: `true`.'
+ ),
+ })
+ .default({})
+ .describe('Configure features that impact Starlight’s Markdown processing.'),
});
export const StarlightConfigSchema = UserConfigSchema.strict()
diff --git a/packages/starlight/virtual-internal.d.ts b/packages/starlight/virtual-internal.d.ts
index 111760e9..3002b451 100644
--- a/packages/starlight/virtual-internal.d.ts
+++ b/packages/starlight/virtual-internal.d.ts
@@ -4,6 +4,8 @@ declare module 'virtual:starlight/git-info' {
declare module 'virtual:starlight/user-css' {}
+declare module 'virtual:starlight/optional-css' {}
+
declare module 'virtual:starlight/user-images' {
type ImageMetadata = import('astro').ImageMetadata;
export const logos: {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d1fd5525..671f572a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -164,6 +164,9 @@ importers:
packages/starlight:
dependencies:
+ '@astrojs/markdown-remark':
+ specifier: ^6.3.1
+ version: 6.3.1
'@astrojs/mdx':
specifier: ^4.2.3
version: 4.2.4(astro@5.6.2(@types/node@18.16.19)(jiti@2.4.2)(lightningcss@1.29.3)(rollup@4.36.0)(tsx@4.15.2)(typescript@5.6.3)(yaml@2.6.1))
@@ -230,6 +233,9 @@ importers:
remark-directive:
specifier: ^3.0.0
version: 3.0.0
+ ultrahtml:
+ specifier: ^1.6.0
+ version: 1.6.0
unified:
specifier: ^11.0.5
version: 11.0.5
@@ -240,9 +246,6 @@ importers:
specifier: ^6.0.2
version: 6.0.3
devDependencies:
- '@astrojs/markdown-remark':
- specifier: ^6.3.1
- version: 6.3.1
'@playwright/test':
specifier: ^1.45.0
version: 1.45.0