starlight-versions 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/components/VersionSearch.astro +7 -2
- package/components/VersionSelect.astro +27 -23
- package/libs/config.ts +18 -4
- package/libs/markdown.ts +52 -16
- package/libs/path.ts +5 -1
- package/libs/starlight.ts +69 -7
- package/libs/versions.ts +236 -34
- package/middleware.ts +99 -12
- package/package.json +1 -1
- package/schema.ts +3 -1
- package/translations.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# starlight-versions
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#76](https://github.com/HiDeoo/starlight-versions/pull/76) [`a4134e7`](https://github.com/HiDeoo/starlight-versions/commit/a4134e7ac0df8778367f4ada77c3b5ee8976cc06) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds support for excluding pages from versioning.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#79](https://github.com/HiDeoo/starlight-versions/pull/79) [`4329441`](https://github.com/HiDeoo/starlight-versions/commit/4329441a853506584a2e9c43429b83a14a5152a6) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes pagination links when using Astro's `base` configuration option with multiple segments.
|
|
12
|
+
|
|
13
|
+
- [#79](https://github.com/HiDeoo/starlight-versions/pull/79) [`4329441`](https://github.com/HiDeoo/starlight-versions/commit/4329441a853506584a2e9c43429b83a14a5152a6) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes invalid versioned links for paths similar to Astro's `base` configuration option.
|
|
14
|
+
|
|
15
|
+
## 0.9.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#73](https://github.com/HiDeoo/starlight-versions/pull/73) [`4dec2d6`](https://github.com/HiDeoo/starlight-versions/commit/4dec2d697cf2529c56668d9b4882aef8d2a7756e) Thanks [@FredySandoval](https://github.com/FredySandoval)! - Adds Spanish UI translations
|
|
20
|
+
|
|
3
21
|
## 0.9.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
|
@@ -10,7 +10,7 @@ import project from 'virtual:starlight/project-context'
|
|
|
10
10
|
import starlightConfig from 'virtual:starlight/user-config'
|
|
11
11
|
import starlightVersionsConfig from 'virtual:starlight-versions-config'
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { getPagefindVersionIdentifiers, getVersionFromSlug, getVersionURL } from '../libs/versions'
|
|
14
14
|
|
|
15
15
|
const { entry } = Astro.locals.starlightRoute
|
|
16
16
|
|
|
@@ -24,7 +24,11 @@ const pagefindTranslations = {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const pageVersion = getVersionFromSlug(starlightVersionsConfig, starlightConfig, entry.id)
|
|
27
|
-
const pageVersionIdentifier =
|
|
27
|
+
const [pageVersionIdentifier, ...otherPageVersionsIdentifiers] = getPagefindVersionIdentifiers(
|
|
28
|
+
starlightVersionsConfig,
|
|
29
|
+
pageVersion,
|
|
30
|
+
entry.id,
|
|
31
|
+
)
|
|
28
32
|
|
|
29
33
|
const dataAttributes: DOMStringMap = {
|
|
30
34
|
'data-pagefind-filter': `version:${pageVersionIdentifier}`,
|
|
@@ -35,6 +39,7 @@ if (project.trailingSlash === 'never') dataAttributes['data-strip-trailing-slash
|
|
|
35
39
|
---
|
|
36
40
|
|
|
37
41
|
<site-search class={Astro.props['class']} {...dataAttributes}>
|
|
42
|
+
{otherPageVersionsIdentifiers.map((version) => <span hidden data-pagefind-filter={`version:${version}`} />)}
|
|
38
43
|
<button data-open-modal disabled aria-label={Astro.locals.t('search.label')} aria-keyshortcuts="Control+K">
|
|
39
44
|
<Icon name="magnifier" />
|
|
40
45
|
<span class="sl-hidden md:sl-block" aria-hidden="true">{Astro.locals.t('search.label')}</span>
|
|
@@ -3,37 +3,41 @@ import { Icon } from '@astrojs/starlight/components'
|
|
|
3
3
|
import starlightConfig from 'virtual:starlight/user-config'
|
|
4
4
|
import starlightVersionsConfig from 'virtual:starlight-versions-config'
|
|
5
5
|
|
|
6
|
-
import { getVersionFromSlug, getVersionURL } from '../libs/versions'
|
|
6
|
+
import { getVersionFromSlug, getVersionURL, isExcludedDocPath } from '../libs/versions'
|
|
7
7
|
|
|
8
|
-
const { id } = Astro.locals.starlightRoute
|
|
8
|
+
const { entry, id } = Astro.locals.starlightRoute
|
|
9
9
|
|
|
10
10
|
const pageVersion = getVersionFromSlug(starlightVersionsConfig, starlightConfig, id)
|
|
11
|
+
const isExcluded = pageVersion === undefined && isExcludedDocPath(starlightVersionsConfig, entry.filePath)
|
|
11
12
|
---
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
value={getVersionURL(starlightVersionsConfig, starlightConfig, Astro.url, undefined).pathname}
|
|
21
|
-
>{starlightVersionsConfig.current.label}</option
|
|
22
|
-
>
|
|
23
|
-
{
|
|
24
|
-
starlightVersionsConfig.versions.map((version) => (
|
|
14
|
+
{
|
|
15
|
+
!isExcluded && (
|
|
16
|
+
<starlight-version-select>
|
|
17
|
+
<label>
|
|
18
|
+
<span class="sr-only">{Astro.locals.t('starlightVersions.select.accessibleLabel')}</span>
|
|
19
|
+
<Icon name="seti:clock" class="icon label-icon" />
|
|
20
|
+
<select value={pageVersion?.label ?? 'current'} autocomplete="off">
|
|
25
21
|
<option
|
|
26
|
-
selected={
|
|
27
|
-
value={getVersionURL(starlightVersionsConfig, starlightConfig, Astro.url,
|
|
22
|
+
selected={pageVersion === undefined}
|
|
23
|
+
value={getVersionURL(starlightVersionsConfig, starlightConfig, Astro.url, undefined).pathname}
|
|
28
24
|
>
|
|
29
|
-
{
|
|
25
|
+
{starlightVersionsConfig.current.label}
|
|
30
26
|
</option>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
{starlightVersionsConfig.versions.map((version) => (
|
|
28
|
+
<option
|
|
29
|
+
selected={version.slug === pageVersion?.slug}
|
|
30
|
+
value={getVersionURL(starlightVersionsConfig, starlightConfig, Astro.url, version).pathname}
|
|
31
|
+
>
|
|
32
|
+
{version.label ?? version.slug}
|
|
33
|
+
</option>
|
|
34
|
+
))}
|
|
35
|
+
</select>
|
|
36
|
+
<Icon name="down-caret" class="icon caret-icon" />
|
|
37
|
+
</label>
|
|
38
|
+
</starlight-version-select>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
37
41
|
|
|
38
42
|
<style>
|
|
39
43
|
label {
|
package/libs/config.ts
CHANGED
|
@@ -18,6 +18,14 @@ export const StarlightVersionsConfigSchema = z
|
|
|
18
18
|
label: z.string().default('Latest'),
|
|
19
19
|
})
|
|
20
20
|
.prefault({}),
|
|
21
|
+
/**
|
|
22
|
+
* A list of source file glob patterns that should be excluded from versioning.
|
|
23
|
+
*
|
|
24
|
+
* Excluded pages always use the latest content across all versions.
|
|
25
|
+
*
|
|
26
|
+
* @default []
|
|
27
|
+
*/
|
|
28
|
+
exclude: z.array(z.string()).default([]),
|
|
21
29
|
/**
|
|
22
30
|
* A list of all archived versions of the documentation.
|
|
23
31
|
*/
|
|
@@ -25,10 +33,16 @@ export const StarlightVersionsConfigSchema = z
|
|
|
25
33
|
message: 'At least one version of the documentation must be defined.',
|
|
26
34
|
}),
|
|
27
35
|
})
|
|
28
|
-
.transform((value) =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
.transform((value) => {
|
|
37
|
+
// This gets populated later when generating versioned sidebars.
|
|
38
|
+
const excludedSlugsByVersion: Record<string, string[]> = {}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
...value,
|
|
42
|
+
excludedSlugsByVersion,
|
|
43
|
+
versionsBySlug: Object.fromEntries(value.versions.map((version) => [version.slug, version])),
|
|
44
|
+
}
|
|
45
|
+
})
|
|
32
46
|
|
|
33
47
|
export type StarlightVersionsUserConfig = z.input<typeof StarlightVersionsConfigSchema>
|
|
34
48
|
export type StarlightVersionsConfig = z.output<typeof StarlightVersionsConfigSchema>
|
package/libs/markdown.ts
CHANGED
|
@@ -10,7 +10,13 @@ import remarkMdx from 'remark-mdx'
|
|
|
10
10
|
import { CONTINUE, SKIP, visit } from 'unist-util-visit'
|
|
11
11
|
import type { VFile } from 'vfile'
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
ensureTrailingSlash,
|
|
15
|
+
isAbsoluteLink,
|
|
16
|
+
stripLeadingAndTrailingSlashes,
|
|
17
|
+
stripLeadingSlash,
|
|
18
|
+
stripTrailingSlash,
|
|
19
|
+
} from './path'
|
|
14
20
|
import { getFrontmatterNodeValue, parseFrontmatter } from './starlight'
|
|
15
21
|
import type { Version, VersionAsset } from './versions'
|
|
16
22
|
|
|
@@ -51,6 +57,10 @@ export function remarkStarlightVersions() {
|
|
|
51
57
|
return handleImports(node, file)
|
|
52
58
|
}
|
|
53
59
|
case 'mdxJsxFlowElement': {
|
|
60
|
+
if (node.name === 'a') {
|
|
61
|
+
return handleLinkElements(node, file)
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
if (node.name && mediaElements.has(node.name)) {
|
|
55
65
|
return handleMediaElements(node, file)
|
|
56
66
|
}
|
|
@@ -99,19 +109,17 @@ function handleFrontmatter(tree: Root, file: VFile) {
|
|
|
99
109
|
}
|
|
100
110
|
}
|
|
101
111
|
|
|
102
|
-
if (typeof frontmatter.prev === 'object' && frontmatter.prev.link
|
|
103
|
-
frontmatter.prev.link =
|
|
112
|
+
if (typeof frontmatter.prev === 'object' && frontmatter.prev.link) {
|
|
113
|
+
frontmatter.prev.link = transformLink(frontmatter.prev.link, file)
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
if (typeof frontmatter.next === 'object' && frontmatter.next.link
|
|
107
|
-
frontmatter.next.link =
|
|
116
|
+
if (typeof frontmatter.next === 'object' && frontmatter.next.link) {
|
|
117
|
+
frontmatter.next.link = transformLink(frontmatter.next.link, file)
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
if (frontmatter.hero?.actions) {
|
|
111
121
|
for (const action of frontmatter.hero.actions) {
|
|
112
|
-
|
|
113
|
-
action.link = addVersionToLink(action.link, file)
|
|
114
|
-
}
|
|
122
|
+
action.link = transformLink(action.link, file)
|
|
115
123
|
}
|
|
116
124
|
}
|
|
117
125
|
|
|
@@ -134,23 +142,50 @@ function handleFrontmatter(tree: Root, file: VFile) {
|
|
|
134
142
|
}
|
|
135
143
|
|
|
136
144
|
function handleLinks(node: Link, file: VFile) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
node.url = addVersionToLink(node.url, file)
|
|
145
|
+
node.url = transformLink(node.url, file)
|
|
140
146
|
|
|
141
147
|
return SKIP
|
|
142
148
|
}
|
|
143
149
|
|
|
144
|
-
function handleLinkElements(node: MdxJsxTextElement, file: VFile) {
|
|
150
|
+
function handleLinkElements(node: MdxJsxFlowElement | MdxJsxTextElement, file: VFile) {
|
|
145
151
|
const href = node.attributes.find((attribute) => attribute.type === 'mdxJsxAttribute' && attribute.name === 'href')
|
|
146
152
|
|
|
147
|
-
if (!href || typeof href.value !== 'string'
|
|
153
|
+
if (!href || typeof href.value !== 'string') return CONTINUE
|
|
148
154
|
|
|
149
|
-
href.value =
|
|
155
|
+
href.value = transformLink(href.value, file)
|
|
150
156
|
|
|
151
157
|
return CONTINUE
|
|
152
158
|
}
|
|
153
159
|
|
|
160
|
+
function transformLink(link: string, file: VFile) {
|
|
161
|
+
if (isAbsoluteLink(link)) return link
|
|
162
|
+
|
|
163
|
+
const excludedLink = getExcludedLink(link, file)
|
|
164
|
+
if (excludedLink) return excludedLink
|
|
165
|
+
|
|
166
|
+
return isPublicAsset(link) ? addVersionToLink(link, file) : link
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getExcludedLink(link: string, file: VFile) {
|
|
170
|
+
const { excludedSlugs, slug } = file.data
|
|
171
|
+
|
|
172
|
+
if (!excludedSlugs || !slug || link.startsWith('#') || link.startsWith('?')) return undefined
|
|
173
|
+
|
|
174
|
+
const isAbsolute = link.startsWith('/')
|
|
175
|
+
const url = new URL(link, `https://example.com/${stripLeadingAndTrailingSlashes(slug)}/`)
|
|
176
|
+
let pathname = url.pathname
|
|
177
|
+
const base = file.data.base ?? ''
|
|
178
|
+
|
|
179
|
+
if (base && (pathname === base || pathname.startsWith(ensureTrailingSlash(base)))) {
|
|
180
|
+
pathname = pathname.slice(base.length)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!excludedSlugs.includes(stripLeadingAndTrailingSlashes(pathname))) return undefined
|
|
184
|
+
if (isAbsolute) return link
|
|
185
|
+
|
|
186
|
+
return `${base}${pathname}${url.search}${url.hash}`
|
|
187
|
+
}
|
|
188
|
+
|
|
154
189
|
function handleImages(node: Image, file: VFile) {
|
|
155
190
|
if (isAbsoluteLink(node.url)) return SKIP
|
|
156
191
|
|
|
@@ -189,10 +224,10 @@ function addVersionToLink(link: string, file: VFile) {
|
|
|
189
224
|
assert.ok(file.data.version, 'A version must be provided to add a version to an Astro asset.')
|
|
190
225
|
|
|
191
226
|
const base = file.data.base ?? ''
|
|
192
|
-
const hasBase =
|
|
227
|
+
const hasBase = base && (link === base || link.startsWith(ensureTrailingSlash(base)))
|
|
193
228
|
|
|
194
229
|
if (hasBase) {
|
|
195
|
-
link = link.
|
|
230
|
+
link = link.slice(base.length)
|
|
196
231
|
}
|
|
197
232
|
|
|
198
233
|
const segments = link.split('/')
|
|
@@ -252,6 +287,7 @@ function isPublicAsset(asset: string) {
|
|
|
252
287
|
export interface TransformContext {
|
|
253
288
|
assets: VersionAsset[]
|
|
254
289
|
base: string
|
|
290
|
+
excludedSlugs: string[]
|
|
255
291
|
locale: string | undefined
|
|
256
292
|
publicDir: URL
|
|
257
293
|
slug: string
|
package/libs/path.ts
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'node:path'
|
|
|
2
2
|
|
|
3
3
|
import { slug } from 'github-slugger'
|
|
4
4
|
|
|
5
|
-
const absoluteLinkRegex = /^
|
|
5
|
+
const absoluteLinkRegex = /^[a-z][a-z\d+.-]*:/i
|
|
6
6
|
|
|
7
7
|
export function ensureTrailingSlash(filePath: string): string {
|
|
8
8
|
if (filePath.endsWith('/')) {
|
|
@@ -28,6 +28,10 @@ export function stripTrailingSlash(filePath: string) {
|
|
|
28
28
|
return filePath.slice(0, -1)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export function stripLeadingAndTrailingSlashes(filePath: string) {
|
|
32
|
+
return stripLeadingSlash(stripTrailingSlash(filePath))
|
|
33
|
+
}
|
|
34
|
+
|
|
31
35
|
export function slugifyPath(filePath: string): string {
|
|
32
36
|
const segments = stripExtension(filePath).split('/')
|
|
33
37
|
|
package/libs/starlight.ts
CHANGED
|
@@ -7,7 +7,16 @@ import type { HookParameters } from '@astrojs/starlight/types'
|
|
|
7
7
|
import type { z } from 'astro/zod'
|
|
8
8
|
import yaml from 'yaml'
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ensureTrailingSlash,
|
|
12
|
+
isAbsoluteLink,
|
|
13
|
+
slugifyPath,
|
|
14
|
+
stripLeadingAndTrailingSlashes,
|
|
15
|
+
stripTrailingSlash,
|
|
16
|
+
} from './path'
|
|
17
|
+
|
|
18
|
+
const frontmatterRegex = /^---\r?\n(?<yaml>.*?)\r?\n---(?:\r?\n|$)/s
|
|
19
|
+
const queryOrFragmentRegex = /[?#]/
|
|
11
20
|
|
|
12
21
|
export function parseFrontmatter(frontmatter: string) {
|
|
13
22
|
return yaml.parse(frontmatter) as StarlightFrontmatter
|
|
@@ -17,13 +26,45 @@ export function getFrontmatterNodeValue(frontmatter: StarlightFrontmatter) {
|
|
|
17
26
|
return yaml.stringify(frontmatter).trim()
|
|
18
27
|
}
|
|
19
28
|
|
|
29
|
+
export function getDocPath(docsDir: URL, doc: URL) {
|
|
30
|
+
return path.relative(url.fileURLToPath(docsDir), url.fileURLToPath(doc)).replaceAll('\\', '/')
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
export function getDocSlug(docsDir: URL, doc: URL) {
|
|
21
|
-
const
|
|
22
|
-
const slug = slugifyPath(docPath)
|
|
34
|
+
const slug = slugifyPath(getDocPath(docsDir, doc))
|
|
23
35
|
|
|
24
36
|
return slug === 'index' ? '/' : slug.replace(/\/index$/, '')
|
|
25
37
|
}
|
|
26
38
|
|
|
39
|
+
export function resolveDocSlug(docsDir: URL, doc: URL, content: string) {
|
|
40
|
+
const frontmatter = frontmatterRegex.exec(content)?.groups?.['yaml']
|
|
41
|
+
const customSlug = frontmatter ? parseFrontmatter(frontmatter).slug : undefined
|
|
42
|
+
|
|
43
|
+
return stripLeadingAndTrailingSlashes(typeof customSlug === 'string' ? customSlug : getDocSlug(docsDir, doc))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// https://github.com/withastro/starlight/blob/ff5090a8fd7ae068b29e3c5af7197c5e5afdfa4e/packages/starlight/utils/slugs.ts#L104
|
|
47
|
+
export function getURLSlug(url: URL) {
|
|
48
|
+
let pathname = decodeURI(url.pathname)
|
|
49
|
+
|
|
50
|
+
const base = stripTrailingSlash(import.meta.env.BASE_URL)
|
|
51
|
+
if (pathname === base || pathname.startsWith(ensureTrailingSlash(base))) pathname = pathname.slice(base.length)
|
|
52
|
+
|
|
53
|
+
const segments = pathname.split('/')
|
|
54
|
+
const htmlExt = '.html'
|
|
55
|
+
|
|
56
|
+
if (segments.at(-1) === 'index.html') {
|
|
57
|
+
// Remove trailing `index.html`.
|
|
58
|
+
segments.pop()
|
|
59
|
+
} else if (segments.at(-1)?.endsWith(htmlExt)) {
|
|
60
|
+
// Remove trailing `.html`.
|
|
61
|
+
const last = segments.pop()
|
|
62
|
+
if (last) segments.push(last.slice(0, -1 * htmlExt.length))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return segments.filter(Boolean).join('/')
|
|
66
|
+
}
|
|
67
|
+
|
|
27
68
|
export function getDocLocale(slug: string, starlightConfig: StarlightUserConfig): string | undefined {
|
|
28
69
|
const locales = Object.keys(starlightConfig.locales ?? {})
|
|
29
70
|
|
|
@@ -36,14 +77,22 @@ export function getDocLocale(slug: string, starlightConfig: StarlightUserConfig)
|
|
|
36
77
|
return locales.find((locale) => locale === localeSegment)
|
|
37
78
|
}
|
|
38
79
|
|
|
39
|
-
export function
|
|
80
|
+
export function getSidebarLinks(sidebar: StarlightSidebar): Extract<StarlightSidebar[number], { type: 'link' }>[] {
|
|
81
|
+
return sidebar.flatMap((entry) => (entry.type === 'group' ? getSidebarLinks(entry.entries) : entry))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function addPrefixToSidebarConfig(
|
|
85
|
+
prefix: string,
|
|
86
|
+
sidebar: SidebarConfigInputItem[],
|
|
87
|
+
excludedSlugs: string[] = [],
|
|
88
|
+
): SidebarConfig {
|
|
40
89
|
return sidebar.map((item) => {
|
|
41
90
|
if (typeof item === 'string') {
|
|
42
|
-
return addPrefixToSlug(prefix, item)
|
|
91
|
+
return isExcludedInternalSidebarSlug(item, excludedSlugs) ? item : addPrefixToSlug(prefix, item)
|
|
43
92
|
} else if ('items' in item) {
|
|
44
93
|
return {
|
|
45
94
|
...item,
|
|
46
|
-
items: addPrefixToSidebarConfig(prefix, item.items),
|
|
95
|
+
items: addPrefixToSidebarConfig(prefix, item.items, excludedSlugs),
|
|
47
96
|
}
|
|
48
97
|
} else if (isSidebarLegacyAutoGroup(item)) {
|
|
49
98
|
return addPrefixToLegacyAutogeneratedGroup(prefix, item)
|
|
@@ -55,12 +104,15 @@ export function addPrefixToSidebarConfig(prefix: string, sidebar: SidebarConfigI
|
|
|
55
104
|
} else if ('slug' in item) {
|
|
56
105
|
return {
|
|
57
106
|
...item,
|
|
58
|
-
slug: addPrefixToSlug(prefix, item.slug),
|
|
107
|
+
slug: isExcludedInternalSidebarSlug(item.slug, excludedSlugs) ? item.slug : addPrefixToSlug(prefix, item.slug),
|
|
59
108
|
}
|
|
60
109
|
} else if (isAbsoluteLink(item.link)) {
|
|
61
110
|
return item
|
|
62
111
|
}
|
|
63
112
|
|
|
113
|
+
const link = item.link.startsWith('./') ? item.link.slice(2) : item.link
|
|
114
|
+
if (isExcludedSidebarSlug(link, excludedSlugs)) return item
|
|
115
|
+
|
|
64
116
|
const segments = item.link.split('/')
|
|
65
117
|
segments.splice(/^[./]/.test(item.link) ? 1 : 0, 0, prefix)
|
|
66
118
|
|
|
@@ -71,6 +123,16 @@ export function addPrefixToSidebarConfig(prefix: string, sidebar: SidebarConfigI
|
|
|
71
123
|
})
|
|
72
124
|
}
|
|
73
125
|
|
|
126
|
+
function isExcludedSidebarSlug(value: string, excludedSlugs: string[]) {
|
|
127
|
+
const pathname = value.split(queryOrFragmentRegex)[0] ?? ''
|
|
128
|
+
const slug = stripLeadingAndTrailingSlashes(pathname)
|
|
129
|
+
return excludedSlugs.includes(slug)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isExcludedInternalSidebarSlug(slug: string, excludedSlugs: string[]) {
|
|
133
|
+
return isExcludedSidebarSlug(slug === 'index' ? '' : slug, excludedSlugs)
|
|
134
|
+
}
|
|
135
|
+
|
|
74
136
|
function isSidebarLegacyAutoGroup(item: SidebarConfigInputItem): item is SidebarLegacyAutoGroup {
|
|
75
137
|
return typeof item === 'object' && 'autogenerate' in item && 'label' in item
|
|
76
138
|
}
|
package/libs/versions.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
1
4
|
import type { StarlightConfig } from '@astrojs/starlight/types'
|
|
2
5
|
import type { AstroConfig, AstroIntegrationLogger } from 'astro'
|
|
3
6
|
import { z } from 'astro/zod'
|
|
4
7
|
|
|
5
8
|
import type { StarlightVersionsConfig } from '..'
|
|
6
|
-
import type
|
|
9
|
+
import { docsVersionsSchema, type DocsVersionsConfig } from '../schema'
|
|
7
10
|
|
|
8
11
|
import {
|
|
9
12
|
copyDirectory,
|
|
@@ -15,10 +18,19 @@ import {
|
|
|
15
18
|
writeJSONFile,
|
|
16
19
|
} from './fs'
|
|
17
20
|
import { transformMarkdown } from './markdown'
|
|
18
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
ensureTrailingSlash,
|
|
23
|
+
getExtension,
|
|
24
|
+
stripExtension,
|
|
25
|
+
stripLeadingAndTrailingSlashes,
|
|
26
|
+
stripTrailingSlash,
|
|
27
|
+
} from './path'
|
|
19
28
|
import { throwPluginError } from './plugin'
|
|
20
29
|
import {
|
|
21
30
|
getDocSlug,
|
|
31
|
+
getDocPath,
|
|
32
|
+
getURLSlug,
|
|
33
|
+
resolveDocSlug,
|
|
22
34
|
type StarlightSidebarUserConfig,
|
|
23
35
|
type StarlightUserConfig,
|
|
24
36
|
addPrefixToSidebarConfig,
|
|
@@ -56,7 +68,7 @@ export const VersionSchema = z.object({
|
|
|
56
68
|
* @example '2.0'
|
|
57
69
|
* @example '3-1-2'
|
|
58
70
|
*/
|
|
59
|
-
slug: z.string().refine((value) =>
|
|
71
|
+
slug: z.string().refine((value) => stripLeadingAndTrailingSlashes(value)),
|
|
60
72
|
})
|
|
61
73
|
|
|
62
74
|
export async function ensureNewVersion(
|
|
@@ -71,6 +83,13 @@ export async function ensureNewVersion(
|
|
|
71
83
|
|
|
72
84
|
if (!newVersion) return
|
|
73
85
|
|
|
86
|
+
const excludedDocs = await getExcludedDocs(
|
|
87
|
+
config,
|
|
88
|
+
locales.filter((locale) => locale !== 'root'),
|
|
89
|
+
docsDir,
|
|
90
|
+
)
|
|
91
|
+
const excludedSlugs = [...excludedDocs.values()].toSorted()
|
|
92
|
+
|
|
74
93
|
const assets: VersionAsset[] = []
|
|
75
94
|
|
|
76
95
|
await copyDirectory(docsDir, new URL(ensureTrailingSlash(newVersion.slug), docsDir), async (entry) => {
|
|
@@ -101,11 +120,14 @@ export async function ensureNewVersion(
|
|
|
101
120
|
return new URL(`../../${localeDir}/${newVersion.slug}/`, entry.dest)
|
|
102
121
|
}
|
|
103
122
|
|
|
123
|
+
if (excludedDocs.has(getDocPath(docsDir, entry.url))) return false
|
|
124
|
+
|
|
104
125
|
const slug = getDocSlug(docsDir, entry.url)
|
|
105
126
|
|
|
106
127
|
const md = await transformMarkdown(entry.content, {
|
|
107
128
|
assets: [],
|
|
108
129
|
base: stripTrailingSlash(astroConfig.base),
|
|
130
|
+
excludedSlugs,
|
|
109
131
|
locale: getDocLocale(slug, starlightConfig),
|
|
110
132
|
publicDir: astroConfig.publicDir,
|
|
111
133
|
slug,
|
|
@@ -122,7 +144,7 @@ export async function ensureNewVersion(
|
|
|
122
144
|
await copyFile(asset.source, asset.dest)
|
|
123
145
|
}
|
|
124
146
|
|
|
125
|
-
await makeVersionConfig(newVersion, starlightConfig, astroConfig.srcDir)
|
|
147
|
+
await makeVersionConfig(newVersion, starlightConfig, astroConfig.srcDir, excludedSlugs)
|
|
126
148
|
|
|
127
149
|
logger.info(`Created new version '${newVersion.slug}'.`)
|
|
128
150
|
}
|
|
@@ -139,11 +161,24 @@ export async function getVersionedSidebar(
|
|
|
139
161
|
},
|
|
140
162
|
]
|
|
141
163
|
|
|
164
|
+
const excludedSlugsByVersion: Record<string, string[]> = {}
|
|
165
|
+
|
|
142
166
|
for (const version of config.versions) {
|
|
143
|
-
const
|
|
144
|
-
|
|
167
|
+
const versionConfig = await getVersionConfig(version, srcDir)
|
|
168
|
+
const excludedSlugs = versionConfig.excluded
|
|
169
|
+
|
|
170
|
+
excludedSlugsByVersion[version.slug] = excludedSlugs
|
|
171
|
+
|
|
172
|
+
sidebar.push({
|
|
173
|
+
label: version.slug,
|
|
174
|
+
items: versionConfig.sidebar
|
|
175
|
+
? addPrefixToSidebarConfig(version.slug, versionConfig.sidebar, excludedSlugs)
|
|
176
|
+
: [{ autogenerate: { directory: version.slug } }],
|
|
177
|
+
})
|
|
145
178
|
}
|
|
146
179
|
|
|
180
|
+
config.excludedSlugsByVersion = excludedSlugsByVersion
|
|
181
|
+
|
|
147
182
|
return sidebar
|
|
148
183
|
}
|
|
149
184
|
|
|
@@ -170,15 +205,26 @@ export function getVersionURL(
|
|
|
170
205
|
url: URL,
|
|
171
206
|
version: Version | undefined,
|
|
172
207
|
): URL {
|
|
208
|
+
const canonicalSlug = getCanonicalSlug(config, starlightConfig, url)
|
|
209
|
+
|
|
210
|
+
if (version && isSlugExcludedFromVersion(config, version, canonicalSlug)) {
|
|
211
|
+
return getVersionURL(
|
|
212
|
+
{ ...config, current: { ...config.current, redirect: 'same-page' } },
|
|
213
|
+
starlightConfig,
|
|
214
|
+
url,
|
|
215
|
+
undefined,
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
173
219
|
const versionURL = new URL(url)
|
|
174
220
|
const versionSlug = version?.slug ?? ''
|
|
175
221
|
const versionRedirect = version?.redirect ?? config.current.redirect
|
|
176
222
|
|
|
177
223
|
const base = stripTrailingSlash(import.meta.env.BASE_URL)
|
|
178
|
-
const hasBase = versionURL.pathname.startsWith(base)
|
|
224
|
+
const hasBase = versionURL.pathname === base || versionURL.pathname.startsWith(ensureTrailingSlash(base))
|
|
179
225
|
|
|
180
226
|
if (hasBase) {
|
|
181
|
-
versionURL.pathname = versionURL.pathname.
|
|
227
|
+
versionURL.pathname = versionURL.pathname.slice(base.length)
|
|
182
228
|
}
|
|
183
229
|
|
|
184
230
|
let baseSegment: string | undefined
|
|
@@ -243,6 +289,42 @@ export function getVersionURL(
|
|
|
243
289
|
return versionURL
|
|
244
290
|
}
|
|
245
291
|
|
|
292
|
+
export function getCanonicalSlug(config: StarlightVersionsConfig, starlightConfig: StarlightConfig, url: URL) {
|
|
293
|
+
const slug = getURLSlug(url)
|
|
294
|
+
const version = getVersionFromSlug(config, starlightConfig, slug)
|
|
295
|
+
return version ? stripVersionSegment(slug, version.slug) : slug
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function isSlugExcludedFromVersion(config: StarlightVersionsConfig, version: Version | undefined, slug: string) {
|
|
299
|
+
return version ? (config.excludedSlugsByVersion[version.slug]?.includes(slug) ?? false) : false
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function isExcludedDocPath(config: StarlightVersionsConfig, filePath: string) {
|
|
303
|
+
const normalizedPath = filePath.replaceAll('\\', '/')
|
|
304
|
+
const docsRoot = 'content/docs/'
|
|
305
|
+
const docsRootIndex = normalizedPath.indexOf(docsRoot)
|
|
306
|
+
if (docsRootIndex === -1) return false
|
|
307
|
+
|
|
308
|
+
const docsPath = normalizedPath.slice(docsRootIndex + docsRoot.length)
|
|
309
|
+
|
|
310
|
+
return config.exclude.some((excludeGlob) => path.posix.matchesGlob(docsPath, excludeGlob))
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function getPagefindVersionIdentifiers(
|
|
314
|
+
config: StarlightVersionsConfig,
|
|
315
|
+
version: Version | undefined,
|
|
316
|
+
slug: string,
|
|
317
|
+
): [string, ...string[]] {
|
|
318
|
+
if (version) return [version.slug]
|
|
319
|
+
|
|
320
|
+
return [
|
|
321
|
+
'current',
|
|
322
|
+
...config.versions
|
|
323
|
+
.filter((archivedVersion) => isSlugExcludedFromVersion(config, archivedVersion, slug))
|
|
324
|
+
.map((archivedVersion) => archivedVersion.slug),
|
|
325
|
+
]
|
|
326
|
+
}
|
|
327
|
+
|
|
246
328
|
// An undefined version is valid and represents the current version.
|
|
247
329
|
export function getVersionFromSlug(
|
|
248
330
|
config: StarlightVersionsConfig,
|
|
@@ -268,8 +350,95 @@ export function getVersionFromSlug(
|
|
|
268
350
|
return config.versions.find((version) => version.slug === versionSegment)
|
|
269
351
|
}
|
|
270
352
|
|
|
271
|
-
export function
|
|
272
|
-
|
|
353
|
+
export function addExcludedLinksToVersionSidebar(
|
|
354
|
+
currentSidebar: StarlightSidebar,
|
|
355
|
+
archivedSidebar: StarlightSidebar,
|
|
356
|
+
version: Version,
|
|
357
|
+
excludedSlugs: string[],
|
|
358
|
+
baseURL: URL,
|
|
359
|
+
) {
|
|
360
|
+
addExcludedLinksToVersionSidebarEntries(currentSidebar, archivedSidebar, version.slug, excludedSlugs, baseURL)
|
|
361
|
+
|
|
362
|
+
return archivedSidebar
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function addExcludedLinksToVersionSidebarEntries(
|
|
366
|
+
currentSidebar: StarlightSidebar,
|
|
367
|
+
archivedSidebar: StarlightSidebar,
|
|
368
|
+
version: string,
|
|
369
|
+
excludedSlugs: string[],
|
|
370
|
+
baseURL: URL,
|
|
371
|
+
) {
|
|
372
|
+
let cursor = 0
|
|
373
|
+
|
|
374
|
+
for (const entry of currentSidebar) {
|
|
375
|
+
const archivedIndex = archivedSidebar.findIndex(
|
|
376
|
+
(candidate, index) => index >= cursor && isMatchingSidebarEntry(entry, candidate, version, baseURL),
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if (archivedIndex !== -1) {
|
|
380
|
+
const archivedEntry = archivedSidebar[archivedIndex]
|
|
381
|
+
|
|
382
|
+
if (entry.type === 'group' && archivedEntry?.type === 'group') {
|
|
383
|
+
addExcludedLinksToVersionSidebarEntries(entry.entries, archivedEntry.entries, version, excludedSlugs, baseURL)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
cursor = archivedIndex + 1
|
|
387
|
+
continue
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const clone = cloneExcludedSidebarEntry(entry, excludedSlugs, baseURL)
|
|
391
|
+
if (!clone) continue
|
|
392
|
+
|
|
393
|
+
archivedSidebar.splice(cursor, 0, clone)
|
|
394
|
+
cursor++
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function cloneExcludedSidebarEntry(
|
|
399
|
+
entry: StarlightSidebar[number],
|
|
400
|
+
excludedSlugs: string[],
|
|
401
|
+
baseURL: URL,
|
|
402
|
+
): StarlightSidebar[number] | undefined {
|
|
403
|
+
if (!('autogenerate' in entry)) return undefined
|
|
404
|
+
|
|
405
|
+
if (entry.type === 'link') {
|
|
406
|
+
const slug = getSidebarSlug(entry.href, baseURL)
|
|
407
|
+
if (!excludedSlugs.includes(slug)) return undefined
|
|
408
|
+
return { ...entry, isCurrent: false }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const entries = entry.entries.flatMap((child) => {
|
|
412
|
+
const clone = cloneExcludedSidebarEntry(child, excludedSlugs, baseURL)
|
|
413
|
+
return clone ? [clone] : []
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
return entries.length > 0 ? { ...entry, entries } : undefined
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function isMatchingSidebarEntry(
|
|
420
|
+
current: StarlightSidebar[number],
|
|
421
|
+
archived: StarlightSidebar[number],
|
|
422
|
+
version: string,
|
|
423
|
+
baseURL: URL,
|
|
424
|
+
) {
|
|
425
|
+
if (current.type !== archived.type) return false
|
|
426
|
+
if (current.type === 'link' && archived.type === 'link') {
|
|
427
|
+
return getSidebarSlug(current.href, baseURL) === getSidebarSlug(archived.href, baseURL, version)
|
|
428
|
+
}
|
|
429
|
+
if (current.type !== 'group' || archived.type !== 'group') return false
|
|
430
|
+
|
|
431
|
+
const currentAuto = 'autogenerate' in current
|
|
432
|
+
const archivedAuto = 'autogenerate' in archived
|
|
433
|
+
if (currentAuto !== archivedAuto || current.label !== archived.label) return false
|
|
434
|
+
if (!currentAuto || !archivedAuto) return true
|
|
435
|
+
|
|
436
|
+
return current.autogenerate.directory === stripVersionSegment(archived.autogenerate.directory, version)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function getSidebarSlug(href: string, baseURL: URL, version?: string) {
|
|
440
|
+
const slug = getURLSlug(new URL(href, baseURL))
|
|
441
|
+
return version ? stripVersionSegment(slug, version) : slug
|
|
273
442
|
}
|
|
274
443
|
|
|
275
444
|
// An undefined version is valid and represents the current version.
|
|
@@ -278,12 +447,9 @@ export function getVersionFromPaginationLink(
|
|
|
278
447
|
link: string,
|
|
279
448
|
locale: string | undefined,
|
|
280
449
|
): Version | undefined {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// Remove the base segment if configured.
|
|
285
|
-
segments.splice(0, 1)
|
|
286
|
-
}
|
|
450
|
+
const base = stripTrailingSlash(import.meta.env.BASE_URL)
|
|
451
|
+
const pathname = link === base || link.startsWith(ensureTrailingSlash(base)) ? link.slice(base.length) : link
|
|
452
|
+
const segments = stripLeadingAndTrailingSlashes(pathname).split('/')
|
|
287
453
|
|
|
288
454
|
if (locale) {
|
|
289
455
|
// Remove the locale segment if the current locale is not a root locale.
|
|
@@ -297,30 +463,53 @@ export function getVersionFromPaginationLink(
|
|
|
297
463
|
return config.versions.find((version) => version.slug === versionSegment)
|
|
298
464
|
}
|
|
299
465
|
|
|
300
|
-
async function getSidebarVersionGroup(version: Version, srcDir: URL) {
|
|
301
|
-
const versionConfig = await getVersionConfig(version, srcDir)
|
|
302
|
-
|
|
303
|
-
if (!versionConfig.sidebar) {
|
|
304
|
-
return {
|
|
305
|
-
label: version.slug,
|
|
306
|
-
items: [{ autogenerate: { directory: version.slug } }],
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
label: version.slug,
|
|
312
|
-
items: addPrefixToSidebarConfig(version.slug, versionConfig.sidebar),
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
466
|
async function getVersionConfig(version: Version, srcDir: URL) {
|
|
317
467
|
try {
|
|
318
|
-
return
|
|
468
|
+
return docsVersionsSchema().parse(
|
|
469
|
+
await readJSONFile<Partial<DocsVersionsConfig>>(getVersionConfigURL(version, srcDir)),
|
|
470
|
+
)
|
|
319
471
|
} catch (error) {
|
|
320
472
|
throw new Error(`Failed to read the version '${version.slug}' configuration file.`, { cause: error })
|
|
321
473
|
}
|
|
322
474
|
}
|
|
323
475
|
|
|
476
|
+
async function getExcludedDocs(config: StarlightVersionsConfig, locales: string[], docsDir: URL) {
|
|
477
|
+
const excludedDocs = new Map<string, string>()
|
|
478
|
+
|
|
479
|
+
async function visitDirectory(directory: URL) {
|
|
480
|
+
for (const entry of await listDirectory(directory)) {
|
|
481
|
+
const isDirectory = await isDirectoryEntry(entry)
|
|
482
|
+
const entryURL = new URL(isDirectory ? ensureTrailingSlash(entry.name) : entry.name, directory)
|
|
483
|
+
const docPath = getDocPath(docsDir, entryURL)
|
|
484
|
+
|
|
485
|
+
if (isDirectory) {
|
|
486
|
+
const [firstSegment, secondSegment] = docPath.split('/')
|
|
487
|
+
|
|
488
|
+
if (
|
|
489
|
+
(firstSegment && firstSegment in config.versionsBySlug) ||
|
|
490
|
+
(firstSegment && locales.includes(firstSegment) && secondSegment && secondSegment in config.versionsBySlug)
|
|
491
|
+
) {
|
|
492
|
+
continue
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
await visitDirectory(entryURL)
|
|
496
|
+
|
|
497
|
+
continue
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (!entry.isFile() || !config.exclude.some((excludeGlob) => path.posix.matchesGlob(docPath, excludeGlob))) {
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
excludedDocs.set(docPath, resolveDocSlug(docsDir, entryURL, await fs.readFile(entryURL, 'utf8')))
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
await visitDirectory(docsDir)
|
|
509
|
+
|
|
510
|
+
return excludedDocs
|
|
511
|
+
}
|
|
512
|
+
|
|
324
513
|
async function checkForNewVersion(config: StarlightVersionsConfig, docsDir: URL): Promise<Version | undefined> {
|
|
325
514
|
let newVersion: Version | undefined
|
|
326
515
|
|
|
@@ -345,12 +534,18 @@ async function checkForNewVersion(config: StarlightVersionsConfig, docsDir: URL)
|
|
|
345
534
|
return newVersion
|
|
346
535
|
}
|
|
347
536
|
|
|
348
|
-
async function makeVersionConfig(
|
|
537
|
+
async function makeVersionConfig(
|
|
538
|
+
version: Version,
|
|
539
|
+
starlightConfig: StarlightUserConfig,
|
|
540
|
+
srcDir: URL,
|
|
541
|
+
excludedSlugs: string[],
|
|
542
|
+
) {
|
|
349
543
|
const versionsDir = getVersionContentCollectionURL(srcDir)
|
|
350
544
|
|
|
351
545
|
await ensureDirectory(versionsDir)
|
|
352
546
|
await writeJSONFile(getVersionConfigURL(version, srcDir), {
|
|
353
547
|
sidebar: starlightConfig.sidebar,
|
|
548
|
+
excluded: excludedSlugs,
|
|
354
549
|
} satisfies DocsVersionsConfig)
|
|
355
550
|
}
|
|
356
551
|
|
|
@@ -362,6 +557,13 @@ function getVersionConfigURL(version: Version, srcDir: URL) {
|
|
|
362
557
|
return new URL(`${version.slug}.json`, getVersionContentCollectionURL(srcDir))
|
|
363
558
|
}
|
|
364
559
|
|
|
560
|
+
function stripVersionSegment(route: string, version: string) {
|
|
561
|
+
const segments = route.split('/')
|
|
562
|
+
const index = segments.indexOf(version)
|
|
563
|
+
if (index !== -1) segments.splice(index, 1)
|
|
564
|
+
return segments.join('/')
|
|
565
|
+
}
|
|
566
|
+
|
|
365
567
|
export type Version = z.output<typeof VersionSchema>
|
|
366
568
|
|
|
367
569
|
export interface VersionAsset {
|
package/middleware.ts
CHANGED
|
@@ -2,32 +2,119 @@ import { defineRouteMiddleware, type StarlightRouteData } from '@astrojs/starlig
|
|
|
2
2
|
import starlightConfig from 'virtual:starlight/user-config'
|
|
3
3
|
import starlightVersionsConfig from 'virtual:starlight-versions-config'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { getSidebarLinks } from './libs/starlight'
|
|
6
|
+
import {
|
|
7
|
+
addExcludedLinksToVersionSidebar,
|
|
8
|
+
getCanonicalSlug,
|
|
9
|
+
getVersionFromPaginationLink,
|
|
10
|
+
getVersionFromSlug,
|
|
11
|
+
getVersionSidebar,
|
|
12
|
+
isSlugExcludedFromVersion,
|
|
13
|
+
type Version,
|
|
14
|
+
} from './libs/versions'
|
|
6
15
|
|
|
7
16
|
export const onRequest = defineRouteMiddleware((context) => {
|
|
8
17
|
const { starlightRoute } = context.locals
|
|
9
|
-
const { entry, locale,
|
|
10
|
-
|
|
11
|
-
starlightRoute.sidebar = getVersionSidebar(
|
|
12
|
-
getVersionFromSlug(starlightVersionsConfig, starlightConfig, entry.id),
|
|
13
|
-
sidebar,
|
|
14
|
-
)
|
|
18
|
+
const { entry, locale, sidebar } = starlightRoute
|
|
15
19
|
|
|
16
20
|
const pageVersion = getVersionFromSlug(starlightVersionsConfig, starlightConfig, entry.id)
|
|
21
|
+
const versionSidebar = getVersionSidebar(pageVersion, sidebar)
|
|
22
|
+
|
|
23
|
+
if (pageVersion) {
|
|
24
|
+
const excludedSlugs = starlightVersionsConfig.excludedSlugsByVersion[pageVersion.slug] ?? []
|
|
25
|
+
if (excludedSlugs.length > 0) {
|
|
26
|
+
addExcludedLinksToVersionSidebar(
|
|
27
|
+
getVersionSidebar(undefined, sidebar),
|
|
28
|
+
versionSidebar,
|
|
29
|
+
pageVersion,
|
|
30
|
+
excludedSlugs,
|
|
31
|
+
context.url,
|
|
32
|
+
)
|
|
33
|
+
starlightRoute.pagination = getPaginationFromSidebar(starlightRoute, versionSidebar)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
starlightRoute.sidebar = versionSidebar
|
|
17
38
|
|
|
18
|
-
starlightRoute.pagination.prev = getPaginationLink(locale, pageVersion, pagination.prev)
|
|
19
|
-
starlightRoute.pagination.next = getPaginationLink(locale, pageVersion, pagination.next)
|
|
39
|
+
starlightRoute.pagination.prev = getPaginationLink(context.url, locale, pageVersion, starlightRoute.pagination.prev)
|
|
40
|
+
starlightRoute.pagination.next = getPaginationLink(context.url, locale, pageVersion, starlightRoute.pagination.next)
|
|
20
41
|
})
|
|
21
42
|
|
|
22
|
-
function getPaginationLink(
|
|
43
|
+
function getPaginationLink(
|
|
44
|
+
baseURL: URL,
|
|
45
|
+
locale: string | undefined,
|
|
46
|
+
currentVersion: Version | undefined,
|
|
47
|
+
link: PaginationLink,
|
|
48
|
+
) {
|
|
23
49
|
if (!link) return undefined
|
|
24
50
|
|
|
25
51
|
const linkVersion = getVersionFromPaginationLink(starlightVersionsConfig, link.href, locale)
|
|
52
|
+
const canonicalSlug = getCanonicalSlug(starlightVersionsConfig, starlightConfig, new URL(link.href, baseURL))
|
|
26
53
|
|
|
27
|
-
// If the current version is not the same as the link version, remove
|
|
28
|
-
return (currentVersion === undefined && linkVersion === undefined) ||
|
|
54
|
+
// If the current version is not the same as the link version and it's not an excluded version link, remove it.
|
|
55
|
+
return (currentVersion === undefined && linkVersion === undefined) ||
|
|
56
|
+
currentVersion?.slug === linkVersion?.slug ||
|
|
57
|
+
(linkVersion === undefined && isSlugExcludedFromVersion(starlightVersionsConfig, currentVersion, canonicalSlug))
|
|
29
58
|
? link
|
|
30
59
|
: undefined
|
|
31
60
|
}
|
|
32
61
|
|
|
62
|
+
// https://github.com/withastro/starlight/blob/main/packages/starlight/utils/navigation.ts#L481
|
|
63
|
+
function getPaginationFromSidebar(
|
|
64
|
+
route: StarlightRouteData,
|
|
65
|
+
sidebar: StarlightRouteData['sidebar'],
|
|
66
|
+
): StarlightRouteData['pagination'] {
|
|
67
|
+
const entries = getSidebarLinks(sidebar)
|
|
68
|
+
const currentIndex = entries.findIndex((entry) => entry.isCurrent)
|
|
69
|
+
|
|
70
|
+
const prev = applyPaginationConfig(
|
|
71
|
+
entries[currentIndex - 1],
|
|
72
|
+
starlightConfig.pagination,
|
|
73
|
+
route.entry.data.prev,
|
|
74
|
+
route.pagination.prev,
|
|
75
|
+
)
|
|
76
|
+
const next = applyPaginationConfig(
|
|
77
|
+
currentIndex > -1 ? entries[currentIndex + 1] : undefined,
|
|
78
|
+
starlightConfig.pagination,
|
|
79
|
+
route.entry.data.next,
|
|
80
|
+
route.pagination.next,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return { prev, next }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// https://github.com/withastro/starlight/blob/main/packages/starlight/utils/navigation.ts#L501
|
|
87
|
+
function applyPaginationConfig(
|
|
88
|
+
link: PaginationLink,
|
|
89
|
+
paginationEnabled: boolean,
|
|
90
|
+
config: StarlightRouteData['entry']['data']['prev'],
|
|
91
|
+
existing: PaginationLink,
|
|
92
|
+
): PaginationLink {
|
|
93
|
+
// Explicitly remove the link.
|
|
94
|
+
if (config === false) return undefined
|
|
95
|
+
// Use the generated link if any.
|
|
96
|
+
else if (config === true) return link
|
|
97
|
+
// If a link exists, update its label if needed.
|
|
98
|
+
else if (typeof config === 'string' && link) {
|
|
99
|
+
return { ...link, label: config }
|
|
100
|
+
} else if (typeof config === 'object') {
|
|
101
|
+
if (link) {
|
|
102
|
+
// If a link exists, update both its label and href if needed.
|
|
103
|
+
return {
|
|
104
|
+
...link,
|
|
105
|
+
label: config.label ?? link.label,
|
|
106
|
+
href: config.link ?? link.href,
|
|
107
|
+
// Explicitly remove sidebar link attributes for prev/next links.
|
|
108
|
+
attrs: {},
|
|
109
|
+
}
|
|
110
|
+
} else if (config.link && config.label) {
|
|
111
|
+
// If there is no link and the frontmatter contains both a URL and a label, use it.
|
|
112
|
+
return existing
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Otherwise, if the global config is enabled, return the generated link if any.
|
|
117
|
+
return paginationEnabled ? link : undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
33
120
|
type PaginationLink = StarlightRouteData['pagination']['next']
|
package/package.json
CHANGED
package/schema.ts
CHANGED
|
@@ -4,10 +4,12 @@ import type { StarlightSidebarUserConfig } from './libs/starlight'
|
|
|
4
4
|
|
|
5
5
|
export function docsVersionsSchema() {
|
|
6
6
|
return z.object({
|
|
7
|
-
sidebar: z.
|
|
7
|
+
sidebar: z.custom<NonNullable<StarlightSidebarUserConfig>>().optional(),
|
|
8
|
+
excluded: z.array(z.string()).default([]),
|
|
8
9
|
})
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export interface DocsVersionsConfig {
|
|
12
13
|
sidebar?: StarlightSidebarUserConfig
|
|
14
|
+
excluded: string[]
|
|
13
15
|
}
|
package/translations.ts
CHANGED
|
@@ -19,4 +19,15 @@ export const Translations = {
|
|
|
19
19
|
'starlightVersions.search.outdated.slug': 'Suche nur in der Version {{slug}}.',
|
|
20
20
|
'starlightVersions.select.accessibleLabel': 'Version auswählen',
|
|
21
21
|
},
|
|
22
|
+
es: {
|
|
23
|
+
'starlightVersions.link.latest':
|
|
24
|
+
'Cambia a la <a href={{link}}>versión más reciente</a> para consultar la documentación actualizada.',
|
|
25
|
+
'starlightVersions.outdated.label': 'Este contenido es para {{label}}.',
|
|
26
|
+
'starlightVersions.outdated.slug': 'Este contenido es para la versión {{slug}}.',
|
|
27
|
+
'starlightVersions.search.link.latest':
|
|
28
|
+
'Cambia a la <a href={{link}}>versión más reciente</a> para obtener resultados actualizados.',
|
|
29
|
+
'starlightVersions.search.outdated.label': 'Búsqueda limitada a {{label}}.',
|
|
30
|
+
'starlightVersions.search.outdated.slug': 'Búsqueda limitada a la versión {{slug}}.',
|
|
31
|
+
'starlightVersions.select.accessibleLabel': 'Seleccionar versión',
|
|
32
|
+
},
|
|
22
33
|
}
|