@lupinum/ginko-docs 0.2.2 → 0.2.5

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 (206) hide show
  1. package/README.md +130 -73
  2. package/app/app.config.ts +15 -13
  3. package/app/app.vue +4 -2
  4. package/app/assets/css/prose.css +2203 -149
  5. package/app/assets/css/tailwind.css +60 -38
  6. package/app/assets/css/theme-palettes.css +309 -0
  7. package/app/components/content/DocumentPageShell.vue +1 -1
  8. package/app/components/content/Feedback.vue +12 -11
  9. package/app/components/content/ImageZoomDialog.vue +84 -0
  10. package/app/components/content/PageMarkdownCopy.vue +2 -0
  11. package/app/components/content/imageZoom.ts +25 -0
  12. package/app/components/mdc/MdcAccordion.vue +6 -3
  13. package/app/components/mdc/MdcAccordionItem.vue +7 -16
  14. package/app/components/mdc/MdcApi.vue +159 -0
  15. package/app/components/mdc/MdcAside.vue +6 -10
  16. package/app/components/mdc/MdcCallout.vue +9 -10
  17. package/app/components/mdc/MdcCard.vue +36 -47
  18. package/app/components/mdc/MdcCards.vue +30 -0
  19. package/app/components/mdc/MdcCodeGroup.vue +4 -0
  20. package/app/components/mdc/MdcCodeTree.vue +234 -0
  21. package/app/components/mdc/MdcCollapse.vue +61 -0
  22. package/app/components/mdc/MdcError.vue +8 -1
  23. package/app/components/mdc/MdcExcerpt.vue +26 -0
  24. package/app/components/mdc/MdcFigure.vue +54 -20
  25. package/app/components/mdc/MdcFiles.vue +120 -0
  26. package/app/components/mdc/MdcIdea.vue +2 -1
  27. package/app/components/mdc/MdcInfo.vue +2 -1
  28. package/app/components/mdc/MdcInlineToc.vue +51 -0
  29. package/app/components/{ui/dialog/DialogHeader.vue → mdc/MdcKbd.vue} +2 -4
  30. package/app/components/mdc/MdcNote.vue +2 -1
  31. package/app/components/mdc/MdcQuiz.vue +43 -83
  32. package/app/components/mdc/MdcQuizQuestion.vue +104 -109
  33. package/app/components/mdc/MdcReadMore.vue +57 -77
  34. package/app/components/mdc/MdcSteps.vue +45 -11
  35. package/app/components/mdc/MdcSuccess.vue +8 -1
  36. package/app/components/mdc/MdcTabs.vue +44 -26
  37. package/app/components/mdc/MdcTimeline.vue +4 -1
  38. package/app/components/mdc/MdcTimelineItem.vue +18 -27
  39. package/app/components/mdc/MdcWarning.vue +8 -1
  40. package/app/components/mdc/api.utils.ts +132 -0
  41. package/app/components/mdc/code-tree.utils.ts +22 -0
  42. package/app/components/mdc/quiz.utils.ts +38 -0
  43. package/app/components/prose/ProseBlockquote.vue +17 -1
  44. package/app/components/prose/ProseH1.vue +1 -1
  45. package/app/components/prose/ProseH2.vue +7 -2
  46. package/app/components/prose/ProseH3.vue +7 -2
  47. package/app/components/prose/ProseH4.vue +17 -2
  48. package/app/components/{ui/kbd/KbdGroup.vue → prose/ProseH5.vue} +4 -3
  49. package/app/components/{ui/card/CardTitle.vue → prose/ProseH6.vue} +4 -3
  50. package/app/components/prose/ProseImg.vue +34 -99
  51. package/app/components/prose/ProsePre.vue +63 -100
  52. package/app/components/prose/ProseTable.vue +5 -3
  53. package/app/components/site/ModeToggle.vue +12 -3
  54. package/app/components/site/SiteBanner.vue +43 -2
  55. package/app/components/site/SiteFooter.vue +3 -5
  56. package/app/components/site/SiteHeader.vue +266 -132
  57. package/app/components/site/SiteHeroCode.vue +164 -0
  58. package/app/components/site/SiteLocaleSwitcher.vue +25 -9
  59. package/app/components/site/SiteSocialLinks.vue +31 -0
  60. package/app/components/ui/badge/Badge.vue +1 -1
  61. package/app/components/ui/button/Button.vue +1 -1
  62. package/app/components/ui/collapsible/CollapsibleContent.vue +1 -1
  63. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +14 -4
  64. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  65. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +1 -1
  66. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +1 -1
  67. package/app/components/ui/dropdown-menu/index.ts +0 -9
  68. package/app/components/ui/kbd/Kbd.vue +1 -1
  69. package/app/components/ui/kbd/index.ts +0 -1
  70. package/app/components/ui/scroll-area/ScrollArea.vue +1 -1
  71. package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
  72. package/app/components/ui/separator/Separator.vue +1 -1
  73. package/app/components/ui/sheet/SheetContent.vue +1 -1
  74. package/app/components/ui/sheet/SheetDescription.vue +1 -1
  75. package/app/components/ui/sheet/SheetOverlay.vue +1 -1
  76. package/app/components/ui/sheet/SheetTitle.vue +1 -1
  77. package/app/components/ui/sheet/index.ts +0 -3
  78. package/app/components/ui/tabs/Tabs.vue +1 -1
  79. package/app/components/ui/tabs/TabsList.vue +1 -1
  80. package/app/components/ui/tabs/TabsTrigger.vue +1 -1
  81. package/app/components/ui/tabs/index.ts +0 -1
  82. package/app/composables/site-navigation.utils.ts +69 -12
  83. package/app/composables/useContentRouteAlternates.ts +13 -21
  84. package/app/composables/useLocalizedRouteSwitch.ts +1 -1
  85. package/app/composables/useMetaKey.ts +18 -0
  86. package/app/composables/useProseAppearance.ts +24 -0
  87. package/app/composables/useSiteNavigation.ts +10 -40
  88. package/app/config/site.utils.ts +1 -1
  89. package/app/error.vue +1 -5
  90. package/app/features/docs/components/DocsBreadcrumb.vue +37 -0
  91. package/app/features/docs/components/DocsContributeLinks.vue +28 -18
  92. package/app/features/docs/components/DocsMobileToc.vue +12 -8
  93. package/app/features/docs/components/DocsPageContent.vue +93 -47
  94. package/app/features/docs/components/DocsPageNav.vue +1 -1
  95. package/app/features/docs/components/DocsSidebar.vue +37 -21
  96. package/app/features/docs/components/DocsSidebarItem.vue +53 -100
  97. package/app/features/docs/components/DocsSidebarRow.vue +27 -0
  98. package/app/features/docs/components/DocsSidebarTabs.vue +46 -22
  99. package/app/features/docs/components/DocsToc.vue +80 -34
  100. package/app/features/docs/composables/useDocsNavigation.ts +2 -2
  101. package/app/features/docs/composables/useDocsNavigationData.ts +4 -1
  102. package/app/features/docs/composables/useRevealActive.ts +58 -0
  103. package/app/features/docs/composables/useScrollspy.ts +87 -19
  104. package/app/features/docs/docs-navigation.ts +17 -30
  105. package/app/features/docs/toc-context.ts +6 -0
  106. package/app/features/search/command-center.ts +11 -90
  107. package/app/features/search/components/SiteCommandCenter.vue +58 -38
  108. package/app/features/search/useCommandCenter.ts +48 -48
  109. package/app/layouts/docs.vue +1 -1
  110. package/app/lib/errors.ts +2 -49
  111. package/app/pages/blog/[slug].vue +26 -9
  112. package/app/pages/blog/index.vue +38 -14
  113. package/app/pages/docs/index.vue +10 -14
  114. package/app/pages/index.vue +180 -61
  115. package/app/plugins/ginko-docs-theme.ts +12 -0
  116. package/app/utils/content.ts +11 -0
  117. package/app/utils/file-icons.ts +90 -0
  118. package/app/utils/index.ts +7 -1
  119. package/app/utils/repository.ts +20 -0
  120. package/content.js +68 -50
  121. package/content.ts +51 -32
  122. package/i18n/messages/global/blog.ts +4 -0
  123. package/i18n/messages/global/command.ts +6 -2
  124. package/i18n/messages/global/docs.ts +4 -0
  125. package/i18n/messages/global/nav.ts +6 -3
  126. package/i18n/messages/index.ts +3 -3
  127. package/icon-bundle.ts +9 -56
  128. package/index.d.ts +7 -1
  129. package/modules/feature-routing.ts +15 -0
  130. package/nuxt.config.ts +21 -7
  131. package/package.json +29 -26
  132. package/runtime/server/agent-markdown.ts +0 -2
  133. package/server/api/_ginko-docs/redirects.get.ts +28 -0
  134. package/server/middleware/redirects.ts +22 -0
  135. package/server/routes/[locale]/blog/rss.xml.get.ts +13 -0
  136. package/server/routes/blog/rss.xml.get.ts +5 -0
  137. package/server/utils/blog-feed.ts +49 -0
  138. package/server/utils/feed.ts +70 -0
  139. package/server/utils/redirects.ts +38 -0
  140. package/server/utils/redirects.utils.ts +58 -0
  141. package/shared/theme-palettes.ts +37 -0
  142. package/shared/types/app-config.ts +75 -10
  143. package/tags.ts +47 -59
  144. package/app/components/mdc/MdcAlert.vue +0 -34
  145. package/app/components/mdc/MdcCardGroup.vue +0 -47
  146. package/app/components/mdc/MdcDocImg.vue +0 -25
  147. package/app/components/mdc/MdcField.vue +0 -62
  148. package/app/components/mdc/MdcFieldGroup.vue +0 -18
  149. package/app/components/mdc/MdcPassage.vue +0 -32
  150. package/app/components/mdc/MdcQuizOption.vue +0 -91
  151. package/app/components/mdc/MdcReadMoreGroup.vue +0 -18
  152. package/app/components/mdc/MdcShortcut.vue +0 -46
  153. package/app/components/mdc/MdcStep.vue +0 -42
  154. package/app/components/ui/accordion/Accordion.vue +0 -15
  155. package/app/components/ui/accordion/AccordionContent.vue +0 -23
  156. package/app/components/ui/accordion/AccordionItem.vue +0 -24
  157. package/app/components/ui/accordion/AccordionTrigger.vue +0 -35
  158. package/app/components/ui/accordion/index.ts +0 -4
  159. package/app/components/ui/alert/Alert.vue +0 -21
  160. package/app/components/ui/alert/AlertDescription.vue +0 -22
  161. package/app/components/ui/alert/AlertTitle.vue +0 -22
  162. package/app/components/ui/alert/index.ts +0 -34
  163. package/app/components/ui/card/Card.vue +0 -16
  164. package/app/components/ui/card/CardAction.vue +0 -17
  165. package/app/components/ui/card/CardContent.vue +0 -14
  166. package/app/components/ui/card/CardDescription.vue +0 -14
  167. package/app/components/ui/card/CardFooter.vue +0 -14
  168. package/app/components/ui/card/CardHeader.vue +0 -22
  169. package/app/components/ui/card/card-variants.ts +0 -15
  170. package/app/components/ui/card/index.ts +0 -8
  171. package/app/components/ui/checkbox/Checkbox.vue +0 -35
  172. package/app/components/ui/checkbox/index.ts +0 -1
  173. package/app/components/ui/dialog/Dialog.vue +0 -15
  174. package/app/components/ui/dialog/DialogClose.vue +0 -12
  175. package/app/components/ui/dialog/DialogContent.vue +0 -69
  176. package/app/components/ui/dialog/DialogDescription.vue +0 -21
  177. package/app/components/ui/dialog/DialogFooter.vue +0 -15
  178. package/app/components/ui/dialog/DialogOverlay.vue +0 -26
  179. package/app/components/ui/dialog/DialogPortal.vue +0 -12
  180. package/app/components/ui/dialog/DialogTitle.vue +0 -21
  181. package/app/components/ui/dialog/DialogTrigger.vue +0 -12
  182. package/app/components/ui/dialog/index.ts +0 -10
  183. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +0 -36
  184. package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +0 -12
  185. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +0 -25
  186. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +0 -17
  187. package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +0 -15
  188. package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +0 -29
  189. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +0 -31
  190. package/app/components/ui/input/Input.vue +0 -23
  191. package/app/components/ui/input/index.ts +0 -1
  192. package/app/components/ui/label/Label.vue +0 -14
  193. package/app/components/ui/label/index.ts +0 -1
  194. package/app/components/ui/sheet/SheetClose.vue +0 -12
  195. package/app/components/ui/sheet/SheetFooter.vue +0 -12
  196. package/app/components/ui/sheet/SheetHeader.vue +0 -12
  197. package/app/components/ui/switch/Switch.vue +0 -39
  198. package/app/components/ui/switch/index.ts +0 -1
  199. package/app/components/ui/tabs/TabsContent.vue +0 -21
  200. package/app/components/ui/textarea/Textarea.vue +0 -31
  201. package/app/components/ui/textarea/index.ts +0 -1
  202. package/app/features/docs/composables/useDocsEntryPath.ts +0 -13
  203. package/app/lib/utils.ts +0 -7
  204. package/i18n/messages/source.ts +0 -5
  205. package/runtime/markdown/inline-code-highlight.ts +0 -155
  206. package/runtime/utils.ts +0 -7
