@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,461 @@
|
|
|
1
|
+
import type { DuxtSource } from '../../sources-resolve';
|
|
2
|
+
import { duxtManifest } from '../../sections-resolve';
|
|
3
|
+
import type { PageRecord } from '../../validate-report';
|
|
4
|
+
import { page, PANELS } from './shell';
|
|
5
|
+
import type { Doc, DocGroup, IndexedSection } from './render/content';
|
|
6
|
+
import { renderChecks, renderPages, renderSearch } from './render/content';
|
|
7
|
+
import { renderPaths, renderSources, renderVersions } from './render/sources';
|
|
8
|
+
import type { RedirectRule } from './render/system';
|
|
9
|
+
import {
|
|
10
|
+
renderCache,
|
|
11
|
+
renderConfig,
|
|
12
|
+
renderI18n,
|
|
13
|
+
renderRedirects
|
|
14
|
+
} from './render/system';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Every panel, rendered from a site that does not exist.
|
|
18
|
+
*
|
|
19
|
+
* The documentation has to show what a panel looks like, and the two obvious
|
|
20
|
+
* ways are both bad: a screenshot goes stale the day a column is added, and
|
|
21
|
+
* pointing the reader at the real route only works inside a dev server — the
|
|
22
|
+
* tab is not registered in a build at all, so a published page would frame a
|
|
23
|
+
* 404.
|
|
24
|
+
*
|
|
25
|
+
* So the panels are rendered here through the SAME functions the tab uses,
|
|
26
|
+
* over a fixture site. What the reader sees in the documentation is the real
|
|
27
|
+
* renderer's output; when a panel gains a column, the pictures in the docs
|
|
28
|
+
* gain it in the same commit, because there are no pictures.
|
|
29
|
+
*
|
|
30
|
+
* The fixture is one imagined site, not ten unrelated tables: `acme/sdk` in two
|
|
31
|
+
* versions and two languages, `acme/cli` in one — which is the smallest shape
|
|
32
|
+
* that makes every panel say something. A single-source site would render a
|
|
33
|
+
* Versions tab with nothing in it.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const SOURCES: DuxtSource[] = [
|
|
37
|
+
{
|
|
38
|
+
path: 'docs',
|
|
39
|
+
repo: 'acme/sdk',
|
|
40
|
+
refs: [
|
|
41
|
+
{ branch: 'main', label: 'v2' },
|
|
42
|
+
{ tag: 'v1.9.4', label: 'v1.9', status: 'deprecated' }
|
|
43
|
+
],
|
|
44
|
+
locales: ['en', 'de'],
|
|
45
|
+
history: true,
|
|
46
|
+
// A generated section, because a fixture without one shows a Sources table
|
|
47
|
+
// that cannot be told apart from the one this layer had before sections
|
|
48
|
+
// existed — and a Checks panel that never demonstrates the findings an
|
|
49
|
+
// artefact produces.
|
|
50
|
+
generated: [{ type: 'changelog', path: 'CHANGELOG.md', label: 'Releases' }]
|
|
51
|
+
},
|
|
52
|
+
{ path: 'docs', repo: 'acme/cli' }
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const RESOLVED = duxtManifest(SOURCES, { defaultLocale: 'en' });
|
|
56
|
+
|
|
57
|
+
const ROOT = '/srv/acme-docs';
|
|
58
|
+
|
|
59
|
+
/** The layer's own config, as this fixture site would have written it. */
|
|
60
|
+
const CONSUMER = {
|
|
61
|
+
title: 'Acme SDK',
|
|
62
|
+
version: 'v2.4.0',
|
|
63
|
+
sources: SOURCES,
|
|
64
|
+
resolvedSources: RESOLVED,
|
|
65
|
+
landing: {
|
|
66
|
+
headline: {
|
|
67
|
+
en: 'Ship the SDK, not the glue',
|
|
68
|
+
de: 'Weniger Kleber, mehr SDK'
|
|
69
|
+
},
|
|
70
|
+
command: 'npm i @acme/sdk'
|
|
71
|
+
},
|
|
72
|
+
footer: { note: '© Acme Inc.' }
|
|
73
|
+
} as unknown as Record<string, unknown>;
|
|
74
|
+
|
|
75
|
+
/* -------------------------------------------------------------------------- */
|
|
76
|
+
/* The pages the fixture site serves */
|
|
77
|
+
/* -------------------------------------------------------------------------- */
|
|
78
|
+
|
|
79
|
+
interface PageSeed {
|
|
80
|
+
path: string;
|
|
81
|
+
title?: string;
|
|
82
|
+
german?: string;
|
|
83
|
+
description?: boolean;
|
|
84
|
+
icon?: string;
|
|
85
|
+
file: string;
|
|
86
|
+
updated?: string;
|
|
87
|
+
contributors?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const SDK_PAGES: PageSeed[] = [
|
|
91
|
+
{
|
|
92
|
+
path: '/',
|
|
93
|
+
title: 'Acme SDK',
|
|
94
|
+
german: 'Acme SDK',
|
|
95
|
+
description: true,
|
|
96
|
+
icon: 'lucide:book-open-text',
|
|
97
|
+
file: 'index.md',
|
|
98
|
+
updated: '2026-02-11',
|
|
99
|
+
contributors: 6
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
path: '/getting-started',
|
|
103
|
+
title: 'Getting started',
|
|
104
|
+
german: 'Erste Schritte',
|
|
105
|
+
description: true,
|
|
106
|
+
icon: 'lucide:rocket',
|
|
107
|
+
file: '1.getting-started/index.md',
|
|
108
|
+
updated: '2026-02-09',
|
|
109
|
+
contributors: 4
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
path: '/getting-started/install',
|
|
113
|
+
title: 'Install',
|
|
114
|
+
german: 'Installation',
|
|
115
|
+
description: true,
|
|
116
|
+
file: '1.getting-started/1.install.md',
|
|
117
|
+
updated: '2026-01-28',
|
|
118
|
+
contributors: 3
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
path: '/guides/deploying',
|
|
122
|
+
title: 'Deploying',
|
|
123
|
+
german: 'Deployen',
|
|
124
|
+
description: true,
|
|
125
|
+
icon: 'lucide:ship',
|
|
126
|
+
file: '2.guides/1.deploying.md',
|
|
127
|
+
updated: '2026-02-10',
|
|
128
|
+
contributors: 2
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
path: '/guides/caching',
|
|
132
|
+
title: 'Caching',
|
|
133
|
+
description: true,
|
|
134
|
+
file: '2.guides/2.caching.md',
|
|
135
|
+
updated: '2026-02-12',
|
|
136
|
+
contributors: 1
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
path: '/reference/client',
|
|
140
|
+
title: 'Client',
|
|
141
|
+
german: 'Client',
|
|
142
|
+
description: true,
|
|
143
|
+
icon: 'lucide:plug',
|
|
144
|
+
file: '3.reference/1.client.md',
|
|
145
|
+
updated: '2026-02-04',
|
|
146
|
+
contributors: 5
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
// The row every Pages panel is opened for: a page that made it into the
|
|
150
|
+
// build with no frontmatter to speak of.
|
|
151
|
+
path: '/reference/errors',
|
|
152
|
+
file: '3.reference/2.errors.md',
|
|
153
|
+
updated: '2025-12-19',
|
|
154
|
+
contributors: 1
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
path: '/changelog',
|
|
158
|
+
title: 'Changelog',
|
|
159
|
+
description: true,
|
|
160
|
+
icon: 'lucide:history',
|
|
161
|
+
file: '4.changelog.md',
|
|
162
|
+
updated: '2026-02-12',
|
|
163
|
+
contributors: 8
|
|
164
|
+
}
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const CLI_PAGES: PageSeed[] = [
|
|
168
|
+
{
|
|
169
|
+
path: '/',
|
|
170
|
+
title: 'Acme CLI',
|
|
171
|
+
description: true,
|
|
172
|
+
icon: 'lucide:terminal',
|
|
173
|
+
file: 'index.md'
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
path: '/getting-started',
|
|
177
|
+
title: 'Getting started',
|
|
178
|
+
description: true,
|
|
179
|
+
file: '1.getting-started.md'
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
path: '/commands',
|
|
183
|
+
title: 'Commands',
|
|
184
|
+
description: true,
|
|
185
|
+
icon: 'lucide:list',
|
|
186
|
+
file: '2.commands/index.md'
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
path: '/commands/run',
|
|
190
|
+
title: 'acme run',
|
|
191
|
+
description: true,
|
|
192
|
+
file: '2.commands/1.run.md'
|
|
193
|
+
}
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Which logical page each collection is missing.
|
|
198
|
+
*
|
|
199
|
+
* The gaps are the point. A translation lagging two pages behind and a version
|
|
200
|
+
* that predates a guide are exactly what the Versions matrix exists to show,
|
|
201
|
+
* and a fixture where every column is a full house would show nothing at all.
|
|
202
|
+
*/
|
|
203
|
+
const MISSING: Record<string, string[]> = {
|
|
204
|
+
docs_de_sdk: ['/guides/caching', '/changelog'],
|
|
205
|
+
docs_sdk_v1_9: ['/guides/caching'],
|
|
206
|
+
docs_de_sdk_v1_9: ['/guides/caching', '/changelog', '/reference/errors']
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The pages a changelog becomes: an overview, then one release each.
|
|
211
|
+
*
|
|
212
|
+
* Declared beside the docs seeds rather than inside them because they are not
|
|
213
|
+
* a docs tree — no `description`, no history of their own, and a name this
|
|
214
|
+
* layer invented rather than one anybody wrote.
|
|
215
|
+
*/
|
|
216
|
+
const RELEASES: { path: string; title: string; file: string }[] = [
|
|
217
|
+
{ path: '', title: 'Releases', file: 'index.md' },
|
|
218
|
+
{ path: '/0.2.0', title: '0.2.0', file: '1.0.2.0.md' },
|
|
219
|
+
{ path: '/0.1.1', title: '0.1.1', file: '2.0.1.1.md' },
|
|
220
|
+
{ path: '/0.1.0', title: '0.1.0', file: '3.0.1.0.md' }
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
// What reading that CHANGELOG said about it. A near miss rather than a clean
|
|
224
|
+
// read, for the same reason the docs tree has a stale link in it: the panel is
|
|
225
|
+
// documented by what it looks like when something is wrong.
|
|
226
|
+
for (const source of RESOLVED) {
|
|
227
|
+
if (!source.generated) continue;
|
|
228
|
+
|
|
229
|
+
source.generated.report = {
|
|
230
|
+
pages: RELEASES.length,
|
|
231
|
+
warnings: [
|
|
232
|
+
'the heading "1.3 (Feb 2026)" on line 41 reads like a release but does ' +
|
|
233
|
+
'not parse as one, so it became part of the release above it instead ' +
|
|
234
|
+
'of a page of its own. A release heading is a version, optionally ' +
|
|
235
|
+
'followed by a date in parentheses: `## 1.4.0 (2024-02-01)`.'
|
|
236
|
+
]
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function docsOf(source: DuxtResolvedSource): Doc[] {
|
|
241
|
+
if (source.generated) {
|
|
242
|
+
return RELEASES.map((release) => ({
|
|
243
|
+
path: `${source.prefix}${release.path}`,
|
|
244
|
+
title: release.title,
|
|
245
|
+
id: `${source.collection}/${release.file}`
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const seeds = source.repo === 'cli' ? CLI_PAGES : SDK_PAGES;
|
|
250
|
+
const skip = new Set(MISSING[source.collection] ?? []);
|
|
251
|
+
const german = source.locale === 'de';
|
|
252
|
+
|
|
253
|
+
return seeds
|
|
254
|
+
.filter((seed) => !skip.has(seed.path))
|
|
255
|
+
.map((seed) => ({
|
|
256
|
+
path: `${source.prefix}${seed.path === '/' ? '' : seed.path}` || '/',
|
|
257
|
+
title: german ? seed.german : seed.title,
|
|
258
|
+
description: seed.description
|
|
259
|
+
? 'One line, for the card and the OG image.'
|
|
260
|
+
: undefined,
|
|
261
|
+
icon: seed.icon,
|
|
262
|
+
id: `${source.collection}/${german ? 'de/' : ''}${seed.file}`,
|
|
263
|
+
lastUpdated:
|
|
264
|
+
source.history && seed.updated
|
|
265
|
+
? `${seed.updated}T09:12:00Z`
|
|
266
|
+
: undefined,
|
|
267
|
+
contributors: Array.from(
|
|
268
|
+
{ length: seed.contributors ?? 0 },
|
|
269
|
+
(_, index) => ({
|
|
270
|
+
name: `Contributor ${index + 1}`
|
|
271
|
+
})
|
|
272
|
+
)
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const GROUPS: DocGroup[] = RESOLVED.map((source) => ({
|
|
277
|
+
collection: source.collection,
|
|
278
|
+
docs: docsOf(source)
|
|
279
|
+
}));
|
|
280
|
+
|
|
281
|
+
const BY_COLLECTION = new Map(
|
|
282
|
+
GROUPS.map((group) => [group.collection, group.docs.map((doc) => doc.path)])
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The same pages as the validator sees them.
|
|
287
|
+
*
|
|
288
|
+
* Two deliberate faults, because a Checks panel with nothing in it teaches
|
|
289
|
+
* nobody what a finding looks like: one link to a page that was renamed, and
|
|
290
|
+
* one page with no frontmatter.
|
|
291
|
+
*/
|
|
292
|
+
const RECORDS: PageRecord[] = GROUPS.flatMap((group) =>
|
|
293
|
+
group.docs.map((doc) => ({
|
|
294
|
+
collection: group.collection,
|
|
295
|
+
path: doc.path,
|
|
296
|
+
file: doc.id ?? doc.path,
|
|
297
|
+
title: doc.title,
|
|
298
|
+
description: doc.description,
|
|
299
|
+
anchors: new Set(['install', 'usage']),
|
|
300
|
+
links:
|
|
301
|
+
doc.path.endsWith('/getting-started') && group.collection === 'docs_sdk'
|
|
302
|
+
? [{ href: '/guides/cacheing' }, { href: '/guides/deploying#install' }]
|
|
303
|
+
: []
|
|
304
|
+
}))
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
const SECTIONS: IndexedSection[] = [
|
|
308
|
+
{
|
|
309
|
+
collection: 'docs_sdk',
|
|
310
|
+
id: 'docs_sdk/1.getting-started/1.install.md#install',
|
|
311
|
+
title: 'Install',
|
|
312
|
+
titles: ['Getting started'],
|
|
313
|
+
content:
|
|
314
|
+
'Add the package with your package manager of choice. The SDK ships types, so nothing else is needed to get autocompletion in an editor.'
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
collection: 'docs_sdk',
|
|
318
|
+
id: 'docs_sdk/2.guides/1.deploying.md#build-output',
|
|
319
|
+
title: 'Build output',
|
|
320
|
+
titles: ['Deploying'],
|
|
321
|
+
content:
|
|
322
|
+
'The build writes a single server bundle and a public directory. Both are what a host needs; nothing else in the repository is read at runtime.'
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
collection: 'docs_sdk',
|
|
326
|
+
id: 'docs_sdk/2.guides/2.caching.md#invalidation',
|
|
327
|
+
title: 'Invalidation',
|
|
328
|
+
titles: ['Guides', 'Caching'],
|
|
329
|
+
content:
|
|
330
|
+
'A cached response is keyed by the request path and the account tier, so a change of plan is visible without a purge.'
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
collection: 'docs_de_sdk',
|
|
334
|
+
id: 'docs_de_sdk/de/1.getting-started/1.install.md#installation',
|
|
335
|
+
title: 'Installation',
|
|
336
|
+
titles: ['Erste Schritte'],
|
|
337
|
+
content:
|
|
338
|
+
'Das Paket wird mit dem Paketmanager der Wahl hinzugefügt. Typen liegen bei, mehr braucht ein Editor nicht.'
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
collection: 'docs_cli',
|
|
342
|
+
id: 'docs_cli/2.commands/1.run.md#flags',
|
|
343
|
+
title: 'Flags',
|
|
344
|
+
titles: ['Commands', 'acme run'],
|
|
345
|
+
content:
|
|
346
|
+
'Every flag has a long form. The short forms exist for the three that are typed most often and are listed beside them.'
|
|
347
|
+
}
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
const LOCALE_KEYS = [
|
|
351
|
+
'duxt.nav.open',
|
|
352
|
+
'duxt.nav.close',
|
|
353
|
+
'duxt.search.placeholder',
|
|
354
|
+
'duxt.search.empty',
|
|
355
|
+
'duxt.toc.title',
|
|
356
|
+
'duxt.page.edit',
|
|
357
|
+
'duxt.page.updated',
|
|
358
|
+
'duxt.version.switch'
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
const CACHE = [
|
|
362
|
+
{
|
|
363
|
+
name: 'github.com-acme-sdk-main',
|
|
364
|
+
size: 4.6 * 1024 * 1024,
|
|
365
|
+
modified: new Date('2026-02-12T08:41:00Z')
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: 'github.com-acme-sdk-v1.9.4',
|
|
369
|
+
size: 3.9 * 1024 * 1024,
|
|
370
|
+
modified: new Date('2026-01-30T17:03:00Z')
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: 'github.com-acme-cli-main',
|
|
374
|
+
size: 820 * 1024,
|
|
375
|
+
modified: new Date('2026-02-11T12:20:00Z')
|
|
376
|
+
}
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
const REDIRECTS: RedirectRule[] = [
|
|
380
|
+
{
|
|
381
|
+
from: '/sdk/guides/deploy',
|
|
382
|
+
to: '/sdk/guides/deploying',
|
|
383
|
+
status: 301,
|
|
384
|
+
mine: true
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
from: '/de/sdk/guides/deploy',
|
|
388
|
+
to: '/de/sdk/guides/deploying',
|
|
389
|
+
status: 301,
|
|
390
|
+
mine: true
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
from: '/sdk/v1-9/guides/deploy',
|
|
394
|
+
to: '/sdk/v1-9/guides/deploying',
|
|
395
|
+
status: 301,
|
|
396
|
+
mine: true
|
|
397
|
+
},
|
|
398
|
+
{ from: '/sitemap.xml', to: '/sitemap_index.xml', status: 307, mine: false }
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
/* -------------------------------------------------------------------------- */
|
|
402
|
+
/* The panels */
|
|
403
|
+
/* -------------------------------------------------------------------------- */
|
|
404
|
+
|
|
405
|
+
/** The body of one panel, rendered from the fixture site. */
|
|
406
|
+
export function previewBody(slug: string): string {
|
|
407
|
+
switch (slug) {
|
|
408
|
+
case 'paths':
|
|
409
|
+
return renderPaths({
|
|
410
|
+
// A page that v2 has and v1.9 does not: the fallback's own case, and
|
|
411
|
+
// the reason this panel exists.
|
|
412
|
+
input: '/de/sdk/v1-9/guides/caching',
|
|
413
|
+
locales: ['en', 'de'],
|
|
414
|
+
fallbackLocale: 'en',
|
|
415
|
+
sources: RESOLVED,
|
|
416
|
+
found: null,
|
|
417
|
+
byCollection: BY_COLLECTION
|
|
418
|
+
});
|
|
419
|
+
case 'pages':
|
|
420
|
+
return renderPages(GROUPS, ROOT);
|
|
421
|
+
case 'versions':
|
|
422
|
+
return renderVersions(RESOLVED, BY_COLLECTION);
|
|
423
|
+
case 'checks':
|
|
424
|
+
return renderChecks(RESOLVED, RECORDS, ROOT);
|
|
425
|
+
case 'search':
|
|
426
|
+
return renderSearch('install', SECTIONS);
|
|
427
|
+
case 'config':
|
|
428
|
+
return renderConfig(CONSUMER);
|
|
429
|
+
case 'i18n':
|
|
430
|
+
return renderI18n({
|
|
431
|
+
files: [
|
|
432
|
+
{ dir: 'en', keys: LOCALE_KEYS },
|
|
433
|
+
{ dir: 'de', keys: LOCALE_KEYS },
|
|
434
|
+
{ dir: 'fr', keys: LOCALE_KEYS.slice(0, -2) },
|
|
435
|
+
{ dir: 'pt-BR', keys: [...LOCALE_KEYS, 'duxt.page.translate'] }
|
|
436
|
+
],
|
|
437
|
+
locales: ['en', 'de', 'fr', 'pt-BR'],
|
|
438
|
+
defaultLocale: 'en'
|
|
439
|
+
});
|
|
440
|
+
case 'cache':
|
|
441
|
+
return renderCache({ dataDir: `${ROOT}/.data/content`, entries: CACHE });
|
|
442
|
+
case 'redirects':
|
|
443
|
+
return renderRedirects(REDIRECTS);
|
|
444
|
+
default:
|
|
445
|
+
return renderSources(RESOLVED, `${ROOT}/app/app.config.ts`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** The file a preview is written to, and the name a docs page asks for. */
|
|
450
|
+
export const previewFile = (slug: string) => `${slug || 'sources'}.html`;
|
|
451
|
+
|
|
452
|
+
/** Every panel the documentation can embed. */
|
|
453
|
+
export const PREVIEW_TABS = PANELS.map((panel) => ({
|
|
454
|
+
...panel,
|
|
455
|
+
file: previewFile(panel.slug)
|
|
456
|
+
}));
|
|
457
|
+
|
|
458
|
+
/** One panel as a whole document, without the tab row — see `page`. */
|
|
459
|
+
export function previewPage(slug: string): string {
|
|
460
|
+
return page(slug, previewBody(slug), true);
|
|
461
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { PageRecord } from '../../../validate-report';
|
|
2
|
+
import { report } from '../../../validate-report';
|
|
3
|
+
import {
|
|
4
|
+
code,
|
|
5
|
+
dim,
|
|
6
|
+
editor,
|
|
7
|
+
escape,
|
|
8
|
+
filter,
|
|
9
|
+
row,
|
|
10
|
+
stat,
|
|
11
|
+
stats,
|
|
12
|
+
table,
|
|
13
|
+
tag
|
|
14
|
+
} from '../shell';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The three panels that read the content itself, rather than the config.
|
|
18
|
+
*
|
|
19
|
+
* All three describe what the RUNNING site holds: the queries in
|
|
20
|
+
* `../content.ts` ask Content's own database, the same one the pages render
|
|
21
|
+
* from, and hand the result here. A panel that parsed its own copy of the
|
|
22
|
+
* Markdown would answer about a site that does not exist.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface Doc {
|
|
26
|
+
path: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
icon?: string;
|
|
30
|
+
id?: string;
|
|
31
|
+
lastUpdated?: string;
|
|
32
|
+
contributors?: { name: string }[];
|
|
33
|
+
body?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DocGroup {
|
|
37
|
+
collection: string;
|
|
38
|
+
docs: Doc[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Every page, with what the frontmatter and the git history gave it. */
|
|
42
|
+
export function renderPages(groups: DocGroup[], rootDir = ''): string {
|
|
43
|
+
const total = groups.reduce((sum, group) => sum + group.docs.length, 0);
|
|
44
|
+
const untitled = groups.reduce(
|
|
45
|
+
(sum, group) => sum + group.docs.filter((doc) => !doc.title).length,
|
|
46
|
+
0
|
|
47
|
+
);
|
|
48
|
+
const dated = groups.reduce(
|
|
49
|
+
(sum, group) => sum + group.docs.filter((doc) => doc.lastUpdated).length,
|
|
50
|
+
0
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const summary = stats([
|
|
54
|
+
stat(total, 'pages'),
|
|
55
|
+
stat(groups.length, 'collections'),
|
|
56
|
+
stat(dated, 'with history', dated ? '' : 'warn'),
|
|
57
|
+
stat(untitled, 'without a title', untitled ? 'warn' : 'ok')
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
// One block per collection, the first opened: a site with five versions has
|
|
61
|
+
// five near-identical tables, and all five expanded is a page nobody reads.
|
|
62
|
+
const sections = groups.map(({ collection, docs }, index) => {
|
|
63
|
+
const rows = docs.map((doc) =>
|
|
64
|
+
row([
|
|
65
|
+
code(doc.path),
|
|
66
|
+
escape(doc.title) || tag('missing', 'warn'),
|
|
67
|
+
doc.description ? dim('yes') : tag('missing', 'warn'),
|
|
68
|
+
doc.icon ? code(doc.icon) : dim('—'),
|
|
69
|
+
doc.lastUpdated
|
|
70
|
+
? escape(doc.lastUpdated.slice(0, 10))
|
|
71
|
+
: dim('no history'),
|
|
72
|
+
doc.contributors?.length
|
|
73
|
+
? escape(String(doc.contributors.length))
|
|
74
|
+
: dim('—'),
|
|
75
|
+
editor(doc.id, doc.id?.split('/').slice(1).join('/'), rootDir)
|
|
76
|
+
])
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return `<details${index === 0 ? ' open' : ''}>
|
|
80
|
+
<summary>${escape(collection)} <span class="dim">${docs.length} pages</span></summary>
|
|
81
|
+
${docs.length > 12 ? filter(`Filter ${collection}`) : ''}
|
|
82
|
+
${table(
|
|
83
|
+
[
|
|
84
|
+
'Path',
|
|
85
|
+
'Title',
|
|
86
|
+
'Description',
|
|
87
|
+
'Icon',
|
|
88
|
+
'Last updated',
|
|
89
|
+
'Contributors',
|
|
90
|
+
'File'
|
|
91
|
+
],
|
|
92
|
+
rows,
|
|
93
|
+
'Content produced no pages here. Every URL under this prefix is a 404 — check the source’s `path` and `refs`; the Checks panel reports it as an error.'
|
|
94
|
+
)}
|
|
95
|
+
</details>`;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return `${summary}${sections.join('')}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The build validator, run against the served site.
|
|
103
|
+
*
|
|
104
|
+
* Identical checks to `pnpm build`, from `validate-report.ts` — the point is
|
|
105
|
+
* not a second opinion but a place where the findings stay put. A warning
|
|
106
|
+
* printed during a build has scrolled away by the time anyone reads it, and
|
|
107
|
+
* "last updated" on a broken link is exactly when you want the file name.
|
|
108
|
+
*/
|
|
109
|
+
export function renderChecks(
|
|
110
|
+
sources: DuxtResolvedSource[],
|
|
111
|
+
pages: PageRecord[],
|
|
112
|
+
rootDir = ''
|
|
113
|
+
): string {
|
|
114
|
+
const { errors, warnings, notes } = report(sources, pages);
|
|
115
|
+
|
|
116
|
+
const list = (findings: string[], kind: string) =>
|
|
117
|
+
table(
|
|
118
|
+
['', kind === 'error' ? 'Error' : 'Warning'],
|
|
119
|
+
findings.map((finding) =>
|
|
120
|
+
row([tag(kind, kind), fileLinked(finding, rootDir)])
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const summary = stats([
|
|
125
|
+
stat(pages.length, 'pages checked'),
|
|
126
|
+
stat(sources.length, 'collections'),
|
|
127
|
+
stat(errors.length, 'errors', errors.length ? 'error' : 'ok'),
|
|
128
|
+
stat(warnings.length, 'warnings', warnings.length ? 'warn' : 'ok')
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
// Notes are not findings and must not read like them: a language's coverage
|
|
132
|
+
// is a state of the site, and putting it in the warnings table would make
|
|
133
|
+
// every translated site look as though something were wrong with it.
|
|
134
|
+
//
|
|
135
|
+
// A note indented by two spaces belongs UNDER the line above it — the report
|
|
136
|
+
// is one summary per language followed by the files behind the figure, and
|
|
137
|
+
// HTML collapses that indent to a single space if it is left as text.
|
|
138
|
+
const coverage = notes.length
|
|
139
|
+
? `<h2>Translations</h2><ul class="steps">${notes
|
|
140
|
+
.map((note) =>
|
|
141
|
+
note.startsWith(' ')
|
|
142
|
+
? `<li class="dim" style="margin-left:1.5rem">${fileLinked(note.trim(), rootDir)}</li>`
|
|
143
|
+
: `<li><strong>${fileLinked(note, rootDir)}</strong></li>`
|
|
144
|
+
)
|
|
145
|
+
.join('')}</ul>`
|
|
146
|
+
: '';
|
|
147
|
+
|
|
148
|
+
return `${summary}${errors.length ? `<h2>Errors</h2>${list(errors, 'error')}` : ''}${
|
|
149
|
+
warnings.length ? `<h2>Warnings</h2>${list(warnings, 'warn')}` : ''
|
|
150
|
+
}${coverage}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Findings name their file in quotes; turn the first one into an editor link.
|
|
155
|
+
*
|
|
156
|
+
* The message stays exactly the string the build prints — the two must not
|
|
157
|
+
* drift — so the link is added by reading it rather than by threading a second
|
|
158
|
+
* field through the report.
|
|
159
|
+
*/
|
|
160
|
+
function fileLinked(finding: string, rootDir: string): string {
|
|
161
|
+
const match = /"([^"]+\.md)"/.exec(finding);
|
|
162
|
+
if (!match) return escape(finding);
|
|
163
|
+
|
|
164
|
+
const [quoted, file] = match;
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
escape(finding.slice(0, match.index)) +
|
|
168
|
+
editor(file, quoted, rootDir) +
|
|
169
|
+
escape(finding.slice(match.index + quoted.length))
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface IndexedSection {
|
|
174
|
+
collection: string;
|
|
175
|
+
id: string;
|
|
176
|
+
title?: string;
|
|
177
|
+
titles?: string[];
|
|
178
|
+
content?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* What the search index holds.
|
|
183
|
+
*
|
|
184
|
+
* The search had three separate bugs — a filter fighting a filter, results that
|
|
185
|
+
* were heading fragments, table cells run together without separators — and all
|
|
186
|
+
* three were invisible because nobody could see the sections the index is built
|
|
187
|
+
* from. This is that list, with the same query the client runs.
|
|
188
|
+
*/
|
|
189
|
+
export function renderSearch(term: string, sections: IndexedSection[]): string {
|
|
190
|
+
const form = `<form method="get">
|
|
191
|
+
<input type="search" name="q" value="${escape(term)}" placeholder="A word as a reader would type it" autofocus>
|
|
192
|
+
<button type="submit">Search</button>
|
|
193
|
+
</form>`;
|
|
194
|
+
|
|
195
|
+
const needle = term.trim().toLowerCase();
|
|
196
|
+
|
|
197
|
+
const matching = needle
|
|
198
|
+
? sections.filter(
|
|
199
|
+
(section) =>
|
|
200
|
+
section.title?.toLowerCase().includes(needle) ||
|
|
201
|
+
section.content?.toLowerCase().includes(needle)
|
|
202
|
+
)
|
|
203
|
+
: sections;
|
|
204
|
+
|
|
205
|
+
// The id is the longest string in the table and the least worth reading in
|
|
206
|
+
// full, so it is the one that gets clipped — otherwise it takes the width the
|
|
207
|
+
// indexed text needs, and the column that answers the question is a sliver.
|
|
208
|
+
const rows = matching.slice(0, 200).map(
|
|
209
|
+
(section) => `<tr>
|
|
210
|
+
<td>${code(section.collection)}</td>
|
|
211
|
+
<td><code class="trunc" title="${escape(section.id)}">${escape(section.id)}</code></td>
|
|
212
|
+
<td>${escape(section.title)}</td>
|
|
213
|
+
<td>${escape((section.titles ?? []).join(' › ')) || dim('—')}</td>
|
|
214
|
+
<td class="wide">${escape((section.content ?? '').slice(0, 180))}${(section.content ?? '').length > 180 ? dim(' …') : ''}</td>
|
|
215
|
+
</tr>`
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const summary = stats([
|
|
219
|
+
stat(sections.length, 'sections indexed'),
|
|
220
|
+
needle
|
|
221
|
+
? stat(matching.length, 'matching', matching.length ? '' : 'warn')
|
|
222
|
+
: undefined,
|
|
223
|
+
matching.length > 200 ? stat(200, 'shown', 'warn') : undefined
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
return `${form}${summary}<div class="note">One row per heading — which is what the dialog groups back into pages. The text column is exactly what FTS5 matches against.</div>${table(
|
|
227
|
+
['Collection', 'Id', 'Heading', 'Under', 'Indexed text'],
|
|
228
|
+
rows,
|
|
229
|
+
needle
|
|
230
|
+
? `Nothing in the index matches “${escape(term)}”. FTS5 matches terms and prefixes, so a typo returns nothing.`
|
|
231
|
+
: 'The index is empty.'
|
|
232
|
+
)}`;
|
|
233
|
+
}
|