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
7 changes: 7 additions & 0 deletions .changeset/quick-apes-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@astrojs/starlight": patch
---

Fixes invalid `<head>` output when configuration is missing:
- Omits `<meta property="og:description" />` if Starlight’s `description` option is unset
- Omits `<link rel="canonical" />` and `<meta property="og:url" />` if Astro’s `site` option is unset
32 changes: 32 additions & 0 deletions packages/starlight/__tests__/basics/omit-canonical.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { expect, test, vi } from 'vitest';
import { getRouteDataTestContext } from '../test-utils';
import { generateRouteData } from '../../utils/routing/data';
import { routes } from '../../utils/routing';

vi.mock('astro:content', async () =>
(await import('../test-utils')).mockedAstroContent({
docs: [
['index.mdx', { title: 'Home Page' }],
[
'environmental-impact.md',
{
title: 'Eco-friendly docs',
description:
'Learn how Starlight can help you build greener documentation sites and reduce your carbon footprint.',
},
],
],
})
);

test('omits link canonical tag when site is not set', () => {
const route = routes[0]!;
const { head } = generateRouteData({
props: { ...route, headings: [] },
context: getRouteDataTestContext({ setSite: false }),
});

const canonicalExists = head.some((tag) => tag.tag === 'link' && tag.attrs?.rel === 'canonical');

expect(canonicalExists).toBe(false);
});
6 changes: 3 additions & 3 deletions packages/starlight/__tests__/basics/route-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,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' }] },
context: getRouteDataTestContext('/getting-started/'),
context: getRouteDataTestContext({ pathname: '/getting-started/' }),
});
expect(data.toc).toBeUndefined();
});
Expand All @@ -72,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' }] },
context: getRouteDataTestContext('/showcase/'),
context: getRouteDataTestContext({ pathname: '/showcase/' }),
});
expect(data.toc).toBeUndefined();
});
Expand All @@ -81,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' }] },
context: getRouteDataTestContext('/showcase/'),
context: getRouteDataTestContext({ pathname: '/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
Expand Up @@ -27,7 +27,7 @@ test('throws a validation error if a built-in field required by the user schema
await expect(() =>
generateStarlightPageRouteData({
props: starlightPageProps,
context: getRouteDataTestContext('/test-slug'),
context: getRouteDataTestContext({ pathname: '/test-slug' }),
})
).rejects.toThrowErrorMatchingInlineSnapshot(`
"[AstroUserError]:
Expand All @@ -49,7 +49,7 @@ test('returns new field defined in the user schema', async () => {
category,
},
},
context: getRouteDataTestContext('/test-slug'),
context: getRouteDataTestContext({ pathname: '/test-slug' }),
});
// @ts-expect-error - Custom field defined in the user schema.
expect(data.entry.data.category).toBe(category);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const starlightPagePathname = '/test-slug';
test('adds data to route shape', async () => {
const data = await generateStarlightPageRouteData({
props: starlightPageProps,
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({pathname: starlightPagePathname}),
});
// Starlight pages infer the slug from the URL.
expect(data.slug).toBe('test-slug');
Expand Down Expand Up @@ -70,7 +70,7 @@ test('adds custom data to route shape', async () => {
};
const data = await generateStarlightPageRouteData({
props,
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
});
expect(data.hasSidebar).toBe(props.hasSidebar);
expect(data.entryMeta.dir).toBe(props.dir);
Expand All @@ -91,7 +91,7 @@ test('adds custom frontmatter data to route shape', async () => {
};
const data = await generateStarlightPageRouteData({
props,
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
});
expect(data.entry.data.head).toMatchInlineSnapshot(`
[
Expand All @@ -112,7 +112,7 @@ test('adds custom frontmatter data to route shape', async () => {
test('uses generated sidebar when no sidebar is provided', async () => {
const data = await generateStarlightPageRouteData({
props: starlightPageProps,
context: getRouteDataTestContext('/getting-started/'),
context: getRouteDataTestContext({ pathname: '/getting-started/'}),
});
expect(data.sidebar).toMatchInlineSnapshot(`
[
Expand Down Expand Up @@ -197,7 +197,7 @@ test('uses provided sidebar if any', async () => {
'reference/frontmatter',
],
},
context: getRouteDataTestContext('/test/2'),
context: getRouteDataTestContext({ pathname: '/test/2' }),
});
expect(data.sidebar).toMatchInlineSnapshot(`
[
Expand Down Expand Up @@ -271,7 +271,7 @@ test('throws error if sidebar is malformated', async () => {
},
],
},
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
})
).rejects.toThrowErrorMatchingInlineSnapshot(`
"[AstroUserError]:
Expand Down Expand Up @@ -299,7 +299,7 @@ test('uses provided pagination if any', async () => {
},
},
},
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
});
expect(data.pagination).toMatchInlineSnapshot(`
{
Expand Down Expand Up @@ -330,7 +330,7 @@ test('uses provided headings if any', async () => {
];
const data = await generateStarlightPageRouteData({
props: { ...starlightPageProps, headings },
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
});
expect(data.headings).toEqual(headings);
});
Expand All @@ -346,7 +346,7 @@ test('generates the table of contents for provided headings', async () => {
{ depth: 4, slug: 'heading-3', text: 'Heading 3' },
],
},
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
});
expect(data.toc).toMatchInlineSnapshot(`
{
Expand Down Expand Up @@ -395,7 +395,7 @@ test('respects the `tableOfContents` level configuration', async () => {
},
},
},
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
});
expect(data.toc).toMatchInlineSnapshot(`
{
Expand Down Expand Up @@ -440,7 +440,7 @@ test('disables table of contents if frontmatter includes `tableOfContents: false
tableOfContents: false,
},
},
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname}),
});
expect(data.toc).toBeUndefined();
});
Expand All @@ -458,7 +458,7 @@ test('disables table of contents for splash template', async () => {
template: 'splash',
},
},
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
});
expect(data.toc).toBeUndefined();
});
Expand All @@ -473,7 +473,7 @@ test('hides the sidebar if the `hasSidebar` option is not specified and the spla
template: 'splash',
},
},
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
});
expect(data.hasSidebar).toBe(false);
});
Expand All @@ -488,7 +488,7 @@ test('uses provided edit URL if any', async () => {
editUrl,
},
},
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
});
expect(data.editUrl).toEqual(new URL(editUrl));
expect(data.entry.data.editUrl).toEqual(editUrl);
Expand All @@ -504,14 +504,14 @@ test('strips unknown frontmatter properties', async () => {
unknown: 'test',
},
},
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
});
expect('unknown' in data.entry.data).toBe(false);
});

