@dogsbay/docs-layout 0.2.0-beta.2 → 0.2.0-beta.21

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/docs-layout",
3
- "version": "0.2.0-beta.2",
3
+ "version": "0.2.0-beta.21",
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.2",
33
- "@dogsbay/primitives": "0.2.0-beta.2"
32
+ "@dogsbay/ui": "0.2.0-beta.21",
33
+ "@dogsbay/primitives": "0.2.0-beta.21"
34
34
  },
35
35
  "devDependencies": {
36
36
  "vitest": "^3.0.0"
@@ -256,6 +256,23 @@ interface Props {
256
256
  * elements in `<body>`.
257
257
  */
258
258
  category?: string[];
259
+ /**
260
+ * Custom-taxonomy values from `meta.taxonomies` — anything declared
261
+ * in `taxonomies:` config that isn't one of the five hardcoded
262
+ * built-ins (`tags`, `category`, `audience`, `type`, `status`).
263
+ *
264
+ * Each entry becomes one `<div data-pagefind-filter="<name>:<value>">`
265
+ * inside the indexed body, so the search dialog grows a checkbox
266
+ * group for every custom taxonomy automatically. No extra config
267
+ * needed beyond declaring the taxonomy in `dogsbay.config.yml` —
268
+ * the search dialog discovers facets at index time and renders
269
+ * whatever Pagefind reports.
270
+ *
271
+ * Display labels for the checkboxes flow through
272
+ * `taxonomyDisplay[<name>]` (same prefix/label config that drives
273
+ * chip rendering elsewhere). When unset, raw slugs are shown.
274
+ */
275
+ taxonomies?: Record<string, string[]>;
259
276
  /**
260
277
  * Map of taxonomy name → index path for declared taxonomies.
261
278
  * Used to wire links from built-in field badges (TypeBadge,
@@ -398,6 +415,7 @@ const {
398
415
  pageType,
399
416
  audience,
400
417
  category,
418
+ taxonomies,
401
419
  taxonomyIndexPaths,
402
420
  taxonomyDisplay,
403
421
  autoH1,
@@ -428,6 +446,16 @@ const showLlmActionsInline =
428
446
  llmActionsEnabled
429
447
  && (llmActionsPlacement === "inline" || llmActionsPlacement === "both");
430
448
  const llmFooterLink = !!llmActions && llmActions.footerLink !== false;
449
+ // Per-mount llms.txt URL — Dogsbay emits `<basePath>/llms.txt`
450
+ // (sitemap-index pattern), so the footer link must be basePath-
451
+ // prefixed too. Falls back to `/llms.txt` when basePath is empty
452
+ // or unset, matching the platform's host-root single-site case.
453
+ const llmsLinkHrefResolved = (() => {
454
+ const bp = (basePath ?? "").replace(/\/+$/, "");
455
+ if (!bp) return "/llms.txt";
456
+ const prefix = bp.startsWith("/") ? bp : `/${bp}`;
457
+ return `${prefix}/llms.txt`;
458
+ })();
431
459
 
432
460
  // Compute href targets for the type / status badges. A field is
433
461
  // linkable only when (a) the user declared a `taxonomies.<field>`
@@ -453,9 +481,25 @@ const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
453
481
  const metaDescription = description ?? siteDescription;
454
482
  const metaOgImage = ogImage ?? defaultOgImage;
455
483
  const isAbsoluteSiteUrl = /^https?:\/\//.test(siteUrl);
484
+ // Compose canonical from ORIGIN + pathname. siteUrl may carry a
485
+ // path component (the urlBase that drives Astro's `base` — see
486
+ // plans/astro-base-from-site-url.md), and Astro.url.pathname
487
+ // already includes that prefix. Naively concatenating siteUrl +
488
+ // pathname double-counts the urlBase (e.g. .../repo/repo/page).
489
+ // Strip path off siteUrl by reparsing as a URL.
490
+ let canonicalOrigin: string | undefined;
491
+ if (isAbsoluteSiteUrl) {
492
+ try {
493
+ const u = new URL(siteUrl);
494
+ canonicalOrigin = `${u.protocol}//${u.host}`;
495
+ } catch {
496
+ // Malformed siteUrl — fall back to the original (no path) behavior.
497
+ canonicalOrigin = siteUrl.replace(/\/$/, "");
498
+ }
499
+ }
456
500
  const computedCanonical = canonicalUrl
457
- ?? (isAbsoluteSiteUrl
458
- ? siteUrl.replace(/\/$/, "") + Astro.url.pathname
501
+ ?? (canonicalOrigin
502
+ ? canonicalOrigin + Astro.url.pathname
459
503
  : undefined);
460
504
 
461
505
  // Markdown mirror — append `.md` to the current path for the alternate link
@@ -720,6 +764,20 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
720
764
  ))}
721
765
  {status && <div hidden data-pagefind-filter={`status:${status}`}></div>}
722
766
  {pageType && <div hidden data-pagefind-filter={`type:${pageType}`}></div>}
767
+ {/*
768
+ Custom-taxonomy filters. Any taxonomy declared in
769
+ `dogsbay.config.yml` that isn't one of the five built-ins
770
+ flows through here, so `difficulty: intermediate` (etc.)
771
+ becomes a real Pagefind facet checkbox automatically.
772
+ See plans/beta-launch-followups.md for context.
773
+ */}
774
+ {taxonomies && Object.entries(taxonomies).flatMap(([name, values]) =>
775
+ Array.isArray(values)
776
+ ? values.map((value) => (
777
+ <div hidden data-pagefind-filter={`${name}:${value}`}></div>
778
+ ))
779
+ : []
780
+ )}
723
781
 
724
782
  <div class:list={["mx-auto", wideLayout ? "max-w-7xl" : "max-w-3xl"]}>
725
783
  {/*
@@ -802,6 +860,7 @@ const siteIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
802
860
  next={next}
803
861
  copyright={copyright}
804
862
  llmsLink={llmFooterLink}
863
+ llmsLinkHref={llmsLinkHrefResolved}
805
864
  />
806
865
  </div>
807
866
  </main>
@@ -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 — clickable parent paths.
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 out: { label: string; href: string }[] = [
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: `${data.indexPath}/${term.fullPath.slice(0, i + 1).join("/")}/`,
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
- <a href={c.href} class="hover:text-foreground hover:underline">{c.label}</a>
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
  ))}
@@ -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 ?? []);