@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 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
 
@@ -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 { Breadcrumbs, TableOfContents } from '@levino/shipyard-base/components'
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
- const fullTree = toSidebarEntries(docs)
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
- const pagination = getPaginationInfo(Astro.url.pathname, entries, docs)
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
- <Breadcrumbs navigation={entries} />
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.2",
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
- "@levino/shipyard-base": "^0.6.1"
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"