From fdb96f3dd18fd898d519a14929ec64d46fb96e7b Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Thu, 30 Nov 2023 20:53:46 +0100 Subject: [PATCH 01/37] feat: add basic virtual page prototype --- docs/astro.config.mjs | 3 + docs/package.json | 3 +- docs/src/content/docs/reference/plugins.md | 149 ++++++++++ .../basics/virtual-route-data.test.ts | 262 ++++++++++++++++++ .../starlight/components/VirtualPage.astro | 10 + packages/starlight/package.json | 4 + packages/starlight/utils/route-data.ts | 70 ++++- packages/starlight/utils/routing.ts | 32 ++- packages/virtual-pages-demo/Route.astro | 80 ++++++ packages/virtual-pages-demo/index.ts | 36 +++ packages/virtual-pages-demo/package.json | 23 ++ packages/virtual-pages-demo/things.ts | 4 + pnpm-lock.yaml | 9 + 13 files changed, 675 insertions(+), 10 deletions(-) create mode 100644 packages/starlight/__tests__/basics/virtual-route-data.test.ts create mode 100644 packages/starlight/components/VirtualPage.astro create mode 100644 packages/virtual-pages-demo/Route.astro create mode 100644 packages/virtual-pages-demo/index.ts create mode 100644 packages/virtual-pages-demo/package.json create mode 100644 packages/virtual-pages-demo/things.ts diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 3cbc155eed1..3acbaae8d5b 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -1,5 +1,6 @@ import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; +import { virtualPagesDemo } from 'virtual-pages-demo'; export const locales = { root: { label: 'English', lang: 'en' }, @@ -30,6 +31,8 @@ export default defineConfig({ trailingSlash: 'always', integrations: [ starlight({ + // TODO(HiDeoo) Remove me + plugins: [virtualPagesDemo()], title: 'Starlight', logo: { light: '/src/assets/logo-light.svg', diff --git a/docs/package.json b/docs/package.json index e071ee02e06..4bfe2fba2fe 100644 --- a/docs/package.json +++ b/docs/package.json @@ -17,7 +17,8 @@ "@types/culori": "^2.0.0", "astro": "^3.2.3", "culori": "^3.2.0", - "sharp": "^0.32.5" + "sharp": "^0.32.5", + "virtual-pages-demo": "workspace:*" }, "devDependencies": { "hast-util-from-html": "^1.0.2", diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index de859cf1053..35db2bda9a2 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -161,3 +161,152 @@ The example above will log a message that includes the provided info message: ```shell [long-process-plugin] Starting long process… ``` + +## Route injection + +Plugins [adding](#addintegration) an Astro integration can inject routes using the Integrations API [`injectRoute`](https://docs.astro.build/en/reference/integrations-reference/#injectroute-option) function to render dynamically generated content. +By default, pages rendered on custom routes do not use the Starlight layout. To use the Starlight layout, pages must wrap their content with the `` component. + +```astro +--- +// plugin/src/Example.astro +import VirtualPage, { + type VirtualPageProps, +} from '@astrojs/starlight/components/VirtualPage.astro'; +import CustomComponent from './CustomComponent.astro'; + +const props = { + title: 'My custom page', + slug: 'custom-page/example', + template: 'doc', + hasSidebar: true, + headings: [{ depth: 2, slug: 'description', text: 'Description' }], + dir: 'ltr', + lang: 'en', + pagefind: true, + head: [], +} satisfies VirtualPageProps; +--- + + +

Description

+ +
+``` + +### Props + +#### Required props + +The `` component requires the following props: + +##### `title` + +**type:** `string` + +The page title displayed at the top of the page, in browser tabs, and in page metadata. + +##### `slug` + +**Type:** `string` + +The slug of the page. + +##### `headings` + +Type: `{ depth: number; slug: string; text: string }[]` + +Array of all headings of the page. + +##### `template` + +**type:** `'doc' | 'splash'` + +Set the layout template for this page. +Use `'splash'` to use a wider layout without any sidebars. + +##### `pagefind` + +**type:** `boolean` + +Set whether this page should be included in the [Pagefind](https://pagefind.app/) search index. + +##### `dir` + +**Type:** `'ltr' | 'rtl'` + +Page writing direction. + +##### `lang` + +**Type:** `string` + +BCP-47 language tag for this page’s locale, e.g. `en`, `zh-CN`, or `pt-BR`. + +##### `head` + +**Type:** `{ tag: string; attrs: Record; content: string }[]` + +Additional tags to your page’s ``. Similar to the [global `head` option](/reference/configuration/#head). + +##### `hasSidebar` + +**Type:** `boolean` + +Whether or not the sidebar should be displayed on this page. + +#### Optional props + +Additionaly, the following props can be provided to customize the page: + +##### `description` + +**type:** `string` + +The page description is used for page metadata and will be picked up by search engines and in social media previews. + +##### `sidebar` + +**type:** `SidebarEntry[] | undefined` +**default:** the sidebar generated based on the [global `sidebar` config](/reference/configuration/#sidebar) + +Site navigation sidebar entries for this page or fallback to the global `sidebar` option if not provided. + +##### `tableOfContents` + +**type:** `false | { minHeadingLevel: number; maxHeadingLevel: number; }` + +Overrides the [global `tableOfContents` config](/reference/configuration/#tableofcontents). +Customize the heading levels to be included or set to `false` to hide the table of contents on this page. + +##### `lastUpdated` + +**type:** `Date` + +A valid [YAML timestamp](https://yaml.org/type/timestamp.html) to display the last updated date of the page. + +##### `prev` + +**type:** `boolean | string | { link?: string; label?: string }` + +Overrides the [global `pagination` option](/reference/configuration/#pagination). If a string is specified, the generated link text will be replaced and if an object is specified, both the link and the text will be overridden. + +##### `next` + +**type:** `boolean | string | { link?: string; label?: string }` + +Same as [`prev`](#prev) but for the next page link. + +##### `hero` + +**type:** [`HeroConfig`](/reference/frontmatter/#heroconfig) + +Add a hero component to the top of this page. Works well with `template: splash`. Similar to the [frontmatter `hero` option](/reference/frontmatter/#hero). + +##### `banner` + +**type:** `{ content: string }` + +Displays an announcement banner at the top of this page. + +The `content` value can include HTML for links or other content. diff --git a/packages/starlight/__tests__/basics/virtual-route-data.test.ts b/packages/starlight/__tests__/basics/virtual-route-data.test.ts new file mode 100644 index 00000000000..b4a89bfba9f --- /dev/null +++ b/packages/starlight/__tests__/basics/virtual-route-data.test.ts @@ -0,0 +1,262 @@ +import { expect, test, vi } from 'vitest'; +import { generateVirtualRouteData, type VirtualPageProps } from '../../utils/route-data'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + docs: [ + ['index.mdx', { title: 'Home Page' }], + ['getting-started.mdx', { title: 'Getting Started' }], + ], + }) +); + +const virtualPageProps: VirtualPageProps = { + dir: 'rtl', + hasSidebar: true, + head: [], + headings: [], + lang: 'ks', + lastUpdated: new Date(), + pagefind: true, + slug: 'test-slug', + template: 'doc', + title: 'This is a test title', +}; + +test('adds data to route shape', () => { + const data = generateVirtualRouteData({ + props: virtualPageProps, + url: new URL('https://example.com'), + }); + // Virtual pages respect the slug passed in props. + expect(data.slug).toBe(virtualPageProps.slug); + // Virtual pages generate an ID based on their slug. + expect(data.id).toBeDefined(); + // Virtual pages cannot be fallbacks. + expect(data.isFallback).toBeUndefined(); + // Virtual pages cannot be edited. + expect(data.editUrl).toBeUndefined(); + expect(data.entry.data.editUrl).toBe(false); + // Virtual pages are part of the docs collection. + expect(data.entry.collection).toBe('docs'); + // Virtual pages respect the passed data. + expect(data.hasSidebar).toBe(virtualPageProps.hasSidebar); + expect(data.entry.data.lastUpdated).toBe(virtualPageProps.lastUpdated); + expect(data.entry.data.pagefind).toBe(virtualPageProps.pagefind); + expect(data.entry.data.template).toBe(virtualPageProps.template); + expect(data.entry.data.title).toBe(virtualPageProps.title); + // Virtual pages respect the entry meta. + expect(data.entryMeta.dir).toBe(virtualPageProps.dir); + expect(data.entryMeta.lang).toBe(virtualPageProps.lang); +}); + +test('uses generated sidebar when no sidebar is provided', () => { + const data = generateVirtualRouteData({ + props: virtualPageProps, + url: new URL('https://example.com'), + }); + expect(data.sidebar.map((entry) => entry.label)).toMatchInlineSnapshot(` + [ + "Home Page", + "Getting Started", + ] + `); +}); + +test('uses provided sidebar if any', () => { + const data = generateVirtualRouteData({ + props: { + ...virtualPageProps, + sidebar: [ + { + type: 'link', + label: 'Custom link 1', + href: '/test/1', + isCurrent: false, + badge: undefined, + attrs: {}, + }, + { + type: 'link', + label: 'Custom link 2', + href: '/test/2', + isCurrent: false, + badge: undefined, + attrs: {}, + }, + ], + }, + url: new URL('https://example.com'), + }); + expect(data.sidebar.map((entry) => entry.label)).toMatchInlineSnapshot(` + [ + "Custom link 1", + "Custom link 2", + ] + `); +}); + +test('uses provided pagination if any', () => { + const data = generateVirtualRouteData({ + props: { + ...virtualPageProps, + prev: { + label: 'Previous link', + link: '/test/prev', + }, + next: { + label: 'Next link', + link: '/test/next', + }, + }, + url: new URL('https://example.com'), + }); + expect(data.pagination).toMatchInlineSnapshot(` + { + "next": { + "attrs": {}, + "badge": undefined, + "href": "/test/next", + "isCurrent": false, + "label": "Next link", + "type": "link", + }, + "prev": { + "attrs": {}, + "badge": undefined, + "href": "/test/prev", + "isCurrent": false, + "label": "Previous link", + "type": "link", + }, + } + `); +}); + +test('generates the table of contents for provided headings', () => { + const data = generateVirtualRouteData({ + props: { + ...virtualPageProps, + headings: [ + { depth: 2, slug: 'heading-1', text: 'Heading 1' }, + { depth: 3, slug: 'heading-2', text: 'Heading 2' }, + // Should be ignored as it's too deep with default config. + { depth: 4, slug: 'heading-3', text: 'Heading 3' }, + ], + }, + url: new URL('https://example.com'), + }); + expect(data.toc).toMatchInlineSnapshot(` + { + "items": [ + { + "children": [], + "depth": 2, + "slug": "_top", + "text": "Overview", + }, + { + "children": [ + { + "children": [], + "depth": 3, + "slug": "heading-2", + "text": "Heading 2", + }, + ], + "depth": 2, + "slug": "heading-1", + "text": "Heading 1", + }, + ], + "maxHeadingLevel": 3, + "minHeadingLevel": 2, + } + `); +}); + +test('respects the `tableOfContents` level configuration', () => { + const data = generateVirtualRouteData({ + props: { + ...virtualPageProps, + headings: [ + // Should be ignored as it's not deep enough. + { depth: 2, slug: 'heading-1', text: 'Heading 1' }, + { depth: 3, slug: 'heading-2', text: 'Heading 2' }, + { depth: 4, slug: 'heading-3', text: 'Heading 3' }, + ], + tableOfContents: { + minHeadingLevel: 3, + maxHeadingLevel: 4, + }, + }, + url: new URL('https://example.com'), + }); + expect(data.toc).toMatchInlineSnapshot(` + { + "items": [ + { + "children": [ + { + "children": [ + { + "children": [], + "depth": 4, + "slug": "heading-3", + "text": "Heading 3", + }, + ], + "depth": 3, + "slug": "heading-2", + "text": "Heading 2", + }, + ], + "depth": 2, + "slug": "_top", + "text": "Overview", + }, + ], + "maxHeadingLevel": 4, + "minHeadingLevel": 3, + } + `); +}); + +test('disables table of contents if frontmatter includes `tableOfContents: false`', () => { + const data = generateVirtualRouteData({ + props: { + ...virtualPageProps, + headings: [ + { depth: 2, slug: 'heading-1', text: 'Heading 1' }, + { depth: 3, slug: 'heading-2', text: 'Heading 2' }, + ], + tableOfContents: false, + }, + url: new URL('https://example.com'), + }); + expect(data.toc).toBeUndefined(); +}); + +test('disables table of contents for splash template', () => { + const data = generateVirtualRouteData({ + props: { + ...virtualPageProps, + headings: [ + { depth: 2, slug: 'heading-1', text: 'Heading 1' }, + { depth: 3, slug: 'heading-2', text: 'Heading 2' }, + ], + template: 'splash', + }, + url: new URL('https://example.com'), + }); + expect(data.toc).toBeUndefined(); +}); + +test('includes localized labels', () => { + const data = generateVirtualRouteData({ + props: virtualPageProps, + url: new URL('https://example.com'), + }); + expect(data.labels).toBeDefined(); + expect(data.labels['skipLink.label']).toBe('Skip to content'); +}); diff --git a/packages/starlight/components/VirtualPage.astro b/packages/starlight/components/VirtualPage.astro new file mode 100644 index 00000000000..21d72c64610 --- /dev/null +++ b/packages/starlight/components/VirtualPage.astro @@ -0,0 +1,10 @@ +--- +import { generateVirtualRouteData, type VirtualPageProps as Props } from '../utils/route-data'; +import Page from './Page.astro'; + +export type VirtualPageProps = Props; +--- + + + + diff --git a/packages/starlight/package.json b/packages/starlight/package.json index 0495dabf6d7..04610b07c46 100644 --- a/packages/starlight/package.json +++ b/packages/starlight/package.json @@ -102,6 +102,10 @@ "types": "./components/Page.astro.tsx", "import": "./components/Page.astro" }, + "./components/VirtualPage.astro": { + "types": "./components/VirtualPage.astro.tsx", + "import": "./components/VirtualPage.astro" + }, "./components/Footer.astro": { "types": "./components/Footer.astro.tsx", "import": "./components/Footer.astro" diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts index 484fea08d52..bff5bbb5b62 100644 --- a/packages/starlight/utils/route-data.ts +++ b/packages/starlight/utils/route-data.ts @@ -5,22 +5,25 @@ import config from 'virtual:starlight/user-config'; import { generateToC, type TocItem } from './generateToC'; import { getFileCommitDate } from './git'; import { getPrevNextLinks, getSidebar, type SidebarEntry } from './navigation'; -import { ensureTrailingSlash } from './path'; -import type { Route } from './routing'; -import { localizedId } from './slugs'; +import { ensureTrailingSlash, stripLeadingAndTrailingSlashes } from './path'; +import type { Route, StarlightDocsEntry, VirtualDocsEntry, VirtualRoute } from './routing'; +import { localizedId, slugToLocaleData } from './slugs'; import { useTranslations } from './translations'; interface PageProps extends Route { headings: MarkdownHeading[]; } -export interface StarlightRouteData extends Route { +interface BaseRouteData { /** Array of Markdown headings extracted from the current page. */ headings: MarkdownHeading[]; - /** Site navigation sidebar entries for this page. */ - sidebar: SidebarEntry[]; /** Whether or not the sidebar should be displayed on this page. */ hasSidebar: boolean; +} + +export interface StarlightRouteData extends BaseRouteData, Route { + /** Site navigation sidebar entries for this page. */ + sidebar: SidebarEntry[]; /** Links to the previous and next page in the sidebar if enabled. */ pagination: ReturnType; /** Table of contents for this page if enabled. */ @@ -33,6 +36,11 @@ export interface StarlightRouteData extends Route { labels: ReturnType['all']>; } +export interface VirtualPageProps extends BaseRouteData, VirtualRoute { + /** Site navigation sidebar entries for this page or fallback to the generated sidebar. */ + sidebar?: SidebarEntry[] | undefined; +} + export function generateRouteData({ props, url, @@ -54,6 +62,56 @@ export function generateRouteData({ }; } +export function generateVirtualRouteData({ + props, + url, +}: { + props: VirtualPageProps; + url: URL; +}): StarlightRouteData { + const { dir, lastUpdated, lang, next, pagefind, prev, slug, tableOfContents, template, title } = + props; + const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; + const entryMeta = slugToLocaleData(slug); + const sidebar = props.sidebar ?? getSidebar(url.pathname, entryMeta.locale); + const virtualEntry: VirtualDocsEntry = { + id, + slug, + body: '', + collection: 'docs', + data: { + editUrl: false, + head: props.head, + lastUpdated, + next, + pagefind, + prev, + tableOfContents, + template, + title, + sidebar: { + attrs: {}, + hidden: false, + }, + }, + }; + const entry = virtualEntry as StarlightDocsEntry; + return { + ...props, + ...entryMeta, + id, + editUrl: undefined, + entry, + entryMeta: { dir: dir, lang: lang, locale: entryMeta.locale }, + labels: useTranslations(entryMeta.locale).all(), + lastUpdated: lastUpdated instanceof Date ? lastUpdated : undefined, + pagination: getPrevNextLinks(sidebar, config.pagination, entry.data), + sidebar, + slug, + toc: getToC({ ...props, entry, entryMeta, id, locale: entryMeta.locale }), + }; +} + function getToC({ entry, locale, headings }: PageProps) { const tocConfig = entry.data.template === 'splash' diff --git a/packages/starlight/utils/routing.ts b/packages/starlight/utils/routing.ts index 5b0b7091d9d..e76ffefc95b 100644 --- a/packages/starlight/utils/routing.ts +++ b/packages/starlight/utils/routing.ts @@ -18,13 +18,27 @@ export type StarlightDocsEntry = Omit, 'slug'> & { slug: string; }; -export interface Route extends LocaleData { +// A docs entry used for virtual pages meant to be rendered by plugins and which is safe to cast +// to a`StarlightDocsEntry`. +// A virtual docs entry cannot be rendered like a content collection entry. +export type VirtualDocsEntry = Omit & { + /** + * The unique ID for this virtual page which cannot be inferred from codegen like content + * collection entries. + */ + id: string; +}; + +interface BaseRoute { + /** The slug, a.k.a. permalink, for this page. */ + slug: string; +} + +export interface Route extends BaseRoute, LocaleData { /** Content collection entry for the current page. Includes frontmatter at `data`. */ entry: StarlightDocsEntry; /** Locale metadata for the page content. Can be different from top-level locale values when a page is using fallback content. */ entryMeta: LocaleData; - /** The slug, a.k.a. permalink, for this page. */ - slug: string; /** The unique ID for this page. */ id: string; /** True if this page is untranslated in the current language and using fallback content from the default locale. */ @@ -32,6 +46,18 @@ export interface Route extends LocaleData { [key: string]: unknown; } +/** + * This matches the shape of a collection entry data frontmatter object minus the following + * properties: + * + * - `editUrl`: Virtual pages cannot be edited. + * - `sidebar`: The sidebar frontmatter prop only works for pages in an autogenerated links + * group. Virtual page links cannot be autogenerated. + */ +export type VirtualRoute = BaseRoute & + Omit & + Omit; + interface Path extends GetStaticPathsItem { params: { slug: string | undefined }; props: Route; diff --git a/packages/virtual-pages-demo/Route.astro b/packages/virtual-pages-demo/Route.astro new file mode 100644 index 00000000000..f516c6c09e2 --- /dev/null +++ b/packages/virtual-pages-demo/Route.astro @@ -0,0 +1,80 @@ +--- +import type { InferGetStaticPropsType } from 'astro'; +import VirtualPage, { + type VirtualPageProps, +} from '@astrojs/starlight/components/VirtualPage.astro'; +import { getThings } from './things'; + +export function getStaticPaths() { + return getThings().map((thing) => { + const slug = `virtual-pages-demo/${thing}`; + return { + params: { + // Generate the routes: + // - /virtual-pages-demo/a/ + // - /virtual-pages-demo/b/ + // - /virtual-pages-demo/c/ + virtualPagesDemoSlug: slug, + }, + props: { + // Pass down the generated slug and the thing to the page + slug, + thing, + }, + }; + }); +} + +type Props = InferGetStaticPropsType; + +const { slug, thing } = Astro.props; + +// Generates the props for the virtual page +function getVirtualPageProps() { + const isThingB = thing === 'b'; + const isThingC = thing === 'c'; + + return { + dir: isThingB ? 'rtl' : 'ltr', + hasSidebar: !isThingB, + head: [], + headings: isThingC + ? [ + { depth: 2, slug: 'thing-a', text: 'Hello 2' }, + { depth: 3, slug: 'thing-a', text: 'Hello 3' }, + { depth: 4, slug: 'thing-a', text: 'Hello 4' }, + ] + : [], + lang: 'en', + pagefind: true, + sidebar: isThingC + ? [ + { + type: 'group', + label: 'All the links', + entries: [ + { + type: 'link', + label: 'Custom link 2', + href: '/getting-started/', + isCurrent: false, + badge: { text: 'Wow', variant: 'danger' }, + attrs: {}, + }, + ], + collapsed: false, + badge: undefined, + }, + ] + : undefined, + slug, + tableOfContents: isThingC ? { maxHeadingLevel: 4, minHeadingLevel: 2 } : false, + template: 'doc', + title: `Virtual Pages Demo: ${thing}`, + } satisfies VirtualPageProps; +} +--- + + +
some content from a plugin: {thing}
+
diff --git a/packages/virtual-pages-demo/index.ts b/packages/virtual-pages-demo/index.ts new file mode 100644 index 00000000000..533e85a5a5c --- /dev/null +++ b/packages/virtual-pages-demo/index.ts @@ -0,0 +1,36 @@ +import type { StarlightPlugin } from '@astrojs/starlight/types'; +import { getThings } from './things'; + +export function virtualPagesDemo(): StarlightPlugin { + return { + name: 'virtual-pages-demo', + hooks: { + setup({ addIntegration, config, updateConfig }) { + addIntegration({ + name: 'virtual-pages-demo-integration', + hooks: { + 'astro:config:setup': ({ injectRoute }) => { + injectRoute({ + entryPoint: 'virtual-pages-demo/route', + pattern: '[...virtualPagesDemoSlug]', + }); + }, + }, + }); + + updateConfig({ + sidebar: [ + ...(config.sidebar ?? []), + { + label: 'Virtual pages', + items: getThings().map((thing) => ({ + label: `Thing ${thing}`, + link: `virtual-pages-demo/${thing}/`, + })), + }, + ], + }); + }, + }, + }; +} diff --git a/packages/virtual-pages-demo/package.json b/packages/virtual-pages-demo/package.json new file mode 100644 index 00000000000..e9749cb50cc --- /dev/null +++ b/packages/virtual-pages-demo/package.json @@ -0,0 +1,23 @@ +{ + "name": "virtual-pages-demo", + "version": "0.0.1", + "private": true, + "description": "Virtual pages demo", + "author": "Chris Swithinbank ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/withastro/starlight", + "directory": "packages/virtual-pages-demo" + }, + "bugs": "https://github.com/withastro/starlight/issues", + "homepage": "https://starlight.astro.build", + "type": "module", + "exports": { + ".": "./index.ts", + "./route": "./Route.astro" + }, + "peerDependencies": { + "@astrojs/starlight": ">=0.14.0" + } +} diff --git a/packages/virtual-pages-demo/things.ts b/packages/virtual-pages-demo/things.ts new file mode 100644 index 00000000000..cc9bd9d83a5 --- /dev/null +++ b/packages/virtual-pages-demo/things.ts @@ -0,0 +1,4 @@ +export function getThings() { + // Imagine data generated dynamically somehow + return ['a', 'b', 'c']; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cff253ba7f1..f575cb95600 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: sharp: specifier: ^0.32.5 version: 0.32.6 + virtual-pages-demo: + specifier: workspace:* + version: link:../packages/virtual-pages-demo devDependencies: hast-util-from-html: specifier: ^1.0.2 @@ -234,6 +237,12 @@ importers: specifier: ^0.33.0 version: 0.33.0 + packages/virtual-pages-demo: + dependencies: + '@astrojs/starlight': + specifier: '>=0.14.0' + version: link:../starlight + packages: /@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.11.0): From 62e3ffc57e443e66030ca0733a76fda71a6077a8 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Wed, 27 Dec 2023 11:41:03 +0100 Subject: [PATCH 02/37] feat: add virtual frontmatter --- docs/src/content/docs/reference/plugins.md | 51 ++++---- .../basics/virtual-route-data.test.ts | 42 +++++-- packages/starlight/schema.ts | 118 ++++++++++-------- packages/starlight/utils/route-data.ts | 15 +-- packages/starlight/utils/routing.ts | 13 +- packages/virtual-pages-demo/Route.astro | 5 +- packages/virtual-pages-demo/index.ts | 2 +- 7 files changed, 141 insertions(+), 105 deletions(-) diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index 62c68556d8d..48d20416aba 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -208,50 +208,31 @@ The page title displayed at the top of the page, in browser tabs, and in page me ##### `slug` -**Type:** `string` +**type:** `string` The slug of the page. ##### `headings` -Type: `{ depth: number; slug: string; text: string }[]` +**type:** `{ depth: number; slug: string; text: string }[]` Array of all headings of the page. -##### `template` - -**type:** `'doc' | 'splash'` - -Set the layout template for this page. -Use `'splash'` to use a wider layout without any sidebars. - -##### `pagefind` - -**type:** `boolean` - -Set whether this page should be included in the [Pagefind](https://pagefind.app/) search index. - ##### `dir` -**Type:** `'ltr' | 'rtl'` +**type:** `'ltr' | 'rtl'` Page writing direction. ##### `lang` -**Type:** `string` +**type:** `string` BCP-47 language tag for this page’s locale, e.g. `en`, `zh-CN`, or `pt-BR`. -##### `head` - -**Type:** `{ tag: string; attrs: Record; content: string }[]` - -Additional tags to your page’s ``. Similar to the [global `head` option](/reference/configuration/#head). - ##### `hasSidebar` -**Type:** `boolean` +**type:** `boolean` Whether or not the sidebar should be displayed on this page. @@ -265,6 +246,28 @@ Additionaly, the following props can be provided to customize the page: The page description is used for page metadata and will be picked up by search engines and in social media previews. +##### `template` + +**type:** `'doc' | 'splash'` +**default:** `'doc'` + +Set the layout template for this page. +Use `'splash'` to use a wider layout without any sidebars (if defined, [`hasSidebar`](#hassidebar) takes precedence). + +##### `pagefind` + +**type:** `boolean` +**default:** `true` + +Set whether this page should be included in the [Pagefind](https://pagefind.app/) search index. + +##### `head` + +**type:** `{ tag: string; attrs: Record; content: string }[]` +**default:** `[]` + +Additional tags to your page’s ``. Similar to the [global `head` option](/reference/configuration/#head). + ##### `sidebar` **type:** `SidebarEntry[] | undefined` diff --git a/packages/starlight/__tests__/basics/virtual-route-data.test.ts b/packages/starlight/__tests__/basics/virtual-route-data.test.ts index b4a89bfba9f..a0ca0e0108c 100644 --- a/packages/starlight/__tests__/basics/virtual-route-data.test.ts +++ b/packages/starlight/__tests__/basics/virtual-route-data.test.ts @@ -13,13 +13,9 @@ vi.mock('astro:content', async () => const virtualPageProps: VirtualPageProps = { dir: 'rtl', hasSidebar: true, - head: [], headings: [], lang: 'ks', - lastUpdated: new Date(), - pagefind: true, slug: 'test-slug', - template: 'doc', title: 'This is a test title', }; @@ -39,17 +35,44 @@ test('adds data to route shape', () => { expect(data.entry.data.editUrl).toBe(false); // Virtual pages are part of the docs collection. expect(data.entry.collection).toBe('docs'); + // Virtual pages get virtual frontmatter defaults. + expect(data.entry.data.head).toEqual([]); + expect(data.entry.data.pagefind).toBe(true); + expect(data.entry.data.template).toBe('doc'); // Virtual pages respect the passed data. expect(data.hasSidebar).toBe(virtualPageProps.hasSidebar); - expect(data.entry.data.lastUpdated).toBe(virtualPageProps.lastUpdated); - expect(data.entry.data.pagefind).toBe(virtualPageProps.pagefind); - expect(data.entry.data.template).toBe(virtualPageProps.template); expect(data.entry.data.title).toBe(virtualPageProps.title); // Virtual pages respect the entry meta. expect(data.entryMeta.dir).toBe(virtualPageProps.dir); expect(data.entryMeta.lang).toBe(virtualPageProps.lang); }); +test('adds custom virtual frontmatter data to route shape', () => { + const props: VirtualPageProps = { + ...virtualPageProps, + head: [{ tag: 'meta', attrs: { name: 'og:test', content: 'test' } }], + lastUpdated: new Date(), + pagefind: false, + template: 'splash', + }; + const data = generateVirtualRouteData({ props, url: new URL('https://example.com') }); + expect(data.entry.data.head).toMatchInlineSnapshot(` + [ + { + "attrs": { + "content": "test", + "name": "og:test", + }, + "content": "", + "tag": "meta", + }, + ] + `); + expect(data.entry.data.lastUpdated).toEqual(props.lastUpdated); + expect(data.entry.data.pagefind).toBe(props.pagefind); + expect(data.entry.data.template).toBe(props.template); +}); + test('uses generated sidebar when no sidebar is provided', () => { const data = generateVirtualRouteData({ props: virtualPageProps, @@ -252,6 +275,11 @@ test('disables table of contents for splash template', () => { expect(data.toc).toBeUndefined(); }); +// TODO(HiDeoo) +test.todo( + 'hides the sidebar if the `hasSidebar` option is not specified and the splash template is used' +); + test('includes localized labels', () => { const data = generateVirtualRouteData({ props: virtualPageProps, diff --git a/packages/starlight/schema.ts b/packages/starlight/schema.ts index a3534af055e..06597fd991b 100644 --- a/packages/starlight/schema.ts +++ b/packages/starlight/schema.ts @@ -8,19 +8,75 @@ import { HeroSchema } from './schemas/hero'; import { SidebarLinkItemHTMLAttributesSchema } from './schemas/sidebar'; export { i18nSchema } from './schemas/i18n'; -/** Default content collection schema for Starlight’s `docs` collection. */ -const StarlightFrontmatterSchema = (context: SchemaContext) => - z.object({ - /** The title of the current page. Required. */ - title: z.string(), +/** + * The frontmatter schema for virtual pages that is also extended to define the default schema for + * Starlight’s `docs` content collection. + * The frontmatter schema for virtual pages cannot include the following properties: + * + * - `editUrl`: Virtual pages cannot be edited. + * - `sidebar`: The sidebar frontmatter prop only works for pages in an autogenerated links + * group. Virtual page links cannot be autogenerated. + * - `hero`: Virtual pages do not have access to the schema context containing the Astro image + * helper. + */ +export const StarlightVirtualFrontmatterSchema = z.object({ + /** The title of the current page. Required. */ + title: z.string(), - /** - * A short description of the current page’s content. Optional, but recommended. - * A good description is 150–160 characters long and outlines the key content - * of the page in a clear and engaging way. - */ - description: z.string().optional(), + /** + * A short description of the current page’s content. Optional, but recommended. + * A good description is 150–160 characters long and outlines the key content + * of the page in a clear and engaging way. + */ + description: z.string().optional(), + + /** Set custom `` tags just for this page. */ + head: HeadConfigSchema(), + + /** Override global table of contents configuration for this page. */ + tableOfContents: TableOfContentsSchema().optional(), + + /** + * Set the layout style for this page. + * Can be `'doc'` (the default) or `'splash'` for a wider layout without any sidebars. + */ + template: z.enum(['doc', 'splash']).default('doc'), + + /** + * The last update date of the current page. + * Overrides the `lastUpdated` global config or the date generated from the Git history. + */ + lastUpdated: z.union([z.date(), z.boolean()]).optional(), + + /** + * The previous navigation link configuration. + * Overrides the `pagination` global config or the link text and/or URL. + */ + prev: PrevNextLinkConfigSchema(), + /** + * The next navigation link configuration. + * Overrides the `pagination` global config or the link text and/or URL. + */ + next: PrevNextLinkConfigSchema(), + + /** Display an announcement banner at the top of this page. */ + banner: z + .object({ + /** The content of the banner. Supports HTML syntax. */ + content: z.string(), + }) + .optional(), + + /** Pagefind indexing for this page - set to false to disable. */ + pagefind: z.boolean().default(true), +}); +/** Type of Starlight’s virtual frontmatter schema. */ +export type StarlightVirtualFrontmatter = z.input; + +/** Default content collection schema for Starlight’s `docs` collection. */ +const StarlightFrontmatterSchema = (context: SchemaContext) => + StarlightVirtualFrontmatterSchema.extend({ /** * Custom URL where a reader can edit this page. * Overrides the `editLink.baseUrl` global config if set. @@ -29,38 +85,9 @@ const StarlightFrontmatterSchema = (context: SchemaContext) => */ editUrl: z.union([z.string().url(), z.boolean()]).optional().default(true), - /** Set custom `` tags just for this page. */ - head: HeadConfigSchema(), - - /** Override global table of contents configuration for this page. */ - tableOfContents: TableOfContentsSchema().optional(), - - /** - * Set the layout style for this page. - * Can be `'doc'` (the default) or `'splash'` for a wider layout without any sidebars. - */ - template: z.enum(['doc', 'splash']).default('doc'), - /** Display a hero section on this page. */ hero: HeroSchema(context).optional(), - /** - * The last update date of the current page. - * Overrides the `lastUpdated` global config or the date generated from the Git history. - */ - lastUpdated: z.union([z.date(), z.boolean()]).optional(), - - /** - * The previous navigation link configuration. - * Overrides the `pagination` global config or the link text and/or URL. - */ - prev: PrevNextLinkConfigSchema(), - /** - * The next navigation link configuration. - * Overrides the `pagination` global config or the link text and/or URL. - */ - next: PrevNextLinkConfigSchema(), - sidebar: z .object({ /** @@ -92,17 +119,6 @@ const StarlightFrontmatterSchema = (context: SchemaContext) => attrs: SidebarLinkItemHTMLAttributesSchema(), }) .default({}), - - /** Display an announcement banner at the top of this page. */ - banner: z - .object({ - /** The content of the banner. Supports HTML syntax. */ - content: z.string(), - }) - .optional(), - - /** Pagefind indexing for this page - set to false to disable. */ - pagefind: z.boolean().default(true), }); /** Type of Starlight’s default frontmatter schema. */ type DefaultSchema = ReturnType; diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts index bff5bbb5b62..e066b123f7b 100644 --- a/packages/starlight/utils/route-data.ts +++ b/packages/starlight/utils/route-data.ts @@ -9,6 +9,7 @@ import { ensureTrailingSlash, stripLeadingAndTrailingSlashes } from './path'; import type { Route, StarlightDocsEntry, VirtualDocsEntry, VirtualRoute } from './routing'; import { localizedId, slugToLocaleData } from './slugs'; import { useTranslations } from './translations'; +import { StarlightVirtualFrontmatterSchema } from '../schema'; interface PageProps extends Route { headings: MarkdownHeading[]; @@ -69,8 +70,8 @@ export function generateVirtualRouteData({ props: VirtualPageProps; url: URL; }): StarlightRouteData { - const { dir, lastUpdated, lang, next, pagefind, prev, slug, tableOfContents, template, title } = - props; + const { dir, lastUpdated, lang, slug } = props; + const virtualFrontmatter = StarlightVirtualFrontmatterSchema.parse(props); const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; const entryMeta = slugToLocaleData(slug); const sidebar = props.sidebar ?? getSidebar(url.pathname, entryMeta.locale); @@ -80,15 +81,8 @@ export function generateVirtualRouteData({ body: '', collection: 'docs', data: { + ...virtualFrontmatter, editUrl: false, - head: props.head, - lastUpdated, - next, - pagefind, - prev, - tableOfContents, - template, - title, sidebar: { attrs: {}, hidden: false, @@ -103,6 +97,7 @@ export function generateVirtualRouteData({ editUrl: undefined, entry, entryMeta: { dir: dir, lang: lang, locale: entryMeta.locale }, + hasSidebar: props.hasSidebar ?? entry.data.template !== 'splash', labels: useTranslations(entryMeta.locale).all(), lastUpdated: lastUpdated instanceof Date ? lastUpdated : undefined, pagination: getPrevNextLinks(sidebar, config.pagination, entry.data), diff --git a/packages/starlight/utils/routing.ts b/packages/starlight/utils/routing.ts index e76ffefc95b..c5eff2a113c 100644 --- a/packages/starlight/utils/routing.ts +++ b/packages/starlight/utils/routing.ts @@ -9,6 +9,7 @@ import { slugToParam, } from './slugs'; import { validateLogoImports } from './validateLogoImports'; +import type { StarlightVirtualFrontmatter } from '../schema'; // Validate any user-provided logos imported correctly. // We do this here so all pages trigger it and at the top level so it runs just once. @@ -47,16 +48,10 @@ export interface Route extends BaseRoute, LocaleData { } /** - * This matches the shape of a collection entry data frontmatter object minus the following - * properties: - * - * - `editUrl`: Virtual pages cannot be edited. - * - `sidebar`: The sidebar frontmatter prop only works for pages in an autogenerated links - * group. Virtual page links cannot be autogenerated. + * The definition of a virtual route containing for convenience at the top level the frontmatter + * data of the virtual page. */ -export type VirtualRoute = BaseRoute & - Omit & - Omit; +export type VirtualRoute = BaseRoute & Omit & StarlightVirtualFrontmatter; interface Path extends GetStaticPathsItem { params: { slug: string | undefined }; diff --git a/packages/virtual-pages-demo/Route.astro b/packages/virtual-pages-demo/Route.astro index f516c6c09e2..74d65ef2fc1 100644 --- a/packages/virtual-pages-demo/Route.astro +++ b/packages/virtual-pages-demo/Route.astro @@ -34,10 +34,11 @@ function getVirtualPageProps() { const isThingB = thing === 'b'; const isThingC = thing === 'c'; + // TODO(HiDeoo) Add an example with the minimal props required + return { dir: isThingB ? 'rtl' : 'ltr', hasSidebar: !isThingB, - head: [], headings: isThingC ? [ { depth: 2, slug: 'thing-a', text: 'Hello 2' }, @@ -46,7 +47,6 @@ function getVirtualPageProps() { ] : [], lang: 'en', - pagefind: true, sidebar: isThingC ? [ { @@ -69,7 +69,6 @@ function getVirtualPageProps() { : undefined, slug, tableOfContents: isThingC ? { maxHeadingLevel: 4, minHeadingLevel: 2 } : false, - template: 'doc', title: `Virtual Pages Demo: ${thing}`, } satisfies VirtualPageProps; } diff --git a/packages/virtual-pages-demo/index.ts b/packages/virtual-pages-demo/index.ts index 533e85a5a5c..246d70ffe68 100644 --- a/packages/virtual-pages-demo/index.ts +++ b/packages/virtual-pages-demo/index.ts @@ -11,7 +11,7 @@ export function virtualPagesDemo(): StarlightPlugin { hooks: { 'astro:config:setup': ({ injectRoute }) => { injectRoute({ - entryPoint: 'virtual-pages-demo/route', + entrypoint: 'virtual-pages-demo/route', pattern: '[...virtualPagesDemoSlug]', }); }, From 9151563bde8b1aab656c838bd71959783606108a Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Wed, 27 Dec 2023 11:54:45 +0100 Subject: [PATCH 03/37] feat: make `hasSidebar` prop optional --- docs/src/content/docs/reference/plugins.md | 13 ++++++------ .../basics/virtual-route-data.test.ts | 20 +++++++++++++------ packages/starlight/utils/route-data.ts | 9 +++++++-- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index 48d20416aba..a4a81c5b9ba 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -230,12 +230,6 @@ Page writing direction. BCP-47 language tag for this page’s locale, e.g. `en`, `zh-CN`, or `pt-BR`. -##### `hasSidebar` - -**type:** `boolean` - -Whether or not the sidebar should be displayed on this page. - #### Optional props Additionaly, the following props can be provided to customize the page: @@ -275,6 +269,13 @@ Additional tags to your page’s ``. Similar to the [global `head` option] Site navigation sidebar entries for this page or fallback to the global `sidebar` option if not provided. +##### `hasSidebar` + +**type:** `boolean` +**default:** `false` if [`template`](#template) is `'splash'`, otherwise `true` + +Whether or not the sidebar should be displayed on this page. + ##### `tableOfContents` **type:** `false | { minHeadingLevel: number; maxHeadingLevel: number; }` diff --git a/packages/starlight/__tests__/basics/virtual-route-data.test.ts b/packages/starlight/__tests__/basics/virtual-route-data.test.ts index a0ca0e0108c..9350246cce7 100644 --- a/packages/starlight/__tests__/basics/virtual-route-data.test.ts +++ b/packages/starlight/__tests__/basics/virtual-route-data.test.ts @@ -12,7 +12,6 @@ vi.mock('astro:content', async () => const virtualPageProps: VirtualPageProps = { dir: 'rtl', - hasSidebar: true, headings: [], lang: 'ks', slug: 'test-slug', @@ -39,8 +38,8 @@ test('adds data to route shape', () => { expect(data.entry.data.head).toEqual([]); expect(data.entry.data.pagefind).toBe(true); expect(data.entry.data.template).toBe('doc'); + expect(data.hasSidebar).toBe(true); // Virtual pages respect the passed data. - expect(data.hasSidebar).toBe(virtualPageProps.hasSidebar); expect(data.entry.data.title).toBe(virtualPageProps.title); // Virtual pages respect the entry meta. expect(data.entryMeta.dir).toBe(virtualPageProps.dir); @@ -51,6 +50,7 @@ test('adds custom virtual frontmatter data to route shape', () => { const props: VirtualPageProps = { ...virtualPageProps, head: [{ tag: 'meta', attrs: { name: 'og:test', content: 'test' } }], + hasSidebar: false, lastUpdated: new Date(), pagefind: false, template: 'splash', @@ -71,6 +71,7 @@ test('adds custom virtual frontmatter data to route shape', () => { expect(data.entry.data.lastUpdated).toEqual(props.lastUpdated); expect(data.entry.data.pagefind).toBe(props.pagefind); expect(data.entry.data.template).toBe(props.template); + expect(data.hasSidebar).toBe(props.hasSidebar); }); test('uses generated sidebar when no sidebar is provided', () => { @@ -275,10 +276,17 @@ test('disables table of contents for splash template', () => { expect(data.toc).toBeUndefined(); }); -// TODO(HiDeoo) -test.todo( - 'hides the sidebar if the `hasSidebar` option is not specified and the splash template is used' -); +test('hides the sidebar if the `hasSidebar` option is not specified and the splash template is used', () => { + const { hasSidebar, ...otherProps } = virtualPageProps; + const data = generateVirtualRouteData({ + props: { + ...otherProps, + template: 'splash', + }, + url: new URL('https://example.com'), + }); + expect(data.hasSidebar).toBe(false); +}); test('includes localized labels', () => { const data = generateVirtualRouteData({ diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts index e066b123f7b..cce3dbec3a6 100644 --- a/packages/starlight/utils/route-data.ts +++ b/packages/starlight/utils/route-data.ts @@ -18,13 +18,13 @@ interface PageProps extends Route { interface BaseRouteData { /** Array of Markdown headings extracted from the current page. */ headings: MarkdownHeading[]; - /** Whether or not the sidebar should be displayed on this page. */ - hasSidebar: boolean; } export interface StarlightRouteData extends BaseRouteData, Route { /** Site navigation sidebar entries for this page. */ sidebar: SidebarEntry[]; + /** Whether or not the sidebar should be displayed on this page. */ + hasSidebar: boolean; /** Links to the previous and next page in the sidebar if enabled. */ pagination: ReturnType; /** Table of contents for this page if enabled. */ @@ -40,6 +40,11 @@ export interface StarlightRouteData extends BaseRouteData, Route { export interface VirtualPageProps extends BaseRouteData, VirtualRoute { /** Site navigation sidebar entries for this page or fallback to the generated sidebar. */ sidebar?: SidebarEntry[] | undefined; + /** + * Whether or not the sidebar should be displayed on this page or disabled only when using the + * `splash` template. + */ + hasSidebar?: boolean; } export function generateRouteData({ From e49999f3c3d13ebe1851dd35d66e7d440a946e03 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Wed, 27 Dec 2023 12:14:58 +0100 Subject: [PATCH 04/37] feat: make `headings` prop optional --- docs/src/content/docs/reference/plugins.md | 13 ++-- .../basics/virtual-route-data.test.ts | 14 ++++- packages/starlight/utils/route-data.ts | 22 +++---- packages/virtual-pages-demo/Route.astro | 59 ++++++++++--------- 4 files changed, 58 insertions(+), 50 deletions(-) diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index a4a81c5b9ba..12358f54dcb 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -212,12 +212,6 @@ The page title displayed at the top of the page, in browser tabs, and in page me The slug of the page. -##### `headings` - -**type:** `{ depth: number; slug: string; text: string }[]` - -Array of all headings of the page. - ##### `dir` **type:** `'ltr' | 'rtl'` @@ -262,6 +256,13 @@ Set whether this page should be included in the [Pagefind](https://pagefind.app/ Additional tags to your page’s ``. Similar to the [global `head` option](/reference/configuration/#head). +##### `headings` + +**type:** `{ depth: number; slug: string; text: string }[]` +**default:** `[]` + +Array of all headings of the page. + ##### `sidebar` **type:** `SidebarEntry[] | undefined` diff --git a/packages/starlight/__tests__/basics/virtual-route-data.test.ts b/packages/starlight/__tests__/basics/virtual-route-data.test.ts index 9350246cce7..c30430a5e12 100644 --- a/packages/starlight/__tests__/basics/virtual-route-data.test.ts +++ b/packages/starlight/__tests__/basics/virtual-route-data.test.ts @@ -12,7 +12,6 @@ vi.mock('astro:content', async () => const virtualPageProps: VirtualPageProps = { dir: 'rtl', - headings: [], lang: 'ks', slug: 'test-slug', title: 'This is a test title', @@ -39,6 +38,7 @@ test('adds data to route shape', () => { expect(data.entry.data.pagefind).toBe(true); expect(data.entry.data.template).toBe('doc'); expect(data.hasSidebar).toBe(true); + expect(data.headings).toEqual([]); // Virtual pages respect the passed data. expect(data.entry.data.title).toBe(virtualPageProps.title); // Virtual pages respect the entry meta. @@ -157,6 +157,18 @@ test('uses provided pagination if any', () => { `); }); +test('uses provided headings if any', () => { + const headings = [ + { depth: 2, slug: 'heading-1', text: 'Heading 1' }, + { depth: 3, slug: 'heading-2', text: 'Heading 2' }, + ]; + const data = generateVirtualRouteData({ + props: { ...virtualPageProps, headings }, + url: new URL('https://example.com'), + }); + expect(data.headings).toEqual(headings); +}); + test('generates the table of contents for provided headings', () => { const data = generateVirtualRouteData({ props: { diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts index cce3dbec3a6..7e472bb41ff 100644 --- a/packages/starlight/utils/route-data.ts +++ b/packages/starlight/utils/route-data.ts @@ -18,13 +18,13 @@ interface PageProps extends Route { interface BaseRouteData { /** Array of Markdown headings extracted from the current page. */ headings: MarkdownHeading[]; + /** Whether or not the sidebar should be displayed on this page. */ + hasSidebar: boolean; + /** Site navigation sidebar entries for this page. */ + sidebar: SidebarEntry[]; } export interface StarlightRouteData extends BaseRouteData, Route { - /** Site navigation sidebar entries for this page. */ - sidebar: SidebarEntry[]; - /** Whether or not the sidebar should be displayed on this page. */ - hasSidebar: boolean; /** Links to the previous and next page in the sidebar if enabled. */ pagination: ReturnType; /** Table of contents for this page if enabled. */ @@ -37,15 +37,7 @@ export interface StarlightRouteData extends BaseRouteData, Route { labels: ReturnType['all']>; } -export interface VirtualPageProps extends BaseRouteData, VirtualRoute { - /** Site navigation sidebar entries for this page or fallback to the generated sidebar. */ - sidebar?: SidebarEntry[] | undefined; - /** - * Whether or not the sidebar should be displayed on this page or disabled only when using the - * `splash` template. - */ - hasSidebar?: boolean; -} +export type VirtualPageProps = Partial & VirtualRoute; export function generateRouteData({ props, @@ -80,6 +72,7 @@ export function generateVirtualRouteData({ const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; const entryMeta = slugToLocaleData(slug); const sidebar = props.sidebar ?? getSidebar(url.pathname, entryMeta.locale); + const headings = props.headings ?? []; const virtualEntry: VirtualDocsEntry = { id, slug, @@ -103,12 +96,13 @@ export function generateVirtualRouteData({ entry, entryMeta: { dir: dir, lang: lang, locale: entryMeta.locale }, hasSidebar: props.hasSidebar ?? entry.data.template !== 'splash', + headings, labels: useTranslations(entryMeta.locale).all(), lastUpdated: lastUpdated instanceof Date ? lastUpdated : undefined, pagination: getPrevNextLinks(sidebar, config.pagination, entry.data), sidebar, slug, - toc: getToC({ ...props, entry, entryMeta, id, locale: entryMeta.locale }), + toc: getToC({ ...props, entry, entryMeta, headings, id, locale: entryMeta.locale }), }; } diff --git a/packages/virtual-pages-demo/Route.astro b/packages/virtual-pages-demo/Route.astro index 74d65ef2fc1..28df3cb0bc1 100644 --- a/packages/virtual-pages-demo/Route.astro +++ b/packages/virtual-pages-demo/Route.astro @@ -36,41 +36,42 @@ function getVirtualPageProps() { // TODO(HiDeoo) Add an example with the minimal props required - return { + const props: VirtualPageProps = { dir: isThingB ? 'rtl' : 'ltr', hasSidebar: !isThingB, - headings: isThingC - ? [ - { depth: 2, slug: 'thing-a', text: 'Hello 2' }, - { depth: 3, slug: 'thing-a', text: 'Hello 3' }, - { depth: 4, slug: 'thing-a', text: 'Hello 4' }, - ] - : [], lang: 'en', - sidebar: isThingC - ? [ - { - type: 'group', - label: 'All the links', - entries: [ - { - type: 'link', - label: 'Custom link 2', - href: '/getting-started/', - isCurrent: false, - badge: { text: 'Wow', variant: 'danger' }, - attrs: {}, - }, - ], - collapsed: false, - badge: undefined, - }, - ] - : undefined, slug, tableOfContents: isThingC ? { maxHeadingLevel: 4, minHeadingLevel: 2 } : false, title: `Virtual Pages Demo: ${thing}`, - } satisfies VirtualPageProps; + }; + + if (isThingC) { + props.headings = [ + { depth: 2, slug: 'thing-a', text: 'Hello 2' }, + { depth: 3, slug: 'thing-a', text: 'Hello 3' }, + { depth: 4, slug: 'thing-a', text: 'Hello 4' }, + ]; + props.sidebar = [ + { + type: 'group', + label: 'All the links', + entries: [ + { + type: 'link', + label: 'Custom link 2', + href: '/getting-started/', + isCurrent: false, + badge: { text: 'Wow', variant: 'danger' }, + attrs: {}, + }, + ], + collapsed: false, + badge: undefined, + }, + ]; + } + + return props; } --- From 4577df5aaf44c68b832219f3bcc60a8375e5e9aa Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Wed, 27 Dec 2023 14:32:38 +0100 Subject: [PATCH 05/37] feat: make `dir` & `lang` props optional --- docs/src/content/docs/reference/plugins.md | 26 +++++++++--------- .../basics/virtual-route-data.test.ts | 27 ++++++++++++------- packages/starlight/utils/route-data.ts | 27 ++++++++++++++----- packages/starlight/utils/routing.ts | 4 ++- 4 files changed, 55 insertions(+), 29 deletions(-) diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index 12358f54dcb..06ffeffe69d 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -212,18 +212,6 @@ The page title displayed at the top of the page, in browser tabs, and in page me The slug of the page. -##### `dir` - -**type:** `'ltr' | 'rtl'` - -Page writing direction. - -##### `lang` - -**type:** `string` - -BCP-47 language tag for this page’s locale, e.g. `en`, `zh-CN`, or `pt-BR`. - #### Optional props Additionaly, the following props can be provided to customize the page: @@ -308,6 +296,20 @@ Same as [`prev`](#prev) but for the next page link. Add a hero component to the top of this page. Works well with `template: splash`. Similar to the [frontmatter `hero` option](/reference/frontmatter/#hero). +##### `dir` + +**type:** `'ltr' | 'rtl'` +**default:** The page writing direction. + +Page content writing direction. + +##### `lang` + +**type:** `string` +**default:** The page language tag. + +BCP-47 language tag for this page’s content locale, e.g. `en`, `zh-CN`, or `pt-BR`. + ##### `banner` **type:** `{ content: string }` diff --git a/packages/starlight/__tests__/basics/virtual-route-data.test.ts b/packages/starlight/__tests__/basics/virtual-route-data.test.ts index c30430a5e12..c4f5e2559b4 100644 --- a/packages/starlight/__tests__/basics/virtual-route-data.test.ts +++ b/packages/starlight/__tests__/basics/virtual-route-data.test.ts @@ -11,8 +11,6 @@ vi.mock('astro:content', async () => ); const virtualPageProps: VirtualPageProps = { - dir: 'rtl', - lang: 'ks', slug: 'test-slug', title: 'This is a test title', }; @@ -37,20 +35,32 @@ test('adds data to route shape', () => { expect(data.entry.data.head).toEqual([]); expect(data.entry.data.pagefind).toBe(true); expect(data.entry.data.template).toBe('doc'); - expect(data.hasSidebar).toBe(true); - expect(data.headings).toEqual([]); // Virtual pages respect the passed data. expect(data.entry.data.title).toBe(virtualPageProps.title); - // Virtual pages respect the entry meta. - expect(data.entryMeta.dir).toBe(virtualPageProps.dir); - expect(data.entryMeta.lang).toBe(virtualPageProps.lang); + // Virtual pages get expected defaults. + expect(data.hasSidebar).toBe(true); + expect(data.headings).toEqual([]); + expect(data.entryMeta.dir).toBe('ltr'); + expect(data.entryMeta.lang).toBe('en'); +}); + +test('adds custom data to route shape', () => { + const props: VirtualPageProps = { + ...virtualPageProps, + hasSidebar: false, + dir: 'rtl', + lang: 'ks', + }; + const data = generateVirtualRouteData({ props, url: new URL('https://example.com') }); + expect(data.hasSidebar).toBe(props.hasSidebar); + expect(data.entryMeta.dir).toBe(props.dir); + expect(data.entryMeta.lang).toBe(props.lang); }); test('adds custom virtual frontmatter data to route shape', () => { const props: VirtualPageProps = { ...virtualPageProps, head: [{ tag: 'meta', attrs: { name: 'og:test', content: 'test' } }], - hasSidebar: false, lastUpdated: new Date(), pagefind: false, template: 'splash', @@ -71,7 +81,6 @@ test('adds custom virtual frontmatter data to route shape', () => { expect(data.entry.data.lastUpdated).toEqual(props.lastUpdated); expect(data.entry.data.pagefind).toBe(props.pagefind); expect(data.entry.data.template).toBe(props.template); - expect(data.hasSidebar).toBe(props.hasSidebar); }); test('uses generated sidebar when no sidebar is provided', () => { diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts index 7e472bb41ff..cc5befbf8b7 100644 --- a/packages/starlight/utils/route-data.ts +++ b/packages/starlight/utils/route-data.ts @@ -67,11 +67,11 @@ export function generateVirtualRouteData({ props: VirtualPageProps; url: URL; }): StarlightRouteData { - const { dir, lastUpdated, lang, slug } = props; + const { lastUpdated, slug } = props; const virtualFrontmatter = StarlightVirtualFrontmatterSchema.parse(props); const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; - const entryMeta = slugToLocaleData(slug); - const sidebar = props.sidebar ?? getSidebar(url.pathname, entryMeta.locale); + const localeData = slugToLocaleData(slug); + const sidebar = props.sidebar ?? getSidebar(url.pathname, localeData.locale); const headings = props.headings ?? []; const virtualEntry: VirtualDocsEntry = { id, @@ -88,21 +88,34 @@ export function generateVirtualRouteData({ }, }; const entry = virtualEntry as StarlightDocsEntry; + const entryMeta: StarlightRouteData['entryMeta'] = { + dir: props.dir ?? localeData.dir, + lang: props.lang ?? localeData.lang, + locale: localeData.locale, + }; return { ...props, - ...entryMeta, + ...localeData, id, editUrl: undefined, entry, - entryMeta: { dir: dir, lang: lang, locale: entryMeta.locale }, + entryMeta, hasSidebar: props.hasSidebar ?? entry.data.template !== 'splash', headings, - labels: useTranslations(entryMeta.locale).all(), + labels: useTranslations(localeData.locale).all(), lastUpdated: lastUpdated instanceof Date ? lastUpdated : undefined, pagination: getPrevNextLinks(sidebar, config.pagination, entry.data), sidebar, slug, - toc: getToC({ ...props, entry, entryMeta, headings, id, locale: entryMeta.locale }), + toc: getToC({ + ...props, + ...localeData, + entry, + entryMeta, + headings, + id, + locale: localeData.locale, + }), }; } diff --git a/packages/starlight/utils/routing.ts b/packages/starlight/utils/routing.ts index c5eff2a113c..c044e46f3dd 100644 --- a/packages/starlight/utils/routing.ts +++ b/packages/starlight/utils/routing.ts @@ -51,7 +51,9 @@ export interface Route extends BaseRoute, LocaleData { * The definition of a virtual route containing for convenience at the top level the frontmatter * data of the virtual page. */ -export type VirtualRoute = BaseRoute & Omit & StarlightVirtualFrontmatter; +export type VirtualRoute = BaseRoute & + Partial> & + StarlightVirtualFrontmatter; interface Path extends GetStaticPathsItem { params: { slug: string | undefined }; From 478ddc2990b967189d347387817cff4f4b935894 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Wed, 27 Dec 2023 14:35:13 +0100 Subject: [PATCH 06/37] chore: add example with mininal set of virtual route props --- packages/virtual-pages-demo/Route.astro | 11 ++++++++--- packages/virtual-pages-demo/things.ts | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/virtual-pages-demo/Route.astro b/packages/virtual-pages-demo/Route.astro index 28df3cb0bc1..0f91e80a349 100644 --- a/packages/virtual-pages-demo/Route.astro +++ b/packages/virtual-pages-demo/Route.astro @@ -30,11 +30,16 @@ type Props = InferGetStaticPropsType; const { slug, thing } = Astro.props; // Generates the props for the virtual page -function getVirtualPageProps() { +function getVirtualPageProps(): VirtualPageProps { const isThingB = thing === 'b'; const isThingC = thing === 'c'; + const isThingD = thing === 'd'; - // TODO(HiDeoo) Add an example with the minimal props required + const title = `Virtual Pages Demo: ${thing}`; + + if (isThingD) { + return { slug, title }; + } const props: VirtualPageProps = { dir: isThingB ? 'rtl' : 'ltr', @@ -42,7 +47,7 @@ function getVirtualPageProps() { lang: 'en', slug, tableOfContents: isThingC ? { maxHeadingLevel: 4, minHeadingLevel: 2 } : false, - title: `Virtual Pages Demo: ${thing}`, + title, }; if (isThingC) { diff --git a/packages/virtual-pages-demo/things.ts b/packages/virtual-pages-demo/things.ts index cc9bd9d83a5..724f2d57227 100644 --- a/packages/virtual-pages-demo/things.ts +++ b/packages/virtual-pages-demo/things.ts @@ -1,4 +1,4 @@ export function getThings() { // Imagine data generated dynamically somehow - return ['a', 'b', 'c']; + return ['a', 'b', 'c', 'd']; } From 6aa37e06ec1ab275e235add8f9c9542b473d3d8d Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Wed, 27 Dec 2023 15:09:28 +0100 Subject: [PATCH 07/37] fix: virtual page hero --- packages/starlight/schema.ts | 101 +++++++++++---------- packages/starlight/utils/route-data.ts | 27 +++++- packages/virtual-pages-demo/Route.astro | 19 ++++ packages/virtual-pages-demo/hero-star.webp | Bin 0 -> 114506 bytes packages/virtual-pages-demo/things.ts | 2 +- 5 files changed, 97 insertions(+), 52 deletions(-) create mode 100644 packages/virtual-pages-demo/hero-star.webp diff --git a/packages/starlight/schema.ts b/packages/starlight/schema.ts index 06597fd991b..c4d147fd9e7 100644 --- a/packages/starlight/schema.ts +++ b/packages/starlight/schema.ts @@ -16,67 +16,71 @@ export { i18nSchema } from './schemas/i18n'; * - `editUrl`: Virtual pages cannot be edited. * - `sidebar`: The sidebar frontmatter prop only works for pages in an autogenerated links * group. Virtual page links cannot be autogenerated. - * - `hero`: Virtual pages do not have access to the schema context containing the Astro image - * helper. */ -export const StarlightVirtualFrontmatterSchema = z.object({ - /** The title of the current page. Required. */ - title: z.string(), +export const StarlightVirtualFrontmatterSchema = (context: SchemaContext) => + z.object({ + /** The title of the current page. Required. */ + title: z.string(), - /** - * A short description of the current page’s content. Optional, but recommended. - * A good description is 150–160 characters long and outlines the key content - * of the page in a clear and engaging way. - */ - description: z.string().optional(), + /** + * A short description of the current page’s content. Optional, but recommended. + * A good description is 150–160 characters long and outlines the key content + * of the page in a clear and engaging way. + */ + description: z.string().optional(), - /** Set custom `` tags just for this page. */ - head: HeadConfigSchema(), + /** Set custom `` tags just for this page. */ + head: HeadConfigSchema(), - /** Override global table of contents configuration for this page. */ - tableOfContents: TableOfContentsSchema().optional(), + /** Override global table of contents configuration for this page. */ + tableOfContents: TableOfContentsSchema().optional(), - /** - * Set the layout style for this page. - * Can be `'doc'` (the default) or `'splash'` for a wider layout without any sidebars. - */ - template: z.enum(['doc', 'splash']).default('doc'), + /** + * Set the layout style for this page. + * Can be `'doc'` (the default) or `'splash'` for a wider layout without any sidebars. + */ + template: z.enum(['doc', 'splash']).default('doc'), - /** - * The last update date of the current page. - * Overrides the `lastUpdated` global config or the date generated from the Git history. - */ - lastUpdated: z.union([z.date(), z.boolean()]).optional(), + /** Display a hero section on this page. */ + hero: HeroSchema(context).optional(), - /** - * The previous navigation link configuration. - * Overrides the `pagination` global config or the link text and/or URL. - */ - prev: PrevNextLinkConfigSchema(), - /** - * The next navigation link configuration. - * Overrides the `pagination` global config or the link text and/or URL. - */ - next: PrevNextLinkConfigSchema(), + /** + * The last update date of the current page. + * Overrides the `lastUpdated` global config or the date generated from the Git history. + */ + lastUpdated: z.union([z.date(), z.boolean()]).optional(), + + /** + * The previous navigation link configuration. + * Overrides the `pagination` global config or the link text and/or URL. + */ + prev: PrevNextLinkConfigSchema(), + /** + * The next navigation link configuration. + * Overrides the `pagination` global config or the link text and/or URL. + */ + next: PrevNextLinkConfigSchema(), - /** Display an announcement banner at the top of this page. */ - banner: z - .object({ - /** The content of the banner. Supports HTML syntax. */ - content: z.string(), - }) - .optional(), + /** Display an announcement banner at the top of this page. */ + banner: z + .object({ + /** The content of the banner. Supports HTML syntax. */ + content: z.string(), + }) + .optional(), - /** Pagefind indexing for this page - set to false to disable. */ - pagefind: z.boolean().default(true), -}); + /** Pagefind indexing for this page - set to false to disable. */ + pagefind: z.boolean().default(true), + }); /** Type of Starlight’s virtual frontmatter schema. */ -export type StarlightVirtualFrontmatter = z.input; +export type StarlightVirtualFrontmatter = z.input< + ReturnType +>; /** Default content collection schema for Starlight’s `docs` collection. */ const StarlightFrontmatterSchema = (context: SchemaContext) => - StarlightVirtualFrontmatterSchema.extend({ + StarlightVirtualFrontmatterSchema(context).extend({ /** * Custom URL where a reader can edit this page. * Overrides the `editLink.baseUrl` global config if set. @@ -85,9 +89,6 @@ const StarlightFrontmatterSchema = (context: SchemaContext) => */ editUrl: z.union([z.string().url(), z.boolean()]).optional().default(true), - /** Display a hero section on this page. */ - hero: HeroSchema(context).optional(), - sidebar: z .object({ /** diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts index cc5befbf8b7..feb61ecdd45 100644 --- a/packages/starlight/utils/route-data.ts +++ b/packages/starlight/utils/route-data.ts @@ -1,4 +1,5 @@ import type { MarkdownHeading } from 'astro'; +import { z } from 'astro/zod'; import { fileURLToPath } from 'node:url'; import project from 'virtual:starlight/project-context'; import config from 'virtual:starlight/user-config'; @@ -68,7 +69,7 @@ export function generateVirtualRouteData({ url: URL; }): StarlightRouteData { const { lastUpdated, slug } = props; - const virtualFrontmatter = StarlightVirtualFrontmatterSchema.parse(props); + const virtualFrontmatter = getVirtualFrontmatter(props); const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; const localeData = slugToLocaleData(slug); const sidebar = props.sidebar ?? getSidebar(url.pathname, localeData.locale); @@ -119,6 +120,30 @@ export function generateVirtualRouteData({ }; } +/** Extract the virtual frontmatter properties from the props received by a virtual page. */ +function getVirtualFrontmatter(props: VirtualPageProps) { + // This needs to be in sync with ImageMetadata. + // https://github.com/withastro/astro/blob/cf993bc263b58502096f00d383266cd179f331af/packages/astro/src/assets/types.ts#L32 + return StarlightVirtualFrontmatterSchema({ + image: () => + z.object({ + src: z.string(), + width: z.number(), + height: z.number(), + format: z.union([ + z.literal('png'), + z.literal('jpg'), + z.literal('jpeg'), + z.literal('tiff'), + z.literal('webp'), + z.literal('gif'), + z.literal('svg'), + z.literal('avif'), + ]), + }), + }).parse(props); +} + function getToC({ entry, locale, headings }: PageProps) { const tocConfig = entry.data.template === 'splash' diff --git a/packages/virtual-pages-demo/Route.astro b/packages/virtual-pages-demo/Route.astro index 0f91e80a349..e3775cd7707 100644 --- a/packages/virtual-pages-demo/Route.astro +++ b/packages/virtual-pages-demo/Route.astro @@ -4,6 +4,7 @@ import VirtualPage, { type VirtualPageProps, } from '@astrojs/starlight/components/VirtualPage.astro'; import { getThings } from './things'; +import heroStar from './hero-star.webp'; export function getStaticPaths() { return getThings().map((thing) => { @@ -14,6 +15,8 @@ export function getStaticPaths() { // - /virtual-pages-demo/a/ // - /virtual-pages-demo/b/ // - /virtual-pages-demo/c/ + // - /virtual-pages-demo/d/ + // - /virtual-pages-demo/e/ virtualPagesDemoSlug: slug, }, props: { @@ -34,11 +37,27 @@ function getVirtualPageProps(): VirtualPageProps { const isThingB = thing === 'b'; const isThingC = thing === 'c'; const isThingD = thing === 'd'; + const isThingE = thing === 'e'; const title = `Virtual Pages Demo: ${thing}`; if (isThingD) { + // Page with minimal set of props. return { slug, title }; + } else if (isThingE) { + // Page with the `splash` template and a hero including an image. + return { + hero: { + title: 'Hello', + tagline: 'This is a tagline', + image: { + alt: 'A star from a plugin', + file: heroStar, + }, + }, + slug, + title, + }; } const props: VirtualPageProps = { diff --git a/packages/virtual-pages-demo/hero-star.webp b/packages/virtual-pages-demo/hero-star.webp new file mode 100644 index 0000000000000000000000000000000000000000..0d9ef67fa6614e0a71538a122703e60aac79a411 GIT binary patch literal 114506 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 Date: Wed, 27 Dec 2023 15:11:41 +0100 Subject: [PATCH 08/37] chore: add banner example --- packages/virtual-pages-demo/Route.astro | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/virtual-pages-demo/Route.astro b/packages/virtual-pages-demo/Route.astro index e3775cd7707..b28ee9e78eb 100644 --- a/packages/virtual-pages-demo/Route.astro +++ b/packages/virtual-pages-demo/Route.astro @@ -45,8 +45,9 @@ function getVirtualPageProps(): VirtualPageProps { // Page with minimal set of props. return { slug, title }; } else if (isThingE) { - // Page with the `splash` template and a hero including an image. + // Page with the `splash` template, a hero including an image and a banner. return { + banner: { content: 'This is a banner' }, hero: { title: 'Hello', tagline: 'This is a tagline', From 88455a7fadecbaa2801d2ab419d17e228e67375a Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Fri, 29 Dec 2023 08:56:48 +0100 Subject: [PATCH 09/37] feat: add `makeVirtualStaticPaths()` helper --- docs/src/content/docs/reference/plugins.md | 58 ++++++++++++++++++- .../__tests__/basics/virtual-page.test.ts | 29 ++++++++++ .../i18n-root-locale/virtual-page.test.ts | 47 +++++++++++++++ .../__tests__/i18n/virtual-page.test.ts | 56 ++++++++++++++++++ .../starlight/components/VirtualPage.astro | 2 + packages/starlight/utils/route-data.ts | 13 +++-- packages/starlight/utils/routing.ts | 6 +- packages/starlight/utils/virtual-page.ts | 34 +++++++++++ packages/virtual-pages-demo/Route.astro | 57 ++++++++++-------- 9 files changed, 269 insertions(+), 33 deletions(-) create mode 100644 packages/starlight/__tests__/basics/virtual-page.test.ts create mode 100644 packages/starlight/__tests__/i18n-root-locale/virtual-page.test.ts create mode 100644 packages/starlight/__tests__/i18n/virtual-page.test.ts create mode 100644 packages/starlight/utils/virtual-page.ts diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index 06ffeffe69d..26b466ca991 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -165,7 +165,13 @@ The example above will log a message that includes the provided info message: ## Route injection Plugins [adding](#addintegration) an Astro integration can inject routes using the Integrations API [`injectRoute`](https://docs.astro.build/en/reference/integrations-reference/#injectroute-option) function to render dynamically generated content. -By default, pages rendered on custom routes do not use the Starlight layout. To use the Starlight layout, pages must wrap their content with the `` component. +By default, pages rendered on custom routes do not use the Starlight layout. + +To do so, pages must wrap their content with the `` component and can use the [`makeVirtualStaticPaths()`](#makevirtualstaticpaths) helper function to generate localized routes for multilingual sites. + +### VirtualPage + +The `` component can be used to render a page on a custom route using the Starlight layout. ```astro --- @@ -194,8 +200,6 @@ const props = { ``` -### Props - #### Required props The `` component requires the following props: @@ -317,3 +321,51 @@ BCP-47 language tag for this page’s content locale, e.g. `en`, `zh-CN`, or `pt Displays an announcement banner at the top of this page. The `content` value can include HTML for links or other content. + +### `makeVirtualStaticPaths()` + +[Dynamic routes](https://docs.astro.build/en/core-concepts/routing/#static-ssg-mode) must exports a `getStaticPaths()` function that returns an array of objects with a `params` property. Each of these objects will generate a corresponding route. + +The `makeVirtualStaticPaths()` helper function can be used to generate dynamic localized routes for multilingual sites, supporting multiple languages and marking some routes as [fallback routes](/guides/i18n/#fallback-content). + +This helper should be called with another function as its only argument and return an array of objects with a `params` property just like `getStaticPaths()`. +This function will be called with an object containing the `getLocalizedSlugs()` function that takes a `slug` and returns an array of corresponding localized slugs and their associated BCP-47 language tag. + +```astro +--- +// plugin/src/Example.astro +import type { InferGetStaticPropsType } from 'astro'; +import VirtualPage, { + makeVirtualStaticPaths, +} from '@astrojs/starlight/components/VirtualPage.astro'; + +export const { getStaticPaths } = makeVirtualStaticPaths( + ({ getLocalizedSlugs }) => { + return getLocalizedSlugs('foo/bar').map(({ slug, lang }) => ({ + params: { slug }, + props: { slug, lang }, + })); + } +); + +type Props = InferGetStaticPropsType; + +const { slug, lang } = Astro.props; + +// This plugin page only exists in English and French. +// All other languages will be marked as fallback pages. +const isFallback = lang !== 'en' && lang !== 'fr'; +--- + + + {lang === 'fr' ? 'Ceci est un exemple' : 'This is an example'} + +``` + +The above example will generate the following routes: + +- For a monolingual site, only the `foo/bar` route would be generated. +- For a multilingual site configured with English, French, and no [root locale](/guides/i18n/#use-a-root-locale), the `en/foo/bar` and `fr/foo/bar` routes would be generated. +- For a multilingual site configured with English as the root locale and French, the `foo/bar` and `fr/foo/bar` routes would be generated. + +Learn more about `getStaticPaths()` in the [Astro Documentation](https://docs.astro.build/en/reference/api-reference/#getstaticpaths). diff --git a/packages/starlight/__tests__/basics/virtual-page.test.ts b/packages/starlight/__tests__/basics/virtual-page.test.ts new file mode 100644 index 00000000000..da85551e3b8 --- /dev/null +++ b/packages/starlight/__tests__/basics/virtual-page.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from 'vitest'; +import { makeVirtualStaticPaths } from '../../utils/virtual-page'; + +test('returns the same slug with the default language', () => { + const { getStaticPaths } = makeVirtualStaticPaths(({ getLocalizedSlugs }) => { + return getLocalizedSlugs(`foo/bar`) + .map(({ slug, lang }) => { + return { + params: { slug }, + props: { slug, lang }, + }; + }) + .flat(); + }); + + expect(getStaticPaths()).toMatchInlineSnapshot(` + [ + { + "params": { + "slug": "foo/bar", + }, + "props": { + "lang": "en", + "slug": "foo/bar", + }, + }, + ] + `); +}); diff --git a/packages/starlight/__tests__/i18n-root-locale/virtual-page.test.ts b/packages/starlight/__tests__/i18n-root-locale/virtual-page.test.ts new file mode 100644 index 00000000000..da919eae9ae --- /dev/null +++ b/packages/starlight/__tests__/i18n-root-locale/virtual-page.test.ts @@ -0,0 +1,47 @@ +import { expect, test } from 'vitest'; +import { makeVirtualStaticPaths } from '../../utils/virtual-page'; + +test('returns all the localized slugs with the associated languages', () => { + const { getStaticPaths } = makeVirtualStaticPaths(({ getLocalizedSlugs }) => { + return getLocalizedSlugs(`test/foo`) + .map(({ slug, lang }) => { + return { + params: { slug }, + props: { slug, lang }, + }; + }) + .flat(); + }); + + expect(getStaticPaths()).toMatchInlineSnapshot(` + [ + { + "params": { + "slug": "test/foo", + }, + "props": { + "lang": "fr", + "slug": "test/foo", + }, + }, + { + "params": { + "slug": "en/test/foo", + }, + "props": { + "lang": "en-US", + "slug": "en/test/foo", + }, + }, + { + "params": { + "slug": "ar/test/foo", + }, + "props": { + "lang": "ar", + "slug": "ar/test/foo", + }, + }, + ] + `); +}); diff --git a/packages/starlight/__tests__/i18n/virtual-page.test.ts b/packages/starlight/__tests__/i18n/virtual-page.test.ts new file mode 100644 index 00000000000..7ab398f0529 --- /dev/null +++ b/packages/starlight/__tests__/i18n/virtual-page.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from 'vitest'; +import { makeVirtualStaticPaths } from '../../utils/virtual-page'; + +test('returns all the localized slugs with the associated languages', () => { + const { getStaticPaths } = makeVirtualStaticPaths(({ getLocalizedSlugs }) => { + return getLocalizedSlugs(`test/foo`) + .map(({ slug, lang }) => { + return { + params: { slug }, + props: { slug, lang }, + }; + }) + .flat(); + }); + + expect(getStaticPaths()).toMatchInlineSnapshot(` + [ + { + "params": { + "slug": "fr/test/foo", + }, + "props": { + "lang": "fr", + "slug": "fr/test/foo", + }, + }, + { + "params": { + "slug": "en/test/foo", + }, + "props": { + "lang": "en-US", + "slug": "en/test/foo", + }, + }, + { + "params": { + "slug": "ar/test/foo", + }, + "props": { + "lang": "ar", + "slug": "ar/test/foo", + }, + }, + { + "params": { + "slug": "pt-br/test/foo", + }, + "props": { + "lang": "pt-BR", + "slug": "pt-br/test/foo", + }, + }, + ] + `); +}); diff --git a/packages/starlight/components/VirtualPage.astro b/packages/starlight/components/VirtualPage.astro index 21d72c64610..846717b52c3 100644 --- a/packages/starlight/components/VirtualPage.astro +++ b/packages/starlight/components/VirtualPage.astro @@ -2,6 +2,8 @@ import { generateVirtualRouteData, type VirtualPageProps as Props } from '../utils/route-data'; import Page from './Page.astro'; +export { makeVirtualStaticPaths } from '../utils/virtual-page'; + export type VirtualPageProps = Props; --- diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts index feb61ecdd45..ad75894c984 100644 --- a/packages/starlight/utils/route-data.ts +++ b/packages/starlight/utils/route-data.ts @@ -68,7 +68,7 @@ export function generateVirtualRouteData({ props: VirtualPageProps; url: URL; }): StarlightRouteData { - const { lastUpdated, slug } = props; + const { isFallback, lastUpdated, slug, ...routeProps } = props; const virtualFrontmatter = getVirtualFrontmatter(props); const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; const localeData = slugToLocaleData(slug); @@ -94,8 +94,8 @@ export function generateVirtualRouteData({ lang: props.lang ?? localeData.lang, locale: localeData.locale, }; - return { - ...props, + const routeData: StarlightRouteData = { + ...routeProps, ...localeData, id, editUrl: undefined, @@ -109,15 +109,20 @@ export function generateVirtualRouteData({ sidebar, slug, toc: getToC({ - ...props, + ...routeProps, ...localeData, entry, entryMeta, headings, id, locale: localeData.locale, + slug, }), }; + if (isFallback) { + routeData.isFallback = true; + } + return routeData; } /** Extract the virtual frontmatter properties from the props received by a virtual page. */ diff --git a/packages/starlight/utils/routing.ts b/packages/starlight/utils/routing.ts index c044e46f3dd..b6af7e41740 100644 --- a/packages/starlight/utils/routing.ts +++ b/packages/starlight/utils/routing.ts @@ -51,8 +51,10 @@ export interface Route extends BaseRoute, LocaleData { * The definition of a virtual route containing for convenience at the top level the frontmatter * data of the virtual page. */ -export type VirtualRoute = BaseRoute & - Partial> & +export type VirtualRoute = BaseRoute & { + /** Defines if this page is untranslated in the current language and using fallback content from the default locale. */ + isFallback?: boolean; +} & Partial> & StarlightVirtualFrontmatter; interface Path extends GetStaticPathsItem { diff --git a/packages/starlight/utils/virtual-page.ts b/packages/starlight/utils/virtual-page.ts new file mode 100644 index 00000000000..89c6fcd0997 --- /dev/null +++ b/packages/starlight/utils/virtual-page.ts @@ -0,0 +1,34 @@ +import type { GetStaticPathsResult } from 'astro'; +import config from 'virtual:starlight/user-config'; +import { localizedSlug } from './slugs'; + +export function makeVirtualStaticPaths( + getVirtualStaticPaths: (options: GetVirtualStaticPathsOptions) => TResult | Promise +): { + getStaticPaths: () => TResult | Promise; +} { + return { + getStaticPaths: () => + getVirtualStaticPaths({ + /** Helper function to generate localized slugs for a given slug. */ + getLocalizedSlugs(slug: string) { + // If the site is not multilingual, we only need to return the same slug with the default language. + if (!config.isMultilingual) { + return [{ slug: localizedSlug(slug, undefined), lang: config.defaultLocale.lang }]; + } + + // Otherwise, we need to return the slug for each configured locale. + return Object.entries(config.locales).map(([locale, localeConfig]) => { + return { + slug: localizedSlug(slug, locale === 'root' ? undefined : locale), + lang: localeConfig?.lang!, + }; + }); + }, + }), + }; +} + +interface GetVirtualStaticPathsOptions { + getLocalizedSlugs(slug: string): { slug: string; lang: string }[]; +} diff --git a/packages/virtual-pages-demo/Route.astro b/packages/virtual-pages-demo/Route.astro index b28ee9e78eb..7f987de4219 100644 --- a/packages/virtual-pages-demo/Route.astro +++ b/packages/virtual-pages-demo/Route.astro @@ -1,36 +1,38 @@ --- import type { InferGetStaticPropsType } from 'astro'; import VirtualPage, { + makeVirtualStaticPaths, type VirtualPageProps, } from '@astrojs/starlight/components/VirtualPage.astro'; + import { getThings } from './things'; import heroStar from './hero-star.webp'; -export function getStaticPaths() { - return getThings().map((thing) => { - const slug = `virtual-pages-demo/${thing}`; - return { - params: { - // Generate the routes: - // - /virtual-pages-demo/a/ - // - /virtual-pages-demo/b/ - // - /virtual-pages-demo/c/ - // - /virtual-pages-demo/d/ - // - /virtual-pages-demo/e/ - virtualPagesDemoSlug: slug, - }, - props: { - // Pass down the generated slug and the thing to the page - slug, - thing, - }, - }; - }); -} +export const { getStaticPaths } = makeVirtualStaticPaths(async ({ getLocalizedSlugs }) => { + return getThings() + .map((thing) => { + // Generate all the localized routes for the page + return getLocalizedSlugs(`virtual-pages-demo/${thing}`).map(({ slug, lang }) => { + return { + params: { + virtualPagesDemoSlug: slug, + }, + props: { + // Pass down the generated slug and the thing to the page + slug, + thing, + // Pass down the lang of the page + lang, + }, + }; + }); + }) + .flat(); +}); type Props = InferGetStaticPropsType; -const { slug, thing } = Astro.props; +const { slug, thing, lang } = Astro.props; // Generates the props for the virtual page function getVirtualPageProps(): VirtualPageProps { @@ -61,6 +63,7 @@ function getVirtualPageProps(): VirtualPageProps { }; } + // Other pages with some other props modified for testing. const props: VirtualPageProps = { dir: isThingB ? 'rtl' : 'ltr', hasSidebar: !isThingB, @@ -98,8 +101,14 @@ function getVirtualPageProps(): VirtualPageProps { return props; } + +// This plugin is only available in English and French. +const isFallback = lang !== 'en' && lang !== 'fr'; --- - -
some content from a plugin: {thing}
+ +
+ {lang === 'fr' ? "Contenu provenant d'un plugin :" : 'some content from a plugin:'} + {thing} +
From 573702145c3a34a09443be32be56f137910d6d23 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Fri, 29 Dec 2023 08:59:52 +0100 Subject: [PATCH 10/37] docs: add `isFallback` documentation --- docs/src/content/docs/reference/plugins.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index 26b466ca991..e08db0a584b 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -322,6 +322,12 @@ Displays an announcement banner at the top of this page. The `content` value can include HTML for links or other content. +##### `isFallback` + +**type:** `boolean` + +Indicates if this page is untranslated in the current language and using [fallback content](/guides/i18n/#fallback-content). + ### `makeVirtualStaticPaths()` [Dynamic routes](https://docs.astro.build/en/core-concepts/routing/#static-ssg-mode) must exports a `getStaticPaths()` function that returns an array of objects with a `params` property. Each of these objects will generate a corresponding route. From fc20da3eae42c6306f4a2d3926a61ad699b86c88 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Fri, 29 Dec 2023 09:06:02 +0100 Subject: [PATCH 11/37] docs: reorder virtual page props --- docs/src/content/docs/reference/plugins.md | 86 +++++++++++----------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index e08db0a584b..d123c91244c 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -204,17 +204,17 @@ const props = { The `` component requires the following props: -##### `title` +##### `slug` **type:** `string` -The page title displayed at the top of the page, in browser tabs, and in page metadata. +The slug of the page. -##### `slug` +##### `title` **type:** `string` -The slug of the page. +The page title displayed at the top of the page, in browser tabs, and in page metadata. #### Optional props @@ -226,21 +226,6 @@ Additionaly, the following props can be provided to customize the page: The page description is used for page metadata and will be picked up by search engines and in social media previews. -##### `template` - -**type:** `'doc' | 'splash'` -**default:** `'doc'` - -Set the layout template for this page. -Use `'splash'` to use a wider layout without any sidebars (if defined, [`hasSidebar`](#hassidebar) takes precedence). - -##### `pagefind` - -**type:** `boolean` -**default:** `true` - -Set whether this page should be included in the [Pagefind](https://pagefind.app/) search index. - ##### `head` **type:** `{ tag: string; attrs: Record; content: string }[]` @@ -248,13 +233,6 @@ Set whether this page should be included in the [Pagefind](https://pagefind.app/ Additional tags to your page’s ``. Similar to the [global `head` option](/reference/configuration/#head). -##### `headings` - -**type:** `{ depth: number; slug: string; text: string }[]` -**default:** `[]` - -Array of all headings of the page. - ##### `sidebar` **type:** `SidebarEntry[] | undefined` @@ -269,6 +247,13 @@ Site navigation sidebar entries for this page or fallback to the global `sidebar Whether or not the sidebar should be displayed on this page. +##### `headings` + +**type:** `{ depth: number; slug: string; text: string }[]` +**default:** `[]` + +Array of all headings of the page. + ##### `tableOfContents` **type:** `false | { minHeadingLevel: number; maxHeadingLevel: number; }` @@ -276,12 +261,40 @@ Whether or not the sidebar should be displayed on this page. Overrides the [global `tableOfContents` config](/reference/configuration/#tableofcontents). Customize the heading levels to be included or set to `false` to hide the table of contents on this page. +##### `template` + +**type:** `'doc' | 'splash'` +**default:** `'doc'` + +Set the layout template for this page. +Use `'splash'` to use a wider layout without any sidebars (if defined, [`hasSidebar`](#hassidebar) takes precedence). + ##### `lastUpdated` **type:** `Date` A valid [YAML timestamp](https://yaml.org/type/timestamp.html) to display the last updated date of the page. +##### `dir` + +**type:** `'ltr' | 'rtl'` +**default:** The page writing direction. + +Page content writing direction. + +##### `lang` + +**type:** `string` +**default:** The page language tag. + +BCP-47 language tag for this page’s content locale, e.g. `en`, `zh-CN`, or `pt-BR`. + +##### `isFallback` + +**type:** `boolean` + +Indicates if this page is untranslated in the current language and using [fallback content](/guides/i18n/#fallback-content). + ##### `prev` **type:** `boolean | string | { link?: string; label?: string }` @@ -300,20 +313,6 @@ Same as [`prev`](#prev) but for the next page link. Add a hero component to the top of this page. Works well with `template: splash`. Similar to the [frontmatter `hero` option](/reference/frontmatter/#hero). -##### `dir` - -**type:** `'ltr' | 'rtl'` -**default:** The page writing direction. - -Page content writing direction. - -##### `lang` - -**type:** `string` -**default:** The page language tag. - -BCP-47 language tag for this page’s content locale, e.g. `en`, `zh-CN`, or `pt-BR`. - ##### `banner` **type:** `{ content: string }` @@ -322,11 +321,12 @@ Displays an announcement banner at the top of this page. The `content` value can include HTML for links or other content. -##### `isFallback` +##### `pagefind` -**type:** `boolean` +**type:** `boolean` +**default:** `true` -Indicates if this page is untranslated in the current language and using [fallback content](/guides/i18n/#fallback-content). +Set whether this page should be included in the [Pagefind](https://pagefind.app/) search index. ### `makeVirtualStaticPaths()` From 162b23e9ce576e26572182f008235fbc20acecd9 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Fri, 29 Dec 2023 09:09:03 +0100 Subject: [PATCH 12/37] docs: rewrite virtual page code example --- docs/src/content/docs/reference/plugins.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index d123c91244c..96204d5d507 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -184,18 +184,11 @@ import CustomComponent from './CustomComponent.astro'; const props = { title: 'My custom page', slug: 'custom-page/example', - template: 'doc', - hasSidebar: true, - headings: [{ depth: 2, slug: 'description', text: 'Description' }], - dir: 'ltr', - lang: 'en', - pagefind: true, - head: [], } satisfies VirtualPageProps; --- -

Description

+

This is a custom page with a custom component:

``` From 104ddd0eeb67361da28e41fec2faf40f3f2f344c Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Mon, 15 Jan 2024 14:58:45 +0100 Subject: [PATCH 13/37] chore: update pr --- docs/astro.config.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index bef4848775e..539c04ac30b 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -34,8 +34,6 @@ export default defineConfig({ trailingSlash: 'always', integrations: [ starlight({ - // TODO(HiDeoo) Remove me - plugins: [virtualPagesDemo()], title: 'Starlight', logo: { light: '/src/assets/logo-light.svg', @@ -210,7 +208,10 @@ export default defineConfig({ errorOnInconsistentLocale: true, }), ] - : [], + : [ + // TODO(HiDeoo) Remove me + virtualPagesDemo(), + ], }), ], }); From 7a9db5a6e523b54a97dec8f582db4dbd50da1610 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:46:20 +0100 Subject: [PATCH 14/37] feat: remove `makeVirtualStaticPaths()` helper --- docs/src/content/docs/reference/plugins.md | 51 +---------------- .../__tests__/basics/virtual-page.test.ts | 29 ---------- .../i18n-root-locale/virtual-page.test.ts | 47 ---------------- .../__tests__/i18n/virtual-page.test.ts | 56 ------------------- .../starlight/components/VirtualPage.astro | 2 - packages/starlight/utils/virtual-page.ts | 34 ----------- packages/virtual-pages-demo/Route.astro | 52 ++++++++--------- 7 files changed, 25 insertions(+), 246 deletions(-) delete mode 100644 packages/starlight/__tests__/basics/virtual-page.test.ts delete mode 100644 packages/starlight/__tests__/i18n-root-locale/virtual-page.test.ts delete mode 100644 packages/starlight/__tests__/i18n/virtual-page.test.ts delete mode 100644 packages/starlight/utils/virtual-page.ts diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index 249d246a308..9a2d3bdbfea 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -166,8 +166,7 @@ The example above will log a message that includes the provided info message: Plugins [adding](#addintegration) an Astro integration can inject routes using the Integrations API [`injectRoute`](https://docs.astro.build/en/reference/integrations-reference/#injectroute-option) function to render dynamically generated content. By default, pages rendered on custom routes do not use the Starlight layout. - -To do so, pages must wrap their content with the `` component and can use the [`makeVirtualStaticPaths()`](#makevirtualstaticpaths) helper function to generate localized routes for multilingual sites. +To do so, pages must wrap their content with the `` component. ### VirtualPage @@ -320,51 +319,3 @@ The `content` value can include HTML for links or other content. **default:** `true` Set whether this page should be included in the [Pagefind](https://pagefind.app/) search index. - -### `makeVirtualStaticPaths()` - -[Dynamic routes](https://docs.astro.build/en/core-concepts/routing/#static-ssg-mode) must exports a `getStaticPaths()` function that returns an array of objects with a `params` property. Each of these objects will generate a corresponding route. - -The `makeVirtualStaticPaths()` helper function can be used to generate dynamic localized routes for multilingual sites, supporting multiple languages and marking some routes as [fallback routes](/guides/i18n/#fallback-content). - -This helper should be called with another function as its only argument and return an array of objects with a `params` property just like `getStaticPaths()`. -This function will be called with an object containing the `getLocalizedSlugs()` function that takes a `slug` and returns an array of corresponding localized slugs and their associated BCP-47 language tag. - -```astro ---- -// plugin/src/Example.astro -import type { InferGetStaticPropsType } from 'astro'; -import VirtualPage, { - makeVirtualStaticPaths, -} from '@astrojs/starlight/components/VirtualPage.astro'; - -export const { getStaticPaths } = makeVirtualStaticPaths( - ({ getLocalizedSlugs }) => { - return getLocalizedSlugs('foo/bar').map(({ slug, lang }) => ({ - params: { slug }, - props: { slug, lang }, - })); - } -); - -type Props = InferGetStaticPropsType; - -const { slug, lang } = Astro.props; - -// This plugin page only exists in English and French. -// All other languages will be marked as fallback pages. -const isFallback = lang !== 'en' && lang !== 'fr'; ---- - - - {lang === 'fr' ? 'Ceci est un exemple' : 'This is an example'} - -``` - -The above example will generate the following routes: - -- For a monolingual site, only the `foo/bar` route would be generated. -- For a multilingual site configured with English, French, and no [root locale](/guides/i18n/#use-a-root-locale), the `en/foo/bar` and `fr/foo/bar` routes would be generated. -- For a multilingual site configured with English as the root locale and French, the `foo/bar` and `fr/foo/bar` routes would be generated. - -Learn more about `getStaticPaths()` in the [Astro Documentation](https://docs.astro.build/en/reference/api-reference/#getstaticpaths). diff --git a/packages/starlight/__tests__/basics/virtual-page.test.ts b/packages/starlight/__tests__/basics/virtual-page.test.ts deleted file mode 100644 index da85551e3b8..00000000000 --- a/packages/starlight/__tests__/basics/virtual-page.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect, test } from 'vitest'; -import { makeVirtualStaticPaths } from '../../utils/virtual-page'; - -test('returns the same slug with the default language', () => { - const { getStaticPaths } = makeVirtualStaticPaths(({ getLocalizedSlugs }) => { - return getLocalizedSlugs(`foo/bar`) - .map(({ slug, lang }) => { - return { - params: { slug }, - props: { slug, lang }, - }; - }) - .flat(); - }); - - expect(getStaticPaths()).toMatchInlineSnapshot(` - [ - { - "params": { - "slug": "foo/bar", - }, - "props": { - "lang": "en", - "slug": "foo/bar", - }, - }, - ] - `); -}); diff --git a/packages/starlight/__tests__/i18n-root-locale/virtual-page.test.ts b/packages/starlight/__tests__/i18n-root-locale/virtual-page.test.ts deleted file mode 100644 index da919eae9ae..00000000000 --- a/packages/starlight/__tests__/i18n-root-locale/virtual-page.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { expect, test } from 'vitest'; -import { makeVirtualStaticPaths } from '../../utils/virtual-page'; - -test('returns all the localized slugs with the associated languages', () => { - const { getStaticPaths } = makeVirtualStaticPaths(({ getLocalizedSlugs }) => { - return getLocalizedSlugs(`test/foo`) - .map(({ slug, lang }) => { - return { - params: { slug }, - props: { slug, lang }, - }; - }) - .flat(); - }); - - expect(getStaticPaths()).toMatchInlineSnapshot(` - [ - { - "params": { - "slug": "test/foo", - }, - "props": { - "lang": "fr", - "slug": "test/foo", - }, - }, - { - "params": { - "slug": "en/test/foo", - }, - "props": { - "lang": "en-US", - "slug": "en/test/foo", - }, - }, - { - "params": { - "slug": "ar/test/foo", - }, - "props": { - "lang": "ar", - "slug": "ar/test/foo", - }, - }, - ] - `); -}); diff --git a/packages/starlight/__tests__/i18n/virtual-page.test.ts b/packages/starlight/__tests__/i18n/virtual-page.test.ts deleted file mode 100644 index 7ab398f0529..00000000000 --- a/packages/starlight/__tests__/i18n/virtual-page.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { expect, test } from 'vitest'; -import { makeVirtualStaticPaths } from '../../utils/virtual-page'; - -test('returns all the localized slugs with the associated languages', () => { - const { getStaticPaths } = makeVirtualStaticPaths(({ getLocalizedSlugs }) => { - return getLocalizedSlugs(`test/foo`) - .map(({ slug, lang }) => { - return { - params: { slug }, - props: { slug, lang }, - }; - }) - .flat(); - }); - - expect(getStaticPaths()).toMatchInlineSnapshot(` - [ - { - "params": { - "slug": "fr/test/foo", - }, - "props": { - "lang": "fr", - "slug": "fr/test/foo", - }, - }, - { - "params": { - "slug": "en/test/foo", - }, - "props": { - "lang": "en-US", - "slug": "en/test/foo", - }, - }, - { - "params": { - "slug": "ar/test/foo", - }, - "props": { - "lang": "ar", - "slug": "ar/test/foo", - }, - }, - { - "params": { - "slug": "pt-br/test/foo", - }, - "props": { - "lang": "pt-BR", - "slug": "pt-br/test/foo", - }, - }, - ] - `); -}); diff --git a/packages/starlight/components/VirtualPage.astro b/packages/starlight/components/VirtualPage.astro index 846717b52c3..21d72c64610 100644 --- a/packages/starlight/components/VirtualPage.astro +++ b/packages/starlight/components/VirtualPage.astro @@ -2,8 +2,6 @@ import { generateVirtualRouteData, type VirtualPageProps as Props } from '../utils/route-data'; import Page from './Page.astro'; -export { makeVirtualStaticPaths } from '../utils/virtual-page'; - export type VirtualPageProps = Props; --- diff --git a/packages/starlight/utils/virtual-page.ts b/packages/starlight/utils/virtual-page.ts deleted file mode 100644 index 89c6fcd0997..00000000000 --- a/packages/starlight/utils/virtual-page.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { GetStaticPathsResult } from 'astro'; -import config from 'virtual:starlight/user-config'; -import { localizedSlug } from './slugs'; - -export function makeVirtualStaticPaths( - getVirtualStaticPaths: (options: GetVirtualStaticPathsOptions) => TResult | Promise -): { - getStaticPaths: () => TResult | Promise; -} { - return { - getStaticPaths: () => - getVirtualStaticPaths({ - /** Helper function to generate localized slugs for a given slug. */ - getLocalizedSlugs(slug: string) { - // If the site is not multilingual, we only need to return the same slug with the default language. - if (!config.isMultilingual) { - return [{ slug: localizedSlug(slug, undefined), lang: config.defaultLocale.lang }]; - } - - // Otherwise, we need to return the slug for each configured locale. - return Object.entries(config.locales).map(([locale, localeConfig]) => { - return { - slug: localizedSlug(slug, locale === 'root' ? undefined : locale), - lang: localeConfig?.lang!, - }; - }); - }, - }), - }; -} - -interface GetVirtualStaticPathsOptions { - getLocalizedSlugs(slug: string): { slug: string; lang: string }[]; -} diff --git a/packages/virtual-pages-demo/Route.astro b/packages/virtual-pages-demo/Route.astro index 7f987de4219..fec3b8f0e92 100644 --- a/packages/virtual-pages-demo/Route.astro +++ b/packages/virtual-pages-demo/Route.astro @@ -1,38 +1,37 @@ --- import type { InferGetStaticPropsType } from 'astro'; import VirtualPage, { - makeVirtualStaticPaths, type VirtualPageProps, } from '@astrojs/starlight/components/VirtualPage.astro'; import { getThings } from './things'; import heroStar from './hero-star.webp'; -export const { getStaticPaths } = makeVirtualStaticPaths(async ({ getLocalizedSlugs }) => { - return getThings() - .map((thing) => { - // Generate all the localized routes for the page - return getLocalizedSlugs(`virtual-pages-demo/${thing}`).map(({ slug, lang }) => { - return { - params: { - virtualPagesDemoSlug: slug, - }, - props: { - // Pass down the generated slug and the thing to the page - slug, - thing, - // Pass down the lang of the page - lang, - }, - }; - }); - }) - .flat(); -}); +export function getStaticPaths() { + return getThings().map((thing) => { + const slug = `virtual-pages-demo/${thing}`; + return { + params: { + // Generate the routes: + // - /virtual-pages-demo/a/ + // - /virtual-pages-demo/b/ + // - /virtual-pages-demo/c/ + // - /virtual-pages-demo/d/ + // - /virtual-pages-demo/e/ + virtualPagesDemoSlug: slug, + }, + props: { + // Pass down the generated slug and the thing to the page + slug, + thing, + }, + }; + }); +} type Props = InferGetStaticPropsType; -const { slug, thing, lang } = Astro.props; +const { slug, thing } = Astro.props; // Generates the props for the virtual page function getVirtualPageProps(): VirtualPageProps { @@ -101,14 +100,11 @@ function getVirtualPageProps(): VirtualPageProps { return props; } - -// This plugin is only available in English and French. -const isFallback = lang !== 'en' && lang !== 'fr'; --- - +
- {lang === 'fr' ? "Contenu provenant d'un plugin :" : 'some content from a plugin:'} + Some content from a plugin: {thing}
From 611976a04170524980d87a361f95557f07527b0c Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Tue, 23 Jan 2024 13:48:34 +0100 Subject: [PATCH 15/37] refactor: isolate virtual page code --- .../basics/virtual-route-data.test.ts | 2 +- .../starlight/components/VirtualPage.astro | 2 +- packages/starlight/schema.ts | 59 +++---- packages/starlight/utils/route-data.ts | 111 +----------- packages/starlight/utils/routing.ts | 31 +--- packages/starlight/utils/virtual-page.ts | 162 ++++++++++++++++++ 6 files changed, 196 insertions(+), 171 deletions(-) create mode 100644 packages/starlight/utils/virtual-page.ts diff --git a/packages/starlight/__tests__/basics/virtual-route-data.test.ts b/packages/starlight/__tests__/basics/virtual-route-data.test.ts index c4f5e2559b4..2a95a6ecc55 100644 --- a/packages/starlight/__tests__/basics/virtual-route-data.test.ts +++ b/packages/starlight/__tests__/basics/virtual-route-data.test.ts @@ -1,5 +1,5 @@ import { expect, test, vi } from 'vitest'; -import { generateVirtualRouteData, type VirtualPageProps } from '../../utils/route-data'; +import { generateVirtualRouteData, type VirtualPageProps } from '../../utils/virtual-page'; vi.mock('astro:content', async () => (await import('../test-utils')).mockedAstroContent({ diff --git a/packages/starlight/components/VirtualPage.astro b/packages/starlight/components/VirtualPage.astro index 21d72c64610..d5c03fca659 100644 --- a/packages/starlight/components/VirtualPage.astro +++ b/packages/starlight/components/VirtualPage.astro @@ -1,5 +1,5 @@ --- -import { generateVirtualRouteData, type VirtualPageProps as Props } from '../utils/route-data'; +import { generateVirtualRouteData, type VirtualPageProps as Props } from '../utils/virtual-page'; import Page from './Page.astro'; export type VirtualPageProps = Props; diff --git a/packages/starlight/schema.ts b/packages/starlight/schema.ts index c4d147fd9e7..096132fe524 100644 --- a/packages/starlight/schema.ts +++ b/packages/starlight/schema.ts @@ -8,16 +8,8 @@ import { HeroSchema } from './schemas/hero'; import { SidebarLinkItemHTMLAttributesSchema } from './schemas/sidebar'; export { i18nSchema } from './schemas/i18n'; -/** - * The frontmatter schema for virtual pages that is also extended to define the default schema for - * Starlight’s `docs` content collection. - * The frontmatter schema for virtual pages cannot include the following properties: - * - * - `editUrl`: Virtual pages cannot be edited. - * - `sidebar`: The sidebar frontmatter prop only works for pages in an autogenerated links - * group. Virtual page links cannot be autogenerated. - */ -export const StarlightVirtualFrontmatterSchema = (context: SchemaContext) => +/** Default content collection schema for Starlight’s `docs` collection. */ +export const StarlightFrontmatterSchema = (context: SchemaContext) => z.object({ /** The title of the current page. Required. */ title: z.string(), @@ -29,6 +21,14 @@ export const StarlightVirtualFrontmatterSchema = (context: SchemaContext) => */ description: z.string().optional(), + /** + * Custom URL where a reader can edit this page. + * Overrides the `editLink.baseUrl` global config if set. + * + * Can also be set to `false` to disable showing an edit link on this page. + */ + editUrl: z.union([z.string().url(), z.boolean()]).optional().default(true), + /** Set custom `` tags just for this page. */ head: HeadConfigSchema(), @@ -61,34 +61,6 @@ export const StarlightVirtualFrontmatterSchema = (context: SchemaContext) => */ next: PrevNextLinkConfigSchema(), - /** Display an announcement banner at the top of this page. */ - banner: z - .object({ - /** The content of the banner. Supports HTML syntax. */ - content: z.string(), - }) - .optional(), - - /** Pagefind indexing for this page - set to false to disable. */ - pagefind: z.boolean().default(true), - }); - -/** Type of Starlight’s virtual frontmatter schema. */ -export type StarlightVirtualFrontmatter = z.input< - ReturnType ->; - -/** Default content collection schema for Starlight’s `docs` collection. */ -const StarlightFrontmatterSchema = (context: SchemaContext) => - StarlightVirtualFrontmatterSchema(context).extend({ - /** - * Custom URL where a reader can edit this page. - * Overrides the `editLink.baseUrl` global config if set. - * - * Can also be set to `false` to disable showing an edit link on this page. - */ - editUrl: z.union([z.string().url(), z.boolean()]).optional().default(true), - sidebar: z .object({ /** @@ -120,6 +92,17 @@ const StarlightFrontmatterSchema = (context: SchemaContext) => attrs: SidebarLinkItemHTMLAttributesSchema(), }) .default({}), + + /** Display an announcement banner at the top of this page. */ + banner: z + .object({ + /** The content of the banner. Supports HTML syntax. */ + content: z.string(), + }) + .optional(), + + /** Pagefind indexing for this page - set to false to disable. */ + pagefind: z.boolean().default(true), }); /** Type of Starlight’s default frontmatter schema. */ type DefaultSchema = ReturnType; diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts index f58ef30e1c5..1ad5c9ca07c 100644 --- a/packages/starlight/utils/route-data.ts +++ b/packages/starlight/utils/route-data.ts @@ -1,31 +1,26 @@ import type { MarkdownHeading } from 'astro'; -import { z } from 'astro/zod'; import { fileURLToPath } from 'node:url'; import project from 'virtual:starlight/project-context'; import config from 'virtual:starlight/user-config'; import { generateToC, type TocItem } from './generateToC'; import { getNewestCommitDate } from './git'; import { getPrevNextLinks, getSidebar, type SidebarEntry } from './navigation'; -import { ensureTrailingSlash, stripLeadingAndTrailingSlashes } from './path'; -import type { Route, StarlightDocsEntry, VirtualDocsEntry, VirtualRoute } from './routing'; -import { localizedId, slugToLocaleData } from './slugs'; +import { ensureTrailingSlash } from './path'; +import type { Route } from './routing'; +import { localizedId } from './slugs'; import { useTranslations } from './translations'; -import { StarlightVirtualFrontmatterSchema } from '../schema'; -interface PageProps extends Route { +export interface PageProps extends Route { headings: MarkdownHeading[]; } -interface BaseRouteData { +export interface StarlightRouteData extends Route { /** Array of Markdown headings extracted from the current page. */ headings: MarkdownHeading[]; - /** Whether or not the sidebar should be displayed on this page. */ - hasSidebar: boolean; /** Site navigation sidebar entries for this page. */ sidebar: SidebarEntry[]; -} - -export interface StarlightRouteData extends BaseRouteData, Route { + /** Whether or not the sidebar should be displayed on this page. */ + hasSidebar: boolean; /** Links to the previous and next page in the sidebar if enabled. */ pagination: ReturnType; /** Table of contents for this page if enabled. */ @@ -38,8 +33,6 @@ export interface StarlightRouteData extends BaseRouteData, Route { labels: ReturnType['all']>; } -export type VirtualPageProps = Partial & VirtualRoute; - export function generateRouteData({ props, url, @@ -61,95 +54,7 @@ export function generateRouteData({ }; } -export function generateVirtualRouteData({ - props, - url, -}: { - props: VirtualPageProps; - url: URL; -}): StarlightRouteData { - const { isFallback, lastUpdated, slug, ...routeProps } = props; - const virtualFrontmatter = getVirtualFrontmatter(props); - const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; - const localeData = slugToLocaleData(slug); - const sidebar = props.sidebar ?? getSidebar(url.pathname, localeData.locale); - const headings = props.headings ?? []; - const virtualEntry: VirtualDocsEntry = { - id, - slug, - body: '', - collection: 'docs', - data: { - ...virtualFrontmatter, - editUrl: false, - sidebar: { - attrs: {}, - hidden: false, - }, - }, - }; - const entry = virtualEntry as StarlightDocsEntry; - const entryMeta: StarlightRouteData['entryMeta'] = { - dir: props.dir ?? localeData.dir, - lang: props.lang ?? localeData.lang, - locale: localeData.locale, - }; - const routeData: StarlightRouteData = { - ...routeProps, - ...localeData, - id, - editUrl: undefined, - entry, - entryMeta, - hasSidebar: props.hasSidebar ?? entry.data.template !== 'splash', - headings, - labels: useTranslations(localeData.locale).all(), - lastUpdated: lastUpdated instanceof Date ? lastUpdated : undefined, - pagination: getPrevNextLinks(sidebar, config.pagination, entry.data), - sidebar, - slug, - toc: getToC({ - ...routeProps, - ...localeData, - entry, - entryMeta, - headings, - id, - locale: localeData.locale, - slug, - }), - }; - if (isFallback) { - routeData.isFallback = true; - } - return routeData; -} - -/** Extract the virtual frontmatter properties from the props received by a virtual page. */ -function getVirtualFrontmatter(props: VirtualPageProps) { - // This needs to be in sync with ImageMetadata. - // https://github.com/withastro/astro/blob/cf993bc263b58502096f00d383266cd179f331af/packages/astro/src/assets/types.ts#L32 - return StarlightVirtualFrontmatterSchema({ - image: () => - z.object({ - src: z.string(), - width: z.number(), - height: z.number(), - format: z.union([ - z.literal('png'), - z.literal('jpg'), - z.literal('jpeg'), - z.literal('tiff'), - z.literal('webp'), - z.literal('gif'), - z.literal('svg'), - z.literal('avif'), - ]), - }), - }).parse(props); -} - -function getToC({ entry, locale, headings }: PageProps) { +export function getToC({ entry, locale, headings }: PageProps) { const tocConfig = entry.data.template === 'splash' ? false diff --git a/packages/starlight/utils/routing.ts b/packages/starlight/utils/routing.ts index b6af7e41740..5b0b7091d9d 100644 --- a/packages/starlight/utils/routing.ts +++ b/packages/starlight/utils/routing.ts @@ -9,7 +9,6 @@ import { slugToParam, } from './slugs'; import { validateLogoImports } from './validateLogoImports'; -import type { StarlightVirtualFrontmatter } from '../schema'; // Validate any user-provided logos imported correctly. // We do this here so all pages trigger it and at the top level so it runs just once. @@ -19,27 +18,13 @@ export type StarlightDocsEntry = Omit, 'slug'> & { slug: string; }; -// A docs entry used for virtual pages meant to be rendered by plugins and which is safe to cast -// to a`StarlightDocsEntry`. -// A virtual docs entry cannot be rendered like a content collection entry. -export type VirtualDocsEntry = Omit & { - /** - * The unique ID for this virtual page which cannot be inferred from codegen like content - * collection entries. - */ - id: string; -}; - -interface BaseRoute { - /** The slug, a.k.a. permalink, for this page. */ - slug: string; -} - -export interface Route extends BaseRoute, LocaleData { +export interface Route extends LocaleData { /** Content collection entry for the current page. Includes frontmatter at `data`. */ entry: StarlightDocsEntry; /** Locale metadata for the page content. Can be different from top-level locale values when a page is using fallback content. */ entryMeta: LocaleData; + /** The slug, a.k.a. permalink, for this page. */ + slug: string; /** The unique ID for this page. */ id: string; /** True if this page is untranslated in the current language and using fallback content from the default locale. */ @@ -47,16 +32,6 @@ export interface Route extends BaseRoute, LocaleData { [key: string]: unknown; } -/** - * The definition of a virtual route containing for convenience at the top level the frontmatter - * data of the virtual page. - */ -export type VirtualRoute = BaseRoute & { - /** Defines if this page is untranslated in the current language and using fallback content from the default locale. */ - isFallback?: boolean; -} & Partial> & - StarlightVirtualFrontmatter; - interface Path extends GetStaticPathsItem { params: { slug: string | undefined }; props: Route; diff --git a/packages/starlight/utils/virtual-page.ts b/packages/starlight/utils/virtual-page.ts new file mode 100644 index 00000000000..0070b593c51 --- /dev/null +++ b/packages/starlight/utils/virtual-page.ts @@ -0,0 +1,162 @@ +import { z } from 'astro/zod'; +import type { SchemaContext } from 'astro:content'; +import config from 'virtual:starlight/user-config'; +import { stripLeadingAndTrailingSlashes } from './path'; +import { getToC, type PageProps, type StarlightRouteData } from './route-data'; +import type { StarlightDocsEntry } from './routing'; +import { StarlightFrontmatterSchema } from '../schema'; +import { slugToLocaleData } from './slugs'; +import { getPrevNextLinks, getSidebar } from './navigation'; +import { useTranslations } from './translations'; + +/** + * The frontmatter schema for virtual pages derived from the default schema for Starlight’s `docs` + * content collection. + * The frontmatter schema for virtual pages cannot include some properties which will be omitted. + */ +const StarlightVirtualFrontmatterSchema = (context: SchemaContext) => + StarlightFrontmatterSchema(context).omit({ + /** + * Virtual pages cannot be edited. + */ + editUrl: true, + /** + * The sidebar frontmatter prop only works for pages in an autogenerated links group. Virtual + * page links cannot be autogenerated. + */ + sidebar: true, + }); + +/** Type of Starlight’s virtual frontmatter schema. */ +type StarlightVirtualFrontmatter = z.input>; + +/** + * The props accepted by the `` component. + */ +export type VirtualPageProps = Prettify< + // Remove the index signature from `Route`, omit undesired properties and make the rest optional. + Partial, 'entry' | 'entryMeta' | 'id' | 'locale' | 'slug'>> & + // Add back the mandatory slug property. + Pick & + // Add the sidebar definitions for a virtual page. + Partial> & + // And finally add the virtual frontmatter properties. + StarlightVirtualFrontmatter +>; + +/** + * A docs entry used for virtual pages meant to be rendered by plugins and which is safe to cast + * to a `StarlightDocsEntry`. + * A virtual docs entry cannot be rendered like a content collection entry. + */ +type VirtualDocsEntry = Omit & { + /** + * The unique ID for this virtual page which cannot be inferred from codegen like content + * collection entries. + */ + id: string; +}; + +export function generateVirtualRouteData({ + props, + url, +}: { + props: VirtualPageProps; + url: URL; +}): StarlightRouteData { + const { isFallback, lastUpdated, slug, ...routeProps } = props; + const virtualFrontmatter = getVirtualFrontmatter(props); + const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; + const localeData = slugToLocaleData(slug); + const sidebar = props.sidebar ?? getSidebar(url.pathname, localeData.locale); + const headings = props.headings ?? []; + const virtualEntry: VirtualDocsEntry = { + id, + slug, + body: '', + collection: 'docs', + data: { + ...virtualFrontmatter, + editUrl: false, + sidebar: { + attrs: {}, + hidden: false, + }, + }, + }; + const entry = virtualEntry as StarlightDocsEntry; + const entryMeta: StarlightRouteData['entryMeta'] = { + dir: props.dir ?? localeData.dir, + lang: props.lang ?? localeData.lang, + locale: localeData.locale, + }; + const routeData: StarlightRouteData = { + ...routeProps, + ...localeData, + id, + editUrl: undefined, + entry, + entryMeta, + hasSidebar: props.hasSidebar ?? entry.data.template !== 'splash', + headings, + labels: useTranslations(localeData.locale).all(), + lastUpdated: lastUpdated instanceof Date ? lastUpdated : undefined, + pagination: getPrevNextLinks(sidebar, config.pagination, entry.data), + sidebar, + slug, + toc: getToC({ + ...routeProps, + ...localeData, + entry, + entryMeta, + headings, + id, + locale: localeData.locale, + slug, + }), + }; + if (isFallback) { + routeData.isFallback = true; + } + return routeData; +} + +/** Extract the virtual frontmatter properties from the props received by a virtual page. */ +function getVirtualFrontmatter(props: VirtualPageProps) { + // This needs to be in sync with ImageMetadata. + // https://github.com/withastro/astro/blob/cf993bc263b58502096f00d383266cd179f331af/packages/astro/src/assets/types.ts#L32 + return StarlightVirtualFrontmatterSchema({ + image: () => + z.object({ + src: z.string(), + width: z.number(), + height: z.number(), + format: z.union([ + z.literal('png'), + z.literal('jpg'), + z.literal('jpeg'), + z.literal('tiff'), + z.literal('webp'), + z.literal('gif'), + z.literal('svg'), + z.literal('avif'), + ]), + }), + }).parse(props); +} + +// https://stackoverflow.com/a/66252656/1945960 +type RemoveIndexSignature = { + [Tkey in keyof TType as string extends Tkey + ? never + : number extends Tkey + ? never + : symbol extends Tkey + ? never + : Tkey]: TType[Tkey]; +}; + +// https://www.totaltypescript.com/concepts/the-prettify-helper +type Prettify = { + [TKey in keyof TType]: TType[TKey]; +} & {}; From 482fb11103edaa4247b89f3c75cbe30969c1fd15 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:30:15 +0100 Subject: [PATCH 16/37] feat: add support for edit URLs --- .../basics/virtual-route-data.test.ts | 12 ++++++- packages/starlight/utils/virtual-page.ts | 31 ++++++++++--------- packages/virtual-pages-demo/Route.astro | 3 +- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/starlight/__tests__/basics/virtual-route-data.test.ts b/packages/starlight/__tests__/basics/virtual-route-data.test.ts index 2a95a6ecc55..144d83455e6 100644 --- a/packages/starlight/__tests__/basics/virtual-route-data.test.ts +++ b/packages/starlight/__tests__/basics/virtual-route-data.test.ts @@ -26,7 +26,7 @@ test('adds data to route shape', () => { expect(data.id).toBeDefined(); // Virtual pages cannot be fallbacks. expect(data.isFallback).toBeUndefined(); - // Virtual pages cannot be edited. + // Virtual pages are not editable if no edit URL is passed. expect(data.editUrl).toBeUndefined(); expect(data.entry.data.editUrl).toBe(false); // Virtual pages are part of the docs collection. @@ -317,3 +317,13 @@ test('includes localized labels', () => { expect(data.labels).toBeDefined(); expect(data.labels['skipLink.label']).toBe('Skip to content'); }); + +test.only('uses provided edit URL if any', () => { + const editUrl = 'https://example.com/edit'; + const data = generateVirtualRouteData({ + props: { ...virtualPageProps, editUrl }, + url: new URL('https://example.com'), + }); + expect(data.editUrl).toEqual(new URL(editUrl)); + expect(data.entry.data.editUrl).toEqual(editUrl); +}); diff --git a/packages/starlight/utils/virtual-page.ts b/packages/starlight/utils/virtual-page.ts index 0070b593c51..8bb01d215ce 100644 --- a/packages/starlight/utils/virtual-page.ts +++ b/packages/starlight/utils/virtual-page.ts @@ -12,20 +12,24 @@ import { useTranslations } from './translations'; /** * The frontmatter schema for virtual pages derived from the default schema for Starlight’s `docs` * content collection. - * The frontmatter schema for virtual pages cannot include some properties which will be omitted. + * The frontmatter schema for virtual pages cannot include some properties which will be omitted + * and some others needs to be refined to a stricter type. */ const StarlightVirtualFrontmatterSchema = (context: SchemaContext) => - StarlightFrontmatterSchema(context).omit({ - /** - * Virtual pages cannot be edited. - */ - editUrl: true, - /** - * The sidebar frontmatter prop only works for pages in an autogenerated links group. Virtual - * page links cannot be autogenerated. - */ - sidebar: true, - }); + StarlightFrontmatterSchema(context) + .omit({ + /** + * The sidebar frontmatter prop only works for pages in an autogenerated links group. Virtual + * page links cannot be autogenerated. + */ + sidebar: true, + }) + .extend({ + /** + * Virtual pages can only be edited if an edit URL is explicitly provided. + */ + editUrl: z.union([z.string().url(), z.literal(false)]).default(false), + }); /** Type of Starlight’s virtual frontmatter schema. */ type StarlightVirtualFrontmatter = z.input>; @@ -77,7 +81,6 @@ export function generateVirtualRouteData({ collection: 'docs', data: { ...virtualFrontmatter, - editUrl: false, sidebar: { attrs: {}, hidden: false, @@ -94,7 +97,7 @@ export function generateVirtualRouteData({ ...routeProps, ...localeData, id, - editUrl: undefined, + editUrl: routeProps.editUrl ? new URL(routeProps.editUrl) : undefined, entry, entryMeta, hasSidebar: props.hasSidebar ?? entry.data.template !== 'splash', diff --git a/packages/virtual-pages-demo/Route.astro b/packages/virtual-pages-demo/Route.astro index fec3b8f0e92..7fdea783a25 100644 --- a/packages/virtual-pages-demo/Route.astro +++ b/packages/virtual-pages-demo/Route.astro @@ -46,9 +46,10 @@ function getVirtualPageProps(): VirtualPageProps { // Page with minimal set of props. return { slug, title }; } else if (isThingE) { - // Page with the `splash` template, a hero including an image and a banner. + // Page with the `splash` template, a hero including an image, an edit URL and a banner. return { banner: { content: 'This is a banner' }, + editUrl: 'https://starlight.astro.build/', hero: { title: 'Hello', tagline: 'This is a tagline', From ef9df1f0e23f21e2a23fd778ae19ae5361d57783 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:24:05 +0100 Subject: [PATCH 17/37] refactor: move virtual page frontmatter props to a dedicated property --- docs/src/content/docs/reference/plugins.md | 88 +++---------------- .../basics/virtual-route-data.test.ts | 72 ++++++++++----- packages/starlight/utils/virtual-page.ts | 24 ++--- packages/virtual-pages-demo/Route.astro | 30 ++++--- 4 files changed, 89 insertions(+), 125 deletions(-) diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index 9a2d3bdbfea..946b0f0d08c 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -181,8 +181,10 @@ import VirtualPage, { import CustomComponent from './CustomComponent.astro'; const props = { - title: 'My custom page', slug: 'custom-page/example', + frontmatter: { + title: 'My custom page', + }, } satisfies VirtualPageProps; --- @@ -192,38 +194,22 @@ const props = { ``` -#### Required props - -The `` component requires the following props: +#### Props -##### `slug` +##### `slug` (required) **type:** `string` The slug of the page. -##### `title` - -**type:** `string` - -The page title displayed at the top of the page, in browser tabs, and in page metadata. +##### `frontmatter` (required) -#### Optional props +**type:** `StarlightVirtualFrontmatter` -Additionaly, the following props can be provided to customize the page: +Similar to the [frontmatter properties](/reference/frontmatter/) with the [`title`](/reference/frontmatter/#title-required) property being required and all other properties being optional and the following differences: -##### `description` - -**type:** `string` - -The page description is used for page metadata and will be picked up by search engines and in social media previews. - -##### `head` - -**type:** `{ tag: string; attrs: Record; content: string }[]` -**default:** `[]` - -Additional tags to your page’s ``. Similar to the [global `head` option](/reference/configuration/#head). +- The [`sidebar`](/reference/frontmatter/#sidebar) property is not supported as this option controls how a page is displayed in the sidebar, when using an [autogenerated link group](/reference/configuration/#sidebar) which is not applicable to virtual pages. +- The [`editUrl`](/reference/frontmatter/#editurl) option requires an URL to display an edit link. ##### `sidebar` @@ -246,27 +232,6 @@ Whether or not the sidebar should be displayed on this page. Array of all headings of the page. -##### `tableOfContents` - -**type:** `false | { minHeadingLevel: number; maxHeadingLevel: number; }` - -Overrides the [global `tableOfContents` config](/reference/configuration/#tableofcontents). -Customize the heading levels to be included or set to `false` to hide the table of contents on this page. - -##### `template` - -**type:** `'doc' | 'splash'` -**default:** `'doc'` - -Set the layout template for this page. -Use `'splash'` to use a wider layout without any sidebars (if defined, [`hasSidebar`](#hassidebar) takes precedence). - -##### `lastUpdated` - -**type:** `Date` - -A valid [YAML timestamp](https://yaml.org/type/timestamp.html) to display the last updated date of the page. - ##### `dir` **type:** `'ltr' | 'rtl'` @@ -286,36 +251,3 @@ BCP-47 language tag for this page’s content locale, e.g. `en`, `zh-CN`, or `pt **type:** `boolean` Indicates if this page is untranslated in the current language and using [fallback content](/guides/i18n/#fallback-content). - -##### `prev` - -**type:** `boolean | string | { link?: string; label?: string }` - -Overrides the [global `pagination` option](/reference/configuration/#pagination). If a string is specified, the generated link text will be replaced and if an object is specified, both the link and the text will be overridden. - -##### `next` - -**type:** `boolean | string | { link?: string; label?: string }` - -Same as [`prev`](#prev) but for the next page link. - -##### `hero` - -**type:** [`HeroConfig`](/reference/frontmatter/#heroconfig) - -Add a hero component to the top of this page. Works well with `template: splash`. Similar to the [frontmatter `hero` option](/reference/frontmatter/#hero). - -##### `banner` - -**type:** `{ content: string }` - -Displays an announcement banner at the top of this page. - -The `content` value can include HTML for links or other content. - -##### `pagefind` - -**type:** `boolean` -**default:** `true` - -Set whether this page should be included in the [Pagefind](https://pagefind.app/) search index. diff --git a/packages/starlight/__tests__/basics/virtual-route-data.test.ts b/packages/starlight/__tests__/basics/virtual-route-data.test.ts index 144d83455e6..a50e6e2a584 100644 --- a/packages/starlight/__tests__/basics/virtual-route-data.test.ts +++ b/packages/starlight/__tests__/basics/virtual-route-data.test.ts @@ -12,7 +12,7 @@ vi.mock('astro:content', async () => const virtualPageProps: VirtualPageProps = { slug: 'test-slug', - title: 'This is a test title', + frontmatter: { title: 'This is a test title' }, }; test('adds data to route shape', () => { @@ -36,7 +36,7 @@ test('adds data to route shape', () => { expect(data.entry.data.pagefind).toBe(true); expect(data.entry.data.template).toBe('doc'); // Virtual pages respect the passed data. - expect(data.entry.data.title).toBe(virtualPageProps.title); + expect(data.entry.data.title).toBe(virtualPageProps.frontmatter.title); // Virtual pages get expected defaults. expect(data.hasSidebar).toBe(true); expect(data.headings).toEqual([]); @@ -60,10 +60,13 @@ test('adds custom data to route shape', () => { test('adds custom virtual frontmatter data to route shape', () => { const props: VirtualPageProps = { ...virtualPageProps, - head: [{ tag: 'meta', attrs: { name: 'og:test', content: 'test' } }], - lastUpdated: new Date(), - pagefind: false, - template: 'splash', + frontmatter: { + ...virtualPageProps.frontmatter, + head: [{ tag: 'meta', attrs: { name: 'og:test', content: 'test' } }], + lastUpdated: new Date(), + pagefind: false, + template: 'splash', + }, }; const data = generateVirtualRouteData({ props, url: new URL('https://example.com') }); expect(data.entry.data.head).toMatchInlineSnapshot(` @@ -78,9 +81,9 @@ test('adds custom virtual frontmatter data to route shape', () => { }, ] `); - expect(data.entry.data.lastUpdated).toEqual(props.lastUpdated); - expect(data.entry.data.pagefind).toBe(props.pagefind); - expect(data.entry.data.template).toBe(props.template); + expect(data.entry.data.lastUpdated).toEqual(props.frontmatter.lastUpdated); + expect(data.entry.data.pagefind).toBe(props.frontmatter.pagefind); + expect(data.entry.data.template).toBe(props.frontmatter.template); }); test('uses generated sidebar when no sidebar is provided', () => { @@ -133,13 +136,16 @@ test('uses provided pagination if any', () => { const data = generateVirtualRouteData({ props: { ...virtualPageProps, - prev: { - label: 'Previous link', - link: '/test/prev', - }, - next: { - label: 'Next link', - link: '/test/next', + frontmatter: { + ...virtualPageProps.frontmatter, + prev: { + label: 'Previous link', + link: '/test/prev', + }, + next: { + label: 'Next link', + link: '/test/next', + }, }, }, url: new URL('https://example.com'), @@ -230,9 +236,12 @@ test('respects the `tableOfContents` level configuration', () => { { depth: 3, slug: 'heading-2', text: 'Heading 2' }, { depth: 4, slug: 'heading-3', text: 'Heading 3' }, ], - tableOfContents: { - minHeadingLevel: 3, - maxHeadingLevel: 4, + frontmatter: { + ...virtualPageProps.frontmatter, + tableOfContents: { + minHeadingLevel: 3, + maxHeadingLevel: 4, + }, }, }, url: new URL('https://example.com'), @@ -275,7 +284,10 @@ test('disables table of contents if frontmatter includes `tableOfContents: false { depth: 2, slug: 'heading-1', text: 'Heading 1' }, { depth: 3, slug: 'heading-2', text: 'Heading 2' }, ], - tableOfContents: false, + frontmatter: { + ...virtualPageProps.frontmatter, + tableOfContents: false, + }, }, url: new URL('https://example.com'), }); @@ -290,7 +302,10 @@ test('disables table of contents for splash template', () => { { depth: 2, slug: 'heading-1', text: 'Heading 1' }, { depth: 3, slug: 'heading-2', text: 'Heading 2' }, ], - template: 'splash', + frontmatter: { + ...virtualPageProps.frontmatter, + template: 'splash', + }, }, url: new URL('https://example.com'), }); @@ -302,7 +317,10 @@ test('hides the sidebar if the `hasSidebar` option is not specified and the spla const data = generateVirtualRouteData({ props: { ...otherProps, - template: 'splash', + frontmatter: { + ...otherProps.frontmatter, + template: 'splash', + }, }, url: new URL('https://example.com'), }); @@ -318,10 +336,16 @@ test('includes localized labels', () => { expect(data.labels['skipLink.label']).toBe('Skip to content'); }); -test.only('uses provided edit URL if any', () => { +test('uses provided edit URL if any', () => { const editUrl = 'https://example.com/edit'; const data = generateVirtualRouteData({ - props: { ...virtualPageProps, editUrl }, + props: { + ...virtualPageProps, + frontmatter: { + ...virtualPageProps.frontmatter, + editUrl, + }, + }, url: new URL('https://example.com'), }); expect(data.editUrl).toEqual(new URL(editUrl)); diff --git a/packages/starlight/utils/virtual-page.ts b/packages/starlight/utils/virtual-page.ts index 8bb01d215ce..82760c7db04 100644 --- a/packages/starlight/utils/virtual-page.ts +++ b/packages/starlight/utils/virtual-page.ts @@ -43,9 +43,10 @@ export type VirtualPageProps = Prettify< // Add back the mandatory slug property. Pick & // Add the sidebar definitions for a virtual page. - Partial> & - // And finally add the virtual frontmatter properties. - StarlightVirtualFrontmatter + Partial> & { + // And finally add the virtual frontmatter properties in a `frontmatter` property. + frontmatter: StarlightVirtualFrontmatter; + } >; /** @@ -68,8 +69,8 @@ export function generateVirtualRouteData({ props: VirtualPageProps; url: URL; }): StarlightRouteData { - const { isFallback, lastUpdated, slug, ...routeProps } = props; - const virtualFrontmatter = getVirtualFrontmatter(props); + const { isFallback, frontmatter, slug, ...routeProps } = props; + const virtualFrontmatter = getVirtualFrontmatter(frontmatter); const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; const localeData = slugToLocaleData(slug); const sidebar = props.sidebar ?? getSidebar(url.pathname, localeData.locale); @@ -93,17 +94,20 @@ export function generateVirtualRouteData({ lang: props.lang ?? localeData.lang, locale: localeData.locale, }; + const editUrl = virtualFrontmatter.editUrl ? new URL(virtualFrontmatter.editUrl) : undefined; + const lastUpdated = + virtualFrontmatter.lastUpdated instanceof Date ? virtualFrontmatter.lastUpdated : undefined; const routeData: StarlightRouteData = { ...routeProps, ...localeData, id, - editUrl: routeProps.editUrl ? new URL(routeProps.editUrl) : undefined, + editUrl, entry, entryMeta, hasSidebar: props.hasSidebar ?? entry.data.template !== 'splash', headings, labels: useTranslations(localeData.locale).all(), - lastUpdated: lastUpdated instanceof Date ? lastUpdated : undefined, + lastUpdated, pagination: getPrevNextLinks(sidebar, config.pagination, entry.data), sidebar, slug, @@ -124,8 +128,8 @@ export function generateVirtualRouteData({ return routeData; } -/** Extract the virtual frontmatter properties from the props received by a virtual page. */ -function getVirtualFrontmatter(props: VirtualPageProps) { +/** Validates the virtual frontmatter properties from the props received by a virtual page. */ +function getVirtualFrontmatter(frontmatter: StarlightVirtualFrontmatter) { // This needs to be in sync with ImageMetadata. // https://github.com/withastro/astro/blob/cf993bc263b58502096f00d383266cd179f331af/packages/astro/src/assets/types.ts#L32 return StarlightVirtualFrontmatterSchema({ @@ -145,7 +149,7 @@ function getVirtualFrontmatter(props: VirtualPageProps) { z.literal('avif'), ]), }), - }).parse(props); + }).parse(frontmatter); } // https://stackoverflow.com/a/66252656/1945960 diff --git a/packages/virtual-pages-demo/Route.astro b/packages/virtual-pages-demo/Route.astro index 7fdea783a25..ac90892e084 100644 --- a/packages/virtual-pages-demo/Route.astro +++ b/packages/virtual-pages-demo/Route.astro @@ -44,22 +44,24 @@ function getVirtualPageProps(): VirtualPageProps { if (isThingD) { // Page with minimal set of props. - return { slug, title }; + return { slug, frontmatter: { title } }; } else if (isThingE) { // Page with the `splash` template, a hero including an image, an edit URL and a banner. return { - banner: { content: 'This is a banner' }, - editUrl: 'https://starlight.astro.build/', - hero: { - title: 'Hello', - tagline: 'This is a tagline', - image: { - alt: 'A star from a plugin', - file: heroStar, + slug, + frontmatter: { + banner: { content: 'This is a banner' }, + editUrl: 'https://starlight.astro.build/', + hero: { + title: 'Hello', + tagline: 'This is a tagline', + image: { + alt: 'A star from a plugin', + file: heroStar, + }, }, + title, }, - slug, - title, }; } @@ -69,8 +71,10 @@ function getVirtualPageProps(): VirtualPageProps { hasSidebar: !isThingB, lang: 'en', slug, - tableOfContents: isThingC ? { maxHeadingLevel: 4, minHeadingLevel: 2 } : false, - title, + frontmatter: { + title, + tableOfContents: isThingC ? { maxHeadingLevel: 4, minHeadingLevel: 2 } : false, + }, }; if (isThingC) { From de6ef91ca4299c78722b19ddde7027b7bd13db98 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:44:17 +0100 Subject: [PATCH 18/37] docs: fix invalid link --- docs/src/content/docs/reference/plugins.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index 946b0f0d08c..222c5ac8096 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -221,7 +221,7 @@ Site navigation sidebar entries for this page or fallback to the global `sidebar ##### `hasSidebar` **type:** `boolean` -**default:** `false` if [`template`](#template) is `'splash'`, otherwise `true` +**default:** `false` if [`template`](/reference/frontmatter/#template) is `'splash'`, otherwise `true` Whether or not the sidebar should be displayed on this page. From 2f6905ed53472f8741cca82321d3b794a36c2856 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:35:17 +0100 Subject: [PATCH 19/37] feat: WIP use user frontmatter schema for validation --- docs/src/components/about-astro.astro | 6 +++ docs/src/content/config.ts | 11 ++++- .../integrations/virtual-user-config.ts | 19 ++++++++ packages/starlight/props.ts | 2 + packages/starlight/schema.ts | 2 +- packages/starlight/utils/virtual-page.ts | 47 +++++++++++++------ packages/starlight/virtual.d.ts | 4 ++ 7 files changed, 74 insertions(+), 17 deletions(-) diff --git a/docs/src/components/about-astro.astro b/docs/src/components/about-astro.astro index 665d11258a6..62a454f3860 100644 --- a/docs/src/components/about-astro.astro +++ b/docs/src/components/about-astro.astro @@ -1,4 +1,10 @@ --- +// TODO(HiDeoo) Remove this, only for testing purposes +import type { StarlightVirtualFrontmatter } from '@astrojs/starlight/props'; + +type Testing = StarlightVirtualFrontmatter; +// FIXME(HiDeoo) ^ This is `any` :( + interface Props { title: string; } diff --git a/docs/src/content/config.ts b/docs/src/content/config.ts index 9df91b60444..ee47ecf36f4 100644 --- a/docs/src/content/config.ts +++ b/docs/src/content/config.ts @@ -1,7 +1,14 @@ -import { defineCollection } from 'astro:content'; +import { defineCollection, z } from 'astro:content'; import { docsSchema, i18nSchema } from '@astrojs/starlight/schema'; export const collections = { - docs: defineCollection({ schema: docsSchema() }), + docs: defineCollection({ + schema: docsSchema({ + extend: z.object({ + // TODO(HiDeoo) Remove this + REMOVE_ME: z.string().optional(), + }), + }), + }), i18n: defineCollection({ type: 'data', schema: i18nSchema() }), }; diff --git a/packages/starlight/integrations/virtual-user-config.ts b/packages/starlight/integrations/virtual-user-config.ts index 29a2e51239d..17712b685af 100644 --- a/packages/starlight/integrations/virtual-user-config.ts +++ b/packages/starlight/integrations/virtual-user-config.ts @@ -29,6 +29,24 @@ export function vitePluginStarlightUserConfig( ]) ); + // TODO(HiDeoo) WIP: Move or inline this somewhere else when things are working. + const collectionConfig = `import { defineCollection } from 'astro:content'; +import { docsSchema, i18nSchema } from '@astrojs/starlight/schema'; + +let userCollections; +try { + // TODO(HiDeoo) Comment why this works and relies on Vite behavior. + userCollections = (await import('/src/content/config.ts')).collections; +} catch {} +if (!userCollections) { + userCollections = { + docs: defineCollection({ schema: docsSchema() }), + i18n: defineCollection({ type: 'data', schema: i18nSchema() }), + }; +} +export const collections = userCollections; +`; + /** Map of virtual module names to their code contents as strings. */ const modules = { 'virtual:starlight/user-config': `export default ${JSON.stringify(opts)}`, @@ -48,6 +66,7 @@ export function vitePluginStarlightUserConfig( opts.logo.light )}; export const logos = { dark, light };` : 'export const logos = {};', + 'virtual:starlight/collection-config': collectionConfig, ...virtualComponentModules, } satisfies Record; diff --git a/packages/starlight/props.ts b/packages/starlight/props.ts index 76c119e40ea..1fa97404b16 100644 --- a/packages/starlight/props.ts +++ b/packages/starlight/props.ts @@ -1 +1,3 @@ export type { StarlightRouteData as Props } from './utils/route-data'; +// TODO(HiDeoo) Remove this, temporary export for testing purposes. +export type { StarlightVirtualFrontmatter } from './utils/virtual-page'; diff --git a/packages/starlight/schema.ts b/packages/starlight/schema.ts index 096132fe524..a3534af055e 100644 --- a/packages/starlight/schema.ts +++ b/packages/starlight/schema.ts @@ -9,7 +9,7 @@ import { SidebarLinkItemHTMLAttributesSchema } from './schemas/sidebar'; export { i18nSchema } from './schemas/i18n'; /** Default content collection schema for Starlight’s `docs` collection. */ -export const StarlightFrontmatterSchema = (context: SchemaContext) => +const StarlightFrontmatterSchema = (context: SchemaContext) => z.object({ /** The title of the current page. Required. */ title: z.string(), diff --git a/packages/starlight/utils/virtual-page.ts b/packages/starlight/utils/virtual-page.ts index 82760c7db04..2593d4be442 100644 --- a/packages/starlight/utils/virtual-page.ts +++ b/packages/starlight/utils/virtual-page.ts @@ -1,10 +1,10 @@ import { z } from 'astro/zod'; import type { SchemaContext } from 'astro:content'; import config from 'virtual:starlight/user-config'; +import { collections } from 'virtual:starlight/collection-config'; import { stripLeadingAndTrailingSlashes } from './path'; import { getToC, type PageProps, type StarlightRouteData } from './route-data'; import type { StarlightDocsEntry } from './routing'; -import { StarlightFrontmatterSchema } from '../schema'; import { slugToLocaleData } from './slugs'; import { getPrevNextLinks, getSidebar } from './navigation'; import { useTranslations } from './translations'; @@ -15,24 +15,43 @@ import { useTranslations } from './translations'; * The frontmatter schema for virtual pages cannot include some properties which will be omitted * and some others needs to be refined to a stricter type. */ -const StarlightVirtualFrontmatterSchema = (context: SchemaContext) => - StarlightFrontmatterSchema(context) - .omit({ - /** - * The sidebar frontmatter prop only works for pages in an autogenerated links group. Virtual - * page links cannot be autogenerated. - */ - sidebar: true, - }) - .extend({ +const StarlightVirtualFrontmatterSchema = (context: SchemaContext) => { + const docsSchema = collections.docs.schema!; + const schema = typeof docsSchema === 'function' ? docsSchema(context) : docsSchema; + + return schema + .and( + z.object({ + /** + * Virtual pages can only be edited if an edit URL is explicitly provided. + */ + editUrl: z.union([z.string().url(), z.literal(false)]).default(false), + }) + ) + .transform((frontmatter) => { /** - * Virtual pages can only be edited if an edit URL is explicitly provided. + * The `sidebar` frontmatter prop only works for pages in an autogenerated links group. + * Virtual page links cannot be autogenerated. + * + * The removal of the `sidebar` prop is done using a transformer and not using the usual + * omit method because when the frontmatter schema is extended by the user, an intersection + * between the default schema and the user schema is created using the `and` method. + * Intersections in Zod returns a `ZodIntersection` object which does not have some methods + * like `omit` or `pick`. + * + * @see https://github.com/colinhacks/zod#intersections */ - editUrl: z.union([z.string().url(), z.literal(false)]).default(false), + const { sidebar, ...others } = frontmatter; + return others; }); +}; /** Type of Starlight’s virtual frontmatter schema. */ -type StarlightVirtualFrontmatter = z.input>; +// TODO(HiDeoo) This should not end up being exported, it's temporary to test reading the type somewhere else. +// TODO(HiDeoo) Note that the type is not `any` +export type StarlightVirtualFrontmatter = z.input< + ReturnType +>; /** * The props accepted by the `` component. diff --git a/packages/starlight/virtual.d.ts b/packages/starlight/virtual.d.ts index 9a04a5723c2..93cce4430ab 100644 --- a/packages/starlight/virtual.d.ts +++ b/packages/starlight/virtual.d.ts @@ -24,6 +24,10 @@ declare module 'virtual:starlight/user-images' { }; } +declare module 'virtual:starlight/collection-config' { + export const collections: import('astro:content').ContentConfig['collections']; +} + declare module 'virtual:starlight/components/Banner' { const Banner: typeof import('./components/Banner.astro').default; export default Banner; From ef16232cf9c5c4c73d34ce41f5e10cbf9b46b831 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Wed, 31 Jan 2024 19:33:39 +0100 Subject: [PATCH 20/37] feat: more WIP to use the user frontmatter schema for validation --- docs/package.json | 2 +- docs/src/components/about-astro.astro | 6 -- docs/src/content/config.ts | 11 +-- examples/basics/package.json | 2 +- examples/tailwind/package.json | 2 +- package.json | 2 +- .../basics/virtual-route-data-extend.test.ts | 54 +++++++++++ .../basics/virtual-route-data.test.ts | 19 ++++ packages/starlight/__tests__/test-utils.ts | 11 +++ .../integrations/virtual-user-config.ts | 40 ++++---- packages/starlight/package.json | 2 +- packages/starlight/props.ts | 4 +- packages/starlight/utils/virtual-page.ts | 71 +++++++------- pnpm-lock.yaml | 93 ++++++++++++------- 14 files changed, 210 insertions(+), 109 deletions(-) create mode 100644 packages/starlight/__tests__/basics/virtual-route-data-extend.test.ts diff --git a/docs/package.json b/docs/package.json index 6129319c216..0fc9958e9cf 100644 --- a/docs/package.json +++ b/docs/package.json @@ -18,7 +18,7 @@ "@astrojs/starlight": "workspace:*", "@lunariajs/core": "^0.0.25", "@types/culori": "^2.0.0", - "astro": "^4.2.1", + "astro": "^4.2.7", "culori": "^3.2.0", "sharp": "^0.32.5", "virtual-pages-demo": "workspace:*" diff --git a/docs/src/components/about-astro.astro b/docs/src/components/about-astro.astro index 62a454f3860..665d11258a6 100644 --- a/docs/src/components/about-astro.astro +++ b/docs/src/components/about-astro.astro @@ -1,10 +1,4 @@ --- -// TODO(HiDeoo) Remove this, only for testing purposes -import type { StarlightVirtualFrontmatter } from '@astrojs/starlight/props'; - -type Testing = StarlightVirtualFrontmatter; -// FIXME(HiDeoo) ^ This is `any` :( - interface Props { title: string; } diff --git a/docs/src/content/config.ts b/docs/src/content/config.ts index ee47ecf36f4..9df91b60444 100644 --- a/docs/src/content/config.ts +++ b/docs/src/content/config.ts @@ -1,14 +1,7 @@ -import { defineCollection, z } from 'astro:content'; +import { defineCollection } from 'astro:content'; import { docsSchema, i18nSchema } from '@astrojs/starlight/schema'; export const collections = { - docs: defineCollection({ - schema: docsSchema({ - extend: z.object({ - // TODO(HiDeoo) Remove this - REMOVE_ME: z.string().optional(), - }), - }), - }), + docs: defineCollection({ schema: docsSchema() }), i18n: defineCollection({ type: 'data', schema: i18nSchema() }), }; diff --git a/examples/basics/package.json b/examples/basics/package.json index 44e341c163b..c7e66e982b0 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/starlight": "^0.16.0", - "astro": "^4.2.1", + "astro": "^4.2.7", "sharp": "^0.32.5" } } diff --git a/examples/tailwind/package.json b/examples/tailwind/package.json index 3968d5059b1..468337b6df7 100644 --- a/examples/tailwind/package.json +++ b/examples/tailwind/package.json @@ -14,7 +14,7 @@ "@astrojs/starlight": "^0.16.0", "@astrojs/starlight-tailwind": "^2.0.1", "@astrojs/tailwind": "^5.1.0", - "astro": "^4.2.1", + "astro": "^4.2.7", "sharp": "^0.32.5", "tailwindcss": "^3.4.1" } diff --git a/package.json b/package.json index 3a1501afda0..f9812285337 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.1", "@size-limit/file": "^8.2.4", - "astro": "^4.2.1", + "astro": "^4.2.7", "prettier": "^3.0.0", "prettier-plugin-astro": "^0.11.0", "size-limit": "^8.2.4" diff --git a/packages/starlight/__tests__/basics/virtual-route-data-extend.test.ts b/packages/starlight/__tests__/basics/virtual-route-data-extend.test.ts new file mode 100644 index 00000000000..8cd244d5ce9 --- /dev/null +++ b/packages/starlight/__tests__/basics/virtual-route-data-extend.test.ts @@ -0,0 +1,54 @@ +import { z } from 'astro:content'; +import { assert, expect, test, vi } from 'vitest'; +import { generateVirtualRouteData, type VirtualPageProps } from '../../utils/virtual-page'; + +vi.mock('virtual:starlight/collection-config', async () => { + const { z } = await vi.importActual('astro:content'); + return (await import('../test-utils')).mockedCollectionConfig({ + extend: z.object({ + // Make the built-in description field required. + description: z.string(), + // Add a new optional field. + category: z.string().optional(), + }), + }); +}); + +const virtualPageProps: VirtualPageProps = { + slug: 'test-slug', + frontmatter: { title: 'This is a test title' }, +}; + +test('throws a validation error if a built-in field required by the user schema is not passed down', () => { + expect.assertions(3); + + try { + generateVirtualRouteData({ + props: virtualPageProps, + url: new URL('https://example.com'), + }); + } catch (error) { + assert(error instanceof z.ZodError); + expect(error.errors).toHaveLength(1); + expect(error.errors.at(0)?.path).toEqual(['description']); + expect(error.errors.at(0)?.code).toBe('invalid_type'); + } +}); + +test('returns new field defined in the user schema', () => { + const category = 'test category'; + const data = generateVirtualRouteData({ + props: { + ...virtualPageProps, + frontmatter: { + ...virtualPageProps.frontmatter, + description: 'test description', + // @ts-expect-error - Custom field defined in the user schema. + category, + }, + }, + url: new URL('https://example.com'), + }); + // @ts-expect-error - Custom field defined in the user schema. + expect(data.entry.data.category).toBe(category); +}); diff --git a/packages/starlight/__tests__/basics/virtual-route-data.test.ts b/packages/starlight/__tests__/basics/virtual-route-data.test.ts index a50e6e2a584..141805aca48 100644 --- a/packages/starlight/__tests__/basics/virtual-route-data.test.ts +++ b/packages/starlight/__tests__/basics/virtual-route-data.test.ts @@ -1,6 +1,10 @@ import { expect, test, vi } from 'vitest'; import { generateVirtualRouteData, type VirtualPageProps } from '../../utils/virtual-page'; +vi.mock('virtual:starlight/collection-config', async () => + (await import('../test-utils')).mockedCollectionConfig() +); + vi.mock('astro:content', async () => (await import('../test-utils')).mockedAstroContent({ docs: [ @@ -351,3 +355,18 @@ test('uses provided edit URL if any', () => { expect(data.editUrl).toEqual(new URL(editUrl)); expect(data.entry.data.editUrl).toEqual(editUrl); }); + +test('strips unknown frontmatter properties', () => { + const data = generateVirtualRouteData({ + props: { + ...virtualPageProps, + frontmatter: { + ...virtualPageProps.frontmatter, + // @ts-expect-error - This is an unknown property. + unknown: 'test', + }, + }, + url: new URL('https://example.com'), + }); + expect('unknown' in data.entry.data).toBe(false); +}); diff --git a/packages/starlight/__tests__/test-utils.ts b/packages/starlight/__tests__/test-utils.ts index 96ff4f649e9..7c145378f81 100644 --- a/packages/starlight/__tests__/test-utils.ts +++ b/packages/starlight/__tests__/test-utils.ts @@ -56,3 +56,14 @@ export async function mockedAstroContent({ getCollection: (collection: 'docs' | 'i18n') => (collection === 'i18n' ? mockDicts : mockDocs), }; } + +export async function mockedCollectionConfig(docsUserSchema?: Parameters[0]) { + const content = await vi.importActual('astro:content'); + const schemas = await vi.importActual('../schema'); + return { + collections: { + docs: content.defineCollection({ schema: schemas.docsSchema(docsUserSchema) }), + i18n: content.defineCollection({ type: 'data', schema: schemas.i18nSchema() }), + }, + }; +} diff --git a/packages/starlight/integrations/virtual-user-config.ts b/packages/starlight/integrations/virtual-user-config.ts index 17712b685af..cdb2f4eb70d 100644 --- a/packages/starlight/integrations/virtual-user-config.ts +++ b/packages/starlight/integrations/virtual-user-config.ts @@ -29,23 +29,29 @@ export function vitePluginStarlightUserConfig( ]) ); - // TODO(HiDeoo) WIP: Move or inline this somewhere else when things are working. - const collectionConfig = `import { defineCollection } from 'astro:content'; -import { docsSchema, i18nSchema } from '@astrojs/starlight/schema'; + // TODO(HiDeoo) Clean this + const collectionConfigPath = '/src/content/config.ts'; + // const collectionConfigPath = resolve(fileURLToPath(root), 'src/content/test.ts'); -let userCollections; -try { - // TODO(HiDeoo) Comment why this works and relies on Vite behavior. - userCollections = (await import('/src/content/config.ts')).collections; -} catch {} -if (!userCollections) { - userCollections = { - docs: defineCollection({ schema: docsSchema() }), - i18n: defineCollection({ type: 'data', schema: i18nSchema() }), - }; -} -export const collections = userCollections; -`; + // TODO(HiDeoo) Comment this when this is working + const collectionConfigModule = `import { defineCollection } from 'astro:content'; + import { docsSchema, i18nSchema } from '@astrojs/starlight/schema'; + let userCollections; + try { + userCollections = (await import('${collectionConfigPath}')).collections; + // TODO(HiDeoo) Remove this + console.log('🚨 [virtual-user-config.ts:41] userCollections:', userCollections) + } catch (error) { + // TODO(HiDeoo) Remove this + console.log('🚨 [virtual-user-config.ts:43] error:', error) + } + if (!userCollections) { + userCollections = { + docs: defineCollection({ schema: docsSchema() }), + i18n: defineCollection({ type: 'data', schema: i18nSchema() }), + }; + } + export const collections = userCollections;`; /** Map of virtual module names to their code contents as strings. */ const modules = { @@ -66,7 +72,7 @@ export const collections = userCollections; opts.logo.light )}; export const logos = { dark, light };` : 'export const logos = {};', - 'virtual:starlight/collection-config': collectionConfig, + 'virtual:starlight/collection-config': collectionConfigModule, ...virtualComponentModules, } satisfies Record; diff --git a/packages/starlight/package.json b/packages/starlight/package.json index e93346c38ae..68729122611 100644 --- a/packages/starlight/package.json +++ b/packages/starlight/package.json @@ -173,7 +173,7 @@ "@astrojs/markdown-remark": "^4.0.1", "@types/node": "^18.16.19", "@vitest/coverage-v8": "^0.33.0", - "astro": "^4.2.1", + "astro": "^4.2.7", "vitest": "^0.33.0" }, "dependencies": { diff --git a/packages/starlight/props.ts b/packages/starlight/props.ts index 1fa97404b16..a0f47d4df80 100644 --- a/packages/starlight/props.ts +++ b/packages/starlight/props.ts @@ -1,3 +1 @@ -export type { StarlightRouteData as Props } from './utils/route-data'; -// TODO(HiDeoo) Remove this, temporary export for testing purposes. -export type { StarlightVirtualFrontmatter } from './utils/virtual-page'; +export type { StarlightRouteData as Props } from './utils/route-data'; \ No newline at end of file diff --git a/packages/starlight/utils/virtual-page.ts b/packages/starlight/utils/virtual-page.ts index 2593d4be442..93f643c8834 100644 --- a/packages/starlight/utils/virtual-page.ts +++ b/packages/starlight/utils/virtual-page.ts @@ -1,5 +1,5 @@ import { z } from 'astro/zod'; -import type { SchemaContext } from 'astro:content'; +import type { SchemaContext, ContentConfig } from 'astro:content'; import config from 'virtual:starlight/user-config'; import { collections } from 'virtual:starlight/collection-config'; import { stripLeadingAndTrailingSlashes } from './path'; @@ -16,42 +16,47 @@ import { useTranslations } from './translations'; * and some others needs to be refined to a stricter type. */ const StarlightVirtualFrontmatterSchema = (context: SchemaContext) => { - const docsSchema = collections.docs.schema!; + // We manually cast the schema to its own type directly imported from `astro:content` even though + // the virtual module definition is using this same type otherwise the type will fail to be + // resolved by consumers and fall back to `any`. + const docsSchema = (collections.docs.schema as ContentConfig['collections']['docs']['schema'])!; const schema = typeof docsSchema === 'function' ? docsSchema(context) : docsSchema; - return schema - .and( - z.object({ - /** - * Virtual pages can only be edited if an edit URL is explicitly provided. - */ - editUrl: z.union([z.string().url(), z.literal(false)]).default(false), - }) - ) - .transform((frontmatter) => { - /** - * The `sidebar` frontmatter prop only works for pages in an autogenerated links group. - * Virtual page links cannot be autogenerated. - * - * The removal of the `sidebar` prop is done using a transformer and not using the usual - * omit method because when the frontmatter schema is extended by the user, an intersection - * between the default schema and the user schema is created using the `and` method. - * Intersections in Zod returns a `ZodIntersection` object which does not have some methods - * like `omit` or `pick`. - * - * @see https://github.com/colinhacks/zod#intersections - */ - const { sidebar, ...others } = frontmatter; - return others; - }); + return schema.transform((frontmatter) => { + /** + * Virtual pages can only be edited if an edit URL is explicitly provided. + * The `sidebar` frontmatter prop only works for pages in an autogenerated links group. + * Virtual page links cannot be autogenerated. + * + * These changes to the schema are done using a transformer and not using the usual `omit` + * method because when the frontmatter schema is extended by the user, an intersection between + * the default schema and the user schema is created using the `and` method. Intersections in + * Zod returns a `ZodIntersection` object which does not have some methods like `omit` or + * `pick`. + * + * This transformer only sets the `editUrl` default value and removes the `sidebar` property + * from the validated output but does not appply any changes to the input schema type itself so + * this needs to be done manually. + * + * @see StarlightVirtualFrontmatter + * @see https://github.com/colinhacks/zod#intersections + */ + const { editUrl, sidebar, ...others } = frontmatter; + const virtualEditUrl = editUrl === undefined || editUrl === true ? false : editUrl; + return { ...others, editUrl: virtualEditUrl }; + }); }; -/** Type of Starlight’s virtual frontmatter schema. */ -// TODO(HiDeoo) This should not end up being exported, it's temporary to test reading the type somewhere else. -// TODO(HiDeoo) Note that the type is not `any` -export type StarlightVirtualFrontmatter = z.input< - ReturnType ->; +/** + * Type of Starlight’s virtual frontmatter schema. + * We manually refines the `editUrl` type and omit the `sidebar` property as it's not possible to + * do that on the schema itself using Zod but the proper validation is still using a transformer. + * @see StarlightVirtualFrontmatterSchema + */ +type StarlightVirtualFrontmatter = Omit< + z.input>, + 'editUrl' | 'sidebar' +> & { editUrl?: string | false }; /** * The props accepted by the `` component. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad12eba5bf5..adbfd9fbc34 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^8.2.4 version: 8.2.4(size-limit@8.2.4) astro: - specifier: ^4.2.1 - version: 4.2.1(@types/node@18.16.19) + specifier: ^4.2.7 + version: 4.2.8(@types/node@18.16.19) prettier: specifier: ^3.0.0 version: 3.0.0 @@ -34,7 +34,7 @@ importers: dependencies: '@astro-community/astro-embed-youtube': specifier: ^0.4.4 - version: 0.4.4(astro@4.2.1) + version: 0.4.4(astro@4.2.8) '@astrojs/starlight': specifier: workspace:* version: link:../packages/starlight @@ -45,8 +45,8 @@ importers: specifier: ^2.0.0 version: 2.0.0 astro: - specifier: ^4.2.1 - version: 4.2.1(@types/node@18.16.19) + specifier: ^4.2.7 + version: 4.2.8(@types/node@18.16.19) culori: specifier: ^3.2.0 version: 3.2.0 @@ -77,7 +77,7 @@ importers: version: 13.0.1 starlight-links-validator: specifier: ^0.5.1 - version: 0.5.1(@astrojs/starlight@packages+starlight)(astro@4.2.1) + version: 0.5.1(@astrojs/starlight@packages+starlight)(astro@4.2.8) start-server-and-test: specifier: ^2.0.0 version: 2.0.0 @@ -91,8 +91,8 @@ importers: specifier: ^0.16.0 version: link:../../packages/starlight astro: - specifier: ^4.2.1 - version: 4.2.1(@types/node@18.16.19) + specifier: ^4.2.7 + version: 4.2.8(@types/node@18.16.19) sharp: specifier: ^0.32.5 version: 0.32.6 @@ -107,10 +107,10 @@ importers: version: link:../../packages/tailwind '@astrojs/tailwind': specifier: ^5.1.0 - version: 5.1.0(astro@4.2.1)(tailwindcss@3.4.1) + version: 5.1.0(astro@4.2.8)(tailwindcss@3.4.1) astro: - specifier: ^4.2.1 - version: 4.2.1(@types/node@18.16.19) + specifier: ^4.2.7 + version: 4.2.8(@types/node@18.16.19) sharp: specifier: ^0.32.5 version: 0.32.6 @@ -134,7 +134,7 @@ importers: dependencies: '@astrojs/mdx': specifier: ^2.0.4 - version: 2.0.4(astro@4.2.1) + version: 2.0.4(astro@4.2.8) '@astrojs/sitemap': specifier: ^3.0.4 version: 3.0.4 @@ -149,7 +149,7 @@ importers: version: 4.0.3 astro-expressive-code: specifier: ^0.31.0 - version: 0.31.0(astro@4.2.1) + version: 0.31.0(astro@4.2.8) bcp-47: specifier: ^2.1.0 version: 2.1.0 @@ -194,8 +194,8 @@ importers: specifier: ^0.33.0 version: 0.33.0(vitest@0.33.0) astro: - specifier: ^4.2.1 - version: 4.2.1(@types/node@18.16.19) + specifier: ^4.2.7 + version: 4.2.8(@types/node@18.16.19) vitest: specifier: ^0.33.0 version: 0.33.0 @@ -207,7 +207,7 @@ importers: version: link:../starlight '@astrojs/tailwind': specifier: ^5.0.0 - version: 5.1.0(astro@4.2.1)(tailwindcss@3.4.1) + version: 5.1.0(astro@4.2.8)(tailwindcss@3.4.1) tailwindcss: specifier: ^3.3.3 version: 3.4.1 @@ -376,12 +376,12 @@ packages: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.19 - /@astro-community/astro-embed-youtube@0.4.4(astro@4.2.1): + /@astro-community/astro-embed-youtube@0.4.4(astro@4.2.8): resolution: {integrity: sha512-fYlycLrJFNnibZ9VHPSJO766kO2IgqYQU4mBd4iaDMaicL0gGX9cVZ80QdnpzGrI6w0XOJOY7prx86eWEVBy8w==} peerDependencies: astro: ^2.0.0 || ^3.0.0-beta || ^4.0.0-beta dependencies: - astro: 4.2.1(@types/node@18.16.19) + astro: 4.2.8(@types/node@18.16.19) lite-youtube-embed: 0.2.0 dev: false @@ -435,8 +435,29 @@ packages: vfile: 6.0.1 transitivePeerDependencies: - supports-color + dev: true - /@astrojs/mdx@2.0.4(astro@4.2.1): + /@astrojs/markdown-remark@4.2.1: + resolution: {integrity: sha512-2RQBIwrq+2qPYtp99bH+eL5hfbK0BoxXla85lHsRpIX/IsGqFrPX6pXI2cbWPihBwGbKCdxS6uZNX2QerZWwpQ==} + dependencies: + '@astrojs/prism': 3.0.0 + github-slugger: 2.0.0 + import-meta-resolve: 4.0.0 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.0 + remark-gfm: 4.0.0 + remark-parse: 11.0.0 + remark-rehype: 11.0.0 + remark-smartypants: 2.0.0 + shikiji: 0.9.19 + unified: 11.0.4 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + transitivePeerDependencies: + - supports-color + + /@astrojs/mdx@2.0.4(astro@4.2.8): resolution: {integrity: sha512-8q8p7AfiGa6CEKUEEWDLZ7HsfonmZlzx8HITZp8eJnkh+n6mmD9vQTpuFNjJc3hiiMEEKLGTIjOUGAU4aGBkrA==} engines: {node: '>=18.14.1'} peerDependencies: @@ -445,7 +466,7 @@ packages: '@astrojs/markdown-remark': 4.0.1 '@mdx-js/mdx': 3.0.0 acorn: 8.11.2 - astro: 4.2.1(@types/node@18.16.19) + astro: 4.2.8(@types/node@18.16.19) es-module-lexer: 1.4.1 estree-util-visit: 2.0.0 github-slugger: 2.0.0 @@ -475,13 +496,13 @@ packages: zod: 3.22.4 dev: false - /@astrojs/tailwind@5.1.0(astro@4.2.1)(tailwindcss@3.4.1): + /@astrojs/tailwind@5.1.0(astro@4.2.8)(tailwindcss@3.4.1): resolution: {integrity: sha512-BJoCDKuWhU9FT2qYg+fr6Nfb3qP4ShtyjXGHKA/4mHN94z7BGcmauQK23iy+YH5qWvTnhqkd6mQPQ1yTZTe9Ig==} peerDependencies: astro: ^3.0.0 || ^4.0.0 tailwindcss: ^3.0.24 dependencies: - astro: 4.2.1(@types/node@18.16.19) + astro: 4.2.8(@types/node@18.16.19) autoprefixer: 10.4.15(postcss@8.4.33) postcss: 8.4.33 postcss-load-config: 4.0.2(postcss@8.4.33) @@ -2105,23 +2126,23 @@ packages: hasBin: true dev: false - /astro-expressive-code@0.31.0(astro@4.2.1): + /astro-expressive-code@0.31.0(astro@4.2.8): resolution: {integrity: sha512-o6eFrRSYLnlM/2FKkO3MgkbmVxT8N6DJcKvbRf1wbUcRXpz7s1KfugbdsaGw3ABEWUBuQIBsRppcGGw2L816Vg==} peerDependencies: astro: ^3.3.0 || ^4.0.0-beta dependencies: - astro: 4.2.1(@types/node@18.16.19) + astro: 4.2.8(@types/node@18.16.19) remark-expressive-code: 0.31.0 dev: false - /astro@4.2.1(@types/node@18.16.19): - resolution: {integrity: sha512-TcrveW2/lohmljrbTUgcDxajEdF1yK+zBvb7SXroloGix/d4jkegO6GANFgvyy0zprMyajW7qgJEFyhWUX86Vw==} + /astro@4.2.8(@types/node@18.16.19): + resolution: {integrity: sha512-h78IAdSEPMo1bvR40HECQYpnMPfDnk9WxRNJ1+Hw5szk4k5IMUw3nG153nErJABRnaxb6WLv7dtS4tukzJz0mw==} engines: {node: '>=18.14.1', npm: '>=6.14.0'} hasBin: true dependencies: '@astrojs/compiler': 2.5.0 '@astrojs/internal-helpers': 0.2.1 - '@astrojs/markdown-remark': 4.1.0 + '@astrojs/markdown-remark': 4.2.1 '@astrojs/telemetry': 3.0.4 '@babel/core': 7.23.5 '@babel/generator': 7.23.5 @@ -2139,6 +2160,7 @@ packages: clsx: 2.0.0 common-ancestor-path: 1.0.1 cookie: 0.6.0 + cssesc: 3.0.0 debug: 4.3.4 deterministic-object-hash: 2.0.2 devalue: 4.3.2 @@ -2177,8 +2199,8 @@ packages: tsconfck: 3.0.0 unist-util-visit: 5.0.0 vfile: 6.0.1 - vite: 5.0.11(@types/node@18.16.19) - vitefu: 0.2.5(vite@5.0.11) + vite: 5.0.12(@types/node@18.16.19) + vitefu: 0.2.5(vite@5.0.12) which-pm: 2.1.1 yargs-parser: 21.1.1 zod: 3.22.4 @@ -2684,7 +2706,6 @@ packages: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true - dev: false /csv-generate@3.4.3: resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} @@ -6394,7 +6415,7 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true - /starlight-links-validator@0.5.1(@astrojs/starlight@packages+starlight)(astro@4.2.1): + /starlight-links-validator@0.5.1(@astrojs/starlight@packages+starlight)(astro@4.2.8): resolution: {integrity: sha512-6iNl/bTTFN65T3fx1oMdspZltlYyonmrnno4dWjMzJuFHO79ZGKyzrtuMD59kiM9Y85eSy3MOpU1wuPoY28fMg==} engines: {node: '>=18.14.1'} peerDependencies: @@ -6402,7 +6423,7 @@ packages: astro: '>=4.0.0' dependencies: '@astrojs/starlight': link:packages/starlight - astro: 4.2.1(@types/node@18.16.19) + astro: 4.2.8(@types/node@18.16.19) github-slugger: 2.0.0 hast-util-from-html: 2.0.1 hast-util-has-property: 3.0.0 @@ -7120,8 +7141,8 @@ packages: fsevents: 2.3.3 dev: true - /vite@5.0.11(@types/node@18.16.19): - resolution: {integrity: sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==} + /vite@5.0.12(@types/node@18.16.19): + resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -7155,7 +7176,7 @@ packages: optionalDependencies: fsevents: 2.3.3 - /vitefu@0.2.5(vite@5.0.11): + /vitefu@0.2.5(vite@5.0.12): resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} peerDependencies: vite: ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -7163,7 +7184,7 @@ packages: vite: optional: true dependencies: - vite: 5.0.11(@types/node@18.16.19) + vite: 5.0.12(@types/node@18.16.19) /vitest@0.33.0: resolution: {integrity: sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==} From 93eb5086dc896a380088b8116ac6e33ba547db75 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Thu, 1 Feb 2024 11:05:08 +0100 Subject: [PATCH 21/37] feat: use user frontmatter schema for validation --- .../basics/virtual-route-data-extend.test.ts | 8 +-- .../basics/virtual-route-data.test.ts | 60 +++++++++---------- .../starlight/components/VirtualPage.astro | 2 +- .../integrations/virtual-user-config.ts | 30 ++-------- packages/starlight/props.ts | 2 +- packages/starlight/utils/virtual-page.ts | 37 +++++++----- packages/starlight/virtual.d.ts | 2 +- 7 files changed, 64 insertions(+), 77 deletions(-) diff --git a/packages/starlight/__tests__/basics/virtual-route-data-extend.test.ts b/packages/starlight/__tests__/basics/virtual-route-data-extend.test.ts index 8cd244d5ce9..4e759e54dbb 100644 --- a/packages/starlight/__tests__/basics/virtual-route-data-extend.test.ts +++ b/packages/starlight/__tests__/basics/virtual-route-data-extend.test.ts @@ -19,11 +19,11 @@ const virtualPageProps: VirtualPageProps = { frontmatter: { title: 'This is a test title' }, }; -test('throws a validation error if a built-in field required by the user schema is not passed down', () => { +test('throws a validation error if a built-in field required by the user schema is not passed down', async () => { expect.assertions(3); try { - generateVirtualRouteData({ + await generateVirtualRouteData({ props: virtualPageProps, url: new URL('https://example.com'), }); @@ -35,9 +35,9 @@ test('throws a validation error if a built-in field required by the user schema } }); -test('returns new field defined in the user schema', () => { +test('returns new field defined in the user schema', async () => { const category = 'test category'; - const data = generateVirtualRouteData({ + const data = await generateVirtualRouteData({ props: { ...virtualPageProps, frontmatter: { diff --git a/packages/starlight/__tests__/basics/virtual-route-data.test.ts b/packages/starlight/__tests__/basics/virtual-route-data.test.ts index 141805aca48..13d36387d46 100644 --- a/packages/starlight/__tests__/basics/virtual-route-data.test.ts +++ b/packages/starlight/__tests__/basics/virtual-route-data.test.ts @@ -19,8 +19,8 @@ const virtualPageProps: VirtualPageProps = { frontmatter: { title: 'This is a test title' }, }; -test('adds data to route shape', () => { - const data = generateVirtualRouteData({ +test('adds data to route shape', async () => { + const data = await generateVirtualRouteData({ props: virtualPageProps, url: new URL('https://example.com'), }); @@ -48,20 +48,20 @@ test('adds data to route shape', () => { expect(data.entryMeta.lang).toBe('en'); }); -test('adds custom data to route shape', () => { +test('adds custom data to route shape', async () => { const props: VirtualPageProps = { ...virtualPageProps, hasSidebar: false, dir: 'rtl', lang: 'ks', }; - const data = generateVirtualRouteData({ props, url: new URL('https://example.com') }); + const data = await generateVirtualRouteData({ props, url: new URL('https://example.com') }); expect(data.hasSidebar).toBe(props.hasSidebar); expect(data.entryMeta.dir).toBe(props.dir); expect(data.entryMeta.lang).toBe(props.lang); }); -test('adds custom virtual frontmatter data to route shape', () => { +test('adds custom virtual frontmatter data to route shape', async () => { const props: VirtualPageProps = { ...virtualPageProps, frontmatter: { @@ -72,7 +72,7 @@ test('adds custom virtual frontmatter data to route shape', () => { template: 'splash', }, }; - const data = generateVirtualRouteData({ props, url: new URL('https://example.com') }); + const data = await generateVirtualRouteData({ props, url: new URL('https://example.com') }); expect(data.entry.data.head).toMatchInlineSnapshot(` [ { @@ -90,8 +90,8 @@ test('adds custom virtual frontmatter data to route shape', () => { expect(data.entry.data.template).toBe(props.frontmatter.template); }); -test('uses generated sidebar when no sidebar is provided', () => { - const data = generateVirtualRouteData({ +test('uses generated sidebar when no sidebar is provided', async () => { + const data = await generateVirtualRouteData({ props: virtualPageProps, url: new URL('https://example.com'), }); @@ -103,8 +103,8 @@ test('uses generated sidebar when no sidebar is provided', () => { `); }); -test('uses provided sidebar if any', () => { - const data = generateVirtualRouteData({ +test('uses provided sidebar if any', async () => { + const data = await generateVirtualRouteData({ props: { ...virtualPageProps, sidebar: [ @@ -136,8 +136,8 @@ test('uses provided sidebar if any', () => { `); }); -test('uses provided pagination if any', () => { - const data = generateVirtualRouteData({ +test('uses provided pagination if any', async () => { + const data = await generateVirtualRouteData({ props: { ...virtualPageProps, frontmatter: { @@ -176,20 +176,20 @@ test('uses provided pagination if any', () => { `); }); -test('uses provided headings if any', () => { +test('uses provided headings if any', async () => { const headings = [ { depth: 2, slug: 'heading-1', text: 'Heading 1' }, { depth: 3, slug: 'heading-2', text: 'Heading 2' }, ]; - const data = generateVirtualRouteData({ + const data = await generateVirtualRouteData({ props: { ...virtualPageProps, headings }, url: new URL('https://example.com'), }); expect(data.headings).toEqual(headings); }); -test('generates the table of contents for provided headings', () => { - const data = generateVirtualRouteData({ +test('generates the table of contents for provided headings', async () => { + const data = await generateVirtualRouteData({ props: { ...virtualPageProps, headings: [ @@ -230,8 +230,8 @@ test('generates the table of contents for provided headings', () => { `); }); -test('respects the `tableOfContents` level configuration', () => { - const data = generateVirtualRouteData({ +test('respects the `tableOfContents` level configuration', async () => { + const data = await generateVirtualRouteData({ props: { ...virtualPageProps, headings: [ @@ -280,8 +280,8 @@ test('respects the `tableOfContents` level configuration', () => { `); }); -test('disables table of contents if frontmatter includes `tableOfContents: false`', () => { - const data = generateVirtualRouteData({ +test('disables table of contents if frontmatter includes `tableOfContents: false`', async () => { + const data = await generateVirtualRouteData({ props: { ...virtualPageProps, headings: [ @@ -298,8 +298,8 @@ test('disables table of contents if frontmatter includes `tableOfContents: false expect(data.toc).toBeUndefined(); }); -test('disables table of contents for splash template', () => { - const data = generateVirtualRouteData({ +test('disables table of contents for splash template', async () => { + const data = await generateVirtualRouteData({ props: { ...virtualPageProps, headings: [ @@ -316,9 +316,9 @@ test('disables table of contents for splash template', () => { expect(data.toc).toBeUndefined(); }); -test('hides the sidebar if the `hasSidebar` option is not specified and the splash template is used', () => { +test('hides the sidebar if the `hasSidebar` option is not specified and the splash template is used', async () => { const { hasSidebar, ...otherProps } = virtualPageProps; - const data = generateVirtualRouteData({ + const data = await generateVirtualRouteData({ props: { ...otherProps, frontmatter: { @@ -331,8 +331,8 @@ test('hides the sidebar if the `hasSidebar` option is not specified and the spla expect(data.hasSidebar).toBe(false); }); -test('includes localized labels', () => { - const data = generateVirtualRouteData({ +test('includes localized labels', async () => { + const data = await generateVirtualRouteData({ props: virtualPageProps, url: new URL('https://example.com'), }); @@ -340,9 +340,9 @@ test('includes localized labels', () => { expect(data.labels['skipLink.label']).toBe('Skip to content'); }); -test('uses provided edit URL if any', () => { +test('uses provided edit URL if any', async () => { const editUrl = 'https://example.com/edit'; - const data = generateVirtualRouteData({ + const data = await generateVirtualRouteData({ props: { ...virtualPageProps, frontmatter: { @@ -356,8 +356,8 @@ test('uses provided edit URL if any', () => { expect(data.entry.data.editUrl).toEqual(editUrl); }); -test('strips unknown frontmatter properties', () => { - const data = generateVirtualRouteData({ +test('strips unknown frontmatter properties', async () => { + const data = await generateVirtualRouteData({ props: { ...virtualPageProps, frontmatter: { diff --git a/packages/starlight/components/VirtualPage.astro b/packages/starlight/components/VirtualPage.astro index d5c03fca659..9d20023e47b 100644 --- a/packages/starlight/components/VirtualPage.astro +++ b/packages/starlight/components/VirtualPage.astro @@ -5,6 +5,6 @@ import Page from './Page.astro'; export type VirtualPageProps = Props; --- - + diff --git a/packages/starlight/integrations/virtual-user-config.ts b/packages/starlight/integrations/virtual-user-config.ts index cdb2f4eb70d..d752a966126 100644 --- a/packages/starlight/integrations/virtual-user-config.ts +++ b/packages/starlight/integrations/virtual-user-config.ts @@ -29,30 +29,6 @@ export function vitePluginStarlightUserConfig( ]) ); - // TODO(HiDeoo) Clean this - const collectionConfigPath = '/src/content/config.ts'; - // const collectionConfigPath = resolve(fileURLToPath(root), 'src/content/test.ts'); - - // TODO(HiDeoo) Comment this when this is working - const collectionConfigModule = `import { defineCollection } from 'astro:content'; - import { docsSchema, i18nSchema } from '@astrojs/starlight/schema'; - let userCollections; - try { - userCollections = (await import('${collectionConfigPath}')).collections; - // TODO(HiDeoo) Remove this - console.log('🚨 [virtual-user-config.ts:41] userCollections:', userCollections) - } catch (error) { - // TODO(HiDeoo) Remove this - console.log('🚨 [virtual-user-config.ts:43] error:', error) - } - if (!userCollections) { - userCollections = { - docs: defineCollection({ schema: docsSchema() }), - i18n: defineCollection({ type: 'data', schema: i18nSchema() }), - }; - } - export const collections = userCollections;`; - /** Map of virtual module names to their code contents as strings. */ const modules = { 'virtual:starlight/user-config': `export default ${JSON.stringify(opts)}`, @@ -72,7 +48,11 @@ export function vitePluginStarlightUserConfig( opts.logo.light )}; export const logos = { dark, light };` : 'export const logos = {};', - 'virtual:starlight/collection-config': collectionConfigModule, + 'virtual:starlight/collection-config': `let userCollections; + try { + userCollections = (await import('/src/content/config.ts')).collections; + } catch {} + export const collections = userCollections;`, ...virtualComponentModules, } satisfies Record; diff --git a/packages/starlight/props.ts b/packages/starlight/props.ts index a0f47d4df80..76c119e40ea 100644 --- a/packages/starlight/props.ts +++ b/packages/starlight/props.ts @@ -1 +1 @@ -export type { StarlightRouteData as Props } from './utils/route-data'; \ No newline at end of file +export type { StarlightRouteData as Props } from './utils/route-data'; diff --git a/packages/starlight/utils/virtual-page.ts b/packages/starlight/utils/virtual-page.ts index 93f643c8834..40ff253f4f2 100644 --- a/packages/starlight/utils/virtual-page.ts +++ b/packages/starlight/utils/virtual-page.ts @@ -1,13 +1,13 @@ import { z } from 'astro/zod'; -import type { SchemaContext, ContentConfig } from 'astro:content'; +import { type ContentConfig, type SchemaContext } from 'astro:content'; import config from 'virtual:starlight/user-config'; -import { collections } from 'virtual:starlight/collection-config'; import { stripLeadingAndTrailingSlashes } from './path'; import { getToC, type PageProps, type StarlightRouteData } from './route-data'; import type { StarlightDocsEntry } from './routing'; import { slugToLocaleData } from './slugs'; import { getPrevNextLinks, getSidebar } from './navigation'; import { useTranslations } from './translations'; +import { docsSchema } from '../schema'; /** * The frontmatter schema for virtual pages derived from the default schema for Starlight’s `docs` @@ -15,12 +15,9 @@ import { useTranslations } from './translations'; * The frontmatter schema for virtual pages cannot include some properties which will be omitted * and some others needs to be refined to a stricter type. */ -const StarlightVirtualFrontmatterSchema = (context: SchemaContext) => { - // We manually cast the schema to its own type directly imported from `astro:content` even though - // the virtual module definition is using this same type otherwise the type will fail to be - // resolved by consumers and fall back to `any`. - const docsSchema = (collections.docs.schema as ContentConfig['collections']['docs']['schema'])!; - const schema = typeof docsSchema === 'function' ? docsSchema(context) : docsSchema; +const StarlightVirtualFrontmatterSchema = async (context: SchemaContext) => { + const userDocsSchema = await getUserDocsSchema(); + const schema = typeof userDocsSchema === 'function' ? userDocsSchema(context) : userDocsSchema; return schema.transform((frontmatter) => { /** @@ -54,7 +51,7 @@ const StarlightVirtualFrontmatterSchema = (context: SchemaContext) => { * @see StarlightVirtualFrontmatterSchema */ type StarlightVirtualFrontmatter = Omit< - z.input>, + z.input>>, 'editUrl' | 'sidebar' > & { editUrl?: string | false }; @@ -86,15 +83,15 @@ type VirtualDocsEntry = Omit & { id: string; }; -export function generateVirtualRouteData({ +export async function generateVirtualRouteData({ props, url, }: { props: VirtualPageProps; url: URL; -}): StarlightRouteData { +}): Promise { const { isFallback, frontmatter, slug, ...routeProps } = props; - const virtualFrontmatter = getVirtualFrontmatter(frontmatter); + const virtualFrontmatter = await getVirtualFrontmatter(frontmatter); const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; const localeData = slugToLocaleData(slug); const sidebar = props.sidebar ?? getSidebar(url.pathname, localeData.locale); @@ -153,10 +150,10 @@ export function generateVirtualRouteData({ } /** Validates the virtual frontmatter properties from the props received by a virtual page. */ -function getVirtualFrontmatter(frontmatter: StarlightVirtualFrontmatter) { +async function getVirtualFrontmatter(frontmatter: StarlightVirtualFrontmatter) { // This needs to be in sync with ImageMetadata. // https://github.com/withastro/astro/blob/cf993bc263b58502096f00d383266cd179f331af/packages/astro/src/assets/types.ts#L32 - return StarlightVirtualFrontmatterSchema({ + const schema = await StarlightVirtualFrontmatterSchema({ image: () => z.object({ src: z.string(), @@ -173,7 +170,17 @@ function getVirtualFrontmatter(frontmatter: StarlightVirtualFrontmatter) { z.literal('avif'), ]), }), - }).parse(frontmatter); + }); + + return schema.parse(frontmatter); +} + +/** Returns the user docs schema and falls back to the default schema if needed. */ +async function getUserDocsSchema(): Promise< + NonNullable +> { + const userCollections = (await import('virtual:starlight/collection-config')).collections; + return userCollections?.docs.schema ?? docsSchema(); } // https://stackoverflow.com/a/66252656/1945960 diff --git a/packages/starlight/virtual.d.ts b/packages/starlight/virtual.d.ts index 93cce4430ab..4dfce6e3b11 100644 --- a/packages/starlight/virtual.d.ts +++ b/packages/starlight/virtual.d.ts @@ -25,7 +25,7 @@ declare module 'virtual:starlight/user-images' { } declare module 'virtual:starlight/collection-config' { - export const collections: import('astro:content').ContentConfig['collections']; + export const collections: import('astro:content').ContentConfig['collections'] | undefined; } declare module 'virtual:starlight/components/Banner' { From a887c623b48dcf34783d6cc9e7194ec0320d47e7 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Thu, 1 Feb 2024 11:06:13 +0100 Subject: [PATCH 22/37] refactor: virtual page type helpers --- packages/starlight/utils/virtual-page.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/starlight/utils/virtual-page.ts b/packages/starlight/utils/virtual-page.ts index 40ff253f4f2..499dd32fae9 100644 --- a/packages/starlight/utils/virtual-page.ts +++ b/packages/starlight/utils/virtual-page.ts @@ -184,17 +184,17 @@ async function getUserDocsSchema(): Promise< } // https://stackoverflow.com/a/66252656/1945960 -type RemoveIndexSignature = { - [Tkey in keyof TType as string extends Tkey +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never - : number extends Tkey + : number extends K ? never - : symbol extends Tkey + : symbol extends K ? never - : Tkey]: TType[Tkey]; + : K]: T[K]; }; // https://www.totaltypescript.com/concepts/the-prettify-helper -type Prettify = { - [TKey in keyof TType]: TType[TKey]; +type Prettify = { + [K in keyof T]: T[K]; } & {}; From b974a7d8135737676cf2c848152e5f0d6e626d96 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Sat, 10 Feb 2024 12:35:34 +0100 Subject: [PATCH 23/37] chore: fix lockfile --- pnpm-lock.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb72fea16ff..d5fce763070 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1691,6 +1691,7 @@ packages: /@vitest/snapshot@1.2.2: resolution: {integrity: sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==} + dependencies: magic-string: 0.30.5 pathe: 1.1.2 pretty-format: 29.7.0 @@ -1725,6 +1726,7 @@ packages: /acorn-walk@8.3.2: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} dev: true /acorn@8.11.3: @@ -1880,6 +1882,7 @@ packages: /astro-expressive-code@0.32.4(astro@4.3.4): resolution: {integrity: sha512-/Kq8wLMz0X2gbLWGmPryqEdFV/om/GROsoLtPFqLrLCRD5CpwxXAW185BIGZKf4iYsyJim1vvcpQm5Y9hV5B1g==} + peerDependencies: astro: ^3.3.0 || ^4.0.0-beta dependencies: astro: 4.3.4(@types/node@18.16.19) @@ -6131,6 +6134,7 @@ packages: /starlight-links-validator@0.5.3(@astrojs/starlight@packages+starlight)(astro@4.3.4): resolution: {integrity: sha512-v79rwmzjQlEMVL8sZ4dalD/jhFOUvGZ2/f4CvxCySZ9KbEN9nDmgV8zJgfpmTzhbcYQ35wzyUinF4QNxgKVA4g==} + engines: {node: '>=18.14.1'} peerDependencies: '@astrojs/starlight': '>=0.15.0' astro: '>=4.0.0' From 36ddbe51029deffb34ce5fcfa16f5e65421a9e74 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Sat, 10 Feb 2024 15:14:19 +0100 Subject: [PATCH 24/37] chore: bump development and starter templates astro version to `4.3.5` --- docs/package.json | 2 +- examples/basics/package.json | 2 +- examples/tailwind/package.json | 2 +- package.json | 2 +- packages/starlight/package.json | 2 +- pnpm-lock.yaml | 57 ++++++++++++++++----------------- 6 files changed, 33 insertions(+), 34 deletions(-) diff --git a/docs/package.json b/docs/package.json index 67112e3e370..3787ccbca52 100644 --- a/docs/package.json +++ b/docs/package.json @@ -18,7 +18,7 @@ "@astrojs/starlight": "workspace:*", "@lunariajs/core": "^0.0.25", "@types/culori": "^2.0.0", - "astro": "^4.3.4", + "astro": "^4.3.5", "culori": "^3.2.0", "sharp": "^0.32.5", "virtual-pages-demo": "workspace:*" diff --git a/examples/basics/package.json b/examples/basics/package.json index cb7c2419f07..226bbf1c082 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@astrojs/starlight": "^0.18.0", - "astro": "^4.3.4", + "astro": "^4.3.5", "sharp": "^0.32.5" } } diff --git a/examples/tailwind/package.json b/examples/tailwind/package.json index c737cd57b54..b4d9c556b9a 100644 --- a/examples/tailwind/package.json +++ b/examples/tailwind/package.json @@ -14,7 +14,7 @@ "@astrojs/starlight": "^0.18.0", "@astrojs/starlight-tailwind": "^2.0.1", "@astrojs/tailwind": "^5.1.0", - "astro": "^4.3.4", + "astro": "^4.3.5", "sharp": "^0.32.5", "tailwindcss": "^3.4.1" } diff --git a/package.json b/package.json index 697b1ebd10d..e0609e2ae0e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.1", "@size-limit/file": "^8.2.4", - "astro": "^4.3.4", + "astro": "^4.3.5", "prettier": "^3.0.0", "prettier-plugin-astro": "^0.13.0", "size-limit": "^8.2.4" diff --git a/packages/starlight/package.json b/packages/starlight/package.json index 92149e9ea3b..5f8b099dde2 100644 --- a/packages/starlight/package.json +++ b/packages/starlight/package.json @@ -177,7 +177,7 @@ "@astrojs/markdown-remark": "^4.2.1", "@types/node": "^18.16.19", "@vitest/coverage-v8": "^1.2.2", - "astro": "^4.3.4", + "astro": "^4.3.5", "vitest": "^1.2.2" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5fce763070..476ffe66f04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^8.2.4 version: 8.2.4(size-limit@8.2.4) astro: - specifier: ^4.3.4 - version: 4.3.4(@types/node@18.16.19) + specifier: ^4.3.5 + version: 4.3.5(@types/node@18.16.19) prettier: specifier: ^3.0.0 version: 3.0.0 @@ -34,7 +34,7 @@ importers: dependencies: '@astro-community/astro-embed-youtube': specifier: ^0.4.4 - version: 0.4.4(astro@4.3.4) + version: 0.4.4(astro@4.3.5) '@astrojs/starlight': specifier: workspace:* version: link:../packages/starlight @@ -45,8 +45,8 @@ importers: specifier: ^2.0.0 version: 2.0.0 astro: - specifier: ^4.3.4 - version: 4.3.4(@types/node@18.16.19) + specifier: ^4.3.5 + version: 4.3.5(@types/node@18.16.19) culori: specifier: ^3.2.0 version: 3.2.0 @@ -77,7 +77,7 @@ importers: version: 13.0.1 starlight-links-validator: specifier: ^0.5.3 - version: 0.5.3(@astrojs/starlight@packages+starlight)(astro@4.3.4) + version: 0.5.3(@astrojs/starlight@packages+starlight)(astro@4.3.5) start-server-and-test: specifier: ^2.0.0 version: 2.0.0 @@ -91,8 +91,8 @@ importers: specifier: ^0.18.0 version: link:../../packages/starlight astro: - specifier: ^4.3.4 - version: 4.3.4(@types/node@18.16.19) + specifier: ^4.3.5 + version: 4.3.5(@types/node@18.16.19) sharp: specifier: ^0.32.5 version: 0.32.6 @@ -107,10 +107,10 @@ importers: version: link:../../packages/tailwind '@astrojs/tailwind': specifier: ^5.1.0 - version: 5.1.0(astro@4.3.4)(tailwindcss@3.4.1) + version: 5.1.0(astro@4.3.5)(tailwindcss@3.4.1) astro: - specifier: ^4.3.4 - version: 4.3.4(@types/node@18.16.19) + specifier: ^4.3.5 + version: 4.3.5(@types/node@18.16.19) sharp: specifier: ^0.32.5 version: 0.32.6 @@ -134,7 +134,7 @@ importers: dependencies: '@astrojs/mdx': specifier: ^2.1.1 - version: 2.1.1(astro@4.3.4) + version: 2.1.1(astro@4.3.5) '@astrojs/sitemap': specifier: ^3.0.5 version: 3.0.5 @@ -149,7 +149,7 @@ importers: version: 4.0.3 astro-expressive-code: specifier: ^0.32.4 - version: 0.32.4(astro@4.3.4) + version: 0.32.4(astro@4.3.5) bcp-47: specifier: ^2.1.0 version: 2.1.0 @@ -194,8 +194,8 @@ importers: specifier: ^1.2.2 version: 1.2.2(vitest@1.2.2) astro: - specifier: ^4.3.4 - version: 4.3.4(@types/node@18.16.19) + specifier: ^4.3.5 + version: 4.3.5(@types/node@18.16.19) vitest: specifier: ^1.2.2 version: 1.2.2(@types/node@18.16.19) @@ -207,7 +207,7 @@ importers: version: link:../starlight '@astrojs/tailwind': specifier: ^5.0.0 - version: 5.1.0(astro@4.3.4)(tailwindcss@3.4.1) + version: 5.1.0(astro@4.3.5)(tailwindcss@3.4.1) tailwindcss: specifier: ^3.3.3 version: 3.4.1 @@ -376,12 +376,12 @@ packages: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.19 - /@astro-community/astro-embed-youtube@0.4.4(astro@4.3.4): + /@astro-community/astro-embed-youtube@0.4.4(astro@4.3.5): resolution: {integrity: sha512-fYlycLrJFNnibZ9VHPSJO766kO2IgqYQU4mBd4iaDMaicL0gGX9cVZ80QdnpzGrI6w0XOJOY7prx86eWEVBy8w==} peerDependencies: astro: ^2.0.0 || ^3.0.0-beta || ^4.0.0-beta dependencies: - astro: 4.3.4(@types/node@18.16.19) + astro: 4.3.5(@types/node@18.16.19) lite-youtube-embed: 0.2.0 dev: false @@ -414,9 +414,8 @@ packages: vfile: 6.0.1 transitivePeerDependencies: - supports-color - dev: true - /@astrojs/mdx@2.1.1(astro@4.3.4): + /@astrojs/mdx@2.1.1(astro@4.3.5): resolution: {integrity: sha512-AgGFdE7HOGmoFooGvMSatkA9FiSKwyVW7ImHot/bXJ6uAbFfu6iG2ht18Cf1pT22Hda/6iSCGWusFvBv0/EnKQ==} engines: {node: '>=18.14.1'} peerDependencies: @@ -425,7 +424,7 @@ packages: '@astrojs/markdown-remark': 4.2.1 '@mdx-js/mdx': 3.0.0 acorn: 8.11.3 - astro: 4.3.4(@types/node@18.16.19) + astro: 4.3.5(@types/node@18.16.19) es-module-lexer: 1.4.1 estree-util-visit: 2.0.0 github-slugger: 2.0.0 @@ -455,13 +454,13 @@ packages: zod: 3.22.4 dev: false - /@astrojs/tailwind@5.1.0(astro@4.3.4)(tailwindcss@3.4.1): + /@astrojs/tailwind@5.1.0(astro@4.3.5)(tailwindcss@3.4.1): resolution: {integrity: sha512-BJoCDKuWhU9FT2qYg+fr6Nfb3qP4ShtyjXGHKA/4mHN94z7BGcmauQK23iy+YH5qWvTnhqkd6mQPQ1yTZTe9Ig==} peerDependencies: astro: ^3.0.0 || ^4.0.0 tailwindcss: ^3.0.24 dependencies: - astro: 4.3.4(@types/node@18.16.19) + astro: 4.3.5(@types/node@18.16.19) autoprefixer: 10.4.15(postcss@8.4.33) postcss: 8.4.33 postcss-load-config: 4.0.2(postcss@8.4.33) @@ -1880,18 +1879,18 @@ packages: hasBin: true dev: false - /astro-expressive-code@0.32.4(astro@4.3.4): + /astro-expressive-code@0.32.4(astro@4.3.5): resolution: {integrity: sha512-/Kq8wLMz0X2gbLWGmPryqEdFV/om/GROsoLtPFqLrLCRD5CpwxXAW185BIGZKf4iYsyJim1vvcpQm5Y9hV5B1g==} peerDependencies: astro: ^3.3.0 || ^4.0.0-beta dependencies: - astro: 4.3.4(@types/node@18.16.19) + astro: 4.3.5(@types/node@18.16.19) hast-util-to-html: 8.0.4 remark-expressive-code: 0.32.4 dev: false - /astro@4.3.4(@types/node@18.16.19): - resolution: {integrity: sha512-BWzGGn/PuwmT0DWX+yXa/jUq99e85AGh9/C5IFAKIfp22Nk88dOfX89RoqKBWPPp2BrK2vsdCFd0WUv8XJh80w==} + /astro@4.3.5(@types/node@18.16.19): + resolution: {integrity: sha512-7jPffNlcmDO94NlkWe/hUWta/pIjlx1LVD/DZb/fyjT1Jv+7mGhKZBIjkDfeVpequW70mep8cAS5RM7Pxa0Gdg==} engines: {node: '>=18.14.1', npm: '>=6.14.0'} hasBin: true dependencies: @@ -6132,7 +6131,7 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true - /starlight-links-validator@0.5.3(@astrojs/starlight@packages+starlight)(astro@4.3.4): + /starlight-links-validator@0.5.3(@astrojs/starlight@packages+starlight)(astro@4.3.5): resolution: {integrity: sha512-v79rwmzjQlEMVL8sZ4dalD/jhFOUvGZ2/f4CvxCySZ9KbEN9nDmgV8zJgfpmTzhbcYQ35wzyUinF4QNxgKVA4g==} engines: {node: '>=18.14.1'} peerDependencies: @@ -6140,7 +6139,7 @@ packages: astro: '>=4.0.0' dependencies: '@astrojs/starlight': link:packages/starlight - astro: 4.3.4(@types/node@18.16.19) + astro: 4.3.5(@types/node@18.16.19) github-slugger: 2.0.0 hast-util-from-html: 2.0.1 hast-util-has-property: 3.0.0 From 3ff92d741411fedc805c31f75c5945018cc0fa3b Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Sat, 10 Feb 2024 16:01:03 +0100 Subject: [PATCH 25/37] chore: rename virtual pages to Starlight pages --- docs/src/content/docs/reference/plugins.md | 22 ++-- ... starlight-page-route-data-extend.test.ts} | 17 +-- ...t.ts => starlight-page-route-data.test.ts} | 107 +++++++++--------- .../starlight/components/StarlightPage.astro | 13 +++ .../starlight/components/VirtualPage.astro | 10 -- packages/starlight/package.json | 6 +- .../{virtual-page.ts => starlight-page.ts} | 66 +++++------ packages/virtual-pages-demo/Route.astro | 16 +-- 8 files changed, 133 insertions(+), 124 deletions(-) rename packages/starlight/__tests__/basics/{virtual-route-data-extend.test.ts => starlight-page-route-data-extend.test.ts} (80%) rename packages/starlight/__tests__/basics/{virtual-route-data.test.ts => starlight-page-route-data.test.ts} (75%) create mode 100644 packages/starlight/components/StarlightPage.astro delete mode 100644 packages/starlight/components/VirtualPage.astro rename packages/starlight/utils/{virtual-page.ts => starlight-page.ts} (69%) diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index 222c5ac8096..150e3c0a052 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -166,18 +166,18 @@ The example above will log a message that includes the provided info message: Plugins [adding](#addintegration) an Astro integration can inject routes using the Integrations API [`injectRoute`](https://docs.astro.build/en/reference/integrations-reference/#injectroute-option) function to render dynamically generated content. By default, pages rendered on custom routes do not use the Starlight layout. -To do so, pages must wrap their content with the `` component. +To do so, pages must wrap their content with the `` component. -### VirtualPage +### `StarlightPage` -The `` component can be used to render a page on a custom route using the Starlight layout. +The `` component can be used to render a page on a custom route using the Starlight layout. ```astro --- // plugin/src/Example.astro -import VirtualPage, { - type VirtualPageProps, -} from '@astrojs/starlight/components/VirtualPage.astro'; +import StarlightPage, { + type StarlightPageProps, +} from '@astrojs/starlight/components/StarlightPage.astro'; import CustomComponent from './CustomComponent.astro'; const props = { @@ -185,13 +185,13 @@ const props = { frontmatter: { title: 'My custom page', }, -} satisfies VirtualPageProps; +} satisfies StarlightPageProps; --- - +

This is a custom page with a custom component:

-
+
``` #### Props @@ -204,11 +204,11 @@ The slug of the page. ##### `frontmatter` (required) -**type:** `StarlightVirtualFrontmatter` +**type:** `StarlightPageFrontmatter` Similar to the [frontmatter properties](/reference/frontmatter/) with the [`title`](/reference/frontmatter/#title-required) property being required and all other properties being optional and the following differences: -- The [`sidebar`](/reference/frontmatter/#sidebar) property is not supported as this option controls how a page is displayed in the sidebar, when using an [autogenerated link group](/reference/configuration/#sidebar) which is not applicable to virtual pages. +- The [`sidebar`](/reference/frontmatter/#sidebar) property is not supported as this option controls how a page is displayed in the sidebar, when using an [autogenerated link group](/reference/configuration/#sidebar) which is not applicable to pages using the `` component. - The [`editUrl`](/reference/frontmatter/#editurl) option requires an URL to display an edit link. ##### `sidebar` diff --git a/packages/starlight/__tests__/basics/virtual-route-data-extend.test.ts b/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts similarity index 80% rename from packages/starlight/__tests__/basics/virtual-route-data-extend.test.ts rename to packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts index 4e759e54dbb..6bad408b105 100644 --- a/packages/starlight/__tests__/basics/virtual-route-data-extend.test.ts +++ b/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts @@ -1,6 +1,9 @@ import { z } from 'astro:content'; import { assert, expect, test, vi } from 'vitest'; -import { generateVirtualRouteData, type VirtualPageProps } from '../../utils/virtual-page'; +import { + generateStarlightPageRouteData, + type StarlightPageProps, +} from '../../utils/starlight-page'; vi.mock('virtual:starlight/collection-config', async () => { const { z } = await vi.importActual('astro:content'); @@ -14,7 +17,7 @@ vi.mock('virtual:starlight/collection-config', async () => { }); }); -const virtualPageProps: VirtualPageProps = { +const starlightPageProps: StarlightPageProps = { slug: 'test-slug', frontmatter: { title: 'This is a test title' }, }; @@ -23,8 +26,8 @@ test('throws a validation error if a built-in field required by the user schema expect.assertions(3); try { - await generateVirtualRouteData({ - props: virtualPageProps, + await generateStarlightPageRouteData({ + props: starlightPageProps, url: new URL('https://example.com'), }); } catch (error) { @@ -37,11 +40,11 @@ test('throws a validation error if a built-in field required by the user schema test('returns new field defined in the user schema', async () => { const category = 'test category'; - const data = await generateVirtualRouteData({ + const data = await generateStarlightPageRouteData({ props: { - ...virtualPageProps, + ...starlightPageProps, frontmatter: { - ...virtualPageProps.frontmatter, + ...starlightPageProps.frontmatter, description: 'test description', // @ts-expect-error - Custom field defined in the user schema. category, diff --git a/packages/starlight/__tests__/basics/virtual-route-data.test.ts b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts similarity index 75% rename from packages/starlight/__tests__/basics/virtual-route-data.test.ts rename to packages/starlight/__tests__/basics/starlight-page-route-data.test.ts index 13d36387d46..2f5c033a24f 100644 --- a/packages/starlight/__tests__/basics/virtual-route-data.test.ts +++ b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts @@ -1,5 +1,8 @@ import { expect, test, vi } from 'vitest'; -import { generateVirtualRouteData, type VirtualPageProps } from '../../utils/virtual-page'; +import { + generateStarlightPageRouteData, + type StarlightPageProps, +} from '../../utils/starlight-page'; vi.mock('virtual:starlight/collection-config', async () => (await import('../test-utils')).mockedCollectionConfig() @@ -14,34 +17,34 @@ vi.mock('astro:content', async () => }) ); -const virtualPageProps: VirtualPageProps = { +const starlightPageProps: StarlightPageProps = { slug: 'test-slug', frontmatter: { title: 'This is a test title' }, }; test('adds data to route shape', async () => { - const data = await generateVirtualRouteData({ - props: virtualPageProps, + const data = await generateStarlightPageRouteData({ + props: starlightPageProps, url: new URL('https://example.com'), }); - // Virtual pages respect the slug passed in props. - expect(data.slug).toBe(virtualPageProps.slug); - // Virtual pages generate an ID based on their slug. + // Starlight pages respect the slug passed in props. + expect(data.slug).toBe(starlightPageProps.slug); + // Starlight pages generate an ID based on their slug. expect(data.id).toBeDefined(); - // Virtual pages cannot be fallbacks. + // Starlight pages cannot be fallbacks. expect(data.isFallback).toBeUndefined(); - // Virtual pages are not editable if no edit URL is passed. + // Starlight pages are not editable if no edit URL is passed. expect(data.editUrl).toBeUndefined(); expect(data.entry.data.editUrl).toBe(false); - // Virtual pages are part of the docs collection. + // Starlight pages are part of the docs collection. expect(data.entry.collection).toBe('docs'); - // Virtual pages get virtual frontmatter defaults. + // Starlight pages get dedicated frontmatter defaults. expect(data.entry.data.head).toEqual([]); expect(data.entry.data.pagefind).toBe(true); expect(data.entry.data.template).toBe('doc'); - // Virtual pages respect the passed data. - expect(data.entry.data.title).toBe(virtualPageProps.frontmatter.title); - // Virtual pages get expected defaults. + // Starlight pages respect the passed data. + expect(data.entry.data.title).toBe(starlightPageProps.frontmatter.title); + // Starlight pages get expected defaults. expect(data.hasSidebar).toBe(true); expect(data.headings).toEqual([]); expect(data.entryMeta.dir).toBe('ltr'); @@ -49,30 +52,30 @@ test('adds data to route shape', async () => { }); test('adds custom data to route shape', async () => { - const props: VirtualPageProps = { - ...virtualPageProps, + const props: StarlightPageProps = { + ...starlightPageProps, hasSidebar: false, dir: 'rtl', lang: 'ks', }; - const data = await generateVirtualRouteData({ props, url: new URL('https://example.com') }); + const data = await generateStarlightPageRouteData({ props, url: new URL('https://example.com') }); expect(data.hasSidebar).toBe(props.hasSidebar); expect(data.entryMeta.dir).toBe(props.dir); expect(data.entryMeta.lang).toBe(props.lang); }); -test('adds custom virtual frontmatter data to route shape', async () => { - const props: VirtualPageProps = { - ...virtualPageProps, +test('adds custom frontmatter data to route shape', async () => { + const props: StarlightPageProps = { + ...starlightPageProps, frontmatter: { - ...virtualPageProps.frontmatter, + ...starlightPageProps.frontmatter, head: [{ tag: 'meta', attrs: { name: 'og:test', content: 'test' } }], lastUpdated: new Date(), pagefind: false, template: 'splash', }, }; - const data = await generateVirtualRouteData({ props, url: new URL('https://example.com') }); + const data = await generateStarlightPageRouteData({ props, url: new URL('https://example.com') }); expect(data.entry.data.head).toMatchInlineSnapshot(` [ { @@ -91,8 +94,8 @@ test('adds custom virtual frontmatter data to route shape', async () => { }); test('uses generated sidebar when no sidebar is provided', async () => { - const data = await generateVirtualRouteData({ - props: virtualPageProps, + const data = await generateStarlightPageRouteData({ + props: starlightPageProps, url: new URL('https://example.com'), }); expect(data.sidebar.map((entry) => entry.label)).toMatchInlineSnapshot(` @@ -104,9 +107,9 @@ test('uses generated sidebar when no sidebar is provided', async () => { }); test('uses provided sidebar if any', async () => { - const data = await generateVirtualRouteData({ + const data = await generateStarlightPageRouteData({ props: { - ...virtualPageProps, + ...starlightPageProps, sidebar: [ { type: 'link', @@ -137,11 +140,11 @@ test('uses provided sidebar if any', async () => { }); test('uses provided pagination if any', async () => { - const data = await generateVirtualRouteData({ + const data = await generateStarlightPageRouteData({ props: { - ...virtualPageProps, + ...starlightPageProps, frontmatter: { - ...virtualPageProps.frontmatter, + ...starlightPageProps.frontmatter, prev: { label: 'Previous link', link: '/test/prev', @@ -181,17 +184,17 @@ test('uses provided headings if any', async () => { { depth: 2, slug: 'heading-1', text: 'Heading 1' }, { depth: 3, slug: 'heading-2', text: 'Heading 2' }, ]; - const data = await generateVirtualRouteData({ - props: { ...virtualPageProps, headings }, + const data = await generateStarlightPageRouteData({ + props: { ...starlightPageProps, headings }, url: new URL('https://example.com'), }); expect(data.headings).toEqual(headings); }); test('generates the table of contents for provided headings', async () => { - const data = await generateVirtualRouteData({ + const data = await generateStarlightPageRouteData({ props: { - ...virtualPageProps, + ...starlightPageProps, headings: [ { depth: 2, slug: 'heading-1', text: 'Heading 1' }, { depth: 3, slug: 'heading-2', text: 'Heading 2' }, @@ -231,9 +234,9 @@ test('generates the table of contents for provided headings', async () => { }); test('respects the `tableOfContents` level configuration', async () => { - const data = await generateVirtualRouteData({ + const data = await generateStarlightPageRouteData({ props: { - ...virtualPageProps, + ...starlightPageProps, headings: [ // Should be ignored as it's not deep enough. { depth: 2, slug: 'heading-1', text: 'Heading 1' }, @@ -241,7 +244,7 @@ test('respects the `tableOfContents` level configuration', async () => { { depth: 4, slug: 'heading-3', text: 'Heading 3' }, ], frontmatter: { - ...virtualPageProps.frontmatter, + ...starlightPageProps.frontmatter, tableOfContents: { minHeadingLevel: 3, maxHeadingLevel: 4, @@ -281,15 +284,15 @@ test('respects the `tableOfContents` level configuration', async () => { }); test('disables table of contents if frontmatter includes `tableOfContents: false`', async () => { - const data = await generateVirtualRouteData({ + const data = await generateStarlightPageRouteData({ props: { - ...virtualPageProps, + ...starlightPageProps, headings: [ { depth: 2, slug: 'heading-1', text: 'Heading 1' }, { depth: 3, slug: 'heading-2', text: 'Heading 2' }, ], frontmatter: { - ...virtualPageProps.frontmatter, + ...starlightPageProps.frontmatter, tableOfContents: false, }, }, @@ -299,15 +302,15 @@ test('disables table of contents if frontmatter includes `tableOfContents: false }); test('disables table of contents for splash template', async () => { - const data = await generateVirtualRouteData({ + const data = await generateStarlightPageRouteData({ props: { - ...virtualPageProps, + ...starlightPageProps, headings: [ { depth: 2, slug: 'heading-1', text: 'Heading 1' }, { depth: 3, slug: 'heading-2', text: 'Heading 2' }, ], frontmatter: { - ...virtualPageProps.frontmatter, + ...starlightPageProps.frontmatter, template: 'splash', }, }, @@ -317,8 +320,8 @@ test('disables table of contents for splash template', async () => { }); test('hides the sidebar if the `hasSidebar` option is not specified and the splash template is used', async () => { - const { hasSidebar, ...otherProps } = virtualPageProps; - const data = await generateVirtualRouteData({ + const { hasSidebar, ...otherProps } = starlightPageProps; + const data = await generateStarlightPageRouteData({ props: { ...otherProps, frontmatter: { @@ -332,8 +335,8 @@ test('hides the sidebar if the `hasSidebar` option is not specified and the spla }); test('includes localized labels', async () => { - const data = await generateVirtualRouteData({ - props: virtualPageProps, + const data = await generateStarlightPageRouteData({ + props: starlightPageProps, url: new URL('https://example.com'), }); expect(data.labels).toBeDefined(); @@ -342,11 +345,11 @@ test('includes localized labels', async () => { test('uses provided edit URL if any', async () => { const editUrl = 'https://example.com/edit'; - const data = await generateVirtualRouteData({ + const data = await generateStarlightPageRouteData({ props: { - ...virtualPageProps, + ...starlightPageProps, frontmatter: { - ...virtualPageProps.frontmatter, + ...starlightPageProps.frontmatter, editUrl, }, }, @@ -357,11 +360,11 @@ test('uses provided edit URL if any', async () => { }); test('strips unknown frontmatter properties', async () => { - const data = await generateVirtualRouteData({ + const data = await generateStarlightPageRouteData({ props: { - ...virtualPageProps, + ...starlightPageProps, frontmatter: { - ...virtualPageProps.frontmatter, + ...starlightPageProps.frontmatter, // @ts-expect-error - This is an unknown property. unknown: 'test', }, diff --git a/packages/starlight/components/StarlightPage.astro b/packages/starlight/components/StarlightPage.astro new file mode 100644 index 00000000000..52e5c58e3fe --- /dev/null +++ b/packages/starlight/components/StarlightPage.astro @@ -0,0 +1,13 @@ +--- +import { + generateStarlightPageRouteData, + type StarlightPageProps as Props, +} from '../utils/starlight-page'; +import Page from './Page.astro'; + +export type StarlightPageProps = Props; +--- + + + + diff --git a/packages/starlight/components/VirtualPage.astro b/packages/starlight/components/VirtualPage.astro deleted file mode 100644 index 9d20023e47b..00000000000 --- a/packages/starlight/components/VirtualPage.astro +++ /dev/null @@ -1,10 +0,0 @@ ---- -import { generateVirtualRouteData, type VirtualPageProps as Props } from '../utils/virtual-page'; -import Page from './Page.astro'; - -export type VirtualPageProps = Props; ---- - - - - diff --git a/packages/starlight/package.json b/packages/starlight/package.json index 5f8b099dde2..f3e91d9ddfd 100644 --- a/packages/starlight/package.json +++ b/packages/starlight/package.json @@ -102,9 +102,9 @@ "types": "./components/Page.astro.tsx", "import": "./components/Page.astro" }, - "./components/VirtualPage.astro": { - "types": "./components/VirtualPage.astro.tsx", - "import": "./components/VirtualPage.astro" + "./components/StarlightPage.astro": { + "types": "./components/StarlightPage.astro.tsx", + "import": "./components/StarlightPage.astro" }, "./components/Footer.astro": { "types": "./components/Footer.astro.tsx", diff --git a/packages/starlight/utils/virtual-page.ts b/packages/starlight/utils/starlight-page.ts similarity index 69% rename from packages/starlight/utils/virtual-page.ts rename to packages/starlight/utils/starlight-page.ts index 499dd32fae9..611d9b1681d 100644 --- a/packages/starlight/utils/virtual-page.ts +++ b/packages/starlight/utils/starlight-page.ts @@ -10,20 +10,20 @@ import { useTranslations } from './translations'; import { docsSchema } from '../schema'; /** - * The frontmatter schema for virtual pages derived from the default schema for Starlight’s `docs` - * content collection. - * The frontmatter schema for virtual pages cannot include some properties which will be omitted + * The frontmatter schema for Starlight pages derived from the default schema for Starlight’s + * `docs` content collection. + * The frontmatter schema for Starlight pages cannot include some properties which will be omitted * and some others needs to be refined to a stricter type. */ -const StarlightVirtualFrontmatterSchema = async (context: SchemaContext) => { +const StarlightPageFrontmatterSchema = async (context: SchemaContext) => { const userDocsSchema = await getUserDocsSchema(); const schema = typeof userDocsSchema === 'function' ? userDocsSchema(context) : userDocsSchema; return schema.transform((frontmatter) => { /** - * Virtual pages can only be edited if an edit URL is explicitly provided. + * Starlight pages can only be edited if an edit URL is explicitly provided. * The `sidebar` frontmatter prop only works for pages in an autogenerated links group. - * Virtual page links cannot be autogenerated. + * Starlight pages edit links cannot be autogenerated. * * These changes to the schema are done using a transformer and not using the usual `omit` * method because when the frontmatter schema is extended by the user, an intersection between @@ -35,89 +35,89 @@ const StarlightVirtualFrontmatterSchema = async (context: SchemaContext) => { * from the validated output but does not appply any changes to the input schema type itself so * this needs to be done manually. * - * @see StarlightVirtualFrontmatter + * @see StarlightPageFrontmatter * @see https://github.com/colinhacks/zod#intersections */ const { editUrl, sidebar, ...others } = frontmatter; - const virtualEditUrl = editUrl === undefined || editUrl === true ? false : editUrl; - return { ...others, editUrl: virtualEditUrl }; + const pageEditUrl = editUrl === undefined || editUrl === true ? false : editUrl; + return { ...others, editUrl: pageEditUrl }; }); }; /** - * Type of Starlight’s virtual frontmatter schema. + * Type of Starlight pages frontmatter schema. * We manually refines the `editUrl` type and omit the `sidebar` property as it's not possible to * do that on the schema itself using Zod but the proper validation is still using a transformer. - * @see StarlightVirtualFrontmatterSchema + * @see StarlightPageFrontmatterSchema */ -type StarlightVirtualFrontmatter = Omit< - z.input>>, +type StarlightPageFrontmatter = Omit< + z.input>>, 'editUrl' | 'sidebar' > & { editUrl?: string | false }; /** - * The props accepted by the `` component. + * The props accepted by the `` component. */ -export type VirtualPageProps = Prettify< +export type StarlightPageProps = Prettify< // Remove the index signature from `Route`, omit undesired properties and make the rest optional. Partial, 'entry' | 'entryMeta' | 'id' | 'locale' | 'slug'>> & // Add back the mandatory slug property. Pick & - // Add the sidebar definitions for a virtual page. + // Add the sidebar definitions for a Starlight page. Partial> & { - // And finally add the virtual frontmatter properties in a `frontmatter` property. - frontmatter: StarlightVirtualFrontmatter; + // And finally add the Starlight page frontmatter properties in a `frontmatter` property. + frontmatter: StarlightPageFrontmatter; } >; /** - * A docs entry used for virtual pages meant to be rendered by plugins and which is safe to cast + * A docs entry used for Starlight pages meant to be rendered by plugins and which is safe to cast * to a `StarlightDocsEntry`. - * A virtual docs entry cannot be rendered like a content collection entry. + * A Starlight page docs entry cannot be rendered like a content collection entry. */ -type VirtualDocsEntry = Omit & { +type StarlightPageDocsEntry = Omit & { /** - * The unique ID for this virtual page which cannot be inferred from codegen like content + * The unique ID for this Starlight page which cannot be inferred from codegen like content * collection entries. */ id: string; }; -export async function generateVirtualRouteData({ +export async function generateStarlightPageRouteData({ props, url, }: { - props: VirtualPageProps; + props: StarlightPageProps; url: URL; }): Promise { const { isFallback, frontmatter, slug, ...routeProps } = props; - const virtualFrontmatter = await getVirtualFrontmatter(frontmatter); + const pageFrontmatter = await getStarlightPageFrontmatter(frontmatter); const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; const localeData = slugToLocaleData(slug); const sidebar = props.sidebar ?? getSidebar(url.pathname, localeData.locale); const headings = props.headings ?? []; - const virtualEntry: VirtualDocsEntry = { + const pageDocsEntry: StarlightPageDocsEntry = { id, slug, body: '', collection: 'docs', data: { - ...virtualFrontmatter, + ...pageFrontmatter, sidebar: { attrs: {}, hidden: false, }, }, }; - const entry = virtualEntry as StarlightDocsEntry; + const entry = pageDocsEntry as StarlightDocsEntry; const entryMeta: StarlightRouteData['entryMeta'] = { dir: props.dir ?? localeData.dir, lang: props.lang ?? localeData.lang, locale: localeData.locale, }; - const editUrl = virtualFrontmatter.editUrl ? new URL(virtualFrontmatter.editUrl) : undefined; + const editUrl = pageFrontmatter.editUrl ? new URL(pageFrontmatter.editUrl) : undefined; const lastUpdated = - virtualFrontmatter.lastUpdated instanceof Date ? virtualFrontmatter.lastUpdated : undefined; + pageFrontmatter.lastUpdated instanceof Date ? pageFrontmatter.lastUpdated : undefined; const routeData: StarlightRouteData = { ...routeProps, ...localeData, @@ -149,11 +149,11 @@ export async function generateVirtualRouteData({ return routeData; } -/** Validates the virtual frontmatter properties from the props received by a virtual page. */ -async function getVirtualFrontmatter(frontmatter: StarlightVirtualFrontmatter) { +/** Validates the Starlight page frontmatter properties from the props received by a Starlight page. */ +async function getStarlightPageFrontmatter(frontmatter: StarlightPageFrontmatter) { // This needs to be in sync with ImageMetadata. // https://github.com/withastro/astro/blob/cf993bc263b58502096f00d383266cd179f331af/packages/astro/src/assets/types.ts#L32 - const schema = await StarlightVirtualFrontmatterSchema({ + const schema = await StarlightPageFrontmatterSchema({ image: () => z.object({ src: z.string(), diff --git a/packages/virtual-pages-demo/Route.astro b/packages/virtual-pages-demo/Route.astro index ac90892e084..144d2d010e4 100644 --- a/packages/virtual-pages-demo/Route.astro +++ b/packages/virtual-pages-demo/Route.astro @@ -1,8 +1,8 @@ --- import type { InferGetStaticPropsType } from 'astro'; -import VirtualPage, { - type VirtualPageProps, -} from '@astrojs/starlight/components/VirtualPage.astro'; +import StarlightPage, { + type StarlightPageProps, +} from '@astrojs/starlight/components/StarlightPage.astro'; import { getThings } from './things'; import heroStar from './hero-star.webp'; @@ -33,8 +33,8 @@ type Props = InferGetStaticPropsType; const { slug, thing } = Astro.props; -// Generates the props for the virtual page -function getVirtualPageProps(): VirtualPageProps { +// Generates the props for the Starlight page +function getStarlightPageProps(): StarlightPageProps { const isThingB = thing === 'b'; const isThingC = thing === 'c'; const isThingD = thing === 'd'; @@ -66,7 +66,7 @@ function getVirtualPageProps(): VirtualPageProps { } // Other pages with some other props modified for testing. - const props: VirtualPageProps = { + const props: StarlightPageProps = { dir: isThingB ? 'rtl' : 'ltr', hasSidebar: !isThingB, lang: 'en', @@ -107,9 +107,9 @@ function getVirtualPageProps(): VirtualPageProps { } --- - +
Some content from a plugin: {thing}
-
+
From 8678fda6ccb26eb6527ee8a0da2f1450e6ecf537 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Sat, 10 Feb 2024 16:24:00 +0100 Subject: [PATCH 26/37] feat: improve Starlight page frontmatter validation errors --- .../starlight-page-route-data-extend.test.ts | 14 ++++++++++---- packages/starlight/utils/error-map.ts | 4 ++++ packages/starlight/utils/plugins.ts | 6 +----- packages/starlight/utils/starlight-page.ts | 12 +++++++++++- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts b/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts index 6bad408b105..568d7f70677 100644 --- a/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts +++ b/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts @@ -31,10 +31,16 @@ test('throws a validation error if a built-in field required by the user schema url: new URL('https://example.com'), }); } catch (error) { - assert(error instanceof z.ZodError); - expect(error.errors).toHaveLength(1); - expect(error.errors.at(0)?.path).toEqual(['description']); - expect(error.errors.at(0)?.code).toBe('invalid_type'); + assert(error instanceof Error); + const lines = error.message.split('\n'); + // The first line should be a user-friendly error message describing the exact issue and the second line should be + // the missing description field. + expect(lines).toHaveLength(2); + const [message, missingField] = lines; + expect(message).toMatchInlineSnapshot( + `"Invalid frontmatter props passed to the \`\` component."` + ); + expect(missingField).toMatchInlineSnapshot(`"**description**: Required"`); } }); diff --git a/packages/starlight/utils/error-map.ts b/packages/starlight/utils/error-map.ts index d1006d35692..fd5c3660c50 100644 --- a/packages/starlight/utils/error-map.ts +++ b/packages/starlight/utils/error-map.ts @@ -11,6 +11,10 @@ type TypeOrLiteralErrByPathEntry = { expected: unknown[]; }; +export function throwValidationError(error: z.ZodError, message: string): never { + throw new Error(`${message}\n${error.issues.map((i) => i.message).join('\n')}`); +} + export const errorMap: z.ZodErrorMap = (baseError, ctx) => { const baseErrorPath = flattenErrorPath(baseError.path); if (baseError.code === 'invalid_union') { diff --git a/packages/starlight/utils/plugins.ts b/packages/starlight/utils/plugins.ts index 40e449d57f3..bca72087a77 100644 --- a/packages/starlight/utils/plugins.ts +++ b/packages/starlight/utils/plugins.ts @@ -1,7 +1,7 @@ import type { AstroIntegration } from 'astro'; import { z } from 'astro/zod'; import { StarlightConfigSchema, type StarlightUserConfig } from '../utils/user-config'; -import { errorMap } from '../utils/error-map'; +import { errorMap, throwValidationError } from '../utils/error-map'; /** * Runs Starlight plugins in the order that they are configured after validating the user-provided @@ -82,10 +82,6 @@ export async function runPlugins( return { integrations, starlightConfig: starlightConfig.data }; } -function throwValidationError(error: z.ZodError, message: string): never { - throw new Error(`${message}\n${error.issues.map((i) => i.message).join('\n')}`); -} - // https://github.com/withastro/astro/blob/910eb00fe0b70ca80bd09520ae100e8c78b675b5/packages/astro/src/core/config/schema.ts#L113 const astroIntegrationSchema = z.object({ name: z.string(), diff --git a/packages/starlight/utils/starlight-page.ts b/packages/starlight/utils/starlight-page.ts index 611d9b1681d..464d90a2abe 100644 --- a/packages/starlight/utils/starlight-page.ts +++ b/packages/starlight/utils/starlight-page.ts @@ -1,6 +1,7 @@ import { z } from 'astro/zod'; import { type ContentConfig, type SchemaContext } from 'astro:content'; import config from 'virtual:starlight/user-config'; +import { errorMap, throwValidationError } from './error-map'; import { stripLeadingAndTrailingSlashes } from './path'; import { getToC, type PageProps, type StarlightRouteData } from './route-data'; import type { StarlightDocsEntry } from './routing'; @@ -172,7 +173,16 @@ async function getStarlightPageFrontmatter(frontmatter: StarlightPageFrontmatter }), }); - return schema.parse(frontmatter); + const pageFrontmatter = schema.safeParse(frontmatter, { errorMap }); + + if (!pageFrontmatter.success) { + throwValidationError( + pageFrontmatter.error, + 'Invalid frontmatter props passed to the `` component.' + ); + } + + return pageFrontmatter.data; } /** Returns the user docs schema and falls back to the default schema if needed. */ From e29eccdeff06ecc84436a2b08f27010ddd48ccf2 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Sat, 10 Feb 2024 16:28:36 +0100 Subject: [PATCH 27/37] chore: remove `virtual-pages-demo` --- docs/astro.config.mjs | 6 +- docs/package.json | 3 +- packages/virtual-pages-demo/Route.astro | 115 --------------------- packages/virtual-pages-demo/hero-star.webp | Bin 114506 -> 0 bytes packages/virtual-pages-demo/index.ts | 36 ------- packages/virtual-pages-demo/package.json | 23 ----- packages/virtual-pages-demo/things.ts | 4 - pnpm-lock.yaml | 9 -- 8 files changed, 2 insertions(+), 194 deletions(-) delete mode 100644 packages/virtual-pages-demo/Route.astro delete mode 100644 packages/virtual-pages-demo/hero-star.webp delete mode 100644 packages/virtual-pages-demo/index.ts delete mode 100644 packages/virtual-pages-demo/package.json delete mode 100644 packages/virtual-pages-demo/things.ts diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 0437ba95506..005921e7da0 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -1,7 +1,6 @@ import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; import starlightLinksValidator from 'starlight-links-validator'; -import { virtualPagesDemo } from 'virtual-pages-demo'; export const locales = { root: { label: 'English', lang: 'en' }, @@ -201,10 +200,7 @@ export default defineConfig({ errorOnInconsistentLocale: true, }), ] - : [ - // TODO(HiDeoo) Remove me - virtualPagesDemo(), - ], + : [], }), ], }); diff --git a/docs/package.json b/docs/package.json index 3787ccbca52..8a751b95ef7 100644 --- a/docs/package.json +++ b/docs/package.json @@ -20,8 +20,7 @@ "@types/culori": "^2.0.0", "astro": "^4.3.5", "culori": "^3.2.0", - "sharp": "^0.32.5", - "virtual-pages-demo": "workspace:*" + "sharp": "^0.32.5" }, "devDependencies": { "@types/hast": "^3.0.3", diff --git a/packages/virtual-pages-demo/Route.astro b/packages/virtual-pages-demo/Route.astro deleted file mode 100644 index 144d2d010e4..00000000000 --- a/packages/virtual-pages-demo/Route.astro +++ /dev/null @@ -1,115 +0,0 @@ ---- -import type { InferGetStaticPropsType } from 'astro'; -import StarlightPage, { - type StarlightPageProps, -} from '@astrojs/starlight/components/StarlightPage.astro'; - -import { getThings } from './things'; -import heroStar from './hero-star.webp'; - -export function getStaticPaths() { - return getThings().map((thing) => { - const slug = `virtual-pages-demo/${thing}`; - return { - params: { - // Generate the routes: - // - /virtual-pages-demo/a/ - // - /virtual-pages-demo/b/ - // - /virtual-pages-demo/c/ - // - /virtual-pages-demo/d/ - // - /virtual-pages-demo/e/ - virtualPagesDemoSlug: slug, - }, - props: { - // Pass down the generated slug and the thing to the page - slug, - thing, - }, - }; - }); -} - -type Props = InferGetStaticPropsType; - -const { slug, thing } = Astro.props; - -// Generates the props for the Starlight page -function getStarlightPageProps(): StarlightPageProps { - const isThingB = thing === 'b'; - const isThingC = thing === 'c'; - const isThingD = thing === 'd'; - const isThingE = thing === 'e'; - - const title = `Virtual Pages Demo: ${thing}`; - - if (isThingD) { - // Page with minimal set of props. - return { slug, frontmatter: { title } }; - } else if (isThingE) { - // Page with the `splash` template, a hero including an image, an edit URL and a banner. - return { - slug, - frontmatter: { - banner: { content: 'This is a banner' }, - editUrl: 'https://starlight.astro.build/', - hero: { - title: 'Hello', - tagline: 'This is a tagline', - image: { - alt: 'A star from a plugin', - file: heroStar, - }, - }, - title, - }, - }; - } - - // Other pages with some other props modified for testing. - const props: StarlightPageProps = { - dir: isThingB ? 'rtl' : 'ltr', - hasSidebar: !isThingB, - lang: 'en', - slug, - frontmatter: { - title, - tableOfContents: isThingC ? { maxHeadingLevel: 4, minHeadingLevel: 2 } : false, - }, - }; - - if (isThingC) { - props.headings = [ - { depth: 2, slug: 'thing-a', text: 'Hello 2' }, - { depth: 3, slug: 'thing-a', text: 'Hello 3' }, - { depth: 4, slug: 'thing-a', text: 'Hello 4' }, - ]; - props.sidebar = [ - { - type: 'group', - label: 'All the links', - entries: [ - { - type: 'link', - label: 'Custom link 2', - href: '/getting-started/', - isCurrent: false, - badge: { text: 'Wow', variant: 'danger' }, - attrs: {}, - }, - ], - collapsed: false, - badge: undefined, - }, - ]; - } - - return props; -} ---- - - -
- Some content from a plugin: - {thing} -
-
diff --git a/packages/virtual-pages-demo/hero-star.webp b/packages/virtual-pages-demo/hero-star.webp deleted file mode 100644 index 0d9ef67fa6614e0a71538a122703e60aac79a411..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 114506 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 { - injectRoute({ - entrypoint: 'virtual-pages-demo/route', - pattern: '[...virtualPagesDemoSlug]', - }); - }, - }, - }); - - updateConfig({ - sidebar: [ - ...(config.sidebar ?? []), - { - label: 'Virtual pages', - items: getThings().map((thing) => ({ - label: `Thing ${thing}`, - link: `virtual-pages-demo/${thing}/`, - })), - }, - ], - }); - }, - }, - }; -} diff --git a/packages/virtual-pages-demo/package.json b/packages/virtual-pages-demo/package.json deleted file mode 100644 index e9749cb50cc..00000000000 --- a/packages/virtual-pages-demo/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "virtual-pages-demo", - "version": "0.0.1", - "private": true, - "description": "Virtual pages demo", - "author": "Chris Swithinbank ", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/withastro/starlight", - "directory": "packages/virtual-pages-demo" - }, - "bugs": "https://github.com/withastro/starlight/issues", - "homepage": "https://starlight.astro.build", - "type": "module", - "exports": { - ".": "./index.ts", - "./route": "./Route.astro" - }, - "peerDependencies": { - "@astrojs/starlight": ">=0.14.0" - } -} diff --git a/packages/virtual-pages-demo/things.ts b/packages/virtual-pages-demo/things.ts deleted file mode 100644 index 476a0fa0af1..00000000000 --- a/packages/virtual-pages-demo/things.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function getThings() { - // Imagine data generated dynamically somehow - return ['a', 'b', 'c', 'd', 'e']; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 476ffe66f04..eea64a14834 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,9 +53,6 @@ importers: sharp: specifier: ^0.32.5 version: 0.32.6 - virtual-pages-demo: - specifier: workspace:* - version: link:../packages/virtual-pages-demo devDependencies: '@types/hast': specifier: ^3.0.3 @@ -222,12 +219,6 @@ importers: specifier: ^1.2.2 version: 1.2.2(@types/node@18.16.19) - packages/virtual-pages-demo: - dependencies: - '@astrojs/starlight': - specifier: '>=0.14.0' - version: link:../starlight - packages: /@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.11.0): From 9a04f7d96a750ba83317cd28bfaf876bd4476578 Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Mon, 12 Feb 2024 19:18:34 +0100 Subject: [PATCH 28/37] Chris docs: first pass --- docs/src/content/docs/getting-started.mdx | 38 +----- docs/src/content/docs/guides/pages.mdx | 149 +++++++++++++++++++++ docs/src/content/docs/reference/plugins.md | 90 ------------- 3 files changed, 151 insertions(+), 126 deletions(-) create mode 100644 docs/src/content/docs/guides/pages.mdx diff --git a/docs/src/content/docs/getting-started.mdx b/docs/src/content/docs/getting-started.mdx index d65806145a9..eb3a8f3e25b 100644 --- a/docs/src/content/docs/getting-started.mdx +++ b/docs/src/content/docs/getting-started.mdx @@ -83,43 +83,9 @@ Open this URL to start browsing your site. Starlight is ready for you to add new content, or bring your existing files! -#### File formats +Add new pages to your site by creating Markdown files in the `src/content/docs/` directory. -Starlight supports authoring content in Markdown and MDX with no configuration required. -You can add support for Markdoc by installing the experimental [Astro Markdoc integration](https://docs.astro.build/en/guides/integrations-guide/markdoc/). - -#### Add pages - -Add new pages to your site by creating `.md` or `.mdx` files in `src/content/docs/`. -Use sub-folders to organize your files and to create multiple path segments. - -For example, the following file structure will generate pages at `example.com/hello-world` and `example.com/guides/faq`: - -import FileTree from '~/components/file-tree.astro'; - - - -- src/ - - content/ - - docs/ - - guides/ - - faq.md - - hello-world.md - - - -#### Type-safe frontmatter - -All Starlight pages share a customizable [common set of frontmatter properties](/reference/frontmatter/) to control how the page appears: - -```md ---- -title: Hello, World! -description: This is a page in my Starlight-powered site ---- -``` - -If you forget anything important, Starlight will let you know. +Read more about file-based routing and support for MDX and Markdoc files in the [“Pages”](/guides/pages/) guide. ### Next steps diff --git a/docs/src/content/docs/guides/pages.mdx b/docs/src/content/docs/guides/pages.mdx new file mode 100644 index 00000000000..985498dd8bf --- /dev/null +++ b/docs/src/content/docs/guides/pages.mdx @@ -0,0 +1,149 @@ +--- +title: Pages +description: Learn how to create and manage your documentation site’s pages with Starlight. +sidebar: + order: 1 +--- + +Starlight generates your site’s HTML pages based on your content, with flexible options provided via Markdown frontmatter. +In addition, Starlight projects have full access to Astro’s powerful page generation tools. +This guide shows how page generation works in Starlight. + +## Content pages + +### File formats + +Starlight supports authoring content in Markdown and MDX with no configuration required. +You can add support for Markdoc by installing the experimental [Astro Markdoc integration](https://docs.astro.build/en/guides/integrations-guide/markdoc/). + +### Add pages + +Add new pages to your site by creating `.md` or `.mdx` files in `src/content/docs/`. +Use sub-folders to organize your files and to create multiple path segments. + +For example, the following file structure will generate pages at `example.com/hello-world` and `example.com/reference/faq`: + +import FileTree from '~/components/file-tree.astro'; + + + +- src/ + - content/ + - docs/ + - hello-world.md + - reference/ + - faq.md + + + +### Type-safe frontmatter + +All Starlight pages share a customizable [common set of frontmatter properties](/reference/frontmatter/) to control how the page appears: + +```md +--- +title: Hello, World! +description: This is a page in my Starlight-powered site +--- +``` + +If you forget anything important, Starlight will let you know. + +## Custom pages + +For advanced use cases, you can create custom pages in Astro’s `src/pages/` directory. +This is helpful if you need to build pages with a completely custom layout or generate a page from an alternative data source. +The `src/pages/` directory uses file-based routing and includes support for `.astro` files. + +Read more in the [“Pages” guide in the Astro docs](https://docs.astro.build/en/basics/astro-pages/). + +### Using Starlight’s design in custom pages + +To use the Starlight layout in custom pages, wrap your page content with the `` component. +This can be helpful if you are generating content dynamically but still want to use Starlight’s design. + +```astro +--- +// src/pages/custom-page/example.astro +import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; +import CustomComponent from './CustomComponent.astro'; +--- + + +

This is a custom page with a custom component:

+ +
+``` + +#### Props + +The `` component accepts the following props. + +##### `slug` (required) + +**type:** `string` + +The slug of the page. + +:::danger +Needs updating and moving lower in the page probably if we make slug optional. +::: + +##### `frontmatter` (required) + +**type:** `StarlightPageFrontmatter` + +Set the [frontmatter properties](/reference/frontmatter/) for this page, similar to frontmatter in Markdown pages. +The [`title`](/reference/frontmatter/#title-required) property is required and all other properties are optional. + +The following properties differ from Markdown frontmatter: + +- The [`editUrl`](/reference/frontmatter/#editurl) option requires a URL to display an edit link. +- The [`sidebar`](/reference/frontmatter/#sidebar) property is not supported. In Markdown frontmatter, this option allows customization of [autogenerated link groups](/reference/configuration/#sidebar), which is not applicable to pages using the `` component. + +##### `sidebar` + +**type:** `SidebarEntry[] | undefined` +**default:** the sidebar generated based on the [global `sidebar` config](/reference/configuration/#sidebar) + +Provide a custom site navigation sidebar for this page. +If not set, the page will use the default global sidebar. + +##### `hasSidebar` + +**type:** `boolean` +**default:** `false` if [`frontmatter.template`](/reference/frontmatter/#template) is `'splash'`, otherwise `true` + +Set if the sidebar should be displayed on this page. + +##### `headings` + +**type:** `{ depth: number; slug: string; text: string }[]` +**default:** `[]` + +Provide an array of all the headings on this page. +Starlight will generate the page table of contents from these headings if provided. + +##### `dir` + +**type:** `'ltr' | 'rtl'` +**default:** the writing direction for the current locale + +Set the writing direction for this page’s content. + +##### `lang` + +**type:** `string` +**default:** the language of the current locale + +Set the BCP-47 language tag for this page’s content, e.g. `en`, `zh-CN`, or `pt-BR`. + +##### `isFallback` + +**type:** `boolean` +**default:** `false` + +Indicate if this page is untranslated in the current language and using [fallback content](/guides/i18n/#fallback-content). diff --git a/docs/src/content/docs/reference/plugins.md b/docs/src/content/docs/reference/plugins.md index 150e3c0a052..ff37185871b 100644 --- a/docs/src/content/docs/reference/plugins.md +++ b/docs/src/content/docs/reference/plugins.md @@ -161,93 +161,3 @@ The example above will log a message that includes the provided info message: ```shell [long-process-plugin] Starting long process… ``` - -## Route injection - -Plugins [adding](#addintegration) an Astro integration can inject routes using the Integrations API [`injectRoute`](https://docs.astro.build/en/reference/integrations-reference/#injectroute-option) function to render dynamically generated content. -By default, pages rendered on custom routes do not use the Starlight layout. -To do so, pages must wrap their content with the `` component. - -### `StarlightPage` - -The `` component can be used to render a page on a custom route using the Starlight layout. - -```astro ---- -// plugin/src/Example.astro -import StarlightPage, { - type StarlightPageProps, -} from '@astrojs/starlight/components/StarlightPage.astro'; -import CustomComponent from './CustomComponent.astro'; - -const props = { - slug: 'custom-page/example', - frontmatter: { - title: 'My custom page', - }, -} satisfies StarlightPageProps; ---- - - -

This is a custom page with a custom component:

- -
-``` - -#### Props - -##### `slug` (required) - -**type:** `string` - -The slug of the page. - -##### `frontmatter` (required) - -**type:** `StarlightPageFrontmatter` - -Similar to the [frontmatter properties](/reference/frontmatter/) with the [`title`](/reference/frontmatter/#title-required) property being required and all other properties being optional and the following differences: - -- The [`sidebar`](/reference/frontmatter/#sidebar) property is not supported as this option controls how a page is displayed in the sidebar, when using an [autogenerated link group](/reference/configuration/#sidebar) which is not applicable to pages using the `` component. -- The [`editUrl`](/reference/frontmatter/#editurl) option requires an URL to display an edit link. - -##### `sidebar` - -**type:** `SidebarEntry[] | undefined` -**default:** the sidebar generated based on the [global `sidebar` config](/reference/configuration/#sidebar) - -Site navigation sidebar entries for this page or fallback to the global `sidebar` option if not provided. - -##### `hasSidebar` - -**type:** `boolean` -**default:** `false` if [`template`](/reference/frontmatter/#template) is `'splash'`, otherwise `true` - -Whether or not the sidebar should be displayed on this page. - -##### `headings` - -**type:** `{ depth: number; slug: string; text: string }[]` -**default:** `[]` - -Array of all headings of the page. - -##### `dir` - -**type:** `'ltr' | 'rtl'` -**default:** The page writing direction. - -Page content writing direction. - -##### `lang` - -**type:** `string` -**default:** The page language tag. - -BCP-47 language tag for this page’s content locale, e.g. `en`, `zh-CN`, or `pt-BR`. - -##### `isFallback` - -**type:** `boolean` - -Indicates if this page is untranslated in the current language and using [fallback content](/guides/i18n/#fallback-content). From 2d7973d98accabf8402b0d4354d41839ed033246 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Tue, 13 Feb 2024 09:15:11 +0100 Subject: [PATCH 29/37] test: remove unused import --- .../__tests__/basics/starlight-page-route-data-extend.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts b/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts index 568d7f70677..8326041fb68 100644 --- a/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts +++ b/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts @@ -1,4 +1,3 @@ -import { z } from 'astro:content'; import { assert, expect, test, vi } from 'vitest'; import { generateStarlightPageRouteData, From 9665a9d84ea0c5d9b1824016e6cb814a1cea37c6 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Tue, 13 Feb 2024 11:05:32 +0100 Subject: [PATCH 30/37] feat: infer slug from `Astro.url` --- docs/src/content/docs/guides/pages.mdx | 16 +------- .../starlight/__tests__/basics/slugs.test.ts | 32 +++++++++++++++- .../starlight-page-route-data-extend.test.ts | 5 +-- .../basics/starlight-page-route-data.test.ts | 37 ++++++++++--------- packages/starlight/utils/slugs.ts | 18 +++++++++ packages/starlight/utils/starlight-page.ts | 7 ++-- 6 files changed, 75 insertions(+), 40 deletions(-) diff --git a/docs/src/content/docs/guides/pages.mdx b/docs/src/content/docs/guides/pages.mdx index 985498dd8bf..9c94b6e1aa4 100644 --- a/docs/src/content/docs/guides/pages.mdx +++ b/docs/src/content/docs/guides/pages.mdx @@ -69,10 +69,7 @@ import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; import CustomComponent from './CustomComponent.astro'; --- - +

This is a custom page with a custom component:

@@ -82,16 +79,6 @@ import CustomComponent from './CustomComponent.astro'; The `` component accepts the following props. -##### `slug` (required) - -**type:** `string` - -The slug of the page. - -:::danger -Needs updating and moving lower in the page probably if we make slug optional. -::: - ##### `frontmatter` (required) **type:** `StarlightPageFrontmatter` @@ -101,6 +88,7 @@ The [`title`](/reference/frontmatter/#title-required) property is required and a The following properties differ from Markdown frontmatter: +- The [`slug`](/reference/frontmatter/#slug) property is not supported and is automatically set based on the custom page’s URL. - The [`editUrl`](/reference/frontmatter/#editurl) option requires a URL to display an edit link. - The [`sidebar`](/reference/frontmatter/#sidebar) property is not supported. In Markdown frontmatter, this option allows customization of [autogenerated link groups](/reference/configuration/#sidebar), which is not applicable to pages using the `` component. diff --git a/packages/starlight/__tests__/basics/slugs.test.ts b/packages/starlight/__tests__/basics/slugs.test.ts index 6c17414bee3..b350999d0d1 100644 --- a/packages/starlight/__tests__/basics/slugs.test.ts +++ b/packages/starlight/__tests__/basics/slugs.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from 'vitest'; +import { describe, expect, test, vi } from 'vitest'; import { localeToLang, localizedId, @@ -6,6 +6,7 @@ import { slugToLocaleData, slugToParam, slugToPathname, + urlToSlug, } from '../../utils/slugs'; describe('slugToLocaleData', () => { @@ -76,3 +77,32 @@ describe('localizedSlug', () => { expect(localizedSlug('test', undefined)).toBe('test'); }); }); + +describe('urlToSlug', () => { + test('returns slugs with `build.output: "directory"`', () => { + expect(urlToSlug(new URL('https://example.com/slug'))).toBe('slug'); + expect(urlToSlug(new URL('https://example.com/dir/page/'))).toBe('dir/page'); + expect(urlToSlug(new URL('https://example.com/dir/sub-dir/page/'))).toBe('dir/sub-dir/page'); + }); + + test('returns slugs with `build.output: "file"`', () => { + expect(urlToSlug(new URL('https://example.com/slug.html'))).toBe('slug'); + expect(urlToSlug(new URL('https://example.com/dir/page/index.html'))).toBe('dir/page'); + expect(urlToSlug(new URL('https://example.com/dir/sub-dir/page.html'))).toBe( + 'dir/sub-dir/page' + ); + }); + + // It is currently not possible to test this as stubbing BASE_URL is not supported due to + // `vite-plugin-env` controlling it and the lack of a way to pass in an Astro config using + // `getViteConfig()` from `astro/config`. + test.todo('returns slugs with a custom `base` option', () => { + vi.stubEnv('BASE_URL', '/base/'); + expect(urlToSlug(new URL('https://example.com/base/slug'))).toBe('slug'); + expect(urlToSlug(new URL('https://example.com/base/dir/page/'))).toBe('dir/page'); + expect(urlToSlug(new URL('https://example.com/base/dir/sub-dir/page/'))).toBe( + 'dir/sub-dir/page' + ); + vi.unstubAllEnvs(); + }); +}); diff --git a/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts b/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts index 8326041fb68..39e6095545a 100644 --- a/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts +++ b/packages/starlight/__tests__/basics/starlight-page-route-data-extend.test.ts @@ -17,7 +17,6 @@ vi.mock('virtual:starlight/collection-config', async () => { }); const starlightPageProps: StarlightPageProps = { - slug: 'test-slug', frontmatter: { title: 'This is a test title' }, }; @@ -27,7 +26,7 @@ test('throws a validation error if a built-in field required by the user schema try { await generateStarlightPageRouteData({ props: starlightPageProps, - url: new URL('https://example.com'), + url: new URL('https://example.com/test-slug'), }); } catch (error) { assert(error instanceof Error); @@ -55,7 +54,7 @@ test('returns new field defined in the user schema', async () => { category, }, }, - url: new URL('https://example.com'), + url: new URL('https://example.com/test-slug'), }); // @ts-expect-error - Custom field defined in the user schema. expect(data.entry.data.category).toBe(category); diff --git a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts index 2f5c033a24f..0484f6105a1 100644 --- a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts +++ b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts @@ -18,17 +18,18 @@ vi.mock('astro:content', async () => ); const starlightPageProps: StarlightPageProps = { - slug: 'test-slug', frontmatter: { title: 'This is a test title' }, }; +const starlightPageUrl = new URL('https://example.com/test-slug'); + test('adds data to route shape', async () => { const data = await generateStarlightPageRouteData({ props: starlightPageProps, - url: new URL('https://example.com'), + url: starlightPageUrl, }); - // Starlight pages respect the slug passed in props. - expect(data.slug).toBe(starlightPageProps.slug); + // Starlight pages infer the slug from the URL. + expect(data.slug).toBe('test-slug'); // Starlight pages generate an ID based on their slug. expect(data.id).toBeDefined(); // Starlight pages cannot be fallbacks. @@ -58,7 +59,7 @@ test('adds custom data to route shape', async () => { dir: 'rtl', lang: 'ks', }; - const data = await generateStarlightPageRouteData({ props, url: new URL('https://example.com') }); + const data = await generateStarlightPageRouteData({ props, url: starlightPageUrl }); expect(data.hasSidebar).toBe(props.hasSidebar); expect(data.entryMeta.dir).toBe(props.dir); expect(data.entryMeta.lang).toBe(props.lang); @@ -75,7 +76,7 @@ test('adds custom frontmatter data to route shape', async () => { template: 'splash', }, }; - const data = await generateStarlightPageRouteData({ props, url: new URL('https://example.com') }); + const data = await generateStarlightPageRouteData({ props, url: starlightPageUrl }); expect(data.entry.data.head).toMatchInlineSnapshot(` [ { @@ -96,7 +97,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, - url: new URL('https://example.com'), + url: starlightPageUrl, }); expect(data.sidebar.map((entry) => entry.label)).toMatchInlineSnapshot(` [ @@ -129,7 +130,7 @@ test('uses provided sidebar if any', async () => { }, ], }, - url: new URL('https://example.com'), + url: starlightPageUrl, }); expect(data.sidebar.map((entry) => entry.label)).toMatchInlineSnapshot(` [ @@ -155,7 +156,7 @@ test('uses provided pagination if any', async () => { }, }, }, - url: new URL('https://example.com'), + url: starlightPageUrl, }); expect(data.pagination).toMatchInlineSnapshot(` { @@ -186,7 +187,7 @@ test('uses provided headings if any', async () => { ]; const data = await generateStarlightPageRouteData({ props: { ...starlightPageProps, headings }, - url: new URL('https://example.com'), + url: starlightPageUrl, }); expect(data.headings).toEqual(headings); }); @@ -202,7 +203,7 @@ test('generates the table of contents for provided headings', async () => { { depth: 4, slug: 'heading-3', text: 'Heading 3' }, ], }, - url: new URL('https://example.com'), + url: starlightPageUrl, }); expect(data.toc).toMatchInlineSnapshot(` { @@ -251,7 +252,7 @@ test('respects the `tableOfContents` level configuration', async () => { }, }, }, - url: new URL('https://example.com'), + url: starlightPageUrl, }); expect(data.toc).toMatchInlineSnapshot(` { @@ -296,7 +297,7 @@ test('disables table of contents if frontmatter includes `tableOfContents: false tableOfContents: false, }, }, - url: new URL('https://example.com'), + url: starlightPageUrl, }); expect(data.toc).toBeUndefined(); }); @@ -314,7 +315,7 @@ test('disables table of contents for splash template', async () => { template: 'splash', }, }, - url: new URL('https://example.com'), + url: starlightPageUrl, }); expect(data.toc).toBeUndefined(); }); @@ -329,7 +330,7 @@ test('hides the sidebar if the `hasSidebar` option is not specified and the spla template: 'splash', }, }, - url: new URL('https://example.com'), + url: starlightPageUrl, }); expect(data.hasSidebar).toBe(false); }); @@ -337,7 +338,7 @@ test('hides the sidebar if the `hasSidebar` option is not specified and the spla test('includes localized labels', async () => { const data = await generateStarlightPageRouteData({ props: starlightPageProps, - url: new URL('https://example.com'), + url: starlightPageUrl, }); expect(data.labels).toBeDefined(); expect(data.labels['skipLink.label']).toBe('Skip to content'); @@ -353,7 +354,7 @@ test('uses provided edit URL if any', async () => { editUrl, }, }, - url: new URL('https://example.com'), + url: starlightPageUrl, }); expect(data.editUrl).toEqual(new URL(editUrl)); expect(data.entry.data.editUrl).toEqual(editUrl); @@ -369,7 +370,7 @@ test('strips unknown frontmatter properties', async () => { unknown: 'test', }, }, - url: new URL('https://example.com'), + url: starlightPageUrl, }); expect('unknown' in data.entry.data).toBe(false); }); diff --git a/packages/starlight/utils/slugs.ts b/packages/starlight/utils/slugs.ts index 2b45fd8d741..b7e076c9071 100644 --- a/packages/starlight/utils/slugs.ts +++ b/packages/starlight/utils/slugs.ts @@ -101,3 +101,21 @@ export function localizedId(id: string, locale: string | undefined): string { return id; } } + +/** Extract the slug from a URL. */ +export function urlToSlug(url: URL): string { + let pathname = url.pathname; + const base = import.meta.env.BASE_URL.replace(/\/$/, ''); + if (pathname.startsWith(base)) pathname = pathname.replace(base, ''); + const segments = pathname.split('/'); + const htmlExt = '.html'; + if (segments.at(-1) === 'index.html') { + // Remove trailing `index.html`. + segments.pop(); + } else if (segments.at(-1)?.endsWith(htmlExt)) { + // Remove trailing `.html`. + const last = segments.pop(); + if (last) segments.push(last.slice(0, -1 * htmlExt.length)); + } + return segments.filter(Boolean).join('/'); +} diff --git a/packages/starlight/utils/starlight-page.ts b/packages/starlight/utils/starlight-page.ts index 464d90a2abe..c08ce4fa679 100644 --- a/packages/starlight/utils/starlight-page.ts +++ b/packages/starlight/utils/starlight-page.ts @@ -5,7 +5,7 @@ import { errorMap, throwValidationError } from './error-map'; import { stripLeadingAndTrailingSlashes } from './path'; import { getToC, type PageProps, type StarlightRouteData } from './route-data'; import type { StarlightDocsEntry } from './routing'; -import { slugToLocaleData } from './slugs'; +import { slugToLocaleData, urlToSlug } from './slugs'; import { getPrevNextLinks, getSidebar } from './navigation'; import { useTranslations } from './translations'; import { docsSchema } from '../schema'; @@ -62,8 +62,6 @@ type StarlightPageFrontmatter = Omit< export type StarlightPageProps = Prettify< // Remove the index signature from `Route`, omit undesired properties and make the rest optional. Partial, 'entry' | 'entryMeta' | 'id' | 'locale' | 'slug'>> & - // Add back the mandatory slug property. - Pick & // Add the sidebar definitions for a Starlight page. Partial> & { // And finally add the Starlight page frontmatter properties in a `frontmatter` property. @@ -91,7 +89,8 @@ export async function generateStarlightPageRouteData({ props: StarlightPageProps; url: URL; }): Promise { - const { isFallback, frontmatter, slug, ...routeProps } = props; + const { isFallback, frontmatter, ...routeProps } = props; + const slug = urlToSlug(url); const pageFrontmatter = await getStarlightPageFrontmatter(frontmatter); const id = `${stripLeadingAndTrailingSlashes(slug)}.md`; const localeData = slugToLocaleData(slug); From a7d66fcf2817f560d87077535c3fe03f937491fe Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Wed, 14 Feb 2024 10:03:46 +0100 Subject: [PATCH 31/37] test: add `urlToSlug` root url tests --- packages/starlight/__tests__/basics/slugs.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/starlight/__tests__/basics/slugs.test.ts b/packages/starlight/__tests__/basics/slugs.test.ts index b350999d0d1..6b7c9bd16b0 100644 --- a/packages/starlight/__tests__/basics/slugs.test.ts +++ b/packages/starlight/__tests__/basics/slugs.test.ts @@ -80,12 +80,14 @@ describe('localizedSlug', () => { describe('urlToSlug', () => { test('returns slugs with `build.output: "directory"`', () => { + expect(urlToSlug(new URL('https://example.com'))).toBe(''); expect(urlToSlug(new URL('https://example.com/slug'))).toBe('slug'); expect(urlToSlug(new URL('https://example.com/dir/page/'))).toBe('dir/page'); expect(urlToSlug(new URL('https://example.com/dir/sub-dir/page/'))).toBe('dir/sub-dir/page'); }); test('returns slugs with `build.output: "file"`', () => { + expect(urlToSlug(new URL('https://example.com/index.html'))).toBe(''); expect(urlToSlug(new URL('https://example.com/slug.html'))).toBe('slug'); expect(urlToSlug(new URL('https://example.com/dir/page/index.html'))).toBe('dir/page'); expect(urlToSlug(new URL('https://example.com/dir/sub-dir/page.html'))).toBe( @@ -98,6 +100,7 @@ describe('urlToSlug', () => { // `getViteConfig()` from `astro/config`. test.todo('returns slugs with a custom `base` option', () => { vi.stubEnv('BASE_URL', '/base/'); + expect(urlToSlug(new URL('https://example.com/base'))).toBe(''); expect(urlToSlug(new URL('https://example.com/base/slug'))).toBe('slug'); expect(urlToSlug(new URL('https://example.com/base/dir/page/'))).toBe('dir/page'); expect(urlToSlug(new URL('https://example.com/base/dir/sub-dir/page/'))).toBe( From 8e0c9fa2b999ce300cf5860a039d843c63b4e9a4 Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Fri, 16 Feb 2024 13:07:58 +0100 Subject: [PATCH 32/37] Add docs link Co-authored-by: Sarah Rainsberger --- docs/src/content/docs/guides/pages.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/guides/pages.mdx b/docs/src/content/docs/guides/pages.mdx index 9c94b6e1aa4..db45f0691bc 100644 --- a/docs/src/content/docs/guides/pages.mdx +++ b/docs/src/content/docs/guides/pages.mdx @@ -53,7 +53,7 @@ If you forget anything important, Starlight will let you know. For advanced use cases, you can create custom pages in Astro’s `src/pages/` directory. This is helpful if you need to build pages with a completely custom layout or generate a page from an alternative data source. -The `src/pages/` directory uses file-based routing and includes support for `.astro` files. +The `src/pages/` directory uses [Astro's file-based routing](https://docs.astro.build/en/basics/astro-pages/#file-based-routing) and includes support for `.astro` files. Read more in the [“Pages” guide in the Astro docs](https://docs.astro.build/en/basics/astro-pages/). From 4bd9ee309597a37cd706c5447dfd6e0ad97959cb Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Fri, 16 Feb 2024 13:08:42 +0100 Subject: [PATCH 33/37] Fallback improvement Co-authored-by: Sarah Rainsberger --- docs/src/content/docs/guides/pages.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/guides/pages.mdx b/docs/src/content/docs/guides/pages.mdx index db45f0691bc..4b21394fb84 100644 --- a/docs/src/content/docs/guides/pages.mdx +++ b/docs/src/content/docs/guides/pages.mdx @@ -134,4 +134,4 @@ Set the BCP-47 language tag for this page’s content, e.g. `en`, `zh-CN`, or `p **type:** `boolean` **default:** `false` -Indicate if this page is untranslated in the current language and using [fallback content](/guides/i18n/#fallback-content). +Indicate if this page is using [fallback content](/guides/i18n/#fallback-content) because there is no translation for the current language. From 0741800a5c3dc8b7d99781c953fb4a1b17d3744b Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Fri, 16 Feb 2024 15:13:21 +0100 Subject: [PATCH 34/37] Nice whether today --- docs/src/content/docs/guides/pages.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/guides/pages.mdx b/docs/src/content/docs/guides/pages.mdx index 4b21394fb84..e1672396540 100644 --- a/docs/src/content/docs/guides/pages.mdx +++ b/docs/src/content/docs/guides/pages.mdx @@ -105,7 +105,7 @@ If not set, the page will use the default global sidebar. **type:** `boolean` **default:** `false` if [`frontmatter.template`](/reference/frontmatter/#template) is `'splash'`, otherwise `true` -Set if the sidebar should be displayed on this page. +Control whether or not the sidebar should be displayed on this page. ##### `headings` From 02ca9420df58e6cf8b019ac0c46a49956ce82cb1 Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Fri, 16 Feb 2024 23:12:29 +0100 Subject: [PATCH 35/37] Improve custom pages section based on feedback --- docs/src/content/docs/guides/pages.mdx | 18 ++++++++++++++++-- docs/src/pages/demo.astro | 26 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 docs/src/pages/demo.astro diff --git a/docs/src/content/docs/guides/pages.mdx b/docs/src/content/docs/guides/pages.mdx index e1672396540..3c659577bfd 100644 --- a/docs/src/content/docs/guides/pages.mdx +++ b/docs/src/content/docs/guides/pages.mdx @@ -51,9 +51,23 @@ If you forget anything important, Starlight will let you know. ## Custom pages -For advanced use cases, you can create custom pages in Astro’s `src/pages/` directory. +For advanced use cases, you can add custom pages by creating a `src/pages/` directory. +The `src/pages/` directory uses [Astro's file-based routing](https://docs.astro.build/en/basics/astro-pages/#file-based-routing) and includes support for `.astro` files amongst other page formats. This is helpful if you need to build pages with a completely custom layout or generate a page from an alternative data source. -The `src/pages/` directory uses [Astro's file-based routing](https://docs.astro.build/en/basics/astro-pages/#file-based-routing) and includes support for `.astro` files. + +For example, this project mixes Markdown content in `src/content/docs/` with Astro and HTML routes in `src/pages/`: + + + +- src/ + - content/ + - docs/ + - hello-world.md + - pages/ + - custom.astro + - archived.html + + Read more in the [“Pages” guide in the Astro docs](https://docs.astro.build/en/basics/astro-pages/). diff --git a/docs/src/pages/demo.astro b/docs/src/pages/demo.astro new file mode 100644 index 00000000000..85560e71b52 --- /dev/null +++ b/docs/src/pages/demo.astro @@ -0,0 +1,26 @@ +--- +import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; +--- + + From 802acb0b25fc9347ba14878202b54024de83f335 Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Fri, 16 Feb 2024 23:14:06 +0100 Subject: [PATCH 36/37] Add link Co-authored-by: Sarah Rainsberger --- docs/src/content/docs/guides/pages.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/content/docs/guides/pages.mdx b/docs/src/content/docs/guides/pages.mdx index 3c659577bfd..33ab5d29e5d 100644 --- a/docs/src/content/docs/guides/pages.mdx +++ b/docs/src/content/docs/guides/pages.mdx @@ -6,7 +6,7 @@ sidebar: --- Starlight generates your site’s HTML pages based on your content, with flexible options provided via Markdown frontmatter. -In addition, Starlight projects have full access to Astro’s powerful page generation tools. +In addition, Starlight projects have full access to [Astro’s powerful page generation tools](https://docs.astro.build/en/basics/astro-pages/). This guide shows how page generation works in Starlight. ## Content pages From ba4c60d9edff24e4f24b9ebef4ef5016fd607299 Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Fri, 16 Feb 2024 23:16:18 +0100 Subject: [PATCH 37/37] Comment out `sidebar` docs for first release --- docs/src/content/docs/guides/pages.mdx | 10 +++++----- docs/src/pages/demo.astro | 26 -------------------------- 2 files changed, 5 insertions(+), 31 deletions(-) delete mode 100644 docs/src/pages/demo.astro diff --git a/docs/src/content/docs/guides/pages.mdx b/docs/src/content/docs/guides/pages.mdx index 33ab5d29e5d..0daeef16c5b 100644 --- a/docs/src/content/docs/guides/pages.mdx +++ b/docs/src/content/docs/guides/pages.mdx @@ -106,13 +106,13 @@ The following properties differ from Markdown frontmatter: - The [`editUrl`](/reference/frontmatter/#editurl) option requires a URL to display an edit link. - The [`sidebar`](/reference/frontmatter/#sidebar) property is not supported. In Markdown frontmatter, this option allows customization of [autogenerated link groups](/reference/configuration/#sidebar), which is not applicable to pages using the `` component. -##### `sidebar` +{/* ##### `sidebar` */} -**type:** `SidebarEntry[] | undefined` -**default:** the sidebar generated based on the [global `sidebar` config](/reference/configuration/#sidebar) +{/* **type:** `SidebarEntry[] | undefined` */} +{/* **default:** the sidebar generated based on the [global `sidebar` config](/reference/configuration/#sidebar) */} -Provide a custom site navigation sidebar for this page. -If not set, the page will use the default global sidebar. +{/* Provide a custom site navigation sidebar for this page. */} +{/* If not set, the page will use the default global sidebar. */} ##### `hasSidebar` diff --git a/docs/src/pages/demo.astro b/docs/src/pages/demo.astro deleted file mode 100644 index 85560e71b52..00000000000 --- a/docs/src/pages/demo.astro +++ /dev/null @@ -1,26 +0,0 @@ ---- -import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; ---- - -