@lupinum/ginko-docs 0.4.0-rc.9 → 0.5.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.
Files changed (304) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/LICENSE +1 -1
  3. package/README.md +300 -53
  4. package/app/app.config.ts +44 -55
  5. package/app/assets/docs.css +619 -0
  6. package/app/components/DocsBanner.vue +29 -0
  7. package/app/components/DocsExample.vue +43 -0
  8. package/app/components/DocsFooter.vue +19 -0
  9. package/app/components/DocsHeader.vue +113 -0
  10. package/app/components/DocsIcon.vue +12 -0
  11. package/app/components/DocsLanding.vue +58 -0
  12. package/app/components/DocsLanguage.vue +40 -0
  13. package/app/components/DocsNavSheet.vue +37 -0
  14. package/app/components/DocsNavTree.vue +38 -0
  15. package/app/components/DocsPage.vue +88 -0
  16. package/app/components/DocsPageActions.vue +47 -0
  17. package/app/components/DocsPageEnd.ts +12 -0
  18. package/app/components/DocsPost.vue +58 -0
  19. package/app/components/DocsSearch.vue +136 -0
  20. package/app/components/DocsShell.vue +36 -0
  21. package/app/components/DocsSidebar.vue +19 -0
  22. package/app/components/DocsSidebarEnd.ts +4 -0
  23. package/app/components/DocsTable.vue +11 -0
  24. package/app/components/DocsThemeToggle.vue +45 -0
  25. package/app/components/DocsToc.vue +73 -0
  26. package/app/composables/useDocs.ts +53 -0
  27. package/app/composables/useDocsBlog.ts +33 -0
  28. package/app/composables/useDocsCopy.ts +17 -0
  29. package/app/composables/useDocsFrame.ts +11 -0
  30. package/app/composables/useDocsPopover.ts +36 -0
  31. package/app/composables/useDocsSite.ts +31 -0
  32. package/app/docs-pages/blog.vue +55 -0
  33. package/app/docs-pages/changelog.vue +35 -0
  34. package/app/docs-pages/doc.vue +16 -0
  35. package/app/docs-pages/docs.vue +9 -0
  36. package/app/docs-pages/index.vue +12 -0
  37. package/app/docs-pages/post.vue +8 -0
  38. package/app/error.vue +38 -115
  39. package/app/layouts/default.vue +29 -7
  40. package/app/lib/blogTypes.ts +8 -0
  41. package/app/lib/docsFirst.ts +11 -0
  42. package/app/lib/docsInit.ts +18 -0
  43. package/app/lib/docsNav.ts +36 -0
  44. package/app/lib/docsPage.ts +84 -0
  45. package/app/lib/markdownComponents.ts +15 -0
  46. package/app/lib/previewToc.ts +18 -0
  47. package/dist/config.d.mts +252 -0
  48. package/dist/config.d.ts +252 -0
  49. package/dist/config.mjs +83 -0
  50. package/dist/icons.d.mts +38 -0
  51. package/dist/icons.d.ts +38 -0
  52. package/dist/icons.mjs +175 -0
  53. package/dist/pages.d.mts +5 -0
  54. package/dist/pages.d.ts +5 -0
  55. package/dist/pages.mjs +68 -0
  56. package/i18n/de.json +1 -0
  57. package/i18n/en.json +1 -0
  58. package/i18n/fr.json +1 -0
  59. package/icons/README.md +2 -0
  60. package/icons/bluesky.svg +1 -0
  61. package/icons/claude.svg +1 -0
  62. package/icons/discord.svg +1 -0
  63. package/icons/github.svg +1 -0
  64. package/icons/linkedin.svg +1 -0
  65. package/icons/mastodon.svg +1 -0
  66. package/icons/openai.svg +1 -0
  67. package/icons/x.svg +1 -0
  68. package/icons/youtube.svg +1 -0
  69. package/nuxt.config.ts +22 -212
  70. package/package.json +44 -76
  71. package/public/_docs/fonts/LICENSE +188 -0
  72. package/public/_docs/fonts/inter-latin-wght-normal.woff2 +0 -0
  73. package/public/_docs/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  74. package/public/_docs/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  75. package/server/feeds/blog-locale.get.ts +9 -0
  76. package/server/feeds/blog.get.ts +4 -0
  77. package/server/lib/blogFeed.ts +35 -0
  78. package/server/mcp/handler.ts +108 -0
  79. package/server/routes/__ginko/docs-mcp.ts +35 -0
  80. package/shared/layerRoutes.ts +66 -0
  81. package/shared/paths.ts +4 -0
  82. package/app/app.vue +0 -98
  83. package/app/assets/css/prose.css +0 -3240
  84. package/app/assets/css/tailwind.css +0 -394
  85. package/app/assets/css/theme-palettes.css +0 -309
  86. package/app/assets/css/theme-presets.css +0 -141
  87. package/app/components/OgImage/GinkoDocs.satori.vue +0 -238
  88. package/app/components/content/DocumentPageShell.vue +0 -55
  89. package/app/components/content/Feedback.vue +0 -102
  90. package/app/components/content/ImageZoomDialog.vue +0 -84
  91. package/app/components/content/PageMarkdownCopy.vue +0 -104
  92. package/app/components/content/imageZoom.ts +0 -25
  93. package/app/components/mdc/MdcAccordion.vue +0 -67
  94. package/app/components/mdc/MdcAccordionItem.vue +0 -42
  95. package/app/components/mdc/MdcApi.vue +0 -159
  96. package/app/components/mdc/MdcAside.vue +0 -23
  97. package/app/components/mdc/MdcCallout.vue +0 -61
  98. package/app/components/mdc/MdcCard.vue +0 -175
  99. package/app/components/mdc/MdcCards.vue +0 -30
  100. package/app/components/mdc/MdcCenter.vue +0 -71
  101. package/app/components/mdc/MdcCodeGroup.vue +0 -197
  102. package/app/components/mdc/MdcCodeTree.vue +0 -234
  103. package/app/components/mdc/MdcCollapse.vue +0 -61
  104. package/app/components/mdc/MdcColumn.vue +0 -70
  105. package/app/components/mdc/MdcDropcap.vue +0 -84
  106. package/app/components/mdc/MdcError.vue +0 -23
  107. package/app/components/mdc/MdcExcerpt.vue +0 -26
  108. package/app/components/mdc/MdcFigure.vue +0 -85
  109. package/app/components/mdc/MdcFiles.vue +0 -120
  110. package/app/components/mdc/MdcIdea.vue +0 -17
  111. package/app/components/mdc/MdcInfo.vue +0 -17
  112. package/app/components/mdc/MdcInlineToc.vue +0 -51
  113. package/app/components/mdc/MdcKbd.vue +0 -10
  114. package/app/components/mdc/MdcLayout.vue +0 -57
  115. package/app/components/mdc/MdcNote.vue +0 -17
  116. package/app/components/mdc/MdcQuiz.vue +0 -241
  117. package/app/components/mdc/MdcQuizQuestion.vue +0 -221
  118. package/app/components/mdc/MdcReadMore.vue +0 -70
  119. package/app/components/mdc/MdcSteps.vue +0 -59
  120. package/app/components/mdc/MdcSuccess.vue +0 -23
  121. package/app/components/mdc/MdcTab.vue +0 -17
  122. package/app/components/mdc/MdcTabs.vue +0 -150
  123. package/app/components/mdc/MdcTimeline.vue +0 -17
  124. package/app/components/mdc/MdcTimelineItem.vue +0 -40
  125. package/app/components/mdc/MdcWarning.vue +0 -23
  126. package/app/components/mdc/api.utils.ts +0 -132
  127. package/app/components/mdc/code-tree.utils.ts +0 -22
  128. package/app/components/mdc/icons.ts +0 -10
  129. package/app/components/mdc/quiz.utils.ts +0 -38
  130. package/app/components/prose/ProseA.vue +0 -27
  131. package/app/components/prose/ProseBlockquote.vue +0 -30
  132. package/app/components/prose/ProseCode.vue +0 -14
  133. package/app/components/prose/ProseEm.vue +0 -14
  134. package/app/components/prose/ProseH1.vue +0 -18
  135. package/app/components/prose/ProseH2.vue +0 -29
  136. package/app/components/prose/ProseH3.vue +0 -29
  137. package/app/components/prose/ProseH4.vue +0 -29
  138. package/app/components/prose/ProseH5.vue +0 -15
  139. package/app/components/prose/ProseH6.vue +0 -15
  140. package/app/components/prose/ProseHr.vue +0 -12
  141. package/app/components/prose/ProseImg.vue +0 -55
  142. package/app/components/prose/ProseLi.vue +0 -14
  143. package/app/components/prose/ProseOl.vue +0 -14
  144. package/app/components/prose/ProseP.vue +0 -14
  145. package/app/components/prose/ProsePre.vue +0 -180
  146. package/app/components/prose/ProseStrong.vue +0 -14
  147. package/app/components/prose/ProseTable.vue +0 -18
  148. package/app/components/prose/ProseTbody.vue +0 -14
  149. package/app/components/prose/ProseTd.vue +0 -14
  150. package/app/components/prose/ProseTh.vue +0 -14
  151. package/app/components/prose/ProseThead.vue +0 -14
  152. package/app/components/prose/ProseTr.vue +0 -14
  153. package/app/components/prose/ProseUl.vue +0 -14
  154. package/app/components/site/ModeToggle.vue +0 -81
  155. package/app/components/site/SiteBanner.vue +0 -97
  156. package/app/components/site/SiteFooter.vue +0 -90
  157. package/app/components/site/SiteHeader.vue +0 -371
  158. package/app/components/site/SiteHeroCode.vue +0 -173
  159. package/app/components/site/SiteInteractionLayer.vue +0 -42
  160. package/app/components/site/SiteLocaleSwitcher.vue +0 -190
  161. package/app/components/site/SiteLogoMark.vue +0 -12
  162. package/app/components/site/SiteSkipLink.vue +0 -14
  163. package/app/components/site/SiteSocialLinks.vue +0 -33
  164. package/app/components/site/header-utils.ts +0 -3
  165. package/app/components/site/mode-toggle.utils.ts +0 -18
  166. package/app/components/ui/badge/Badge.vue +0 -28
  167. package/app/components/ui/badge/index.ts +0 -24
  168. package/app/components/ui/button/Button.vue +0 -31
  169. package/app/components/ui/button/index.ts +0 -35
  170. package/app/components/ui/collapsible/Collapsible.vue +0 -15
  171. package/app/components/ui/collapsible/CollapsibleContent.vue +0 -25
  172. package/app/components/ui/collapsible/CollapsibleTrigger.vue +0 -12
  173. package/app/components/ui/collapsible/index.ts +0 -3
  174. package/app/components/ui/dropdown-menu/DropdownMenu.vue +0 -15
  175. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +0 -50
  176. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +0 -41
  177. package/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +0 -15
  178. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +0 -37
  179. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +0 -23
  180. package/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue +0 -14
  181. package/app/components/ui/dropdown-menu/index.ts +0 -7
  182. package/app/components/ui/kbd/Kbd.vue +0 -23
  183. package/app/components/ui/kbd/index.ts +0 -21
  184. package/app/components/ui/scroll-area/ScrollArea.vue +0 -39
  185. package/app/components/ui/scroll-area/ScrollBar.vue +0 -33
  186. package/app/components/ui/scroll-area/index.ts +0 -2
  187. package/app/components/ui/separator/Separator.vue +0 -27
  188. package/app/components/ui/separator/index.ts +0 -1
  189. package/app/components/ui/sheet/Sheet.vue +0 -15
  190. package/app/components/ui/sheet/SheetContent.vue +0 -59
  191. package/app/components/ui/sheet/SheetDescription.vue +0 -21
  192. package/app/components/ui/sheet/SheetOverlay.vue +0 -26
  193. package/app/components/ui/sheet/SheetTitle.vue +0 -21
  194. package/app/components/ui/sheet/SheetTrigger.vue +0 -12
  195. package/app/components/ui/sheet/index.ts +0 -5
  196. package/app/components/ui/tabs/Tabs.vue +0 -24
  197. package/app/components/ui/tabs/TabsList.vue +0 -26
  198. package/app/components/ui/tabs/TabsTrigger.vue +0 -28
  199. package/app/components/ui/tabs/index.ts +0 -3
  200. package/app/composables/site-navigation.utils.ts +0 -133
  201. package/app/composables/useCanonicalUrl.ts +0 -9
  202. package/app/composables/useContentRouteAlternates.ts +0 -35
  203. package/app/composables/useGinkoAnalytics.ts +0 -52
  204. package/app/composables/useGinkoDocsConfig.ts +0 -6
  205. package/app/composables/useGinkoOgImage.ts +0 -20
  206. package/app/composables/useLocalizedPath.ts +0 -12
  207. package/app/composables/useLocalizedRouteSwitch.ts +0 -31
  208. package/app/composables/useMetaKey.ts +0 -18
  209. package/app/composables/usePageMarkdownActions.ts +0 -79
  210. package/app/composables/useProseAppearance.ts +0 -24
  211. package/app/composables/useSchemaJsonLd.ts +0 -25
  212. package/app/composables/useSiteNavigation.ts +0 -64
  213. package/app/config/site.utils.ts +0 -36
  214. package/app/features/docs/components/DocsBreadcrumb.vue +0 -37
  215. package/app/features/docs/components/DocsContributeLinks.vue +0 -75
  216. package/app/features/docs/components/DocsMobileToc.vue +0 -108
  217. package/app/features/docs/components/DocsPageContent.vue +0 -234
  218. package/app/features/docs/components/DocsPageNav.vue +0 -57
  219. package/app/features/docs/components/DocsSidebar.vue +0 -145
  220. package/app/features/docs/components/DocsSidebarDropdown.vue +0 -70
  221. package/app/features/docs/components/DocsSidebarItem.vue +0 -122
  222. package/app/features/docs/components/DocsSidebarList.vue +0 -35
  223. package/app/features/docs/components/DocsSidebarRow.vue +0 -27
  224. package/app/features/docs/components/DocsSidebarTabs.vue +0 -59
  225. package/app/features/docs/components/DocsToc.vue +0 -106
  226. package/app/features/docs/composables/useDocsNavigation.ts +0 -29
  227. package/app/features/docs/composables/useDocsNavigationData.ts +0 -18
  228. package/app/features/docs/composables/useRevealActive.ts +0 -58
  229. package/app/features/docs/composables/useScrollspy.ts +0 -135
  230. package/app/features/docs/docs-navigation.ts +0 -157
  231. package/app/features/docs/toc-context.ts +0 -6
  232. package/app/features/search/command-center.ts +0 -110
  233. package/app/features/search/components/SiteCommandCenter.vue +0 -359
  234. package/app/features/search/components/SiteSearchPageHighlight.vue +0 -126
  235. package/app/features/search/useCommandCenter.ts +0 -292
  236. package/app/layouts/blog.vue +0 -15
  237. package/app/layouts/docs.vue +0 -20
  238. package/app/lib/errors.ts +0 -37
  239. package/app/lib/schema-org.ts +0 -38
  240. package/app/pages/blog/[slug].vue +0 -228
  241. package/app/pages/blog/index.vue +0 -130
  242. package/app/pages/docs/[...slug].vue +0 -10
  243. package/app/pages/docs/index.vue +0 -19
  244. package/app/pages/index.vue +0 -278
  245. package/app/plugins/ginko-docs-theme.ts +0 -14
  246. package/app/plugins/ssr-width.ts +0 -6
  247. package/app/utils/content.ts +0 -67
  248. package/app/utils/file-icons.ts +0 -90
  249. package/app/utils/index.ts +0 -7
  250. package/app/utils/repository.ts +0 -20
  251. package/components.ts +0 -9
  252. package/content-collections.ts +0 -89
  253. package/content.js +0 -200
  254. package/content.ts +0 -104
  255. package/i18n/i18n.config.ts +0 -10
  256. package/i18n/locales.ts +0 -81
  257. package/i18n/messages/global/banner.ts +0 -8
  258. package/i18n/messages/global/blog.ts +0 -18
  259. package/i18n/messages/global/command.ts +0 -49
  260. package/i18n/messages/global/docs.ts +0 -36
  261. package/i18n/messages/global/errors.ts +0 -36
  262. package/i18n/messages/global/feedback.ts +0 -11
  263. package/i18n/messages/global/index.ts +0 -21
  264. package/i18n/messages/global/nav.ts +0 -26
  265. package/i18n/messages/global/site.ts +0 -4
  266. package/i18n/messages/global/theme.ts +0 -9
  267. package/i18n/messages/index.ts +0 -20
  268. package/i18n/messages/localize.ts +0 -35
  269. package/i18n/routes.ts +0 -33
  270. package/icon-bundle.ts +0 -116
  271. package/index.d.ts +0 -37
  272. package/modules/analytics-boundary.ts +0 -23
  273. package/modules/feature-routing.ts +0 -68
  274. package/modules/syntax-highlighting.ts +0 -120
  275. package/public/fonts/public-sans/LICENSE +0 -93
  276. package/public/fonts/public-sans/README.md +0 -12
  277. package/public/fonts/public-sans/public-sans-400-normal-latin.woff +0 -0
  278. package/public/fonts/public-sans/public-sans-500-normal-latin.woff +0 -0
  279. package/public/fonts/public-sans/public-sans-600-normal-latin.woff +0 -0
  280. package/public/fonts/public-sans/public-sans-700-normal-latin.woff +0 -0
  281. package/runtime/generate-favicons.mjs +0 -124
  282. package/runtime/server/agent-markdown.ts +0 -19
  283. package/server/api/_ginko-docs/redirects.get.ts +0 -28
  284. package/server/mcp/tools/get-page.ts +0 -14
  285. package/server/mcp/tools/list-pages.ts +0 -10
  286. package/server/middleware/redirects.ts +0 -22
  287. package/server/plugins/agent-markdown.ts +0 -6
  288. package/server/plugins/sitemap-policy.ts +0 -12
  289. package/server/routes/.well-known/ai-catalog.json.get.ts +0 -10
  290. package/server/routes/[locale]/blog/rss.xml.get.ts +0 -13
  291. package/server/routes/blog/rss.xml.get.ts +0 -5
  292. package/server/routes/mcp/server-card.get.ts +0 -14
  293. package/server/utils/blog-feed.ts +0 -51
  294. package/server/utils/feed.ts +0 -70
  295. package/server/utils/mcp-server-card.ts +0 -45
  296. package/server/utils/redirects.ts +0 -38
  297. package/server/utils/redirects.utils.ts +0 -58
  298. package/server/utils/sitemap.ts +0 -45
  299. package/shared/route-slugs.ts +0 -5
  300. package/shared/theme-palettes.ts +0 -37
  301. package/shared/theme-presets.ts +0 -3
  302. package/shared/types/app-config.ts +0 -172
  303. package/shared/types/nuxt-config.ts +0 -14
  304. package/tags.ts +0 -192
