@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.
- package/README.md +130 -73
- package/app/app.config.ts +15 -13
- package/app/app.vue +4 -2
- package/app/assets/css/prose.css +2203 -149
- package/app/assets/css/tailwind.css +60 -38
- package/app/assets/css/theme-palettes.css +309 -0
- package/app/components/content/DocumentPageShell.vue +1 -1
- package/app/components/content/Feedback.vue +12 -11
- package/app/components/content/ImageZoomDialog.vue +84 -0
- package/app/components/content/PageMarkdownCopy.vue +2 -0
- package/app/components/content/imageZoom.ts +25 -0
- package/app/components/mdc/MdcAccordion.vue +6 -3
- package/app/components/mdc/MdcAccordionItem.vue +7 -16
- package/app/components/mdc/MdcApi.vue +159 -0
- package/app/components/mdc/MdcAside.vue +6 -10
- package/app/components/mdc/MdcCallout.vue +9 -10
- package/app/components/mdc/MdcCard.vue +36 -47
- package/app/components/mdc/MdcCards.vue +30 -0
- package/app/components/mdc/MdcCodeGroup.vue +4 -0
- package/app/components/mdc/MdcCodeTree.vue +234 -0
- package/app/components/mdc/MdcCollapse.vue +61 -0
- package/app/components/mdc/MdcError.vue +8 -1
- package/app/components/mdc/MdcExcerpt.vue +26 -0
- package/app/components/mdc/MdcFigure.vue +54 -20
- package/app/components/mdc/MdcFiles.vue +120 -0
- package/app/components/mdc/MdcIdea.vue +2 -1
- package/app/components/mdc/MdcInfo.vue +2 -1
- package/app/components/mdc/MdcInlineToc.vue +51 -0
- package/app/components/{ui/dialog/DialogHeader.vue → mdc/MdcKbd.vue} +2 -4
- package/app/components/mdc/MdcNote.vue +2 -1
- package/app/components/mdc/MdcQuiz.vue +43 -83
- package/app/components/mdc/MdcQuizQuestion.vue +104 -109
- package/app/components/mdc/MdcReadMore.vue +57 -77
- package/app/components/mdc/MdcSteps.vue +45 -11
- package/app/components/mdc/MdcSuccess.vue +8 -1
- package/app/components/mdc/MdcTabs.vue +44 -26
- package/app/components/mdc/MdcTimeline.vue +4 -1
- package/app/components/mdc/MdcTimelineItem.vue +18 -27
- package/app/components/mdc/MdcWarning.vue +8 -1
- package/app/components/mdc/api.utils.ts +132 -0
- package/app/components/mdc/code-tree.utils.ts +22 -0
- package/app/components/mdc/quiz.utils.ts +38 -0
- package/app/components/prose/ProseBlockquote.vue +17 -1
- package/app/components/prose/ProseH1.vue +1 -1
- package/app/components/prose/ProseH2.vue +7 -2
- package/app/components/prose/ProseH3.vue +7 -2
- package/app/components/prose/ProseH4.vue +17 -2
- package/app/components/{ui/kbd/KbdGroup.vue → prose/ProseH5.vue} +4 -3
- package/app/components/{ui/card/CardTitle.vue → prose/ProseH6.vue} +4 -3
- package/app/components/prose/ProseImg.vue +34 -99
- package/app/components/prose/ProsePre.vue +63 -100
- package/app/components/prose/ProseTable.vue +5 -3
- package/app/components/site/ModeToggle.vue +12 -3
- package/app/components/site/SiteBanner.vue +43 -2
- package/app/components/site/SiteFooter.vue +3 -5
- package/app/components/site/SiteHeader.vue +266 -132
- package/app/components/site/SiteHeroCode.vue +164 -0
- package/app/components/site/SiteLocaleSwitcher.vue +25 -9
- package/app/components/site/SiteSocialLinks.vue +31 -0
- package/app/components/ui/badge/Badge.vue +1 -1
- package/app/components/ui/button/Button.vue +1 -1
- package/app/components/ui/collapsible/CollapsibleContent.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +14 -4
- package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +1 -1
- package/app/components/ui/dropdown-menu/index.ts +0 -9
- package/app/components/ui/kbd/Kbd.vue +1 -1
- package/app/components/ui/kbd/index.ts +0 -1
- package/app/components/ui/scroll-area/ScrollArea.vue +1 -1
- package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
- package/app/components/ui/separator/Separator.vue +1 -1
- package/app/components/ui/sheet/SheetContent.vue +1 -1
- package/app/components/ui/sheet/SheetDescription.vue +1 -1
- package/app/components/ui/sheet/SheetOverlay.vue +1 -1
- package/app/components/ui/sheet/SheetTitle.vue +1 -1
- package/app/components/ui/sheet/index.ts +0 -3
- package/app/components/ui/tabs/Tabs.vue +1 -1
- package/app/components/ui/tabs/TabsList.vue +1 -1
- package/app/components/ui/tabs/TabsTrigger.vue +1 -1
- package/app/components/ui/tabs/index.ts +0 -1
- package/app/composables/site-navigation.utils.ts +69 -12
- package/app/composables/useContentRouteAlternates.ts +13 -21
- package/app/composables/useLocalizedRouteSwitch.ts +1 -1
- package/app/composables/useMetaKey.ts +18 -0
- package/app/composables/useProseAppearance.ts +24 -0
- package/app/composables/useSiteNavigation.ts +10 -40
- package/app/config/site.utils.ts +1 -1
- package/app/error.vue +1 -5
- package/app/features/docs/components/DocsBreadcrumb.vue +37 -0
- package/app/features/docs/components/DocsContributeLinks.vue +28 -18
- package/app/features/docs/components/DocsMobileToc.vue +12 -8
- package/app/features/docs/components/DocsPageContent.vue +93 -47
- package/app/features/docs/components/DocsPageNav.vue +1 -1
- package/app/features/docs/components/DocsSidebar.vue +37 -21
- package/app/features/docs/components/DocsSidebarItem.vue +53 -100
- package/app/features/docs/components/DocsSidebarRow.vue +27 -0
- package/app/features/docs/components/DocsSidebarTabs.vue +46 -22
- package/app/features/docs/components/DocsToc.vue +80 -34
- package/app/features/docs/composables/useDocsNavigation.ts +2 -2
- package/app/features/docs/composables/useDocsNavigationData.ts +4 -1
- package/app/features/docs/composables/useRevealActive.ts +58 -0
- package/app/features/docs/composables/useScrollspy.ts +87 -19
- package/app/features/docs/docs-navigation.ts +17 -30
- package/app/features/docs/toc-context.ts +6 -0
- package/app/features/search/command-center.ts +11 -90
- package/app/features/search/components/SiteCommandCenter.vue +58 -38
- package/app/features/search/useCommandCenter.ts +48 -48
- package/app/layouts/docs.vue +1 -1
- package/app/lib/errors.ts +2 -49
- package/app/pages/blog/[slug].vue +26 -9
- package/app/pages/blog/index.vue +38 -14
- package/app/pages/docs/index.vue +10 -14
- package/app/pages/index.vue +180 -61
- package/app/plugins/ginko-docs-theme.ts +12 -0
- package/app/utils/content.ts +11 -0
- package/app/utils/file-icons.ts +90 -0
- package/app/utils/index.ts +7 -1
- package/app/utils/repository.ts +20 -0
- package/content.js +68 -50
- package/content.ts +51 -32
- package/i18n/messages/global/blog.ts +4 -0
- package/i18n/messages/global/command.ts +6 -2
- package/i18n/messages/global/docs.ts +4 -0
- package/i18n/messages/global/nav.ts +6 -3
- package/i18n/messages/index.ts +3 -3
- package/icon-bundle.ts +9 -56
- package/index.d.ts +7 -1
- package/modules/feature-routing.ts +15 -0
- package/nuxt.config.ts +21 -7
- package/package.json +29 -26
- package/runtime/server/agent-markdown.ts +0 -2
- package/server/api/_ginko-docs/redirects.get.ts +28 -0
- package/server/middleware/redirects.ts +22 -0
- package/server/routes/[locale]/blog/rss.xml.get.ts +13 -0
- package/server/routes/blog/rss.xml.get.ts +5 -0
- package/server/utils/blog-feed.ts +49 -0
- package/server/utils/feed.ts +70 -0
- package/server/utils/redirects.ts +38 -0
- package/server/utils/redirects.utils.ts +58 -0
- package/shared/theme-palettes.ts +37 -0
- package/shared/types/app-config.ts +75 -10
- package/tags.ts +47 -59
- package/app/components/mdc/MdcAlert.vue +0 -34
- package/app/components/mdc/MdcCardGroup.vue +0 -47
- package/app/components/mdc/MdcDocImg.vue +0 -25
- package/app/components/mdc/MdcField.vue +0 -62
- package/app/components/mdc/MdcFieldGroup.vue +0 -18
- package/app/components/mdc/MdcPassage.vue +0 -32
- package/app/components/mdc/MdcQuizOption.vue +0 -91
- package/app/components/mdc/MdcReadMoreGroup.vue +0 -18
- package/app/components/mdc/MdcShortcut.vue +0 -46
- package/app/components/mdc/MdcStep.vue +0 -42
- package/app/components/ui/accordion/Accordion.vue +0 -15
- package/app/components/ui/accordion/AccordionContent.vue +0 -23
- package/app/components/ui/accordion/AccordionItem.vue +0 -24
- package/app/components/ui/accordion/AccordionTrigger.vue +0 -35
- package/app/components/ui/accordion/index.ts +0 -4
- package/app/components/ui/alert/Alert.vue +0 -21
- package/app/components/ui/alert/AlertDescription.vue +0 -22
- package/app/components/ui/alert/AlertTitle.vue +0 -22
- package/app/components/ui/alert/index.ts +0 -34
- package/app/components/ui/card/Card.vue +0 -16
- package/app/components/ui/card/CardAction.vue +0 -17
- package/app/components/ui/card/CardContent.vue +0 -14
- package/app/components/ui/card/CardDescription.vue +0 -14
- package/app/components/ui/card/CardFooter.vue +0 -14
- package/app/components/ui/card/CardHeader.vue +0 -22
- package/app/components/ui/card/card-variants.ts +0 -15
- package/app/components/ui/card/index.ts +0 -8
- package/app/components/ui/checkbox/Checkbox.vue +0 -35
- package/app/components/ui/checkbox/index.ts +0 -1
- package/app/components/ui/dialog/Dialog.vue +0 -15
- package/app/components/ui/dialog/DialogClose.vue +0 -12
- package/app/components/ui/dialog/DialogContent.vue +0 -69
- package/app/components/ui/dialog/DialogDescription.vue +0 -21
- package/app/components/ui/dialog/DialogFooter.vue +0 -15
- package/app/components/ui/dialog/DialogOverlay.vue +0 -26
- package/app/components/ui/dialog/DialogPortal.vue +0 -12
- package/app/components/ui/dialog/DialogTitle.vue +0 -21
- package/app/components/ui/dialog/DialogTrigger.vue +0 -12
- package/app/components/ui/dialog/index.ts +0 -10
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +0 -36
- package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +0 -12
- package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +0 -25
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +0 -17
- package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +0 -15
- package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +0 -29
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +0 -31
- package/app/components/ui/input/Input.vue +0 -23
- package/app/components/ui/input/index.ts +0 -1
- package/app/components/ui/label/Label.vue +0 -14
- package/app/components/ui/label/index.ts +0 -1
- package/app/components/ui/sheet/SheetClose.vue +0 -12
- package/app/components/ui/sheet/SheetFooter.vue +0 -12
- package/app/components/ui/sheet/SheetHeader.vue +0 -12
- package/app/components/ui/switch/Switch.vue +0 -39
- package/app/components/ui/switch/index.ts +0 -1
- package/app/components/ui/tabs/TabsContent.vue +0 -21
- package/app/components/ui/textarea/Textarea.vue +0 -31
- package/app/components/ui/textarea/index.ts +0 -1
- package/app/features/docs/composables/useDocsEntryPath.ts +0 -13
- package/app/lib/utils.ts +0 -7
- package/i18n/messages/source.ts +0 -5
- package/runtime/markdown/inline-code-highlight.ts +0 -155
- 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: {
|
|
15
|
-
|
|
16
|
-
|
|
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:
|
|
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:
|
|
41
|
-
readingTime:
|
|
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
|
|
55
|
-
type BlogCollection = ContentCollectionConfig<typeof
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 ?
|
|
117
|
+
route: i18n ? routeSlugs.docs : routeSlugs.docs.en,
|
|
92
118
|
agent: { section: "optional", markdown: true },
|
|
93
119
|
strict: true,
|
|
94
|
-
schema:
|
|
120
|
+
schema: docsSchemaWithLastmod,
|
|
95
121
|
});
|
|
96
122
|
const blog = defineCollection({
|
|
97
123
|
type: "page",
|
|
98
|
-
source: "2.blog
|
|
124
|
+
source: "2.blog/*.md",
|
|
99
125
|
i18n: i18n ? true : undefined,
|
|
100
|
-
route: i18n ?
|
|
126
|
+
route: i18n ? routeSlugs.blog : routeSlugs.blog.en,
|
|
101
127
|
agent: { section: "blog", markdown: true },
|
|
102
128
|
strict: true,
|
|
103
|
-
schema:
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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;
|
package/i18n/messages/index.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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,
|
|
93
|
-
aliases?: Record<string,
|
|
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
|
|
109
|
-
|
|
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?:
|
|
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 {
|
|
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.
|
|
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: [
|
|
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.
|
|
4
|
-
"description": "A
|
|
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.
|
|
56
|
-
"@iconify-json/lucide": "^1.2.
|
|
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.
|
|
59
|
-
"@nuxt/image": "^2.
|
|
60
|
-
"@nuxt/kit": "^4.
|
|
61
|
-
"@nuxt/scripts": "^
|
|
62
|
-
"@nuxtjs/color-mode": "^4.0.
|
|
63
|
-
"@nuxtjs/i18n": "^10.
|
|
64
|
-
"@nuxtjs/mcp-toolkit": "0.
|
|
65
|
-
"@nuxtjs/robots": "^6.1.
|
|
66
|
-
"@nuxtjs/sitemap": "^8.
|
|
67
|
-
"@resvg/resvg-js": "^2.6.
|
|
68
|
-
"@shikijs/transformers": "^4.
|
|
69
|
-
"@tailwindcss/vite": "^4.3.
|
|
70
|
-
"@vueuse/core": "^14.
|
|
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
|
-
"
|
|
73
|
+
"motion-v": "^2.3.0",
|
|
74
74
|
"nitropack": "^2.13.4",
|
|
75
|
-
"nuxt-og-image": "^6.7.
|
|
76
|
-
"reka-ui": "^2.
|
|
77
|
-
"satori": "^0.
|
|
78
|
-
"shiki": "^4.
|
|
79
|
-
"tailwind-merge": "^3.
|
|
80
|
-
"tailwindcss": "^4.
|
|
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.
|
|
86
|
-
"nuxt": "
|
|
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("&", "&")
|
|
7
|
+
.replaceAll("<", "<")
|
|
8
|
+
.replaceAll(">", ">")
|
|
9
|
+
.replaceAll('"', """);
|
|
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
|
+
});
|