@astrojs/starlight 0.31.1 → 0.32.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/components/Banner.astro +1 -3
  3. package/components/ContentPanel.astro +0 -4
  4. package/components/DraftContentNotice.astro +0 -1
  5. package/components/EditLink.astro +1 -2
  6. package/components/FallbackContentNotice.astro +0 -1
  7. package/components/Footer.astro +3 -5
  8. package/components/Head.astro +1 -2
  9. package/components/Header.astro +5 -6
  10. package/components/Hero.astro +1 -2
  11. package/components/LanguageSelect.astro +2 -3
  12. package/components/LastUpdated.astro +1 -3
  13. package/components/MarkdownContent.astro +0 -1
  14. package/components/MobileMenuFooter.astro +3 -4
  15. package/components/MobileMenuToggle.astro +0 -1
  16. package/components/MobileTableOfContents.astro +1 -2
  17. package/components/Page.astro +32 -32
  18. package/components/PageFrame.astro +2 -3
  19. package/components/PageSidebar.astro +3 -5
  20. package/components/PageTitle.astro +1 -2
  21. package/components/Pagination.astro +1 -2
  22. package/components/Search.astro +17 -3
  23. package/components/Sidebar.astro +3 -5
  24. package/components/SidebarPersister.astro +1 -2
  25. package/components/SidebarSublist.astro +2 -1
  26. package/components/SiteTitle.astro +1 -2
  27. package/components/SkipLink.astro +0 -1
  28. package/components/SocialIcons.astro +0 -1
  29. package/components/StarlightPage.astro +7 -3
  30. package/components/TableOfContents.astro +1 -2
  31. package/components/ThemeProvider.astro +0 -1
  32. package/components/ThemeSelect.astro +0 -1
  33. package/components/TwoColumnContent.astro +1 -5
  34. package/index.ts +7 -9
  35. package/integrations/asides.ts +4 -7
  36. package/integrations/expressive-code/index.ts +15 -10
  37. package/integrations/shared/{pathToLocale.ts → absolutePathToLang.ts} +7 -5
  38. package/integrations/virtual-user-config.ts +27 -0
  39. package/locals.d.ts +26 -0
  40. package/locals.ts +37 -2
  41. package/package.json +5 -3
  42. package/props.ts +13 -1
  43. package/route-data.ts +11 -0
  44. package/routes/common.astro +5 -11
  45. package/routes/ssr/index.astro +1 -1
  46. package/routes/static/404.astro +1 -41
  47. package/routes/static/index.astro +1 -4
  48. package/schemas/pagefind.ts +97 -33
  49. package/types.ts +1 -0
  50. package/utils/i18n.ts +0 -20
  51. package/utils/navigation.ts +19 -36
  52. package/utils/plugins.ts +316 -141
  53. package/utils/{route-data.ts → routing/data.ts} +56 -30
  54. package/utils/{routing.ts → routing/index.ts} +6 -44
  55. package/utils/routing/middleware.ts +81 -0
  56. package/utils/routing/types.ts +96 -0
  57. package/utils/slugs.ts +2 -10
  58. package/utils/starlight-page.ts +2 -10
  59. package/utils/user-config.ts +8 -0
  60. package/virtual-internal.d.ts +4 -0
@@ -1,39 +1,34 @@
1
- import type { MarkdownHeading } from 'astro';
1
+ import type { APIContext, MarkdownHeading } from 'astro';
2
+ import project from 'virtual:starlight/project-context';
2
3
  import config from 'virtual:starlight/user-config';
3
- import { generateToC, type TocItem } from './generateToC';
4
+ import { generateToC } from '../generateToC';
4
5
  import { getNewestCommitDate } from 'virtual:starlight/git-info';
5
- import { getPrevNextLinks, getSidebar, type SidebarEntry } from './navigation';
6
- import { ensureTrailingSlash } from './path';
7
- import type { Route } from './routing';
8
- import { formatPath } from './format-path';
9
- import { useTranslations } from './translations';
10
- import { DeprecatedLabelsPropProxy } from './i18n';
6
+ import { getPrevNextLinks, getSidebar } from '../navigation';
7
+ import { ensureTrailingSlash } from '../path';
8
+ import { getRouteBySlugParam, normalizeCollectionEntry } from '../routing';
9
+ import type {
10
+ Route,
11
+ StarlightDocsCollectionEntry,
12
+ StarlightDocsEntry,
13
+ StarlightRouteData,
14
+ } from './types';
15
+ import { formatPath } from '../format-path';
16
+ import { useTranslations } from '../translations';
17
+ import { BuiltInDefaultLocale } from '../i18n';
18
+ import { getEntry, render } from 'astro:content';
19
+ import { getCollectionPathFromRoot } from '../collection';
11
20
 
