@dogsbay/format-astro 0.2.0-beta.147 → 0.2.0-beta.149

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/dist/project.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ExportPage, NavItem } from "@dogsbay/types";
2
- import { type ProductModel } from "@dogsbay/site-files";
2
+ import { type VersionMenu, type ProductModel } from "@dogsbay/site-files";
3
3
  import { type BlogEmitConfig } from "./blog.js";
4
4
  import { type RedirectsConfig } from "./redirects.js";
5
5
  /**
@@ -189,8 +189,21 @@ export interface AstroProjectOptions {
189
189
  canonical?: "self";
190
190
  noindex?: boolean;
191
191
  sitemap?: boolean;
192
+ badge?: boolean;
192
193
  path?: "";
193
194
  }>;
195
+ /**
196
+ * Units whose home has no page, forwarded to their first page (site-files `homeForwards`):
197
+ * `[from, to]` site paths without the base path, written as redirects.
198
+ */
199
+ homeForwards?: Array<[string, string]>;
200
+ /** Leave out a redirect the build cannot honour, with a warning, instead of failing. */
201
+ allowBrokenRedirects?: boolean;
202
+ /**
203
+ * The version menu's links and limit (`content.versionMenu`, @dogsbay/site-files
204
+ * version-menu.ts). With a `products:` block each product's comes from the model.
205
+ */
206
+ versionMenu?: VersionMenu;
194
207
  /**
195
208
  * Which version is the default. Marked in switcherMap.json so
196
209
  * the switcher can highlight it, and drives the default-version
@@ -603,6 +616,8 @@ export interface SwitcherMap {
603
616
  default?: boolean;
604
617
  /** Served at the site root (`path: ""`): its pages have no version segment. */
605
618
  root?: boolean;
619
+ /** "Version: <label>" above each page's title. */
620
+ badge?: boolean;
606
621
  }>;
607
622
  /**
608
623
  * Declared locales in declaration order. Same role as
@@ -632,6 +647,30 @@ export interface SwitcherMap {
632
647
  eol?: boolean;
633
648
  prerelease?: boolean;
634
649
  default?: boolean;
650
+ badge?: boolean;
651
+ home: string;
652
+ }>>;
653
+ /**
654
+ * The version menu's links (hrefs as served, base path included) and limit,
655
+ * keyed by product name, `_` for the site-wide versions and the unnamed
656
+ * source. Left out when no menu has any.
657
+ */
658
+ versionMenu?: Record<string, {
659
+ links: Array<{
660
+ label: string;
661
+ href: string;
662
+ }>;
663
+ limit?: number;
664
+ }>;
665
+ /**
666
+ * With a `products:` block and two or more products: the product menu per
667
+ * locale (`<locale|_>`), in the block's order, each entry going to the
668
+ * product's home (its default version, in the locale when it has one) —
669
+ * site-files `productSwitcher`, as dogsbay-ssg's product menu.
670
+ */
671
+ products?: Record<string, Array<{
672
+ name: string;
673
+ label: string;
635
674
  home: string;
636
675
  }>>;
637
676
  }
package/dist/project.js CHANGED
@@ -30,7 +30,7 @@ import { buildSitemapFiles, buildSitemapIndex } from "./sitemap.js";
30
30
  import { canonicalPlan } from "./canonical.js";
31
31
  import { headMetadata } from "./head-metadata.js";
32
32
  import { buildHeadersFile, buildRobotsTxt as buildRobotsTxtFile } from "@dogsbay/site-files";
33
- import { mergeHeaders, mergeRedirects, productRootRedirects, siteFiles, siteRootTarget, switcher as productSwitcherRows, toPagesScript, toScript, unitKeyString, unitManifest, unitOf, } from "@dogsbay/site-files";
33
+ import { mergeHeaders, mergeRedirects, productRootRedirects, siteFiles, siteRootTarget, switcher as productSwitcherRows, productSwitcher, isVersionMenuHref, versionMenuHref, toPagesScript, toScript, unitKeyString, unitManifest, unitOf, } from "@dogsbay/site-files";
34
34
  import { assembleBook, estimateTreeBytes } from "./granularity.js";
