@levino/shipyard-docs 0.6.2 → 0.6.3
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/README.md +40 -1
- package/astro/Layout.astro +159 -5
- package/package.json +4 -2
- package/src/index.ts +797 -146
- package/src/rehypeVersionLinks.test.ts +319 -0
- package/src/rehypeVersionLinks.ts +156 -0
- package/src/routeHelpers.test.ts +657 -1
- package/src/routeHelpers.ts +221 -0
- package/src/sidebarEntries.test.ts +154 -1
- package/src/sidebarEntries.ts +33 -0
- package/src/versionHelpers.ts +64 -0
- package/src/versionSchema.test.ts +404 -0
- package/src/virtual-module.d.ts +68 -0
package/README.md
CHANGED
|
@@ -56,11 +56,50 @@ export default defineConfig({
|
|
|
56
56
|
})
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
### Versioned Documentation
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// astro.config.ts
|
|
63
|
+
export default defineConfig({
|
|
64
|
+
integrations: [
|
|
65
|
+
shipyardDocs({
|
|
66
|
+
routeBasePath: 'docs',
|
|
67
|
+
versions: {
|
|
68
|
+
current: 'v2',
|
|
69
|
+
available: [
|
|
70
|
+
{ version: 'v2', label: 'Version 2.0 (Latest)' },
|
|
71
|
+
{ version: 'v1', label: 'Version 1.0', banner: 'unmaintained' },
|
|
72
|
+
],
|
|
73
|
+
deprecated: ['v1'],
|
|
74
|
+
stable: 'v2',
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
],
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
With versioned content collection:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// content.config.ts
|
|
85
|
+
import { defineCollection } from 'astro:content'
|
|
86
|
+
import { createVersionedDocsCollection } from '@levino/shipyard-docs'
|
|
87
|
+
|
|
88
|
+
const docs = defineCollection(
|
|
89
|
+
createVersionedDocsCollection('./docs', {
|
|
90
|
+
versions: ['v1', 'v2'],
|
|
91
|
+
})
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
export const collections = { docs }
|
|
95
|
+
```
|
|
96
|
+
|
|
59
97
|
### Routes
|
|
60
98
|
|
|
61
99
|
The integration automatically injects these routes:
|
|
62
100
|
|
|
63
|
-
- `/[routeBasePath]/[...slug]` - Documentation pages
|
|
101
|
+
- `/[routeBasePath]/[...slug]` - Documentation pages (without versioning)
|
|
102
|
+
- `/[routeBasePath]/[version]/[...slug]` - Versioned documentation pages
|
|
64
103
|
|
|
65
104
|
With i18n enabled, routes are prefixed with `[locale]`.
|
|
66
105
|
|
package/astro/Layout.astro
CHANGED
|
@@ -3,13 +3,19 @@ import { i18n } from 'astro:config/server'
|
|
|
3
3
|
import { type CollectionKey, getCollection, render } from 'astro:content'
|
|
4
4
|
import { docsConfigs } from 'virtual:shipyard-docs-configs'
|
|
5
5
|
import type { NavigationTree } from '@levino/shipyard-base'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Breadcrumbs,
|
|
8
|
+
DeprecationBanner,
|
|
9
|
+
TableOfContents,
|
|
10
|
+
VersionBadge,
|
|
11
|
+
VersionSelector,
|
|
12
|
+
} from '@levino/shipyard-base/components'
|
|
7
13
|
import BaseLayout from '@levino/shipyard-base/layouts/Page.astro'
|
|
8
14
|
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
|
|
9
15
|
import { Array as EffectArray, Option } from 'effect'
|
|
10
16
|
import { getPaginationInfo } from '../src/pagination'
|
|
11
17
|
import type { DocsData } from '../src/sidebarEntries'
|
|
12
|
-
import { toSidebarEntries } from '../src/sidebarEntries'
|
|
18
|
+
import { filterDocsForVersion, toSidebarEntries } from '../src/sidebarEntries'
|
|
13
19
|
import DocMetadata from './DocMetadata.astro'
|
|
14
20
|
import DocPagination from './DocPagination.astro'
|
|
15
21
|
import LlmsTxtSidebarLabel from './LlmsTxtSidebarLabel.astro'
|
|
@@ -45,6 +51,11 @@ interface Props {
|
|
|
45
51
|
* The name of the author who last updated this page.
|
|
46
52
|
*/
|
|
47
53
|
lastAuthor?: string
|
|
54
|
+
/**
|
|
55
|
+
* The current version being viewed (for versioned docs).
|
|
56
|
+
* If not provided, it will be extracted from the URL.
|
|
57
|
+
*/
|
|
58
|
+
currentVersion?: string
|
|
48
59
|
/**
|
|
49
60
|
* Whether to hide the table of contents on this page.
|
|
50
61
|
* @default false
|
|
@@ -84,6 +95,7 @@ const {
|
|
|
84
95
|
editUrl,
|
|
85
96
|
lastUpdated,
|
|
86
97
|
lastAuthor,
|
|
98
|
+
currentVersion: currentVersionProp,
|
|
87
99
|
hideTableOfContents = false,
|
|
88
100
|
hideTitle = false,
|
|
89
101
|
keywords,
|
|
@@ -96,6 +108,40 @@ const {
|
|
|
96
108
|
// Normalize the route base path
|
|
97
109
|
const normalizedBasePath = routeBasePath.replace(/^\/+|\/+$/g, '')
|
|
98
110
|
|
|
111
|
+
// Get version configuration for this docs instance
|
|
112
|
+
const versionsConfig = docsConfigs[normalizedBasePath]?.versions
|
|
113
|
+
|
|
114
|
+
// Extract current version from URL if not provided via props
|
|
115
|
+
// URL patterns: /docs/v1.0/... or /en/docs/v1.0/...
|
|
116
|
+
const extractVersionFromUrl = (): string | undefined => {
|
|
117
|
+
if (!versionsConfig) return undefined
|
|
118
|
+
|
|
119
|
+
const path = Astro.url.pathname
|
|
120
|
+
const basePath = normalizedBasePath
|
|
121
|
+
|
|
122
|
+
// Find the segment after the base path
|
|
123
|
+
// Handle both /docs/v1.0/... and /en/docs/v1.0/...
|
|
124
|
+
const basePathIndex = path.indexOf(`/${basePath}/`)
|
|
125
|
+
if (basePathIndex === -1) return undefined
|
|
126
|
+
|
|
127
|
+
const afterBasePath = path.substring(basePathIndex + basePath.length + 2)
|
|
128
|
+
const nextSegment = afterBasePath.split('/')[0]
|
|
129
|
+
|
|
130
|
+
// Handle 'latest' alias - resolve to current version
|
|
131
|
+
if (nextSegment === 'latest') {
|
|
132
|
+
return versionsConfig.current
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if this segment matches a known version path
|
|
136
|
+
const matchingVersion = versionsConfig.available.find(
|
|
137
|
+
(v) => v.path === nextSegment || v.version === nextSegment,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return matchingVersion?.version
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const currentVersion = currentVersionProp ?? extractVersionFromUrl()
|
|
144
|
+
|
|
99
145
|
// Get the collection name from config, defaulting to the route base path
|
|
100
146
|
const collectionName =
|
|
101
147
|
docsConfigs[normalizedBasePath]?.collectionName ?? normalizedBasePath
|
|
@@ -165,7 +211,14 @@ const docs =
|
|
|
165
211
|
)
|
|
166
212
|
.then((promises) => Promise.all(promises)))
|
|
167
213
|
|
|
168
|
-
|
|
214
|
+
// Filter docs by version for sidebar if versioning is enabled
|
|
215
|
+
// This ensures the sidebar only shows docs from the current version
|
|
216
|
+
const sidebarDocs =
|
|
217
|
+
currentVersion && versionsConfig
|
|
218
|
+
? filterDocsForVersion(docs, currentVersion)
|
|
219
|
+
: docs
|
|
220
|
+
|
|
221
|
+
const fullTree = toSidebarEntries(sidebarDocs)
|
|
169
222
|
|
|
170
223
|
const entries: NavigationTree =
|
|
171
224
|
i18n && Astro.currentLocale
|
|
@@ -174,7 +227,8 @@ const entries: NavigationTree =
|
|
|
174
227
|
|
|
175
228
|
// Compute pagination info for the current page BEFORE adding llms.txt
|
|
176
229
|
// (llms.txt should not be part of pagination navigation)
|
|
177
|
-
|
|
230
|
+
// Use sidebarDocs for versioned docs to keep pagination within the same version
|
|
231
|
+
const pagination = getPaginationInfo(Astro.url.pathname, entries, sidebarDocs)
|
|
178
232
|
|
|
179
233
|
// Add llms.txt link to sidebar if enabled for this docs instance
|
|
180
234
|
// This is added AFTER pagination computation so it doesn't affect prev/next navigation
|
|
@@ -190,13 +244,113 @@ if (docsConfig?.llmsTxtEnabled) {
|
|
|
190
244
|
labelHtml: llmsTxtLabelHtml,
|
|
191
245
|
}
|
|
192
246
|
}
|
|
247
|
+
|
|
248
|
+
// Prepare version selector props (only if versions are configured)
|
|
249
|
+
const hasVersions = versionsConfig && versionsConfig.available.length > 1
|
|
250
|
+
const versionSelectorProps = hasVersions
|
|
251
|
+
? {
|
|
252
|
+
versions: versionsConfig.available,
|
|
253
|
+
currentVersion: currentVersion ?? versionsConfig.current,
|
|
254
|
+
stableVersion: versionsConfig.stable,
|
|
255
|
+
deprecatedVersions: versionsConfig.deprecated,
|
|
256
|
+
}
|
|
257
|
+
: null
|
|
258
|
+
|
|
259
|
+
// Prepare version badge props (show if versioning is enabled)
|
|
260
|
+
const versionBadgeProps =
|
|
261
|
+
versionsConfig && currentVersion
|
|
262
|
+
? {
|
|
263
|
+
version: currentVersion,
|
|
264
|
+
stableVersion: versionsConfig.stable,
|
|
265
|
+
currentVersion: versionsConfig.current,
|
|
266
|
+
deprecatedVersions: versionsConfig.deprecated,
|
|
267
|
+
banner: versionsConfig.available.find(
|
|
268
|
+
(v) => v.version === currentVersion,
|
|
269
|
+
)?.banner,
|
|
270
|
+
isLatestAlias: Astro.url.pathname.includes('/latest/'),
|
|
271
|
+
}
|
|
272
|
+
: null
|
|
273
|
+
|
|
274
|
+
// Check if current version is deprecated
|
|
275
|
+
const isDeprecated =
|
|
276
|
+
versionsConfig &&
|
|
277
|
+
currentVersion &&
|
|
278
|
+
(versionsConfig.deprecated?.includes(currentVersion) ||
|
|
279
|
+
versionsConfig.available.find((v) => v.version === currentVersion)
|
|
280
|
+
?.banner === 'unmaintained')
|
|
281
|
+
|
|
282
|
+
// Prepare deprecation banner props (show only for deprecated versions)
|
|
283
|
+
// This block only executes when isDeprecated is true, which guarantees versionsConfig and currentVersion exist
|
|
284
|
+
const deprecationBannerProps =
|
|
285
|
+
isDeprecated && versionsConfig && currentVersion
|
|
286
|
+
? (() => {
|
|
287
|
+
// Find the current version info
|
|
288
|
+
const currentVersionInfo = versionsConfig.available.find(
|
|
289
|
+
(v) => v.version === currentVersion,
|
|
290
|
+
)
|
|
291
|
+
// Find the latest version info
|
|
292
|
+
const latestVersionInfo = versionsConfig.available.find(
|
|
293
|
+
(v) => v.version === versionsConfig.current,
|
|
294
|
+
)
|
|
295
|
+
// Build the URL to the same page in the latest version
|
|
296
|
+
const currentPath = Astro.url.pathname
|
|
297
|
+
const currentVersionPath = currentVersionInfo?.path ?? currentVersion
|
|
298
|
+
const latestVersionPath =
|
|
299
|
+
latestVersionInfo?.path ?? versionsConfig.current
|
|
300
|
+
// Escape special regex characters in the version path
|
|
301
|
+
const escapedVersionPath = currentVersionPath.replace(
|
|
302
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
303
|
+
'\\$&',
|
|
304
|
+
)
|
|
305
|
+
const latestVersionUrl = currentPath.replace(
|
|
306
|
+
new RegExp(`/${escapedVersionPath}/`),
|
|
307
|
+
`/${latestVersionPath}/`,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
version: currentVersion,
|
|
312
|
+
label: currentVersionInfo?.label,
|
|
313
|
+
currentVersion: versionsConfig.current,
|
|
314
|
+
currentVersionLabel: latestVersionInfo?.label,
|
|
315
|
+
latestVersionUrl,
|
|
316
|
+
}
|
|
317
|
+
})()
|
|
318
|
+
: null
|
|
193
319
|
---
|
|
194
320
|
|
|
195
321
|
<BaseLayout sidebarNavigation={entries} keywords={keywords} image={image} canonicalUrl={canonicalUrl} customMetaTags={customMetaTags} title={titleMeta}>
|
|
322
|
+
{
|
|
323
|
+
versionSelectorProps && (
|
|
324
|
+
<VersionSelector
|
|
325
|
+
slot="navbarExtra"
|
|
326
|
+
variant="dropdown"
|
|
327
|
+
{...versionSelectorProps}
|
|
328
|
+
/>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
{
|
|
332
|
+
versionSelectorProps && (
|
|
333
|
+
<VersionSelector
|
|
334
|
+
slot="sidebarExtra"
|
|
335
|
+
variant="list"
|
|
336
|
+
{...versionSelectorProps}
|
|
337
|
+
/>
|
|
338
|
+
)
|
|
339
|
+
}
|
|
196
340
|
<div class="grid grid-cols-12 gap-6 max-w-7xl mx-auto">
|
|
197
341
|
<div class:list={['col-span-12', { 'xl:col-span-9': !hideTableOfContents }]}>
|
|
342
|
+
{deprecationBannerProps && (
|
|
343
|
+
<div class="not-prose mb-4">
|
|
344
|
+
<DeprecationBanner {...deprecationBannerProps} />
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
198
347
|
<div class:list={['prose', 'max-w-none', { 'hide-title': hideTitle }]}>
|
|
199
|
-
<
|
|
348
|
+
<div class="flex items-center gap-3 flex-wrap not-prose mb-2">
|
|
349
|
+
<Breadcrumbs navigation={entries} />
|
|
350
|
+
{versionBadgeProps && (
|
|
351
|
+
<VersionBadge {...versionBadgeProps} size="sm" />
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
200
354
|
{!hideTableOfContents && <TableOfContents links={headings} class="xl:hidden" />}
|
|
201
355
|
<slot />
|
|
202
356
|
<DocMetadata editUrl={editUrl} lastUpdated={lastUpdated} lastAuthor={lastAuthor} />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@levino/shipyard-docs",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Documentation plugin for shipyard with automatic sidebar, pagination, and git metadata",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -26,10 +26,12 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"effect": "^3.12.5",
|
|
28
28
|
"ramda": "^0.31",
|
|
29
|
-
"
|
|
29
|
+
"unist-util-visit": "^5.0.0",
|
|
30
|
+
"@levino/shipyard-base": "^0.6.3"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@tailwindcss/typography": "^0.5.16",
|
|
34
|
+
"@types/hast": "^3.0.4",
|
|
33
35
|
"@types/ramda": "^0.31",
|
|
34
36
|
"astro": "^5.15",
|
|
35
37
|
"vitest": "^2.1.8"
|