@rimelight/cms 0.0.1 → 0.0.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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +16 -14
  3. package/src/admin/api/media-file.ts +39 -0
  4. package/src/admin/api/media.ts +199 -0
  5. package/src/admin/layouts/CMSDashboardLayout.astro +178 -0
  6. package/src/admin/layouts/DefaultParentLayout.astro +50 -0
  7. package/src/admin/pages/assets.astro +424 -0
  8. package/src/admin/pages/byline-schema.astro +62 -0
  9. package/src/admin/pages/bylines.astro +16 -0
  10. package/src/admin/pages/categories.astro +21 -0
  11. package/src/admin/pages/content-types.astro +55 -0
  12. package/src/admin/pages/dashboard.astro +61 -0
  13. package/src/admin/pages/pages-edit.astro +595 -0
  14. package/src/admin/pages/pages-index.astro +146 -0
  15. package/src/admin/pages/pages-new.astro +130 -0
  16. package/src/admin/pages/pages-preview.astro +111 -0
  17. package/src/admin/pages/pages-review.astro +236 -0
  18. package/src/admin/pages/settings.astro +203 -0
  19. package/src/admin/pages/tags.astro +21 -0
  20. package/src/admin/pages/templates-edit.astro +234 -0
  21. package/src/admin/pages/templates-index.astro +95 -0
  22. package/src/admin/pages/templates-new.astro +193 -0
  23. package/src/admin/pages/users.astro +74 -0
  24. package/src/admin/pages/versions.astro +74 -0
  25. package/src/astro/BlockRenderer.astro +44 -96
  26. package/src/astro/PageRenderer.astro +9 -2
  27. package/src/astro/blocks/CalloutBlock.astro +7 -7
  28. package/src/astro/blocks/CardBlock.astro +5 -5
  29. package/src/astro/blocks/CodeBlock.astro +2 -2
  30. package/src/astro/blocks/FileTreeBlock.astro +7 -7
  31. package/src/astro/blocks/ImageBlock.astro +2 -2
  32. package/src/astro/blocks/OrderedListBlock.astro +2 -2
  33. package/src/astro/blocks/ParagraphBlock.astro +3 -3
  34. package/src/astro/blocks/SectionBlock.astro +6 -6
  35. package/src/astro/blocks/StepItemBlock.astro +3 -3
  36. package/src/astro/blocks/StepsBlock.astro +1 -1
  37. package/src/astro/blocks/TableBlock.astro +6 -6
  38. package/src/astro/blocks/TabsBlock.astro +9 -9
  39. package/src/astro/blocks/UnorderedListBlock.astro +2 -2
  40. package/src/astro/blocks/index.ts +35 -0
  41. package/src/client/components/RimelightBlock.tsx +19 -19
  42. package/src/client/components/RimelightBlockPicker.tsx +9 -13
  43. package/src/client/components/RimelightBylinesManager.tsx +438 -0
  44. package/src/client/components/RimelightEditor.tsx +3 -3
  45. package/src/client/components/RimelightPreview.tsx +37 -45
  46. package/src/client/components/RimelightPropertiesPanel.tsx +368 -26
  47. package/src/client/components/RimelightTaxonomyManager.tsx +369 -0
  48. package/src/client/components/RimelightTaxonomyMasterDetail.tsx +412 -0
  49. package/src/client/components/RimelightTemplateEditor.tsx +6 -8
  50. package/src/client/components/editors/CalloutEditor.tsx +2 -4
  51. package/src/client/components/editors/CardsEditor.tsx +2 -4
  52. package/src/client/components/editors/CodeEditor.tsx +1 -3
  53. package/src/client/components/editors/FileTreeEditor.tsx +2 -4
  54. package/src/client/components/editors/ImageEditor.tsx +3 -3
  55. package/src/client/components/editors/ParagraphEditor.tsx +2 -2
  56. package/src/client/components/editors/SceneEditor.tsx +6 -12
  57. package/src/client/components/editors/ScriptEditor.tsx +8 -16
  58. package/src/client/components/editors/SectionEditor.tsx +4 -4
  59. package/src/client/components/editors/StepItemEditor.tsx +2 -2
  60. package/src/client/components/editors/StepsEditor.tsx +2 -4
  61. package/src/client/components/editors/TabItemEditor.tsx +3 -3
  62. package/src/client/components/editors/TableEditor.tsx +5 -5
  63. package/src/client/components/editors/TabsEditor.tsx +2 -4
  64. package/src/client/components/index.ts +3 -0
  65. package/src/client/loader.ts +3 -0
  66. package/src/core/diff.ts +145 -0
  67. package/src/core/excerpt.ts +159 -0
  68. package/src/core/preview.ts +87 -0
  69. package/src/core/registry.ts +0 -39
  70. package/src/core/types/blocks.ts +83 -6
  71. package/src/core/types/bylines.ts +14 -0
  72. package/src/core/types/media.ts +13 -0
  73. package/src/core/types/taxonomies.ts +20 -0
  74. package/src/cron/scheduled.ts +57 -0
  75. package/src/docs/agent.ts +116 -0
  76. package/src/docs/config.ts +25 -0
  77. package/src/docs/index.ts +5 -0
  78. package/src/docs/routing.ts +104 -0
  79. package/src/docs/sidebar.ts +251 -0
  80. package/src/docs/toc.ts +47 -0
  81. package/src/env.d.ts +13 -0
  82. package/src/index.ts +40 -6
  83. package/src/integration.ts +196 -0
  84. package/src/loader/live.ts +254 -0
  85. package/src/markdown/index.ts +0 -1
  86. package/src/mcp/index.ts +228 -0
  87. package/src/schema/bylines.ts +23 -0
  88. package/src/schema/index.ts +3 -0
  89. package/src/schema/page_versions.ts +2 -0
  90. package/src/schema/pages.ts +2 -0
  91. package/src/schema/site_settings.ts +41 -0
  92. package/src/schema/taxonomies.ts +49 -0
  93. package/src/services/bylines.ts +74 -0
  94. package/src/services/search-indexer.ts +61 -0
  95. package/src/services/site-settings.ts +108 -0
  96. package/src/services/taxonomies.ts +276 -0
  97. package/src/storage/index.ts +253 -0
  98. package/src/astro/blocks/DialogueBlock.astro +0 -23
  99. package/src/astro/blocks/SceneBlock.astro +0 -65
  100. package/src/astro/blocks/ScriptBlock.astro +0 -59
  101. package/src/core/template-sync.ts +0 -75
  102. package/src/markdown/parser.ts +0 -347
  103. package/src/migration.ts +0 -155
  104. package/src/search/sync.ts +0 -32