test('generates data with a similar root shape to regular route data', async () => {
const route = routes[0]!;
const context = getRouteDataTestContext(starlightPagePathname);
const context = getRouteDataTestContext({ pathname: starlightPagePathname });
const data = generateRouteData({
props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
context,
Expand Down Expand Up @@ -542,7 +542,7 @@ test('parses an ImageMetadata object successfully', async () => {
},
},
},
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
});
expect(data.entry.data.hero?.image).toBeDefined();
// @ts-expect-error — image’s type can be different shapes but we know it’s this one here
Expand All @@ -569,7 +569,7 @@ test('parses an image that is also a function successfully', async () => {
},
},
},
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
});
expect(data.entry.data.hero?.image).toBeDefined();
// @ts-expect-error — image’s type can be different shapes but we know it’s this one here
Expand Down Expand Up @@ -599,7 +599,7 @@ test('fails to parse an image without the expected metadata properties', async (
},
},
},
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
})
).rejects.toThrowErrorMatchingInlineSnapshot(`
"[AstroUserError]:
Expand All @@ -617,7 +617,7 @@ test('adds data to route shape when the `docs` collection is not defined', async

const data = await generateStarlightPageRouteData({
props: starlightPageProps,
context: getRouteDataTestContext(starlightPagePathname),
context: getRouteDataTestContext({ pathname: starlightPagePathname }),
});
expect(data.entry.data.title).toBe(starlightPageProps.frontmatter.title);

Expand Down
41 changes: 30 additions & 11 deletions packages/starlight/__tests__/head/head.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getRouteDataTestContext } from '../test-utils';
import { generateRouteData } from '../../utils/routing/data';
import { routes } from '../../utils/routing';
import type { HeadConfig } from '../../schemas/head';
import { type Route } from '../../utils/routing/types';

vi.mock('astro:content', async () =>
(await import('../test-utils')).mockedAstroContent({
Expand Down Expand Up @@ -44,7 +45,7 @@ test('includes description based on Starlight `description` configuration', () =
});

test('includes description based on page `description` frontmatter field if provided', () => {
const head = getTestHead([], routes[1]);
const head = getTestHead({ heads: [], route: routes[1] });
expect(head).toContainEqual({
tag: 'meta',
attrs: {
Expand All @@ -67,7 +68,7 @@ test('includes `twitter:site` based on Starlight `social` configuration', () =>
});

test('merges two <title> tags', () => {
const head = getTestHead([{ tag: 'title', content: 'Override' }]);
const head = getTestHead({ heads: [{ tag: 'title', content: 'Override' }] });
expect(head.filter((tag) => tag.tag === 'title')).toEqual([
{ tag: 'title', content: 'Override' },
]);
Expand All @@ -78,7 +79,7 @@ test('merges two <link rel="canonical" href="" /> tags', () => {
tag: 'link',
attrs: { rel: 'canonical', href: 'https://astro.build' },
} as const;
const head = getTestHead([customLink]);
const head = getTestHead({ heads: [customLink] });
expect(head.filter((tag) => tag.tag === 'link' && tag.attrs?.rel === 'canonical')).toEqual([
customLink,
]);
Expand All @@ -89,7 +90,7 @@ test('merges two <link rel="sitemap" href="" /> tags', () => {
tag: 'link',
attrs: { rel: 'sitemap', href: '/sitemap-custom.xml' },
} as const;
const head = getTestHead([customLink]);
const head = getTestHead({ heads: [customLink] });
expect(head.filter((tag) => tag.tag === 'link' && tag.attrs?.rel === 'sitemap')).toEqual([
customLink,
]);
Expand All @@ -100,7 +101,7 @@ test('does not merge same link tags', () => {
tag: 'link',
attrs: { rel: 'stylesheet', href: 'secondary.css' },
} as const;
const head = getTestHead([customLink]);
const head = getTestHead({ heads: [customLink] });
expect(head.filter((tag) => tag.tag === 'link' && tag.attrs?.rel === 'stylesheet')).toEqual([
{ tag: 'link', attrs: { rel: 'stylesheet', href: 'primary.css' } },
customLink,
Expand All @@ -115,7 +116,7 @@ describe.each([['name'], ['property'], ['http-equiv']])(
tag: 'meta',
attrs: { [prop]: 'x', content: 'Test' },
} as const;
const head = getTestHead([customMeta]);
const head = getTestHead({ heads: [customMeta] });
expect(head.filter((tag) => tag.tag === 'meta' && tag.attrs?.[prop] === 'x')).toEqual([
customMeta,
]);
Expand All @@ -126,7 +127,7 @@ describe.each([['name'], ['property'], ['http-equiv']])(
tag: 'meta',
attrs: { [prop]: 'y', content: 'Test' },
} as const;
const head = getTestHead([customMeta]);
const head = getTestHead({ heads: [customMeta]});
expect(
head.filter(
(tag) => tag.tag === 'meta' && (tag.attrs?.[prop] === 'x' || tag.attrs?.[prop] === 'y')
Expand Down Expand Up @@ -169,7 +170,7 @@ test('sorts head by tag importance', () => {
});

test('places the default favicon below any user provided icons', () => {
const head = getTestHead([
const head = getTestHead({heads: [
{
tag: 'link',
attrs: {
Expand All @@ -178,7 +179,7 @@ test('places the default favicon below any user provided icons', () => {
sizes: '32x32',
},
},
]);
]});

const defaultFaviconIndex = head.findIndex(
(tag) => tag.tag === 'link' && tag.attrs?.rel === 'shortcut icon'
Expand All @@ -188,7 +189,22 @@ test('places the default favicon below any user provided icons', () => {
expect(defaultFaviconIndex).toBeGreaterThan(userFaviconIndex);
});

function getTestHead(heads: HeadConfig = [], route = routes[0]!): HeadConfig {
test('omits meta og:url tag when site is not set', () => {
const head = getTestHead({setSite: false});

const ogUrlExists = head.some((tag) => tag.tag === 'meta' && tag.attrs?.property === 'og:url');

expect(ogUrlExists).toBe(false);
});

type GetTestHeadOptions = {
heads?: HeadConfig,
route?: Route | undefined,
setSite?: boolean
}

function getTestHead({heads = [], route = routes[0]!, setSite }: GetTestHeadOptions = {}): HeadConfig {

return generateRouteData({
props: {
...route,
Expand All @@ -201,6 +217,9 @@ function getTestHead(heads: HeadConfig = [], route = routes[0]!): HeadConfig {
},
},
},
context: getRouteDataTestContext(),
context:
setSite === undefined
? getRouteDataTestContext()
: getRouteDataTestContext({ setSite: setSite }),
}).head;
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ test('fallback routes use fallback entry last updated dates', () => {
...route,
headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }],
},
context: getRouteDataTestContext('/en'),
context: getRouteDataTestContext({ pathname: '/en' }),
});

expect(getNewestCommitDate).toHaveBeenCalledOnce();
Expand Down
Loading