@dogsbay/format-astro 0.2.0-beta.144 → 0.2.0-beta.145

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.
@@ -7,7 +7,7 @@
7
7
  * plans/canonical-versions.md, step 2.
8
8
  */
9
9
  import type { ExportPage } from "@dogsbay/types";
10
- import { type VersionSeo } from "@dogsbay/site-files";
10
+ import { type ProductModel, type VersionSeo } from "@dogsbay/site-files";
11
11
  export interface CanonicalPlanOptions {
12
12
  /** Pages the build does not emit (fragments, `excludeFromRoutes`): never a canonical target. */
13
13
  isEmitted?: (page: ExportPage) => boolean;
@@ -18,6 +18,8 @@ export interface CanonicalPlanOptions {
18
18
  id: string;
19
19
  } & VersionSeo>;
20
20
  defaultVersion?: string;
21
+ /** With a `products:` block: each product's own versions and default replace `versions`/`defaultVersion`. */
22
+ products?: ProductModel;
21
23
  redirects?: Record<string, string | {
22
24
  to: string;
23
25
  status?: number;
package/dist/canonical.js CHANGED
@@ -1,4 +1,4 @@
1
- import { decideCanonical } from "@dogsbay/site-files";
1
+ import { decideCanonical, productDefault } from "@dogsbay/site-files";
2
2
  import { normalizePrefix, originOf, pageHref } from "./sitemap.js";
3
3
  import { inPageUnit, pageUnitPrefix } from "./redirects.js";
4
4
  const URL_RE = /^[a-z][a-z0-9+.-]*:/i;
@@ -60,27 +60,31 @@ export function canonicalPlan(pages, o) {
60
60
  return s;
61
61
  return s.endsWith("/" + inV) ? s.slice(0, -inV.length - 1) : s === inV ? "" : s;
62
62
  };
63
+ // The page's product's versions and default (the site-wide ones without a products: block).
64
+ const axisOf = (p) => o.products ? productDefault(o.products, p.multiSource?.namespace) ?? {} : { versions: o.versions, defaultVersion: o.defaultVersion };
63
65
  const defaults = new Map();
64
- if (o.defaultVersion) {
66
+ {
65
67
  for (const p of pages) {
66
- if (p.multiSource?.version !== o.defaultVersion || !live(p))
68
+ const dv = axisOf(p).defaultVersion;
69
+ if (!dv || p.multiSource?.version !== dv || !live(p))
67
70
  continue;
68
71
  const g = defaults.get(group(p)) ?? { prefix: prefixOf(p), pages: new Map() };
69
72
  g.pages.set(inVersion(p), slugOf(p));
70
73
  defaults.set(group(p), g);
71
74
  }
72
75
  }
73
- const hasOlder = pages.some((p) => p.multiSource?.version && p.multiSource.version !== o.defaultVersion);
76
+ const hasOlder = pages.some((p) => p.multiSource?.version && p.multiSource.version !== axisOf(p).defaultVersion);
74
77
  const url = (slug) => (origin ? `${origin}${pageHref(slug, prefix)}` : undefined);
75
78
  for (const p of pages) {
76
79
  if (!live(p))
77
80
  continue;
78
- const version = o.versions?.find((v) => v.id === p.multiSource?.version);
81
+ const axis = axisOf(p);
82
+ const version = axis.versions?.find((v) => v.id === p.multiSource?.version);
79
83
  const table = defaults.get(group(p));
80
84
  const fm = typeof p.frontmatter?.canonicalUrl === "string" ? p.frontmatter.canonicalUrl : undefined;
81
85
  const d = decideCanonical({
82
86
  selfUrl: url(slugOf(p)),
83
- isDefault: !hasOlder || !p.multiSource?.version || p.multiSource.version === o.defaultVersion || !table,
87
+ isDefault: !hasOlder || !p.multiSource?.version || p.multiSource.version === axis.defaultVersion || !table,
84
88
  path: inVersion(p),
85
89
  version,
86
90
  frontmatter: fm,
package/dist/index.d.ts CHANGED
@@ -31,5 +31,6 @@ export type { DiffMarks } from "./diff-decoration.js";
31
31
  */
32
32
  export { BOOK_SEGMENT, assembleBook, bookHeading, bookSlugFrom, estimateTreeBytes, openAsPageNode, rewriteIntraBookLinks, shiftHeadings, slugFromHref, } from "./granularity.js";
33
33
  export type { AssembleBookOptions, BookView } from "./granularity.js";
34
- export { pageUnitPrefix, inPageUnit, normalizeTarget } from "./redirects.js";
34
+ export { pageUnitPrefix, inPageUnit, normalizeTarget, collectRedirects } from "./redirects.js";
35
+ export { canonicalPlan } from "./canonical.js";
35
36
  export { TONES, CARD_TONE_CLASSES, TAB_VARIANTS, THEME_SCOPES } from "./tone-classes.js";
package/dist/index.js CHANGED
@@ -59,6 +59,7 @@ export { DIFF_CSS, decorateDiff, diffMarksOf, hasDiffMarks, injectAttrs, inlineS
59
59
  * See plans/granularity-views.md and docs/single-page-view.md.
60
60
  */
61
61
  export { BOOK_SEGMENT, assembleBook, bookHeading, bookSlugFrom, estimateTreeBytes, openAsPageNode, rewriteIntraBookLinks, shiftHeadings, slugFromHref, } from "./granularity.js";
62
- export { pageUnitPrefix, inPageUnit, normalizeTarget } from "./redirects.js";
62
+ export { pageUnitPrefix, inPageUnit, normalizeTarget, collectRedirects } from "./redirects.js";
63
+ export { canonicalPlan } from "./canonical.js";
63
64
  // Variants, tones and scoped themes: the class tables both renderers draw from
64
65
  export { TONES, CARD_TONE_CLASSES, TAB_VARIANTS, THEME_SCOPES } from "./tone-classes.js";
package/dist/project.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ExportPage, NavItem } from "@dogsbay/types";
2
+ import { type ProductModel } from "@dogsbay/site-files";
2
3
  import { type BlogEmitConfig } from "./blog.js";
3
4
  import { type RedirectsConfig } from "./redirects.js";
4
5
  /**
@@ -197,6 +198,14 @@ export interface AstroProjectOptions {
197
198
  * default (enforced at config load).
198
199
  */
199
200
  defaultVersion?: string;
201
+ /**
202
+ * With a `products:` block: the model from @dogsbay/site-files
203
+ * (`productModel`). Each product's own versions and default then drive
204
+ * canonicals, the version switcher (`productVersions`), the forward from
205
+ * a product's root to its default version, and the data files
206
+ * `_dogsbay/versions.js` and `<unit home>_dogsbay/pages.js`.
207
+ */
208
+ products?: ProductModel;
200
209
  /**
201
210
  * Declared locales for the i18n axis. Same role as
202
211
  * `versions` for the locale axis.
@@ -601,6 +610,20 @@ export interface SwitcherMap {
601
610
  * the full URL plus the (locale, version) combo it represents.
602
611
  */
603
612
  byLogicalKey: Record<string, SwitcherVariant[]>;
613
+ /**
614
+ * With a `products:` block: each product's version rows per locale, keyed
615
+ * `<product|_>/<locale|_>`, in config order, hidden versions left out, each
616
+ * with its home URL (where the row goes when the page is not in it). The
617
+ * version switcher uses these instead of `versions`.
618
+ */
619
+ productVersions?: Record<string, Array<{
620
+ id: string;
621
+ label?: string;
622
+ eol?: boolean;
623
+ prerelease?: boolean;
624
+ default?: boolean;
625
+ home: string;
626
+ }>>;
604
627
  }
605
628
  export interface SwitcherVariant {
606
629
  /** Present when the locale axis is active for this site. */
package/dist/project.js CHANGED
@@ -30,6 +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 { productRootRedirects, siteFiles, siteRootTarget, switcher as productSwitcherRows, toPagesScript, toScript, unitKeyString, unitManifest, unitOf, } from "@dogsbay/site-files";
33
34
  import { assembleBook, estimateTreeBytes } from "./granularity.js";
34
35
  /**
35
36
  * The per-topic "Read as single page" affordance (granularity book view).
@@ -1776,6 +1777,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
1776
1777
  basePath: combined,
1777
1778
  versions: options.versions,
1778
1779
  defaultVersion: options.defaultVersion,
1780
+ products: options.products,
1779
1781
  redirects: options.redirects,
1780
1782
  });
1781
1783
  // hreflang alternates, WebSite on home pages, BreadcrumbList from the nav (src/head-metadata.ts).
@@ -2348,13 +2350,16 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
2348
2350
  // With the default version at the root (`path: ""`) and no locale or
2349
2351
  // namespace, it has no subtree: its entries are the ones outside every
2350
2352
  // other version's subtree.
2351
- const firstHref = (defaultVersionPrefixes.length > 0
2352
- ? findFirstNavHrefUnder(outputNav, defaultVersionPrefixes, [...subtreeRoots]
2353
- .filter(([q]) => defaultVersionPrefixes.some((d) => `${combined}/${q}`.startsWith(`${d}/`)))
2354
- .map(([q]) => `${combined}/${q}`))
2355
- : rootVersion && subtreeRoots.size > 0
2356
- ? findFirstNavHrefUnder(outputNav, [combined], nestedIn(""))
2357
- : undefined) ?? findFirstNavHref(outputNav, basePath);
2353
+ // With products and no unnamed source, `/` goes to the first product's default home (site-files siteRootTarget).
2354
+ const productRoot = options.products ? siteRootTarget(options.products) : undefined;
2355
+ const firstHref = productRoot !== undefined ? `${combined}${productRoot}` :
2356
+ (defaultVersionPrefixes.length > 0
2357
+ ? findFirstNavHrefUnder(outputNav, defaultVersionPrefixes, [...subtreeRoots]
2358
+ .filter(([q]) => defaultVersionPrefixes.some((d) => `${combined}/${q}`.startsWith(`${d}/`)))
2359
+ .map(([q]) => `${combined}/${q}`))
2360
+ : rootVersion && subtreeRoots.size > 0
2361
+ ? findFirstNavHrefUnder(outputNav, [combined], nestedIn(""))
2362
+ : undefined) ?? findFirstNavHref(outputNav, basePath);
2358
2363
  const needsRootRedirect = basePath !== "" ||
2359
2364
  (!section && !rootIndexEmitted && firstHref !== "");
2360
2365
  if (needsRootRedirect) {
@@ -2457,7 +2462,10 @@ function emitRedirects(outputDir, options, pages) {
2457
2462
  // Front matter paths are relative to the page's language and version (#096): collectRedirects needs its original slug.
2458
2463
  multiSource: p.multiSource,
2459
2464
  })),
2460
- config: options.redirects,
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 }
2468
+ : options.redirects,
2461
2469
  prefix,
2462
2470
  });
2463
2471
  if (errors.length > 0) {
@@ -2572,7 +2580,7 @@ export function emitSwitcherMap(pages, outputDir, options) {
2572
2580
  }
2573
2581
  const versionAxisActive = versionsSeen.size >= 2;
2574
2582
  const localeAxisActive = localesSeen.size >= 2;
2575
- if (!versionAxisActive && !localeAxisActive) {
2583
+ if (!versionAxisActive && !localeAxisActive && !options.products) {
2576
2584
  mkdirSync(dataDir, { recursive: true });
2577
2585
  writeFileSync(outPath, JSON.stringify({ versions: [], locales: [], byLogicalKey: {} }, null, 2));
2578
2586
  return;
@@ -2601,9 +2609,73 @@ export function emitSwitcherMap(pages, outputDir, options) {
2601
2609
  byLogicalKey[key].push(variant);
2602
2610
  }
2603
2611
  const data = { versions, locales, byLogicalKey };
2612
+ if (options.products) {
2613
+ data.productVersions = productVersionRows(options.products, combined);
2614
+ emitProductDataFiles(pages, outputDir, options, options.products);
2615
+ }
2604
2616
  mkdirSync(dataDir, { recursive: true });
2605
2617
  writeFileSync(outPath, JSON.stringify(data, null, 2));
2606
2618
  }
2619
+ /** Each product's switcher rows per locale (`<product|_>/<locale|_>`), with full home URLs. */
2620
+ function productVersionRows(model, combined) {
2621
+ const out = {};
2622
+ const locales = model.localeAxis ? [...new Set(model.units.map((u) => u.locale))] : [undefined];
2623
+ for (const p of model.products) {
2624
+ if (!p.versions.length)
2625
+ continue;
2626
+ for (const l of locales) {
2627
+ const rows = productSwitcherRows(model, p.name, l);
2628
+ if (!rows.length)
2629
+ continue;
2630
+ out[`${p.name ?? "_"}/${l ?? "_"}`] = rows.map((r) => ({
2631
+ id: r.id,
2632
+ label: r.label,
2633
+ ...(r.eol ? { eol: true } : {}),
2634
+ ...(r.prerelease ? { prerelease: true } : {}),
2635
+ ...(r.isDefault ? { default: true } : {}),
2636
+ home: `${combined}${r.home}`,
2637
+ }));
2638
+ }
2639
+ }
2640
+ return out;
2641
+ }
2642
+ /**
2643
+ * The data files dogsbay-ssg writes too (@dogsbay/site-files products.ts):
2644
+ * `_dogsbay/versions.js` (+ `.json`), the switchers and product switcher every
2645
+ * page can load, and each unit's page list in its own folder,
2646
+ * `<home>_dogsbay/pages.js`. Scripts, so they also work from `file://`.
2647
+ */
2648
+ function emitProductDataFiles(pages, outputDir, options, model) {
2649
+ const publicDir = join(outputDir, "public", ...basePathSegments(normalizeBasePath(options.basePath)));
2650
+ const byUnit = new Map();
2651
+ for (const page of pages) {
2652
+ const ms = page.multiSource;
2653
+ if (!ms || page.redirect || page.fragment === true)
2654
+ continue;
2655
+ const key = unitKeyString({ ...(ms.namespace ? { product: ms.namespace } : {}), ...(ms.locale ? { locale: ms.locale } : {}), ...(ms.version ? { version: ms.version } : {}) });
2656
+ const list = byUnit.get(key) ?? [];
2657
+ list.push(ms.originalSlug.replace(/^\/+|\/+$/g, "").replace(/(^|\/)index$/, ""));
2658
+ byUnit.set(key, list);
2659
+ }
2660
+ const built = new Set(model.units.map(unitKeyString));
2661
+ const manifests = [...byUnit.entries()]
2662
+ .filter(([key]) => built.has(key))
2663
+ .map(([key, list]) => {
2664
+ const [product, locale, version] = key.split("/").map((x) => (x === "_" ? undefined : x));
2665
+ return unitManifest({ ...(product ? { product } : {}), ...(locale ? { locale } : {}), ...(version ? { version } : {}) }, { pages: [...new Set(list)].sort(), sitemap: [], redirects: [] });
2666
+ });
2667
+ const files = siteFiles(model, manifests);
2668
+ const dataDir = join(publicDir, "_dogsbay");
2669
+ mkdirSync(dataDir, { recursive: true });
2670
+ writeFileSync(join(dataDir, "versions.js"), toScript("__DOGSBAY_VERSIONS__", files.versions));
2671
+ writeFileSync(join(dataDir, "versions.json"), JSON.stringify(files.versions));
2672
+ for (const m of manifests) {
2673
+ const unit = unitOf(model, { ...(m.product ? { product: m.product } : {}), ...(m.locale ? { locale: m.locale } : {}), ...(m.version ? { version: m.version } : {}) });
2674
+ const dest = join(publicDir, ...unit.segments, "_dogsbay", "pages.js");
2675
+ mkdirSync(dirname(dest), { recursive: true });
2676
+ writeFileSync(dest, toPagesScript(unit.home, m.pages));
2677
+ }
2678
+ }
2607
2679
  /**
2608
2680
  * Compose a switcherMap axis header from declared entries +
2609
2681
  * the set of ids actually seen in pages. Honours declared
@@ -3163,6 +3235,7 @@ function emitSitemapFiles(outputDir, options, pages) {
3163
3235
  basePath: combined,
3164
3236
  versions: options.versions,
3165
3237
  defaultVersion: options.defaultVersion,
3238
+ products: options.products,
3166
3239
  redirects: options.redirects,
3167
3240
  });
3168
3241
  // More than one file past 50,000 URLs, the protocol's limit per file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/format-astro",
3
- "version": "0.2.0-beta.144",
3
+ "version": "0.2.0-beta.145",
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.144",
36
- "@dogsbay/render-html": "0.2.0-beta.144",
37
- "@dogsbay/serialize-core": "0.2.0-beta.144",
38
- "@dogsbay/site-files": "0.2.0-beta.144",
39
- "@dogsbay/types": "0.2.0-beta.144",
40
- "@dogsbay/ui": "0.2.0-beta.144"
35
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.145",
36
+ "@dogsbay/serialize-core": "0.2.0-beta.145",
37
+ "@dogsbay/render-html": "0.2.0-beta.145",
38
+ "@dogsbay/site-files": "0.2.0-beta.145",
39
+ "@dogsbay/ui": "0.2.0-beta.145",
40
+ "@dogsbay/types": "0.2.0-beta.145"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",