35
35
  /**
36
36
  * The per-topic "Read as single page" affordance (granularity book view).
@@ -1479,6 +1479,8 @@ function emitNotFoundPage(outputDir, options) {
1479
1479
  const sitemap = withBasePath(combined, "/sitemap-index.xml");
1480
1480
  const llms = withBasePath(combined, "/llms.txt");
1481
1481
  const showLlms = options.llmsTxt !== false;
1482
+ // The product menu (only its menu: the page is in no product, so it reads "Product")
1483
+ const productMenu = options.products ? productMenuRows(options.products, combined) !== undefined : false;
1482
1484
  writeFileSync(path, `---
1483
1485
  // AUTO-GENERATED by dogsbay site build — safe to delete; will regenerate next build.
1484
1486
  // To customize this page, remove the marker line above (or modify the file
@@ -1487,7 +1489,8 @@ import "@/styles/global.css";
1487
1489
  import DocsLayout from "@dogsbay/docs-layout/DocsLayout.astro";
1488
1490
  import type { SiteConfig } from "@dogsbay/types";
1489
1491
  import navData from "@/data/nav.json";
1490
- import siteConfigData from "@/data/site.json";
1492
+ import siteConfigData from "@/data/site.json";${productMenu ? `
1493
+ import switcherMapData from "@/data/switcherMap.json";` : ""}
1491
1494
  const siteConfig = siteConfigData as SiteConfig;
1492
1495
  const favicon = (siteConfigData as { favicon?: string }).favicon;
1493
1496
  ---
@@ -1502,7 +1505,8 @@ const favicon = (siteConfigData as { favicon?: string }).favicon;
1502
1505
  favicon={favicon}
1503
1506
  noindex={true}
1504
1507
  excludeFromSearch={true}
1505
- ogType="website"
1508
+ ogType="website"${productMenu ? `
1509
+ switcherMap={{ versions: [], byLogicalKey: {}, products: switcherMapData.products } as never}` : ""}
1506
1510
  >
1507
1511
  <article class="docs-prose">
1508
1512
  <h1>Page not found</h1>
@@ -2454,6 +2458,8 @@ export function emitConfigDerivedFiles(outputDir, options, pages = []) {
2454
2458
  */
2455
2459
  function emitRedirects(outputDir, options, pages) {
2456
2460
  const prefix = combinedPrefix(options);
2461
+ // A product's root goes straight to where its default version's home forwards (one hop, no chain to report)
2462
+ const forwarded = new Map(options.homeForwards ?? []);
2457
2463
  const { rules, errors, warnings } = collectRedirects({
2458
2464
  pages: pages.map((p) => ({
2459
2465
  slug: p.slug,
@@ -2462,11 +2468,13 @@ function emitRedirects(outputDir, options, pages) {
2462
2468
  // Front matter paths are relative to the page's language and version (#096): collectRedirects needs its original slug.
2463
2469
  multiSource: p.multiSource,
2464
2470
  })),
2465
- // A product's root forwards to its default version's home; a redirect the config gives for it wins.
2466
- config: options.products
2467
- ? { ...Object.fromEntries(productRootRedirects(options.products)), ...options.redirects }
2471
+ // A product's root forwards to its default version's home, a unit with no home page to its
2472
+ // first page (homeForwards); a redirect the config gives for either wins.
2473
+ config: options.products || options.homeForwards?.length
2474
+ ? { ...Object.fromEntries((options.products ? productRootRedirects(options.products) : []).map(([from, to]) => [from, forwarded.get(to) ?? to])), ...Object.fromEntries(forwarded), ...options.redirects }
2468
2475
  : options.redirects,
2469
2476
  prefix,
2477
+ lenient: options.allowBrokenRedirects === true,
2470
2478
  });
2471
2479
  if (errors.length > 0) {
2472
2480
  throw new Error(`redirects: ${errors.length} problem(s) — refusing to build.\n - ${errors.join("\n - ")}`);
@@ -2621,14 +2629,54 @@ export function emitSwitcherMap(pages, outputDir, options) {
2621
2629
  byLogicalKey[key].push(variant);
2622
2630
  }
2623
2631
  const data = { versions, locales, byLogicalKey };
2632
+ const menus = versionMenus(options, combined);
2633
+ if (menus)
2634
+ data.versionMenu = menus;
2624
2635
  if (options.products) {
2625
2636
  data.productVersions = productVersionRows(options.products, combined);
2637
+ const productMenu = productMenuRows(options.products, combined);
2638
+ if (productMenu)
2639
+ data.products = productMenu;
2626
2640
  emitProductDataFiles(pages, outputDir, options, options.products);
2627
2641
  }
2628
2642
  mkdirSync(dataDir, { recursive: true });
2629
2643
  writeFileSync(outPath, JSON.stringify(data, null, 2));
2630
2644
  }
2645
+ /** The product menu per locale (`<locale|_>`), or nothing with fewer than two products. */
2646
+ function productMenuRows(model, combined) {
2647
+ const locales = model.localeAxis ? [...new Set(model.units.map((u) => u.locale))] : [undefined];
2648
+ const out = {};
2649
+ for (const l of locales) {
2650
+ const rows = productSwitcher(model, l);
2651
+ if (rows.length < 2)
2652
+ continue;
2653
+ out[l ?? "_"] = rows.map((r) => ({ name: r.name, label: r.label, home: `${combined}${r.home}` }));
2654
+ }
2655
+ return Object.keys(out).length ? out : undefined;
2656
+ }
2631
2657
  /** Each product's switcher rows per locale (`<product|_>/<locale|_>`), with full home URLs. */
2658
+ /** Each version menu's links and limit, per product (`_`: site-wide), hrefs as the site serves them. */
2659
+ function versionMenus(options, combined) {
2660
+ const out = {};
2661
+ // A site path may be written with the base path (as redirects take it): dropped, then the served prefix added
2662
+ const base = normalizeBasePath(options.basePath).replace(/\/+$/, "");
2663
+ const unbased = (href) => (base && (href === base || href.startsWith(`${base}/`)) ? href.slice(base.length) || "/" : href);
2664
+ const add = (key, m) => {
2665
+ if (!m)
2666
+ return;
2667
+ // Checked at config load; options built in code are checked again, so a script URL never reaches the page
2668
+ const links = m.links.filter((l) => typeof l.label === "string" && l.label.trim() && isVersionMenuHref(l.href));
2669
+ if (!links.length && m.limit === undefined)
2670
+ return;
2671
+ out[key] = { links: links.map((l) => ({ label: l.label, href: versionMenuHref(unbased(l.href), combined) })), ...(m.limit !== undefined ? { limit: m.limit } : {}) };
2672
+ };
2673
+ if (options.products)
2674
+ for (const p of options.products.products)
2675
+ add(p.name ?? "_", p.versionMenu);
2676
+ else
2677
+ add("_", options.versionMenu);
2678
+ return Object.keys(out).length ? out : undefined;
2679
+ }
2632
2680
  function productVersionRows(model, combined) {
2633
2681
  const out = {};
2634
2682
  const locales = model.localeAxis ? [...new Set(model.units.map((u) => u.locale))] : [undefined];
@@ -2645,6 +2693,7 @@ function productVersionRows(model, combined) {
2645
2693
  ...(r.eol ? { eol: true } : {}),
2646
2694
  ...(r.prerelease ? { prerelease: true } : {}),
2647
2695
  ...(r.isDefault ? { default: true } : {}),
2696
+ ...(r.badge ? { badge: true } : {}),
2648
2697
  home: `${combined}${r.home}`,
2649
2698
  }));
2650
2699
  }