12
21
  export interface PageProps extends Route {
13
22
  headings: MarkdownHeading[];
14
23
  }
15
24
 
16
- export interface StarlightRouteData extends Route {
17
- /** Title of the site. */
18
- siteTitle: string;
19
- /** URL or path used as the link when clicking on the site title. */
20
- siteTitleHref: string;
21
- /** Array of Markdown headings extracted from the current page. */
22
- headings: MarkdownHeading[];
23
- /** Site navigation sidebar entries for this page. */
24
- sidebar: SidebarEntry[];
25
- /** Whether or not the sidebar should be displayed on this page. */
26
- hasSidebar: boolean;
27
- /** Links to the previous and next page in the sidebar if enabled. */
28
- pagination: ReturnType<typeof getPrevNextLinks>;
29
- /** Table of contents for this page if enabled. */
30
- toc: { minHeadingLevel: number; maxHeadingLevel: number; items: TocItem[] } | undefined;
31
- /** JS Date object representing when this page was last updated if enabled. */
32
- lastUpdated: Date | undefined;
33
- /** URL object for the address where this page can be edited if enabled. */
34
- editUrl: URL | undefined;
35
- /** @deprecated Use `Astro.locals.t()` instead. */
36
- labels: Record<string, never>;
25
+ export async function useRouteData(context: APIContext): Promise<StarlightRouteData> {
26
+ const route =
27
+ ('slug' in context.params && getRouteBySlugParam(context.params.slug)) ||
28
+ (await get404Route(context.locals));
29
+ const { Content, headings } = await render(route.entry);
30
+ const routeData = generateRouteData({ props: { ...route, headings }, url: context.url });
31
+ return { ...routeData, Content };
37
32
  }
38
33
 
39
34
  export function generateRouteData({
@@ -56,7 +51,6 @@ export function generateRouteData({
56
51
  toc: getToC(props),
57
52
  lastUpdated: getLastUpdated(props),
58
53
  editUrl: getEditUrl(props),
59
- labels: DeprecatedLabelsPropProxy,
60
54
  };
61
55
  }
62
56
 
@@ -121,3 +115,35 @@ export function getSiteTitle(lang: string): string {
121
115
  export function getSiteTitleHref(locale: string | undefined): string {
122
116
  return formatPath(locale || '/');
123
117
  }
118
+
119
+ /** Generate a route object for Starlight’s 404 page. */
120
+ async function get404Route(locals: App.Locals): Promise<Route> {
121
+ const { lang = BuiltInDefaultLocale.lang, dir = BuiltInDefaultLocale.dir } =
122
+ config.defaultLocale || {};
123
+ let locale = config.defaultLocale?.locale;
124
+ if (locale === 'root') locale = undefined;
125
+
126
+ const entryMeta = { dir, lang, locale };
127
+
128
+ const fallbackEntry: StarlightDocsEntry = {
129
+ slug: '404',
130
+ id: '404',
131
+ body: '',
132
+ collection: 'docs',
133
+ data: {
134
+ title: '404',
135
+ template: 'splash',
136
+ editUrl: false,
137
+ head: [],
138
+ hero: { tagline: locals.t('404.text'), actions: [] },
139
+ pagefind: false,
140
+ sidebar: { hidden: false, attrs: {} },
141
+ draft: false,
142
+ },
143
+ filePath: `${getCollectionPathFromRoot('docs', project)}/404.md`,
144
+ };
145
+
146
+ const userEntry = (await getEntry('docs', '404')) as StarlightDocsCollectionEntry;
147
+ const entry = userEntry ? normalizeCollectionEntry(userEntry) : fallbackEntry;
148
+ return { ...entryMeta, entryMeta, entry, id: entry.id, slug: entry.slug };
149
+ }
@@ -1,55 +1,17 @@
1
1
  import type { GetStaticPathsItem } from 'astro';
2
- import { type CollectionEntry, getCollection } from 'astro:content';
2
+ import { getCollection } from 'astro:content';
3
3
  import config from 'virtual:starlight/user-config';
4
4
  import project from 'virtual:starlight/project-context';
5
- import { getCollectionPathFromRoot } from './collection';
6
- import {
7
- type LocaleData,
8
- localizedId,
9
- localizedSlug,
10
- slugToLocaleData,
11
- slugToParam,
12
- } from './slugs';
13
- import { validateLogoImports } from './validateLogoImports';
14
- import { BuiltInDefaultLocale } from './i18n';
5
+ import { getCollectionPathFromRoot } from '../collection';
6
+ import { localizedId, localizedSlug, slugToLocaleData, slugToParam } from '../slugs';
7
+ import { validateLogoImports } from '../validateLogoImports';
8
+ import { BuiltInDefaultLocale } from '../i18n';
9
+ import type { Route, StarlightDocsCollectionEntry, StarlightDocsEntry } from './types';
15
10
 
16
11
  // Validate any user-provided logos imported correctly.
17
12
  // We do this here so all pages trigger it and at the top level so it runs just once.
18
13
  validateLogoImports();
19
14
 
20
- // The type returned from `CollectionEntry` is different for legacy collections and collections
21
- // using a loader. This type is a common subset of both types.
22
- export type StarlightDocsCollectionEntry = Omit<
23
- CollectionEntry<'docs'>,
24
- 'id' | 'filePath' | 'render' | 'slug'
25
- > & {
26
- // Update the `id` property to be a string like in the loader type.
27
- id: string;
28
- // Add the `filePath` property which is only present in the loader type.
29
- filePath?: string;
30
- // Add the `slug` property which is only present in the legacy type.
31
- slug?: string;
32
- };
33
-
34
- export type StarlightDocsEntry = StarlightDocsCollectionEntry & {
35
- filePath: string;
36
- slug: string;
37
- };
38
-
39
- export interface Route extends LocaleData {
40
- /** Content collection entry for the current page. Includes frontmatter at `data`. */
41
- entry: StarlightDocsEntry;
42
- /** Locale metadata for the page content. Can be different from top-level locale values when a page is using fallback content. */
43
- entryMeta: LocaleData;
44
- /** @deprecated Migrate to the new Content Layer API and use `id` instead. */
45
- slug: string;
46
- /** The slug or unique ID if using the `legacy.collections` flag. */
47
- id: string;
48
- /** True if this page is untranslated in the current language and using fallback content from the default locale. */
49
- isFallback?: true;
50
- [key: string]: unknown;
51
- }
52
-
53
15
  interface Path extends GetStaticPathsItem {
54
16
  params: { slug: string | undefined };
55
17
  props: Route;
@@ -0,0 +1,81 @@
1
+ import type { APIContext } from 'astro';
2
+ import { klona } from 'klona/lite';
3
+ import { routeMiddleware } from 'virtual:starlight/route-middleware';
4
+ import type { StarlightRouteData } from './types';
5
+
6
+ /**
7
+ * Adds a deep clone of the passed `routeData` object to locals and then runs middleware.
8
+ * @param context Astro context object
9
+ * @param routeData Initial route data object to attach.
10
+ */
11
+ export async function attachRouteDataAndRunMiddleware(
12
+ context: APIContext,
13
+ routeData: StarlightRouteData
14
+ ) {
15
+ context.locals.starlightRoute = klona(routeData);
16
+ const runner = new MiddlewareRunner(context, routeMiddleware);
17
+ await runner.run();
18
+ }
19
+
20
+ type MiddlewareHandler<T> = (context: T, next: () => Promise<void>) => void | Promise<void>;
21
+
22
+ /**
23
+ * A middleware function wrapper that only allows a single execution of the wrapped function.
24
+ * Subsequent calls to `run()` are no-ops.
25
+ */
26
+ class MiddlewareRunnerStep<T> {
27
+ #callback: MiddlewareHandler<T> | null;
28
+ constructor(callback: MiddlewareHandler<T>) {
29
+ this.#callback = callback;
30
+ }
31
+ async run(context: T, next: () => Promise<void>): Promise<void> {
32
+ if (this.#callback) {
33
+ await this.#callback(context, next);
34
+ this.#callback = null;
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Class that runs a stack of middleware handlers with an initial context object.
41
+ * Middleware functions can mutate properties of the `context` object, but cannot replace it.
42
+ *
43
+ * @example
44
+ * const context = { value: 10 };
45
+ * const timesTwo = async (ctx, next) => {
46
+ * await next();
47
+ * ctx.value *= 2;
48
+ * };
49
+ * const addFive = async (ctx) => {
50
+ * ctx.value += 5;
51
+ * }
52
+ * const runner = new MiddlewareRunner(context, [timesTwo, addFive]);
53
+ * runner.run();
54
+ * console.log(context); // { value: 30 }
55
+ */
56
+ class MiddlewareRunner<T> {
57
+ #context: T;
58
+ #steps: Array<MiddlewareRunnerStep<T>>;
59
+
60
+ constructor(
61
+ /** Context object passed as the first argument to each middleware function. */
62
+ context: T,
63
+ /** Array of middleware functions to run in sequence. */
64
+ stack: Array<MiddlewareHandler<T>> = []
65
+ ) {
66
+ this.#context = context;
67
+ this.#steps = stack.map((callback) => new MiddlewareRunnerStep(callback));
68
+ }
69
+
70
+ async #stepThrough(steps: Array<MiddlewareRunnerStep<T>>) {
71
+ let currentStep: MiddlewareRunnerStep<T>;
72
+ while (steps.length > 0) {
73
+ [currentStep, ...steps] = steps as [MiddlewareRunnerStep<T>, ...MiddlewareRunnerStep<T>[]];
74
+ await currentStep.run(this.#context, async () => this.#stepThrough(steps));
75
+ }
76
+ }
77
+
78
+ async run() {
79
+ await this.#stepThrough(this.#steps);
80
+ }
81
+ }
@@ -0,0 +1,96 @@
1
+ import type { MarkdownHeading } from 'astro';
2
+ import type { CollectionEntry, RenderResult } from 'astro:content';
3
+ import type { TocItem } from '../generateToC';
4
+ import type { LinkHTMLAttributes } from '../../schemas/sidebar';
5
+ import type { Badge } from '../../schemas/badge';
6
+
7
+ export interface LocaleData {
8
+ /** Writing direction. */
9
+ dir: 'ltr' | 'rtl';
10
+ /** BCP-47 language tag. */
11
+ lang: string;
12
+ /** The base path at which a language is served. `undefined` for root locale slugs. */
13
+ locale: string | undefined;
14
+ }
15
+
16
+ export interface SidebarLink {
17
+ type: 'link';
18
+ label: string;
19
+ href: string;
20
+ isCurrent: boolean;
21
+ badge: Badge | undefined;
22
+ attrs: LinkHTMLAttributes;
23
+ }
24
+
25
+ export interface SidebarGroup {
26
+ type: 'group';
27
+ label: string;
28
+ entries: (SidebarLink | SidebarGroup)[];
29
+ collapsed: boolean;
30
+ badge: Badge | undefined;
31
+ }
32
+
33
+ export type SidebarEntry = SidebarLink | SidebarGroup;
34
+
35
+ export interface PaginationLinks {
36
+ /** Link to previous page in the sidebar. */
37
+ prev: SidebarLink | undefined;
38
+ /** Link to next page in the sidebar. */
39
+ next: SidebarLink | undefined;
40
+ }
41
+
42
+ // The type returned from `CollectionEntry` is different for legacy collections and collections
43
+ // using a loader. This type is a common subset of both types.
44
+ export type StarlightDocsCollectionEntry = Omit<
45
+ CollectionEntry<'docs'>,
46
+ 'id' | 'filePath' | 'render' | 'slug'
47
+ > & {
48
+ // Update the `id` property to be a string like in the loader type.
49
+ id: string;
50
+ // Add the `filePath` property which is only present in the loader type.
51
+ filePath?: string;
52
+ // Add the `slug` property which is only present in the legacy type.
53
+ slug?: string;
54
+ };
55
+
56
+ export type StarlightDocsEntry = StarlightDocsCollectionEntry & {
57
+ filePath: string;
58
+ slug: string;
59
+ };
60
+
61
+ export interface Route extends LocaleData {
62
+ /** Content collection entry for the current page. Includes frontmatter at `data`. */
63
+ entry: StarlightDocsEntry;
64
+ /** Locale metadata for the page content. Can be different from top-level locale values when a page is using fallback content. */
65
+ entryMeta: LocaleData;
66
+ /** @deprecated Migrate to the new Content Layer API and use `id` instead. */
67
+ slug: string;
68
+ /** The slug or unique ID if using the `legacy.collections` flag. */
69
+ id: string;
70
+ /** True if this page is untranslated in the current language and using fallback content from the default locale. */
71
+ isFallback?: true;
72
+ [key: string]: unknown;
73
+ }
74
+
75
+ export interface StarlightRouteData extends Route {
76
+ /** Title of the site. */
77
+ siteTitle: string;
78
+ /** URL or path used as the link when clicking on the site title. */
79
+ siteTitleHref: string;
80
+ /** Array of Markdown headings extracted from the current page. */
81
+ headings: MarkdownHeading[];
82
+ /** Site navigation sidebar entries for this page. */
83
+ sidebar: SidebarEntry[];
84
+ /** Whether or not the sidebar should be displayed on this page. */
85
+ hasSidebar: boolean;
86
+ /** Links to the previous and next page in the sidebar if enabled. */
87
+ pagination: PaginationLinks;
88
+ /** Table of contents for this page if enabled. */
89
+ toc: { minHeadingLevel: number; maxHeadingLevel: number; items: TocItem[] } | undefined;
90
+ /** JS Date object representing when this page was last updated if enabled. */
91
+ lastUpdated: Date | undefined;
92
+ /** URL object for the address where this page can be edited if enabled. */
93
+ editUrl: URL | undefined;
94
+ /** An Astro component to render the current page’s content if this route is a Markdown page. */
95
+ Content?: RenderResult['Content'];
96
+ }
package/utils/slugs.ts CHANGED
@@ -1,16 +1,8 @@
1
1
  import config from 'virtual:starlight/user-config';
2
+ import { slugToLocale as getLocaleFromSlug } from '../integrations/shared/slugToLocale';
2
3
  import { BuiltInDefaultLocale } from './i18n';
3
4
  import { stripTrailingSlash } from './path';
4
- import { slugToLocale as getLocaleFromSlug } from '../integrations/shared/slugToLocale';
5
-
6
- export interface LocaleData {
7
- /** Writing direction. */
8
- dir: 'ltr' | 'rtl';
9
- /** BCP-47 language tag. */
10
- lang: string;
11
- /** The base path at which a language is served. `undefined` for root locale slugs. */
12
- locale: string | undefined;
13
- }
5
+ import type { LocaleData } from './routing/types';
14
6
 
15
7
  /**
16
8
  * Get the “locale” of a slug. This is the base path at which a language is served.
@@ -5,19 +5,12 @@ 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 {
9
- getSiteTitle,
10
- getSiteTitleHref,
11
- getToC,
12
- type PageProps,
13
- type StarlightRouteData,
14
- } from './route-data';
15
- import type { StarlightDocsEntry } from './routing';
8
+ import { getSiteTitle, getSiteTitleHref, getToC, type PageProps } from './routing/data';
9
+ import type { StarlightDocsEntry, StarlightRouteData } from './routing/types';
16
10
  import { slugToLocaleData, urlToSlug } from './slugs';
17
11
  import { getPrevNextLinks, getSidebar, getSidebarFromConfig } from './navigation';
18
12
  import { docsSchema } from '../schema';
19
13
  import type { Prettify, RemoveIndexSignature } from './types';
20
- import { DeprecatedLabelsPropProxy } from './i18n';
21
14
  import { SidebarItemSchema } from '../schemas/sidebar';
22
15
  import type { StarlightConfig, StarlightUserConfig } from './user-config';
23
16
 
@@ -153,7 +146,6 @@ export async function generateStarlightPageRouteData({
153
146
  entryMeta,
154
147
  hasSidebar: props.hasSidebar ?? entry.data.template !== 'splash',
155
148
  headings,
156
- labels: DeprecatedLabelsPropProxy,
157
149
  lastUpdated,
158
150
  pagination: getPrevNextLinks(sidebar, config.pagination, entry.data),
159
151
  sidebar,
@@ -226,6 +226,14 @@ const UserConfigSchema = z.object({
226
226
  .boolean()
227
227
  .default(false)
228
228
  .describe('Enable displaying a “Built with Starlight” link in your site’s footer.'),
229
+
230
+ /** Add middleware to process Starlight’s route data for each page. */
231
+ routeMiddleware: z
232
+ .string()
233
+ .transform((string) => [string])
234
+ .or(z.string().array())
235
+ .default([])
236
+ .describe('Add middleware to process Starlight’s route data for each page.'),
229
237
  });
230
238
 
231
239
  export const StarlightConfigSchema = UserConfigSchema.strict()
@@ -16,6 +16,10 @@ declare module 'virtual:starlight/collection-config' {
16
16
  export const collections: import('astro:content').ContentConfig['collections'] | undefined;
17
17
  }
18
18
 
19
+ declare module 'virtual:starlight/route-middleware' {
20
+ export const routeMiddleware: Array<import('./route-data').RouteMiddlewareHandler>;
21
+ }
22
+
19
23
  declare module 'virtual:starlight/pagefind-config' {
20
24
  export const pagefindUserConfig: Partial<
21
25
  Extract<import('./types').StarlightConfig['pagefind'], object>