package/package.json CHANGED
@@ -1,103 +1,71 @@
1
1
  {
2
2
  "name": "@lupinum/ginko-docs",
3
- "version": "0.4.0-rc.9",
4
- "description": "A Nuxt documentation layer powered by Ginko Content.",
5
- "keywords": [
6
- "content",
7
- "documentation",
8
- "i18n",
9
- "markdown",
10
- "nuxt"
11
- ],
12
- "homepage": "https://ginko-docs.lupinum.com",
13
- "bugs": {
14
- "url": "https://github.com/lupinum-dev/ginko-docs/issues"
15
- },
3
+ "version": "0.5.0",
4
+ "type": "module",
5
+ "description": "Docs sites on Ginko: a Nuxt layer with navigation, search, blog, changelog and Markdown components.",
16
6
  "license": "MIT",
17
7
  "author": "Lupinum OG <info@lupinum.com> (https://lupinum.com)",
18
8
  "repository": {
19
9
  "type": "git",
20
- "url": "git+https://github.com/lupinum-dev/ginko-docs.git",
21
- "directory": "layer"
10
+ "url": "git+https://github.com/lupinum-dev/ginko.git",
11
+ "directory": "packages/ginko-docs"
12
+ },
13
+ "homepage": "https://github.com/lupinum-dev/ginko/tree/main/packages/ginko-docs#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/lupinum-dev/ginko/issues"
22
16
  },
