Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions .changeset/famous-grapes-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@astrojs/starlight': minor
---

Ensures that Starlight CSS layer order is predictable in custom pages using the `<StarlightPage>` component.

Previously, due to how [import order](https://docs.astro.build/en/guides/styling/#import-order) works in Astro, the `<StarlightPage>` component had to be the first import in custom pages to set up [cascade layers](https://starlight.astro.build/guides/css-and-tailwind/#cascade-layers) used internally by Starlight to manage the order of its styles.

With this change, this restriction no longer applies and Starlight’s styles will be applied correctly regardless of the import order of the `<StarlightPage>` component.
7 changes: 0 additions & 7 deletions docs/src/content/docs/guides/pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,13 @@ Read more in the [“Pages” guide in the Astro docs](https://docs.astro.build/

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.
This component must be the first import in your file to set up [cascade layers](/guides/css-and-tailwind/#cascade-layers) and ensure a predictable CSS order.

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 the `<StarlightPage>` component first to set up cascade layers.
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';

// Import any other components you want to use in your custom page.
import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro';
import CustomComponent from './CustomComponent.astro';
---
Expand All @@ -109,7 +105,6 @@ The `<StarlightPage />` component renders a full page of content using Starlight

```astro
---
// Import the `<StarlightPage>` component first to set up cascade layers.
import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
---

Expand All @@ -118,8 +113,6 @@ import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
</StarlightPage>
```

Due to how [import order](https://docs.astro.build/en/guides/styling/#import-order) works in Astro, the `<StarlightPage />` component must be the first import in your file to set up [cascade layers](/guides/css-and-tailwind/#cascade-layers) used internally by Starlight to manage the order of its styles.

The `<StarlightPage />` component accepts the following props.

##### `frontmatter`
Expand Down
26 changes: 26 additions & 0 deletions packages/starlight/__e2e__/basics.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fs from 'node:fs/promises';
import { expect, testFactory, type Locator } from './test-utils';

const test = testFactory('./fixtures/basics/');
Expand Down Expand Up @@ -467,6 +468,31 @@ test.describe('components', () => {
});
});

test.describe('css layer order', () => {
test('ensures that the StarlightPage component is always imported first to ensure a predictable CSS layer order in custom pages', async ({
page,
makeServer,
}) => {
const starlight = await makeServer('dev', { mode: 'dev' });
await starlight.goto('/starlight-page-css-layer-order');

const firstStyleContent = await page.evaluate(
() => document.head.querySelector('style')?.textContent ?? ''
);

const expectedLayersOrder = await fs.readFile(
new URL('../style/layers.css', import.meta.url),
'utf-8'
);

// Ensure that the first style block in the head contains the expected layers order rather
// the styles of the link button wrapped in a `@layer` block at-rule automatically declaring
// a new layer and thus potentially breaking the intended layers order as the initial order
// in which layers are declared indicates which layer has precedence.
expect(firstStyleContent).toBe(expectedLayersOrder);
});
});

async function expectSelectedTab(tabs: Locator, label: string, panel?: string) {
expect(
(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
import { LinkButton } from '@astrojs/starlight/components';

import AnchorHeading from '@astrojs/starlight/components/AnchorHeading.astro';

import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';

/**
* This page is used to test the CSS layer order in custom pages using the StarlightPage> component
* where we cannot ensure a correct import order of CSS.
* Note that the order of imports in this file is important and should not be changed.
*/
---

<StarlightPage frontmatter={{ title: 'A custom page' }}>
<AnchorHeading level="2" id="a-sub-heading">A Sub heading</AnchorHeading>

<p>Custom page content and a <a href="/tabs">link</a> to another page.</p>

<p>
<LinkButton href="/tabs">Tabs link button</LinkButton>
</p>
</StarlightPage>
2 changes: 2 additions & 0 deletions packages/starlight/__tests__/test-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getViteConfig } from 'astro/config';
import { vitePluginStarlightUserConfig } from '../integrations/virtual-user-config';
import { runPlugins, type StarlightUserConfigWithPlugins } from '../utils/plugins';
import { createTestPluginContext } from './test-plugin-utils';
import { vitePluginStarlightCssLayerOrder } from '../integrations/vite-layer-order';

const testLegacyCollections = process.env.LEGACY_COLLECTIONS === 'true';

Expand All @@ -29,6 +30,7 @@ export async function defineVitestConfig(
);
return getViteConfig({
plugins: [
vitePluginStarlightCssLayerOrder(),
vitePluginStarlightUserConfig(
command,
starlightConfig,
Expand Down
2 changes: 2 additions & 0 deletions packages/starlight/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { fileURLToPath } from 'node:url';
import { starlightAsides, starlightDirectivesRestorationIntegration } from './integrations/asides';
import { starlightExpressiveCode } from './integrations/expressive-code/index';
import { starlightSitemap } from './integrations/sitemap';
import { vitePluginStarlightCssLayerOrder } from './integrations/vite-layer-order';
import { vitePluginStarlightUserConfig } from './integrations/virtual-user-config';
import { rehypeRtlCodeSupport } from './integrations/code-rtl-support';
import {
Expand Down Expand Up @@ -118,6 +119,7 @@ export default function StarlightIntegration(
updateConfig({
vite: {
plugins: [
vitePluginStarlightCssLayerOrder(),
vitePluginStarlightUserConfig(command, starlightConfig, config, pluginTranslations),
],
},
Expand Down
66 changes: 66 additions & 0 deletions packages/starlight/integrations/vite-layer-order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { ViteUserConfig } from 'astro';
import MagicString from 'magic-string';

const starlightPageImportSource = '@astrojs/starlight/components/StarlightPage.astro';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m thinking to myself this will break if anyone ever aliases this, but never mind 😁


/**
* Vite plugin that ensures the StarlightPage component is always imported first when imported in
* an Astro file.
*
* This is necessary to ensure a predictable CSS layer order which is defined by the `<Page />`
* imported by the `<StarlightPage />` component. If a user imports any other component using
* cascade layers before the `<StarlightPage />` component, it will result in undesired layers
* being created before we explicitly set the expected layer order.
*/
export function vitePluginStarlightCssLayerOrder(): VitePlugin {
return {
name: 'vite-plugin-starlight-css-layer-order',
enforce: 'pre',
transform(code, id) {
if (
!id.endsWith('.astro') ||
id.endsWith(starlightPageImportSource) ||
code.indexOf('StarlightPage.astro') === -1
) {
return;
}

let ast: ReturnType<typeof this.parse>;

try {
ast = this.parse(code);
} catch {
return;
}

let hasStarlightPageImport = false;

for (const node of ast.body) {
if (node.type !== 'ImportDeclaration') continue;
if (node.source.value !== starlightPageImportSource) continue;

const importDefaultSpecifier = node.specifiers.find(
(specifier) => specifier.type === 'ImportDefaultSpecifier'
);
if (!importDefaultSpecifier) continue;

hasStarlightPageImport = true;
break;
}

if (!hasStarlightPageImport) return;

// Format path to unix style path.
const filename = id.replace(/\\/g, '/');
const ms = new MagicString(code, { filename });
ms.prepend(`import "${starlightPageImportSource}";\n`);

return {
code: ms.toString(),
map: ms.generateMap({ hires: 'boundary' }),
};
},
};
}

type VitePlugin = NonNullable<ViteUserConfig['plugins']>[number];
1 change: 1 addition & 0 deletions packages/starlight/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@
"i18next": "^23.11.5",
"js-yaml": "^4.1.0",
"klona": "^2.0.6",
"magic-string": "^0.30.17",
"mdast-util-directive": "^3.0.0",
"mdast-util-to-markdown": "^2.1.0",
"mdast-util-to-string": "^4.0.0",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.