From 0e5806fa351ff98ea927fe3c5b245dd28852a3a0 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Tue, 4 Mar 2025 19:26:14 +0100 Subject: [PATCH] feat: excluded pages (#22) --- .changeset/light-candles-divide.md | 9 ++ docs/astro.config.ts | 71 ++++++++-------- docs/src/content/docs/demo/excluded/page.mdx | 13 +++ .../src/content/docs/demo/excluded/splash.mdx | 11 +++ .../docs/demo/{secrets => unlisted}/page.mdx | 2 +- .../demo/{secrets => unlisted}/splash.mdx | 2 +- docs/src/content/docs/docs/configuration.mdx | 52 +++++++++++- .../src/content/docs/docs/getting-started.mdx | 2 +- .../docs/docs/guides/excluded-pages.mdx | 82 +++++++++++++++++++ .../docs/docs/guides/unlisted-pages.mdx | 2 + .../demo/excluded/custom-no-sidebar.astro | 16 ++++ .../src/pages/demo/excluded/custom-page.astro | 29 +++++++ .../pages/demo/excluded/custom-splash.astro | 15 ++++ .../custom-no-sidebar.astro | 2 +- .../{secrets => unlisted}/custom-page.astro | 2 +- .../{secrets => unlisted}/custom-splash.astro | 2 +- packages/starlight-sidebar-topics/index.ts | 27 ++++-- .../starlight-sidebar-topics/libs/config.ts | 20 +++++ .../starlight-sidebar-topics/libs/pathname.ts | 5 ++ .../starlight-sidebar-topics/libs/vite.ts | 33 ++++++-- .../starlight-sidebar-topics/middleware.ts | 15 +++- .../starlight-sidebar-topics/package.json | 6 +- .../tests/excluded.test.ts | 16 ++++ .../tests/fixtures/BasePage.ts | 5 +- .../tests/sidebar.test.ts | 15 +++- .../tests/unlisted.test.ts | 10 +-- .../starlight-sidebar-topics/virtual.d.ts | 6 ++ pnpm-lock.yaml | 11 +++ 28 files changed, 412 insertions(+), 69 deletions(-) create mode 100644 .changeset/light-candles-divide.md create mode 100644 docs/src/content/docs/demo/excluded/page.mdx create mode 100644 docs/src/content/docs/demo/excluded/splash.mdx rename docs/src/content/docs/demo/{secrets => unlisted}/page.mdx (94%) rename docs/src/content/docs/demo/{secrets => unlisted}/splash.mdx (92%) create mode 100644 docs/src/content/docs/docs/guides/excluded-pages.mdx create mode 100644 docs/src/pages/demo/excluded/custom-no-sidebar.astro create mode 100644 docs/src/pages/demo/excluded/custom-page.astro create mode 100644 docs/src/pages/demo/excluded/custom-splash.astro rename docs/src/pages/demo/{secrets => unlisted}/custom-no-sidebar.astro (84%) rename docs/src/pages/demo/{secrets => unlisted}/custom-page.astro (88%) rename docs/src/pages/demo/{secrets => unlisted}/custom-splash.astro (86%) create mode 100644 packages/starlight-sidebar-topics/tests/excluded.test.ts diff --git a/.changeset/light-candles-divide.md b/.changeset/light-candles-divide.md new file mode 100644 index 0000000..3fdfca0 --- /dev/null +++ b/.changeset/light-candles-divide.md @@ -0,0 +1,9 @@ +--- +'starlight-sidebar-topics': minor +--- + +Adds a new [`exclude`](https://starlight-sidebar-topics.netlify.app/docs/configuration#exclude) plugin configuration option to exclude pages from any topic. + +This options can be useful for custom pages that use a custom site navigation sidebar which do not belong to any topic. Excluded pages will use the built-in Starlight sidebar and not render a list of topics. + +See the [“Excluded Pages”](https://starlight-sidebar-topics.netlify.app/docs/guides/excluded-pages) guide to learn more about how to exclude content pages from any topic. diff --git a/docs/astro.config.ts b/docs/astro.config.ts index aaecae8..0e7098c 100644 --- a/docs/astro.config.ts +++ b/docs/astro.config.ts @@ -11,44 +11,49 @@ export default defineConfig({ baseUrl: 'https://github.com/HiDeoo/starlight-sidebar-topics/edit/main/docs/', }, plugins: [ - starlightSidebarTopics([ - { - label: 'Documentation', - link: '/docs/getting-started/', - icon: 'open-book', - items: [ - { label: 'Start Here', items: ['docs/getting-started', 'docs/configuration'] }, - { label: 'Guides', autogenerate: { directory: 'docs/guides' } }, - { label: 'Resources', items: [{ label: 'Plugins and Tools', slug: 'docs/resources/starlight' }] }, - ], - }, - { - id: 'demo', - label: { - en: 'Demo', - fr: 'Démo', + starlightSidebarTopics( + [ + { + label: 'Documentation', + link: '/docs/getting-started/', + icon: 'open-book', + items: [ + { label: 'Start Here', items: ['docs/getting-started', 'docs/configuration'] }, + { label: 'Guides', autogenerate: { directory: 'docs/guides' } }, + { label: 'Resources', items: [{ label: 'Plugins and Tools', slug: 'docs/resources/starlight' }] }, + ], }, - link: '/demo/', - icon: 'puzzle', - items: [ - { label: 'API', autogenerate: { directory: 'demo/api' } }, - { label: 'Components', autogenerate: { directory: 'demo/components' } }, - { label: 'Commands', autogenerate: { directory: 'demo/commands' }, collapsed: true }, - ], - badge: { - text: { - en: 'Stub', - fr: 'Ébauche', + { + id: 'demo', + label: { + en: 'Demo', + fr: 'Démo', + }, + link: '/demo/', + icon: 'puzzle', + items: [ + { label: 'API', autogenerate: { directory: 'demo/api' } }, + { label: 'Components', autogenerate: { directory: 'demo/components' } }, + { label: 'Commands', autogenerate: { directory: 'demo/commands' }, collapsed: true }, + ], + badge: { + text: { + en: 'Stub', + fr: 'Ébauche', + }, + variant: 'caution', }, - variant: 'caution', }, - }, + { + label: 'Starlight Docs', + link: 'https://starlight.astro.build/', + icon: 'starlight', + }, + ], { - label: 'Starlight Docs', - link: 'https://starlight.astro.build/', - icon: 'starlight', + exclude: ['/demo/excluded/**/*', '/*/demo/excluded/**/*'], }, - ]), + ), ], social: { blueSky: 'https://bsky.app/profile/hideoo.dev', diff --git a/docs/src/content/docs/demo/excluded/page.mdx b/docs/src/content/docs/demo/excluded/page.mdx new file mode 100644 index 0000000..92971e6 --- /dev/null +++ b/docs/src/content/docs/demo/excluded/page.mdx @@ -0,0 +1,13 @@ +--- +title: Excluded with sidebar +pagefind: false +--- + +This page is not listed in any topic sidebar configuration, is not associated any topic through the `topic` frontmatter field, and is also excluded in the plugin configuration. + +See the [“Excluded Pages”](/docs/guides/excluded-pages) guide to learn how to exclude content pages from any topic. + +:::note +This page exists solely for demonstration purposes and contains no useful information. +See the Starlight Sidebar Topics plugin [documentation](/docs/getting-started/) for more information. +::: diff --git a/docs/src/content/docs/demo/excluded/splash.mdx b/docs/src/content/docs/demo/excluded/splash.mdx new file mode 100644 index 0000000..c9124bb --- /dev/null +++ b/docs/src/content/docs/demo/excluded/splash.mdx @@ -0,0 +1,11 @@ +--- +title: Excluded without sidebar +pagefind: false +--- + +This page using the `splash` template is not listed in any topic sidebar configuration, is not associated with any topic through the `topic` frontmatter field, and is also excluded in the plugin configuration. + +:::note +This page exists solely for demonstration purposes and contains no useful information. +See the Starlight Sidebar Topics plugin [documentation](/docs/getting-started/) for more information. +::: diff --git a/docs/src/content/docs/demo/secrets/page.mdx b/docs/src/content/docs/demo/unlisted/page.mdx similarity index 94% rename from docs/src/content/docs/demo/secrets/page.mdx rename to docs/src/content/docs/demo/unlisted/page.mdx index 69fe2b5..30a5ff3 100644 --- a/docs/src/content/docs/demo/secrets/page.mdx +++ b/docs/src/content/docs/demo/unlisted/page.mdx @@ -1,5 +1,5 @@ --- -title: Secrets with sidebar +title: Unlisted with sidebar topic: demo pagefind: false --- diff --git a/docs/src/content/docs/demo/secrets/splash.mdx b/docs/src/content/docs/demo/unlisted/splash.mdx similarity index 92% rename from docs/src/content/docs/demo/secrets/splash.mdx rename to docs/src/content/docs/demo/unlisted/splash.mdx index 809537b..6c0a6c0 100644 --- a/docs/src/content/docs/demo/secrets/splash.mdx +++ b/docs/src/content/docs/demo/unlisted/splash.mdx @@ -1,5 +1,5 @@ --- -title: Secrets without sidebar +title: Unlisted without sidebar template: splash pagefind: false --- diff --git a/docs/src/content/docs/docs/configuration.mdx b/docs/src/content/docs/docs/configuration.mdx index 6da02f2..9a42369 100644 --- a/docs/src/content/docs/docs/configuration.mdx +++ b/docs/src/content/docs/docs/configuration.mdx @@ -5,7 +5,7 @@ description: An overview of the configuration options supported by the Starlight The Starlight Sidebar Topics can be configured inside the `astro.config.mjs` configuration file of your project: -```js {12} +```js {13,16} // astro.config.mjs // @ts-check import starlight from '@astrojs/starlight' @@ -16,9 +16,14 @@ export default defineConfig({ integrations: [ starlight({ plugins: [ - starlightSidebarTopics([ - // Topics configuration goes here. - ]), + starlightSidebarTopics( + [ + // Topics configuration goes here. + ], + { + // Plugin configuration goes here (optional). + }, + ), ], title: 'My Docs', }), @@ -180,3 +185,42 @@ starlightSidebarTopics([ }, ]) ``` + +## Plugin configuration + +The Starlight Sidebar Topics plugin accepts an optional configuration object as the second argument. + +The configuration object can be used to customize the plugin global behavior and the following options are available: + +### `exclude` + +**type:** `string[]` +**default:** `[]` + +A list of pages or [glob patterns](https://github.com/micromatch/picomatch#globbing-features) that should be excluded from any topic. + +This options can be useful for [custom pages](https://starlight.astro.build/guides/pages/#custom-pages) that use a [custom site navigation sidebar](https://starlight.astro.build/guides/pages/#sidebar) which do not belong to any topic. +Excluded pages will use the built-in Starlight sidebar and not render a list of topics. + +The following example excludes all pages under the `/blog` directory from any topic: + +```js {10} +starlightSidebarTopics( + [ + { + label: 'Guides', + link: '/guides/', + items: ['guides/concepts', 'guides/courses'], + }, + ], + { + exclude: ['/blog', '/blog/**/*'], + }, +) +``` + +See the [“Excluded Pages”](/docs/guides/excluded-pages) guide to learn how to exclude content pages from any topic. + +:::tip +You can use this [webpage](https://www.digitalocean.com/community/tools/glob) to generate and test glob patterns. +::: diff --git a/docs/src/content/docs/docs/getting-started.mdx b/docs/src/content/docs/docs/getting-started.mdx index b5473be..e5e184a 100644 --- a/docs/src/content/docs/docs/getting-started.mdx +++ b/docs/src/content/docs/docs/getting-started.mdx @@ -78,4 +78,4 @@ import { PackageManagers } from '@hideoo/starlight-plugins-docs-components' -The Starlight Sidebar Topics plugin behavior can be tweaked using various [topic configuration options](/docs/configuration/#topic-configuration). +The Starlight Sidebar Topics plugin behavior can be tweaked using various [configuration options](/docs/configuration/). diff --git a/docs/src/content/docs/docs/guides/excluded-pages.mdx b/docs/src/content/docs/docs/guides/excluded-pages.mdx new file mode 100644 index 0000000..946cfb9 --- /dev/null +++ b/docs/src/content/docs/docs/guides/excluded-pages.mdx @@ -0,0 +1,82 @@ +--- +title: Excluded Pages +description: Learn how to exclude content pages from any topic. +--- + +By default, the Starlight Sidebar Topics plugin expect that every pages in your project is associated with a topic. +This is done by including the page in a [topic sidebar configuration](/docs/configuration#items) or using the `topic` frontmatter field for [unlisted pages](/docs/guides/unlisted-pages/). + +However, there are cases where you may have [custom pages](https://starlight.astro.build/guides/pages/#custom-pages) that use a [custom site navigation sidebar](https://starlight.astro.build/guides/pages/#sidebar) which do not belong to any topic. +Excluding these pages from any topic will prevent the plugin from rendering a list of topics in the sidebar and use the built-in Starlight sidebar instead. + +## Exclude pages + +To exclude some pages from any topic, you can use the [`exclude`](/docs/configuration#exclude) plugin configuration option. + +```js {20} +// astro.config.mjs +// @ts-check +import starlight from '@astrojs/starlight' +import { defineConfig } from 'astro/config' +import starlightSidebarTopics from 'starlight-sidebar-topics' + +export default defineConfig({ + integrations: [ + starlight({ + plugins: [ + starlightSidebarTopics( + [ + { + label: 'Guides', + link: '/guides/', + items: ['guides/concepts', 'guides/courses'], + }, + ], + { + exclude: ['/changelog', '/changelog/**/*'], + }, + ), + ], + title: 'My Docs', + }), + ], +}) +``` + +For example, given the above configuration and following file structure: + +import { FileTree } from '@astrojs/starlight/components' + + + +- src/ + - content/ + - docs/ + - guides/ + - concepts.md + - courses.md + - pages/ + - changelog.astro + - … + + + +And the `changelog.astro` page content rendering the following custom page with a custom site navigation sidebar: + +```astro title="src/pages/changelog.astro" {3-7} + + … + +``` + +Visiting the `guides/concepts` and `guides/courses` pages will display the sidebar of the "Guides" topic while the `changelog` page will use the custom site navigation sidebar defined in the `changelog.astro` page using the built-in Starlight sidebar. + +- `guides/concepts` and `guides/courses` are explicitly listed in the "Guides" topic sidebar configuration under the [`items`](/docs/configuration#items) key. +- `changelog` is excluded from any topic using the [`exclude`](/docs/configuration#exclude) plugin configuration option. diff --git a/docs/src/content/docs/docs/guides/unlisted-pages.mdx b/docs/src/content/docs/docs/guides/unlisted-pages.mdx index 9b0c01f..442e6a1 100644 --- a/docs/src/content/docs/docs/guides/unlisted-pages.mdx +++ b/docs/src/content/docs/docs/guides/unlisted-pages.mdx @@ -1,6 +1,8 @@ --- title: Unlisted Pages description: Learn how to associate content pages that are not listed in any topic sidebar configuration with a specific topic. +sidebar: + order: 1 --- By default, the Starlight Sidebar Topics plugin expect that every [content pages](https://starlight.astro.build/guides/pages/#content-pages) in your project is associated with a topic. diff --git a/docs/src/pages/demo/excluded/custom-no-sidebar.astro b/docs/src/pages/demo/excluded/custom-no-sidebar.astro new file mode 100644 index 0000000..06a1bcd --- /dev/null +++ b/docs/src/pages/demo/excluded/custom-no-sidebar.astro @@ -0,0 +1,16 @@ +--- +import { Aside } from '@astrojs/starlight/components' +import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro' +--- + + +

+ This custom page with hasSidebar set to false is not listed in any topic sidebar configuration, + is not associated with any topic through the topic frontmatter field, and is also excluded in the plugin + configuration. +

+ +
diff --git a/docs/src/pages/demo/excluded/custom-page.astro b/docs/src/pages/demo/excluded/custom-page.astro new file mode 100644 index 0000000..a81ff02 --- /dev/null +++ b/docs/src/pages/demo/excluded/custom-page.astro @@ -0,0 +1,29 @@ +--- +import { Aside } from '@astrojs/starlight/components' +import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro' +--- + + +

+ This custom page with a custom sidebar is not listed in any topic sidebar configuration, is not associated + with any topic through the topic frontmatter field, and is also excluded in the plugin configuration. +

+

+ See the “Excluded Pages” guide to learn how to exclude content pages from + any topic. +

+ +
diff --git a/docs/src/pages/demo/excluded/custom-splash.astro b/docs/src/pages/demo/excluded/custom-splash.astro new file mode 100644 index 0000000..0e3b8c6 --- /dev/null +++ b/docs/src/pages/demo/excluded/custom-splash.astro @@ -0,0 +1,15 @@ +--- +import { Aside } from '@astrojs/starlight/components' +import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro' +--- + + +

+ This custom page using the splash template is not listed in any topic sidebar configuration, is not associated + with any topic through the topic frontmatter field, and is also excluded in the plugin configuration. +

+ +
diff --git a/docs/src/pages/demo/secrets/custom-no-sidebar.astro b/docs/src/pages/demo/unlisted/custom-no-sidebar.astro similarity index 84% rename from docs/src/pages/demo/secrets/custom-no-sidebar.astro rename to docs/src/pages/demo/unlisted/custom-no-sidebar.astro index 1adb961..01ce812 100644 --- a/docs/src/pages/demo/secrets/custom-no-sidebar.astro +++ b/docs/src/pages/demo/unlisted/custom-no-sidebar.astro @@ -3,7 +3,7 @@ import { Aside } from '@astrojs/starlight/components' import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro' --- - +

This custom page with hasSidebar set to false is not listed in any topic sidebar configuration and is not associated with any topic through the topic frontmatter field. diff --git a/docs/src/pages/demo/secrets/custom-page.astro b/docs/src/pages/demo/unlisted/custom-page.astro similarity index 88% rename from docs/src/pages/demo/secrets/custom-page.astro rename to docs/src/pages/demo/unlisted/custom-page.astro index 75e1047..dcdd8ed 100644 --- a/docs/src/pages/demo/secrets/custom-page.astro +++ b/docs/src/pages/demo/unlisted/custom-page.astro @@ -3,7 +3,7 @@ import { Aside } from '@astrojs/starlight/components' import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro' --- - +

This custom page is not listed in any topic sidebar configuration but is associated with the "Demo" topic through the topic: demo frontmatter entry. diff --git a/docs/src/pages/demo/secrets/custom-splash.astro b/docs/src/pages/demo/unlisted/custom-splash.astro similarity index 86% rename from docs/src/pages/demo/secrets/custom-splash.astro rename to docs/src/pages/demo/unlisted/custom-splash.astro index d4be255..926d175 100644 --- a/docs/src/pages/demo/secrets/custom-splash.astro +++ b/docs/src/pages/demo/unlisted/custom-splash.astro @@ -3,7 +3,7 @@ import { Aside } from '@astrojs/starlight/components' import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro' --- - +

This custom page using the splash template is not listed in any topic sidebar configuration and is not associated with any topic through the topic frontmatter field. diff --git a/packages/starlight-sidebar-topics/index.ts b/packages/starlight-sidebar-topics/index.ts index bcb84fe..a6da628 100644 --- a/packages/starlight-sidebar-topics/index.ts +++ b/packages/starlight-sidebar-topics/index.ts @@ -1,12 +1,20 @@ import type { StarlightPlugin, StarlightUserConfig } from '@astrojs/starlight/types' -import { StarlightSidebarTopicsConfigSchema, type StarlightSidebarTopicsUserConfig } from './libs/config' +import { + StarlightSidebarTopicsConfigSchema, + StarlightSidebarTopicsOptionsSchema, + type StarlightSidebarTopicsUserConfig, + type StarlightSidebarTopicsUserOptions, +} from './libs/config' import { overrideStarlightComponent, throwPluginError } from './libs/plugin' import { vitePluginStarlightSidebarTopics } from './libs/vite' export type { StarlightSidebarTopicsConfig, StarlightSidebarTopicsUserConfig } from './libs/config' -export default function starlightSidebarTopicsPlugin(userConfig: StarlightSidebarTopicsUserConfig): StarlightPlugin { +export default function starlightSidebarTopicsPlugin( + userConfig: StarlightSidebarTopicsUserConfig, + userOptions?: StarlightSidebarTopicsUserOptions, +): StarlightPlugin { const parsedConfig = StarlightSidebarTopicsConfigSchema.safeParse(userConfig) if (!parsedConfig.success) { @@ -15,10 +23,19 @@ export default function starlightSidebarTopicsPlugin(userConfig: StarlightSideba ) } + const parsedOptions = StarlightSidebarTopicsOptionsSchema.safeParse(userOptions) + + if (!parsedOptions.success) { + throwPluginError( + `The provided plugin options are invalid.\n${parsedOptions.error.issues.map((issue) => issue.message).join('\n')}`, + ) + } + const config = parsedConfig.data + const options = parsedOptions.data return { - name: 'starlight-sidebar-topics-plugin', + name: 'starlight-sidebar-topics', hooks: { 'config:setup'({ addIntegration, addRouteMiddleware, command, config: starlightConfig, logger, updateConfig }) { if (command !== 'dev' && command !== 'build') return @@ -30,7 +47,7 @@ export default function starlightSidebarTopicsPlugin(userConfig: StarlightSideba ) } - addRouteMiddleware({ entrypoint: 'starlight-sidebar-topics/middleware' }) + addRouteMiddleware({ entrypoint: 'starlight-sidebar-topics/middleware', order: 'pre' }) const sidebar: StarlightUserConfig['sidebar'] = [] @@ -51,7 +68,7 @@ export default function starlightSidebarTopicsPlugin(userConfig: StarlightSideba name: 'starlight-sidebar-topics-integration', hooks: { 'astro:config:setup': ({ updateConfig }) => { - updateConfig({ vite: { plugins: [vitePluginStarlightSidebarTopics(config)] } }) + updateConfig({ vite: { plugins: [vitePluginStarlightSidebarTopics(config, options)] } }) }, }, }) diff --git a/packages/starlight-sidebar-topics/libs/config.ts b/packages/starlight-sidebar-topics/libs/config.ts index 16bb238..3d28fcd 100644 --- a/packages/starlight-sidebar-topics/libs/config.ts +++ b/packages/starlight-sidebar-topics/libs/config.ts @@ -57,10 +57,30 @@ const sidebarTopicGroupSchema = sidebarTopicBaseSchema.extend({ export const StarlightSidebarTopicsConfigSchema = z.union([sidebarTopicGroupSchema, sidebarTopicLinkSchema]).array() +export const StarlightSidebarTopicsOptionsSchema = z + .object({ + /** + * Defines a list of pages or glob patterns that should be excluded from any topic. + * + * This options can be useful for custom pages that use a custom site navigation sidebar which do not belong to any + * topic. Excluded pages will use the built-in Starlight sidebar and not render a list of topics. + * + * @default [] + */ + exclude: z.array(z.string()).default([]), + }) + .strict() + .default({}) + export type StarlightSidebarTopicsUserConfig = z.input export type StarlightSidebarTopicsConfig = z.output +export type StarlightSidebarTopicsUserOptions = z.input +export type StarlightSidebarTopicsOptions = z.output + export type StarlightSidebarTopicsSharedConfig = ( | (z.output & { type: 'link' }) | (Omit, 'items'> & { type: 'group' }) )[] + +export type StarlightSidebarTopicsSharedOptions = StarlightSidebarTopicsOptions diff --git a/packages/starlight-sidebar-topics/libs/pathname.ts b/packages/starlight-sidebar-topics/libs/pathname.ts index 20b3b13..2823832 100644 --- a/packages/starlight-sidebar-topics/libs/pathname.ts +++ b/packages/starlight-sidebar-topics/libs/pathname.ts @@ -15,3 +15,8 @@ export function stripTrailingSlash(pathname: string) { if (pathname.endsWith('/')) pathname = pathname.slice(0, -1) return pathname } + +export function ensureLeadingSlash(pathname: string): string { + if (pathname.startsWith('/')) return pathname + return `/${pathname}` +} diff --git a/packages/starlight-sidebar-topics/libs/vite.ts b/packages/starlight-sidebar-topics/libs/vite.ts index 131415f..b14d04a 100644 --- a/packages/starlight-sidebar-topics/libs/vite.ts +++ b/packages/starlight-sidebar-topics/libs/vite.ts @@ -1,29 +1,44 @@ import type { ViteUserConfig } from 'astro' -import type { StarlightSidebarTopicsConfig, StarlightSidebarTopicsSharedConfig } from './config' - -const moduleId = 'virtual:starlight-sidebar-topics/config' - -export function vitePluginStarlightSidebarTopics(config: StarlightSidebarTopicsConfig): VitePlugin { - const resolvedModuleId = `\0${moduleId}` +import type { + StarlightSidebarTopicsConfig, + StarlightSidebarTopicsOptions, + StarlightSidebarTopicsSharedConfig, +} from './config' +export function vitePluginStarlightSidebarTopics( + config: StarlightSidebarTopicsConfig, + options: StarlightSidebarTopicsOptions, +): VitePlugin { const sharedConfig: StarlightSidebarTopicsSharedConfig = config.map((topic) => { if (!('items' in topic)) return { ...topic, type: 'link' } const { items, ...topicWithoutItems } = topic return { ...topicWithoutItems, type: 'group' } }) - const moduleContent = `export default ${JSON.stringify(sharedConfig)}` + const modules = { + 'virtual:starlight-sidebar-topics/config': `export default ${JSON.stringify(sharedConfig)}`, + 'virtual:starlight-sidebar-topics/options': `export default ${JSON.stringify(options)}`, + } + + const moduleResolutionMap = Object.fromEntries( + (Object.keys(modules) as (keyof typeof modules)[]).map((key) => [resolveVirtualModuleId(key), key]), + ) return { name: 'vite-plugin-starlight-sidebar-topics', load(id) { - return id === resolvedModuleId ? moduleContent : undefined + const moduleId = moduleResolutionMap[id] + return moduleId ? modules[moduleId] : undefined }, resolveId(id) { - return id === moduleId ? resolvedModuleId : undefined + return id in modules ? resolveVirtualModuleId(id) : undefined }, } } +function resolveVirtualModuleId(id: TModuleId): `\0${TModuleId}` { + return `\0${id}` +} + type VitePlugin = NonNullable[number] diff --git a/packages/starlight-sidebar-topics/middleware.ts b/packages/starlight-sidebar-topics/middleware.ts index e9183a6..8929a51 100644 --- a/packages/starlight-sidebar-topics/middleware.ts +++ b/packages/starlight-sidebar-topics/middleware.ts @@ -1,7 +1,10 @@ import { defineRouteMiddleware } from '@astrojs/starlight/route-data' +import picomatch from 'picomatch' import config from 'virtual:starlight-sidebar-topics/config' +import options from 'virtual:starlight-sidebar-topics/options' import { StarlightSidebarTopicsLocalsSymbol, type StarlightSidebarTopicsLocals } from './libs/locals' +import { ensureLeadingSlash } from './libs/pathname' import { throwPluginError } from './libs/plugin' import { getCurrentTopic, isTopicFirstPage, isTopicLastPage } from './libs/sidebar' @@ -12,13 +15,19 @@ export const onRequest = defineRouteMiddleware((context) => { if (hasSidebar) { const currentTopic = getCurrentTopic(config, sidebar, id, entry) - if (!currentTopic) + if (!currentTopic) { + if (picomatch(options.exclude)(ensureLeadingSlash(id))) return + throwPluginError( `Failed to find the topic for the \`${id}\` page.`, - `Either include this page in the sidebar configuration of the desired topic using the \`items\` property or to associate an unlisted page with a topic, use the \`topic\` frontmatter property and set it to the desired topic ID. + `Either include this page in the sidebar configuration of the desired topic using the \`items\` property, associate an unlisted page with a topic using the \`topic\` frontmatter property and set it to the desired topic ID, or exclude this page any topic using the \`exclude\` option. + +Learn more in the following guides: - Learn more about unlisted pages in the ["Unlisted pages"](https://starlight-sidebar-topics.netlify.app/docs/guides/unlisted-pages/) guide.`, +- [Unlisted pages](https://starlight-sidebar-topics.netlify.app/docs/guides/unlisted-pages/) +- [Excluded pages](https://starlight-sidebar-topics.netlify.app/docs/guides/excluded-pages/)`, ) + } starlightRoute.sidebar = currentTopic.sidebar // @ts-expect-error - See `libs/locals` for more information. diff --git a/packages/starlight-sidebar-topics/package.json b/packages/starlight-sidebar-topics/package.json index 74b4f22..24d8936 100644 --- a/packages/starlight-sidebar-topics/package.json +++ b/packages/starlight-sidebar-topics/package.json @@ -17,8 +17,12 @@ "test": "playwright install --with-deps chromium && playwright test", "lint": "eslint . --cache --max-warnings=0" }, + "dependencies": { + "picomatch": "^4.0.2" + }, "devDependencies": { - "@playwright/test": "^1.49.1" + "@playwright/test": "^1.49.1", + "@types/picomatch": "^3.0.2" }, "peerDependencies": { "@astrojs/starlight": ">=0.32.0" diff --git a/packages/starlight-sidebar-topics/tests/excluded.test.ts b/packages/starlight-sidebar-topics/tests/excluded.test.ts new file mode 100644 index 0000000..ec7fbbd --- /dev/null +++ b/packages/starlight-sidebar-topics/tests/excluded.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from './test' + +const secretPages = [ + '/excluded/page/', + '/excluded/splash/', + '/excluded/custom-page/', + '/excluded/custom-splash/', + '/excluded/custom-no-sidebar/', +] + +for (const secretPage of secretPages) { + test(`supports excluded pages: ${secretPage}`, async ({ demoPage }) => { + await demoPage.goto(secretPage) + await expect(demoPage.page.getByText('This page exists solely for demonstration purposes')).toBeVisible() + }) +} diff --git a/packages/starlight-sidebar-topics/tests/fixtures/BasePage.ts b/packages/starlight-sidebar-topics/tests/fixtures/BasePage.ts index 9566dfc..6f82465 100644 --- a/packages/starlight-sidebar-topics/tests/fixtures/BasePage.ts +++ b/packages/starlight-sidebar-topics/tests/fixtures/BasePage.ts @@ -37,8 +37,9 @@ export class BasePage { return locators[0]?.innerText() } - async getSidebarItems() { - const [, ...sidebarLists] = await this.#sidebarLists.all() + async getSidebarItems(isDefaultSidebar = false) { + const allSidebarLists = await this.#sidebarLists.all() + const sidebarLists = isDefaultSidebar ? allSidebarLists : allSidebarLists.slice(1) const sidebarItems = await Promise.all( sidebarLists.map((list) => list.getByRole('listitem').locator(':is(div, a) > span').all()), ) diff --git a/packages/starlight-sidebar-topics/tests/sidebar.test.ts b/packages/starlight-sidebar-topics/tests/sidebar.test.ts index 0d537f5..490b7e7 100644 --- a/packages/starlight-sidebar-topics/tests/sidebar.test.ts +++ b/packages/starlight-sidebar-topics/tests/sidebar.test.ts @@ -28,6 +28,7 @@ test('uses topic sidebars', async ({ demoPage, docPage }) => { 'Configuration', 'Guides', 'Unlisted Pages', + 'Excluded Pages', 'Resources', 'Plugins and Tools', ]) @@ -42,7 +43,19 @@ test('uses topic sidebars', async ({ demoPage, docPage }) => { }) test('supports unlisted pages', async ({ demoPage, docPage }) => { - await demoPage.goto('/secrets/page/') + await demoPage.goto('/unlisted/page/') expect(await docPage.getSidebarItems()).toEqual(expectedDemoSidebarItems) }) + +test('supports excluded custom pages with a custom sidebar', async ({ demoPage, docPage }) => { + await demoPage.goto('/excluded/custom-page/') + + expect(await docPage.getSidebarItems(true)).toEqual([ + 'Home', + 'Start Here', + 'Getting Started', + 'Configuration', + 'Demo', + ]) +}) diff --git a/packages/starlight-sidebar-topics/tests/unlisted.test.ts b/packages/starlight-sidebar-topics/tests/unlisted.test.ts index 284b8d4..f1e88ff 100644 --- a/packages/starlight-sidebar-topics/tests/unlisted.test.ts +++ b/packages/starlight-sidebar-topics/tests/unlisted.test.ts @@ -1,11 +1,11 @@ import { expect, test } from './test' const secretPages = [ - '/secrets/page/', - '/secrets/splash/', - '/secrets/custom-page/', - '/secrets/custom-splash/', - '/secrets/custom-no-sidebar/', + '/unlisted/page/', + '/unlisted/splash/', + '/unlisted/custom-page/', + '/unlisted/custom-splash/', + '/unlisted/custom-no-sidebar/', ] for (const secretPage of secretPages) { diff --git a/packages/starlight-sidebar-topics/virtual.d.ts b/packages/starlight-sidebar-topics/virtual.d.ts index eda6a03..e045d44 100644 --- a/packages/starlight-sidebar-topics/virtual.d.ts +++ b/packages/starlight-sidebar-topics/virtual.d.ts @@ -3,3 +3,9 @@ declare module 'virtual:starlight-sidebar-topics/config' { export default StarlightSidebarTopicsConfig } + +declare module 'virtual:starlight-sidebar-topics/options' { + const StarlightSidebarTopicsOptions: import('./libs/config').StarlightSidebarTopicsSharedOptions + + export default StarlightSidebarTopicsOptions +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37742a8..b4f2cb3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,10 +65,16 @@ importers: '@astrojs/starlight': specifier: '>=0.32.0' version: 0.32.0(astro@5.3.0(rollup@4.34.8)(typescript@5.7.2)(yaml@2.6.1)) + picomatch: + specifier: ^4.0.2 + version: 4.0.2 devDependencies: '@playwright/test': specifier: ^1.49.1 version: 1.49.1 + '@types/picomatch': + specifier: ^3.0.2 + version: 3.0.2 packages: @@ -844,6 +850,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/picomatch@3.0.2': + resolution: {integrity: sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA==} + '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} @@ -4510,6 +4519,8 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/picomatch@3.0.2': {} + '@types/sax@1.2.7': dependencies: '@types/node': 17.0.45