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