Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add head content to route data #2927

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
5 changes: 5 additions & 0 deletions .changeset/happy-pens-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/starlight': patch
---

Fixes an issue where overriding the [canonical URL](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#canonical) of a page using the [`head` configuration option](https://starlight.astro.build/reference/configuration/#head) or [`head` frontmatter field](https://starlight.astro.build/reference/frontmatter/#head) would strip any other `<link>` tags from the `<head>`.
5 changes: 5 additions & 0 deletions .changeset/happy-snails-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/starlight': patch
---

Fixes an issue where generated [canonical URLs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel#canonical) would include a trailing slash when using the [`trailingSlash` Astro option](https://docs.astro.build/en/reference/configuration-reference/#trailingslash) is set to `'never'`.
9 changes: 9 additions & 0 deletions .changeset/long-emus-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@astrojs/starlight': minor
---

Adds the [`head`](https://starlight.astro.build/reference/route-data/#head) route data property which contains an array of all tags to include in the `<head>` of the current page.

Previously, the [`<Head>`](https://starlight.astro.build/reference/overrides/#head-1) component was responsible for generating a list of tags to include in the `<head>` of the current page and rendering them.
This data is now available as `Astro.locals.starlightRoute.head` instead and can be modified using [route data middleware](https://starlight.astro.build/guides/route-data/#customizing-route-data).
The `<Head>` component now only renders the tags provided in `Astro.locals.starlightRoute.head`.
3 changes: 1 addition & 2 deletions docs/src/content/docs/reference/overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ They should only include [elements permitted inside `<head>`](https://developer.
**Default component:** [`Head.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Head.astro)

Component rendered inside each page’s `<head>`.
Includes important tags including `<title>`, and `<meta charset="utf-8">`.

Override this component as a last resort.
Prefer the [`head`](/reference/configuration/#head) option Starlight config if possible.
Prefer the [`head` config option](/reference/configuration/#head), the [`head` frontmatter field](/reference/frontmatter/#head), or a [route data middleware](/guides/route-data/#customizing-route-data) to customize the route data rendered by the default component if possible.

#### `ThemeProvider`

Expand Down
7 changes: 7 additions & 0 deletions docs/src/content/docs/reference/route-data.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ JavaScript `Date` object representing when this page was last updated if enabled

`URL` object for the address where this page can be edited if enabled.

### `head`

**type:** [`HeadConfig[]`](/reference/configuration/#headconfig)

Array of all tags to include in the `<head>` of the current page.
Includes important tags including `<title>`, and `<meta charset="utf-8">`.

## Utilities

### `defineRouteMiddleware()`
Expand Down
75 changes: 75 additions & 0 deletions packages/starlight/__tests__/basics/format-canonical.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, expect, test } from 'vitest';
import { formatCanonical, type FormatCanonicalOptions } from '../../utils/canonical';

describe.each<{
options: FormatCanonicalOptions;
tests: Array<{ href: string; expected: string }>;
}>([
{
options: { format: 'file', trailingSlash: 'ignore' },
tests: [
{ href: 'https://example.com/index.html', expected: 'https://example.com/index.html' },
{
href: 'https://example.com/reference/configuration.html',
expected: 'https://example.com/reference/configuration.html',
},
],
},
{
options: { format: 'file', trailingSlash: 'always' },
tests: [
{ href: 'https://example.com/index.html', expected: 'https://example.com/index.html' },
{
href: 'https://example.com/reference/configuration.html',
expected: 'https://example.com/reference/configuration.html',
},
],
},
{
options: { format: 'file', trailingSlash: 'never' },
tests: [
{ href: 'https://example.com/index.html', expected: 'https://example.com/index.html' },
{
href: 'https://example.com/reference/configuration.html',
expected: 'https://example.com/reference/configuration.html',
},
],
},
{
options: { format: 'directory', trailingSlash: 'always' },
tests: [
{ href: 'https://example.com/', expected: 'https://example.com/' },
{
href: 'https://example.com/reference/configuration/',
expected: 'https://example.com/reference/configuration/',
},
],
},
{
options: { format: 'directory', trailingSlash: 'never' },
tests: [
{ href: 'https://example.com/', expected: 'https://example.com' },
{
href: 'https://example.com/reference/configuration/',
expected: 'https://example.com/reference/configuration',
},
],
},
{
options: { format: 'directory', trailingSlash: 'ignore' },
tests: [
{ href: 'https://example.com/', expected: 'https://example.com/' },
{
href: 'https://example.com/reference/configuration/',
expected: 'https://example.com/reference/configuration/',
},
],
},
])(
'formatCanonical() with { format: $options.format, trailingSlash: $options.trailingSlash }',
({ options, tests }) => {
test.each(tests)('returns $expected for $href', async ({ href, expected }) => {
expect(formatCanonical(href, options)).toBe(expected);
});
}
);
115 changes: 0 additions & 115 deletions packages/starlight/__tests__/basics/head.test.ts

This file was deleted.

9 changes: 5 additions & 4 deletions packages/starlight/__tests__/basics/route-data.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test, vi } from 'vitest';
import { getRouteDataTestContext } from '../test-utils';
import { generateRouteData } from '../../utils/routing/data';
import { routes } from '../../utils/routing';

Expand All @@ -17,7 +18,7 @@ test('adds data to route shape', () => {
const route = routes[0]!;
const data = generateRouteData({
props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
url: new URL('https://example.com'),
context: getRouteDataTestContext(),
});
expect(data.hasSidebar).toBe(true);
expect(data).toHaveProperty('lastUpdated');
Expand Down Expand Up @@ -62,7 +63,7 @@ test('disables table of contents for splash template', () => {
const route = routes[1]!;
const data = generateRouteData({
props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
url: new URL('https://example.com/getting-started/'),
context: getRouteDataTestContext('/getting-started/'),
});
expect(data.toc).toBeUndefined();
});
Expand All @@ -71,7 +72,7 @@ test('disables table of contents if frontmatter includes `tableOfContents: false
const route = routes[2]!;
const data = generateRouteData({
props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
url: new URL('https://example.com/showcase/'),
context: getRouteDataTestContext('/showcase/'),
});
expect(data.toc).toBeUndefined();
});
Expand All @@ -80,7 +81,7 @@ test('uses explicit last updated date from frontmatter', () => {
const route = routes[3]!;
const data = generateRouteData({
props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
url: new URL('https://example.com/showcase/'),
context: getRouteDataTestContext('/showcase/'),
});
expect(data.lastUpdated).toBeInstanceOf(Date);
expect(data.lastUpdated).toEqual(route.entry.data.lastUpdated);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test, vi } from 'vitest';
import { getRouteDataTestContext } from '../test-utils';
import {
generateStarlightPageRouteData,
type StarlightPageProps,
Expand Down Expand Up @@ -26,7 +27,7 @@ test('throws a validation error if a built-in field required by the user schema
await expect(() =>
generateStarlightPageRouteData({
props: starlightPageProps,
url: new URL('https://example.com/test-slug'),
context: getRouteDataTestContext('/test-slug'),
})
).rejects.toThrowErrorMatchingInlineSnapshot(`
"[AstroUserError]:
Expand All @@ -48,7 +49,7 @@ test('returns new field defined in the user schema', async () => {
category,
},
},
url: new URL('https://example.com/test-slug'),
context: getRouteDataTestContext('/test-slug'),
});
// @ts-expect-error - Custom field defined in the user schema.
expect(data.entry.data.category).toBe(category);
Expand Down
Loading