@@ -2715,6 +2764,7 @@ function composeAxisHeader(declared, seen, defaultId, allowEol) {
2715
2764
  ...(allowEol && d.eol === true ? { eol: true } : {}),
2716
2765
  ...(allowEol && d.prerelease === true ? { prerelease: true } : {}),
2717
2766
  ...(allowEol && d.hidden === true ? { hidden: true } : {}),
2767
+ ...(allowEol && d.badge === true ? { badge: true } : {}),
2718
2768
  ...(defaultId === d.id ? { default: true } : {}),
2719
2769
  });
2720
2770
  seenInDeclared.add(d.id);
@@ -54,6 +54,8 @@ export interface CollectRedirectsInput {
54
54
  config?: RedirectsConfig;
55
55
  /** URL prefix the site is served under (urlBase + basePath), "" when at the root. */
56
56
  prefix: string;
57
+ /** A rule the build cannot honour is left out with a warning instead of failing (`--allow-broken-redirects`). */
58
+ lenient?: boolean;
57
59
  }
58
60
  export interface CollectRedirectsResult {
59
61
  rules: RedirectRule[];
package/dist/redirects.js CHANGED
@@ -172,7 +172,7 @@ export function collectRedirects(input) {
172
172
  });
173
173
  }
174
174
  const resolved = resolveRedirects(candidates, {
175
- strict: true,
175
+ strict: input.lenient !== true,
176
176
  // A target's "#anchor" or "?query" is not part of the page it names.
177
177
  isPage: (path) => livePaths.has(path.split(/[?#]/)[0]),
178
178
  cloudflareLimits: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/format-astro",
3
- "version": "0.2.0-beta.147",
3
+ "version": "0.2.0-beta.149",
4
4
  "description": "Astro format plugin for Dogsbay — export TreeNode[] to Astro component markup",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,12 +32,12 @@
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
34
  "markdown-it": "^14.3.0",
35
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.147",
36
- "@dogsbay/serialize-core": "0.2.0-beta.147",
37
- "@dogsbay/render-html": "0.2.0-beta.147",
38
- "@dogsbay/site-files": "0.2.0-beta.147",
39
- "@dogsbay/types": "0.2.0-beta.147",
40
- "@dogsbay/ui": "0.2.0-beta.147"
35
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.149",
36
+ "@dogsbay/render-html": "0.2.0-beta.149",
37
+ "@dogsbay/serialize-core": "0.2.0-beta.149",
38
+ "@dogsbay/site-files": "0.2.0-beta.149",
39
+ "@dogsbay/types": "0.2.0-beta.149",
40
+ "@dogsbay/ui": "0.2.0-beta.149"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",