@kirchdev/duxt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/app/app.config.ts +13 -0
- package/app/app.vue +122 -0
- package/app/assets/css/duxt.css +555 -0
- package/app/assets/css/typeset.css +498 -0
- package/app/components/DuxtBrand.vue +53 -0
- package/app/components/DuxtBreadcrumb.vue +37 -0
- package/app/components/DuxtCodeBlock.vue +143 -0
- package/app/components/DuxtCopyPage.vue +117 -0
- package/app/components/DuxtFooter.vue +86 -0
- package/app/components/DuxtHeader.vue +496 -0
- package/app/components/DuxtJsonEditor.vue +359 -0
- package/app/components/DuxtLandingProse.vue +90 -0
- package/app/components/DuxtLandingShowcase.vue +295 -0
- package/app/components/DuxtLiveWindow.vue +343 -0
- package/app/components/DuxtLocale.vue +86 -0
- package/app/components/DuxtMetaList.vue +76 -0
- package/app/components/DuxtNavigation.vue +94 -0
- package/app/components/DuxtNavigationLink.vue +41 -0
- package/app/components/DuxtOpenApiCallbacks.vue +92 -0
- package/app/components/DuxtOpenApiClient.vue +1339 -0
- package/app/components/DuxtOpenApiMedia.vue +108 -0
- package/app/components/DuxtOpenApiMethod.vue +59 -0
- package/app/components/DuxtOpenApiParameters.vue +114 -0
- package/app/components/DuxtOpenApiResponses.vue +109 -0
- package/app/components/DuxtOpenApiSchema.vue +365 -0
- package/app/components/DuxtOpenApiSecurity.vue +115 -0
- package/app/components/DuxtPageFeedback.vue +56 -0
- package/app/components/DuxtPageInfo.vue +194 -0
- package/app/components/DuxtPageNav.vue +93 -0
- package/app/components/DuxtPreviewSkeleton.vue +262 -0
- package/app/components/DuxtProgress.vue +45 -0
- package/app/components/DuxtSearch.vue +271 -0
- package/app/components/DuxtSections.vue +65 -0
- package/app/components/DuxtShortcuts.vue +46 -0
- package/app/components/DuxtSkipLink.vue +34 -0
- package/app/components/DuxtToc.vue +86 -0
- package/app/components/DuxtTranslationBanner.vue +64 -0
- package/app/components/DuxtVersion.vue +168 -0
- package/app/components/DuxtVersionBanner.vue +109 -0
- package/app/components/OgImage/Duxt.satori.vue +82 -0
- package/app/components/content/Accordion.vue +37 -0
- package/app/components/content/AccordionItem.vue +46 -0
- package/app/components/content/Callout.vue +58 -0
- package/app/components/content/ChangelogGroup.vue +91 -0
- package/app/components/content/ChangelogReleases.vue +520 -0
- package/app/components/content/CodeGroup.vue +183 -0
- package/app/components/content/DevtoolsPanel.vue +107 -0
- package/app/components/content/Field.vue +50 -0
- package/app/components/content/FieldGroup.vue +8 -0
- package/app/components/content/FileTree.vue +92 -0
- package/app/components/content/Mermaid.vue +70 -0
- package/app/components/content/OpenApiOperation.vue +185 -0
- package/app/components/content/OpenApiOperations.vue +72 -0
- package/app/components/content/OpenApiOverview.vue +302 -0
- package/app/components/content/PackageManagers.vue +187 -0
- package/app/components/content/PageCards.vue +48 -0
- package/app/components/content/Partial.vue +65 -0
- package/app/components/content/Preview.vue +173 -0
- package/app/components/content/ProseA.vue +55 -0
- package/app/components/content/ProseH2.vue +53 -0
- package/app/components/content/ProseH3.vue +53 -0
- package/app/components/content/ProseH4.vue +53 -0
- package/app/components/content/ProseImg.vue +87 -0
- package/app/components/content/ProsePre.vue +58 -0
- package/app/components/content/ProseTable.vue +11 -0
- package/app/components/content/Since.vue +21 -0
- package/app/components/content/Steps.vue +20 -0
- package/app/components/content/Tab.vue +23 -0
- package/app/components/content/TabGroup.vue +58 -0
- package/app/components/ui/alert/Alert.vue +21 -0
- package/app/components/ui/alert/AlertDescription.vue +22 -0
- package/app/components/ui/alert/AlertTitle.vue +22 -0
- package/app/components/ui/alert/index.ts +24 -0
- package/app/components/ui/badge/Badge.vue +28 -0
- package/app/components/ui/badge/index.ts +52 -0
- package/app/components/ui/breadcrumb/Breadcrumb.vue +13 -0
- package/app/components/ui/breadcrumb/BreadcrumbEllipsis.vue +22 -0
- package/app/components/ui/breadcrumb/BreadcrumbItem.vue +17 -0
- package/app/components/ui/breadcrumb/BreadcrumbLink.vue +24 -0
- package/app/components/ui/breadcrumb/BreadcrumbList.vue +22 -0
- package/app/components/ui/breadcrumb/BreadcrumbPage.vue +20 -0
- package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +21 -0
- package/app/components/ui/breadcrumb/index.ts +7 -0
- package/app/components/ui/button/Button.vue +31 -0
- package/app/components/ui/button/index.ts +39 -0
- package/app/components/ui/card/Card.vue +22 -0
- package/app/components/ui/card/CardAction.vue +22 -0
- package/app/components/ui/card/CardContent.vue +14 -0
- package/app/components/ui/card/CardDescription.vue +17 -0
- package/app/components/ui/card/CardFooter.vue +17 -0
- package/app/components/ui/card/CardHeader.vue +22 -0
- package/app/components/ui/card/CardTitle.vue +17 -0
- package/app/components/ui/card/index.ts +7 -0
- package/app/components/ui/collapsible/Collapsible.vue +19 -0
- package/app/components/ui/collapsible/CollapsibleContent.vue +12 -0
- package/app/components/ui/collapsible/CollapsibleTrigger.vue +12 -0
- package/app/components/ui/collapsible/index.ts +3 -0
- package/app/components/ui/command/Command.vue +98 -0
- package/app/components/ui/command/CommandDialog.vue +42 -0
- package/app/components/ui/command/CommandEmpty.vue +31 -0
- package/app/components/ui/command/CommandGroup.vue +52 -0
- package/app/components/ui/command/CommandInput.vue +45 -0
- package/app/components/ui/command/CommandItem.vue +85 -0
- package/app/components/ui/command/CommandList.vue +32 -0
- package/app/components/ui/command/CommandSeparator.vue +23 -0
- package/app/components/ui/command/CommandShortcut.vue +19 -0
- package/app/components/ui/command/index.ts +29 -0
- package/app/components/ui/dialog/Dialog.vue +15 -0
- package/app/components/ui/dialog/DialogClose.vue +12 -0
- package/app/components/ui/dialog/DialogContent.vue +61 -0
- package/app/components/ui/dialog/DialogDescription.vue +25 -0
- package/app/components/ui/dialog/DialogFooter.vue +30 -0
- package/app/components/ui/dialog/DialogHeader.vue +17 -0
- package/app/components/ui/dialog/DialogOverlay.vue +28 -0
- package/app/components/ui/dialog/DialogScrollContent.vue +65 -0
- package/app/components/ui/dialog/DialogTitle.vue +25 -0
- package/app/components/ui/dialog/DialogTrigger.vue +12 -0
- package/app/components/ui/dialog/index.ts +10 -0
- package/app/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +47 -0
- package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +47 -0
- package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +12 -0
- package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +41 -0
- package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +27 -0
- package/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +48 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +19 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +34 -0
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +34 -0
- package/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
- package/app/components/ui/dropdown-menu/index.ts +16 -0
- package/app/components/ui/input/Input.vue +35 -0
- package/app/components/ui/input/index.ts +1 -0
- package/app/components/ui/navigation-menu/NavigationMenu.vue +42 -0
- package/app/components/ui/navigation-menu/NavigationMenuContent.vue +35 -0
- package/app/components/ui/navigation-menu/NavigationMenuIndicator.vue +32 -0
- package/app/components/ui/navigation-menu/NavigationMenuItem.vue +23 -0
- package/app/components/ui/navigation-menu/NavigationMenuLink.vue +30 -0
- package/app/components/ui/navigation-menu/NavigationMenuList.vue +30 -0
- package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +31 -0
- package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +30 -0
- package/app/components/ui/navigation-menu/index.ts +14 -0
- package/app/components/ui/scroll-area/ScrollArea.vue +31 -0
- package/app/components/ui/scroll-area/ScrollBar.vue +38 -0
- package/app/components/ui/scroll-area/index.ts +2 -0
- package/app/components/ui/select/Select.vue +15 -0
- package/app/components/ui/select/SelectContent.vue +59 -0
- package/app/components/ui/select/SelectGroup.vue +19 -0
- package/app/components/ui/select/SelectItem.vue +42 -0
- package/app/components/ui/select/SelectItemText.vue +12 -0
- package/app/components/ui/select/SelectLabel.vue +16 -0
- package/app/components/ui/select/SelectScrollDownButton.vue +28 -0
- package/app/components/ui/select/SelectScrollUpButton.vue +28 -0
- package/app/components/ui/select/SelectSeparator.vue +20 -0
- package/app/components/ui/select/SelectTrigger.vue +32 -0
- package/app/components/ui/select/SelectValue.vue +12 -0
- package/app/components/ui/select/index.ts +11 -0
- package/app/components/ui/separator/Separator.vue +30 -0
- package/app/components/ui/separator/index.ts +1 -0
- package/app/components/ui/sheet/Sheet.vue +15 -0
- package/app/components/ui/sheet/SheetClose.vue +12 -0
- package/app/components/ui/sheet/SheetContent.vue +68 -0
- package/app/components/ui/sheet/SheetDescription.vue +23 -0
- package/app/components/ui/sheet/SheetFooter.vue +15 -0
- package/app/components/ui/sheet/SheetHeader.vue +15 -0
- package/app/components/ui/sheet/SheetOverlay.vue +28 -0
- package/app/components/ui/sheet/SheetTitle.vue +23 -0
- package/app/components/ui/sheet/SheetTrigger.vue +12 -0
- package/app/components/ui/sheet/index.ts +8 -0
- package/app/components/ui/sidebar/Sidebar.vue +110 -0
- package/app/components/ui/sidebar/SidebarContent.vue +23 -0
- package/app/components/ui/sidebar/SidebarFooter.vue +18 -0
- package/app/components/ui/sidebar/SidebarGroup.vue +18 -0
- package/app/components/ui/sidebar/SidebarGroupAction.vue +31 -0
- package/app/components/ui/sidebar/SidebarGroupContent.vue +18 -0
- package/app/components/ui/sidebar/SidebarGroupLabel.vue +30 -0
- package/app/components/ui/sidebar/SidebarHeader.vue +18 -0
- package/app/components/ui/sidebar/SidebarInput.vue +19 -0
- package/app/components/ui/sidebar/SidebarInset.vue +23 -0
- package/app/components/ui/sidebar/SidebarMenu.vue +18 -0
- package/app/components/ui/sidebar/SidebarMenuAction.vue +42 -0
- package/app/components/ui/sidebar/SidebarMenuBadge.vue +28 -0
- package/app/components/ui/sidebar/SidebarMenuButton.vue +60 -0
- package/app/components/ui/sidebar/SidebarMenuButtonChild.vue +36 -0
- package/app/components/ui/sidebar/SidebarMenuItem.vue +18 -0
- package/app/components/ui/sidebar/SidebarMenuSkeleton.vue +35 -0
- package/app/components/ui/sidebar/SidebarMenuSub.vue +24 -0
- package/app/components/ui/sidebar/SidebarMenuSubButton.vue +43 -0
- package/app/components/ui/sidebar/SidebarMenuSubItem.vue +18 -0
- package/app/components/ui/sidebar/SidebarProvider.vue +109 -0
- package/app/components/ui/sidebar/SidebarRail.vue +35 -0
- package/app/components/ui/sidebar/SidebarSeparator.vue +19 -0
- package/app/components/ui/sidebar/SidebarTrigger.vue +26 -0
- package/app/components/ui/sidebar/index.ts +62 -0
- package/app/components/ui/sidebar/utils.ts +19 -0
- package/app/components/ui/skeleton/Skeleton.vue +17 -0
- package/app/components/ui/skeleton/index.ts +1 -0
- package/app/components/ui/sonner/Sonner.vue +67 -0
- package/app/components/ui/sonner/index.ts +1 -0
- package/app/components/ui/tabs/Tabs.vue +26 -0
- package/app/components/ui/tabs/TabsContent.vue +23 -0
- package/app/components/ui/tabs/TabsList.vue +28 -0
- package/app/components/ui/tabs/TabsTrigger.vue +30 -0
- package/app/components/ui/tabs/index.ts +4 -0
- package/app/components/ui/textarea/Textarea.vue +32 -0
- package/app/components/ui/textarea/index.ts +1 -0
- package/app/components/ui/tooltip/Tooltip.vue +15 -0
- package/app/components/ui/tooltip/TooltipContent.vue +49 -0
- package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
- package/app/components/ui/tooltip/TooltipTrigger.vue +12 -0
- package/app/components/ui/tooltip/index.ts +4 -0
- package/app/composables/useActiveHeading.ts +90 -0
- package/app/composables/useDuxtAnimatedHeight.ts +155 -0
- package/app/composables/useDuxtBreadcrumb.ts +60 -0
- package/app/composables/useDuxtChoice.ts +28 -0
- package/app/composables/useDuxtCollection.ts +89 -0
- package/app/composables/useDuxtConfig.ts +47 -0
- package/app/composables/useDuxtNavigation.ts +87 -0
- package/app/composables/useDuxtPageFocus.ts +30 -0
- package/app/composables/useDuxtPath.ts +42 -0
- package/app/composables/useDuxtSearch.ts +208 -0
- package/app/composables/useDuxtSection.ts +44 -0
- package/app/composables/useDuxtSectionRow.ts +49 -0
- package/app/composables/useDuxtShortcuts.ts +58 -0
- package/app/composables/useDuxtSiteUrl.ts +27 -0
- package/app/composables/useDuxtToast.ts +25 -0
- package/app/composables/useDuxtVersion.ts +138 -0
- package/app/composables/useFuzzySearch.ts +83 -0
- package/app/composables/usePackageManager.ts +4 -0
- package/app/composables/useRecentPages.ts +52 -0
- package/app/error.vue +133 -0
- package/app/layouts/changelog.vue +85 -0
- package/app/layouts/default.vue +23 -0
- package/app/layouts/docs.vue +62 -0
- package/app/layouts/reference.vue +85 -0
- package/app/lib/utils.ts +7 -0
- package/app/middleware/duxt-section-layout.global.ts +57 -0
- package/app/pages/[...slug].vue +441 -0
- package/app/pages/index.vue +479 -0
- package/app/types/duxt.d.ts +955 -0
- package/app/utils/aside.ts +16 -0
- package/app/utils/changelog.ts +182 -0
- package/app/utils/code-highlight.ts +116 -0
- package/app/utils/duxt-config.ts +185 -0
- package/app/utils/duxt-text.ts +135 -0
- package/app/utils/file-icons.ts +62 -0
- package/app/utils/generated-sections.ts +190 -0
- package/app/utils/generated-toc.ts +216 -0
- package/app/utils/heading-link.ts +32 -0
- package/app/utils/locale-path.ts +45 -0
- package/app/utils/navigation-tree.ts +206 -0
- package/app/utils/nearest-page.ts +72 -0
- package/app/utils/openapi.ts +1028 -0
- package/app/utils/package-command.ts +162 -0
- package/app/utils/request-samples.ts +635 -0
- package/app/utils/section-scope.ts +97 -0
- package/app/utils/sheet-navigation.ts +100 -0
- package/app/utils/source-file.ts +84 -0
- package/app/utils/version-choices.ts +68 -0
- package/app/utils/version-paths.ts +65 -0
- package/bin/duxt-report.mjs +45 -0
- package/components.json +19 -0
- package/content-cache.ts +193 -0
- package/content.config.ts +38 -0
- package/duxt-app-config.ts +124 -0
- package/highlight-langs.ts +68 -0
- package/i18n/i18n.config.ts +16 -0
- package/i18n/locales/de/duxt/changelog.json +17 -0
- package/i18n/locales/de/duxt/code.json +13 -0
- package/i18n/locales/de/duxt/defaults.json +22 -0
- package/i18n/locales/de/duxt/devtools.json +7 -0
- package/i18n/locales/de/duxt/error.json +10 -0
- package/i18n/locales/de/duxt/footer.json +7 -0
- package/i18n/locales/de/duxt/locale.json +7 -0
- package/i18n/locales/de/duxt/nav.json +17 -0
- package/i18n/locales/de/duxt/openapi.json +89 -0
- package/i18n/locales/de/duxt/page.json +34 -0
- package/i18n/locales/de/duxt/search.json +12 -0
- package/i18n/locales/de/duxt/shortcuts.json +11 -0
- package/i18n/locales/de/duxt/theme.json +7 -0
- package/i18n/locales/de/duxt/toc.json +7 -0
- package/i18n/locales/de/duxt/version.json +25 -0
- package/i18n/locales/en/duxt/changelog.json +17 -0
- package/i18n/locales/en/duxt/code.json +13 -0
- package/i18n/locales/en/duxt/defaults.json +22 -0
- package/i18n/locales/en/duxt/devtools.json +7 -0
- package/i18n/locales/en/duxt/error.json +10 -0
- package/i18n/locales/en/duxt/footer.json +7 -0
- package/i18n/locales/en/duxt/locale.json +7 -0
- package/i18n/locales/en/duxt/nav.json +17 -0
- package/i18n/locales/en/duxt/openapi.json +89 -0
- package/i18n/locales/en/duxt/page.json +34 -0
- package/i18n/locales/en/duxt/search.json +12 -0
- package/i18n/locales/en/duxt/shortcuts.json +11 -0
- package/i18n/locales/en/duxt/theme.json +7 -0
- package/i18n/locales/en/duxt/toc.json +7 -0
- package/i18n/locales/en/duxt/version.json +25 -0
- package/i18n/locales/es/duxt/changelog.json +17 -0
- package/i18n/locales/es/duxt/code.json +13 -0
- package/i18n/locales/es/duxt/defaults.json +22 -0
- package/i18n/locales/es/duxt/devtools.json +7 -0
- package/i18n/locales/es/duxt/error.json +10 -0
- package/i18n/locales/es/duxt/footer.json +7 -0
- package/i18n/locales/es/duxt/locale.json +7 -0
- package/i18n/locales/es/duxt/nav.json +17 -0
- package/i18n/locales/es/duxt/openapi.json +89 -0
- package/i18n/locales/es/duxt/page.json +34 -0
- package/i18n/locales/es/duxt/search.json +12 -0
- package/i18n/locales/es/duxt/shortcuts.json +11 -0
- package/i18n/locales/es/duxt/theme.json +7 -0
- package/i18n/locales/es/duxt/toc.json +7 -0
- package/i18n/locales/es/duxt/version.json +25 -0
- package/i18n/locales/fr/duxt/changelog.json +17 -0
- package/i18n/locales/fr/duxt/code.json +13 -0
- package/i18n/locales/fr/duxt/defaults.json +22 -0
- package/i18n/locales/fr/duxt/devtools.json +7 -0
- package/i18n/locales/fr/duxt/error.json +10 -0
- package/i18n/locales/fr/duxt/footer.json +7 -0
- package/i18n/locales/fr/duxt/locale.json +7 -0
- package/i18n/locales/fr/duxt/nav.json +17 -0
- package/i18n/locales/fr/duxt/openapi.json +89 -0
- package/i18n/locales/fr/duxt/page.json +34 -0
- package/i18n/locales/fr/duxt/search.json +12 -0
- package/i18n/locales/fr/duxt/shortcuts.json +11 -0
- package/i18n/locales/fr/duxt/theme.json +7 -0
- package/i18n/locales/fr/duxt/toc.json +7 -0
- package/i18n/locales/fr/duxt/version.json +25 -0
- package/i18n/locales/pt/duxt/changelog.json +17 -0
- package/i18n/locales/pt/duxt/code.json +13 -0
- package/i18n/locales/pt/duxt/defaults.json +22 -0
- package/i18n/locales/pt/duxt/devtools.json +7 -0
- package/i18n/locales/pt/duxt/error.json +10 -0
- package/i18n/locales/pt/duxt/footer.json +7 -0
- package/i18n/locales/pt/duxt/locale.json +7 -0
- package/i18n/locales/pt/duxt/nav.json +17 -0
- package/i18n/locales/pt/duxt/openapi.json +89 -0
- package/i18n/locales/pt/duxt/page.json +34 -0
- package/i18n/locales/pt/duxt/search.json +12 -0
- package/i18n/locales/pt/duxt/shortcuts.json +11 -0
- package/i18n/locales/pt/duxt/theme.json +7 -0
- package/i18n/locales/pt/duxt/toc.json +7 -0
- package/i18n/locales/pt/duxt/version.json +25 -0
- package/i18n/locales/pt-BR/duxt/error.json +8 -0
- package/i18n/locales/pt-BR/duxt/nav.json +10 -0
- package/i18n/locales/pt-BR/duxt/page.json +10 -0
- package/i18n/locales/pt-BR/duxt/search.json +11 -0
- package/i18n/locales/pt-BR/duxt/version.json +13 -0
- package/mdc.config.ts +28 -0
- package/modules/config.ts +440 -0
- package/modules/devtools.ts +126 -0
- package/modules/git-meta.ts +349 -0
- package/modules/redirects.ts +152 -0
- package/modules/validate.ts +137 -0
- package/nuxt.config.ts +530 -0
- package/openapi-model.ts +358 -0
- package/openapi-parse.ts +1052 -0
- package/package.json +161 -0
- package/public/devtools/cache.html +318 -0
- package/public/devtools/checks.html +309 -0
- package/public/devtools/config.html +461 -0
- package/public/devtools/i18n.html +309 -0
- package/public/devtools/pages.html +348 -0
- package/public/devtools/paths.html +315 -0
- package/public/devtools/redirects.html +309 -0
- package/public/devtools/search.html +324 -0
- package/public/devtools/sources.html +309 -0
- package/public/devtools/versions.html +318 -0
- package/report.ts +303 -0
- package/repository-root.ts +26 -0
- package/section-reports.ts +55 -0
- package/sections-changelog.ts +644 -0
- package/sections-openapi.ts +450 -0
- package/sections-resolve.ts +711 -0
- package/sections.ts +157 -0
- package/server/devtools/content.ts +120 -0
- package/server/devtools/context.ts +38 -0
- package/server/devtools/entry-path.ts +30 -0
- package/server/devtools/handler.ts +104 -0
- package/server/devtools/preview.ts +461 -0
- package/server/devtools/render/content.ts +233 -0
- package/server/devtools/render/sources.ts +414 -0
- package/server/devtools/render/system.ts +352 -0
- package/server/devtools/shell.ts +611 -0
- package/server/devtools/sources.ts +122 -0
- package/server/devtools/system.ts +178 -0
- package/server/mcp/tools/list-pages.ts +52 -0
- package/server/mcp/tools/read-page.ts +57 -0
- package/server/mcp/tools/search-docs.ts +74 -0
- package/server/middleware/raw-markdown.ts +86 -0
- package/server/routes/llms-full.txt.get.ts +75 -0
- package/server/routes/llms.txt.get.ts +63 -0
- package/server/routes/rss.xml.get.ts +125 -0
- package/server/utils/duxt-server-text.ts +46 -0
- package/sources-git.ts +113 -0
- package/sources-resolve.ts +908 -0
- package/sources.ts +287 -0
- package/validate-report.ts +494 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import { queryCollection } from '@nuxt/content/nitro';
|
|
3
|
+
import { splitLocalePath } from '../../app/utils/locale-path';
|
|
4
|
+
import { sourcesForRoute } from '../../sources-resolve';
|
|
5
|
+
import { context, resolvedSources } from './context';
|
|
6
|
+
import type { FoundPage } from './render/sources';
|
|
7
|
+
import { renderPaths, renderSources, renderVersions } from './render/sources';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The queries behind the three source panels.
|
|
11
|
+
*
|
|
12
|
+
* The resolver is pure and already tested, so none of this reimplements it: the
|
|
13
|
+
* panels call the same functions the theme calls, and hand what came back to
|
|
14
|
+
* `render/sources.ts`. A panel that computed its own answer would be able to
|
|
15
|
+
* disagree with the site, which is the one thing a debugging view must not do.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Content stores a page under its full path, prefix included. */
|
|
19
|
+
const collectionsOf = () =>
|
|
20
|
+
resolvedSources().map((source) => source.collection) as Parameters<
|
|
21
|
+
typeof queryCollection
|
|
22
|
+
>[1][];
|
|
23
|
+
|
|
24
|
+
/** Every page path the site serves, by collection. */
|
|
25
|
+
async function pathsByCollection(
|
|
26
|
+
event: H3Event
|
|
27
|
+
): Promise<Map<string, string[]>> {
|
|
28
|
+
const result = new Map<string, string[]>();
|
|
29
|
+
|
|
30
|
+
await Promise.all(
|
|
31
|
+
collectionsOf().map(async (name) => {
|
|
32
|
+
try {
|
|
33
|
+
const pages = await queryCollection(event, name)
|
|
34
|
+
.select('path')
|
|
35
|
+
.order('path', 'ASC')
|
|
36
|
+
.all();
|
|
37
|
+
|
|
38
|
+
result.set(
|
|
39
|
+
name,
|
|
40
|
+
pages.map((page) => page.path)
|
|
41
|
+
);
|
|
42
|
+
} catch {
|
|
43
|
+
// A collection Content dropped is exactly what the Checks panel is for;
|
|
44
|
+
// here it is simply a collection with no pages.
|
|
45
|
+
result.set(name, []);
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The sources table — what a consumer wrote, as the site resolved it. */
|
|
54
|
+
export function sourcesPanel(): string {
|
|
55
|
+
return renderSources(resolvedSources(), context.appConfigFile);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The path debugger: which source claims a URL, and what it finds there. */
|
|
59
|
+
export async function pathsPanel(
|
|
60
|
+
event: H3Event,
|
|
61
|
+
input: string
|
|
62
|
+
): Promise<string> {
|
|
63
|
+
const sources = resolvedSources();
|
|
64
|
+
const locales = context.locales;
|
|
65
|
+
|
|
66
|
+
if (!input) {
|
|
67
|
+
return renderPaths({
|
|
68
|
+
input,
|
|
69
|
+
locales,
|
|
70
|
+
fallbackLocale: context.fallbackLocale,
|
|
71
|
+
sources,
|
|
72
|
+
found: null,
|
|
73
|
+
byCollection: new Map()
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The same two pure calls the renderer walks the reader through, repeated
|
|
78
|
+
// here only to know WHICH collection to query. Cheap, and it keeps the
|
|
79
|
+
// renderer free of the database.
|
|
80
|
+
//
|
|
81
|
+
// The locale is carried into the choice rather than thrown away with the
|
|
82
|
+
// segment: a translation lives under the SAME prefix as its original, so a
|
|
83
|
+
// lookup given only the stripped path answers with whichever collection
|
|
84
|
+
// sorted first — and told the reader about `docs` when they pasted a `/de/`
|
|
85
|
+
// URL.
|
|
86
|
+
const { locale, path: documentationPath } = splitLocalePath(input, locales);
|
|
87
|
+
const source = sourcesForRoute(
|
|
88
|
+
documentationPath,
|
|
89
|
+
locale,
|
|
90
|
+
sources,
|
|
91
|
+
context.fallbackLocale
|
|
92
|
+
)[0];
|
|
93
|
+
|
|
94
|
+
let found: FoundPage | null = null;
|
|
95
|
+
|
|
96
|
+
if (source) {
|
|
97
|
+
try {
|
|
98
|
+
found = (await queryCollection(
|
|
99
|
+
event,
|
|
100
|
+
source.collection as Parameters<typeof queryCollection>[1]
|
|
101
|
+
)
|
|
102
|
+
.path(documentationPath)
|
|
103
|
+
.first()) as unknown as FoundPage | null;
|
|
104
|
+
} catch {
|
|
105
|
+
found = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return renderPaths({
|
|
110
|
+
input,
|
|
111
|
+
locales,
|
|
112
|
+
fallbackLocale: context.fallbackLocale,
|
|
113
|
+
sources,
|
|
114
|
+
found,
|
|
115
|
+
byCollection: await pathsByCollection(event)
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Which version — and which language — carries which page. */
|
|
120
|
+
export async function versionsPanel(event: H3Event): Promise<string> {
|
|
121
|
+
return renderVersions(resolvedSources(), await pathsByCollection(event));
|
|
122
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { context } from './context';
|
|
4
|
+
import type { CacheEntry, LocaleFile, RedirectRule } from './render/system';
|
|
5
|
+
import {
|
|
6
|
+
renderCache,
|
|
7
|
+
renderConfig,
|
|
8
|
+
renderI18n,
|
|
9
|
+
renderRedirects
|
|
10
|
+
} from './render/system';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* What the panels about the machinery read: the merged config, the message
|
|
14
|
+
* catalogues, Content's download cache and the redirects that shipped.
|
|
15
|
+
*
|
|
16
|
+
* Each answers a question whose answer exists only as a side effect somewhere —
|
|
17
|
+
* inside a defu call, across seven JSON files, in a directory nobody opens, in
|
|
18
|
+
* a route-rule table generated at build time. This half does the reading;
|
|
19
|
+
* `render/system.ts` turns it into tables.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/* -------------------------------------------------------------------------- */
|
|
23
|
+
/* Config */
|
|
24
|
+
/* -------------------------------------------------------------------------- */
|
|
25
|
+
|
|
26
|
+
/** The merged config, with the side of the merge each value came from. */
|
|
27
|
+
export function configPanel(): string {
|
|
28
|
+
const { duxt } = useAppConfig() as { duxt?: Partial<DuxtConfig> };
|
|
29
|
+
|
|
30
|
+
return renderConfig((duxt ?? {}) as unknown as Record<string, unknown>);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* -------------------------------------------------------------------------- */
|
|
34
|
+
/* i18n */
|
|
35
|
+
/* -------------------------------------------------------------------------- */
|
|
36
|
+
|
|
37
|
+
/** Every message key of every locale file the layer ships. */
|
|
38
|
+
export function i18nPanel(): string {
|
|
39
|
+
const root = join(context.layerDir, 'i18n/locales');
|
|
40
|
+
|
|
41
|
+
let dirs: string[];
|
|
42
|
+
try {
|
|
43
|
+
dirs = readdirSync(root, { withFileTypes: true })
|
|
44
|
+
.filter((entry) => entry.isDirectory())
|
|
45
|
+
.map((entry) => entry.name)
|
|
46
|
+
.sort();
|
|
47
|
+
} catch {
|
|
48
|
+
return '<div class="note">No <code>i18n/locales</code> directory in the layer.</div>';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const files: LocaleFile[] = dirs.map((dir) => ({
|
|
52
|
+
dir,
|
|
53
|
+
keys: [...keysOf(join(root, dir, 'duxt'))]
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
return renderI18n({
|
|
57
|
+
files,
|
|
58
|
+
locales: context.locales,
|
|
59
|
+
defaultLocale: context.defaultLocale
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function keysOf(namespaces: string): Set<string> {
|
|
64
|
+
const keys = new Set<string>();
|
|
65
|
+
|
|
66
|
+
let files: string[];
|
|
67
|
+
try {
|
|
68
|
+
files = readdirSync(namespaces).filter((file) => file.endsWith('.json'));
|
|
69
|
+
} catch {
|
|
70
|
+
return keys;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
try {
|
|
75
|
+
flatten(
|
|
76
|
+
JSON.parse(readFileSync(join(namespaces, file), 'utf8')) as unknown,
|
|
77
|
+
'',
|
|
78
|
+
keys
|
|
79
|
+
);
|
|
80
|
+
} catch {
|
|
81
|
+
keys.add(`${file}: unreadable`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return keys;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function flatten(node: unknown, prefix: string, out: Set<string>): void {
|
|
89
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
|
90
|
+
if (prefix) out.add(prefix);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
|
|
95
|
+
flatten(value, prefix ? `${prefix}.${key}` : key, out);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* -------------------------------------------------------------------------- */
|
|
100
|
+
/* Cache */
|
|
101
|
+
/* -------------------------------------------------------------------------- */
|
|
102
|
+
|
|
103
|
+
/** Content's download cache, and a way to drop one entry. */
|
|
104
|
+
export function cachePanel(message?: string): string {
|
|
105
|
+
let entries: CacheEntry[] | undefined;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
entries = readdirSync(context.dataDir, { withFileTypes: true })
|
|
109
|
+
.filter((entry) => entry.isDirectory())
|
|
110
|
+
.map((entry) => {
|
|
111
|
+
const path = join(context.dataDir, entry.name);
|
|
112
|
+
return {
|
|
113
|
+
name: entry.name,
|
|
114
|
+
size: sizeOf(path),
|
|
115
|
+
modified: statSync(path).mtime
|
|
116
|
+
};
|
|
117
|
+
})
|
|
118
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
119
|
+
} catch {
|
|
120
|
+
entries = undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return renderCache({ dataDir: context.dataDir, entries, message });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sizeOf(path: string): number {
|
|
127
|
+
let total = 0;
|
|
128
|
+
|
|
129
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
130
|
+
const child = join(path, entry.name);
|
|
131
|
+
total += entry.isDirectory() ? sizeOf(child) : statSync(child).size;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return total;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* -------------------------------------------------------------------------- */
|
|
138
|
+
/* Redirects */
|
|
139
|
+
/* -------------------------------------------------------------------------- */
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Every redirect the server ships, and which of them the layer wrote.
|
|
143
|
+
*
|
|
144
|
+
* Read rather than recomputed on purpose: a rule that was generated and a rule
|
|
145
|
+
* that shipped are different claims, and only the second one redirects anybody.
|
|
146
|
+
* Reading them back means reading EVERY module's, so `modules/redirects.ts`
|
|
147
|
+
* records which paths are its own beside them — nothing in a route rule says
|
|
148
|
+
* who wrote it.
|
|
149
|
+
*/
|
|
150
|
+
export function redirectsPanel(): string {
|
|
151
|
+
const config = useRuntimeConfig() as {
|
|
152
|
+
nitro?: { routeRules?: Record<string, { redirect?: unknown }> };
|
|
153
|
+
duxt?: { redirects?: string[] };
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const rules = config.nitro?.routeRules;
|
|
157
|
+
if (!rules) return renderRedirects();
|
|
158
|
+
|
|
159
|
+
const ours = new Set(config.duxt?.redirects ?? []);
|
|
160
|
+
|
|
161
|
+
const redirects: RedirectRule[] = Object.entries(rules)
|
|
162
|
+
.filter(([, rule]) => rule?.redirect)
|
|
163
|
+
.map(([from, rule]) => {
|
|
164
|
+
const redirect = rule.redirect as
|
|
165
|
+
| string
|
|
166
|
+
| { to?: string; statusCode?: number };
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
from,
|
|
170
|
+
to: typeof redirect === 'string' ? redirect : (redirect.to ?? '?'),
|
|
171
|
+
status:
|
|
172
|
+
typeof redirect === 'string' ? '302' : (redirect.statusCode ?? 302),
|
|
173
|
+
mine: ours.has(from)
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return renderRedirects(redirects);
|
|
178
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { queryCollection } from '@nuxt/content/nitro';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The documentation's table of contents, for a model.
|
|
5
|
+
*
|
|
6
|
+
* Built on @nuxtjs/mcp-toolkit rather than a JSON endpoint of our own: it
|
|
7
|
+
* speaks the actual protocol through the official SDK, so a client connects to
|
|
8
|
+
* `/mcp` instead of someone writing a server around our shape.
|
|
9
|
+
*/
|
|
10
|
+
/** Every collection the manifest names — a versioned site has none called `docs`. */
|
|
11
|
+
function duxtCollections(): 'docs'[] {
|
|
12
|
+
const { duxt } = useAppConfig() as { duxt?: Partial<DuxtConfig> };
|
|
13
|
+
const names = duxt?.resolvedSources?.length
|
|
14
|
+
? duxt.resolvedSources.map((source) => source.collection)
|
|
15
|
+
: ['docs'];
|
|
16
|
+
return names as 'docs'[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default defineMcpTool({
|
|
20
|
+
name: 'list_pages',
|
|
21
|
+
title: 'List documentation pages',
|
|
22
|
+
description:
|
|
23
|
+
'Every page of this documentation with its path, title and description. ' +
|
|
24
|
+
'Start here, then read a page with `read_page`.',
|
|
25
|
+
annotations: { readOnlyHint: true },
|
|
26
|
+
|
|
27
|
+
// A tool without an `inputSchema` is called with ONE argument: the SDK hands
|
|
28
|
+
// the handler `extra` directly rather than `(args, extra)`. Written the other
|
|
29
|
+
// way round, `extra` was undefined and every call failed on `extra.event`.
|
|
30
|
+
async handler(extra) {
|
|
31
|
+
const pages = (
|
|
32
|
+
await Promise.all(
|
|
33
|
+
duxtCollections().map((name) =>
|
|
34
|
+
queryCollection(extra.event, name)
|
|
35
|
+
.select('path', 'title', 'description')
|
|
36
|
+
.all()
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
).flat();
|
|
40
|
+
|
|
41
|
+
const listing = pages
|
|
42
|
+
.filter((page) => page.path)
|
|
43
|
+
.sort((a, b) => a.path!.localeCompare(b.path!))
|
|
44
|
+
.map(
|
|
45
|
+
(page) =>
|
|
46
|
+
`- ${page.path} — ${page.title}${page.description ? `: ${page.description}` : ''}`
|
|
47
|
+
)
|
|
48
|
+
.join('\n');
|
|
49
|
+
|
|
50
|
+
return { content: [{ type: 'text', text: listing }] };
|
|
51
|
+
}
|
|
52
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { queryCollection } from '@nuxt/content/nitro';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One page's prose. Returns the stored content rather than rendered HTML — a
|
|
6
|
+
* model reading documentation wants the text, not the theme around it.
|
|
7
|
+
*/
|
|
8
|
+
/** Every collection the manifest names — a versioned site has none called `docs`. */
|
|
9
|
+
function duxtCollections(): 'docs'[] {
|
|
10
|
+
const { duxt } = useAppConfig() as { duxt?: Partial<DuxtConfig> };
|
|
11
|
+
const names = duxt?.resolvedSources?.length
|
|
12
|
+
? duxt.resolvedSources.map((source) => source.collection)
|
|
13
|
+
: ['docs'];
|
|
14
|
+
return names as 'docs'[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default defineMcpTool({
|
|
18
|
+
name: 'read_page',
|
|
19
|
+
title: 'Read a documentation page',
|
|
20
|
+
description: 'The full content of one page. Paths come from `list_pages`.',
|
|
21
|
+
annotations: { readOnlyHint: true },
|
|
22
|
+
|
|
23
|
+
inputSchema: {
|
|
24
|
+
path: z.string().describe('Page path, for example /guide/deploying')
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async handler({ path }, extra) {
|
|
28
|
+
// Try each collection: a path carries its own prefix, so at most one has it.
|
|
29
|
+
const found = await Promise.all(
|
|
30
|
+
duxtCollections().map((name) =>
|
|
31
|
+
queryCollection(extra.event, name).path(path).first()
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
const page = found.find(Boolean);
|
|
35
|
+
|
|
36
|
+
if (!page) {
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: 'text',
|
|
41
|
+
text: `No page at ${path}. Use \`list_pages\` for valid paths.`
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
isError: true
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: `# ${page.title}\n\n${page.description ?? ''}\n\n${JSON.stringify(page.body)}`
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { queryCollection } from '@nuxt/content/nitro';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Search, so a model does not have to read every page to find one.
|
|
6
|
+
*
|
|
7
|
+
* A LIKE over title and description rather than the FTS index the browser
|
|
8
|
+
* uses: that index is built client-side from the collection dump, and a server
|
|
9
|
+
* tool has the database itself.
|
|
10
|
+
*/
|
|
11
|
+
/** Every collection the manifest names — a versioned site has none called `docs`. */
|
|
12
|
+
function duxtCollections(): 'docs'[] {
|
|
13
|
+
const { duxt } = useAppConfig() as { duxt?: Partial<DuxtConfig> };
|
|
14
|
+
const names = duxt?.resolvedSources?.length
|
|
15
|
+
? duxt.resolvedSources.map((source) => source.collection)
|
|
16
|
+
: ['docs'];
|
|
17
|
+
return names as 'docs'[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default defineMcpTool({
|
|
21
|
+
name: 'search_docs',
|
|
22
|
+
title: 'Search the documentation',
|
|
23
|
+
description: 'Find pages whose title or description matches a term.',
|
|
24
|
+
annotations: { readOnlyHint: true },
|
|
25
|
+
|
|
26
|
+
inputSchema: {
|
|
27
|
+
query: z.string().describe('What to look for')
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async handler({ query }, extra) {
|
|
31
|
+
const term = `%${query}%`;
|
|
32
|
+
|
|
33
|
+
// Both conditions in ONE group. Content joins the query's top-level
|
|
34
|
+
// conditions with AND regardless of how each was added, so a `where()`
|
|
35
|
+
// followed by an `orWhere()` asks for a term in the title AND in the
|
|
36
|
+
// description — which matched nothing at all. Inside a group the operator
|
|
37
|
+
// is the one the group was opened with, so `orWhere` gives title OR
|
|
38
|
+
// description.
|
|
39
|
+
const pages = (
|
|
40
|
+
await Promise.all(
|
|
41
|
+
duxtCollections().map((name) =>
|
|
42
|
+
queryCollection(extra.event, name)
|
|
43
|
+
.select('path', 'title', 'description')
|
|
44
|
+
.orWhere((group) =>
|
|
45
|
+
group
|
|
46
|
+
.where('title', 'LIKE', term)
|
|
47
|
+
.where('description', 'LIKE', term)
|
|
48
|
+
)
|
|
49
|
+
.all()
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
).flat();
|
|
53
|
+
|
|
54
|
+
if (!pages.length) {
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: 'text', text: `Nothing matches "${query}".` }]
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: 'text',
|
|
64
|
+
text: pages
|
|
65
|
+
.map(
|
|
66
|
+
(page) =>
|
|
67
|
+
`- ${page.path} — ${page.title}${page.description ? `: ${page.description}` : ''}`
|
|
68
|
+
)
|
|
69
|
+
.join('\n')
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { queryCollection } from '@nuxt/content/nitro';
|
|
2
|
+
import { duxtDefaults, mergeDuxtConfig } from '../../app/utils/duxt-config';
|
|
3
|
+
import { stripFrontmatter } from '../utils/duxt-server-text';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `…/guide/deploying.md` serves the page's Markdown source.
|
|
7
|
+
*
|
|
8
|
+
* What "View as Markdown" opens, and what the ChatGPT and Claude links hand
|
|
9
|
+
* over — a model given the rendered HTML has to undo a layout to find a
|
|
10
|
+
* heading, and one given this reads what the author wrote.
|
|
11
|
+
*
|
|
12
|
+
* A MIDDLEWARE, not a route. Nitro's router matches path segments, and `.md` is
|
|
13
|
+
* a suffix on the last one rather than a segment of its own, so there is no
|
|
14
|
+
* pattern that means "any path ending in .md". The middleware answers those and
|
|
15
|
+
* hands everything else straight on.
|
|
16
|
+
*/
|
|
17
|
+
export default defineEventHandler(async (event) => {
|
|
18
|
+
const path = event.path?.split('?')[0];
|
|
19
|
+
if (!path?.endsWith('.md')) return;
|
|
20
|
+
|
|
21
|
+
const appConfig = useAppConfig() as { duxt?: Partial<DuxtConfig> };
|
|
22
|
+
const duxt = mergeDuxtConfig(appConfig.duxt, duxtDefaults);
|
|
23
|
+
|
|
24
|
+
const wanted = stripLocale(path.slice(0, -'.md'.length));
|
|
25
|
+
|
|
26
|
+
// Longest prefix first, exactly as the app resolves a route: `/app/v2` beats
|
|
27
|
+
// `/app` on `/app/v2/guide`.
|
|
28
|
+
const sources = [...(duxt.resolvedSources ?? [])].sort(
|
|
29
|
+
(a, b) => b.prefix.length - a.prefix.length
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const source =
|
|
33
|
+
sources.find(
|
|
34
|
+
(entry) =>
|
|
35
|
+
!entry.prefix ||
|
|
36
|
+
wanted === entry.prefix ||
|
|
37
|
+
wanted.startsWith(`${entry.prefix}/`)
|
|
38
|
+
) ?? sources[0];
|
|
39
|
+
|
|
40
|
+
if (!source) return;
|
|
41
|
+
|
|
42
|
+
const page = await queryCollection(
|
|
43
|
+
event,
|
|
44
|
+
source.collection as Parameters<typeof queryCollection>[1]
|
|
45
|
+
)
|
|
46
|
+
.path(wanted)
|
|
47
|
+
.select('title', 'rawbody')
|
|
48
|
+
.first();
|
|
49
|
+
|
|
50
|
+
// No page is not this middleware's error to raise: falling through lets the
|
|
51
|
+
// app answer with its own 404, which knows how to suggest a near miss.
|
|
52
|
+
if (!page) return;
|
|
53
|
+
|
|
54
|
+
const body = (page as { rawbody?: string }).rawbody;
|
|
55
|
+
if (!body) return;
|
|
56
|
+
|
|
57
|
+
setHeader(event, 'content-type', 'text/markdown; charset=utf-8');
|
|
58
|
+
return stripFrontmatter(body).trim();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The locale segment, off.
|
|
63
|
+
*
|
|
64
|
+
* The browser is on `/de-DE/guide/deploying`; the page is `/guide/deploying`.
|
|
65
|
+
* Same distinction `stripLocalePrefix` makes in the app, made again here
|
|
66
|
+
* because Nitro has no i18n composable to ask.
|
|
67
|
+
*/
|
|
68
|
+
function stripLocale(path: string): string {
|
|
69
|
+
const first = path.split('/')[1];
|
|
70
|
+
if (!first) return path;
|
|
71
|
+
|
|
72
|
+
const locales = (
|
|
73
|
+
useRuntimeConfig().public as {
|
|
74
|
+
i18n?: { locales?: ({ code: string } | string)[] };
|
|
75
|
+
}
|
|
76
|
+
).i18n?.locales;
|
|
77
|
+
|
|
78
|
+
const codes = (locales ?? []).map((locale) =>
|
|
79
|
+
typeof locale === 'string' ? locale : locale.code
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (!codes.includes(first)) return path;
|
|
83
|
+
|
|
84
|
+
const rest = path.slice(first.length + 1);
|
|
85
|
+
return rest.startsWith('/') ? rest : rest || '/';
|
|
86
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { queryCollection } from '@nuxt/content/nitro';
|
|
2
|
+
// Imported by path, not auto-import: app/ belongs to the Vue app, and Nitro
|
|
3
|
+
// does not see its utils. The defaults have to come from the same file the
|
|
4
|
+
// pages use, or the two descriptions drift apart.
|
|
5
|
+
import { duxtDefaults, mergeDuxtConfig } from '../../app/utils/duxt-config';
|
|
6
|
+
import {
|
|
7
|
+
resolveServerTexts,
|
|
8
|
+
stripFrontmatter
|
|
9
|
+
} from '../utils/duxt-server-text';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* llms-full.txt — the whole documentation as one Markdown file.
|
|
13
|
+
*
|
|
14
|
+
* The companion to `llms.txt`, which is an index: a model that has read the
|
|
15
|
+
* index still has to fetch sixty pages, and a model given this one has read
|
|
16
|
+
* them. The convention (llmstxt.org) names both.
|
|
17
|
+
*
|
|
18
|
+
* The Markdown as written, not the rendered HTML — which is why the collection
|
|
19
|
+
* schema asks Content for `rawbody`. An MDC block reaches the reader as the
|
|
20
|
+
* component call it is, and a code fence is still a fence.
|
|
21
|
+
*/
|
|
22
|
+
export default defineEventHandler(async (event) => {
|
|
23
|
+
const appConfig = useAppConfig() as { duxt?: Partial<DuxtConfig> };
|
|
24
|
+
// Resolved here because Nitro has no i18n: without it the description is
|
|
25
|
+
// printed as the translation key it is.
|
|
26
|
+
const duxt = resolveServerTexts(
|
|
27
|
+
mergeDuxtConfig(appConfig.duxt, duxtDefaults)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const collections = duxt.resolvedSources?.length
|
|
31
|
+
? duxt.resolvedSources.map((source) => source.collection)
|
|
32
|
+
: ['docs'];
|
|
33
|
+
|
|
34
|
+
const pages = (
|
|
35
|
+
await Promise.all(
|
|
36
|
+
collections.map((name) =>
|
|
37
|
+
queryCollection(event, name as Parameters<typeof queryCollection>[1])
|
|
38
|
+
.select('path', 'title', 'description', 'rawbody')
|
|
39
|
+
.all()
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
).flat();
|
|
43
|
+
|
|
44
|
+
const origin = getRequestURL(event).origin;
|
|
45
|
+
|
|
46
|
+
const lines = [
|
|
47
|
+
`# ${duxt.title}`,
|
|
48
|
+
'',
|
|
49
|
+
`> ${duxt.landing?.description ?? 'Documentation.'}`,
|
|
50
|
+
''
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
for (const page of pages
|
|
54
|
+
.filter((page) => page.path)
|
|
55
|
+
.sort((a, b) => a.path!.localeCompare(b.path!))) {
|
|
56
|
+
lines.push(
|
|
57
|
+
`---`,
|
|
58
|
+
'',
|
|
59
|
+
`# ${page.title ?? page.path}`,
|
|
60
|
+
'',
|
|
61
|
+
`Source: ${origin}${page.path}`,
|
|
62
|
+
''
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (page.description) lines.push(`> ${page.description}`, '');
|
|
66
|
+
|
|
67
|
+
// A page whose collection was declared before `rawbody` was asked for has
|
|
68
|
+
// none. Its title and URL are still worth listing.
|
|
69
|
+
const body = (page as { rawbody?: string }).rawbody;
|
|
70
|
+
if (body) lines.push(stripFrontmatter(body).trim(), '');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setHeader(event, 'content-type', 'text/plain; charset=utf-8');
|
|
74
|
+
return lines.join('\n');
|
|
75
|
+
});
|