@dogsbay/docs-layout 0.2.0-beta.4 → 0.2.0-beta.40
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/package.json +3 -3
- package/src/DocsLayout.astro +138 -19
- package/src/SearchDialog.astro +24 -4
- package/src/TaxonomyTerm.astro +15 -4
- package/src/version-redirect.ts +23 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/docs-layout",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.40",
|
|
4
4
|
"description": "Standard documentation layout components for Dogsbay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"./json-ld": "./src/json-ld.ts"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@dogsbay/ui": "0.2.0-beta.
|
|
33
|
-
"@dogsbay/primitives": "0.2.0-beta.
|
|
32
|
+
"@dogsbay/ui": "0.2.0-beta.40",
|
|
33
|
+
"@dogsbay/primitives": "0.2.0-beta.40"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"vitest": "^3.0.0"
|
package/src/DocsLayout.astro
CHANGED
|
@@ -112,7 +112,13 @@ interface Props {
|
|
|
112
112
|
siteDescription?: string;
|
|
113
113
|
/** Copyright text (HTML allowed) */
|
|
114
114
|
copyright?: string;
|
|
115
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Favicon path. No host-root default — that 404s on subpath
|
|
117
|
+
* deploys. The format-astro emitter passes a combined-prefix
|
|
118
|
+
* path (e.g. `/<repo>/favicon.ico`) computed from site.url +
|
|
119
|
+
* basePath. Pass an empty string or `false` to disable. When
|
|
120
|
+
* undefined, no `<link rel="icon">` is emitted.
|
|
121
|
+
*/
|
|
116
122
|
favicon?: string | false;
|
|
117
123
|
/** Per-page OG image URL. Overrides defaultOgImage. */
|
|
118
124
|
ogImage?: string;
|
|
@@ -127,16 +133,25 @@ interface Props {
|
|
|
127
133
|
/** Theme color hint for browsers (hex string) */
|
|
128
134
|
themeColor?: string;
|
|
129
135
|
/**
|
|
130
|
-
* Emit `<meta name="robots"
|
|
136
|
+
* Emit `noindex` in the `<meta name="robots">` directive when
|
|
131
137
|
* true. Tells external search engines (Google, Bing) to skip
|
|
132
|
-
* this page.
|
|
133
|
-
*
|
|
134
|
-
* page
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
+
* indexing this page. Independent of `nofollow` — common pattern
|
|
139
|
+
* for tag / index pages is noindex + follow (don't list this
|
|
140
|
+
* page in results, but do crawl through to the real content).
|
|
141
|
+
*
|
|
142
|
+
* Has NO effect on in-site Pagefind search — for that, use
|
|
143
|
+
* `excludeFromSearch`. The two are independent: a page can be
|
|
144
|
+
* excluded from external SEs but still appear in Pagefind (e.g.
|
|
145
|
+
* duplicate / old content readers might still want to find when
|
|
146
|
+
* they're already on the site), or vice versa.
|
|
138
147
|
*/
|
|
139
148
|
noindex?: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Emit `nofollow` in the `<meta name="robots">` directive when
|
|
151
|
+
* true. Tells crawlers not to follow outbound links from this
|
|
152
|
+
* page. Independent of `noindex`. Default false.
|
|
153
|
+
*/
|
|
154
|
+
nofollow?: boolean;
|
|
140
155
|
/**
|
|
141
156
|
* Exclude this page from in-site Pagefind search results.
|
|
142
157
|
*
|
|
@@ -256,6 +271,23 @@ interface Props {
|
|
|
256
271
|
* elements in `<body>`.
|
|
257
272
|
*/
|
|
258
273
|
category?: string[];
|
|
274
|
+
/**
|
|
275
|
+
* Custom-taxonomy values from `meta.taxonomies` — anything declared
|
|
276
|
+
* in `taxonomies:` config that isn't one of the five hardcoded
|
|
277
|
+
* built-ins (`tags`, `category`, `audience`, `type`, `status`).
|
|
278
|
+
*
|
|
279
|
+
* Each entry becomes one `<div data-pagefind-filter="<name>:<value>">`
|
|
280
|
+
* inside the indexed body, so the search dialog grows a checkbox
|
|
281
|
+
* group for every custom taxonomy automatically. No extra config
|
|
282
|
+
* needed beyond declaring the taxonomy in `dogsbay.config.yml` —
|
|
283
|
+
* the search dialog discovers facets at index time and renders
|
|
284
|
+
* whatever Pagefind reports.
|
|
285
|
+
*
|
|
286
|
+
* Display labels for the checkboxes flow through
|
|
287
|
+
* `taxonomyDisplay[<name>]` (same prefix/label config that drives
|
|
288
|
+
* chip rendering elsewhere). When unset, raw slugs are shown.
|
|
289
|
+
*/
|
|
290
|
+
taxonomies?: Record<string, string[]>;
|
|
259
291
|
/**
|
|
260
292
|
* Map of taxonomy name → index path for declared taxonomies.
|
|
261
293
|
* Used to wire links from built-in field badges (TypeBadge,
|
|
@@ -376,7 +408,7 @@ const {
|
|
|
376
408
|
editUrl,
|
|
377
409
|
lastUpdated,
|
|
378
410
|
copyright,
|
|
379
|
-
favicon
|
|
411
|
+
favicon,
|
|
380
412
|
ogImage,
|
|
381
413
|
defaultOgImage,
|
|
382
414
|
ogType = "article",
|
|
@@ -384,11 +416,18 @@ const {
|
|
|
384
416
|
twitterHandle,
|
|
385
417
|
themeColor,
|
|
386
418
|
noindex,
|
|
419
|
+
nofollow,
|
|
387
420
|
excludeFromSearch,
|
|
388
421
|
plausibleDomain,
|
|
389
422
|
plausibleScriptUrl,
|
|
390
423
|
hideSearch = false,
|
|
391
|
-
pagefindUrl
|
|
424
|
+
// No default for pagefindUrl — host-root absolute paths break on
|
|
425
|
+
// subpath-mounted deploys (GH Pages project pages, multi-mount
|
|
426
|
+
// Cloudflare). format-astro's emitter always passes the
|
|
427
|
+
// combined-prefix-aware URL; manual instantiation must too.
|
|
428
|
+
// Undefined here propagates to SearchDialog where the JS loader
|
|
429
|
+
// throws on first open instead of silently 404'ing the bundle.
|
|
430
|
+
pagefindUrl,
|
|
392
431
|
mdMirror = false,
|
|
393
432
|
tags,
|
|
394
433
|
tagsIndexPath = "/tags",
|
|
@@ -398,6 +437,7 @@ const {
|
|
|
398
437
|
pageType,
|
|
399
438
|
audience,
|
|
400
439
|
category,
|
|
440
|
+
taxonomies,
|
|
401
441
|
taxonomyIndexPaths,
|
|
402
442
|
taxonomyDisplay,
|
|
403
443
|
autoH1,
|
|
@@ -428,6 +468,16 @@ const showLlmActionsInline =
|
|
|
428
468
|
llmActionsEnabled
|
|
429
469
|
&& (llmActionsPlacement === "inline" || llmActionsPlacement === "both");
|
|
430
470
|
const llmFooterLink = !!llmActions && llmActions.footerLink !== false;
|
|
471
|
+
// Per-mount llms.txt URL — Dogsbay emits `<basePath>/llms.txt`
|
|
472
|
+
// (sitemap-index pattern), so the footer link must be basePath-
|
|
473
|
+
// prefixed too. Falls back to `/llms.txt` when basePath is empty
|
|
474
|
+
// or unset, matching the platform's host-root single-site case.
|
|
475
|
+
const llmsLinkHrefResolved = (() => {
|
|
476
|
+
const bp = (basePath ?? "").replace(/\/+$/, "");
|
|
477
|
+
if (!bp) return "/llms.txt";
|
|
478
|
+
const prefix = bp.startsWith("/") ? bp : `/${bp}`;
|
|
479
|
+
return `${prefix}/llms.txt`;
|
|
480
|
+
})();
|
|
431
481
|
|
|
432
482
|
// Compute href targets for the type / status badges. A field is
|
|
433
483
|
// linkable only when (a) the user declared a `taxonomies.<field>`
|
|
@@ -453,9 +503,25 @@ const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
|
|
|
453
503
|
const metaDescription = description ?? siteDescription;
|
|
454
504
|
const metaOgImage = ogImage ?? defaultOgImage;
|
|
455
505
|
const isAbsoluteSiteUrl = /^https?:\/\//.test(siteUrl);
|
|
506
|
+
// Compose canonical from ORIGIN + pathname. siteUrl may carry a
|
|
507
|
+
// path component (the urlBase that drives Astro's `base` — see
|
|
508
|
+
// plans/astro-base-from-site-url.md), and Astro.url.pathname
|
|
509
|
+
// already includes that prefix. Naively concatenating siteUrl +
|
|
510
|
+
// pathname double-counts the urlBase (e.g. .../repo/repo/page).
|
|
511
|
+
// Strip path off siteUrl by reparsing as a URL.
|
|
512
|
+
let canonicalOrigin: string | undefined;
|
|
513
|
+
if (isAbsoluteSiteUrl) {
|
|
514
|
+
try {
|
|
515
|
+
const u = new URL(siteUrl);
|
|
516
|
+
canonicalOrigin = `${u.protocol}//${u.host}`;
|
|
517
|
+
} catch {
|
|
518
|
+
// Malformed siteUrl — fall back to the original (no path) behavior.
|
|
519
|
+
canonicalOrigin = siteUrl.replace(/\/$/, "");
|
|
520
|
+
}
|
|
521
|
+
}
|
|
456
522
|
const computedCanonical = canonicalUrl
|
|
457
|
-
?? (
|
|
458
|
-
?
|
|
523
|
+
?? (canonicalOrigin
|
|
524
|
+
? canonicalOrigin + Astro.url.pathname
|
|
459
525
|
: undefined);
|
|
460
526
|
|
|
461
527
|
// Markdown mirror — append `.md` to the current path for the alternate link
|
|
@@ -510,11 +576,22 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
|
|
510
576
|
<title>{title} | {siteName}</title>
|
|
511
577
|
|
|
512
578
|
{metaDescription && <meta name="description" content={metaDescription} />}
|
|
513
|
-
{favicon
|
|
579
|
+
{favicon && <link rel="icon" href={favicon} />}
|
|
514
580
|
{themeColor && <meta name="theme-color" content={themeColor} />}
|
|
515
581
|
{/* External search engine directive — orthogonal to in-site
|
|
516
|
-
Pagefind exclusion.
|
|
517
|
-
|
|
582
|
+
Pagefind exclusion. `noindex` + `nofollow` are independent
|
|
583
|
+
bits per the meta-robots spec; emit only the directives that
|
|
584
|
+
are set. Combining them when both are set keeps the tag
|
|
585
|
+
compact (`<meta name="robots" content="noindex, nofollow">`)
|
|
586
|
+
instead of emitting two tags. */}
|
|
587
|
+
{(noindex || nofollow) && (
|
|
588
|
+
<meta
|
|
589
|
+
name="robots"
|
|
590
|
+
content={[noindex && "noindex", nofollow && "nofollow"]
|
|
591
|
+
.filter(Boolean)
|
|
592
|
+
.join(", ")}
|
|
593
|
+
/>
|
|
594
|
+
)}
|
|
518
595
|
|
|
519
596
|
{/* In-site Pagefind exclusion is wired via two coordinated
|
|
520
597
|
attributes on <body> and <main> below — see the prop docs
|
|
@@ -529,6 +606,12 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
|
|
529
606
|
|
|
530
607
|
{computedCanonical && <link rel="canonical" href={computedCanonical} />}
|
|
531
608
|
{mdMirrorHref && <link rel="alternate" type="text/markdown" href={mdMirrorHref} />}
|
|
609
|
+
{/* Programmatic llms.txt discovery — agents that follow head
|
|
610
|
+
link rels get the per-mount llms.txt without parsing HTML.
|
|
611
|
+
Mirrors the existing _headers Link rel="describedby" used by
|
|
612
|
+
Cloudflare Pages / Workers. */}
|
|
613
|
+
<link rel="alternate" type="text/plain" title="llms.txt" href={llmsLinkHrefResolved} />
|
|
614
|
+
<link rel="describedby" type="text/plain" href={llmsLinkHrefResolved} />
|
|
532
615
|
|
|
533
616
|
{/* Open Graph */}
|
|
534
617
|
<meta property="og:type" content={ogType} />
|
|
@@ -592,7 +675,7 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
|
|
592
675
|
<SidebarHeader>
|
|
593
676
|
<SidebarMenu>
|
|
594
677
|
<SidebarMenuItem>
|
|
595
|
-
<SidebarMenuButton size="lg" href={
|
|
678
|
+
<SidebarMenuButton size="lg" href={basePath || "/"} isActive={currentPath === basePath || currentPath === "/"}>
|
|
596
679
|
<div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
|
597
680
|
<Fragment set:html={siteIcon} />
|
|
598
681
|
</div>
|
|
@@ -709,9 +792,30 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
|
|
709
792
|
the actual page content. Filter elements live in here
|
|
710
793
|
now to stay inside that scope.
|
|
711
794
|
*/}
|
|
712
|
-
{
|
|
713
|
-
|
|
714
|
-
|
|
795
|
+
{/*
|
|
796
|
+
Slash-nested tags whose prefix is declared in
|
|
797
|
+
`taxonomies.tags.prefixes` emit as per-prefix filter
|
|
798
|
+
divs so each prefix becomes its own Pagefind facet
|
|
799
|
+
column (Difficulty, Topic, Persona, …) instead of
|
|
800
|
+
pooling under a single "Tag" column.
|
|
801
|
+
|
|
802
|
+
Plain tags and tags whose prefix isn't declared fall
|
|
803
|
+
back to the pooled `tag:` filter — backward-compatible
|
|
804
|
+
for sites that haven't declared prefixes.
|
|
805
|
+
|
|
806
|
+
See plans/per-prefix-search-facets.md.
|
|
807
|
+
*/}
|
|
808
|
+
{Array.isArray(tags) && tags.map((tag) => {
|
|
809
|
+
const slash = tag.indexOf("/");
|
|
810
|
+
if (slash > 0) {
|
|
811
|
+
const prefix = tag.slice(0, slash);
|
|
812
|
+
const leaf = tag.slice(slash + 1);
|
|
813
|
+
if (tagPrefixes && tagPrefixes[prefix]) {
|
|
814
|
+
return <div hidden data-pagefind-filter={`${prefix}:${leaf}`}></div>;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return <div hidden data-pagefind-filter={`tag:${tag}`}></div>;
|
|
818
|
+
})}
|
|
715
819
|
{Array.isArray(audience) && audience.map((value) => (
|
|
716
820
|
<div hidden data-pagefind-filter={`audience:${value}`}></div>
|
|
717
821
|
))}
|
|
@@ -720,6 +824,20 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
|
|
720
824
|
))}
|
|
721
825
|
{status && <div hidden data-pagefind-filter={`status:${status}`}></div>}
|
|
722
826
|
{pageType && <div hidden data-pagefind-filter={`type:${pageType}`}></div>}
|
|
827
|
+
{/*
|
|
828
|
+
Custom-taxonomy filters. Any taxonomy declared in
|
|
829
|
+
`dogsbay.config.yml` that isn't one of the five built-ins
|
|
830
|
+
flows through here, so `difficulty: intermediate` (etc.)
|
|
831
|
+
becomes a real Pagefind facet checkbox automatically.
|
|
832
|
+
See plans/beta-launch-followups.md for context.
|
|
833
|
+
*/}
|
|
834
|
+
{taxonomies && Object.entries(taxonomies).flatMap(([name, values]) =>
|
|
835
|
+
Array.isArray(values)
|
|
836
|
+
? values.map((value) => (
|
|
837
|
+
<div hidden data-pagefind-filter={`${name}:${value}`}></div>
|
|
838
|
+
))
|
|
839
|
+
: []
|
|
840
|
+
)}
|
|
723
841
|
|
|
724
842
|
<div class:list={["mx-auto", wideLayout ? "max-w-7xl" : "max-w-3xl"]}>
|
|
725
843
|
{/*
|
|
@@ -802,6 +920,7 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
|
|
802
920
|
next={next}
|
|
803
921
|
copyright={copyright}
|
|
804
922
|
llmsLink={llmFooterLink}
|
|
923
|
+
llmsLinkHref={llmsLinkHrefResolved}
|
|
805
924
|
/>
|
|
806
925
|
</div>
|
|
807
926
|
</main>
|
package/src/SearchDialog.astro
CHANGED
|
@@ -28,8 +28,13 @@ import type { TaxonomyDisplay } from "@dogsbay/types";
|
|
|
28
28
|
|
|
29
29
|
interface Props {
|
|
30
30
|
/**
|
|
31
|
-
* Path
|
|
32
|
-
*
|
|
31
|
+
* Path where Pagefind's index lives, e.g. `/pagefind/` or
|
|
32
|
+
* `/<repo>/pagefind/` for subpath-mounted deploys. NO DEFAULT —
|
|
33
|
+
* a host-root default would silently 404 on subpath deploys
|
|
34
|
+
* (GH Pages project pages, multi-mount Cloudflare). The
|
|
35
|
+
* format-astro emitter passes the combined-prefix-aware URL;
|
|
36
|
+
* manual instantiations must do the same. When undefined, the
|
|
37
|
+
* search dialog throws on first open with a clear console error.
|
|
33
38
|
*/
|
|
34
39
|
pagefindUrl?: string;
|
|
35
40
|
/** Placeholder text for the search input */
|
|
@@ -44,7 +49,7 @@ interface Props {
|
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
const {
|
|
47
|
-
pagefindUrl
|
|
52
|
+
pagefindUrl,
|
|
48
53
|
placeholder = "Search docs...",
|
|
49
54
|
taxonomyDisplay,
|
|
50
55
|
} = Astro.props;
|
|
@@ -218,7 +223,22 @@ const {
|
|
|
218
223
|
async function ensurePagefindLoaded() {
|
|
219
224
|
if (pagefind) return;
|
|
220
225
|
if (loadingPagefind) return loadingPagefind;
|
|
221
|
-
|
|
226
|
+
// pagefindUrl is required — a "/pagefind/" fallback would
|
|
227
|
+
// silently 404 on subpath-mounted deploys (GH Pages project
|
|
228
|
+
// pages, multi-mount Cloudflare). The emitter always passes
|
|
229
|
+
// a combined-prefix-aware value via data-pagefind-url. If it's
|
|
230
|
+
// missing the page wasn't built through format-astro and the
|
|
231
|
+
// caller forgot to pass it.
|
|
232
|
+
const dataUrl = dialog!.dataset.pagefindUrl;
|
|
233
|
+
if (!dataUrl) {
|
|
234
|
+
console.error(
|
|
235
|
+
"[dogsbay] SearchDialog: pagefindUrl prop missing. " +
|
|
236
|
+
"Pass the combined-prefix path (e.g. '/<base>/pagefind/') " +
|
|
237
|
+
"from your DocsLayout instantiation.",
|
|
238
|
+
);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const url = dataUrl + "pagefind.js";
|
|
222
242
|
loadingPagefind = (async () => {
|
|
223
243
|
try {
|
|
224
244
|
const mod = (await import(/* @vite-ignore */ url)) as PagefindModule;
|
package/src/TaxonomyTerm.astro
CHANGED
|
@@ -93,15 +93,22 @@ function segmentLabel(depth: number): string {
|
|
|
93
93
|
|
|
94
94
|
const titleSegments = term.fullPath.map((_, i) => segmentLabel(i));
|
|
95
95
|
|
|
96
|
-
// Breadcrumb segments —
|
|
96
|
+
// Breadcrumb segments — only link to prefixes that have their own
|
|
97
|
+
// term page. When a taxonomy is non-hierarchical, only full-path
|
|
98
|
+
// terms exist in `data.terms`; linking to `/tags/<prefix>/` for an
|
|
99
|
+
// intermediate segment would 404. Render those as plain text instead.
|
|
97
100
|
function breadcrumbs() {
|
|
98
|
-
const
|
|
101
|
+
const termKeys = new Set(data.terms.map((t) => t.fullPath.join("/")));
|
|
102
|
+
const out: { label: string; href?: string }[] = [
|
|
99
103
|
{ label: displayName, href: `${data.indexPath}/` },
|
|
100
104
|
];
|
|
101
105
|
for (let i = 0; i < term.fullPath.length - 1; i++) {
|
|
106
|
+
const prefix = term.fullPath.slice(0, i + 1).join("/");
|
|
102
107
|
out.push({
|
|
103
108
|
label: segmentLabel(i),
|
|
104
|
-
href:
|
|
109
|
+
href: termKeys.has(prefix)
|
|
110
|
+
? `${data.indexPath}/${prefix}/`
|
|
111
|
+
: undefined,
|
|
105
112
|
});
|
|
106
113
|
}
|
|
107
114
|
return out;
|
|
@@ -120,7 +127,11 @@ const crumbs = breadcrumbs();
|
|
|
120
127
|
<nav class="mb-3 text-sm text-muted-foreground" aria-label="Breadcrumbs">
|
|
121
128
|
{crumbs.map((c, i) => (
|
|
122
129
|
<span>
|
|
123
|
-
|
|
130
|
+
{c.href ? (
|
|
131
|
+
<a href={c.href} class="hover:text-foreground hover:underline">{c.label}</a>
|
|
132
|
+
) : (
|
|
133
|
+
<span>{c.label}</span>
|
|
134
|
+
)}
|
|
124
135
|
{i < crumbs.length - 1 && <span class="mx-1.5">/</span>}
|
|
125
136
|
</span>
|
|
126
137
|
))}
|
package/src/version-redirect.ts
CHANGED
|
@@ -35,6 +35,20 @@ export interface AxisRedirectConfig {
|
|
|
35
35
|
defaultLocale?: string;
|
|
36
36
|
/** Full set of declared locale ids. Empty/undefined → axis inactive. */
|
|
37
37
|
knownLocales?: string[];
|
|
38
|
+
/**
|
|
39
|
+
* First-segment names that aren't locale/version-axis-prefixable
|
|
40
|
+
* — e.g. taxonomy index paths like `tags`, `by-type`, `by-status`.
|
|
41
|
+
* Taxonomy routes emit a single global namespace shared across
|
|
42
|
+
* all locales / versions (one `/tags/` for the whole site, not
|
|
43
|
+
* one per locale), so the axis-redirect helper must skip them.
|
|
44
|
+
* Without this skip, chip hrefs to `/<basePath>/tags/...` would
|
|
45
|
+
* 302 to `/<basePath>/<defaultLocale>/tags/...` which 404s.
|
|
46
|
+
*
|
|
47
|
+
* Each entry is the first URL segment after basePath
|
|
48
|
+
* (no leading slash). Sourced from declared
|
|
49
|
+
* `taxonomies.<name>.indexPath` in `dogsbay.config.yml`.
|
|
50
|
+
*/
|
|
51
|
+
globalPrefixes?: string[];
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
/**
|
|
@@ -88,6 +102,15 @@ export function shouldRedirectToDefaultVersion(
|
|
|
88
102
|
// Skip Astro / Pagefind asset paths.
|
|
89
103
|
if (segments[0].startsWith("_") || segments[0] === "pagefind") return null;
|
|
90
104
|
|
|
105
|
+
// Skip global-namespace prefixes (taxonomy index paths and similar
|
|
106
|
+
// routes that don't live under per-locale / per-version trees).
|
|
107
|
+
// Without this, chip hrefs to `/docs/tags/concept/rag/` get
|
|
108
|
+
// redirected to `/docs/<defaultLocale>/tags/concept/rag/` which
|
|
109
|
+
// 404s — the taxonomy routes are emitted once at the unprefixed
|
|
110
|
+
// path. See plans/beta-launch-followups.md.
|
|
111
|
+
const globalPrefixes = config.globalPrefixes ?? [];
|
|
112
|
+
if (globalPrefixes.includes(segments[0])) return null;
|
|
113
|
+
|
|
91
114
|
// Greedy axis detection — locale outermost, version next.
|
|
92
115
|
const knownLocales = new Set(config.knownLocales ?? []);
|
|
93
116
|
const knownVersions = new Set(config.knownVersions ?? []);
|