@astrojs/starlight 0.32.6 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,92 @@
1
1
  # @astrojs/starlight
2
2
 
3
+ ## 0.33.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#3088](https://github.com/withastro/starlight/pull/3088) [`1885049`](https://github.com/withastro/starlight/commit/18850491905fc1bf9e467b1d65c7f1709daf3c30) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes a regression in Starlight version `0.33.0` that caused the description and links to language alternates for multilingual websites to be missing from the` <head>` of the page.
8
+
9
+ - [#3065](https://github.com/withastro/starlight/pull/3065) [`463adf5`](https://github.com/withastro/starlight/commit/463adf53b263a963736cb441bc1dd515f3c81894) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Updates the `social` configuration option TSDoc example to match the shape of the expected value.
10
+
11
+ ## 0.33.0
12
+
13
+ ### Minor Changes
14
+
15
+ - [#3026](https://github.com/withastro/starlight/pull/3026) [`82deb84`](https://github.com/withastro/starlight/commit/82deb847418aedb9c01e05bb9de4b9bd10a1a885) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes a potential list styling issue if the last element of a list item is a `<script>` tag.
16
+
17
+ ⚠️ **BREAKING CHANGE:**
18
+
19
+ This release drops official support for Chromium-based browsers prior to version 105 (released 30 August 2022) and Firefox-based browsers prior to version 121 (released 19 December 2023). You can find a list of currently supported browsers and their versions using this [browserslist query](https://browsersl.ist/#q=%3E+0.5%25%2C+not+dead%2C+Chrome+%3E%3D+105%2C+Edge+%3E%3D+105%2C+Firefox+%3E%3D+121%2C+Safari+%3E%3D+15.4%2C+iOS+%3E%3D+15.4%2C+not+op_mini+all).
20
+
21
+ With this release, Starlight-generated sites will still work fine on those older browsers except for this small detail in list item styling, but future releases may introduce further breaking changes for impacted browsers, including in patch releases.
22
+
23
+ - [#3025](https://github.com/withastro/starlight/pull/3025) [`f87e9ac`](https://github.com/withastro/starlight/commit/f87e9acbf5090a31858c1cde568cc798140f1366) Thanks [@delucis](https://github.com/delucis)! - Makes `social` configuration more flexible.
24
+
25
+ ⚠️ **BREAKING CHANGE:** The `social` configuration option has changed syntax. You will need to update this in `astro.config.mjs` when upgrading.
26
+
27
+ Previously, a limited set of platforms were supported using a shorthand syntax with labels built in to Starlight. While convenient, this approach was less flexible and required dedicated code for each social platform added.
28
+
29
+ Now, you must specify the icon and label for each social link explicitly and you can use any of [Starlight’s built-in icons](https://starlight.astro.build/reference/icons/) for social links.
30
+
31
+ The following example shows updating the old `social` syntax to the new:
32
+
33
+ ```diff
34
+ - social: {
35
+ - github: 'https://github.com/withastro/starlight',
36
+ - discord: 'https://astro.build/chat',
37
+ - },
38
+ + social: [
39
+ + { icon: 'github', label: 'GitHub', href: 'https://github.com/withastro/starlight' },
40
+ + { icon: 'discord', label: 'Discord', href: 'https://astro.build/chat' },
41
+ + ],
42
+ ```
43
+
44
+ - [#2927](https://github.com/withastro/starlight/pull/2927) [`c46904c`](https://github.com/withastro/starlight/commit/c46904c4a16cf1c7f4f895e42cb164474b2301b3) Thanks [@HiDeoo](https://github.com/HiDeoo)! - 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.
45
+
46
+ 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.
47
+ 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).
48
+ The `<Head>` component now only renders the tags provided in `Astro.locals.starlightRoute.head`.
49
+
50
+ - [#2924](https://github.com/withastro/starlight/pull/2924) [`6a56d1b`](https://github.com/withastro/starlight/commit/6a56d1b80d9d67e63e930177cf085a25864e1952) Thanks [@HiDeoo](https://github.com/HiDeoo)! - ⚠️ **BREAKING CHANGE:** Ensures that the `<Badge>` and `<Icon>` components no longer render with a trailing space.
51
+
52
+ In Astro, components that include styles render with a trailing space which can prevent some use cases from working as expected, e.g. when using such components inlined with text. This change ensures that the `<Badge>` and `<Icon>` components no longer render with a trailing space.
53
+
54
+ If you were previously relying on that implementation detail, you may need to update your code to account for this change. For example, considering the following code:
55
+
56
+ ```mdx
57
+ <Badge text="New" />
58
+ Feature
59
+ ```
60
+
61
+ The rendered text would previously include a space between the badge and the text due to the trailing space automatically added by the component:
62
+
63
+ ```
64
+ New Feature
65
+ ```
66
+
67
+ Such code will now render the badge and text without a space:
68
+
69
+ ```
70
+ NewFeature
71
+ ```
72
+
73
+ To fix this, you can add a space between the badge and the text:
74
+
75
+ ```diff
76
+ - <Badge text="New" />Feature
77
+ + <Badge text="New" /> Feature
78
+ ```
79
+
80
+ - [#2727](https://github.com/withastro/starlight/pull/2727) [`7c8fa30`](https://github.com/withastro/starlight/commit/7c8fa30f0ac2459c83b71a8a7b705b16dcf98d6f) Thanks [@techfg](https://github.com/techfg)! - Updates mobile menu toggle styles to display a close icon while the menu is open
81
+
82
+ ### Patch Changes
83
+
84
+ - [#2927](https://github.com/withastro/starlight/pull/2927) [`c46904c`](https://github.com/withastro/starlight/commit/c46904c4a16cf1c7f4f895e42cb164474b2301b3) Thanks [@HiDeoo](https://github.com/HiDeoo)! - 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>`.
85
+
86
+ - [#2927](https://github.com/withastro/starlight/pull/2927) [`c46904c`](https://github.com/withastro/starlight/commit/c46904c4a16cf1c7f4f895e42cb164474b2301b3) Thanks [@HiDeoo](https://github.com/HiDeoo)! - 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'`.
87
+
88
+ - [#3025](https://github.com/withastro/starlight/pull/3025) [`f87e9ac`](https://github.com/withastro/starlight/commit/f87e9acbf5090a31858c1cde568cc798140f1366) Thanks [@delucis](https://github.com/delucis)! - Fixes Starlight’s autogenerated `<meta name="twitter:site">` tags when a Twitter link is set in `social` config. Previously these incorrectly rendered `content="/username"` and now correctly render `content="@username"`.
89
+
3
90
  ## 0.32.6
4
91
 
5
92
  ### Patch Changes
@@ -1,100 +1,5 @@
1
1
  ---
2
- import type { z } from 'astro/zod';
3
- import context from 'virtual:starlight/project-context';
4
- import config from 'virtual:starlight/user-config';
5
- import { version } from '../package.json';
6
- import type { HeadConfigSchema } from '../schemas/head';
7
- import { fileWithBase } from '../utils/base';
8
- import { createHead } from '../utils/head';
9
- import { localizedUrl } from '../utils/localizedUrl';
10
-
11
- const { entry, lang, siteTitle } = Astro.locals.starlightRoute;
12
- const { data } = entry;
13
-
14
- const canonical = Astro.site ? new URL(Astro.url.pathname, Astro.site) : undefined;
15
- const description = data.description || config.description;
16
-
17
- const headDefaults: z.input<ReturnType<typeof HeadConfigSchema>> = [
18
- { tag: 'meta', attrs: { charset: 'utf-8' } },
19
- {
20
- tag: 'meta',
21
- attrs: { name: 'viewport', content: 'width=device-width, initial-scale=1' },
22
- },
23
- { tag: 'title', content: `${data.title} ${config.titleDelimiter} ${siteTitle}` },
24
- { tag: 'link', attrs: { rel: 'canonical', href: canonical?.href } },
25
- { tag: 'meta', attrs: { name: 'generator', content: Astro.generator } },
26
- {
27
- tag: 'meta',
28
- attrs: { name: 'generator', content: `Starlight v${version}` },
29
- },
30
- // Favicon
31
- {
32
- tag: 'link',
33
- attrs: {
34
- rel: 'shortcut icon',
35
- href: fileWithBase(config.favicon.href),
36
- type: config.favicon.type,
37
- },
38
- },
39
- // OpenGraph Tags
40
- { tag: 'meta', attrs: { property: 'og:title', content: data.title } },
41
- { tag: 'meta', attrs: { property: 'og:type', content: 'article' } },
42
- { tag: 'meta', attrs: { property: 'og:url', content: canonical?.href } },
43
- { tag: 'meta', attrs: { property: 'og:locale', content: lang } },
44
- { tag: 'meta', attrs: { property: 'og:description', content: description } },
45
- { tag: 'meta', attrs: { property: 'og:site_name', content: siteTitle } },
46
- // Twitter Tags
47
- {
48
- tag: 'meta',
49
- attrs: { name: 'twitter:card', content: 'summary_large_image' },
50
- },
51
- ];
52
-
53
- if (description)
54
- headDefaults.push({
55
- tag: 'meta',
56
- attrs: { name: 'description', content: description },
57
- });
58
-
59
- // Link to language alternates.
60
- if (canonical && config.isMultilingual) {
61
- for (const locale in config.locales) {
62
- const localeOpts = config.locales[locale];
63
- if (!localeOpts) continue;
64
- headDefaults.push({
65
- tag: 'link',
66
- attrs: {
67
- rel: 'alternate',
68
- hreflang: localeOpts.lang,
69
- href: localizedUrl(canonical, locale, context.trailingSlash).href,
70
- },
71
- });
72
- }
73
- }
74
-
75
- // Link to sitemap, but only when `site` is set.
76
- if (Astro.site) {
77
- headDefaults.push({
78
- tag: 'link',
79
- attrs: {
80
- rel: 'sitemap',
81
- href: fileWithBase('/sitemap-index.xml'),
82
- },
83
- });
84
- }
85
-
86
- // Link to Twitter account if set in Starlight config.
87
- if (config.social?.twitter) {
88
- headDefaults.push({
89
- tag: 'meta',
90
- attrs: {
91
- name: 'twitter:site',
92
- content: new URL(config.social.twitter.url).pathname,
93
- },
94
- });
95
- }
96
-
97
- const head = createHead(headDefaults, config.head, data.head);
2
+ const { head } = Astro.locals.starlightRoute;
98
3
  ---
99
4
 
100
5
  {head.map(({ tag: Tag, attrs, content }) => <Tag {...attrs} set:html={content} />)}
@@ -9,7 +9,8 @@ import Icon from '../user-components/Icon.astro';
9
9
  aria-controls="starlight__sidebar"
10
10
  class="sl-flex md:sl-hidden"
11
11
  >
12
- <Icon name="bars" />
12
+ <Icon name="bars" class="open-menu" />
13
+ <Icon name="close" class="close-menu" />
13
14
  </button>
14
15
  </starlight-menu-button>
15
16
 
@@ -71,6 +72,14 @@ import Icon from '../user-components/Icon.astro';
71
72
  box-shadow: none;
72
73
  }
73
74
 
75
+ [aria-expanded='true'] button .open-menu {
76
+ display: none;
77
+ }
78
+
79
+ :not([aria-expanded='true']) button .close-menu {
80
+ display: none;
81
+ }
82
+
74
83
  :global([data-theme='light']) button {
75
84
  background-color: var(--sl-color-black);
76
85
  color: var(--sl-color-white);
@@ -2,18 +2,16 @@
2
2
  import config from 'virtual:starlight/user-config';
3
3
  import Icon from '../user-components/Icon.astro';
4
4
 
5
- type Platform = keyof NonNullable<typeof config.social>;
6
- type SocialConfig = NonNullable<NonNullable<typeof config.social>[Platform]>;
7
- const links = Object.entries(config.social || {}) as [Platform, SocialConfig][];
5
+ const links = config.social || [];
8
6
  ---
9
7
 
10
8
  {
11
9
  links.length > 0 && (
12
10
  <>
13
- {links.map(([platform, { label, url }]) => (
14
- <a href={url} rel="me" class="sl-flex">
11
+ {links.map(({ label, href, icon }) => (
12
+ <a {href} rel="me" class="sl-flex">
15
13
  <span class="sr-only">{label}</span>
16
- <Icon name={platform} />
14
+ <Icon name={icon} />
17
15
  </a>
18
16
  ))}
19
17
  </>
@@ -10,7 +10,7 @@ export type StarlightPageProps = Props;
10
10
 
11
11
  await attachRouteDataAndRunMiddleware(
12
12
  Astro,
13
- await generateStarlightPageRouteData({ props: Astro.props, url: Astro.url })
13
+ await generateStarlightPageRouteData({ props: Astro.props, context: Astro })
14
14
  );
15
15
  ---
16
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/starlight",
3
- "version": "0.32.6",
3
+ "version": "0.33.1",
4
4
  "description": "Build beautiful, high-performance documentation websites with Astro",
5
5
  "keywords": [
6
6
  "docs",
package/schemas/hero.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import { z } from 'astro/zod';
2
2
  import type { SchemaContext } from 'astro:content';
3
- import { Icons, type StarlightIcon } from '../components/Icons';
4
-
5
- const iconNames = Object.keys(Icons) as [StarlightIcon, ...StarlightIcon[]];
3
+ import { IconSchema } from './icon';
6
4
 
7
5
  export const HeroSchema = ({ image }: SchemaContext) =>
8
6
  z.object({
@@ -55,9 +53,9 @@ export const HeroSchema = ({ image }: SchemaContext) =>
55
53
  * Can be an inline `<svg>` or the name of one of Starlight’s built-in icons.
56
54
  */
57
55
  icon: z
58
- .union([z.enum(iconNames), z.string().startsWith('<svg')])
56
+ .union([IconSchema(), z.string().startsWith('<svg')])
59
57
  .transform((icon) => {
60
- const parsedIcon = z.enum(iconNames).safeParse(icon);
58
+ const parsedIcon = IconSchema().safeParse(icon);
61
59
  return parsedIcon.success
62
60
  ? ({ type: 'icon', name: parsedIcon.data } as const)
63
61
  : ({ type: 'raw', html: icon } as const);
@@ -0,0 +1,7 @@
1
+ import { z } from 'astro/zod';
2
+ import { Icons, type StarlightIcon } from '../components/Icons';
3
+
4
+ const iconNames = Object.keys(Icons) as [StarlightIcon, ...StarlightIcon[]];
5
+
6
+ /** String that matches the name of one of Starlight’s built-in icons. */
7
+ export const IconSchema = () => z.enum(iconNames);
package/schemas/social.ts CHANGED
@@ -1,111 +1,22 @@
1
1
  import { z } from 'astro/zod';
2
+ import { IconSchema } from './icon';
2
3
 
3
- export const socialLinks = [
4
- 'twitter',
5
- 'mastodon',
6
- 'github',
7
- 'gitlab',
8
- 'bitbucket',
9
- 'discord',
10
- 'gitter',
11
- 'codeberg',
12
- 'codePen',
13
- 'youtube',
14
- 'threads',
15
- 'linkedin',
16
- 'twitch',
17
- 'azureDevOps',
18
- 'microsoftTeams',
19
- 'instagram',
20
- 'stackOverflow',
21
- 'x.com',
22
- 'telegram',
23
- 'rss',
24
- 'facebook',
25
- 'email',
26
- 'reddit',
27
- 'patreon',
28
- 'signal',
29
- 'slack',
30
- 'matrix',
31
- 'openCollective',
32
- 'hackerOne',
33
- 'blueSky',
34
- 'discourse',
35
- 'zulip',
36
- 'pinterest',
37
- 'tiktok',
38
- 'nostr',
39
- 'backstage',
40
- 'farcaster',
41
- 'confluence',
42
- 'jira',
43
- 'storybook',
44
- 'npm',
45
- 'sourcehut',
46
- 'substack',
47
- ] as const;
4
+ const LinksSchema = z
5
+ .object({ icon: IconSchema(), label: z.string().min(1), href: z.string() })
6
+ .array()
7
+ .optional();
48
8
 
49
9
  export const SocialLinksSchema = () =>
50
- z
51
- .record(
52
- z.enum(socialLinks),
53
- // Link to the respective social profile for this site
54
- z.string().url()
55
- )
56
- .transform((links) => {
57
- const labelledLinks: Partial<Record<keyof typeof links, { label: string; url: string }>> = {};
58
- for (const _k in links) {
59
- const key = _k as keyof typeof links;
60
- const url = links[key];
61
- if (!url) continue;
62
- const label = {
63
- github: 'GitHub',
64
- gitlab: 'GitLab',
65
- bitbucket: 'Bitbucket',
66
- discord: 'Discord',
67
- gitter: 'Gitter',
68
- twitter: 'Twitter',
69
- mastodon: 'Mastodon',
70
- codeberg: 'Codeberg',
71
- codePen: 'CodePen',
72
- youtube: 'YouTube',
73
- threads: 'Threads',
74
- linkedin: 'LinkedIn',
75
- twitch: 'Twitch',
76
- azureDevOps: 'Azure DevOps',
77
- microsoftTeams: 'Microsoft Teams',
78
- instagram: 'Instagram',
79
- stackOverflow: 'Stack Overflow',
80
- 'x.com': 'X',
81
- telegram: 'Telegram',
82
- rss: 'RSS',
83
- facebook: 'Facebook',
84
- email: 'Email',
85
- reddit: 'Reddit',
86
- patreon: 'Patreon',
87
- signal: 'Signal',
88
- slack: 'Slack',
89
- matrix: 'Matrix',
90
- openCollective: 'Open Collective',
91
- hackerOne: 'Hacker One',
92
- blueSky: 'BlueSky',
93
- discourse: 'Discourse',
94
- zulip: 'Zulip',
95
- pinterest: 'Pinterest',
96
- tiktok: 'TikTok',
97
- nostr: 'Nostr',
98
- backstage: 'Backstage',
99
- farcaster: 'Farcaster',
100
- confluence: 'Confluence',
101
- jira: 'Jira',
102
- storybook: 'Storybook',
103
- npm: 'npm',
104
- sourcehut: 'SourceHut',
105
- substack: 'Substack',
106
- }[key];
107
- labelledLinks[key] = { label, url };
108
- }
109
- return labelledLinks;
110
- })
111
- .optional();
10
+ // Add a more specific error message to help people migrate from the old object syntax.
11
+ // TODO: remove once most people have updated to v0.33 or higher (e.g. when releasing Starlight v1)
12
+ z.preprocess((value, ctx) => {
13
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
14
+ ctx.addIssue({
15
+ code: z.ZodIssueCode.custom,
16
+ message:
17
+ 'Starlight v0.33.0 changed the `social` configuration syntax. Please specify an array of link items instead of an object.\n' +
18
+ 'See the Starlight changelog for details: https://github.com/withastro/starlight/blob/main/packages/starlight/CHANGELOG.md#0330\n',
19
+ });
20
+ }
21
+ return value;
22
+ }, LinksSchema) as unknown as typeof LinksSchema;
@@ -24,7 +24,29 @@
24
24
 
25
25
  .sl-markdown-content
26
26
  li
27
- > :last-child:not(li, ul, ol, a, strong, em, del, span, input, code, br, :where(.not-content *)) {
27
+ > :is(
28
+ :last-child:not(
29
+ li,
30
+ ul,
31
+ ol,
32
+ a,
33
+ strong,
34
+ em,
35
+ del,
36
+ span,
37
+ input,
38
+ code,
39
+ br,
40
+ script,
41
+ :where(.not-content *)
42
+ ),
43
+ /**
44
+ * For list items ending with 1 or multiple script elements (`:has(~ script:last-child)`), we
45
+ * need to style the last non-script element (`:not(script)`) that doesn't have a subsequent
46
+ * sibling that is not a script (`:not(:has(~ :not(script)))`).
47
+ */
48
+ :not(script):has(~ script:last-child):not(:has(~ :not(script)))
49
+ ) {
28
50
  margin-bottom: 1.25rem;
29
51
  }
30
52
 
@@ -16,9 +16,14 @@ const {
16
16
  Astro.props,
17
17
  'Invalid prop passed to the `<Badge/>` component.'
18
18
  );
19
+
20
+ /**
21
+ * The fragment around the element is used as a workaround to avoid a trailing whitespace in the output.
22
+ * @see https://github.com/withastro/compiler/issues/1003
23
+ */
19
24
  ---
20
25
 
21
- <span class:list={['sl-badge', variant, size, customClass]} {...attrs}>{text}</span>
26
+ <><span class:list={['sl-badge', variant, size, customClass]} {...attrs}>{text}</span></>
22
27
 
23
28
  <style>
24
29
  :global(:root) {
@@ -11,17 +11,24 @@ interface Props {
11
11
 
12
12
  const { name, label, size = '1em', color } = Astro.props;
13
13
  const a11yAttrs = label ? ({ 'aria-label': label } as const) : ({ 'aria-hidden': 'true' } as const);
14
+
15
+ /**
16
+ * The fragment around the element is used as a workaround to avoid a trailing whitespace in the output.
17
+ * @see https://github.com/withastro/compiler/issues/1003
18
+ */
14
19
  ---
15
20
 
16
- <svg
17
- {...a11yAttrs}
18
- class={Astro.props.class}
19
- width="16"
20
- height="16"
21
- viewBox="0 0 24 24"
22
- fill="currentColor"
23
- set:html={Icons[name]}
24
- />
21
+ <>
22
+ <svg
23
+ {...a11yAttrs}
24
+ class={Astro.props.class}
25
+ width="16"
26
+ height="16"
27
+ viewBox="0 0 24 24"
28
+ fill="currentColor"
29
+ set:html={Icons[name]}
30
+ />
31
+ </>
25
32
 
26
33
  <style define:vars={{ 'sl-icon-color': color, 'sl-icon-size': size }}>
27
34
  svg {
@@ -0,0 +1,19 @@
1
+ import type { AstroConfig } from 'astro';
2
+ import { ensureTrailingSlash, stripTrailingSlash } from './path';
3
+
4
+ export interface FormatCanonicalOptions {
5
+ format: AstroConfig['build']['format'];
6
+ trailingSlash: AstroConfig['trailingSlash'];
7
+ }
8
+
9
+ const canonicalTrailingSlashStrategies = {
10
+ always: ensureTrailingSlash,
11
+ never: stripTrailingSlash,
12
+ ignore: ensureTrailingSlash,
13
+ };
14
+
15
+ /** Format a canonical link based on the project config. */
16
+ export function formatCanonical(href: string, opts: FormatCanonicalOptions) {
17
+ if (opts.format === 'file') return href;
18
+ return canonicalTrailingSlashStrategies[opts.trailingSlash](href);
19
+ }
package/utils/head.ts CHANGED
@@ -1,9 +1,117 @@
1
+ import config from 'virtual:starlight/user-config';
2
+ import project from 'virtual:starlight/project-context';
3
+ import { version } from '../package.json';
1
4
  import { type HeadConfig, HeadConfigSchema, type HeadUserConfig } from '../schemas/head';
5
+ import type { PageProps, RouteDataContext } from './routing/data';
6
+ import { fileWithBase } from './base';
7
+ import { formatCanonical } from './canonical';
8
+ import { localizedUrl } from './localizedUrl';
2
9
 
3
10
  const HeadSchema = HeadConfigSchema();
4
11
 
12
+ /** Get the head for the current page. */
13
+ export function getHead(
14
+ { entry, lang }: PageProps,
15
+ context: RouteDataContext,
16
+ siteTitle: string
17
+ ): HeadConfig {
18
+ const { data } = entry;
19
+
20
+ const canonical = context.site ? new URL(context.url.pathname, context.site) : undefined;
21
+ const canonicalHref = canonical?.href
22
+ ? formatCanonical(canonical.href, {
23
+ format: project.build.format,
24
+ trailingSlash: project.trailingSlash,
25
+ })
26
+ : undefined;
27
+ const description = data.description || config.description;
28
+
29
+ const headDefaults: HeadUserConfig = [
30
+ { tag: 'meta', attrs: { charset: 'utf-8' } },
31
+ {
32
+ tag: 'meta',
33
+ attrs: { name: 'viewport', content: 'width=device-width, initial-scale=1' },
34
+ },
35
+ { tag: 'title', content: `${data.title} ${config.titleDelimiter} ${siteTitle}` },
36
+ { tag: 'link', attrs: { rel: 'canonical', href: canonicalHref } },
37
+ { tag: 'meta', attrs: { name: 'generator', content: context.generator } },
38
+ {
39
+ tag: 'meta',
40
+ attrs: { name: 'generator', content: `Starlight v${version}` },
41
+ },
42
+ // Favicon
43
+ {
44
+ tag: 'link',
45
+ attrs: {
46
+ rel: 'shortcut icon',
47
+ href: fileWithBase(config.favicon.href),
48
+ type: config.favicon.type,
49
+ },
50
+ },
51
+ // OpenGraph Tags
52
+ { tag: 'meta', attrs: { property: 'og:title', content: data.title } },
53
+ { tag: 'meta', attrs: { property: 'og:type', content: 'article' } },
54
+ { tag: 'meta', attrs: { property: 'og:url', content: canonicalHref } },
55
+ { tag: 'meta', attrs: { property: 'og:locale', content: lang } },
56
+ { tag: 'meta', attrs: { property: 'og:description', content: description } },
57
+ { tag: 'meta', attrs: { property: 'og:site_name', content: siteTitle } },
58
+ // Twitter Tags
59
+ {
60
+ tag: 'meta',
61
+ attrs: { name: 'twitter:card', content: 'summary_large_image' },
62
+ },
63
+ ];
64
+
65
+ if (description)
66
+ headDefaults.push({
67
+ tag: 'meta',
68
+ attrs: { name: 'description', content: description },
69
+ });
70
+
71
+ // Link to language alternates.
72
+ if (canonical && config.isMultilingual) {
73
+ for (const locale in config.locales) {
74
+ const localeOpts = config.locales[locale];
75
+ if (!localeOpts) continue;
76
+ headDefaults.push({
77
+ tag: 'link',
78
+ attrs: {
79
+ rel: 'alternate',
80
+ hreflang: localeOpts.lang,
81
+ href: localizedUrl(canonical, locale, project.trailingSlash).href,
82
+ },
83
+ });
84
+ }
85
+ }
86
+
87
+ // Link to sitemap, but only when `site` is set.
88
+ if (context.site) {
89
+ headDefaults.push({
90
+ tag: 'link',
91
+ attrs: {
92
+ rel: 'sitemap',
93
+ href: fileWithBase('/sitemap-index.xml'),
94
+ },
95
+ });
96
+ }
97
+
98
+ // Link to Twitter account if set in Starlight config.
99
+ const twitterLink = config.social?.find(({ icon }) => icon === 'twitter' || icon === 'x.com');
100
+ if (twitterLink) {
101
+ headDefaults.push({
102
+ tag: 'meta',
103
+ attrs: {
104
+ name: 'twitter:site',
105
+ content: new URL(twitterLink.href).pathname.replace('/', '@'),
106
+ },
107
+ });
108
+ }
109
+
110
+ return createHead(headDefaults, config.head, data.head);
111
+ }
112
+
5
113
  /** Create a fully parsed, merged, and sorted head entry array from multiple sources. */
6
- export function createHead(defaults: HeadUserConfig, ...heads: HeadConfig[]) {
114
+ function createHead(defaults: HeadUserConfig, ...heads: HeadConfig[]) {
7
115
  let head = HeadSchema.parse(defaults);
8
116
  for (const next of heads) {
9
117
  head = mergeHead(head, next);
@@ -26,7 +134,7 @@ function hasTag(head: HeadConfig, entry: HeadConfig[number]): boolean {
26
134
  case 'meta':
27
135
  return hasOneOf(head, entry, ['name', 'property', 'http-equiv']);
28
136
  case 'link':
29
- return head.some(({ attrs }) => attrs.rel === 'canonical');
137
+ return head.some(({ attrs }) => entry.attrs.rel === 'canonical' && attrs.rel === 'canonical');
30
138
  default:
31
139
  return false;
32
140
  }
@@ -17,29 +17,32 @@ import { useTranslations } from '../translations';
17
17
  import { BuiltInDefaultLocale } from '../i18n';
18
18
  import { getEntry, render } from 'astro:content';
19
19
  import { getCollectionPathFromRoot } from '../collection';
20
+ import { getHead } from '../head';
20
21
 
21
22
  export interface PageProps extends Route {
22
23
  headings: MarkdownHeading[];
23
24
  }
24
25
 
26
+ export type RouteDataContext = Pick<APIContext, 'generator' | 'site' | 'url'>;
27
+
25
28
  export async function useRouteData(context: APIContext): Promise<StarlightRouteData> {
26
29
  const route =
27
30
  ('slug' in context.params && getRouteBySlugParam(context.params.slug)) ||
28
31
  (await get404Route(context.locals));
29
32
  const { Content, headings } = await render(route.entry);
30
- const routeData = generateRouteData({ props: { ...route, headings }, url: context.url });
33
+ const routeData = generateRouteData({ props: { ...route, headings }, context });
31
34
  return { ...routeData, Content };
32
35
  }
33
36
 
34
37
  export function generateRouteData({
35
38
  props,
36
- url,
39
+ context,
37
40
  }: {
38
41
  props: PageProps;
39
- url: URL;
42
+ context: RouteDataContext;
40
43
  }): StarlightRouteData {
41
44
  const { entry, locale, lang } = props;
42
- const sidebar = getSidebar(url.pathname, locale);
45
+ const sidebar = getSidebar(context.url.pathname, locale);
43
46
  const siteTitle = getSiteTitle(lang);
44
47
  return {
45
48
  ...props,
@@ -51,6 +54,7 @@ export function generateRouteData({
51
54
  toc: getToC(props),
52
55
  lastUpdated: getLastUpdated(props),
53
56
  editUrl: getEditUrl(props),
57
+ head: getHead(props, context, siteTitle),
54
58
  };
55
59
  }
56
60
 
@@ -3,6 +3,7 @@ import type { CollectionEntry, RenderResult } from 'astro:content';
3
3
  import type { TocItem } from '../generateToC';
4
4
  import type { LinkHTMLAttributes } from '../../schemas/sidebar';
5
5
  import type { Badge } from '../../schemas/badge';
6
+ import type { HeadConfig } from '../../schemas/head';
6
7
 
7
8
  export interface LocaleData {
8
9
  /** Writing direction. */
@@ -93,4 +94,6 @@ export interface StarlightRouteData extends Route {
93
94
  editUrl: URL | undefined;
94
95
  /** An Astro component to render the current page’s content if this route is a Markdown page. */
95
96
  Content?: RenderResult['Content'];
97
+ /** Array of tags to include in the `<head>` of the current page. */
98
+ head: HeadConfig;
96
99
  }
@@ -5,7 +5,13 @@ import config from 'virtual:starlight/user-config';
5
5
  import { getCollectionPathFromRoot } from './collection';
6
6
  import { parseWithFriendlyErrors, parseAsyncWithFriendlyErrors } from './error-map';
7
7
  import { stripLeadingAndTrailingSlashes } from './path';
8
- import { getSiteTitle, getSiteTitleHref, getToC, type PageProps } from './routing/data';
8
+ import {
9
+ getSiteTitle,
10
+ getSiteTitleHref,
11
+ getToC,
12
+ type PageProps,
13
+ type RouteDataContext,
14
+ } from './routing/data';
9
15
  import type { StarlightDocsEntry, StarlightRouteData } from './routing/types';
10
16
  import { slugToLocaleData, urlToSlug } from './slugs';
11
17
  import { getPrevNextLinks, getSidebar, getSidebarFromConfig } from './navigation';
@@ -13,6 +19,7 @@ import { docsSchema } from '../schema';
13
19
  import type { Prettify, RemoveIndexSignature } from './types';
14
20
  import { SidebarItemSchema } from '../schemas/sidebar';
15
21
  import type { StarlightConfig, StarlightUserConfig } from './user-config';
22
+ import { getHead } from './head';
16
23
 
17
24
  /**
18
25
  * The frontmatter schema for Starlight pages derived from the default schema for Starlight’s
@@ -100,12 +107,13 @@ type StarlightPageDocsEntry = Omit<StarlightDocsEntry, 'id' | 'render'> & {
100
107
 
101
108
  export async function generateStarlightPageRouteData({
102
109
  props,
103
- url,
110
+ context,
104
111
  }: {
105
112
  props: StarlightPageProps;
106
- url: URL;
113
+ context: RouteDataContext;
107
114
  }): Promise<StarlightRouteData> {
108
115
  const { frontmatter, ...routeProps } = props;
116
+ const { url } = context;
109
117
  const slug = urlToSlug(url);
110
118
  const pageFrontmatter = await getStarlightPageFrontmatter(frontmatter);
111
119
  const id = project.legacyCollections ? `${stripLeadingAndTrailingSlashes(slug)}.md` : slug;
@@ -137,6 +145,17 @@ export async function generateStarlightPageRouteData({
137
145
  const editUrl = pageFrontmatter.editUrl ? new URL(pageFrontmatter.editUrl) : undefined;
138
146
  const lastUpdated =
139
147
  pageFrontmatter.lastUpdated instanceof Date ? pageFrontmatter.lastUpdated : undefined;
148
+ const pageProps: PageProps = {
149
+ ...routeProps,
150
+ ...localeData,
151
+ entry,
152
+ entryMeta,
153
+ headings,
154
+ id,
155
+ locale: localeData.locale,
156
+ slug,
157
+ };
158
+ const siteTitle = getSiteTitle(localeData.lang);
140
159
  const routeData: StarlightRouteData = {
141
160
  ...routeProps,
142
161
  ...localeData,
@@ -145,23 +164,15 @@ export async function generateStarlightPageRouteData({
145
164
  entry,
146
165
  entryMeta,
147
166
  hasSidebar: props.hasSidebar ?? entry.data.template !== 'splash',
167
+ head: getHead(pageProps, context, siteTitle),
148
168
  headings,
149
169
  lastUpdated,
150
170
  pagination: getPrevNextLinks(sidebar, config.pagination, entry.data),
151
171
  sidebar,
152
- siteTitle: getSiteTitle(localeData.lang),
172
+ siteTitle,
153
173
  siteTitleHref: getSiteTitleHref(localeData.locale),
154
174
  slug,
155
- toc: getToC({
156
- ...routeProps,
157
- ...localeData,
158
- entry,
159
- entryMeta,
160
- headings,
161
- id,
162
- locale: localeData.locale,
163
- slug,
164
- }),
175
+ toc: getToC(pageProps),
165
176
  };
166
177
  return routeData;
167
178
  }
@@ -51,18 +51,13 @@ const UserConfigSchema = z.object({
51
51
  * Optional details about the social media accounts for this site.
52
52
  *
53
53
  * @example
54
- * social: {
55
- * codeberg: 'https://codeberg.org/knut/examples',
56
- * discord: 'https://astro.build/chat',
57
- * github: 'https://github.com/withastro/starlight',
58
- * gitlab: 'https://gitlab.com/delucis',
59
- * linkedin: 'https://www.linkedin.com/company/astroinc',
60
- * mastodon: 'https://m.webtoo.ls/@astro',
61
- * threads: 'https://www.threads.net/@nmoodev',
62
- * twitch: 'https://www.twitch.tv/bholmesdev',
63
- * twitter: 'https://twitter.com/astrodotbuild',
64
- * youtube: 'https://youtube.com/@astrodotbuild',
65
- * }
54
+ * social: [
55
+ * { icon: 'codeberg', label: 'Codeberg', href: 'https://codeberg.org/knut' },
56
+ * { icon: 'discord', label: 'Discord', href: 'https://astro.build/chat' },
57
+ * { icon: 'github', label: 'GitHub', href: 'https://github.com/withastro' },
58
+ * { icon: 'gitlab', label: 'GitLab', href: 'https://gitlab.com/delucis' },
59
+ * { icon: 'mastodon', label: 'Mastodon', href: 'https://m.webtoo.ls/@astro' },
60
+ * ]
66
61
  */
67
62
  social: SocialLinksSchema(),
68
63