package/content.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import {
2
- defineAgentAppPage,
3
2
  defineAgentMetadataFields,
4
3
  defineAgentSection,
5
4
  defineCollection,
@@ -10,10 +9,27 @@ import type { ContentCollectionConfig, ContentConfig } from "@lupinum/ginko-cont
10
9
  import { z } from "zod";
11
10
  import { routeSlugs } from "./shared/route-slugs";
12
11
 
12
+ const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected an ISO date (YYYY-MM-DD)");
13
+ const nonEmptyString = z.string().trim().min(1);
14
+ /** Former public URLs of a page, as served: locale prefix and translated slugs included. */
15
+ const redirectFrom = z
16
+ .array(nonEmptyString.regex(/^\//, "redirectFrom entries must be absolute site paths"))
17
+ .optional();
18
+
19
+ /**
20
+ * Derives the sitemap lastmod from the authored date so it has one source of
21
+ * truth. Route records require normalized UTC ISO values.
22
+ */
23
+ const withSitemapLastmod = <T extends object>(data: T, lastmod: string | undefined) =>
24
+ lastmod ? { ...data, sitemap: { lastmod: `${lastmod}T00:00:00.000Z` } } : data;
25
+
13
26
  export interface GinkoDocsContentOptions {
14
- site: { name: string; description: string; url: string };
15
- locales?: Array<"en" | "de">;
16
- defaultLocale?: "en" | "de";
27
+ site: {
28
+ name: string | { en: string; de: string };
29
+ description: string | { en: string; de: string };
30
+ url: string;
31
+ };
32
+ locales?: readonly ["en"] | readonly ["en", "de"];
17
33
  blog?: boolean;
18
34
  }
19
35
 
@@ -22,7 +38,8 @@ const docsSchema = z.object({
22
38
  description: z.string(),
23
39
  icon: z.string().optional(),
24
40
  badge: z.string().optional(),
25
- updated: z.string().optional(),
41
+ updated: isoDate.optional(),
42
+ redirectFrom,
26
43
  sidebar: z.enum(["section", "group"]).optional(),
27
44
  navigation: z
28
45
  .object({
@@ -33,15 +50,20 @@ const docsSchema = z.object({
33
50
  })
34
51
  .optional(),
35
52
  });
53
+ const docsSchemaWithLastmod = docsSchema.transform((data) =>
54
+ withSitemapLastmod(data, data.updated),
55
+ );
36
56
  const blogSchema = z.object({
37
57
  title: z.string(),
38
58
  description: z.string(),
39
59
  badge: z.string().optional(),
40
- date: z.string(),
41
- readingTime: z.string(),
60
+ date: isoDate,
61
+ readingTime: nonEmptyString,
42
62
  author: reference("authors"),
43
63
  image: z.string().optional(),
64
+ redirectFrom,
44
65
  });
66
+ const blogSchemaWithLastmod = blogSchema.transform((data) => withSitemapLastmod(data, data.date));
45
67
  const authorsSchema = z.object({
46
68
  slug: z.string(),
47
69
  name: z.string(),
@@ -51,8 +73,8 @@ const authorsSchema = z.object({
51
73
  links: z.array(z.object({ label: z.string(), href: z.string() })).optional(),
52
74
  });
53
75
 
54
- type DocsCollection = ContentCollectionConfig<typeof docsSchema>;
55
- type BlogCollection = ContentCollectionConfig<typeof blogSchema>;
76
+ type DocsCollection = ContentCollectionConfig<typeof docsSchemaWithLastmod>;
77
+ type BlogCollection = ContentCollectionConfig<typeof blogSchemaWithLastmod>;
56
78
  type AuthorsCollection = ContentCollectionConfig<typeof authorsSchema>;
57
79
  type DocsContentConfig = ContentConfig<{ docs: DocsCollection }>;
58
80
  type DocsBlogContentConfig = ContentConfig<{
@@ -69,10 +91,14 @@ export function defineGinkoDocsConfig(
69
91
  options: GinkoDocsContentOptions,
70
92
  ): DocsContentConfig | DocsBlogContentConfig {
71
93
  const locales = options.locales ?? ["en"];
72
- const defaultLocale = options.defaultLocale ?? locales[0] ?? "en";
73
- const i18n = locales.length > 1;
74
- const routeMap = (key: "docs" | "blog") =>
75
- Object.fromEntries(locales.map((locale) => [locale, routeSlugs[key][locale]]));
94
+ const validLocales =
95
+ locales.length === 1
96
+ ? locales[0] === "en"
97
+ : locales.length === 2 && locales[0] === "en" && locales[1] === "de";
98
+ if (!validLocales) {
99
+ throw new TypeError('locales must be exactly ["en"] or ["en", "de"]');
100
+ }
101
+ const i18n = locales.length === 2;
76
102
  const metadata = defineAgentMetadataFields([
77
103
  "title",
78
104
  "description",
@@ -88,19 +114,19 @@ export function defineGinkoDocsConfig(
88
114
  type: "page",
89
115
  source: i18n ? "{1.docs,1.dokumentation}/**/*.md" : "docs/**/*.md",
90
116
  i18n: i18n ? true : undefined,
91
- route: i18n ? routeMap("docs") : routeSlugs.docs[defaultLocale],
117
+ route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
92
118
  agent: { section: "optional", markdown: true },
93
119
  strict: true,
94
- schema: docsSchema,
120
+ schema: docsSchemaWithLastmod,
95
121
  });
96
122
  const blog = defineCollection({
97
123
  type: "page",
98
- source: "2.blog/**/*.md",
124
+ source: "2.blog/*.md",
99
125
  i18n: i18n ? true : undefined,
100
- route: i18n ? routeMap("blog") : routeSlugs.blog[defaultLocale],
126
+ route: i18n ? routeSlugs.blog : routeSlugs.blog.en,
101
127
  agent: { section: "blog", markdown: true },
102
128
  strict: true,
103
- schema: blogSchema,
129
+ schema: blogSchemaWithLastmod,
104
130
  });
105
131
  const authors = defineCollection({
106
132
  type: "data",
@@ -119,20 +145,13 @@ export function defineGinkoDocsConfig(
119
145
  },
120
146
  markdown: { metadata: { enabled: true, defaultFields: metadata } },
121
147
  sections: [
122
- ...(options.blog ? [defineAgentSection({ id: "blog", title: "Blog", order: 40 })] : []),
123
- defineAgentSection({ id: "optional", title: "Documentation", order: 100 }),
124
- ],
125
- pages: [
126
- defineAgentAppPage({
127
- id: "home",
128
- route: Object.fromEntries(
129
- locales.map((locale) => [locale, locale === defaultLocale ? "/" : `/${locale}`]),
130
- ),
131
- section: "optional",
132
- title: options.site.name,
133
- description: options.site.description,
134
- metadata,
135
- render: () => `# ${options.site.name}\n\n> ${options.site.description}`,
148
+ ...(options.blog
149
+ ? [defineAgentSection({ id: "blog", title: { en: "Blog", de: "Blog" }, order: 40 })]
150
+ : []),
151
+ defineAgentSection({
152
+ id: "optional",
153
+ title: { en: "Documentation", de: "Dokumentation" },
154
+ order: 100,
136
155
  }),
137
156
  ],
138
157
  },
@@ -11,4 +11,8 @@ export const blog = {
11
11
  toc: { de: "Im Artikel", en: "In this article" },
12
12
  readMore: { de: "Weiterlesen", en: "Read more" },
13
13
  moreArticles: { de: "Weitere Artikel", en: "More articles" },
14
+ empty: {
15
+ de: "Noch keine Artikel veröffentlicht.",
16
+ en: "No articles have been published yet.",
17
+ },
14
18
  } as const;
@@ -19,10 +19,14 @@ export const command = {
19
19
  recent: { de: "Zuletzt", en: "Recent" },
20
20
  pages: { de: "Navigation", en: "Navigation" },
21
21
  docs_nav: { de: "Docs-Navigation", en: "Docs Navigation" },
22
- blog: { de: "Blog-Ergebnisse", en: "Blog Results" },
23
- docs: { de: "Docs-Ergebnisse", en: "Docs Results" },
22
+ search: { de: "Suchergebnisse", en: "Search results" },
24
23
  actions: { de: "Aktionen", en: "Actions" },
25
24
  },
25
+ loading: { de: "Suche läuft …", en: "Searching…" },
26
+ error: {
27
+ de: "Die Suche ist gerade nicht verfügbar. Versuche es erneut.",
28
+ en: "Search is temporarily unavailable. Please try again.",
29
+ },
26
30
  pages: {
27
31
  home: {
28
32
  title: { de: "Start", en: "Home" },
@@ -14,7 +14,10 @@ export const docs = {
14
14
  copiedText: { de: "Text kopiert", en: "Copied text" },
15
15
  copyFailed: { de: "Kopieren fehlgeschlagen", en: "Copy failed" },
16
16
  codeSample: { de: "Codebeispiel", en: "Code sample" },
17
+ showAllLines: { de: "Alle {count} Zeilen anzeigen", en: "Show all {count} lines" },
18
+ showMore: { de: "Mehr anzeigen", en: "Show more" },
17
19
  zoomImage: { de: "Bild vergrößern", en: "Zoom image" },
20
+ lastUpdated: { de: "Zuletzt aktualisiert", en: "Last updated" },
18
21
  moreActions: { de: "Weitere Seitenaktionen", en: "More page actions" },
19
22
  copyLink: { de: "Markdown-Link kopieren", en: "Copy Markdown link" },
20
23
  viewMarkdown: { de: "Als Markdown anzeigen", en: "View as Markdown" },
@@ -22,6 +25,7 @@ export const docs = {
22
25
  askClaude: { de: "In Claude öffnen", en: "Open in Claude" },
23
26
  copyMcpUrl: { de: "MCP-Server-URL kopieren", en: "Copy MCP server URL" },
24
27
  contribute: { de: "Zu dieser Seite beitragen", en: "Contribute to this page" },
28
+ backToTop: { de: "Nach oben", en: "Back to top" },
25
29
  editPage: { de: "Diese Seite bearbeiten", en: "Edit this page" },
26
30
  reportIssue: { de: "Problem melden", en: "Report an issue" },
27
31
  issueTitle: { de: "Dokumentation: {title}", en: "Docs: {title}" },
@@ -3,8 +3,12 @@ export const nav = {
3
3
  home: { de: "Startseite", en: "Home" },
4
4
  main: { de: "Hauptnavigation", en: "Main navigation" },
5
5
  mobile: { de: "Mobile Navigation", en: "Mobile navigation" },
6
- navigationSection: { de: "Navigation", en: "Navigation" },
7
- settingsHelp: { de: "Einstellungen & Hilfe", en: "Settings & Help" },
6
+ menu: { de: "Menü", en: "Menu" },
7
+ docsDescription: {
8
+ de: "Anleitungen, Komponenten, Referenz",
9
+ en: "Guides, components, reference",
10
+ },
11
+ blogDescription: { de: "Releases und Artikel", en: "Releases and writing" },
8
12
  language: { de: "Sprache wechseln", en: "Change language" },
9
13
  openMenu: { de: "Menü öffnen", en: "Open menu" },
10
14
  closeMenu: { de: "Menü schließen", en: "Close menu" },
@@ -13,6 +17,5 @@ export const nav = {
13
17
  company: { de: "Unternehmen", en: "Company" },
14
18
  documentation: { de: "Dokumentation", en: "Documentation" },
15
19
  blog: { de: "Blog", en: "Blog" },
16
- github: { de: "GitHub", en: "GitHub" },
17
20
  externalLink: { de: "externer Link", en: "external link" },
18
21
  } as const;
@@ -1,19 +1,19 @@
1
1
  import { type LocaleCode, localeCodes } from "../locales";
2
2
  import { localizedRoutes } from "../routes";
3
+ import { globalMessages } from "./global";
3
4
  import { localizeMessages } from "./localize";
4
- import { messageSource } from "./source";
5
5
 
6
6
  export const messages = localeCodes.reduce(
7
7
  (localizedMessages, locale) => {
8
8
  localizedMessages[locale] = {
9
- ...localizeMessages(messageSource, locale),
9
+ ...localizeMessages(globalMessages, locale),
10
10
  routes: localizedRoutes[locale],
11
11
  };
12
12
  return localizedMessages;
13
13
  },
14
14
  {} as Record<
15
15
  LocaleCode,
16
- ReturnType<typeof localizeMessages<typeof messageSource>> & {
16
+ ReturnType<typeof localizeMessages<typeof globalMessages>> & {
17
17
  routes: (typeof localizedRoutes)[LocaleCode];
18
18
  }
19
19
  >,
package/icon-bundle.ts CHANGED
@@ -12,12 +12,14 @@ export const layerIconNames = [
12
12
  "logos:yarn",
13
13
  "lucide:arrow-left",
14
14
  "lucide:arrow-right",
15
+ "lucide:arrow-up",
15
16
  "lucide:arrow-up-right",
16
17
  "lucide:atom",
17
18
  "lucide:badge-check",
18
19
  "lucide:blocks",
19
20
  "lucide:book-open",
20
21
  "lucide:bookmark",
22
+ "lucide:calendar",
21
23
  "lucide:braces",
22
24
  "lucide:building-2",
23
25
  "lucide:check",
@@ -41,6 +43,8 @@ export const layerIconNames = [
41
43
  "lucide:file-json",
42
44
  "lucide:file-key",
43
45
  "lucide:file-text",
46
+ "lucide:folder",
47
+ "lucide:folder-open",
44
48
  "lucide:git-branch",
45
49
  "lucide:github",
46
50
  "lucide:hash",
@@ -49,6 +53,7 @@ export const layerIconNames = [
49
53
  "lucide:lightbulb",
50
54
  "lucide:link",
51
55
  "lucide:linkedin",
56
+ "lucide:loader-circle",
52
57
  "lucide:list",
53
58
  "lucide:menu",
54
59
  "lucide:message-circle",
@@ -74,68 +79,16 @@ export const layerIconNames = [
74
79
  "lucide:zap",
75
80
  ] as const;
76
81
 
77
- type IconTransform = {
78
- width?: number;
79
- height?: number;
80
- left?: number;
81
- top?: number;
82
- rotate?: number;
83
- hFlip?: boolean;
84
- vFlip?: boolean;
85
- };
86
- type IconData = IconTransform & { body: string };
87
- type IconAlias = IconTransform & { parent: string };
88
82
  type IconCollection = {
89
83
  prefix: string;
90
84
  width?: number;
91
85
  height?: number;
92
- icons: Record<string, IconData>;
93
- aliases?: Record<string, IconAlias>;
86
+ icons: Record<string, { body: string }>;
87
+ aliases?: Record<string, { parent: string }>;
94
88
  };
95
89
 
96
90
  const require = createRequire(import.meta.url);
97
- const sourceCollections = new Map<string, IconCollection>(
98
- ["circle-flags", "logos", "lucide"].map((prefix) => [
99
- prefix,
100
- require(`@iconify-json/${prefix}/icons.json`) as IconCollection,
101
- ]),
102
- );
103
-
104
- export const layerIconCollections: IconCollection[] = [...sourceCollections.values()].map(
105
- ({ prefix, width, height }) => ({ prefix, width, height, icons: {}, aliases: {} }),
106
- );
107
91
 
108
- const bundledCollections = new Map(
109
- layerIconCollections.map((collection) => [collection.prefix, collection]),
92
+ export const layerIconCollections = ["circle-flags", "logos", "lucide"].map(
93
+ (prefix) => require(`@iconify-json/${prefix}/icons.json`) as IconCollection,
110
94
  );
111
-
112
- function includeIcon(prefix: string, name: string, seen = new Set<string>()) {
113
- const key = `${prefix}:${name}`;
114
- if (seen.has(key)) return;
115
- seen.add(key);
116
-
117
- const source = sourceCollections.get(prefix);
118
- const target = bundledCollections.get(prefix);
119
- if (!source || !target) return;
120
-
121
- const icon = source.icons[name];
122
- if (icon) {
123
- target.icons[name] = icon;
124
- return;
125
- }
126
-
127
- const alias = source.aliases?.[name];
128
- if (!alias) return;
129
- target.aliases![name] = alias;
130
- includeIcon(prefix, alias.parent, seen);
131
- }
132
-
133
- export function includeIconNames(names: Iterable<string>) {
134
- for (const icon of names) {
135
- const separator = icon.indexOf(":");
136
- if (separator < 1) continue;
137
- includeIcon(icon.slice(0, separator), icon.slice(separator + 1));
138
- }
139
- }
140
-
141
- includeIconNames(layerIconNames);
package/index.d.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  import type { GinkoDocsAppConfig } from "./shared/types/app-config";
2
2
 
3
+ type GinkoDocsAppConfigInput<T> = T extends readonly unknown[]
4
+ ? T
5
+ : T extends object
6
+ ? { [Key in keyof T]?: GinkoDocsAppConfigInput<T[Key]> }
7
+ : T;
8
+
3
9
  declare module "nuxt/schema" {
4
10
  interface AppConfig {
5
11
  ginkoDocs: GinkoDocsAppConfig;
6
12
  }
7
13
 
8
14
  interface AppConfigInput {
9
- ginkoDocs?: Partial<GinkoDocsAppConfig>;
15
+ ginkoDocs?: GinkoDocsAppConfigInput<GinkoDocsAppConfig>;
10
16
  }
11
17
  }
12
18
 
@@ -1,10 +1,16 @@
1
1
  import { defineNuxtModule } from "@nuxt/kit";
2
2
  import type {} from "@lupinum/ginko-content";
3
+ import { localeCodes, localizedPath } from "../i18n/locales";
4
+ import { routeSlugs } from "../shared/route-slugs";
3
5
 
4
6
  interface PageRoute {
5
7
  path: string;
6
8
  }
7
9
 
10
+ export const blogFeedRoutes = localeCodes.map(
11
+ (locale) => `${localizedPath(locale, routeSlugs.blog[locale])}/rss.xml`,
12
+ );
13
+
8
14
  export function removeBlogPages(pages: PageRoute[], blogEnabled: boolean) {
9
15
  if (blogEnabled) return;
10
16
 
@@ -28,5 +34,14 @@ export default defineNuxtModule({
28
34
  nuxt.hook("pages:extend", (pages) => {
29
35
  removeBlogPages(pages, blogEnabled);
30
36
  });
37
+
38
+ // content:context runs before Nitro config is finalized, so the feed
39
+ // routes only prerender when the consuming app enables the blog.
40
+ nuxt.hook("nitro:config", (nitroConfig) => {
41
+ if (!blogEnabled) return;
42
+ nitroConfig.prerender ??= {};
43
+ nitroConfig.prerender.routes ??= [];
44
+ nitroConfig.prerender.routes.push(...blogFeedRoutes);
45
+ });
31
46
  },
32
47
  });
package/nuxt.config.ts CHANGED
@@ -6,7 +6,9 @@ import darkPlus from "shiki/dist/themes/dark-plus.mjs";
6
6
  import lightPlus from "shiki/dist/themes/light-plus.mjs";
7
7
  import { contentComponentPolicy, contentComponentTags } from "./tags";
8
8
  import { i18nPages } from "./i18n/routes";
9
- import { includeIconNames, layerIconCollections, layerIconNames } from "./icon-bundle";
9
+ import { localeCodes, localizedPath } from "./i18n/locales";
10
+ import { routeSlugs } from "./shared/route-slugs";
11
+ import { layerIconCollections, layerIconNames } from "./icon-bundle";
10
12
 
11
13
  const root = dirname(fileURLToPath(import.meta.url));
12
14
  const app = join(root, "app");
@@ -41,7 +43,7 @@ export default defineNuxtConfig({
41
43
  },
42
44
  mcp: {
43
45
  name: "Ginko Docs",
44
- version: "0.2.2",
46
+ version: "0.2.5",
45
47
  },
46
48
  components: {
47
49
  dirs: [
@@ -57,6 +59,7 @@ export default defineNuxtConfig({
57
59
  "SiteLocaleSwitcher.vue",
58
60
  "SiteLogoMark.vue",
59
61
  "SiteSkipLink.vue",
62
+ "SiteSocialLinks.vue",
60
63
  ],
61
64
  },
62
65
  {
@@ -69,7 +72,6 @@ export default defineNuxtConfig({
69
72
  { path: join(app, "components/prose"), pathPrefix: false, global: true },
70
73
  ],
71
74
  },
72
- imports: { autoImport: false },
73
75
  colorMode: { classSuffix: "" },
74
76
  icon: {
75
77
  provider: "none",
@@ -116,6 +118,9 @@ export default defineNuxtConfig({
116
118
  },
117
119
  content: {
118
120
  componentPolicy: contentComponentPolicy,
121
+ // Broken internal links and missing #anchors fail the build instead of
122
+ // only landing in the validation report.
123
+ validation: "error",
119
124
  i18n: {
120
125
  translatedSlugs: true,
121
126
  },
@@ -142,6 +147,9 @@ export default defineNuxtConfig({
142
147
  },
143
148
  sitemap: {
144
149
  excludeAppSources: ["nuxt:prerender"],
150
+ // The docs roots prerender as redirects to the first docs page; a sitemap
151
+ // must not list redirecting URLs.
152
+ exclude: localeCodes.map((locale) => localizedPath(locale, routeSlugs.docs[locale])),
145
153
  },
146
154
  app: {
147
155
  head: {
@@ -152,9 +160,6 @@ export default defineNuxtConfig({
152
160
  },
153
161
  },
154
162
  hooks: {
155
- "icon:clientBundleIcons"(icons) {
156
- includeIconNames(icons);
157
- },
158
163
  "components:dirs"(dirs) {
159
164
  const defaultComponentsDir = join(app, "components").replaceAll("\\", "/");
160
165
  const filtered = dirs.filter((entry) => {
@@ -172,7 +177,15 @@ export default defineNuxtConfig({
172
177
  concurrency: 1,
173
178
  crawlLinks: true,
174
179
  failOnError: true,
175
- routes: ["/llms.txt", "/llms-full.txt", "/sitemap.xml", "/robots.txt"],
180
+ routes: [
181
+ "/llms.txt",
182
+ "/llms-full.txt",
183
+ "/sitemap.xml",
184
+ "/robots.txt",
185
+ // Link page over every authored redirectFrom source; crawling it
186
+ // materializes the redirect stubs.
187
+ "/api/_ginko-docs/redirects",
188
+ ],
176
189
  },
177
190
  },
178
191
  vite: {
@@ -181,6 +194,7 @@ export default defineNuxtConfig({
181
194
  "@vueuse/core",
182
195
  "class-variance-authority",
183
196
  "clsx",
197
+ "motion-v",
184
198
  "reka-ui",
185
199
  "tailwind-merge",
186
200
  "zod",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lupinum/ginko-docs",
3
- "version": "0.2.2",
4
- "description": "A polished Nuxt documentation layer powered by Ginko Content.",
3
+ "version": "0.2.5",
4
+ "description": "A Nuxt documentation layer powered by Ginko Content.",
5
5
  "keywords": [
6
6
  "content",
7
7
  "documentation",
@@ -52,40 +52,43 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@iconify-json/circle-flags": "^1.2.10",
55
- "@iconify-json/logos": "^1.2.11",
56
- "@iconify-json/lucide": "^1.2.111",
55
+ "@iconify-json/logos": "^1.2.12",
56
+ "@iconify-json/lucide": "^1.2.123",
57
57
  "@nuxt/fonts": "^0.14.0",
58
- "@nuxt/icon": "^2.2.3",
59
- "@nuxt/image": "^2.0.0",
60
- "@nuxt/kit": "^4.4.6",
61
- "@nuxt/scripts": "^0.13.2",
62
- "@nuxtjs/color-mode": "^4.0.0",
63
- "@nuxtjs/i18n": "^10.4.0",
64
- "@nuxtjs/mcp-toolkit": "0.17.2",
65
- "@nuxtjs/robots": "^6.1.2",
66
- "@nuxtjs/sitemap": "^8.2.2",
67
- "@resvg/resvg-js": "^2.6.0",
68
- "@shikijs/transformers": "^4.2.0",
69
- "@tailwindcss/vite": "^4.3.0",
70
- "@vueuse/core": "^14.2.1",
58
+ "@nuxt/icon": "^2.5.0",
59
+ "@nuxt/image": "^2.1.0",
60
+ "@nuxt/kit": "^4.5.2",
61
+ "@nuxt/scripts": "^1.3.3",
62
+ "@nuxtjs/color-mode": "^4.0.1",
63
+ "@nuxtjs/i18n": "^10.6.0",
64
+ "@nuxtjs/mcp-toolkit": "0.18.1",
65
+ "@nuxtjs/robots": "^6.1.4",
66
+ "@nuxtjs/sitemap": "^8.3.4",
67
+ "@resvg/resvg-js": "^2.6.2",
68
+ "@shikijs/transformers": "^4.4.3",
69
+ "@tailwindcss/vite": "^4.3.3",
70
+ "@vueuse/core": "^14.4.0",
71
71
  "class-variance-authority": "^0.7.1",
72
72
  "clsx": "^2.1.1",
73
- "h3": "^1.15.4",
73
+ "motion-v": "^2.3.0",
74
74
  "nitropack": "^2.13.4",
75
- "nuxt-og-image": "^6.7.2",
76
- "reka-ui": "^2.9.2",
77
- "satori": "^0.19.2",
78
- "shiki": "^4.0.2",
79
- "tailwind-merge": "^3.5.0",
80
- "tailwindcss": "^4.2.2",
75
+ "nuxt-og-image": "^6.7.7",
76
+ "reka-ui": "^2.10.3",
77
+ "satori": "^0.29.0",
78
+ "shiki": "^4.4.3",
79
+ "tailwind-merge": "^3.6.0",
80
+ "tailwindcss": "^4.3.3",
81
81
  "tw-animate-css": "^1.4.0",
82
82
  "zod": "^4.4.3"
83
83
  },
84
84
  "peerDependencies": {
85
- "@lupinum/ginko-content": ">=0.3.0-rc.2 <0.4.0",
86
- "nuxt": "^4.4.6",
85
+ "@lupinum/ginko-content": ">=0.3.5 <0.4.0",
86
+ "nuxt": ">=4.4.7 <5",
87
87
  "vue": "^3.5.35",
88
88
  "vue-router": "^5.1.0"
89
89
  },
90
+ "engines": {
91
+ "node": "^22.18.0 || ^24.11.0 || >=26.0.0"
92
+ },
90
93
  "scripts": {}
91
94
  }
@@ -8,8 +8,6 @@ import { contentComponentTags } from "../../tags";
8
8
  const renderXmlComponent: AgentMarkdownSerializer = (node, ctx) =>
9
9
  ctx.xmlComponent(node.tag || "component", ctx.cleanProps(node), ctx.renderChildren(node));
10
10
 
11
- export const contentComponentAgentMarkdownTags = Object.keys(contentComponentTags);
12
-
13
11
  export function registerContentComponentAgentMarkdownSerializers() {
14
12
  const serializers: AgentMarkdownSerializerMap = {};
15
13
  for (const [tag, component] of Object.entries(contentComponentTags)) {
@@ -0,0 +1,28 @@
1
+ import { defineEventHandler, setHeader } from "h3";
2
+ import { loadRedirectMap } from "../../utils/redirects";
3
+
4
+ const escapeHtml = (value: string) =>
5
+ value
6
+ .replaceAll("&", "&amp;")
7
+ .replaceAll("<", "&lt;")
8
+ .replaceAll(">", "&gt;")
9
+ .replaceAll('"', "&quot;");
10
+
11
+ /**
12
+ * Prerender seed: returns a link page of every `redirectFrom` source so the
13
+ * crawler visits each old path and materializes its redirect stub. Conflict
14
+ * validation throws here, which fails the build via `failOnError`.
15
+ */
16
+ export default defineEventHandler(async (event) => {
17
+ const redirects = await loadRedirectMap(event);
18
+
19
+ if (import.meta.prerender) {
20
+ setHeader(event, "content-type", "text/html; charset=utf-8");
21
+ const links = Array.from(redirects.keys())
22
+ .map((source) => `<a href="${escapeHtml(source)}"></a>`)
23
+ .join("");
24
+ return `<!doctype html><html><head><meta charset="utf-8"></head><body>${links}</body></html>`;
25
+ }
26
+
27
+ return { count: redirects.size, redirects: Object.fromEntries(redirects) };
28
+ });
@@ -0,0 +1,22 @@
1
+ import { defineEventHandler, sendRedirect } from "h3";
2
+ import { loadRedirectMap } from "../utils/redirects";
3
+ import { normalizeRedirectPath } from "../utils/redirects.utils";
4
+
5
+ /**
6
+ * Serves authored `redirectFrom` moves as 301s. During prerender this runs
7
+ * before the catch-all page would SSR a 404 for the crawled old path, so the
8
+ * static build materializes the same meta-refresh stub the docs root uses.
9
+ */
10
+ export default defineEventHandler(async (event) => {
11
+ if (event.method !== "GET" && event.method !== "HEAD") return;
12
+
13
+ const path = normalizeRedirectPath(event.path);
14
+ const lastSegment = path.slice(path.lastIndexOf("/") + 1);
15
+ if (path.startsWith("/api/") || path.startsWith("/_") || lastSegment.includes(".")) return;
16
+
17
+ const redirects = await loadRedirectMap(event);
18
+ const target = redirects.get(path);
19
+ if (target) {
20
+ return sendRedirect(event, target, 301);
21
+ }
22
+ });
@@ -0,0 +1,13 @@
1
+ import { createError, defineEventHandler, getRouterParam } from "h3";
2
+ import { defaultLocale, isLocaleCode } from "../../../../i18n/locales";
3
+ import { serveBlogFeed } from "../../../utils/blog-feed";
4
+
5
+ // A param route keeps the router free of static locale segments; a static
6
+ // /de/... node would shadow ginko-content's /:locale/llms.txt routes.
7
+ export default defineEventHandler(async (event) => {
8
+ const locale = getRouterParam(event, "locale");
9
+ if (!locale || !isLocaleCode(locale) || locale === defaultLocale) {
10
+ throw createError({ statusCode: 404, statusMessage: "Page not found" });
11
+ }
12
+ return serveBlogFeed(event, locale);
13
+ });
@@ -0,0 +1,5 @@
1
+ import { defineEventHandler } from "h3";
2
+ import { defaultLocale } from "../../../i18n/locales";
3
+ import { serveBlogFeed } from "../../utils/blog-feed";
4
+
5
+ export default defineEventHandler((event) => serveBlogFeed(event, defaultLocale));