23
- "bin": {
24
- "ginko-docs-favicons": "./runtime/generate-favicons.mjs"
17
+ "engines": {
18
+ "node": "^22.18.0 || ^24.0.0 || ^26.0.0"
25
19
  },
26
20
  "files": [
21
+ "nuxt.config.ts",
27
22
  "app",
28
- "i18n",
29
- "modules",
30
- "public",
31
- "runtime",
32
23
  "server",
33
24
  "shared",
34
- "content-collections.ts",
35
- "content.js",
36
- "content.ts",
37
- "components.ts",
38
- "icon-bundle.ts",
39
- "index.d.ts",
40
- "nuxt.config.ts",
41
- "tags.ts",
42
- "!**/*.test.ts"
25
+ "icons",
26
+ "public",
27
+ "i18n",
28
+ "dist",
29
+ "CHANGELOG.md"
43
30
  ],
44
- "type": "module",
45
31
  "main": "./nuxt.config.ts",
46
32
  "exports": {
47
33
  ".": "./nuxt.config.ts",
48
- "./content": {
49
- "types": "./content.ts",
50
- "import": "./content.js",
51
- "default": "./content.js"
34
+ "./config": {
35
+ "types": "./dist/config.d.mts",
36
+ "import": "./dist/config.mjs"
52
37
  },
53
- "./app-config": "./shared/types/app-config.ts",
54
- "./components": "./components.ts",
55
- "./nuxt-config": "./shared/types/nuxt-config.ts"
38
+ "./pages": "./dist/pages.mjs",
39
+ "./icons": "./dist/icons.mjs"
56
40
  },
57
41
  "publishConfig": {
58
42
  "access": "public"
59
43
  },
60
44
  "dependencies": {
61
- "@iconify-json/circle-flags": "^1.2.10",
62
- "@iconify-json/logos": "^1.2.12",
63
- "@iconify-json/lucide": "^1.2.123",
64
- "@nuxt/fonts": "^0.14.0",
65
- "@nuxt/icon": "^2.5.0",
66
- "@nuxt/image": "^2.1.0",
67
- "@nuxt/kit": "^4.5.2",
68
- "@nuxt/scripts": "^1.3.3",
69
- "@nuxtjs/color-mode": "^4.0.1",
70
- "@nuxtjs/i18n": "^10.6.0",
71
- "@nuxtjs/mcp-toolkit": "0.18.1",
72
- "@nuxtjs/robots": "^6.1.4",
73
- "@nuxtjs/sitemap": "^8.3.4",
74
- "@resvg/resvg-js": "^2.6.2",
75
- "@shikijs/transformers": "^4.4.3",
76
- "@tailwindcss/vite": "^4.3.3",
77
- "@vueuse/core": "^14.4.0",
78
- "class-variance-authority": "^0.7.1",
79
- "clsx": "^2.1.1",
80
- "h3": "1.15.11",
81
- "motion-v": "^2.3.0",
82
- "nitropack": "^2.13.4",
83
- "nuxt-og-image": "^6.7.8",
84
- "reka-ui": "^2.10.3",
85
- "satori": "^0.19.3",
86
- "sharp": "^0.35.3",
87
- "shiki": "^4.4.3",
88
- "tailwind-merge": "^3.6.0",
89
- "tailwindcss": "^4.3.3",
90
- "tw-animate-css": "^1.4.0",
91
- "zod": "4.4.3"
45
+ "@comark/vue": "0.7.0",
46
+ "@iconify-json/lucide": "^1.2.137",
47
+ "@iconify/utils": "^3.1.7",
48
+ "@lupinum/ginko-components": "^0.5.0",
49
+ "@nuxt/kit": "^4.6.0",
50
+ "beautiful-mermaid": "^1.1.3",
51
+ "katex": "^0.18.11"
92
52
  },
93
53
  "peerDependencies": {
94
- "@lupinum/ginko-content": ">=1.0.0-beta.5 <2.0.0",
95
- "nuxt": ">=4.5.1 <5",
96
- "vue": "^3.5.40",
97
- "vue-router": "^5.1.0"
54
+ "@lupinum/ginko": "^0.5.0",
55
+ "@lupinum/ginko-content": "^0.5.0",
56
+ "@lupinum/ginko-i18n": "^0.5.0",
57
+ "nuxt": "^4.6.0 || ^5.0.0-0",
58
+ "vue": "^3.5.0"
98
59
  },
99
- "engines": {
100
- "node": "^22.18.0 || ^24.11.0 || >=26.0.0"
60
+ "devDependencies": {
61
+ "@lupinum/ginko": "0.5.0",
62
+ "@lupinum/ginko-content": "0.5.0",
63
+ "@lupinum/ginko-i18n": "0.5.0",
64
+ "nuxt": "4.6.0",
65
+ "unbuild": "3.6.1",
66
+ "vue": "3.5.43"
101
67
  },
102
- "scripts": {}
68
+ "scripts": {
69
+ "build": "unbuild"
70
+ }
103
71
  }
@@ -0,0 +1,188 @@
1
+ Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
94
+
95
+ ---- JetBrains Mono ----
96
+ Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) JetBrainsMono-Italic[wght].ttf: Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
97
+
98
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
99
+ This license is copied below, and is also available with a FAQ at:
100
+ http://scripts.sil.org/OFL
101
+
102
+
103
+ -----------------------------------------------------------
104
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
105
+ -----------------------------------------------------------
106
+
107
+ PREAMBLE
108
+ The goals of the Open Font License (OFL) are to stimulate worldwide
109
+ development of collaborative font projects, to support the font creation
110
+ efforts of academic and linguistic communities, and to provide a free and
111
+ open framework in which fonts may be shared and improved in partnership
112
+ with others.
113
+
114
+ The OFL allows the licensed fonts to be used, studied, modified and
115
+ redistributed freely as long as they are not sold by themselves. The
116
+ fonts, including any derivative works, can be bundled, embedded,
117
+ redistributed and/or sold with any software provided that any reserved
118
+ names are not used by derivative works. The fonts and derivatives,
119
+ however, cannot be released under any other type of license. The
120
+ requirement for fonts to remain under this license does not apply
121
+ to any document created using the fonts or their derivatives.
122
+
123
+ DEFINITIONS
124
+ "Font Software" refers to the set of files released by the Copyright
125
+ Holder(s) under this license and clearly marked as such. This may
126
+ include source files, build scripts and documentation.
127
+
128
+ "Reserved Font Name" refers to any names specified as such after the
129
+ copyright statement(s).
130
+
131
+ "Original Version" refers to the collection of Font Software components as
132
+ distributed by the Copyright Holder(s).
133
+
134
+ "Modified Version" refers to any derivative made by adding to, deleting,
135
+ or substituting -- in part or in whole -- any of the components of the
136
+ Original Version, by changing formats or by porting the Font Software to a
137
+ new environment.
138
+
139
+ "Author" refers to any designer, engineer, programmer, technical
140
+ writer or other person who contributed to the Font Software.
141
+
142
+ PERMISSION & CONDITIONS
143
+ Permission is hereby granted, free of charge, to any person obtaining
144
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
145
+ redistribute, and sell modified and unmodified copies of the Font
146
+ Software, subject to the following conditions:
147
+
148
+ 1) Neither the Font Software nor any of its individual components,
149
+ in Original or Modified Versions, may be sold by itself.
150
+
151
+ 2) Original or Modified Versions of the Font Software may be bundled,
152
+ redistributed and/or sold with any software, provided that each copy
153
+ contains the above copyright notice and this license. These can be
154
+ included either as stand-alone text files, human-readable headers or
155
+ in the appropriate machine-readable metadata fields within text or
156
+ binary files as long as those fields can be easily viewed by the user.
157
+
158
+ 3) No Modified Version of the Font Software may use the Reserved Font
159
+ Name(s) unless explicit written permission is granted by the corresponding
160
+ Copyright Holder. This restriction only applies to the primary font name as
161
+ presented to the users.
162
+
163
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
164
+ Software shall not be used to promote, endorse or advertise any
165
+ Modified Version, except to acknowledge the contribution(s) of the
166
+ Copyright Holder(s) and the Author(s) or with their explicit written
167
+ permission.
168
+
169
+ 5) The Font Software, modified or unmodified, in part or in whole,
170
+ must be distributed entirely under this license, and must not be
171
+ distributed under any other license. The requirement for fonts to
172
+ remain under this license does not apply to any document created
173
+ using the Font Software.
174
+
175
+ TERMINATION
176
+ This license becomes null and void if any of the above conditions are
177
+ not met.
178
+
179
+ DISCLAIMER
180
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
181
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
182
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
183
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
184
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
185
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
186
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
187
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
188
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,9 @@
1
+ // The feed of another language: /<locale>/blog/rss.xml. The main language has none here (404), its feed has no prefix.
2
+ // Registered by modules/pages.ts only when the site has `posts`.
3
+ import { createError, defineEventHandler, getRouterParam, useRuntimeConfig } from 'nuxt/server'
4
+ import { serveBlogFeed } from '../lib/blogFeed'
5
+ export default defineEventHandler((event) => {
6
+ const locale = getRouterParam(event, 'locale') ?? ''
7
+ if (locale === (useRuntimeConfig().public.ginko as { mainLocale: string }).mainLocale) throw createError({ statusCode: 404, statusMessage: 'Not found' })
8
+ return serveBlogFeed(event, locale)
9
+ })
@@ -0,0 +1,4 @@
1
+ // The feed of the main language: /blog/rss.xml. Registered by modules/pages.ts only when the site has `posts`.
2
+ import { defineEventHandler, useRuntimeConfig } from 'nuxt/server'
3
+ import { serveBlogFeed } from '../lib/blogFeed'
4
+ export default defineEventHandler((event) => serveBlogFeed(event, (useRuntimeConfig().public.ginko as { mainLocale: string }).mainLocale))
@@ -0,0 +1,35 @@
1
+ // The blog feed of one language, read from the build-time file bundle (what /__ginko/files/<locale>.json serves).
2
+ // Only file blogs have a feed: posts that live on the platform are not in that bundle.
3
+ import { createError, useRuntimeConfig, type RequestEvent } from 'nuxt/server'
4
+ import schema from '#ginko/schema'
5
+ import fileBundles from '#ginko/files'
6
+ import locales from '#ginko/locales'
7
+ import type { Brick } from '@lupinum/ginko'
8
+ import { isVisible, todayIn } from '@lupinum/ginko/internal'
9
+ import { rssXml } from '@lupinum/ginko/basics'
10
+
11
+ export function serveBlogFeed(event: RequestEvent, locale: string) {
12
+ const config = useRuntimeConfig().public.ginko as { name: string; url: string; locales: string[]; timeZone: string; seo: { description?: string } }
13
+ const bundle = config.locales.includes(locale) ? fileBundles[locale as keyof typeof fileBundles] : undefined
14
+ const posts = bundle?.bricks.posts
15
+ const def = schema.bricks.posts as Brick | undefined
16
+ if (!bundle || posts?.kind !== 'collection' || def?.kind !== 'collection') throw createError({ statusCode: 404, statusMessage: 'No blog' })
17
+
18
+ const base = config.url.replace(/\/$/, '')
19
+ const authors = bundle.bricks.authors?.kind === 'collection' ? bundle.bricks.authors.items : []
20
+ const today = todayIn(config.timeZone)
21
+ const route = def.route?.[locale as keyof typeof def.route]
22
+ if (!route) throw createError({ statusCode: 404, statusMessage: 'No blog' })
23
+ const items = posts.items.filter((p) => p.urls?.[bundle.locale] && isVisible(def, p.data, today)).map((p) => ({
24
+ title: String(p.data.title),
25
+ link: base + p.urls![bundle.locale],
26
+ description: typeof p.data.description === 'string' ? p.data.description : undefined,
27
+ date: String(p.data.date),
28
+ authors: ((p.data.authors as string[] | undefined) ?? []).flatMap((id) => authors.find((a) => a.id === id)?.data.name ?? []).map(String),
29
+ }))
30
+ const page = base + route.replace('/[slug]', '')
31
+ // RSS <language> is the BCP 47 tag of the locale (`languages` in the config), like the page head.
32
+ const language = locales.locales.find((l) => l.code === locale)?.language ?? locale
33
+ event.res.headers.set('content-type', 'application/rss+xml; charset=utf-8')
34
+ return rssXml({ title: `${config.name} Blog`, link: page, self: `${page}/rss.xml`, description: config.seo.description ?? `${config.name} Blog`, language, items })
35
+ }
@@ -0,0 +1,108 @@
1
+ // Read-only MCP server for the docs, minimal MCP Streamable HTTP: one JSON-RPC request in, one JSON response out.
2
+ // Methods: initialize, ping, tools/list, tools/call (search_docs, read_doc). No sessions, no streaming, no auth:
3
+ // it only returns what the public site already shows. Pure (the data comes in through `deps`), so tests call it directly.
4
+
5
+ export interface DocsMcpHit { title: string, heading?: string, url: string, excerpt: string }
6
+ export interface DocsMcpDeps {
7
+ search: (query: string, locale?: string) => DocsMcpHit[] | Promise<DocsMcpHit[]>
8
+ /** Markdown of the page at `path`, or undefined when there is none. */
9
+ read: (path: string) => string | undefined | Promise<string | undefined>
10
+ }
11
+
12
+ const PROTOCOL = '2025-03-26'
13
+ const TOOLS = [
14
+ {
15
+ name: 'search_docs',
16
+ description: 'Search the documentation. Returns the best matching pages and sections with their path and an excerpt.',
17
+ inputSchema: { type: 'object', properties: { query: { type: 'string' }, locale: { type: 'string', description: 'Language code, e.g. "en". Default: the main language.' } }, required: ['query'] },
18
+ annotations: { readOnlyHint: true },
19
+ },
20
+ {
21
+ name: 'read_doc',
22
+ description: 'Read one documentation page as Markdown. `path` is the page path from search_docs, e.g. "/en/docs/languages".',
23
+ inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
24
+ annotations: { readOnlyHint: true },
25
+ },
26
+ ]
27
+
28
+ type Json = Record<string, unknown>
29
+ const ok = (id: unknown, result: unknown) => ({ jsonrpc: '2.0', id, result })
30
+ const fail = (id: unknown, code: number, message: string) => ({ jsonrpc: '2.0', id: id ?? null, error: { code, message } })
31
+ const text = (value: string, isError = false) => ({ content: [{ type: 'text', text: value }], ...(isError ? { isError: true } : {}) })
32
+
33
+ async function callTool(params: Json, deps: DocsMcpDeps) {
34
+ const args = (params.arguments ?? {}) as Json
35
+ if (params.name === 'search_docs') {
36
+ if (typeof args.query !== 'string' || !args.query.trim()) return text('`query` must be a non-empty string.', true)
37
+ const hits = await deps.search(args.query, typeof args.locale === 'string' ? args.locale : undefined)
38
+ return text(hits.length
39
+ ? hits.map((h) => `- ${h.heading ? `${h.title} > ${h.heading}` : h.title}: ${h.url}${h.excerpt ? `\n ${h.excerpt}` : ''}`).join('\n')
40
+ : 'No results.')
41
+ }
42
+ if (params.name === 'read_doc') {
43
+ // A page path of this site only: starts with "/", no scheme-relative "//", no "..", and no query or fragment,
44
+ // which would reach other routes (`/__ginko/search/en.json?`) through the `.md` lookup.
45
+ if (typeof args.path !== 'string' || !/^\/(?!\/)[^?#]*$/.test(args.path) || args.path.split('/').includes('..')) return text('`path` must be a page path like "/en/docs/languages".', true)
46
+ const markdown = await deps.read(args.path.replace(/\.md$/, ''))
47
+ return markdown === undefined ? text(`No page at ${args.path}.`, true) : text(markdown)
48
+ }
49
+ return undefined
50
+ }
51
+
52
+ /** One JSON-RPC message. `undefined` means "no response" (a notification). */
53
+ export async function handleMcpMessage(message: unknown, deps: DocsMcpDeps): Promise<Json | undefined> {
54
+ if (typeof message !== 'object' || message === null || Array.isArray(message)) return fail(null, -32600, 'Invalid request')
55
+ const { id, method, params } = message as { id?: unknown, method?: unknown, params?: Json }
56
+ if (typeof method !== 'string') return fail(id, -32600, 'Invalid request')
57
+ if (id === undefined) return undefined // notifications such as notifications/initialized
58
+ switch (method) {
59
+ case 'initialize':
60
+ return ok(id, { protocolVersion: PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: 'ginko-docs', version: '1' } })
61
+ case 'ping':
62
+ return ok(id, {})
63
+ case 'tools/list':
64
+ return ok(id, { tools: TOOLS })
65
+ case 'tools/call': {
66
+ const result = await callTool(params ?? {}, deps)
67
+ return result === undefined ? fail(id, -32602, `Unknown tool: ${String(params?.name)}`) : ok(id, result)
68
+ }
69
+ default:
70
+ return fail(id, -32601, `Method not found: ${method}`)
71
+ }
72
+ }
73
+
74
+ /** Messages in one batch; a client needs a handful, more only multiplies the work of one request. */
75
+ export const MAX_BATCH = 20
76
+
77
+ /** A request body: one message, or a batch of up to MAX_BATCH. */
78
+ export async function handleMcp(body: unknown, deps: DocsMcpDeps): Promise<Json | Json[] | undefined> {
79
+ if (!Array.isArray(body)) return handleMcpMessage(body, deps)
80
+ if (body.length === 0 || body.length > MAX_BATCH) return fail(null, -32600, `Invalid request: a batch holds 1 to ${MAX_BATCH} messages`)
81
+ const out = (await Promise.all(body.map((m) => handleMcpMessage(m, deps)))).filter((r): r is Json => r !== undefined)
82
+ return out.length ? out : undefined
83
+ }
84
+
85
+ /** Largest request body the endpoint reads (1 MB); a JSON-RPC call is a few hundred bytes. */
86
+ export const MAX_BODY_BYTES = 1_000_000
87
+
88
+ /**
89
+ * The body text of `request`, or undefined when it is over `max` bytes. A `content-length` over the limit is
90
+ * refused without reading anything; without one (chunked upload) the stream is cut as soon as it passes the limit.
91
+ */
92
+ export async function readLimitedText(request: Request, max = MAX_BODY_BYTES): Promise<string | undefined> {
93
+ const declared = Number(request.headers.get('content-length'))
94
+ if (Number.isFinite(declared) && declared > max) return undefined
95
+ if (!request.body) return ''
96
+ const reader = request.body.getReader()
97
+ const decoder = new TextDecoder()
98
+ let size = 0
99
+ let result = ''
100
+ for (;;) {
101
+ const { done, value } = await reader.read()
102
+ if (done) break
103
+ size += value.byteLength
104
+ if (size > max) { await reader.cancel(); return undefined }
105
+ result += decoder.decode(value, { stream: true })
106
+ }
107
+ return result + decoder.decode()
108
+ }
@@ -0,0 +1,35 @@
1
+ // `POST /__ginko/docs-mcp`: read-only MCP endpoint of the docs (see ../../mcp/handler.ts). It lives under `/__ginko/`
2
+ // like the other generated endpoints, so it cannot collide with a page or with the platform's own `/mcp`.
3
+ import { createError, defineEventHandler, serverFetch, setResponseStatus } from 'nuxt/server'
4
+ import indexes from '#ginko/search'
5
+ import locales from '#ginko/locales'
6
+ import { loadIndex, search, type SearchIndex } from '@lupinum/ginko/search'
7
+ import { handleMcp, readLimitedText } from '../../mcp/handler'
8
+
9
+ // The loading index per locale, kept as its promise so two first requests share one load.
10
+ const loaded = new Map<string, Promise<SearchIndex>>()
11
+
12
+ export default defineEventHandler(async (event) => {
13
+ if (event.req.method !== 'POST') throw createError({ statusCode: 405, statusMessage: 'POST only' })
14
+ // Over 1 MB: refuse before parsing (a plain Node host has no body limit of its own).
15
+ const raw = await readLimitedText(event.req)
16
+ if (raw === undefined) throw createError({ statusCode: 413, statusMessage: 'Request body too large (limit 1 MB)' })
17
+ let body: unknown
18
+ try { body = raw === '' ? undefined : JSON.parse(raw) } catch { throw createError({ statusCode: 400, statusMessage: 'Body must be JSON' }) }
19
+ const response = await handleMcp(body, {
20
+ async search(query, locale = locales.defaultLocale) {
21
+ const json = Object.hasOwn(indexes, locale) ? indexes[locale as keyof typeof indexes] : undefined
22
+ if (!json) return []
23
+ let index = loaded.get(locale)
24
+ if (!index) loaded.set(locale, index = loadIndex(json))
25
+ return search(await index, query, { limit: 8 })
26
+ },
27
+ // The same Markdown the public `<page>.md` twin returns: ask the site's own middleware, in-process (Nuxt 4.6 and 5).
28
+ async read(path) {
29
+ const response = await serverFetch(event, `${path === '/' ? '/index' : path.replace(/\/+$/, '')}.md`, { forwardHeaders: false }).catch(() => undefined)
30
+ return response?.ok ? response.text() : undefined
31
+ },
32
+ })
33
+ if (response === undefined) { setResponseStatus(event, 202); return '' }
34
+ return response
35
+ })
@@ -0,0 +1,66 @@
1
+ // Which pages and feeds the layer adds for a site, decided from the site's Ginko config and the paths its own pages
2
+ // already take. Pure, so modules/pages.ts only wires the result into Nuxt and the test can check every case.
3
+ export interface SiteOptions { locales?: readonly string[], pages?: Record<string, Record<string, string> | undefined>, bricks?: Record<string, { route?: Record<string, string> } | undefined> }
4
+
5
+ export interface LayerPage { name: string, path: string, /** In `app/docs-pages/`. */ file: string }
6
+
7
+ export interface LayerRoutes {
8
+ docs: { base: string }
9
+ /** Where the bricks put the blog list and the changelog (main language), whoever serves them. The app links here. */
10
+ addresses: { blog?: string, changelog?: string }
11
+ /** The pages the layer adds: each only where the site has no page of its own. */
12
+ pages: LayerPage[]
13
+ /** The blog and changelog the layer serves itself. Feeds are `<blog>/rss.xml` and `/:locale<blog>/rss.xml`. */
14
+ blog?: string
15
+ changelog?: string
16
+ /** A brick is configured but has no address to put a page at (the changelog brick without a route or `pages.changelog`). */
17
+ noAddress: string[]
18
+ }
19
+
20
+ /** The brick's address in the main language, with `[slug]` as `:slug()`; the list page is the parent path. */
21
+ function listPath(route: string | undefined): string | undefined {
22
+ if (!route) return undefined
23
+ const list = route.replace(/\/\[slug\]$/, '')
24
+ return list.includes('[') ? undefined : list || '/'
25
+ }
26
+
27
+ /** A page as Nuxt has it once every `pages:extend` ran: its scanned path and, from the templates' page-paths module, its addresses per language. */
28
+ export interface PageLike { path: string, meta?: { i18n?: false | { paths?: Record<string, string> } } | undefined }
29
+
30
+ /** The addresses a page answers at in the main language: its path and `meta.i18n.paths[main]` when set (`/news` answers at `/blog`). */
31
+ function addressesOf(page: PageLike, main: string): string[] {
32
+ const mapped = page.meta?.i18n && page.meta.i18n.paths?.[main]
33
+ return mapped ? [page.path, mapped] : [page.path]
34
+ }
35
+
36
+ export function layerRoutes(site: SiteOptions | undefined, pages: Iterable<PageLike>): LayerRoutes {
37
+ const bricks = site?.bricks ?? {}
38
+ const main = site?.locales?.[0] ?? ''
39
+ const taken = new Set<string>()
40
+ for (const page of pages) for (const path of addressesOf(page, main)) taken.add(path)
41
+ // docsCollection writes `<base>/[slug]` for the main language; other languages get a prefix from ginko-i18n.
42
+ const docsBase = bricks.docs?.route?.[main]?.replace(/\/\[slug\]$/, '') ?? '/docs'
43
+ const blog = bricks.posts ? listPath(bricks.posts.route?.[main]) : undefined
44
+ // The changelog has no page of its own per entry: its list lives at the route the brick has, else at `pages.changelog`.
45
+ const changelog = bricks.changelog ? listPath(bricks.changelog.route?.[main]) ?? site?.pages?.changelog?.[main] : undefined
46
+ const free = (path: string | undefined) => (path && !taken.has(path) ? path : undefined)
47
+ const noAddress = [...(bricks.posts && !blog ? ['posts'] : []), ...(bricks.changelog && !changelog ? ['changelog'] : [])]
48
+ const addedBlog = free(blog)
49
+ const addedChangelog = free(changelog)
50
+ const wanted: Array<LayerPage | undefined> = [
51
+ { name: 'docs-slug', path: `${docsBase}/:slug()`, file: 'doc.vue' },
52
+ { name: 'docs', path: docsBase, file: 'docs.vue' },
53
+ { name: 'index', path: '/', file: 'index.vue' },
54
+ addedBlog ? { name: 'blog', path: addedBlog, file: 'blog.vue' } : undefined,
55
+ addedBlog ? { name: 'blog-slug', path: `${addedBlog}/:slug()`, file: 'post.vue' } : undefined,
56
+ addedChangelog ? { name: 'changelog', path: addedChangelog, file: 'changelog.vue' } : undefined,
57
+ ]
58
+ return {
59
+ docs: { base: docsBase },
60
+ addresses: { ...(blog && { blog }), ...(changelog && { changelog }) },
61
+ pages: wanted.filter((page): page is LayerPage => !!page && !taken.has(page.path)),
62
+ ...(addedBlog && { blog: addedBlog }),
63
+ ...(addedChangelog && { changelog: addedChangelog }),
64
+ noAddress,
65
+ }
66
+ }
@@ -0,0 +1,4 @@
1
+ // The layer's fixed addresses, shared by the bricks (src/config.ts), the page module and the app. Internal: not
2
+ // exported from `@lupinum/ginko-docs/config`, so the paths can gain a `base` option later without a breaking change.
3
+ export const BLOG_PATH = '/blog'
4
+ export const CHANGELOG_PATH = '/changelog'