@@ -1,5 +1,3 @@
1
- import type { TimeOfDay, Setting, Transition } from "./types/blocks"
2
-
3
1
  export interface BlockDefinition {
4
2
  type: string
5
3
  label: string
@@ -60,40 +58,3 @@ BlockRegistry.register({
60
58
  description: "Code block with syntax highlighting",
61
59
  defaultProps: { code: "", language: "typescript" }
62
60
  })
63
-
64
- BlockRegistry.register({
65
- type: "ScriptBlock",
66
- label: "Script",
67
- description: "Script block with title, synopsis, characters, and plot tracking",
68
- defaultProps: {
69
- title: "",
70
- synopsis: "",
71
- characters: [],
72
- aPlot: "",
73
- bPlot: "",
74
- cPlot: "",
75
- stinger: ""
76
- },
77
- allowChildren: true
78
- })
79
-
80
- BlockRegistry.register({
81
- type: "SceneBlock",
82
- label: "Scene",
83
- description: "Scene block with location, time, setting, and transition",
84
- defaultProps: {
85
- location: "",
86
- timeOfDay: "OTHER" as TimeOfDay,
87
- setting: "OTHER" as Setting,
88
- transition: "NONE" as Transition,
89
- description: ""
90
- },
91
- allowChildren: true
92
- })
93
-
94
- BlockRegistry.register({
95
- type: "DialogueBlock",
96
- label: "Dialogue",
97
- description: "Dialogue block with character, parenthetical, and line",
98
- defaultProps: { character: "", parenthetical: "", line: "" }
99
- })
@@ -17,6 +17,9 @@ export type BlockType =
17
17
  | "CardsBlock"
18
18
  | "CardBlock"
19
19
  | "FileTreeBlock"
20
+ | "ComponentShowcaseBlock"
21
+ | "ApiDocumentationBlock"
22
+ | "LivePreviewBlock"
20
23
  | (string & {})
21
24
 
22
25
  export type TimeOfDay = "MORNING" | "NOON" | "AFTERNOON" | "EVENING" | "NIGHT" | "OTHER"
@@ -117,10 +120,15 @@ export interface CalloutBlockProps {
117
120
  children: BaseBlock[]
118
121
  }
119
122
 
123
+ import type { MediaValue } from "./media"
124
+
120
125
  export interface ImageBlockProps {
121
- src: string
126
+ src: string | MediaValue
122
127
  alt: string
123
128
  caption?: string
129
+ width?: number
130
+ height?: number
131
+ aspectRatio?: string
124
132
  }
125
133
 
126
134
  export interface CodeBlockProps {
@@ -191,6 +199,42 @@ export interface FileTreeBlockProps {
191
199
  tree: FileTreeNode[]
192
200
  }
193
201
 
202
+ export interface ComponentShowcaseBlockProps {
203
+ componentName: string
204
+ componentPath: string
205
+ framework: "astro" | "vue" | "solid"
206
+ defaultProps: Record<string, any>
207
+ propsConfig: Record<string, string[]>
208
+ togglingPattern: "icon" | "select-option" | "tab-segmented" | "pill-toggle"
209
+ codeExamples: {
210
+ astro?: string
211
+ vue?: string
212
+ solid?: string
213
+ }
214
+ playgroundUrl: string | undefined
215
+ }
216
+
217
+ export interface ApiDocumentationBlockProps {
218
+ componentName: string
219
+ sections: {
220
+ props?: boolean
221
+ slots?: boolean
222
+ emits?: boolean
223
+ theme?: boolean
224
+ changelog?: boolean
225
+ }
226
+ hide?: string[]
227
+ show?: string[]
228
+ }
229
+
230
+ export interface LivePreviewBlockProps {
231
+ componentCode: string
232
+ framework: "astro" | "vue" | "solid"
233
+ height?: string
234
+ width?: string
235
+ editable?: boolean
236
+ }
237
+
194
238
  export interface TypedScriptBlockProps extends ScriptBlockProps {
195
239
  children: BaseBlock[]
196
240
  }
@@ -281,6 +325,21 @@ export interface FileTreeBlock extends BaseBlock {
281
325
  props: FileTreeBlockProps
282
326
  }
283
327
 
328
+ export interface ComponentShowcaseBlock extends BaseBlock {
329
+ type: "ComponentShowcaseBlock"
330
+ props: ComponentShowcaseBlockProps
331
+ }
332
+
333
+ export interface ApiDocumentationBlock extends BaseBlock {
334
+ type: "ApiDocumentationBlock"
335
+ props: ApiDocumentationBlockProps
336
+ }
337
+
338
+ export interface LivePreviewBlock extends BaseBlock {
339
+ type: "LivePreviewBlock"
340
+ props: LivePreviewBlockProps
341
+ }
342
+
284
343
  export type TypedBlock =
285
344
  | SectionBlock
286
345
  | ParagraphBlock
@@ -298,6 +357,9 @@ export type TypedBlock =
298
357
  | CardsBlock
299
358
  | CardBlock
300
359
  | FileTreeBlock
360
+ | ComponentShowcaseBlock
361
+ | ApiDocumentationBlock
362
+ | LivePreviewBlock
301
363
 
302
364
  export const ALLOWED_CHILDREN_MAP: Record<string, BlockType[]> = {
303
365
  Root: [
@@ -313,7 +375,10 @@ export const ALLOWED_CHILDREN_MAP: Record<string, BlockType[]> = {
313
375
  "TabsBlock",
314
376
  "StepsBlock",
315
377
  "CardsBlock",
316
- "FileTreeBlock"
378
+ "FileTreeBlock",
379
+ "ComponentShowcaseBlock",
380
+ "ApiDocumentationBlock",
381
+ "LivePreviewBlock"
317
382
  ],
318
383
  SectionBlock: [
319
384
  "SectionBlock",
@@ -326,7 +391,10 @@ export const ALLOWED_CHILDREN_MAP: Record<string, BlockType[]> = {
326
391
  "TabsBlock",
327
392
  "StepsBlock",
328
393
  "CardsBlock",
329
- "FileTreeBlock"
394
+ "FileTreeBlock",
395
+ "ComponentShowcaseBlock",
396
+ "ApiDocumentationBlock",
397
+ "LivePreviewBlock"
330
398
  ],
331
399
  CalloutBlock: [
332
400
  "ParagraphBlock",
@@ -337,7 +405,10 @@ export const ALLOWED_CHILDREN_MAP: Record<string, BlockType[]> = {
337
405
  "TabsBlock",
338
406
  "StepsBlock",
339
407
  "CardsBlock",
340
- "FileTreeBlock"
408
+ "FileTreeBlock",
409
+ "ComponentShowcaseBlock",
410
+ "ApiDocumentationBlock",
411
+ "LivePreviewBlock"
341
412
  ],
342
413
  TabsBlock: ["TabItemBlock"],
343
414
  TabItemBlock: [
@@ -348,7 +419,10 @@ export const ALLOWED_CHILDREN_MAP: Record<string, BlockType[]> = {
348
419
  "TableBlock",
349
420
  "StepsBlock",
350
421
  "CardsBlock",
351
- "FileTreeBlock"
422
+ "FileTreeBlock",
423
+ "ComponentShowcaseBlock",
424
+ "ApiDocumentationBlock",
425
+ "LivePreviewBlock"
352
426
  ],
353
427
  StepsBlock: ["StepItemBlock"],
354
428
  StepItemBlock: [
@@ -359,7 +433,10 @@ export const ALLOWED_CHILDREN_MAP: Record<string, BlockType[]> = {
359
433
  "TableBlock",
360
434
  "TabsBlock",
361
435
  "CardsBlock",
362
- "FileTreeBlock"
436
+ "FileTreeBlock",
437
+ "ComponentShowcaseBlock",
438
+ "ApiDocumentationBlock",
439
+ "LivePreviewBlock"
363
440
  ],
364
441
  CardsBlock: ["CardBlock"],
365
442
  ScriptBlock: ["SceneBlock"],
@@ -0,0 +1,14 @@
1
+ export type BylineRole =
2
+ | "written_by"
3
+ | "reviewed_by"
4
+ | "edited_by"
5
+ | "illustrated_by"
6
+ | "translated_by"
7
+ | "curated_by"
8
+
9
+ export interface BylineCredit {
10
+ userId: string
11
+ role: BylineRole
12
+ customTitle?: string
13
+ displayOrder?: number
14
+ }
@@ -0,0 +1,13 @@
1
+ export interface MediaValue {
2
+ id: string
3
+ src: string
4
+ alt: string
5
+ width?: number
6
+ height?: number
7
+ format?: string
8
+ size?: number
9
+ blurhash?: string
10
+ previewUrl?: string
11
+ provider: "r2" | "external" | "local"
12
+ meta?: Record<string, unknown>
13
+ }
@@ -0,0 +1,20 @@
1
+ import type { Localized } from "./pages"
2
+
3
+ export interface TaxonomyTerm {
4
+ id: string
5
+ taxonomy: string
6
+ slug: string
7
+ label: Localized | string
8
+ description?: Localized | string | null
9
+ parentId?: string | null
10
+ displayOrder?: number
11
+ children?: TaxonomyTerm[]
12
+ count?: number
13
+ createdAt?: Date
14
+ updatedAt?: Date
15
+ }
16
+
17
+ export interface TaxonomyQueryOptions {
18
+ includeCounts?: boolean
19
+ locale?: string
20
+ }
@@ -0,0 +1,57 @@
1
+ import { and, isNotNull, isNull, lt, lte } from "drizzle-orm"
2
+ import { pageDraftLocks } from "../schema/page_draft_locks.ts"
3
+ import { pages } from "../schema/pages.ts"
4
+
5
+ export interface CmsCronResult {
6
+ expiredLocksCleaned: number
7
+ scheduledPagesActive: number
8
+ timestamp: string
9
+ }
10
+
11
+ /**
12
+ * Handles the periodic Cloudflare Worker cron trigger (e.g. "* * * * *").
13
+ *
14
+ * - Cleans up expired page draft locks
15
+ * - Checks for active scheduled pages
16
+ */
17
+ export async function handleCmsCron(
18
+ _event: any,
19
+ _env: any,
20
+ _ctx: any,
21
+ db: any
22
+ ): Promise<CmsCronResult> {
23
+ const result: CmsCronResult = {
24
+ expiredLocksCleaned: 0,
25
+ scheduledPagesActive: 0,
26
+ timestamp: new Date().toISOString()
27
+ }
28
+
29
+ if (!db) return result
30
+
31
+ try {
32
+ const now = new Date()
33
+
34
+ // 1. Purge expired draft edit locks
35
+ const deletedLocks = await db
36
+ .delete(pageDraftLocks)
37
+ .where(lt(pageDraftLocks.expiresAt, now))
38
+ .returning({ pageId: pageDraftLocks.pageId })
39
+
40
+ result.expiredLocksCleaned = deletedLocks.length
41
+
42
+ // 2. Query active published pages in current window
43
+ const activeScheduled = await db
44
+ .select({ id: pages.id, slug: pages.slug })
45
+ .from(pages)
46
+ .where(
47
+ and(isNull(pages.deletedAt), isNotNull(pages.publishedVersionId), lte(pages.postedAt, now))
48
+ )
49
+ .limit(10)
50
+
51
+ result.scheduledPagesActive = activeScheduled.length
52
+ } catch (error) {
53
+ console.error("[CmsCron] Error executing scheduled tasks:", error)
54
+ }
55
+
56
+ return result
57
+ }
@@ -0,0 +1,116 @@
1
+ import { blocksToMarkdown } from "../markdown/index.ts"
2
+ import { pages } from "../schema/pages.ts"
3
+ import { eq, and, isNull, isNotNull } from "drizzle-orm"
4
+
5
+ export interface AgentMarkdownOptions {
6
+ siteUrl?: string
7
+ llmsUrl?: string
8
+ sourceUrl?: string
9
+ }
10
+
11
+ function resolveLocalized(val: unknown, locale: string = "en"): string {
12
+ if (!val) return ""
13
+ if (typeof val === "string") return val
14
+ if (typeof val === "object" && val !== null) {
15
+ const rec = val as Record<string, string>
16
+ return rec[locale] || rec.en || Object.values(rec)[0] || ""
17
+ }
18
+ return typeof val === "number" ? String(val) : ""
19
+ }
20
+
21
+ /**
22
+ * Renders a CMS page record into clean, agent-ready Markdown with YAML frontmatter.
23
+ */
24
+ export function renderPageAsMarkdown(
25
+ page: any,
26
+ locale: string = "en",
27
+ options: AgentMarkdownOptions = {}
28
+ ): string {
29
+ const { siteUrl = "https://rimelight.com", llmsUrl = "/llms.txt", sourceUrl } = options
30
+
31
+ let contentObj = page.content
32
+ if (typeof contentObj === "string") {
33
+ try {
34
+ contentObj = JSON.parse(contentObj)
35
+ } catch {
36
+ contentObj = { blocks: [], properties: {} }
37
+ }
38
+ } else if (!contentObj || typeof contentObj !== "object") {
39
+ contentObj = { blocks: [], properties: {} }
40
+ }
41
+
42
+ const blocks = Array.isArray(contentObj.blocks) ? contentObj.blocks : []
43
+ const properties = contentObj.properties || {}
44
+
45
+ const title = resolveLocalized(page.title, locale) || properties.title || "Untitled"
46
+ const description = resolveLocalized(page.description, locale) || properties.description || ""
47
+ const version = properties.version || page.templateVersion
48
+ const markdownBody = blocksToMarkdown(blocks)
49
+
50
+ const frontmatter = [
51
+ "---",
52
+ `title: ${JSON.stringify(title)}`,
53
+ ...(description ? [`description: ${JSON.stringify(description)}`] : []),
54
+ ...(version ? [`version: ${JSON.stringify(version)}`] : []),
55
+ ...(page.type ? [`type: ${JSON.stringify(page.type)}`] : []),
56
+ "---",
57
+ "",
58
+ "> Documentation Index",
59
+ `> Fetch the complete documentation index at: ${new URL(llmsUrl, siteUrl).href}`,
60
+ "> Use this file to discover all available pages and sections.",
61
+ "",
62
+ `# ${title}`,
63
+ "",
64
+ markdownBody,
65
+ ""
66
+ ]
67
+
68
+ if (sourceUrl) {
69
+ frontmatter.push(`Source: ${new URL(sourceUrl, siteUrl).href}`, "")
70
+ }
71
+
72
+ return frontmatter.join("\n")
73
+ }
74
+
75
+ /**
76
+ * Collate all published pages of a given type into a single corpus markdown document.
77
+ */
78
+ export async function renderCorpusMarkdown(
79
+ db: any,
80
+ options: {
81
+ type?: string
82
+ locale?: string
83
+ siteUrl?: string
84
+ title?: string
85
+ description?: string
86
+ } = {}
87
+ ): Promise<string> {
88
+ const {
89
+ type = "doc",
90
+ locale = "en",
91
+ siteUrl = "https://rimelight.com",
92
+ title = "Rimelight Documentation Corpus",
93
+ description = "Complete single-corpus documentation for AI agents."
94
+ } = options
95
+
96
+ if (!db) return `# ${title}\n\n${description}`
97
+
98
+ const conditions = [isNull(pages.deletedAt), isNotNull(pages.publishedVersionId)]
99
+ if (type) {
100
+ conditions.push(eq(pages.type, type))
101
+ }
102
+
103
+ const rows = await db
104
+ .select()
105
+ .from(pages)
106
+ .where(and(...conditions))
107
+
108
+ const parts = [`# ${title}`, "", description, "", `Generated for site: ${siteUrl}`, "", "---", ""]
109
+
110
+ for (const page of rows) {
111
+ const pageMd = renderPageAsMarkdown(page, locale, { siteUrl })
112
+ parts.push(pageMd, "", "---", "")
113
+ }
114
+
115
+ return parts.join("\n")
116
+ }
@@ -0,0 +1,25 @@
1
+ import type { SidebarItem, SidebarScope } from "@rimelight/ui/components/sidebar/sidebar.ts"
2
+ import type { BadgeProps } from "@rimelight/ui/components/badge/badge.ts"
3
+
4
+ export interface EditLinkConfig {
5
+ baseUrl: string
6
+ }
7
+
8
+ export interface TableOfContentsConfig {
9
+ minHeadingLevel?: number
10
+ maxHeadingLevel?: number
11
+ }
12
+
13
+ export interface DocsConfig {
14
+ basePath?: string
15
+ sidebar:
16
+ | (SidebarItem | SidebarScope)[]
17
+ | {
18
+ scopes: SidebarScope[]
19
+ }
20
+ editLink?: EditLinkConfig
21
+ tableOfContents?: false | TableOfContentsConfig
22
+ }
23
+
24
+ export const defineDocsConfig = <const T extends DocsConfig>(config: T): T => config
25
+ export type { BadgeProps, SidebarItem, SidebarScope }
@@ -0,0 +1,5 @@
1
+ export * from "./config.ts"
2
+ export * from "./sidebar.ts"
3
+ export * from "./routing.ts"
4
+ export * from "./toc.ts"
5
+ export * from "./agent.ts"
@@ -0,0 +1,104 @@
1
+ export interface DocEntry {
2
+ id?: string
3
+ slug?: string
4
+ version?: string
5
+ properties?: Record<string, any>
6
+ [key: string]: any
7
+ }
8
+
9
+ export function getDocsVersions(entries: (DocEntry | string)[], locale?: string): string[] {
10
+ const versions = new Set<string>()
11
+ for (const entry of entries) {
12
+ if (typeof entry === "string") {
13
+ const parts = entry.replace(/\\/g, "/").split("/")
14
+ if (parts[0]?.startsWith("v")) {
15
+ versions.add(parts[0])
16
+ }
17
+ continue
18
+ }
19
+
20
+ if (entry.version && entry.version.startsWith("v")) {
21
+ versions.add(entry.version)
22
+ continue
23
+ }
24
+
25
+ if (entry.properties?.version && String(entry.properties.version).startsWith("v")) {
26
+ versions.add(String(entry.properties.version))
27
+ continue
28
+ }
29
+
30
+ const path = (entry.slug || entry.id || "").replace(/\\/g, "/")
31
+ const parts = path.split("/")
32
+ if (locale && parts[1] === locale && parts[0]?.startsWith("v")) {
33
+ versions.add(parts[0])
34
+ } else if (parts[0]?.startsWith("v")) {
35
+ versions.add(parts[0])
36
+ }
37
+ }
38
+ return Array.from(versions).toSorted((a, b) => {
39
+ return b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" })
40
+ })
41
+ }
42
+
43
+ export function parseDocsSlug(slug: string | undefined, versions: string[]) {
44
+ if (versions.length === 0) {
45
+ return {
46
+ activeVersion: "",
47
+ contentPath: slug || "index",
48
+ includeVersionInUrl: false
49
+ }
50
+ }
51
+
52
+ const defaultVersion = versions[0]
53
+ const isMultiVersion = versions.length > 1
54
+
55
+ if (!slug) {
56
+ return {
57
+ activeVersion: defaultVersion,
58
+ contentPath: "index",
59
+ includeVersionInUrl: isMultiVersion
60
+ }
61
+ }
62
+
63
+ const parts = slug.split("/")
64
+ const firstPart = parts[0]
65
+
66
+ if (firstPart && versions.includes(firstPart)) {
67
+ return {
68
+ activeVersion: firstPart,
69
+ contentPath: parts.slice(1).join("/") || "index",
70
+ includeVersionInUrl: isMultiVersion
71
+ }
72
+ }
73
+
74
+ return {
75
+ activeVersion: defaultVersion,
76
+ contentPath: slug || "index",
77
+ includeVersionInUrl: isMultiVersion
78
+ }
79
+ }
80
+
81
+ export function formatDocsUrl(
82
+ href: string,
83
+ locale: string,
84
+ activeVersion?: string,
85
+ includeVersionInUrl?: boolean
86
+ ): string {
87
+ let url = href.replace("{locale}", locale)
88
+ if (activeVersion && includeVersionInUrl) {
89
+ if (url.includes(`/${activeVersion}/`) || url.endsWith(`/${activeVersion}`)) {
90
+ return url
91
+ }
92
+ const docsPattern = new RegExp(`(/${locale}/docs)(/|$)`)
93
+ const companyDocsPattern = new RegExp(`(/${locale}/company/docs)(/|$)`)
94
+
95
+ if (companyDocsPattern.test(url)) {
96
+ url = url.replace(companyDocsPattern, `$1/${activeVersion}$2`)
97
+ } else if (docsPattern.test(url)) {
98
+ url = url.replace(docsPattern, `$1/${activeVersion}$2`)
99
+ }
100
+
101
+ url = url.replace(/\/{2,}/g, "/")
102
+ }
103
+ return url
104
+ }