@kirchdev/duxt 0.3.4-dev-main.41-23ded41 → 0.3.5-dev-main.43-3310f4b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -9
- package/app/app.vue +17 -11
- package/app/assets/css/duxt.css +47 -6
- package/app/components/DuxtAnnouncements.vue +117 -0
- package/app/components/DuxtBrunoEntries.vue +62 -0
- package/app/components/DuxtCodeBlock.vue +18 -34
- package/app/components/DuxtCodeToolbar.vue +14 -0
- package/app/components/DuxtCopyButton.vue +33 -0
- package/app/components/DuxtCopyPage.vue +32 -30
- package/app/components/DuxtFooter.vue +39 -8
- package/app/components/DuxtHeader.vue +107 -48
- package/app/components/DuxtLandingProse.vue +1 -1
- package/app/components/DuxtLinkLabel.vue +36 -0
- package/app/components/DuxtLiveWindow.vue +8 -8
- package/app/components/DuxtLocale.vue +10 -1
- package/app/components/DuxtNavigation.vue +2 -2
- package/app/components/DuxtOpenApiClient.vue +285 -90
- package/app/components/DuxtOpenApiMedia.vue +30 -18
- package/app/components/DuxtOpenApiSchema.vue +10 -7
- package/app/components/DuxtPageFeedback.vue +7 -0
- package/app/components/DuxtPageInfo.vue +15 -12
- package/app/components/DuxtPageNav.vue +41 -27
- package/app/components/DuxtPreviewSkeleton.vue +4 -4
- package/app/components/DuxtProgress.vue +2 -14
- package/app/components/DuxtSearch.vue +230 -90
- package/app/components/DuxtSearchInput.vue +70 -0
- package/app/components/DuxtSections.vue +11 -1
- package/app/components/DuxtSegmented.vue +64 -0
- package/app/components/DuxtShortcuts.vue +16 -11
- package/app/components/DuxtShortcutsTrigger.vue +49 -0
- package/app/components/DuxtSkipLink.vue +1 -1
- package/app/components/DuxtToc.vue +16 -10
- package/app/components/DuxtTranslationBanner.vue +1 -1
- package/app/components/DuxtVersion.vue +53 -12
- package/app/components/DuxtVersionBanner.vue +5 -5
- package/app/components/content/AccordionItem.vue +2 -2
- package/app/components/content/BrunoOverview.vue +260 -0
- package/app/components/content/BrunoRequest.vue +141 -0
- package/app/components/content/BrunoRequests.vue +104 -0
- package/app/components/content/Callout.vue +5 -5
- package/app/components/content/ChangelogReleases.vue +161 -40
- package/app/components/content/CodeGroup.vue +41 -37
- package/app/components/content/Field.vue +1 -1
- package/app/components/content/FileTree.vue +1 -1
- package/app/components/content/OpenApiOperation.vue +4 -4
- package/app/components/content/OpenApiOperations.vue +1 -1
- package/app/components/content/OpenApiOverview.vue +1 -1
- package/app/components/content/PackageManagers.vue +42 -58
- package/app/components/content/PageCards.vue +7 -4
- package/app/components/content/Preview.vue +15 -39
- package/app/components/content/ProseImg.vue +596 -49
- package/app/components/content/Since.vue +1 -1
- package/app/components/content/Steps.vue +6 -4
- package/app/components/content/TabGroup.vue +13 -12
- package/app/components/ui/breadcrumb/BreadcrumbSeparator.vue +1 -1
- package/app/components/ui/command/CommandShortcut.vue +1 -1
- package/app/components/ui/dialog/DialogContent.vue +2 -2
- package/app/components/ui/dialog/DialogHeader.vue +1 -1
- package/app/components/ui/dialog/DialogScrollContent.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +2 -2
- package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +2 -2
- package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +1 -1
- package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +2 -2
- package/app/components/ui/navigation-menu/NavigationMenuContent.vue +1 -1
- package/app/components/ui/navigation-menu/NavigationMenuTrigger.vue +1 -1
- package/app/components/ui/navigation-menu/NavigationMenuViewport.vue +1 -1
- package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
- package/app/components/ui/select/SelectItem.vue +2 -2
- package/app/components/ui/sheet/SheetContent.vue +29 -3
- package/app/components/ui/sidebar/Sidebar.vue +28 -4
- package/app/components/ui/sidebar/SidebarGroupAction.vue +1 -1
- package/app/components/ui/sidebar/SidebarInset.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuAction.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuBadge.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuSub.vue +1 -1
- package/app/components/ui/sidebar/SidebarMenuSubButton.vue +1 -1
- package/app/components/ui/sidebar/SidebarProvider.vue +7 -16
- package/app/components/ui/sidebar/index.ts +10 -2
- package/app/components/ui/sidebar/utils.ts +0 -1
- package/app/components/ui/sonner/Sonner.vue +18 -0
- package/app/components/ui/tabs/TabsList.vue +13 -3
- package/app/components/ui/tabs/TabsTrigger.vue +24 -3
- package/app/components/ui/tooltip/TooltipContent.vue +2 -2
- package/app/composables/useActiveHeading.ts +3 -25
- package/app/composables/useDuxtAnalytics.ts +50 -0
- package/app/composables/useDuxtAnimatedHeight.ts +8 -38
- package/app/composables/useDuxtAnnouncements.ts +153 -0
- package/app/composables/useDuxtCopy.ts +51 -0
- package/app/composables/useDuxtDirection.ts +23 -0
- package/app/composables/useDuxtNavigation.ts +28 -8
- package/app/composables/useDuxtPath.ts +3 -5
- package/app/composables/useDuxtSearch.ts +87 -35
- package/app/composables/useDuxtSection.ts +15 -1
- package/app/composables/useDuxtSectionRow.ts +6 -5
- package/app/composables/useDuxtShortcuts.ts +319 -20
- package/app/composables/useDuxtVersion.ts +57 -6
- package/app/composables/useDuxtViewportMeasure.ts +37 -0
- package/app/composables/useFuzzySearch.ts +9 -4
- package/app/composables/useRecentPages.ts +14 -17
- package/app/error.vue +112 -74
- package/app/layouts/changelog.vue +9 -1
- package/app/layouts/default.vue +9 -0
- package/app/layouts/docs.vue +9 -1
- package/app/layouts/reference.vue +9 -1
- package/app/middleware/duxt-section-layout.global.ts +1 -3
- package/app/pages/[...slug].vue +146 -35
- package/app/pages/index.vue +15 -23
- package/app/types/duxt.d.ts +312 -4
- package/app/utils/analytics.ts +91 -0
- package/app/utils/announcements.ts +205 -0
- package/app/utils/changelog.ts +0 -28
- package/app/utils/code-highlight.ts +3 -1
- package/app/utils/contributor-avatar.ts +27 -0
- package/app/utils/direction.ts +43 -0
- package/app/utils/duxt-config.ts +43 -0
- package/app/utils/duxt-text.ts +10 -0
- package/app/utils/format-date.ts +28 -0
- package/app/utils/generated-sections.ts +68 -12
- package/app/utils/generated-toc.ts +14 -7
- package/app/utils/locale-path.ts +43 -0
- package/app/utils/navigation-tree.ts +51 -1
- package/app/utils/nearest-page.ts +8 -0
- package/app/utils/openapi.ts +16 -11
- package/app/utils/page-controls.ts +215 -0
- package/app/utils/pill.ts +61 -0
- package/app/utils/prose-image.ts +128 -0
- package/app/utils/search-context.ts +110 -0
- package/app/utils/search-display.ts +248 -0
- package/app/utils/section-scope.ts +65 -4
- package/app/utils/stored-json.ts +31 -0
- package/app/utils/version-choices.ts +6 -13
- package/app/utils/version-paths.ts +21 -0
- package/bin/duxt-cache-key.mjs +61 -0
- package/bin/duxt-og-cache.mjs +58 -0
- package/bin/{duxt-report.mjs → duxt.mjs} +13 -7
- package/bruno-model.ts +169 -0
- package/bruno-parse.ts +656 -0
- package/bruno-zip.ts +195 -0
- package/cli.ts +240 -0
- package/duxt-app-config.ts +18 -1
- package/frontmatter.ts +49 -0
- package/git-contributors.ts +322 -0
- package/i18n/locales/de/duxt/announcement.json +7 -0
- package/i18n/locales/de/duxt/bruno.json +25 -0
- package/i18n/locales/de/duxt/changelog.json +1 -1
- package/i18n/locales/de/duxt/code.json +2 -1
- package/i18n/locales/de/duxt/page.json +9 -0
- package/i18n/locales/de/duxt/shortcuts.json +1 -0
- package/i18n/locales/de/duxt/theme.json +2 -1
- package/i18n/locales/en/duxt/announcement.json +7 -0
- package/i18n/locales/en/duxt/bruno.json +25 -0
- package/i18n/locales/en/duxt/changelog.json +1 -1
- package/i18n/locales/en/duxt/code.json +2 -1
- package/i18n/locales/en/duxt/page.json +9 -0
- package/i18n/locales/en/duxt/shortcuts.json +1 -0
- package/i18n/locales/en/duxt/theme.json +2 -1
- package/i18n/locales/es/duxt/announcement.json +7 -0
- package/i18n/locales/es/duxt/bruno.json +25 -0
- package/i18n/locales/es/duxt/changelog.json +1 -1
- package/i18n/locales/es/duxt/code.json +2 -1
- package/i18n/locales/es/duxt/page.json +9 -0
- package/i18n/locales/es/duxt/shortcuts.json +1 -0
- package/i18n/locales/es/duxt/theme.json +2 -1
- package/i18n/locales/fr/duxt/announcement.json +7 -0
- package/i18n/locales/fr/duxt/bruno.json +25 -0
- package/i18n/locales/fr/duxt/changelog.json +1 -1
- package/i18n/locales/fr/duxt/code.json +2 -1
- package/i18n/locales/fr/duxt/page.json +9 -0
- package/i18n/locales/fr/duxt/shortcuts.json +1 -0
- package/i18n/locales/fr/duxt/theme.json +2 -1
- package/i18n/locales/pt/duxt/announcement.json +7 -0
- package/i18n/locales/pt/duxt/bruno.json +25 -0
- package/i18n/locales/pt/duxt/changelog.json +1 -1
- package/i18n/locales/pt/duxt/code.json +2 -1
- package/i18n/locales/pt/duxt/page.json +9 -0
- package/i18n/locales/pt/duxt/shortcuts.json +1 -0
- package/i18n/locales/pt/duxt/theme.json +2 -1
- package/modules/bruno.ts +118 -0
- package/modules/config.ts +26 -0
- package/modules/devtools.ts +2 -3
- package/modules/git-meta.ts +16 -47
- package/modules/search-records.ts +132 -0
- package/nuxt.config.ts +81 -7
- package/og-image-cache.ts +742 -0
- package/package.json +41 -9
- package/public/devtools/checks.html +2 -2
- package/public/devtools/config.html +59 -4
- package/public/devtools/pages.html +8 -1
- package/public/devtools/paths.html +1 -1
- package/public/devtools/sources.html +2 -2
- package/public/devtools/versions.html +3 -0
- package/report.ts +28 -19
- package/search-records.ts +380 -0
- package/section-input.ts +142 -0
- package/section-markdown.ts +67 -0
- package/section-reports.ts +10 -4
- package/sections-bruno.ts +571 -0
- package/sections-changelog.ts +76 -43
- package/sections-openapi.ts +12 -83
- package/sections-resolve.ts +174 -8
- package/sections.ts +33 -10
- package/server/devtools/shell.ts +1 -1
- package/server/mcp/tools/list-pages.ts +77 -36
- package/server/mcp/tools/list-versions.ts +84 -0
- package/server/mcp/tools/read-page.ts +80 -26
- package/server/mcp/tools/search-docs.ts +87 -53
- package/server/middleware/raw-markdown.ts +1 -1
- package/server/routes/llms-full.txt.get.ts +6 -8
- package/server/routes/llms.txt.get.ts +1 -1
- package/server/utils/duxt-server-text.ts +0 -13
- package/server/utils/llms-pages.ts +24 -9
- package/server/utils/mcp-docs.ts +577 -0
- package/sources-cache.ts +376 -0
- package/sources-git.ts +186 -31
- package/sources-resolve.ts +59 -1
- package/sources.ts +122 -13
- package/tfplugindocs.ts +158 -0
package/sources-resolve.ts
CHANGED
|
@@ -20,6 +20,16 @@ export interface DuxtSource {
|
|
|
20
20
|
* default URL prefix without declaring two page collections for it.
|
|
21
21
|
*/
|
|
22
22
|
content?: boolean;
|
|
23
|
+
/** Per-source conventions for draft pages and reusable Markdown blocks. */
|
|
24
|
+
exclude?: { drafts?: string; partials?: string };
|
|
25
|
+
/**
|
|
26
|
+
* The Markdown dialect this source was generated in.
|
|
27
|
+
*
|
|
28
|
+
* Dialects are layer-owned normalisers, not consumer-supplied transforms:
|
|
29
|
+
* one compact marker keeps an upstream repository readable without making
|
|
30
|
+
* its generator emit duxt-specific Markdown.
|
|
31
|
+
*/
|
|
32
|
+
flavor?: DuxtSourceFlavor;
|
|
23
33
|
/** `owner/name` or a full git URL. Omitted reads the local checkout. */
|
|
24
34
|
repo?: string;
|
|
25
35
|
/**
|
|
@@ -30,6 +40,15 @@ export interface DuxtSource {
|
|
|
30
40
|
* build with "Could not find refs/heads/…".
|
|
31
41
|
*/
|
|
32
42
|
refs?: DuxtRef[];
|
|
43
|
+
/**
|
|
44
|
+
* Discover release tags to publish as versions.
|
|
45
|
+
*
|
|
46
|
+
* Discovery is opt-in: a source still needs to say whether every release,
|
|
47
|
+
* one release per minor, or one per major belongs in its public URLs. Explicit
|
|
48
|
+
* `refs` stay available beside this object and override a discovered tag's
|
|
49
|
+
* label, lifecycle, default and locales.
|
|
50
|
+
*/
|
|
51
|
+
releases?: DuxtSourceReleases;
|
|
33
52
|
/**
|
|
34
53
|
* Languages this source is available in, beyond the one written in `path`.
|
|
35
54
|
*
|
|
@@ -64,6 +83,22 @@ export interface DuxtSource {
|
|
|
64
83
|
version?: string;
|
|
65
84
|
/** Shown in the version switcher and used in the URL; defaults to the ref. */
|
|
66
85
|
label?: string;
|
|
86
|
+
/**
|
|
87
|
+
* What to CALL this source where the site names it to a reader.
|
|
88
|
+
*
|
|
89
|
+
* Display only — it never reaches a collection name, a URL prefix, a version
|
|
90
|
+
* or the ranking, which is what separates it from `label` and `slug`. Those
|
|
91
|
+
* two are addresses that happen to be readable; this is a name that is
|
|
92
|
+
* nothing else, so it is free to be prose and free to be translated.
|
|
93
|
+
*
|
|
94
|
+
* The one thing no rule can derive. A segment is an abbreviation as often as
|
|
95
|
+
* it is a word — `tf`, `sdk`, `api` — and a search result labelled with one
|
|
96
|
+
* tells a reader which URL they are in rather than which project. Unset, the
|
|
97
|
+
* ladder in `app/utils/search-display.ts` falls back to the segment and then
|
|
98
|
+
* to the site's own name, so a site that sets nothing is still never shown
|
|
99
|
+
* the bare `/` this key was added for.
|
|
100
|
+
*/
|
|
101
|
+
name?: string | Record<string, string>;
|
|
67
102
|
/**
|
|
68
103
|
* Segment used in the URL for this source; defaults to the repository name.
|
|
69
104
|
*
|
|
@@ -119,6 +154,9 @@ export interface DuxtSource {
|
|
|
119
154
|
generated?: DuxtGeneratedSection[];
|
|
120
155
|
}
|
|
121
156
|
|
|
157
|
+
/** Source dialects the layer understands. */
|
|
158
|
+
export type DuxtSourceFlavor = 'tfplugindocs';
|
|
159
|
+
|
|
122
160
|
/**
|
|
123
161
|
* Where a version sits in its life.
|
|
124
162
|
*
|
|
@@ -156,6 +194,14 @@ export type DuxtRef =
|
|
|
156
194
|
| ({ branch: string } & DuxtRefOptions)
|
|
157
195
|
| ({ tag: string } & DuxtRefOptions);
|
|
158
196
|
|
|
197
|
+
/** The release lines a source publishes when it discovers Git tags. */
|
|
198
|
+
export interface DuxtSourceReleases {
|
|
199
|
+
/** Every release, or the newest release in each minor or major line. */
|
|
200
|
+
select: 'all' | 'minor' | 'major';
|
|
201
|
+
/** Include SemVer pre-releases; stable releases are the default. */
|
|
202
|
+
prereleases?: boolean;
|
|
203
|
+
}
|
|
204
|
+
|
|
159
205
|
interface DuxtRefOptions {
|
|
160
206
|
/** Shown in the switcher and used in the URL; defaults to the ref name. */
|
|
161
207
|
label?: string;
|
|
@@ -269,6 +315,14 @@ export interface DuxtResolvedSource {
|
|
|
269
315
|
repo?: string;
|
|
270
316
|
/** Version label, when the list has more than one version. */
|
|
271
317
|
version?: string;
|
|
318
|
+
/**
|
|
319
|
+
* The source's display name, as the consumer wrote it — see `DuxtSource`.
|
|
320
|
+
*
|
|
321
|
+
* Carried through UNRESOLVED, because a locale record cannot be collapsed at
|
|
322
|
+
* build time: the manifest is one object for every language the site serves,
|
|
323
|
+
* and `useDuxtConfig` resolves it per request like every other text field.
|
|
324
|
+
*/
|
|
325
|
+
name?: string | Record<string, string>;
|
|
272
326
|
/** True for the version served without a prefix. */
|
|
273
327
|
isDefault: boolean;
|
|
274
328
|
/**
|
|
@@ -298,6 +352,8 @@ export interface DuxtResolvedSource {
|
|
|
298
352
|
status: DuxtSourceStatus;
|
|
299
353
|
/** Whether the build may read this source's git history. */
|
|
300
354
|
history: boolean;
|
|
355
|
+
/** The layer-owned Markdown dialect this collection is normalised from. */
|
|
356
|
+
flavor?: DuxtSourceFlavor;
|
|
301
357
|
/**
|
|
302
358
|
* Present when this collection is a GENERATED SECTION rather than a docs
|
|
303
359
|
* tree — see `resolveGeneratedSections`.
|
|
@@ -646,6 +702,7 @@ export function resolveSources(
|
|
|
646
702
|
prefix,
|
|
647
703
|
repo: segmented(source) ? repoSlug(source) : undefined,
|
|
648
704
|
version,
|
|
705
|
+
name: source.name,
|
|
649
706
|
isDefault,
|
|
650
707
|
repository: entry.repo ?? source.origin?.repo,
|
|
651
708
|
repositoryUrl: entry.repo
|
|
@@ -673,7 +730,8 @@ export function resolveSources(
|
|
|
673
730
|
'current',
|
|
674
731
|
// A local source is a full checkout already; a remote one has to be
|
|
675
732
|
// unshallowed, which is why it has to be asked for.
|
|
676
|
-
history: entry.repo ? (source.history ?? false) : true
|
|
733
|
+
history: entry.repo ? (source.history ?? false) : true,
|
|
734
|
+
flavor: source.flavor
|
|
677
735
|
});
|
|
678
736
|
}
|
|
679
737
|
|
package/sources.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { repositoryRoot };
|
|
|
17
17
|
export type {
|
|
18
18
|
DuxtResolvedSource,
|
|
19
19
|
DuxtSource,
|
|
20
|
+
DuxtSourceFlavor,
|
|
20
21
|
DuxtSourcesOptions
|
|
21
22
|
} from './sources-resolve';
|
|
22
23
|
export { duxtSourceManifest } from './sources-resolve';
|
|
@@ -35,12 +36,81 @@ export { PARTIALS_COLLECTION, partialsCollection };
|
|
|
35
36
|
export const pageSchema = z.object({
|
|
36
37
|
/** Shown beside the entry in the sidebar, the section row and page cards. */
|
|
37
38
|
icon: z.string().optional(),
|
|
38
|
-
/**
|
|
39
|
+
/** Preserved long enough for the tfplugindocs dialect to normalise it. */
|
|
40
|
+
page_title: z.string().optional(),
|
|
41
|
+
/** A tfplugindocs navigation group within its directory category. */
|
|
42
|
+
subcategory: z.string().optional(),
|
|
43
|
+
/** Set by a source dialect; useful to navigation consumers, not authors. */
|
|
44
|
+
category: z.string().optional(),
|
|
45
|
+
/** `landing` renders the page without the docs shell. */
|
|
39
46
|
layout: z.string().optional(),
|
|
40
47
|
/** The latest release of a generated changelog overview. */
|
|
41
48
|
release: z.string().optional(),
|
|
42
|
-
|
|
43
|
-
navigation
|
|
49
|
+
//
|
|
50
|
+
// `navigation` is NOT declared here, deliberately. Content's own page schema
|
|
51
|
+
// already carries it — `boolean | { title, description, icon }`, defaulting
|
|
52
|
+
// to `true` — and a field declared here REPLACES that declaration rather than
|
|
53
|
+
// adding to it. Redeclaring it as a plain boolean cost two things at once: it
|
|
54
|
+
// dropped the object form, and it turned a TEXT column holding `'true'` into
|
|
55
|
+
// a BOOLEAN one holding NULL, which Content's own navigation query then
|
|
56
|
+
// compares against the string `'false'` and can never match. The page control
|
|
57
|
+
// that hides a page from the sidebar belongs to Content; the layer reads it
|
|
58
|
+
// and does not restate it.
|
|
59
|
+
//
|
|
60
|
+
// THE PAGE CONTROLS — the parts of the docs shell a page gets to refuse.
|
|
61
|
+
// What each one means, and how they compose, is `app/utils/page-controls.ts`;
|
|
62
|
+
// they are declared here because the schema is the gate, and an undeclared
|
|
63
|
+
// `toc: false` is dropped before the page component could ever read it. Flat
|
|
64
|
+
// fields rather than one nested object, which was decided: it is what an
|
|
65
|
+
// author writes in every comparable generator.
|
|
66
|
+
//
|
|
67
|
+
// EVERY BOOLEAN CONTROL CARRIES ITS DEFAULT, and that is not decoration.
|
|
68
|
+
// `z.boolean().optional()` generates a `BOOLEAN` column with no default, so a
|
|
69
|
+
// page that says nothing stores NULL — and Content casts a boolean column
|
|
70
|
+
// with `Boolean(value)` on the way back out, on every adapter. `Boolean(null)`
|
|
71
|
+
// is `false`, so every page read as though it had refused every control, and
|
|
72
|
+
// search returned nothing at all on every duxt site (#88). A default makes the
|
|
73
|
+
// column non-null, which is the only place the distinction survives: once the
|
|
74
|
+
// cast has run, "said nothing" and "said false" are the same value, and no
|
|
75
|
+
// predicate downstream can tell them apart. `tests/page-schema-database.test.ts`
|
|
76
|
+
// holds this by reading a page back out of a real database.
|
|
77
|
+
//
|
|
78
|
+
/**
|
|
79
|
+
* `false` drops the contents column; `{ maxDepth }` sets how deep it goes.
|
|
80
|
+
*
|
|
81
|
+
* `@nuxtjs/mdc` reads this same key out of the frontmatter and skips building
|
|
82
|
+
* `body.toc` at all on `false` — the same answer from the other end. The
|
|
83
|
+
* object form is ours alone: MDC's own `depth` is a count from `h2`, and
|
|
84
|
+
* `maxDepth` is the heading level an author actually counts.
|
|
85
|
+
*
|
|
86
|
+
* The one control with no default, because it needs none: a union is a JSON
|
|
87
|
+
* column, and Content leaves a JSON NULL alone rather than casting it — so an
|
|
88
|
+
* unanswered `toc` arrives as `null` and the site's own setting still wins.
|
|
89
|
+
*/
|
|
90
|
+
toc: z
|
|
91
|
+
.union([z.boolean(), z.object({ maxDepth: z.number().optional() })])
|
|
92
|
+
.optional(),
|
|
93
|
+
/** false hides the trail above the title, whatever `duxt.breadcrumb` says. */
|
|
94
|
+
breadcrumb: z.boolean().default(true),
|
|
95
|
+
/** false hides the previous/next pair under the article. */
|
|
96
|
+
prevNext: z.boolean().default(true),
|
|
97
|
+
/** false hides the "was this helpful?" row. */
|
|
98
|
+
feedback: z.boolean().default(true),
|
|
99
|
+
/** false hides the edit link, the last-updated line and the contributors. */
|
|
100
|
+
pageInfo: z.boolean().default(true),
|
|
101
|
+
/** false hides the copy-page and hand-to-a-model control. */
|
|
102
|
+
copyPage: z.boolean().default(true),
|
|
103
|
+
/** true removes the reading-width cap, and nothing else. */
|
|
104
|
+
fullWidth: z.boolean().default(false),
|
|
105
|
+
/**
|
|
106
|
+
* false removes the page from every duxt-owned discovery surface — the client
|
|
107
|
+
* search, its fuzzy fallback, MCP `search_docs` and the two llms indexes.
|
|
108
|
+
*
|
|
109
|
+
* It changes no URL. The page is still served, still canonical, still in the
|
|
110
|
+
* sitemap, still `noindex`-free, and `read_page` still answers for it: this
|
|
111
|
+
* makes a page un-findable, not unpublished.
|
|
112
|
+
*/
|
|
113
|
+
search: z.boolean().default(true),
|
|
44
114
|
/**
|
|
45
115
|
* URLs this page used to be served at. The layer turns them into redirects,
|
|
46
116
|
* because it is the only thing that knows which prefixes exist — the
|
|
@@ -123,13 +193,46 @@ const DRAFTS = '**/*.draft.md';
|
|
|
123
193
|
*
|
|
124
194
|
* Excluded from the page collections themselves, or every partial would also
|
|
125
195
|
* be a page — in the sidebar, in the search, in llms.txt.
|
|
196
|
+
*
|
|
197
|
+
* This is the EXCLUSION glob: the whole subtree, every file in it, whatever
|
|
198
|
+
* the extension. What the partials collection reads is derived from it by
|
|
199
|
+
* `partialsInclude` — the two are not the same glob.
|
|
126
200
|
*/
|
|
127
201
|
const PARTIALS = '**/_partials/**';
|
|
128
202
|
|
|
203
|
+
/**
|
|
204
|
+
* The Markdown inside a partials subtree.
|
|
205
|
+
*
|
|
206
|
+
* One configured value answers two questions, and they do not take the same
|
|
207
|
+
* glob. A page collection has to lose the whole subtree — a screenshot beside
|
|
208
|
+
* a partial must not be read as a page either. The partials collection is
|
|
209
|
+
* itself `type: 'page'`, so it can only carry Markdown: handed the exclusion
|
|
210
|
+
* glob it swallows that same screenshot, and Content parses a PNG as a
|
|
211
|
+
* document. Conflating the two is the regression the `*.md` restriction has
|
|
212
|
+
* already been written once to prevent.
|
|
213
|
+
*
|
|
214
|
+
* Nothing is narrowed: every Markdown file the subtree holds, at whatever
|
|
215
|
+
* depth the consumer's own glob reaches, still lands in the collection. A glob
|
|
216
|
+
* that already names files (`**\/_partials/**\/*.mdc`) is left alone — it has
|
|
217
|
+
* made the restriction itself.
|
|
218
|
+
*/
|
|
219
|
+
function partialsInclude(partials: string) {
|
|
220
|
+
const last = partials.slice(partials.lastIndexOf('/') + 1);
|
|
221
|
+
if (last.includes('.')) return partials;
|
|
222
|
+
if (partials.endsWith('**')) return `${partials}/*.md`;
|
|
223
|
+
if (partials.endsWith('*')) return `${partials}.md`;
|
|
224
|
+
return `${partials}/**/*.md`;
|
|
225
|
+
}
|
|
226
|
+
|
|
129
227
|
/** The dev server shows drafts; a build does not. */
|
|
130
228
|
const includeDrafts = () => process.env.NODE_ENV !== 'production';
|
|
131
229
|
|
|
132
|
-
const excluded = () =>
|
|
230
|
+
const excluded = (source: DuxtSource) => {
|
|
231
|
+
const partials = source.exclude?.partials ?? PARTIALS;
|
|
232
|
+
const drafts = source.exclude?.drafts ?? DRAFTS;
|
|
233
|
+
|
|
234
|
+
return includeDrafts() ? [partials] : [partials, drafts];
|
|
235
|
+
};
|
|
133
236
|
|
|
134
237
|
/**
|
|
135
238
|
* Turn a compact source list into Content collections.
|
|
@@ -188,7 +291,7 @@ export function duxtSources(
|
|
|
188
291
|
source: effective.repo
|
|
189
292
|
? {
|
|
190
293
|
exclude: [
|
|
191
|
-
...excluded(),
|
|
294
|
+
...excluded(source),
|
|
192
295
|
...nested.map((glob) => `${effective.path}/${glob}`)
|
|
193
296
|
],
|
|
194
297
|
// A tag lives outside refs/heads, so it has to be passed as a tag —
|
|
@@ -202,7 +305,7 @@ export function duxtSources(
|
|
|
202
305
|
prefix: entry.prefix
|
|
203
306
|
}
|
|
204
307
|
: {
|
|
205
|
-
exclude: [...excluded(), ...nested],
|
|
308
|
+
exclude: [...excluded(source), ...nested],
|
|
206
309
|
include: '**/*.md',
|
|
207
310
|
cwd: join(repositoryRoot(), effective.path),
|
|
208
311
|
prefix: entry.prefix
|
|
@@ -223,10 +326,13 @@ export function duxtSources(
|
|
|
223
326
|
function partialFolders(
|
|
224
327
|
resolved: DuxtResolvedSource[],
|
|
225
328
|
expanded: ReturnType<typeof expandSources>
|
|
226
|
-
): Map<
|
|
329
|
+
): Map<
|
|
330
|
+
string | undefined,
|
|
331
|
+
{ repo?: string; path: string; partials: string }[]
|
|
332
|
+
> {
|
|
227
333
|
const byLocale = new Map<
|
|
228
334
|
string | undefined,
|
|
229
|
-
{ repo?: string; path: string }[]
|
|
335
|
+
{ repo?: string; path: string; partials: string }[]
|
|
230
336
|
>();
|
|
231
337
|
const seen = new Set<string>();
|
|
232
338
|
|
|
@@ -238,12 +344,13 @@ function partialFolders(
|
|
|
238
344
|
// One entry per REPOSITORY AND FOLDER, not per version: a partial is a
|
|
239
345
|
// block of prose, and reading three versions of it into one collection
|
|
240
346
|
// would give three blocks under one name.
|
|
241
|
-
const
|
|
347
|
+
const partials = source.exclude?.partials ?? PARTIALS;
|
|
348
|
+
const claim = `${key ?? ''}|${effective.repo ?? ''}:${effective.path}:${partials}`;
|
|
242
349
|
if (seen.has(claim)) return;
|
|
243
350
|
seen.add(claim);
|
|
244
351
|
|
|
245
352
|
const list = byLocale.get(key) ?? [];
|
|
246
|
-
list.push({ repo: effective.repo, path: effective.path });
|
|
353
|
+
list.push({ repo: effective.repo, path: effective.path, partials });
|
|
247
354
|
byLocale.set(key, list);
|
|
248
355
|
});
|
|
249
356
|
|
|
@@ -264,15 +371,17 @@ function partialFolders(
|
|
|
264
371
|
* Two sources defining the same name is a collision the build reports rather
|
|
265
372
|
* than resolves; see `modules/validate.ts`.
|
|
266
373
|
*/
|
|
267
|
-
function definePartials(
|
|
374
|
+
function definePartials(
|
|
375
|
+
folders: { repo?: string; path: string; partials: string }[]
|
|
376
|
+
) {
|
|
268
377
|
const entries = folders.map((folder) =>
|
|
269
378
|
folder.repo
|
|
270
379
|
? {
|
|
271
380
|
repository: repoUrl(folder.repo),
|
|
272
|
-
include: `${folder.path}
|
|
381
|
+
include: `${folder.path}/${partialsInclude(folder.partials)}`
|
|
273
382
|
}
|
|
274
383
|
: {
|
|
275
|
-
include:
|
|
384
|
+
include: partialsInclude(folder.partials),
|
|
276
385
|
cwd: join(repositoryRoot(), folder.path)
|
|
277
386
|
}
|
|
278
387
|
) as NonNullable<Parameters<typeof defineCollection>[0]['source']>[];
|
package/tfplugindocs.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { ContentNavigationItem } from '@nuxt/content';
|
|
2
|
+
|
|
3
|
+
/** The order Terraform Registry gives the generated provider categories. */
|
|
4
|
+
const CATEGORY_ORDER = [
|
|
5
|
+
'index',
|
|
6
|
+
'guides',
|
|
7
|
+
'resources',
|
|
8
|
+
'data-sources',
|
|
9
|
+
'ephemeral-resources',
|
|
10
|
+
'list-resources',
|
|
11
|
+
'actions',
|
|
12
|
+
'state-stores',
|
|
13
|
+
'functions'
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
type TfplugindocsPage = Record<string, unknown>;
|
|
17
|
+
type NavigationItem = ContentNavigationItem & { subcategory?: string };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Bring tfplugindocs' Registry-oriented metadata into the ordinary page shape.
|
|
21
|
+
*
|
|
22
|
+
* The files remain ordinary Markdown pages: this only replaces the title a
|
|
23
|
+
* sidebar reads and carries the generator's directory category forward. The
|
|
24
|
+
* body is deliberately untouched, so a provider still renders on the Registry.
|
|
25
|
+
*/
|
|
26
|
+
export function normaliseTfplugindocsPage(
|
|
27
|
+
page: TfplugindocsPage,
|
|
28
|
+
file: string
|
|
29
|
+
) {
|
|
30
|
+
const category = categoryForFile(file);
|
|
31
|
+
const title =
|
|
32
|
+
headingTitle(page.title) ?? pageTitle(page.page_title) ?? fileName(file);
|
|
33
|
+
|
|
34
|
+
page.title = title;
|
|
35
|
+
page.category = category;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function headingTitle(value: unknown): string | undefined {
|
|
39
|
+
if (typeof value !== 'string' || !value.trim()) return undefined;
|
|
40
|
+
|
|
41
|
+
return value.replace(/\s+\([^)]*\)\s*$/, '').trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pageTitle(value: unknown): string | undefined {
|
|
45
|
+
if (typeof value !== 'string' || !value.trim()) return undefined;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
value
|
|
49
|
+
// tfplugindocs writes `<name> <kind> - <provider>`; providers may call
|
|
50
|
+
// themselves `linear` as well as `terraform-provider-linear`.
|
|
51
|
+
.replace(/\s+-\s+[^\s]+\s*$/, '')
|
|
52
|
+
.replace(
|
|
53
|
+
/\s+(?:resource|data source|ephemeral resource|list resource|action|state store|function|provider)\s*$/i,
|
|
54
|
+
''
|
|
55
|
+
)
|
|
56
|
+
.trim()
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function fileName(file: string) {
|
|
61
|
+
return (
|
|
62
|
+
file
|
|
63
|
+
.split('/')
|
|
64
|
+
.at(-1)
|
|
65
|
+
?.replace(/\.[^.]+$/, '') || 'index'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function categoryForFile(file: string) {
|
|
70
|
+
const parts = file.split('/').filter(Boolean);
|
|
71
|
+
const name = fileName(file);
|
|
72
|
+
return name === 'index' ? 'index' : (parts.at(-2) ?? 'index');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Shape a tfplugindocs collection's ordinary Content tree for provider docs.
|
|
77
|
+
*
|
|
78
|
+
* Content already makes the directory categories. Only their Registry order
|
|
79
|
+
* and tfplugindocs' frontmatter-only `subcategory` need layer work. Groups
|
|
80
|
+
* receive synthetic paths solely as stable Vue keys; they never become routes.
|
|
81
|
+
*/
|
|
82
|
+
export function tfplugindocsNavigation(
|
|
83
|
+
tree: ContentNavigationItem[],
|
|
84
|
+
prefix: string
|
|
85
|
+
): ContentNavigationItem[] {
|
|
86
|
+
return orderCategories(tree, prefix).map(groupSubcategories);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function orderCategories(
|
|
90
|
+
items: ContentNavigationItem[],
|
|
91
|
+
prefix: string
|
|
92
|
+
): ContentNavigationItem[] {
|
|
93
|
+
return [...items]
|
|
94
|
+
.sort((left, right) => {
|
|
95
|
+
const difference =
|
|
96
|
+
categoryRank(left.path, prefix) - categoryRank(right.path, prefix);
|
|
97
|
+
return difference || left.title.localeCompare(right.title);
|
|
98
|
+
})
|
|
99
|
+
.map((item) => ({
|
|
100
|
+
...item,
|
|
101
|
+
...(item.children?.length
|
|
102
|
+
? { children: orderCategories(item.children, prefix) }
|
|
103
|
+
: {})
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function categoryRank(path: string | undefined, prefix: string) {
|
|
108
|
+
if (path === prefix) return 0;
|
|
109
|
+
|
|
110
|
+
const category = path?.slice(prefix.length).split('/').filter(Boolean)[0];
|
|
111
|
+
const rank = CATEGORY_ORDER.indexOf(
|
|
112
|
+
category as (typeof CATEGORY_ORDER)[number]
|
|
113
|
+
);
|
|
114
|
+
return rank === -1 ? CATEGORY_ORDER.length + 1 : rank + 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function groupSubcategories(
|
|
118
|
+
item: ContentNavigationItem
|
|
119
|
+
): ContentNavigationItem {
|
|
120
|
+
const children = item.children?.map(groupSubcategories);
|
|
121
|
+
if (!children?.length) return item;
|
|
122
|
+
|
|
123
|
+
const pages = children as NavigationItem[];
|
|
124
|
+
const grouped = new Map<string, NavigationItem[]>();
|
|
125
|
+
const direct: NavigationItem[] = [];
|
|
126
|
+
|
|
127
|
+
for (const child of pages) {
|
|
128
|
+
if (!child.subcategory) {
|
|
129
|
+
direct.push(child);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const group = grouped.get(child.subcategory) ?? [];
|
|
134
|
+
group.push(child);
|
|
135
|
+
grouped.set(child.subcategory, group);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!grouped.size) return { ...item, children };
|
|
139
|
+
|
|
140
|
+
const groups = [...grouped.entries()]
|
|
141
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
142
|
+
.map(([title, members], index) => ({
|
|
143
|
+
title,
|
|
144
|
+
path: `${item.path ?? ''}/__duxt-subcategory-${index}`,
|
|
145
|
+
page: false as const,
|
|
146
|
+
children: [...members].sort((left, right) =>
|
|
147
|
+
left.title.localeCompare(right.title)
|
|
148
|
+
)
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
...item,
|
|
153
|
+
children: [
|
|
154
|
+
...direct.sort((left, right) => left.title.localeCompare(right.title)),
|
|
155
|
+
...groups
|
|
156
|
+
]
|
|
157
|
+
};
|
|
158
|
+
}
|