@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
|
@@ -8,10 +8,8 @@ import {
|
|
|
8
8
|
getDocsNavigationGroups,
|
|
9
9
|
type DocsNavigationItem,
|
|
10
10
|
} from "#ginko-docs/features/docs/docs-navigation";
|
|
11
|
-
import { useDocsEntryPath } from "#ginko-docs/features/docs/composables/useDocsEntryPath";
|
|
12
11
|
import { useDocsNavigation } from "#ginko-docs/features/docs/composables/useDocsNavigation";
|
|
13
12
|
import {
|
|
14
|
-
contentSearchGroup,
|
|
15
13
|
dedupeCommandCenterItems,
|
|
16
14
|
groupCommandCenterItems,
|
|
17
15
|
PAGE_HIGHLIGHT_STORAGE_KEY,
|
|
@@ -21,14 +19,6 @@ import {
|
|
|
21
19
|
type StoredRecentItem,
|
|
22
20
|
} from "#ginko-docs/features/search/command-center";
|
|
23
21
|
|
|
24
|
-
type ContentSearchHit = {
|
|
25
|
-
collection: string;
|
|
26
|
-
path: string;
|
|
27
|
-
title: string;
|
|
28
|
-
excerpt?: string;
|
|
29
|
-
anchor?: string;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
22
|
const MAX_DEFAULT_PAGE_ITEMS = 6;
|
|
33
23
|
const MAX_DEFAULT_DOC_ITEMS = 3;
|
|
34
24
|
|
|
@@ -77,21 +67,27 @@ export async function useCommandCenter() {
|
|
|
77
67
|
} = useCommandCenterState();
|
|
78
68
|
const recentSelections = useLocalStorage<StoredRecentItem[]>("site-command-center-recent", []);
|
|
79
69
|
|
|
80
|
-
const { mainNav,
|
|
70
|
+
const { mainNav, socialLinks } = useSiteNavigation();
|
|
81
71
|
// Start every context-bound composable before the first await. Calling a
|
|
82
72
|
// Nuxt/i18n composable after an awaited operation loses the setup context.
|
|
83
|
-
const docsEntryPathResult = useDocsEntryPath();
|
|
84
73
|
const docsNavigationResult = useDocsNavigation();
|
|
85
74
|
const contentSearchResult = useContentSearch({
|
|
86
75
|
limit: 12,
|
|
87
76
|
locale: () => locale.value,
|
|
88
77
|
});
|
|
89
|
-
const [
|
|
90
|
-
|
|
78
|
+
const [
|
|
79
|
+
{ sections },
|
|
80
|
+
{
|
|
81
|
+
query: searchQuery,
|
|
82
|
+
results: searchResults,
|
|
83
|
+
pending: searchPending,
|
|
84
|
+
error: searchError,
|
|
85
|
+
isEmpty: searchIsEmpty,
|
|
86
|
+
},
|
|
87
|
+
] = await Promise.all([docsNavigationResult, contentSearchResult]);
|
|
91
88
|
|
|
92
89
|
const contentSearchItems = computed<CommandCenterItem[]>(() =>
|
|
93
|
-
searchResults.value.map((hit
|
|
94
|
-
const group = contentSearchGroup(hit.collection);
|
|
90
|
+
searchResults.value.map((hit) => {
|
|
95
91
|
const href = hit.anchor ? `${hit.path}#${hit.anchor}` : hit.path;
|
|
96
92
|
|
|
97
93
|
return {
|
|
@@ -99,10 +95,9 @@ export async function useCommandCenter() {
|
|
|
99
95
|
title: hit.title,
|
|
100
96
|
subtitle: hit.excerpt,
|
|
101
97
|
href,
|
|
102
|
-
group,
|
|
103
|
-
icon:
|
|
104
|
-
|
|
105
|
-
badge: group === "blog" ? t("nav.blog") : t("docs.label"),
|
|
98
|
+
group: "search",
|
|
99
|
+
icon: hit.collection === "blog" ? "lucide:newspaper" : "lucide:book-open",
|
|
100
|
+
badge: hit.collection === "blog" ? t("nav.blog") : t("docs.label"),
|
|
106
101
|
};
|
|
107
102
|
}),
|
|
108
103
|
);
|
|
@@ -130,11 +125,10 @@ export async function useCommandCenter() {
|
|
|
130
125
|
keywords: ["navigation", item.label],
|
|
131
126
|
}));
|
|
132
127
|
|
|
133
|
-
const
|
|
134
|
-
{ label: t("nav.documentation"), href:
|
|
135
|
-
...
|
|
136
|
-
]
|
|
137
|
-
const fromFooter = [...footerNav.value.product, ...footerResources, ...footerNav.value.company]
|
|
128
|
+
const fromFooter = [
|
|
129
|
+
{ label: t("nav.documentation"), href: localizedPath("docs") },
|
|
130
|
+
...socialLinks.value,
|
|
131
|
+
]
|
|
138
132
|
.filter((item) => !(item as { external?: boolean }).external)
|
|
139
133
|
.map<CommandCenterItem>((item) => ({
|
|
140
134
|
id: `page-footer-${item.href}`,
|
|
@@ -175,7 +169,7 @@ export async function useCommandCenter() {
|
|
|
175
169
|
title: section.title,
|
|
176
170
|
subtitle: t("command.pages.documentation"),
|
|
177
171
|
href: section.path,
|
|
178
|
-
group: "
|
|
172
|
+
group: "docs_nav",
|
|
179
173
|
icon: section.icon ?? "lucide:book-open",
|
|
180
174
|
keywords: [section.title],
|
|
181
175
|
},
|
|
@@ -187,7 +181,7 @@ export async function useCommandCenter() {
|
|
|
187
181
|
});
|
|
188
182
|
|
|
189
183
|
const actionItems = computed<CommandCenterItem[]>(() => {
|
|
190
|
-
const github = socialLinks.value.find((item) => item.
|
|
184
|
+
const github = socialLinks.value.find((item) => item.platform === "github");
|
|
191
185
|
return github
|
|
192
186
|
? [
|
|
193
187
|
{
|
|
@@ -197,37 +191,26 @@ export async function useCommandCenter() {
|
|
|
197
191
|
href: github.href,
|
|
198
192
|
external: true,
|
|
199
193
|
group: "actions" as const,
|
|
200
|
-
icon: "lucide:github",
|
|
194
|
+
icon: github.icon ?? "lucide:github",
|
|
201
195
|
keywords: ["repo", "source", "code"],
|
|
202
196
|
},
|
|
203
197
|
]
|
|
204
198
|
: [];
|
|
205
199
|
});
|
|
206
200
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// (higher group priority) shadow duplicate search results while anchored
|
|
210
|
-
// hits (#section) stay distinct.
|
|
211
|
-
const allItems = computed(() =>
|
|
212
|
-
dedupeCommandCenterItems([
|
|
213
|
-
...pageItems.value,
|
|
214
|
-
...docsItems.value,
|
|
215
|
-
...contentSearchItems.value,
|
|
216
|
-
...actionItems.value,
|
|
217
|
-
]),
|
|
201
|
+
const canonicalItems = computed(() =>
|
|
202
|
+
dedupeCommandCenterItems([...pageItems.value, ...docsItems.value, ...actionItems.value]),
|
|
218
203
|
);
|
|
219
204
|
|
|
220
205
|
const recentItems = computed<CommandCenterItem[]>(() => {
|
|
221
206
|
if (query.value) return [];
|
|
222
207
|
|
|
223
|
-
return resolveRecentItems(recentSelections.value,
|
|
208
|
+
return resolveRecentItems(recentSelections.value, canonicalItems.value);
|
|
224
209
|
});
|
|
225
210
|
|
|
226
211
|
const defaultItems = computed<CommandCenterItem[]>(() => {
|
|
227
212
|
const recentSourceIds = new Set(recentItems.value.map((item) => item.sourceId));
|
|
228
|
-
const docSectionItems = docsItems.value
|
|
229
|
-
.filter((item) => item.id.startsWith("doc-switcher-"))
|
|
230
|
-
.slice(0, MAX_DEFAULT_DOC_ITEMS);
|
|
213
|
+
const docSectionItems = docsItems.value.slice(0, MAX_DEFAULT_DOC_ITEMS);
|
|
231
214
|
|
|
232
215
|
return dedupeCommandCenterItems([
|
|
233
216
|
...pageItems.value.slice(0, MAX_DEFAULT_PAGE_ITEMS),
|
|
@@ -237,16 +220,30 @@ export async function useCommandCenter() {
|
|
|
237
220
|
});
|
|
238
221
|
|
|
239
222
|
const groupedItems = computed(() => {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
223
|
+
if (query.value.trim()) {
|
|
224
|
+
return contentSearchItems.value.length
|
|
225
|
+
? [
|
|
226
|
+
{
|
|
227
|
+
id: "search" as const,
|
|
228
|
+
title: t("command.groups.search"),
|
|
229
|
+
items: contentSearchItems.value,
|
|
230
|
+
},
|
|
231
|
+
]
|
|
232
|
+
: [];
|
|
233
|
+
}
|
|
234
|
+
return groupCommandCenterItems([...recentItems.value, ...defaultItems.value], (group) =>
|
|
244
235
|
t(`command.groups.${group}`),
|
|
245
236
|
);
|
|
246
237
|
});
|
|
247
238
|
|
|
248
239
|
function rememberSelection(item: CommandCenterItem) {
|
|
249
|
-
|
|
240
|
+
const canonicalItem =
|
|
241
|
+
item.group === "search"
|
|
242
|
+
? canonicalItems.value.find((candidate) => candidate.href === item.href)
|
|
243
|
+
: item;
|
|
244
|
+
if (canonicalItem) {
|
|
245
|
+
recentSelections.value = rememberRecentItem(canonicalItem, recentSelections.value);
|
|
246
|
+
}
|
|
250
247
|
}
|
|
251
248
|
|
|
252
249
|
function closeCommandCenter() {
|
|
@@ -293,6 +290,9 @@ export async function useCommandCenter() {
|
|
|
293
290
|
open,
|
|
294
291
|
query,
|
|
295
292
|
groupedItems,
|
|
293
|
+
searchPending,
|
|
294
|
+
searchError,
|
|
295
|
+
searchIsEmpty,
|
|
296
296
|
openCommandCenter,
|
|
297
297
|
closeCommandCenter,
|
|
298
298
|
selectItem,
|
package/app/layouts/docs.vue
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<div class="mx-auto flex w-full max-w-screen-2xl min-w-0 flex-1">
|
|
8
8
|
<div
|
|
9
|
-
class="pointer-events-none sticky top-[var(--site-header-height)] z-20 hidden h-[calc(100dvh-var(--site-header-height))] w-[var(--docs-sidebar-width)] shrink-0 overflow-hidden *:pointer-events-auto md:block"
|
|
9
|
+
class="pointer-events-none sticky top-[var(--site-header-height)] z-20 hidden h-[calc(100dvh-var(--site-header-height)-var(--site-banner-height,0px))] w-[var(--docs-sidebar-width)] shrink-0 overflow-hidden *:pointer-events-auto md:block"
|
|
10
10
|
>
|
|
11
11
|
<DocsSidebar />
|
|
12
12
|
</div>
|
package/app/lib/errors.ts
CHANGED
|
@@ -2,31 +2,12 @@ export type ErrorKind = "accessDenied" | "notFound" | "server" | "unavailable";
|
|
|
2
2
|
|
|
3
3
|
export interface NormalizedAppError {
|
|
4
4
|
kind: ErrorKind;
|
|
5
|
-
message: string;
|
|
6
5
|
statusCode: number;
|
|
7
|
-
statusMessage: string;
|
|
8
6
|
}
|
|
9
7
|
|
|
10
8
|
type ErrorLike = {
|
|
11
|
-
data?: unknown;
|
|
12
|
-
fatal?: boolean;
|
|
13
|
-
message?: string;
|
|
14
|
-
name?: string;
|
|
15
9
|
status?: number;
|
|
16
10
|
statusCode?: number;
|
|
17
|
-
statusMessage?: string;
|
|
18
|
-
statusText?: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type ContentNotFoundError = Error & {
|
|
22
|
-
data: {
|
|
23
|
-
kind: "content-not-found";
|
|
24
|
-
};
|
|
25
|
-
fatal: true;
|
|
26
|
-
status: 404;
|
|
27
|
-
statusCode: 404;
|
|
28
|
-
statusMessage: "Not Found";
|
|
29
|
-
statusText: "Not Found";
|
|
30
11
|
};
|
|
31
12
|
|
|
32
13
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
@@ -44,41 +25,13 @@ function toStatusCode(error: ErrorLike): number {
|
|
|
44
25
|
|
|
45
26
|
function errorKind(statusCode: number): ErrorKind {
|
|
46
27
|
if (statusCode === 404) return "notFound";
|
|
47
|
-
if (statusCode === 403) return "accessDenied";
|
|
28
|
+
if (statusCode === 401 || statusCode === 403) return "accessDenied";
|
|
48
29
|
if (statusCode >= 500) return "server";
|
|
49
30
|
return "unavailable";
|
|
50
31
|
}
|
|
51
32
|
|
|
52
|
-
function defaultStatusMessage(statusCode: number, kind: ErrorKind): string {
|
|
53
|
-
if (kind === "notFound") return "Not Found";
|
|
54
|
-
if (kind === "accessDenied") return "Forbidden";
|
|
55
|
-
if (kind === "server") return "Internal Server Error";
|
|
56
|
-
return statusCode >= 400 ? "Unavailable" : "Error";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
33
|
export function normalizeAppError(error: unknown): NormalizedAppError {
|
|
60
34
|
const source = toErrorLike(error);
|
|
61
35
|
const statusCode = toStatusCode(source);
|
|
62
|
-
|
|
63
|
-
const statusMessage =
|
|
64
|
-
source.statusMessage ?? source.statusText ?? defaultStatusMessage(statusCode, kind);
|
|
65
|
-
const message = source.message ?? statusMessage;
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
kind,
|
|
69
|
-
message,
|
|
70
|
-
statusCode,
|
|
71
|
-
statusMessage,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function createContentNotFoundError(): ContentNotFoundError {
|
|
76
|
-
const error = new Error("Content not found") as ContentNotFoundError;
|
|
77
|
-
error.data = { kind: "content-not-found" };
|
|
78
|
-
error.fatal = true;
|
|
79
|
-
error.status = 404;
|
|
80
|
-
error.statusCode = 404;
|
|
81
|
-
error.statusMessage = "Not Found";
|
|
82
|
-
error.statusText = "Not Found";
|
|
83
|
-
return error;
|
|
36
|
+
return { kind: errorKind(statusCode), statusCode };
|
|
84
37
|
}
|
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
import { Badge } from "#ginko-docs/components/ui/badge";
|
|
3
3
|
import ContentFeedback from "#ginko-docs/components/content/Feedback.vue";
|
|
4
4
|
import { Button } from "#ginko-docs/components/ui/button";
|
|
5
|
-
import { createContentNotFoundError } from "#ginko-docs/lib/errors";
|
|
6
5
|
import { createArticleSchema, createBreadcrumbSchema } from "#ginko-docs/lib/schema-org";
|
|
7
6
|
import { getLocalizedSiteText } from "#ginko-docs/config/site.utils";
|
|
8
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
filterTocByDepth,
|
|
9
|
+
flattenTocLinks,
|
|
10
|
+
formatContentDate,
|
|
11
|
+
getMarkdownTocLinks,
|
|
12
|
+
} from "#ginko-docs/utils/content";
|
|
9
13
|
import { computed, watch } from "vue";
|
|
10
14
|
import {
|
|
11
15
|
definePageMeta,
|
|
16
|
+
createError,
|
|
12
17
|
useAppConfig,
|
|
13
18
|
useContentPage,
|
|
14
19
|
useHead,
|
|
@@ -22,7 +27,7 @@ import { useGinkoOgImage } from "#ginko-docs/composables/useGinkoOgImage";
|
|
|
22
27
|
import { useSchemaJsonLd } from "#ginko-docs/composables/useSchemaJsonLd";
|
|
23
28
|
import DocumentPageShell from "#ginko-docs/components/content/DocumentPageShell.vue";
|
|
24
29
|
import PageMarkdownCopy from "#ginko-docs/components/content/PageMarkdownCopy.vue";
|
|
25
|
-
import {
|
|
30
|
+
import { useContentRouteAlternates } from "#ginko-docs/composables/useContentRouteAlternates";
|
|
26
31
|
|
|
27
32
|
definePageMeta({ layout: "blog" });
|
|
28
33
|
|
|
@@ -30,26 +35,26 @@ const { locale, t } = useI18n();
|
|
|
30
35
|
const config = useAppConfig().ginkoDocs;
|
|
31
36
|
const route = useRoute();
|
|
32
37
|
const localizedPath = useLocalizedPath();
|
|
38
|
+
const canonicalUrl = useCanonicalUrl();
|
|
39
|
+
const { sync: syncContentRouteAlternates } = useContentRouteAlternates();
|
|
33
40
|
const {
|
|
34
41
|
page: post,
|
|
35
42
|
previous,
|
|
36
43
|
next,
|
|
37
44
|
error,
|
|
38
45
|
} = await useContentPage("blog", {
|
|
39
|
-
locale: () => locale.value,
|
|
40
46
|
fallback: true,
|
|
41
47
|
populate: { author: "authors" },
|
|
42
48
|
surround: true,
|
|
43
49
|
});
|
|
44
50
|
if (error.value) throw error.value;
|
|
45
|
-
if (!post.value) throw
|
|
51
|
+
if (!post.value) throw createError({ statusCode: 404, statusMessage: "Not Found" });
|
|
46
52
|
syncContentRouteAlternates(post);
|
|
47
53
|
|
|
48
54
|
const pageTitle = computed(() => post.value?.title ?? t("blog.fallbackTitle"));
|
|
49
55
|
const pageDescription = computed(() => post.value?.description ?? t("blog.fallbackDescription"));
|
|
50
|
-
const canonicalUrl = useCanonicalUrl();
|
|
51
56
|
const tocItems = computed(() =>
|
|
52
|
-
filterTocByDepth(flattenTocLinks(post.value?.body
|
|
57
|
+
filterTocByDepth(flattenTocLinks(getMarkdownTocLinks(post.value?.body)), config.toc?.depth ?? 3),
|
|
53
58
|
);
|
|
54
59
|
const formattedDate = computed(() => formatContentDate(post.value?.date, locale.value));
|
|
55
60
|
const siteName = computed(() => getLocalizedSiteText(config.site.name, locale.value));
|
|
@@ -75,7 +80,16 @@ useGinkoOgImage({
|
|
|
75
80
|
});
|
|
76
81
|
|
|
77
82
|
useHead(() => ({
|
|
78
|
-
link: [
|
|
83
|
+
link: [
|
|
84
|
+
{ key: "canonical", rel: "canonical", href: canonicalUrl.value },
|
|
85
|
+
{
|
|
86
|
+
key: "rss",
|
|
87
|
+
rel: "alternate",
|
|
88
|
+
type: "application/rss+xml",
|
|
89
|
+
title: `${t("blog.title")} - ${siteName.value}`,
|
|
90
|
+
href: `${localizedPath("blog")}/rss.xml`,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
79
93
|
meta: [{ property: "og:type", content: "article" }],
|
|
80
94
|
}));
|
|
81
95
|
|
|
@@ -181,7 +195,10 @@ useSchemaJsonLd(() =>
|
|
|
181
195
|
<ContentRenderer :value="post" />
|
|
182
196
|
</div>
|
|
183
197
|
|
|
184
|
-
<ContentFeedback
|
|
198
|
+
<ContentFeedback
|
|
199
|
+
class="mt-14 border-y border-border py-5"
|
|
200
|
+
:label="t('feedback.articleLabel')"
|
|
201
|
+
/>
|
|
185
202
|
|
|
186
203
|
<div v-if="suggestions.length" class="mt-14">
|
|
187
204
|
<h2 class="mb-6 text-sm font-semibold tracking-wider text-muted-foreground uppercase">
|
package/app/pages/blog/index.vue
CHANGED
|
@@ -6,24 +6,16 @@ import { getLocalizedSiteText } from "#ginko-docs/config/site.utils";
|
|
|
6
6
|
import { computed } from "vue";
|
|
7
7
|
import { definePageMeta, useAppConfig, useAsyncData, useHead, useI18n, useSeoMeta } from "#imports";
|
|
8
8
|
import { useCanonicalUrl } from "#ginko-docs/composables/useCanonicalUrl";
|
|
9
|
+
import { useLocalizedPath } from "#ginko-docs/composables/useLocalizedPath";
|
|
10
|
+
|
|
11
|
+
const MAX_BLOG_POSTS = 50;
|
|
9
12
|
|
|
10
13
|
definePageMeta({ layout: "blog" });
|
|
11
14
|
|
|
12
15
|
const { locale, t } = useI18n();
|
|
13
16
|
const postsKey = computed(() => `blog-posts:${locale.value}`);
|
|
14
|
-
const { data: posts } = await useAsyncData(
|
|
15
|
-
postsKey,
|
|
16
|
-
() =>
|
|
17
|
-
many("blog", {
|
|
18
|
-
locale: locale.value,
|
|
19
|
-
fallback: true,
|
|
20
|
-
populate: { author: "authors" },
|
|
21
|
-
sort: { date: "desc" },
|
|
22
|
-
limit: 50,
|
|
23
|
-
}),
|
|
24
|
-
{ watch: [locale] },
|
|
25
|
-
);
|
|
26
17
|
const canonicalUrl = useCanonicalUrl();
|
|
18
|
+
const localizedPath = useLocalizedPath();
|
|
27
19
|
const config = useAppConfig().ginkoDocs;
|
|
28
20
|
const siteName = computed(() => getLocalizedSiteText(config.site.name, locale.value));
|
|
29
21
|
const fullTitle = computed(() => `${t("blog.pageTitle")} - ${siteName.value}`);
|
|
@@ -38,8 +30,37 @@ useSeoMeta({
|
|
|
38
30
|
});
|
|
39
31
|
|
|
40
32
|
useHead(() => ({
|
|
41
|
-
link: [
|
|
33
|
+
link: [
|
|
34
|
+
{ key: "canonical", rel: "canonical", href: canonicalUrl.value },
|
|
35
|
+
{
|
|
36
|
+
key: "rss",
|
|
37
|
+
rel: "alternate",
|
|
38
|
+
type: "application/rss+xml",
|
|
39
|
+
title: fullTitle.value,
|
|
40
|
+
href: `${localizedPath("blog")}/rss.xml`,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
42
43
|
}));
|
|
44
|
+
|
|
45
|
+
const { data: queriedPosts, error } = await useAsyncData(
|
|
46
|
+
postsKey,
|
|
47
|
+
() =>
|
|
48
|
+
many("blog", {
|
|
49
|
+
locale: locale.value,
|
|
50
|
+
fallback: true,
|
|
51
|
+
populate: { author: "authors" },
|
|
52
|
+
sort: { date: "desc" },
|
|
53
|
+
limit: MAX_BLOG_POSTS + 1,
|
|
54
|
+
}),
|
|
55
|
+
{ watch: [locale] },
|
|
56
|
+
);
|
|
57
|
+
if (error.value) throw error.value;
|
|
58
|
+
if ((queriedPosts.value?.length ?? 0) > MAX_BLOG_POSTS) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Blog supports at most ${MAX_BLOG_POSTS} posts; add pagination before publishing more`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const posts = computed(() => queriedPosts.value ?? []);
|
|
43
64
|
</script>
|
|
44
65
|
|
|
45
66
|
<template>
|
|
@@ -49,7 +70,10 @@ useHead(() => ({
|
|
|
49
70
|
<p class="mt-2 text-muted-foreground">{{ t("blog.description") }}</p>
|
|
50
71
|
</div>
|
|
51
72
|
|
|
52
|
-
<
|
|
73
|
+
<p v-if="posts.length === 0" class="py-12 text-center text-sm text-muted-foreground">
|
|
74
|
+
{{ t("blog.empty") }}
|
|
75
|
+
</p>
|
|
76
|
+
<div v-else class="divide-y divide-border">
|
|
53
77
|
<article v-for="post in posts" :key="post.route.resolvedPath" class="group py-8">
|
|
54
78
|
<NuxtLink
|
|
55
79
|
:to="post.route.resolvedPath"
|
package/app/pages/docs/index.vue
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { findFirstNavigationPage } from "@lupinum/ginko-content/navigation";
|
|
3
|
+
import { createError, definePageMeta, navigateTo } from "#imports";
|
|
4
|
+
import { useDocsNavigationData } from "#ginko-docs/features/docs/composables/useDocsNavigationData";
|
|
4
5
|
|
|
5
6
|
definePageMeta({ layout: "docs" });
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (entryPath.value && entryPath.value !== route.path) {
|
|
13
|
-
await navigateTo(entryPath.value, { redirectCode: 302, replace: true });
|
|
8
|
+
const { data, error } = await useDocsNavigationData();
|
|
9
|
+
if (error.value) throw error.value;
|
|
10
|
+
const entryPath = findFirstNavigationPage(data.value ?? undefined)?.path;
|
|
11
|
+
if (!entryPath) {
|
|
12
|
+
throw createError({ statusCode: 404, statusMessage: "Documentation is empty" });
|
|
14
13
|
}
|
|
14
|
+
await navigateTo(entryPath, { redirectCode: 302, replace: true });
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
<template>
|
|
18
|
-
<main id="main-content"
|
|
19
|
-
<NuxtLink v-if="entryPath && entryPath !== route.path" :to="entryPath" class="underline">
|
|
20
|
-
{{ entryPath }}
|
|
21
|
-
</NuxtLink>
|
|
22
|
-
</main>
|
|
18
|
+
<main id="main-content" />
|
|
23
19
|
</template>
|