summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Swithinbank2024-02-20 19:41:08 +0100
committerGitHub2024-02-20 19:41:08 +0100
commitaada6805abc0068f07393585b86978ef5200439c (patch)
tree1a897bef32eef16b51293b68b30184a1f1a76887
parentfc83a05235b74be2bfe6ba8e7f95a8a5a618ead3 (diff)
downloadIT.starlight-aada6805abc0068f07393585b86978ef5200439c.tar.gz
IT.starlight-aada6805abc0068f07393585b86978ef5200439c.tar.bz2
IT.starlight-aada6805abc0068f07393585b86978ef5200439c.zip
Improve DX for `sidebar` prop in `<StarlightPage>` and document it (#1534)
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
-rw-r--r--.changeset/wise-kiwis-sneeze.md5
-rw-r--r--docs/src/content/docs/guides/pages.mdx40
-rw-r--r--packages/starlight/__tests__/basics/starlight-page-route-data.test.ts92
-rw-r--r--packages/starlight/utils/starlight-page.ts99
4 files changed, 224 insertions, 12 deletions
diff --git a/.changeset/wise-kiwis-sneeze.md b/.changeset/wise-kiwis-sneeze.md
new file mode 100644
index 00000000..368aba87
--- /dev/null
+++ b/.changeset/wise-kiwis-sneeze.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/starlight': patch
+---
+
+Improves DX of the `sidebar` prop used by the new `<StarlightPage>` component.
diff --git a/docs/src/content/docs/guides/pages.mdx b/docs/src/content/docs/guides/pages.mdx
index 0daeef16..07db994d 100644
--- a/docs/src/content/docs/guides/pages.mdx
+++ b/docs/src/content/docs/guides/pages.mdx
@@ -104,15 +104,37 @@ 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 `<StarlightPage />` 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. */}
+- The [`sidebar`](/reference/frontmatter/#sidebar) frontmatter property for customizing how the page appears in [autogenerated link groups](/reference/configuration/#sidebar) is not available. Pages using the `<StarlightPage />` component are not part of a collection and cannot be added to an autogenerated sidebar group.
+
+##### `sidebar`
+
+**type:** `SidebarEntry[]`
+**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.
+
+For example, the following page overrides the default sidebar with a link to the homepage and a group of links to different constellations.
+The current page in the sidebar is set using the `isCurrent` property and an optional `badge` has been added to a link item.
+
+```astro {3-13}
+<StarlightPage
+ frontmatter={{ title: 'Orion' }}
+ sidebar={[
+ { label: 'Home', href: '/' },
+ {
+ label: 'Constellations',
+ items: [
+ { label: 'Andromeda', href: '/andromeda/' },
+ { label: 'Orion', href: '/orion/', isCurrent: true },
+ { label: 'Ursa Minor', href: '/ursa-minor/', badge: 'Stub' },
+ ],
+ },
+ ]}
+>
+ Example content.
+</StarlightPage>
+```
##### `hasSidebar`
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 0484f610..064e261a 100644
--- a/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts
+++ b/packages/starlight/__tests__/basics/starlight-page-route-data.test.ts
@@ -1,4 +1,4 @@
-import { expect, test, vi } from 'vitest';
+import { assert, expect, test, vi } from 'vitest';
import {
generateStarlightPageRouteData,
type StarlightPageProps,
@@ -140,6 +140,96 @@ test('uses provided sidebar if any', async () => {
`);
});
+test('uses provided sidebar with minimal config', async () => {
+ const data = await generateStarlightPageRouteData({
+ props: {
+ ...starlightPageProps,
+ sidebar: [
+ { label: 'Custom link 1', href: '/test/1' },
+ { label: 'Custom link 2', href: '/test/2' },
+ ],
+ },
+ url: starlightPageUrl,
+ });
+ expect(data.sidebar.map((entry) => entry.label)).toMatchInlineSnapshot(`
+ [
+ "Custom link 1",
+ "Custom link 2",
+ ]
+ `);
+});
+
+test('supports deprecated `entries` field for sidebar groups', async () => {
+ const data = await generateStarlightPageRouteData({
+ props: {
+ ...starlightPageProps,
+ sidebar: [
+ {
+ label: 'Group',
+ entries: [
+ { label: 'Custom link 1', href: '/test/1' },
+ { label: 'Custom link 2', href: '/test/2' },
+ ],
+ },
+ ],
+ },
+ url: starlightPageUrl,
+ });
+ assert(data.sidebar[0]!.type === 'group');
+ expect(data.sidebar[0]!.entries.map((entry) => entry.label)).toMatchInlineSnapshot(`
+ [
+ "Custom link 1",
+ "Custom link 2",
+ ]
+ `);
+});
+
+test('supports `items` field for sidebar groups', async () => {
+ const data = await generateStarlightPageRouteData({
+ props: {
+ ...starlightPageProps,
+ sidebar: [
+ {
+ label: 'Group',
+ items: [
+ { label: 'Custom link 1', href: '/test/1' },
+ { label: 'Custom link 2', href: '/test/2' },
+ ],
+ },
+ ],
+ },
+ url: starlightPageUrl,
+ });
+ assert(data.sidebar[0]!.type === 'group');
+ expect(data.sidebar[0]!.entries.map((entry) => entry.label)).toMatchInlineSnapshot(`
+ [
+ "Custom link 1",
+ "Custom link 2",
+ ]
+ `);
+});
+
+test('throws error if sidebar is malformated', async () => {
+ expect(() =>
+ generateStarlightPageRouteData({
+ props: {
+ ...starlightPageProps,
+ sidebar: [
+ {
+ label: 'Custom link 1',
+ //@ts-expect-error Intentionally bad type to cause error.
+ href: 5,
+ },
+ ],
+ },
+ url: starlightPageUrl,
+ })
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
+ [Error: Invalid sidebar prop passed to the \`<StarlightPage/>\` component.
+ **0**: Did not match union:]
+ `);
+});
+
test('uses provided pagination if any', async () => {
const data = await generateStarlightPageRouteData({
props: {
diff --git a/packages/starlight/utils/starlight-page.ts b/packages/starlight/utils/starlight-page.ts
index c08ce4fa..5bdb4e9d 100644
--- a/packages/starlight/utils/starlight-page.ts
+++ b/packages/starlight/utils/starlight-page.ts
@@ -9,6 +9,8 @@ import { slugToLocaleData, urlToSlug } from './slugs';
import { getPrevNextLinks, getSidebar } from './navigation';
import { useTranslations } from './translations';
import { docsSchema } from '../schema';
+import { BadgeConfigSchema } from '../schemas/badge';
+import { SidebarLinkItemHTMLAttributesSchema } from '../schemas/sidebar';
/**
* The frontmatter schema for Starlight pages derived from the default schema for Starlight’s
@@ -57,13 +59,104 @@ type StarlightPageFrontmatter = Omit<
> & { editUrl?: string | false };
/**
+ * Link configuration schema for `<StarlightPage>`.
+ * Sets default values where possible to be more user friendly than raw `SidebarEntry` type.
+ */
+const LinkSchema = z
+ .object({
+ /** @deprecated Specifying `type` is no longer required. */
+ type: z.literal('link').default('link'),
+ label: z.string(),
+ href: z.string(),
+ isCurrent: z.boolean().default(false),
+ badge: BadgeConfigSchema(),
+ attrs: SidebarLinkItemHTMLAttributesSchema(),
+ })
+ // Make sure badge is in the object even if undefined — Zod doesn’t seem to have a way to set `undefined` as a default.
+ .transform((item) => ({ badge: undefined, ...item }));
+
+/** Base schema for link groups without the recursive `items` array. */
+const LinkGroupBase = z.object({
+ /** @deprecated Specifying `type` is no longer required. */
+ type: z.literal('group').default('group'),
+ label: z.string(),
+ collapsed: z.boolean().default(false),
+ badge: BadgeConfigSchema(),
+});
+
+// These manual types are needed to correctly type the recursive link group type.
+type ManualLinkGroupInput = Prettify<
+ z.input<typeof LinkGroupBase> &
+ // The original implementation of `<StarlightPage>` in v0.19.0 used `entries`.
+ // We want to use `items` so it matches the sidebar config in `astro.config.mjs`.
+ // Keeping `entries` support for now to not break anyone.
+ // TODO: warn about `entries` usage in a future version
+ // TODO: remove support for `entries` in a future version
+ (| {
+ /** Array of links and subcategories to display in this category. */
+ items: Array<z.input<typeof LinkSchema> | ManualLinkGroupInput>;
+ }
+ | {
+ /**
+ * @deprecated Use `items` instead of `entries`.
+ * Support for `entries` will be removed in a future version of Starlight.
+ */
+ entries: Array<z.input<typeof LinkSchema> | ManualLinkGroupInput>;
+ }
+ )
+>;
+type ManualLinkGroupOutput = z.output<typeof LinkGroupBase> & {
+ entries: Array<z.output<typeof LinkSchema> | ManualLinkGroupOutput>;
+ badge: z.output<typeof LinkGroupBase>['badge'];
+};
+type LinkGroupSchemaType = z.ZodType<ManualLinkGroupOutput, z.ZodTypeDef, ManualLinkGroupInput>;
+/**
+ * Link group configuration schema for `<StarlightPage>`.
+ * Sets default values where possible to be more user friendly than raw `SidebarEntry` type.
+ */
+const LinkGroupSchema: LinkGroupSchemaType = z.preprocess(
+ // Map `items` to `entries` as expected by the `SidebarEntry` type.
+ (arg) => {
+ if (arg && typeof arg === 'object' && 'items' in arg) {
+ const { items, ...rest } = arg;
+ return { ...rest, entries: items };
+ }
+ return arg;
+ },
+ LinkGroupBase.extend({
+ entries: z.lazy(() => z.union([LinkSchema, LinkGroupSchema]).array()),
+ })
+ // Make sure badge is in the object even if undefined.
+ .transform((item) => ({ badge: undefined, ...item }))
+) as LinkGroupSchemaType;
+
+/** Sidebar configuration schema for `<StarlightPage>` */
+const StarlightPageSidebarSchema = z.union([LinkSchema, LinkGroupSchema]).array();
+type StarlightPageSidebarUserConfig = z.input<typeof StarlightPageSidebarSchema>;
+
+/** Parse sidebar prop to ensure all required defaults are in place. */
+const normalizeSidebarProp = (
+ sidebarProp: StarlightPageSidebarUserConfig
+): StarlightRouteData['sidebar'] => {
+ const sidebar = StarlightPageSidebarSchema.safeParse(sidebarProp, { errorMap });
+ if (!sidebar.success) {
+ throwValidationError(
+ sidebar.error,
+ 'Invalid sidebar prop passed to the `<StarlightPage/>` component.'
+ );
+ }
+ return sidebar.data;
+};
+
+/**
* The props accepted by the `<StarlightPage/>` component.
*/
export type StarlightPageProps = Prettify<
// Remove the index signature from `Route`, omit undesired properties and make the rest optional.
Partial<Omit<RemoveIndexSignature<PageProps>, 'entry' | 'entryMeta' | 'id' | 'locale' | 'slug'>> &
// Add the sidebar definitions for a Starlight page.
- Partial<Pick<StarlightRouteData, 'hasSidebar' | 'sidebar'>> & {
+ Partial<Pick<StarlightRouteData, 'hasSidebar'>> & {
+ sidebar?: StarlightPageSidebarUserConfig;
// And finally add the Starlight page frontmatter properties in a `frontmatter` property.
frontmatter: StarlightPageFrontmatter;
}
@@ -94,7 +187,9 @@ export async function generateStarlightPageRouteData({
const pageFrontmatter = await getStarlightPageFrontmatter(frontmatter);
const id = `${stripLeadingAndTrailingSlashes(slug)}.md`;
const localeData = slugToLocaleData(slug);
- const sidebar = props.sidebar ?? getSidebar(url.pathname, localeData.locale);
+ const sidebar = props.sidebar
+ ? normalizeSidebarProp(props.sidebar)
+ : getSidebar(url.pathname, localeData.locale);
const headings = props.headings ?? [];
const pageDocsEntry: StarlightPageDocsEntry = {
id,