@astrojs/starlight 0.26.4 → 0.27.0

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,34 @@
1
1
  # @astrojs/starlight
2
2
 
3
+ ## 0.27.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1255](https://github.com/withastro/starlight/pull/1255) [`6f3202b`](https://github.com/withastro/starlight/commit/6f3202b3eb747de8a1cfcba001ab618d5fdee44a) Thanks [@Fryuni](https://github.com/Fryuni)! - Adds support for server-rendered Starlight pages.
8
+
9
+ When building a project with `hybrid` or `server` output mode, a new `prerender` option on Starlight config can be set to `false` to make all Starlight pages be rendered on-demand:
10
+
11
+ ```ts
12
+ export default defineConfig({
13
+ output: 'server',
14
+ integrations: [
15
+ starlight({
16
+ prerender: false,
17
+ }),
18
+ ],
19
+ });
20
+ ```
21
+
22
+ ### Patch Changes
23
+
24
+ - [#2242](https://github.com/withastro/starlight/pull/2242) [`756e85e`](https://github.com/withastro/starlight/commit/756e85e8e814657c42c4a6f9c299b5bef32aee22) Thanks [@delucis](https://github.com/delucis)! - Refactors the logic for persisting and restoring sidebar state across navigations for better performance on slow or busy devices
25
+
26
+ - [#1255](https://github.com/withastro/starlight/pull/1255) [`6f3202b`](https://github.com/withastro/starlight/commit/6f3202b3eb747de8a1cfcba001ab618d5fdee44a) Thanks [@Fryuni](https://github.com/Fryuni)! - Improves performance of computing the last updated times from Git history.
27
+
28
+ Instead of executing `git` for each docs page, it is now executed twice regardless of the number of pages.
29
+
30
+ - [#1255](https://github.com/withastro/starlight/pull/1255) [`6f3202b`](https://github.com/withastro/starlight/commit/6f3202b3eb747de8a1cfcba001ab618d5fdee44a) Thanks [@Fryuni](https://github.com/Fryuni)! - Fixes last updated times on projects with custom `srcDir`
31
+
3
32
  ## 0.26.4
4
33
 
5
34
  ### Patch Changes
@@ -2,46 +2,16 @@
2
2
  import type { Props } from '../props';
3
3
 
4
4
  import MobileMenuFooter from 'virtual:starlight/components/MobileMenuFooter';
5
- import { getSidebarHash } from '../utils/navigation';
5
+ import SidebarPersister from './SidebarPersister.astro';
6
6
  import SidebarSublist from './SidebarSublist.astro';
7
7
 
8
8
  const { sidebar } = Astro.props;
9
- const hash = getSidebarHash(sidebar);
10
9
  ---
11
10
 
12
- <sl-sidebar-state-persist data-hash={hash}>
11
+ <SidebarPersister {...Astro.props}>
13
12
  <SidebarSublist sublist={sidebar} />
14
- </sl-sidebar-state-persist>
13
+ </SidebarPersister>
14
+
15
15
  <div class="md:sl-hidden">
16
16
  <MobileMenuFooter {...Astro.props} />
17
17
  </div>
18
-
19
- {
20
- /*
21
- Inline script to restore sidebar state as soon as possible.
22
- - On smaller viewports, restoring state is skipped as the sidebar is collapsed inside a menu.
23
- - The state is parsed from session storage and restored.
24
- - This is a progressive enhancement, so any errors are swallowed silently.
25
- */
26
- }
27
- <script is:inline>
28
- (() => {
29
- try {
30
- if (!matchMedia('(min-width: 50em)').matches) return;
31
- const scroller = document.getElementById('starlight__sidebar');
32
- /** @type {HTMLElement | null} */
33
- const target = document.querySelector('sl-sidebar-state-persist');
34
- const state = JSON.parse(sessionStorage.getItem('sl-sidebar-state') || '0');
35
- if (!scroller || !target || !state || target.dataset.hash !== state.hash) return;
36
- target
37
- .querySelectorAll('details')
38
- .forEach((el, idx) => typeof state.open[idx] === 'boolean' && (el.open = state.open[idx]));
39
- scroller.scrollTop = state.scroll;
40
- } catch {}
41
- })();
42
- </script>
43
- <style>
44
- sl-sidebar-state-persist {
45
- display: contents;
46
- }
47
- </style>
@@ -1,7 +1,6 @@
1
1
  // Collect required elements from the DOM.
2
2
  const scroller = document.getElementById('starlight__sidebar');
3
3
  const target = scroller?.querySelector<HTMLElement>('sl-sidebar-state-persist');
4
- const details = [...(target?.querySelectorAll('details') || [])];
5
4
 
6
5
  /** Starlight uses this key to store sidebar state in `sessionStorage`. */
7
6
  const storageKey = 'sl-sidebar-state';
@@ -58,8 +57,9 @@ target?.addEventListener('click', (event) => {
58
57
  // This excludes clicks outside of the `<summary>`, which don’t trigger toggles.
59
58
  const toggledDetails = event.target.closest('summary')?.closest('details');
60
59
  if (!toggledDetails) return;
61
- const index = details.indexOf(toggledDetails);
62
- if (index === -1) return;
60
+ const restoreElement = toggledDetails.querySelector<HTMLElement>('sl-sidebar-restore');
61
+ const index = parseInt(restoreElement?.dataset.index || '');
62
+ if (isNaN(index)) return;
63
63
  setToggleState(!toggledDetails.open, index);
64
64
  });
65
65
 
@@ -0,0 +1,72 @@
1
+ ---
2
+ /*
3
+ This component is designed to wrap the tree of `<SidebarSublist>` components in the sidebar.
4
+
5
+ It does the following:
6
+ - Wraps the tree in an `<sl-sidebar-state-persist>` custom element
7
+ - Before the tree renders, adds an inline script which loads state and defines
8
+ the behaviour for the `<sl-sidebar-restore>` custom element.
9
+ - After the tree renders, adds an inline script which restores the sidebar scroll state.
10
+
11
+ Notes:
12
+ - On smaller viewports, restoring state is skipped as the sidebar is collapsed inside a menu.
13
+ - The state is parsed from session storage and restored.
14
+ - This is a progressive enhancement, so any errors are swallowed silently.
15
+ */
16
+
17
+ import type { Props } from '../props';
18
+ import { getSidebarHash } from '../utils/navigation';
19
+
20
+ const hash = getSidebarHash(Astro.props.sidebar);
21
+
22
+ declare global {
23
+ interface Window {
24
+ /** Restored scroll position. Briefly stored on the `window` global to pass between inline scripts. */
25
+ _starlightScrollRestore?: number;
26
+ }
27
+ }
28
+ ---
29
+
30
+ <sl-sidebar-state-persist data-hash={hash}>
31
+ <script is:inline>
32
+ (() => {
33
+ try {
34
+ if (!matchMedia('(min-width: 50em)').matches) return;
35
+ /** @type {HTMLElement | null} */
36
+ const target = document.querySelector('sl-sidebar-state-persist');
37
+ const state = JSON.parse(sessionStorage.getItem('sl-sidebar-state') || '0');
38
+ if (!target || !state || target.dataset.hash !== state.hash) return;
39
+ window._starlightScrollRestore = state.scroll;
40
+ customElements.define(
41
+ 'sl-sidebar-restore',
42
+ class SidebarRestore extends HTMLElement {
43
+ connectedCallback() {
44
+ try {
45
+ const idx = parseInt(this.dataset.index || '');
46
+ const details = this.closest('details');
47
+ if (details && typeof state.open[idx] === 'boolean') details.open = state.open[idx];
48
+ } catch {}
49
+ }
50
+ }
51
+ );
52
+ } catch {}
53
+ })();
54
+ </script>
55
+
56
+ <slot />
57
+
58
+ <script is:inline>
59
+ (() => {
60
+ const scroller = document.getElementById('starlight__sidebar');
61
+ if (!window._starlightScrollRestore || !scroller) return;
62
+ scroller.scrollTop = window._starlightScrollRestore;
63
+ delete window._starlightScrollRestore;
64
+ })();
65
+ </script>
66
+ </sl-sidebar-state-persist>
67
+
68
+ <style>
69
+ sl-sidebar-state-persist {
70
+ display: contents;
71
+ }
72
+ </style>
@@ -0,0 +1,12 @@
1
+ ---
2
+ /** Unique symbol for storing a running index in `locals`. */
3
+ const currentGroupIndexSymbol = Symbol.for('starlight-sidebar-group-index');
4
+ const locals = Astro.locals as Record<typeof currentGroupIndexSymbol, number>;
5
+
6
+ /** The current sidebar group’s index retrieved from `locals` if set, starting at `0`. */
7
+ const index = locals[currentGroupIndexSymbol] || 0;
8
+ // Increment the index for the next instance.
9
+ locals[currentGroupIndexSymbol] = index + 1;
10
+ ---
11
+
12
+ <sl-sidebar-restore data-index={index}></sl-sidebar-restore>
@@ -2,6 +2,7 @@
2
2
  import { flattenSidebar, type SidebarEntry } from '../utils/navigation';
3
3
  import Icon from '../user-components/Icon.astro';
4
4
  import Badge from '../user-components/Badge.astro';
5
+ import SidebarRestorePoint from './SidebarRestorePoint.astro';
5
6
 
6
7
  interface Props {
7
8
  sublist: SidebarEntry[];
@@ -35,6 +36,7 @@ const { sublist, nested } = Astro.props;
35
36
  <details
36
37
  open={flattenSidebar(entry.entries).some((i) => i.isCurrent) || !entry.collapsed}
37
38
  >
39
+ <SidebarRestorePoint />
38
40
  <summary>
39
41
  <div class="group-label">
40
42
  <span class="large">{entry.label}</span>
package/index.ts CHANGED
@@ -50,17 +50,20 @@ export default function StarlightIntegration({
50
50
  if (!starlightConfig.disable404Route) {
51
51
  injectRoute({
52
52
  pattern: '404',
53
- entrypoint: '@astrojs/starlight/404.astro',
54
- // Ensure page is pre-rendered even when project is on server output mode
55
- prerender: true,
53
+ entrypoint: starlightConfig.prerender
54
+ ? '@astrojs/starlight/routes/static/404.astro'
55
+ : '@astrojs/starlight/routes/ssr/404.astro',
56
+ prerender: starlightConfig.prerender,
56
57
  });
57
58
  }
58
59
  injectRoute({
59
60
  pattern: '[...slug]',
60
- entrypoint: '@astrojs/starlight/index.astro',
61
- // Ensure page is pre-rendered even when project is on server output mode
62
- prerender: true,
61
+ entrypoint: starlightConfig.prerender
62
+ ? '@astrojs/starlight/routes/static/index.astro'
63
+ : '@astrojs/starlight/routes/ssr/index.astro',
64
+ prerender: starlightConfig.prerender,
63
65
  });
66
+
64
67
  // Add built-in integrations only if they are not already added by the user through the
65
68
  // config or by a plugin.
66
69
  const allIntegrations = [...config.integrations, ...integrations];
@@ -73,6 +76,7 @@ export default function StarlightIntegration({
73
76
  if (!allIntegrations.find(({ name }) => name === '@astrojs/mdx')) {
74
77
  integrations.push(mdx({ optimize: true }));
75
78
  }
79
+
76
80
  // Add Starlight directives restoration integration at the end of the list so that remark
77
81
  // plugins injected by Starlight plugins through Astro integrations can handle text and
78
82
  // leaf directives before they are transformed back to their original form.
@@ -87,7 +91,7 @@ export default function StarlightIntegration({
87
91
 
88
92
  updateConfig({
89
93
  vite: {
90
- plugins: [vitePluginStarlightUserConfig(starlightConfig, config)],
94
+ plugins: [vitePluginStarlightUserConfig(command, starlightConfig, config)],
91
95
  },
92
96
  markdown: {
93
97
  remarkPlugins: [
@@ -1,7 +1,8 @@
1
- import type { AstroConfig, ViteUserConfig } from 'astro';
1
+ import type { AstroConfig, HookParameters, ViteUserConfig } from 'astro';
2
2
  import { resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import type { StarlightConfig } from '../utils/user-config';
5
+ import { getAllNewestCommitDate } from '../utils/git';
5
6
 
6
7
  function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
7
8
  return `\0${id}`;
@@ -9,6 +10,7 @@ function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
9
10
 
10
11
  /** Vite plugin that exposes Starlight user config and project context via virtual modules. */
11
12
  export function vitePluginStarlightUserConfig(
13
+ command: HookParameters<'astro:config:setup'>['command'],
12
14
  opts: StarlightConfig,
13
15
  {
14
16
  build,
@@ -29,6 +31,8 @@ export function vitePluginStarlightUserConfig(
29
31
  const resolveId = (id: string, base = root) =>
30
32
  JSON.stringify(id.startsWith('.') ? resolve(fileURLToPath(base), id) : id);
31
33
 
34
+ const docsPath = resolve(fileURLToPath(srcDir), 'content/docs');
35
+
32
36
  const virtualComponentModules = Object.fromEntries(
33
37
  Object.entries(opts.components).map(([name, path]) => [
34
38
  `virtual:starlight/components/${name}`,
@@ -45,6 +49,13 @@ export function vitePluginStarlightUserConfig(
45
49
  srcDir,
46
50
  trailingSlash,
47
51
  })}`,
52
+ 'virtual:starlight/git-info':
53
+ (command !== 'build'
54
+ ? `import { makeAPI } from '${new URL('../utils/git.ts', import.meta.url)}';` +
55
+ `const api = makeAPI(${JSON.stringify(docsPath)});`
56
+ : `import { makeAPI } from '${new URL('../utils/gitInlined.ts', import.meta.url)}';` +
57
+ `const api = makeAPI(${JSON.stringify(getAllNewestCommitDate(docsPath))});`) +
58
+ 'export const getNewestCommitDate = api.getNewestCommitDate;',
48
59
  'virtual:starlight/user-css': opts.customCss.map((id) => `import ${resolveId(id)};`).join(''),
49
60
  'virtual:starlight/user-images': opts.logo
50
61
  ? 'src' in opts.logo
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/starlight",
3
- "version": "0.26.4",
3
+ "version": "0.27.0",
4
4
  "description": "Build beautiful, high-performance documentation websites with Astro",
5
5
  "keywords": [
6
6
  "docs",
@@ -162,8 +162,7 @@
162
162
  "types": "./integrations/expressive-code/hast.d.ts",
163
163
  "default": "./integrations/expressive-code/hast.mjs"
164
164
  },
165
- "./index.astro": "./index.astro",
166
- "./404.astro": "./404.astro",
165
+ "./routes/*": "./routes/*",
167
166
  "./style/markdown.css": "./style/markdown.css"
168
167
  },
169
168
  "peerDependencies": {
@@ -174,8 +173,9 @@
174
173
  "@playwright/test": "^1.45.0",
175
174
  "@types/node": "^18.16.19",
176
175
  "@vitest/coverage-v8": "^1.6.0",
177
- "astro": "^4.10.2",
178
- "vitest": "^1.6.0"
176
+ "astro": "^4.15.3",
177
+ "vitest": "^1.6.0",
178
+ "linkedom": "^0.18.4"
179
179
  },
180
180
  "dependencies": {
181
181
  "@astrojs/mdx": "^3.1.3",
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { generateRouteData } from '../utils/route-data';
3
+ import type { Route } from '../utils/routing';
4
+ import Page from '../components/Page.astro';
5
+
6
+ export type Props = {
7
+ route: Route;
8
+ };
9
+
10
+ const { route } = Astro.props;
11
+
12
+ const { Content, headings } = await route.entry.render();
13
+ const routeData = generateRouteData({ props: { ...route, headings }, url: Astro.url });
14
+ ---
15
+
16
+ <Page {...routeData}><Content frontmatter={route.entry.data} /></Page>
@@ -0,0 +1,7 @@
1
+ ---
2
+ import FourOhFour from '../static/404.astro';
3
+
4
+ export const prerender = false;
5
+ ---
6
+
7
+ <FourOhFour />
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { getRouteBySlugParam } from '../../utils/routing';
3
+ import CommonPage from '../common.astro';
4
+
5
+ export const prerender = false;
6
+
7
+ const route = getRouteBySlugParam(Astro.params.slug);
8
+
9
+ if (route === undefined) {
10
+ return new Response(null, { status: 404 });
11
+ }
12
+ ---
13
+
14
+ <CommonPage route={route} />
@@ -1,12 +1,11 @@
1
1
  ---
2
2
  import { getEntry } from 'astro:content';
3
3
  import config from 'virtual:starlight/user-config';
4
- import EmptyContent from './components/EmptyMarkdown.md';
5
- import Page from './components/Page.astro';
6
- import { generateRouteData } from './utils/route-data';
7
- import type { StarlightDocsEntry } from './utils/routing';
8
- import { useTranslations } from './utils/translations';
9
- import { BuiltInDefaultLocale } from './utils/i18n';
4
+ import EmptyContent from '../../components/EmptyMarkdown.md';
5
+ import type { Route, StarlightDocsEntry } from '../../utils/routing';
6
+ import { useTranslations } from '../../utils/translations';
7
+ import { BuiltInDefaultLocale } from '../../utils/i18n';
8
+ import CommonPage from '../common.astro';
10
9
 
11
10
  export const prerender = true;
12
11
 
@@ -42,11 +41,7 @@ const fallbackEntry: StarlightDocsEntry = {
42
41
 
43
42
  const userEntry = await getEntry('docs', '404');
44
43
  const entry = userEntry || fallbackEntry;
45
- const { Content, headings } = await entry.render();
46
- const route = generateRouteData({
47
- props: { ...entryMeta, entryMeta, headings, entry, id: entry.id, slug: entry.slug },
48
- url: Astro.url,
49
- });
44
+ const route: Route = { ...entryMeta, entryMeta, entry, id: entry.id, slug: entry.slug };
50
45
  ---
51
46
 
52
- <Page {...route}><Content /></Page>
47
+ <CommonPage {route} />
@@ -0,0 +1,15 @@
1
+ ---
2
+ import type { InferGetStaticPropsType } from 'astro';
3
+ import { paths } from '../../utils/routing';
4
+ import CommonPage from '../common.astro';
5
+
6
+ export const prerender = true;
7
+
8
+ export async function getStaticPaths() {
9
+ return paths;
10
+ }
11
+
12
+ type Props = InferGetStaticPropsType<typeof getStaticPaths>;
13
+ ---
14
+
15
+ <CommonPage route={Astro.props} />
package/utils/git.ts CHANGED
@@ -1,7 +1,22 @@
1
- import { basename, dirname } from 'node:path';
1
+ /**
2
+ * Git module to be used from the dev server and from the integration.
3
+ */
4
+
5
+ import { basename, dirname, relative, resolve } from 'node:path';
6
+ import { realpathSync } from 'node:fs';
2
7
  import { spawnSync } from 'node:child_process';
3
8
 
4
- export function getNewestCommitDate(file: string) {
9
+ export type GitAPI = {
10
+ getNewestCommitDate: (file: string) => Date;
11
+ };
12
+
13
+ export const makeAPI = (directory: string): GitAPI => {
14
+ return {
15
+ getNewestCommitDate: (file) => getNewestCommitDate(resolve(directory, file)),
16
+ };
17
+ };
18
+
19
+ export function getNewestCommitDate(file: string): Date {
5
20
  const result = spawnSync('git', ['log', '--format=%ct', '--max-count=1', basename(file)], {
6
21
  cwd: dirname(file),
7
22
  encoding: 'utf-8',
@@ -22,3 +37,76 @@ export function getNewestCommitDate(file: string) {
22
37
  const date = new Date(timestamp * 1000);
23
38
  return date;
24
39
  }
40
+
41
+ function getRepoRoot(directory: string): string {
42
+ const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
43
+ cwd: directory,
44
+ encoding: 'utf-8',
45
+ });
46
+
47
+ if (result.error) {
48
+ return directory;
49
+ }
50
+
51
+ try {
52
+ return realpathSync(result.stdout.trim());
53
+ } catch {
54
+ return directory;
55
+ }
56
+ }
57
+
58
+ export function getAllNewestCommitDate(directory: string): [string, number][] {
59
+ const repoRoot = getRepoRoot(directory);
60
+
61
+ const gitLog = spawnSync(
62
+ 'git',
63
+ [
64
+ 'log',
65
+ // Format each history entry as t:<seconds since epoch>
66
+ '--format=t:%ct',
67
+ // In each entry include the name and status for each modified file
68
+ '--name-status',
69
+ '--',
70
+ directory,
71
+ ],
72
+ {
73
+ cwd: repoRoot,
74
+ encoding: 'utf-8',
75
+ }
76
+ );
77
+
78
+ if (gitLog.error) {
79
+ return [];
80
+ }
81
+
82
+ let runningDate = Date.now();
83
+ const latestDates = new Map<string, number>();
84
+
85
+ for (const logLine of gitLog.stdout.split('\n')) {
86
+ if (logLine.startsWith('t:')) {
87
+ // t:<seconds since epoch>
88
+ runningDate = Number.parseInt(logLine.slice(2)) * 1000;
89
+ }
90
+
91
+ // - Added files take the format `A\t<file>`
92
+ // - Modified files take the format `M\t<file>`
93
+ // - Deleted files take the format `D\t<file>`
94
+ // - Renamed files take the format `R<count>\t<old>\t<new>`
95
+ // - Copied files take the format `C<count>\t<old>\t<new>`
96
+ // The name of the file as of the commit being processed is always
97
+ // the last part of the log line.
98
+ const tabSplit = logLine.lastIndexOf('\t');
99
+ if (tabSplit === -1) continue;
100
+ const fileName = logLine.slice(tabSplit + 1);
101
+
102
+ const currentLatest = latestDates.get(fileName) || 0;
103
+ latestDates.set(fileName, Math.max(currentLatest, runningDate));
104
+ }
105
+
106
+ return Array.from(latestDates.entries()).map(([file, date]) => {
107
+ const fileFullPath = resolve(repoRoot, file);
108
+ const fileInDirectory = relative(directory, fileFullPath);
109
+
110
+ return [fileInDirectory, date];
111
+ });
112
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Git module to be used on production build results.
3
+ * The API is based on inlined git information.
4
+ */
5
+
6
+ import type { GitAPI, getAllNewestCommitDate } from './git';
7
+
8
+ type InlinedData = ReturnType<typeof getAllNewestCommitDate>;
9
+
10
+ export const makeAPI = (data: InlinedData): GitAPI => {
11
+ const trackedDocsFiles = new Map(data);
12
+
13
+ return {
14
+ getNewestCommitDate: (file) => {
15
+ const timestamp = trackedDocsFiles.get(file);
16
+ if (!timestamp) throw new Error(`Failed to retrieve the git history for file "${file}"`);
17
+ return new Date(timestamp);
18
+ },
19
+ };
20
+ };
package/utils/plugins.ts CHANGED
@@ -2,6 +2,7 @@ import type { AstroIntegration } from 'astro';
2
2
  import { z } from 'astro/zod';
3
3
  import { StarlightConfigSchema, type StarlightUserConfig } from '../utils/user-config';
4
4
  import { parseWithFriendlyErrors } from '../utils/error-map';
5
+ import { AstroError } from 'astro/errors';
5
6
 
6
7
  /**
7
8
  * Runs Starlight plugins in the order that they are configured after validating the user-provided
@@ -72,6 +73,14 @@ export async function runPlugins(
72
73
  });
73
74
  }
74
75
 
76
+ if (context.config.output === 'static' && !starlightConfig.prerender) {
77
+ throw new AstroError(
78
+ 'Starlight’s `prerender: false` option requires `output: "hybrid"` or `"server"` in your Astro config.',
79
+ 'Either set `output` in your Astro config or set `prerender: true` in the Starlight options.\n\n' +
80
+ 'Learn more about rendering modes in the Astro docs: https://docs.astro.build/en/basics/rendering-modes/'
81
+ );
82
+ }
83
+
75
84
  return { integrations, starlightConfig };
76
85
  }
77
86
 
@@ -1,9 +1,8 @@
1
1
  import type { MarkdownHeading } from 'astro';
2
- import { fileURLToPath } from 'node:url';
3
2
  import project from 'virtual:starlight/project-context';
4
3
  import config from 'virtual:starlight/user-config';
5
4
  import { generateToC, type TocItem } from './generateToC';
6
- import { getNewestCommitDate } from './git';
5
+ import { getNewestCommitDate } from 'virtual:starlight/git-info';
7
6
  import { getPrevNextLinks, getSidebar, type SidebarEntry } from './navigation';
8
7
  import { ensureTrailingSlash } from './path';
9
8
  import type { Route } from './routing';
@@ -82,11 +81,10 @@ function getLastUpdated({ entry }: PageProps): Date | undefined {
82
81
  const { lastUpdated: configLastUpdated } = config;
83
82
 
84
83
  if (frontmatterLastUpdated ?? configLastUpdated) {
85
- const currentFilePath = fileURLToPath(new URL('src/content/docs/' + entry.id, project.root));
86
84
  try {
87
85
  return frontmatterLastUpdated instanceof Date
88
86
  ? frontmatterLastUpdated
89
- : getNewestCommitDate(currentFilePath);
87
+ : getNewestCommitDate(entry.id);
90
88
  } catch {
91
89
  // If the git command fails, ignore the error.
92
90
  return undefined;
package/utils/routing.ts CHANGED
@@ -100,6 +100,19 @@ function getRoutes(): Route[] {
100
100
  }
101
101
  export const routes = getRoutes();
102
102
 
103
+ function getParamRouteMapping(): ReadonlyMap<string | undefined, Route> {
104
+ const map = new Map<string | undefined, Route>();
105
+ for (const route of routes) {
106
+ map.set(slugToParam(route.slug), route);
107
+ }
108
+ return map;
109
+ }
110
+ const routesBySlugParam = getParamRouteMapping();
111
+
112
+ export function getRouteBySlugParam(slugParam: string | undefined): Route | undefined {
113
+ return routesBySlugParam.get(slugParam?.replace(/\/$/, '') || undefined);
114
+ }
115
+
103
116
  function getPaths(): Path[] {
104
117
  return routes.map((route) => ({
105
118
  params: { slug: slugToParam(route.slug) },
@@ -195,7 +195,7 @@ const UserConfigSchema = z.object({
195
195
  * Set to `false` to disable indexing your site with Pagefind.
196
196
  * This will also hide the default search UI if in use.
197
197
  */
198
- pagefind: z.boolean().default(true),
198
+ pagefind: z.boolean().optional(),
199
199
 
200
200
  /** Specify paths to components that should override Starlight’s default components */
201
201
  components: ComponentConfigSchema(),
@@ -209,6 +209,13 @@ const UserConfigSchema = z.object({
209
209
  /** Disable Starlight's default 404 page. */
210
210
  disable404Route: z.boolean().default(false).describe("Disable Starlight's default 404 page."),
211
211
 
212
+ /**
213
+ * Define whether Starlight pages should be prerendered or not.
214
+ * Defaults to always prerender Starlight pages, even when the project is
215
+ * set to "server" output mode.
216
+ */
217
+ prerender: z.boolean().default(true),
218
+
212
219
  /** Enable displaying a “Built with Starlight” link in your site’s footer. */
213
220
  credits: z
214
221
  .boolean()
@@ -216,8 +223,16 @@ const UserConfigSchema = z.object({
216
223
  .describe('Enable displaying a “Built with Starlight” link in your site’s footer.'),
217
224
  });
218
225
 
219
- export const StarlightConfigSchema = UserConfigSchema.strict().transform(
220
- ({ title, locales, defaultLocale, ...config }, ctx) => {
226
+ export const StarlightConfigSchema = UserConfigSchema.strict()
227
+ .transform((config) => ({
228
+ ...config,
229
+ // Pagefind only defaults to true if prerender is also true.
230
+ pagefind: config.pagefind ?? config.prerender,
231
+ }))
232
+ .refine((config) => !(!config.prerender && config.pagefind), {
233
+ message: 'Pagefind search is not support with prerendering disabled.',
234
+ })
235
+ .transform(({ title, locales, defaultLocale, ...config }, ctx) => {
221
236
  const configuredLocales = Object.keys(locales ?? {});
222
237
 
223
238
  // This is a multilingual site (more than one locale configured) or a monolingual site with
@@ -286,8 +301,7 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform(
286
301
  defaultLocale: defaultLocaleConfig,
287
302
  locales: undefined,
288
303
  } as const;
289
- }
290
- );
304
+ });
291
305
 
292
306
  export type StarlightConfig = z.infer<typeof StarlightConfigSchema>;
293
307
  export type StarlightUserConfig = z.input<typeof StarlightConfigSchema>;
package/virtual.d.ts CHANGED
@@ -14,6 +14,10 @@ declare module 'virtual:starlight/project-context' {
14
14
  export default ProjectContext;
15
15
  }
16
16
 
17
+ declare module 'virtual:starlight/git-info' {
18
+ export function getNewestCommitDate(file: string): Date;
19
+ }
20
+
17
21
  declare module 'virtual:starlight/user-css' {}
18
22
 
19
23
  declare module 'virtual:starlight/user-images' {
package/index.astro DELETED
@@ -1,19 +0,0 @@
1
- ---
2
- import type { InferGetStaticPropsType } from 'astro';
3
- import { generateRouteData } from './utils/route-data';
4
- import { paths } from './utils/routing';
5
-
6
- import Page from './components/Page.astro';
7
-
8
- export const prerender = true;
9
-
10
- export async function getStaticPaths() {
11
- return paths;
12
- }
13
-
14
- type Props = InferGetStaticPropsType<typeof getStaticPaths>;
15
- const { Content, headings } = await Astro.props.entry.render();
16
- const route = generateRouteData({ props: { ...Astro.props, headings }, url: Astro.url });
17
- ---
18
-
19
- <Page {...route}><Content frontmatter={Astro.props.entry.data} /></Page>