Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions docs/src/content/docs/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,8 @@ starlight({

### `markdown`

**type:** `{ headingLinks?: boolean }`
**default:** `{ headingLinks: true }`
**type:** `{ headingLinks?: boolean; processedDirs?: string[] }`
**default:** `{ headingLinks: true, processedDirs: [] }`

Configure Starlight’s Markdown processing.

Expand All @@ -399,6 +399,28 @@ starlight({
}),
```

#### `processedDirs`

**type:** `string[]`
**default:** `[]`

Define additional directories where files should be processed by Starlight’s Markdown pipeline.
By default, only Markdown and MDX content loaded using Starlight's [`docsLoader()`](/reference/configuration/#docsloader) is processed.
Supports local directories relative to the root of your project, e.g. `'./src/data/comments/'`.

Such processing includes support for [clickable heading anchor links](#headinglinks), [asides Markdown directive syntax](/guides/authoring-content/#asides), and RTL support for code blocks.
This option can be useful if you are rendering content from a custom content collection in a [custom page](/guides/pages/#custom-pages) using the `<StarlightPage>` component and expect Starlight's Markdown processing to be applied to that content as well.

```js
starlight({
markdown: {
// Process Markdown files from the `reviews` content collection located in the
// `src/data/reviews/` directory.
processedDirs: ['./src/data/reviews/'],
},
}),
```

### `expressiveCode`

**type:** `StarlightExpressiveCodeOptions | boolean`
Expand Down
7 changes: 6 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ export default tseslint.config(
// or starting with `_`.
'@typescript-eslint/no-unused-vars': [
'error',
{ ignoreRestSiblings: true, destructuredArrayIgnorePattern: '^_', varsIgnorePattern: '^_' },
{
ignoreRestSiblings: true,
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
},
],
// Allow using `any` in rest parameter arrays, e.g. `(...args: any[]) => void`.
'@typescript-eslint/no-explicit-any': ['error', { ignoreRestArgs: true }],
Expand Down
34 changes: 33 additions & 1 deletion packages/starlight/__e2e__/basics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,17 @@ test.describe('components', () => {
await starlight.goto('/reviews/alice');
await expect(page.locator('.sl-anchor-link')).not.toBeAttached();
});

test('renders headings anchor links for entries not part of the `docs` collection matching the `markdown.processedDirs` option', async ({
getProdServer,
page,
}) => {
const starlight = await getProdServer();

// Content entry from the `comments` content collection
await starlight.goto('/comments/bob');
await expect(page.locator('.sl-anchor-link').first()).toBeAttached();
});
});

test.describe('asides', () => {
Expand All @@ -419,12 +430,22 @@ test.describe('components', () => {
// Individual Markdown page
await starlight.goto('/markdown-page');
await expect(page.locator('.starlight-aside')).not.toBeAttached();
await page.pause();

// Content entry from the `reviews` content collection
await starlight.goto('/reviews/alice');
await expect(page.locator('.starlight-aside')).not.toBeAttached();
});

test('renders Markdown asides for entries not part of the `docs` collection matching the `markdown.processedDirs` option', async ({
getProdServer,
page,
}) => {
const starlight = await getProdServer();

// Content entry from the `comments` content collection
await starlight.goto('/comments/bob');
await expect(page.locator('.starlight-aside')).toBeAttached();
});
});

test.describe('RTL support', () => {
Expand All @@ -442,6 +463,17 @@ test.describe('components', () => {
await starlight.goto('/reviews/alice');
await expect(page.locator('code[dir="auto"]')).not.toBeAttached();
});

test('adds RTL support to code and preformatted text elements for entries not part of the `docs` collection matching the `markdown.processedDirs` option', async ({
getProdServer,
page,
}) => {
const starlight = await getProdServer();

// Content entry from the `comments` content collection
await starlight.goto('/comments/bob');
await expect(page.locator('code[dir="auto"]').first()).toBeAttached();
});
});

test.describe('head propagation', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default defineConfig({
starlight({
title: 'Basics',
pagefind: false,
markdown: { processedDirs: ['./src/content/comments/'] },
}),
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import { glob } from 'astro/loaders';

export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
// A collection not handled by Starlight.
// Collections not handled by Starlight.
reviews: defineCollection({
loader: glob({ base: './src/content/reviews', pattern: `**/[^_]*.{md,mdx}` }),
schema: z.object({ title: z.string() }),
}),
comments: defineCollection({
loader: glob({ base: './src/content/comments', pattern: `**/[^_]*.{md,mdx}` }),
schema: z.object({ title: z.string() }),
}),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
title: Comment from Bob
---

# Comment from Bob

This is a comment from Bob.

## Description

This content collection entry is not part of the Starlight `docs` collection.
It is used to test that various remark/rehype plugins are transforming non-docs collection entries matching the `markdown.processedDirs` option.

:::note
This is a note using Starlight Markdown aside syntax.
:::

This is an `inline code` example.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
import { getEntry, render } from 'astro:content';

/**
* This route is used to test that remark/rehype plugins are transofming non-docs collection
* entries matching the `markdown.processedDirs` option.
*/

export function getStaticPaths() {
return [{ params: { comment: 'bob' } }];
}

// @ts-expect-error - we don't generate types for this test fixture before type-checking the entire
// project.
const entry = await getEntry('comments', 'bob');
if (!entry) throw new Error('Could not find Bob review entry.');

const { Content } = await render(entry);
---

<Content />
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import { getEntry, render } from 'astro:content';

/**
* This route is used to test that anchor links for headings are not generated for non-docs
* collection entries.
* This route is used to test that remark/rehype plugins are not transforming non-docs collection
* entries.
*/

export function getStaticPaths() {
Expand Down
1 change: 1 addition & 0 deletions packages/starlight/__tests__/basics/config-errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ test('parses bare minimum valid config successfully', () => {
"locales": undefined,
"markdown": {
"headingLinks": true,
"processedDirs": [],
},
"pagefind": {
"ranking": {
Expand Down
55 changes: 7 additions & 48 deletions packages/starlight/__tests__/remark-rehype/anchor-links.test.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,18 @@
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';
import { getCollectionPosixPath } from '../../utils/collection-fs';
import type { StarlightUserConfig } from '../../utils/user-config';
import { starlightRehypePlugins } from '../../integrations/remark-rehype';
import { createRemarkRehypePluginTestOptions } from './utils';

const starlightConfig = StarlightConfigSchema.parse({
const starlightConfig = {
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 = await 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, {
docsPath: getCollectionPosixPath('docs', astroConfig.srcDir),
starlightConfig,
});
}
} satisfies StarlightUserConfig;

const processor = await createMarkdownProcessor({
rehypePlugins: [
...starlightAutolinkHeadings({
starlightConfig,
astroConfig: {
srcDir: astroConfig.srcDir,
experimental: { headingIdCompat: false },
},
useTranslations,
absolutePathToLang,
}),
...starlightRehypePlugins(await createRemarkRehypePluginTestOptions(starlightConfig)),
],
});

Expand Down Expand Up @@ -74,7 +46,7 @@ test('strips HTML markup in accessible link label', async () => {
## Some _important nested \`HTML\`_
`);
// Heading renders HTML
expect(res.code).includes('Some <em>important nested <code>HTML</code></em>');
expect(res.code).includes('Some <em>important nested <code dir="auto">HTML</code></em>');
// Visually hidden label renders plain text
expect(res.code).includes(
'<span class="sr-only">Section titled “Some important nested HTML”</span>'
Expand All @@ -90,16 +62,3 @@ test('localizes accessible label for the current language', async () => {
);
expect(res.code).includes('<span class="sr-only">Section intitulée « Some text »</span>');
});

test('does not generate anchor links for documents without a file path', async () => {
const res = await processor.render(
`
## Some text
`,
// Rendering Markdown content using the content loader `renderMarkdown()` API does not provide
// a `fileURL` option.
{}
);

expect(res.code).not.includes('Section titled');
});
Loading