@dogsbay/format-astro 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/dist/base-path.d.ts +86 -0
- package/dist/base-path.d.ts.map +1 -1
- package/dist/base-path.js +117 -0
- package/dist/base-path.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/llms-txt.d.ts +13 -2
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +79 -12
- package/dist/llms-txt.js.map +1 -1
- package/dist/project.d.ts +89 -5
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +630 -92
- package/dist/project.js.map +1 -1
- package/dist/serialize.d.ts +13 -0
- package/dist/serialize.d.ts.map +1 -1
- package/dist/serialize.js +136 -16
- package/dist/serialize.js.map +1 -1
- package/dist/sitemap.d.ts +49 -0
- package/dist/sitemap.d.ts.map +1 -0
- package/dist/sitemap.js +119 -0
- package/dist/sitemap.js.map +1 -0
- package/dist/taxonomy.d.ts +13 -0
- package/dist/taxonomy.d.ts.map +1 -1
- package/dist/taxonomy.js +53 -18
- package/dist/taxonomy.js.map +1 -1
- package/package.json +4 -4
package/dist/project.js
CHANGED
|
@@ -4,13 +4,28 @@
|
|
|
4
4
|
* Takes ExportPage[] + NavItem[] and generates a complete Astro project
|
|
5
5
|
* with static .astro pages using real Dogsbay components.
|
|
6
6
|
*/
|
|
7
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, cpSync, readdirSync, statSync, } from "node:fs";
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, cpSync, readdirSync, statSync, } from "node:fs";
|
|
8
8
|
import { join, dirname, relative, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { treeToDogsbayMd } from "@dogsbay/format-dogsbay-md";
|
|
11
|
-
import { treeToAstro } from "./serialize.js";
|
|
11
|
+
import { treeToAstro, TONE_CLASSES } from "./serialize.js";
|
|
12
12
|
import { buildLlmsTxt, buildSectionLlmsTxt, buildLlmsFullTxt } from "./llms-txt.js";
|
|
13
|
-
import {
|
|
13
|
+
import { buildSitemap, buildSitemapIndex } from "./sitemap.js";
|
|
14
|
+
import { normalizeBasePath, basePathSegments, buildCurrentPath, withBasePath, parseSiteUrl, combinePrefix, } from "./base-path.js";
|
|
15
|
+
/**
|
|
16
|
+
* Combined URL prefix = urlBase (Astro `base` from site.url path) +
|
|
17
|
+
* basePath (filesystem layout prefix). Every URL emitter (nav,
|
|
18
|
+
* sitemap, llms.txt, .md mirror, _headers, taxonomy) uses this for
|
|
19
|
+
* href output. Filesystem-layout consumers (mkdir, page output
|
|
20
|
+
* paths) keep using basePath alone — Astro's `base` config adds the
|
|
21
|
+
* urlBase prefix at route time.
|
|
22
|
+
*
|
|
23
|
+
* See plans/astro-base-from-site-url.md.
|
|
24
|
+
*/
|
|
25
|
+
function combinedPrefix(options) {
|
|
26
|
+
const { urlBase } = parseSiteUrl(options.siteUrl);
|
|
27
|
+
return combinePrefix(urlBase, normalizeBasePath(options.basePath));
|
|
28
|
+
}
|
|
14
29
|
import { detectLeadingNodes } from "./lead.js";
|
|
15
30
|
/**
|
|
16
31
|
* Recursively prefix all hrefs in a nav tree.
|
|
@@ -147,6 +162,125 @@ function buildWranglerConfig(siteName, options) {
|
|
|
147
162
|
lines.push(`}`);
|
|
148
163
|
return lines.join("\n") + "\n";
|
|
149
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Build the GitHub Actions workflow YAML for `actions/deploy-pages`.
|
|
167
|
+
*
|
|
168
|
+
* The workflow:
|
|
169
|
+
* 1. Checks out the repo on every push to the default branch.
|
|
170
|
+
* 2. Installs node + pnpm at the Astro project directory, runs
|
|
171
|
+
* `dogsbay site build` (via `pnpm dlx` since Dogsbay is a
|
|
172
|
+
* global CLI, not a project dep), then `pnpm run build`
|
|
173
|
+
* (which runs `astro build && pagefind`).
|
|
174
|
+
* 3. Uploads `<astroDirRel>/dist` as a Pages artifact via
|
|
175
|
+
* `actions/upload-pages-artifact`.
|
|
176
|
+
* 4. Deploys via `actions/deploy-pages`.
|
|
177
|
+
*
|
|
178
|
+
* `astroDirRel` is the path of the Astro output relative to the
|
|
179
|
+
* repo root (typically "astro" — the default config has
|
|
180
|
+
* `output: ./astro`). Empty string is allowed when the project is
|
|
181
|
+
* flat (outputDir === projectDir); the workflow degrades naturally
|
|
182
|
+
* by omitting the `defaults: working-directory` block.
|
|
183
|
+
*
|
|
184
|
+
* Author edits — extra build steps, secrets, deploy gating — survive
|
|
185
|
+
* subsequent `dogsbay site build` runs because the file is written
|
|
186
|
+
* write-if-missing (see emitDeployArtifacts). To start over, delete
|
|
187
|
+
* the workflow file and rebuild.
|
|
188
|
+
*
|
|
189
|
+
* Note on basePath: GitHub Pages serves project sites at
|
|
190
|
+
* `https://<user>.github.io/<repo>/`. Authors who want their docs at
|
|
191
|
+
* the repo root should set `site.basePath: /<repo-name>` (or empty
|
|
192
|
+
* for user/org pages). The platform's basePath plumbing handles all
|
|
193
|
+
* URL rewriting; this workflow doesn't need to know about it.
|
|
194
|
+
*/
|
|
195
|
+
function buildGitHubPagesWorkflow(astroDirRel) {
|
|
196
|
+
// When the Astro output IS the project root, drop the working-
|
|
197
|
+
// directory block and reference cache + artifact paths without a
|
|
198
|
+
// prefix. This is the flat-layout case (rare for site-init flows;
|
|
199
|
+
// common for `dogsbay convert` outputs that get manually wired up).
|
|
200
|
+
const isFlat = astroDirRel === "" || astroDirRel === ".";
|
|
201
|
+
const workingDirBlock = isFlat
|
|
202
|
+
? ""
|
|
203
|
+
: `
|
|
204
|
+
defaults:
|
|
205
|
+
run:
|
|
206
|
+
working-directory: ${astroDirRel}`;
|
|
207
|
+
const cacheDep = isFlat
|
|
208
|
+
? "pnpm-lock.yaml"
|
|
209
|
+
: `${astroDirRel}/pnpm-lock.yaml`;
|
|
210
|
+
const artifactPath = isFlat ? "dist" : `${astroDirRel}/dist`;
|
|
211
|
+
return `# Deploy to GitHub Pages.
|
|
212
|
+
# Generated by \`dogsbay site init --deploy=github-pages\` (or by
|
|
213
|
+
# adding \`deploy: { target: github-pages }\` to dogsbay.config.yml
|
|
214
|
+
# and running \`dogsbay site build\`). Author edits survive every
|
|
215
|
+
# subsequent build — the file is never overwritten. To regenerate
|
|
216
|
+
# from template, delete the file and rebuild.
|
|
217
|
+
#
|
|
218
|
+
# Repo settings: Settings → Pages → Source = "GitHub Actions".
|
|
219
|
+
name: Deploy to GitHub Pages
|
|
220
|
+
|
|
221
|
+
on:
|
|
222
|
+
push:
|
|
223
|
+
branches: [main]
|
|
224
|
+
workflow_dispatch:
|
|
225
|
+
|
|
226
|
+
permissions:
|
|
227
|
+
contents: read
|
|
228
|
+
pages: write
|
|
229
|
+
id-token: write
|
|
230
|
+
|
|
231
|
+
# Allow only one concurrent deployment, skipping queued runs.
|
|
232
|
+
concurrency:
|
|
233
|
+
group: pages
|
|
234
|
+
cancel-in-progress: false
|
|
235
|
+
|
|
236
|
+
jobs:
|
|
237
|
+
build:
|
|
238
|
+
runs-on: ubuntu-latest${workingDirBlock}
|
|
239
|
+
steps:
|
|
240
|
+
- uses: actions/checkout@v4
|
|
241
|
+
|
|
242
|
+
- uses: pnpm/action-setup@v4
|
|
243
|
+
with:
|
|
244
|
+
version: 10
|
|
245
|
+
|
|
246
|
+
- uses: actions/setup-node@v4
|
|
247
|
+
with:
|
|
248
|
+
# Astro 6 requires Node ^20.19.5 || >=22.12.0; pin 22 for
|
|
249
|
+
# forward-compat (Node 20 LTS is fine for Astro 5 sites
|
|
250
|
+
# but the Dogsbay scaffold targets Astro 6).
|
|
251
|
+
node-version: 22
|
|
252
|
+
cache: pnpm
|
|
253
|
+
cache-dependency-path: ${cacheDep}
|
|
254
|
+
|
|
255
|
+
- name: Install dependencies
|
|
256
|
+
run: pnpm install --frozen-lockfile
|
|
257
|
+
|
|
258
|
+
# \`dogsbay\` is a global CLI, not a project dep — pnpm dlx
|
|
259
|
+
# fetches it on demand. To pin a version, replace with e.g.
|
|
260
|
+
# \`pnpm dlx dogsbay@0.2.0-beta.18 site build\`.
|
|
261
|
+
- name: Build with Dogsbay
|
|
262
|
+
run: pnpm dlx dogsbay@beta site build
|
|
263
|
+
|
|
264
|
+
- name: Build Astro site
|
|
265
|
+
run: pnpm run build
|
|
266
|
+
|
|
267
|
+
- name: Upload Pages artifact
|
|
268
|
+
uses: actions/upload-pages-artifact@v3
|
|
269
|
+
with:
|
|
270
|
+
path: ${artifactPath}
|
|
271
|
+
|
|
272
|
+
deploy:
|
|
273
|
+
needs: build
|
|
274
|
+
runs-on: ubuntu-latest
|
|
275
|
+
environment:
|
|
276
|
+
name: github-pages
|
|
277
|
+
url: \${{ steps.deployment.outputs.page_url }}
|
|
278
|
+
steps:
|
|
279
|
+
- name: Deploy to GitHub Pages
|
|
280
|
+
id: deployment
|
|
281
|
+
uses: actions/deploy-pages@v4
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
150
284
|
/**
|
|
151
285
|
* Construct the SiteConfig object that gets serialized to
|
|
152
286
|
* `src/data/site.json`. Backward-compatible: existing fields keep their
|
|
@@ -187,7 +321,23 @@ function buildSiteConfig(siteName, options) {
|
|
|
187
321
|
}
|
|
188
322
|
if (options.taxonomyIndexPaths &&
|
|
189
323
|
Object.keys(options.taxonomyIndexPaths).length > 0) {
|
|
190
|
-
|
|
324
|
+
// Bake basePath into every emitted indexPath so consumers
|
|
325
|
+
// (TypeBadge / StatusBadge / future components) compose hrefs
|
|
326
|
+
// like `${indexPath}/<value>/` and resolve under the configured
|
|
327
|
+
// site base. Without the prefix, `/by-type/tutorial/` 404s on
|
|
328
|
+
// any site with `site.basePath` set. Caller passes raw config
|
|
329
|
+
// values (`/by-type`, `/tags`, etc.) — basePath threading is
|
|
330
|
+
// this emitter's responsibility, matching how `page.url` is
|
|
331
|
+
// already prefixed in the taxonomy data file.
|
|
332
|
+
// Taxonomy index paths are baked into site.json so components
|
|
333
|
+
// (TagList, TaxonomyIndex, TypeBadge) emit correct hrefs at
|
|
334
|
+
// runtime. Use combined so these resolve under the host's
|
|
335
|
+
// served subpath.
|
|
336
|
+
const taxoPrefix = combinedPrefix(options);
|
|
337
|
+
cfg.taxonomyIndexPaths = Object.fromEntries(Object.entries(options.taxonomyIndexPaths).map(([name, raw]) => [
|
|
338
|
+
name,
|
|
339
|
+
withBasePath(taxoPrefix, raw),
|
|
340
|
+
]));
|
|
191
341
|
}
|
|
192
342
|
if (options.taxonomyDisplay &&
|
|
193
343
|
Object.keys(options.taxonomyDisplay).length > 0) {
|
|
@@ -257,6 +407,74 @@ function ensureDirectoryStructure(outputDir, basePath) {
|
|
|
257
407
|
export function emitSiteConfig(outputDir, siteName, options) {
|
|
258
408
|
mkdirSync(join(outputDir, "src", "data"), { recursive: true });
|
|
259
409
|
writeFileSync(join(outputDir, "src", "data", "site.json"), JSON.stringify(buildSiteConfig(siteName, options), null, 2));
|
|
410
|
+
// Auto-generated companion to astro.config.mjs. Carries the
|
|
411
|
+
// site/base values derived from dogsbay.config.yml's site.url so
|
|
412
|
+
// changes propagate without --force-rescaffolding the main
|
|
413
|
+
// astro.config.mjs (which is scaffold-once and may have author
|
|
414
|
+
// edits — custom integrations, build hooks, etc.). The main
|
|
415
|
+
// config imports `dogsbaySite` + `dogsbayBase` from here.
|
|
416
|
+
// See plans/astro-base-from-site-url.md.
|
|
417
|
+
const { origin, urlBase: astroBase } = parseSiteUrl(options.siteUrl);
|
|
418
|
+
const hasSiteUrl = Boolean(options.siteUrl && /^https?:\/\//.test(options.siteUrl));
|
|
419
|
+
const dogsbaySiteJson = hasSiteUrl
|
|
420
|
+
? JSON.stringify(origin ?? options.siteUrl)
|
|
421
|
+
: "undefined";
|
|
422
|
+
const dogsbayBaseJson = astroBase ? JSON.stringify(astroBase) : "undefined";
|
|
423
|
+
// build.inlineStylesheets — defaults to "auto" (Astro's own
|
|
424
|
+
// default; matches our docs-first bias since theme.css is ~120KB
|
|
425
|
+
// and externalizing it lets the file cache cross-page). Authors
|
|
426
|
+
// wanting "always" / "never" set it via dogsbay.config.yml's
|
|
427
|
+
// build.inlineStylesheets. See docs/perf-tuning.md.
|
|
428
|
+
const dogsbayInline = options.inlineStylesheets ?? "auto";
|
|
429
|
+
writeFileSync(join(outputDir, "astro.config.dogsbay.mjs"), [
|
|
430
|
+
"// Auto-generated by `dogsbay site build` — DO NOT EDIT.",
|
|
431
|
+
"// Tracks site.url + derived Astro base + build behaviour from",
|
|
432
|
+
"// dogsbay.config.yml. Edit dogsbay.config.yml and rebuild;",
|
|
433
|
+
"// edits to this file will be overwritten on the next build.",
|
|
434
|
+
`export const dogsbaySite = ${dogsbaySiteJson};`,
|
|
435
|
+
`export const dogsbayBase = ${dogsbayBaseJson};`,
|
|
436
|
+
`export const dogsbayInlineStylesheets = ${JSON.stringify(dogsbayInline)};`,
|
|
437
|
+
"",
|
|
438
|
+
].join("\n"));
|
|
439
|
+
// Migration check: pre-beta.20 sites have an astro.config.mjs that
|
|
440
|
+
// doesn't import the companion. Without the import, the values
|
|
441
|
+
// emitted above are unused and Astro's `base` stays unset — the
|
|
442
|
+
// exact bug this work was meant to close. Warn loudly, with the
|
|
443
|
+
// patch the user needs to apply, until astro.config.mjs is
|
|
444
|
+
// updated. We don't auto-patch because the file may have author
|
|
445
|
+
// edits (custom integrations, build hooks).
|
|
446
|
+
const astroConfigPath = join(outputDir, "astro.config.mjs");
|
|
447
|
+
if (existsSync(astroConfigPath)) {
|
|
448
|
+
const astroConfigSrc = readFileSync(astroConfigPath, "utf-8");
|
|
449
|
+
if (!astroConfigSrc.includes("astro.config.dogsbay.mjs")) {
|
|
450
|
+
console.warn([
|
|
451
|
+
"",
|
|
452
|
+
" ⚠ astro.config.mjs is missing the dogsbay companion import.",
|
|
453
|
+
" Without it, Astro's `base` config stays unset and assets",
|
|
454
|
+
" served from a host subpath (GH Pages project pages,",
|
|
455
|
+
" multi-mount Cloudflare) will 404.",
|
|
456
|
+
"",
|
|
457
|
+
" Add these two lines to astro.config.mjs:",
|
|
458
|
+
"",
|
|
459
|
+
' import {',
|
|
460
|
+
' dogsbaySite,',
|
|
461
|
+
' dogsbayBase,',
|
|
462
|
+
' dogsbayInlineStylesheets,',
|
|
463
|
+
' } from "./astro.config.dogsbay.mjs";',
|
|
464
|
+
"",
|
|
465
|
+
" export default defineConfig({",
|
|
466
|
+
" ...(dogsbaySite ? { site: dogsbaySite } : {}),",
|
|
467
|
+
" ...(dogsbayBase ? { base: dogsbayBase } : {}),",
|
|
468
|
+
" build: { inlineStylesheets: dogsbayInlineStylesheets },",
|
|
469
|
+
" // ...your existing config...",
|
|
470
|
+
" });",
|
|
471
|
+
"",
|
|
472
|
+
" OR regenerate from template (overwrites your edits):",
|
|
473
|
+
" dogsbay site init . --scaffold-only --force",
|
|
474
|
+
"",
|
|
475
|
+
].join("\n"));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
260
478
|
}
|
|
261
479
|
export function emitSiteScaffold(outputDir, siteName, options, writeScaffold) {
|
|
262
480
|
let scaffoldFilesSkipped = 0;
|
|
@@ -271,15 +489,36 @@ export function emitSiteScaffold(outputDir, siteName, options, writeScaffold) {
|
|
|
271
489
|
// - --local outside workspace: use file: paths pointing at monorepo packages
|
|
272
490
|
// - Otherwise: versioned specs (expects registry-published packages)
|
|
273
491
|
const insideWs = isInsideWorkspace(outputDir);
|
|
492
|
+
// Read format-astro's own version at runtime — every @dogsbay/*
|
|
493
|
+
// package ships in lockstep at the same version (publish-beta.sh
|
|
494
|
+
// bumps them together), so format-astro's version is the right
|
|
495
|
+
// peer spec for `@dogsbay/docs-layout`, `@dogsbay/ui`, etc. in
|
|
496
|
+
// the scaffolded astro/package.json. Fixes the "hardcoded
|
|
497
|
+
// ^0.1.0" trap that caused npm install to fail post-publish.
|
|
498
|
+
const dogsbayPeerVersion = (() => {
|
|
499
|
+
try {
|
|
500
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
501
|
+
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf-8"));
|
|
502
|
+
// Caret on a stable version, exact pin on a prerelease (npm
|
|
503
|
+
// treats `^0.2.0-beta.2` as NOT matching `0.2.0-beta.3` — the
|
|
504
|
+
// prerelease semantics force exact-or-explicit-range. Pinning
|
|
505
|
+
// prereleases avoids surprise resolves to incompatible betas.)
|
|
506
|
+
return /-/.test(pkg.version) ? pkg.version : `^${pkg.version}`;
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return "^0.1.0"; // last-resort fallback
|
|
510
|
+
}
|
|
511
|
+
})();
|
|
274
512
|
const dogsbayDep = (name) => {
|
|
275
513
|
if (insideWs)
|
|
276
514
|
return "workspace:*";
|
|
277
515
|
if (options.local)
|
|
278
516
|
return `file:${resolveMonorepoPkg(name)}`;
|
|
279
|
-
return
|
|
517
|
+
return dogsbayPeerVersion;
|
|
280
518
|
};
|
|
281
519
|
// Per-deploy-target additions to package.json
|
|
282
520
|
const isCloudflare = options.deploy === "cloudflare-workers";
|
|
521
|
+
const isGitHubPages = options.deploy === "github-pages";
|
|
283
522
|
const deployScripts = isCloudflare
|
|
284
523
|
? { deploy: "pnpm build && wrangler deploy" }
|
|
285
524
|
: {};
|
|
@@ -305,7 +544,11 @@ export function emitSiteScaffold(outputDir, siteName, options, writeScaffold) {
|
|
|
305
544
|
},
|
|
306
545
|
dependencies: {
|
|
307
546
|
astro: "^6.0.0",
|
|
308
|
-
|
|
547
|
+
// Sitemap is emitted directly by Dogsbay into
|
|
548
|
+
// public/<basePath>/sitemap-{index,0}.xml so multi-mount
|
|
549
|
+
// deploys don't collide at the host root. We deliberately
|
|
550
|
+
// do NOT depend on @astrojs/sitemap (it hardcodes output to
|
|
551
|
+
// dist/ root, which is what we're moving away from).
|
|
309
552
|
// Pagefind is invoked from the build script (see scripts.build above).
|
|
310
553
|
// Lives in dependencies (not devDependencies) so production builds
|
|
311
554
|
// include it; the produced search index is shipped statically and
|
|
@@ -329,6 +572,13 @@ export function emitSiteScaffold(outputDir, siteName, options, writeScaffold) {
|
|
|
329
572
|
"@dogsbay/icons": dogsbayDep("icons"),
|
|
330
573
|
"@dogsbay/elements": dogsbayDep("elements"),
|
|
331
574
|
},
|
|
575
|
+
// Pin transitive Vite to 7. Vite 8 just released; Astro 6
|
|
576
|
+
// peer-deps Vite 7 and prints a warning when 8 is hoisted.
|
|
577
|
+
// Without this override npm picks up Vite 8 by default.
|
|
578
|
+
// Drop this when Astro 7 ships and bumps its peer.
|
|
579
|
+
overrides: {
|
|
580
|
+
vite: "^7",
|
|
581
|
+
},
|
|
332
582
|
...(Object.keys(deployDevDeps).length > 0
|
|
333
583
|
? { devDependencies: deployDevDeps }
|
|
334
584
|
: {}),
|
|
@@ -347,6 +597,18 @@ export function emitSiteScaffold(outputDir, siteName, options, writeScaffold) {
|
|
|
347
597
|
scaffoldFilesSkipped++;
|
|
348
598
|
}
|
|
349
599
|
}
|
|
600
|
+
// GitHub Pages deploy artifacts — workflow + .nojekyll. The actual
|
|
601
|
+
// emission lives in `emitDeployArtifacts` so site-build can also
|
|
602
|
+
// call it on existing sites without going through scaffold (a user
|
|
603
|
+
// adds `deploy: github-pages` to dogsbay.config.yml and reruns
|
|
604
|
+
// `site build` to get the workflow). At scaffold-time we pass
|
|
605
|
+
// forceOverwrite=writeScaffold so `--force` regenerates from
|
|
606
|
+
// template; on regular builds it stays write-if-missing.
|
|
607
|
+
if (isGitHubPages) {
|
|
608
|
+
emitDeployArtifacts(outputDir, options, {
|
|
609
|
+
forceOverwrite: writeScaffold,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
350
612
|
// Generate astro.config.mjs
|
|
351
613
|
// `preserveSymlinks: true` is used with --local to pin local file: deps to
|
|
352
614
|
// their on-disk paths. Inside a pnpm workspace this breaks Astro's internal
|
|
@@ -358,52 +620,50 @@ export function emitSiteScaffold(outputDir, siteName, options, writeScaffold) {
|
|
|
358
620
|
preserveSymlinks: true,
|
|
359
621
|
},`
|
|
360
622
|
: "";
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
// the generated config). Sitemap also filters out frontmatter-noindex pages.
|
|
365
|
-
const hasSiteUrl = Boolean(options.siteUrl && /^https?:\/\//.test(options.siteUrl));
|
|
366
|
-
const sitemapImport = hasSiteUrl ? `import sitemap from "@astrojs/sitemap";\n` : "";
|
|
367
|
-
// Strip any path component from site.url before emitting. The
|
|
368
|
-
// config validator already rejects `site.url` containing a path
|
|
369
|
-
// when `basePath` is non-empty (canonical URLs would double-count
|
|
370
|
-
// the prefix); this is a defensive normalisation for the case
|
|
371
|
-
// where the validator is bypassed or basePath is empty.
|
|
623
|
+
// siteUrl gates absolute-URL emission (sitemap <loc> entries,
|
|
624
|
+
// canonical tags). Without one, both are skipped — relative URLs
|
|
625
|
+
// are still correct, the sitemap is just not generated.
|
|
372
626
|
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
// to
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
// spread is a no-op. See plans/plugin-api.md.
|
|
627
|
+
// Sitemap is emitted directly by Dogsbay (see emitSitemapFiles)
|
|
628
|
+
// into public/<basePath>/sitemap-*.xml. We deliberately do NOT
|
|
629
|
+
// wire @astrojs/sitemap here; that integration hardcodes output
|
|
630
|
+
// to dist/ root, breaking multi-mount deploys.
|
|
631
|
+
const hasSiteUrl = Boolean(options.siteUrl && /^https?:\/\//.test(options.siteUrl));
|
|
632
|
+
// site.url's path component (if any) becomes Astro's `base`. The
|
|
633
|
+
// origin alone goes into `site`. This split lets dogsbay model
|
|
634
|
+
// both axes independently:
|
|
635
|
+
// - Astro's `base` (= urlBase) controls the URL prefix Astro
|
|
636
|
+
// bakes into HTML asset references (`<basePath>/_astro/...`)
|
|
637
|
+
// and the routes Astro generates from src/pages.
|
|
638
|
+
// - dogsbay's basePath controls the filesystem layout
|
|
639
|
+
// (`src/pages/<basePath>/...`).
|
|
640
|
+
// The two compose at emit time — combining for nav hrefs,
|
|
641
|
+
// sitemap, llms.txt, etc. See plans/astro-base-from-site-url.md.
|
|
642
|
+
const { origin, urlBase: astroBase } = parseSiteUrl(options.siteUrl);
|
|
643
|
+
// astro.config.mjs — scaffold-once, but the site/base values flow
|
|
644
|
+
// through a separate auto-generated file (`astro.config.dogsbay.mjs`,
|
|
645
|
+
// emitted unconditionally below) so dogsbay-derived values stay in
|
|
646
|
+
// sync with `dogsbay.config.yml` even on existing sites where the
|
|
647
|
+
// main config is preserved. Same pattern as
|
|
648
|
+
// `astro.config.plugins.mjs` — the import line is the load-bearing
|
|
649
|
+
// bit; the auto-file is what changes.
|
|
397
650
|
if (writeScaffold) {
|
|
398
651
|
writeFileSync(join(outputDir, "astro.config.mjs"), `import { defineConfig } from "astro/config";
|
|
399
652
|
import tailwindcss from "@tailwindcss/vite";
|
|
400
|
-
|
|
653
|
+
import { pluginAliases, pluginFsAllow } from "./astro.config.plugins.mjs";
|
|
654
|
+
import {
|
|
655
|
+
dogsbaySite,
|
|
656
|
+
dogsbayBase,
|
|
657
|
+
dogsbayInlineStylesheets,
|
|
658
|
+
} from "./astro.config.dogsbay.mjs";
|
|
401
659
|
|
|
402
|
-
export default defineConfig({
|
|
660
|
+
export default defineConfig({
|
|
661
|
+
...(dogsbaySite ? { site: dogsbaySite } : {}),
|
|
662
|
+
...(dogsbayBase ? { base: dogsbayBase } : {}),
|
|
403
663
|
output: "static",
|
|
404
664
|
build: {
|
|
405
|
-
inlineStylesheets:
|
|
406
|
-
}
|
|
665
|
+
inlineStylesheets: dogsbayInlineStylesheets,
|
|
666
|
+
},
|
|
407
667
|
vite: {
|
|
408
668
|
plugins: [tailwindcss()],
|
|
409
669
|
resolve: {
|
|
@@ -425,6 +685,9 @@ export default defineConfig({${siteField}
|
|
|
425
685
|
else {
|
|
426
686
|
scaffoldFilesSkipped++;
|
|
427
687
|
}
|
|
688
|
+
// astro.config.dogsbay.mjs is emitted by emitSiteConfig (called
|
|
689
|
+
// above and on every site build) so site/base values stay in
|
|
690
|
+
// sync without a re-scaffold. See its definition for rationale.
|
|
428
691
|
// Always seed an empty astro.config.plugins.mjs so the import in
|
|
429
692
|
// astro.config.mjs resolves before the first plugin-emitting
|
|
430
693
|
// build. Subsequent builds replace it via emitPluginRuntime.
|
|
@@ -495,7 +758,12 @@ export default defineConfig({${siteField}
|
|
|
495
758
|
*/
|
|
496
759
|
export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
497
760
|
const siteName = options.siteName || "Documentation";
|
|
761
|
+
// basePath = filesystem layout prefix (where pages live under
|
|
762
|
+
// src/pages/...). combined = the URL prefix HTML hrefs need
|
|
763
|
+
// (urlBase + basePath). The two diverge whenever site.url has a
|
|
764
|
+
// path component (GH Pages project pages, multi-mount Cloudflare).
|
|
498
765
|
const basePath = normalizeBasePath(options.basePath);
|
|
766
|
+
const combined = combinedPrefix(options);
|
|
499
767
|
const baseSegments = basePathSegments(basePath);
|
|
500
768
|
// Ensure dirs exist (callers may invoke us without going through the
|
|
501
769
|
// full exportAstroProject orchestrator, e.g. dogsbay convert at Step 7).
|
|
@@ -516,7 +784,11 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
516
784
|
// Remove existing entry for this section (full replace)
|
|
517
785
|
existingNav = existingNav.filter((item) => item.label?.toLowerCase() !== siteName.toLowerCase()
|
|
518
786
|
&& item.label?.toLowerCase() !== section.toLowerCase());
|
|
519
|
-
|
|
787
|
+
// Nav hrefs already carry the `combined` prefix (the importer
|
|
788
|
+
// emits them via fileToHref(file, hrefPrefix=combined)).
|
|
789
|
+
// prefixNavHrefs takes the existing prefix and weaves a section
|
|
790
|
+
// segment into it.
|
|
791
|
+
const prefixedNav = prefixNavHrefs(nav, section, combined);
|
|
520
792
|
const sectionLabel = siteName
|
|
521
793
|
|| section.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
522
794
|
existingNav.push({ label: sectionLabel, children: prefixedNav });
|
|
@@ -531,13 +803,16 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
531
803
|
copyAssets(options.sourceDir, outputDir, options.imageOptimization);
|
|
532
804
|
}
|
|
533
805
|
let generated = 0;
|
|
806
|
+
const generatedPaths = new Set();
|
|
534
807
|
const pagesDir = join(outputDir, "src", "pages", ...baseSegments);
|
|
535
808
|
const useImageOpt = options.imageOptimization ?? false;
|
|
536
|
-
// hrefPrefix is the
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
|
|
809
|
+
// hrefPrefix is the COMBINED prefix (urlBase + basePath) — what
|
|
810
|
+
// rendered HTML hrefs need so internal links resolve under the
|
|
811
|
+
// host's served subpath AND under the dogsbay basePath. For
|
|
812
|
+
// simple host-apex deploys with basePath, urlBase is empty so
|
|
813
|
+
// combined === basePath (back-compat). For GH Pages project pages
|
|
814
|
+
// and multi-mount Cloudflare, combined adds the urlBase layer.
|
|
815
|
+
const hrefPrefix = combined;
|
|
541
816
|
for (const page of pages) {
|
|
542
817
|
try {
|
|
543
818
|
// Rewrite internal hrefs to match the output URL structure
|
|
@@ -581,7 +856,23 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
581
856
|
const pageCategory = Array.isArray(pageMeta?.category)
|
|
582
857
|
? pageMeta.category
|
|
583
858
|
: undefined;
|
|
584
|
-
|
|
859
|
+
// Custom-taxonomy values lifted from frontmatter into
|
|
860
|
+
// `meta.taxonomies` by the importer (see `parseMeta` in
|
|
861
|
+
// `@dogsbay/types`). Surfaced to DocsLayout so it can emit one
|
|
862
|
+
// `<div data-pagefind-filter="<name>:<value>">` per entry — this
|
|
863
|
+
// is what makes user-declared taxonomies (`difficulty`, `team`,
|
|
864
|
+
// anything not in the five built-ins) appear as visible facet
|
|
865
|
+
// checkboxes in the search dialog. Without this passthrough
|
|
866
|
+
// they're silently dropped after the importer.
|
|
867
|
+
const pageTaxonomies = pageMeta?.taxonomies && Object.keys(pageMeta.taxonomies).length > 0
|
|
868
|
+
? pageMeta.taxonomies
|
|
869
|
+
: undefined;
|
|
870
|
+
// `tagsIndexPath` flows to `<TagList>` for chip hrefs
|
|
871
|
+
// (`${indexPath}/${tag}/`). Caller passes the raw config value
|
|
872
|
+
// (e.g. `/tags`); we bake basePath here so chips resolve under
|
|
873
|
+
// the configured site base. Without the prefix, every tag chip
|
|
874
|
+
// 404s on any site with `site.basePath` set.
|
|
875
|
+
const tagsIndexPath = withBasePath(basePath, options.tagsIndexPath ?? "/tags");
|
|
585
876
|
// Auto-lede detection. If the markdown body doesn't already
|
|
586
877
|
// start with an H1 / leading paragraph, we ask DocsLayout to
|
|
587
878
|
// render the frontmatter title / description at the top of
|
|
@@ -613,12 +904,20 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
613
904
|
// available via other means. The "Open in" deep links work
|
|
614
905
|
// regardless of mirror availability — agents that can't fetch
|
|
615
906
|
// the page just see the URL in their chat.
|
|
907
|
+
// pageHrefBase uses combined (urlBase + basePath) so the URL
|
|
908
|
+
// resolves correctly when the host serves dist/ at a subpath
|
|
909
|
+
// (GH Pages project page, multi-mount Cloudflare).
|
|
616
910
|
const pageHrefBase = section
|
|
617
|
-
? (
|
|
618
|
-
: (
|
|
911
|
+
? (combined ? `${combined}/${section}/${page.slug}` : `/${section}/${page.slug}`)
|
|
912
|
+
: (combined ? `${combined}/${page.slug}` : `/${page.slug}`);
|
|
619
913
|
const pageMdHref = `${pageHrefBase}.md`;
|
|
620
|
-
|
|
621
|
-
|
|
914
|
+
// For absolute URLs (the "Copy as MD" deep link), use the
|
|
915
|
+
// origin (no path) + the full combined path; siteUrl alone
|
|
916
|
+
// would double-include the urlBase since pageHrefBase already
|
|
917
|
+
// contains it.
|
|
918
|
+
const { origin } = parseSiteUrl(options.siteUrl);
|
|
919
|
+
const pageMdAbsoluteUrl = origin
|
|
920
|
+
? origin + pageMdHref
|
|
622
921
|
: pageMdHref;
|
|
623
922
|
// Markdown body for the Copy button. Reuse the same serializer
|
|
624
923
|
// that produces the .md mirror so what the user copies matches
|
|
@@ -660,7 +959,10 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
660
959
|
"",
|
|
661
960
|
`const headings = ${JSON.stringify(page.headings || [])};`,
|
|
662
961
|
`const nav = navData;`,
|
|
663
|
-
|
|
962
|
+
// currentPath uses combined so it matches nav.json hrefs
|
|
963
|
+
// (which are also combined-prefixed). getPagination compares
|
|
964
|
+
// them as strings; mismatched prefixes break prev/next.
|
|
965
|
+
`const currentPath = "${buildCurrentPath(combined, section, page.slug)}";`,
|
|
664
966
|
// Filter nav to the current (locale, version) bucket
|
|
665
967
|
// before computing prev/next — without this, pagination
|
|
666
968
|
// walks the global nav and a "Next" link can leak from
|
|
@@ -684,6 +986,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
684
986
|
`const pageType = ${JSON.stringify(pageTypeStr ?? null)};`,
|
|
685
987
|
`const pageAudience = ${JSON.stringify(pageAudience ?? null)};`,
|
|
686
988
|
`const pageCategory = ${JSON.stringify(pageCategory ?? null)};`,
|
|
989
|
+
`const pageTaxonomies = ${JSON.stringify(pageTaxonomies ?? null)};`,
|
|
687
990
|
`const tagsIndexPath = ${JSON.stringify(tagsIndexPath)};`,
|
|
688
991
|
`const llmActionsProps = ${JSON.stringify(llmActionsEnabled
|
|
689
992
|
? {
|
|
@@ -729,6 +1032,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
729
1032
|
` pageType={pageType ?? undefined}`,
|
|
730
1033
|
` audience={pageAudience ?? undefined}`,
|
|
731
1034
|
` category={pageCategory ?? undefined}`,
|
|
1035
|
+
` taxonomies={pageTaxonomies ?? undefined}`,
|
|
732
1036
|
` autoH1={${autoH1}}`,
|
|
733
1037
|
` autoLede={${autoLede}}`,
|
|
734
1038
|
` llmActions={llmActionsProps}`,
|
|
@@ -771,6 +1075,7 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
771
1075
|
mkdirSync(dirname(pagePath), { recursive: true });
|
|
772
1076
|
writeFileSync(pagePath, pageLines.join("\n") + "\n");
|
|
773
1077
|
generated++;
|
|
1078
|
+
generatedPaths.add(relative(outputDir, pagePath));
|
|
774
1079
|
// Companion .md endpoint for content negotiation. Prerendered, so
|
|
775
1080
|
// it's served as a static asset at runtime — no Worker overhead.
|
|
776
1081
|
//
|
|
@@ -811,9 +1116,43 @@ export async function emitAstroPages(pages, nav, outputDir, options) {
|
|
|
811
1116
|
// redirect target), and writing a redirect would clobber it.
|
|
812
1117
|
if (basePath !== "") {
|
|
813
1118
|
const firstHref = findFirstNavHref(nav, basePath);
|
|
814
|
-
|
|
1119
|
+
const indexPath = join(outputDir, "src", "pages", "index.astro");
|
|
1120
|
+
writeFileSync(indexPath, `---\nreturn Astro.redirect("${firstHref}");\n---\n`);
|
|
1121
|
+
generatedPaths.add(relative(outputDir, indexPath));
|
|
1122
|
+
}
|
|
1123
|
+
return { generated, outputNav, generatedPaths };
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Copy each passthrough `.astro` source to its computed output path.
|
|
1127
|
+
* Aborts with a clear error if the destination is already in
|
|
1128
|
+
* `generatedPaths` (a generated page from `emitAstroPages` would
|
|
1129
|
+
* silently overwrite the hand-authored file otherwise).
|
|
1130
|
+
*/
|
|
1131
|
+
export function emitPassthroughAstroPages(copies, outputDir, generatedPaths) {
|
|
1132
|
+
if (copies.length === 0)
|
|
1133
|
+
return { copied: 0 };
|
|
1134
|
+
// Collision detection — a generated page and a passthrough page
|
|
1135
|
+
// would write to the same file. Refuse to overwrite; tell the
|
|
1136
|
+
// author exactly which two files conflict.
|
|
1137
|
+
const collisions = [];
|
|
1138
|
+
for (const copy of copies) {
|
|
1139
|
+
if (generatedPaths.has(copy.outputRelPath)) {
|
|
1140
|
+
collisions.push(copy.outputRelPath);
|
|
1141
|
+
}
|
|
815
1142
|
}
|
|
816
|
-
|
|
1143
|
+
if (collisions.length > 0) {
|
|
1144
|
+
throw new Error(`Passthrough Astro page collides with a generated page:\n` +
|
|
1145
|
+
collisions.map((c) => ` - ${c}`).join("\n") + "\n" +
|
|
1146
|
+
`Rename the .astro source or remove the colliding entry from nav.yml.`);
|
|
1147
|
+
}
|
|
1148
|
+
let copied = 0;
|
|
1149
|
+
for (const copy of copies) {
|
|
1150
|
+
const dest = join(outputDir, copy.outputRelPath);
|
|
1151
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
1152
|
+
copyFileSync(copy.sourceAbs, dest);
|
|
1153
|
+
copied++;
|
|
1154
|
+
}
|
|
1155
|
+
return { copied };
|
|
817
1156
|
}
|
|
818
1157
|
// ─── Tier 1: config-derived ─────────────────────────────────────────────
|
|
819
1158
|
// Files driven entirely by config + flags. Always regenerated; site
|
|
@@ -829,6 +1168,54 @@ export function emitConfigDerivedFiles(outputDir, options) {
|
|
|
829
1168
|
const hasSiteUrl = Boolean(options.siteUrl && /^https?:\/\//.test(options.siteUrl));
|
|
830
1169
|
writeFileSync(join(outputDir, "public", "robots.txt"), buildRobotsTxt(options, hasSiteUrl));
|
|
831
1170
|
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Per-deploy-target artifact emission.
|
|
1173
|
+
*
|
|
1174
|
+
* Called from `emitSiteScaffold` (with `forceOverwrite=writeScaffold`
|
|
1175
|
+
* so `--force` regenerates from template) and from `dogsbay site
|
|
1176
|
+
* build` (with `forceOverwrite=false` so an existing site can adopt
|
|
1177
|
+
* a deploy target by editing config and rebuilding — the missing
|
|
1178
|
+
* artifact gets created on the next build).
|
|
1179
|
+
*
|
|
1180
|
+
* Emit policy is the union: write when forced OR when the file is
|
|
1181
|
+
* missing. Author edits to e.g. the workflow YAML survive every
|
|
1182
|
+
* regular build.
|
|
1183
|
+
*
|
|
1184
|
+
* Currently handles `github-pages` (workflow + .nojekyll). The
|
|
1185
|
+
* existing `cloudflare-workers` artifacts (wrangler.jsonc + package
|
|
1186
|
+
* scripts) stay in the scaffold-only path because they overlap with
|
|
1187
|
+
* scaffold-only files (package.json scripts, devDependencies). A
|
|
1188
|
+
* future refactor could fold them in here too.
|
|
1189
|
+
*/
|
|
1190
|
+
export function emitDeployArtifacts(outputDir, options, opts = { forceOverwrite: false }) {
|
|
1191
|
+
if (options.deploy === "github-pages") {
|
|
1192
|
+
// GitHub reads workflows from <repo-root>/.github/workflows/, NOT
|
|
1193
|
+
// from inside subdirectories. Use projectDir (the repo root) for
|
|
1194
|
+
// the workflow file; fall back to outputDir when unset (flat
|
|
1195
|
+
// `dogsbay convert` flows where the Astro project IS the repo).
|
|
1196
|
+
const projectDir = options.projectDir ?? outputDir;
|
|
1197
|
+
// Path of the Astro output relative to the project root. Used by
|
|
1198
|
+
// the workflow's working-directory + cache-dependency-path so
|
|
1199
|
+
// pnpm install / pnpm run build target the right place. Empty
|
|
1200
|
+
// string when outputDir === projectDir (flat layout).
|
|
1201
|
+
const astroDirRel = relative(projectDir, outputDir).replace(/\\/g, "/");
|
|
1202
|
+
const workflowPath = join(projectDir, ".github", "workflows", "deploy.yml");
|
|
1203
|
+
if (opts.forceOverwrite || !existsSync(workflowPath)) {
|
|
1204
|
+
mkdirSync(dirname(workflowPath), { recursive: true });
|
|
1205
|
+
writeFileSync(workflowPath, buildGitHubPagesWorkflow(astroDirRel));
|
|
1206
|
+
}
|
|
1207
|
+
// .nojekyll — must exist in the deployed artifact root so GH
|
|
1208
|
+
// Pages skips Jekyll's `_underscored-paths` filter (Astro's
|
|
1209
|
+
// `_astro/` chunk dir gets eaten otherwise). Lives inside the
|
|
1210
|
+
// Astro project's `public/` so it's copied into `dist/` at
|
|
1211
|
+
// build time.
|
|
1212
|
+
const nojekyllPath = join(outputDir, "public", ".nojekyll");
|
|
1213
|
+
mkdirSync(dirname(nojekyllPath), { recursive: true });
|
|
1214
|
+
if (opts.forceOverwrite || !existsSync(nojekyllPath)) {
|
|
1215
|
+
writeFileSync(nojekyllPath, "");
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
832
1219
|
/**
|
|
833
1220
|
* Emit `src/data/switcherMap.json` describing per-page
|
|
834
1221
|
* version + locale equivalents. Always writes the file —
|
|
@@ -845,7 +1232,10 @@ export function emitConfigDerivedFiles(outputDir, options) {
|
|
|
845
1232
|
* baseline page in a multi-version site).
|
|
846
1233
|
*/
|
|
847
1234
|
export function emitSwitcherMap(pages, outputDir, options) {
|
|
848
|
-
|
|
1235
|
+
// Switcher URLs use combined so the link the dropdown emits
|
|
1236
|
+
// resolves under the host's served subpath (GH Pages project
|
|
1237
|
+
// pages, multi-mount Cloudflare).
|
|
1238
|
+
const combined = combinedPrefix(options);
|
|
849
1239
|
const dataDir = join(outputDir, "src", "data");
|
|
850
1240
|
const outPath = join(dataDir, "switcherMap.json");
|
|
851
1241
|
// Detect axis activation by inspecting the data the loader
|
|
@@ -884,7 +1274,7 @@ export function emitSwitcherMap(pages, outputDir, options) {
|
|
|
884
1274
|
const variant = {
|
|
885
1275
|
...(ms.locale !== undefined ? { locale: ms.locale } : {}),
|
|
886
1276
|
...(ms.version !== undefined ? { version: ms.version } : {}),
|
|
887
|
-
url: `${
|
|
1277
|
+
url: `${combined}/${page.slug}`,
|
|
888
1278
|
};
|
|
889
1279
|
if (!byLogicalKey[key])
|
|
890
1280
|
byLogicalKey[key] = [];
|
|
@@ -962,6 +1352,10 @@ export function emitMissingTranslationStubs(pages, outputDir, options) {
|
|
|
962
1352
|
return;
|
|
963
1353
|
const basePath = normalizeBasePath(options.basePath);
|
|
964
1354
|
const baseSegments = basePathSegments(basePath);
|
|
1355
|
+
// combined drives the redirect URL (the user-facing path they
|
|
1356
|
+
// get bounced to); basePath stays the filesystem path under
|
|
1357
|
+
// src/pages/ where the stub lives.
|
|
1358
|
+
const combined = combinedPrefix(options);
|
|
965
1359
|
// Index existing pages by (slug after locale segment) so we
|
|
966
1360
|
// can detect missing translations cheaply. Key shape:
|
|
967
1361
|
// `<other-axis-prefix>/<originalSlug>` where other-axis-prefix
|
|
@@ -995,7 +1389,7 @@ export function emitMissingTranslationStubs(pages, outputDir, options) {
|
|
|
995
1389
|
const targetUrl = `${basePath}/${targetSlug}`;
|
|
996
1390
|
if (existingByUrl.has(targetUrl))
|
|
997
1391
|
continue; // already translated
|
|
998
|
-
const defaultUrl = `${
|
|
1392
|
+
const defaultUrl = `${combined}/${defaultPage.slug}`;
|
|
999
1393
|
const filePath = join(outputDir, "src", "pages", ...baseSegments, ...targetSlug.split("/"));
|
|
1000
1394
|
// Ensure parent dir exists; write a redirect-stub Astro
|
|
1001
1395
|
// file. Adding `.astro` to the leaf turns it into a
|
|
@@ -1037,10 +1431,20 @@ export function emitAgentReadinessFiles(pages, outputNav, outputDir, siteName, o
|
|
|
1037
1431
|
if (options.llmsTxt !== false) {
|
|
1038
1432
|
emitLlmsTxtFiles(outputDir, siteName, options, outputNav, pages);
|
|
1039
1433
|
// public/_headers — Cloudflare Workers / Pages convention. Adds an
|
|
1040
|
-
// RFC 8288 Link header pointing agents at
|
|
1041
|
-
// HTML. Emitted alongside
|
|
1434
|
+
// RFC 8288 Link header pointing agents at this mount's llms.txt
|
|
1435
|
+
// (basePath-prefixed) without parsing HTML. Emitted alongside
|
|
1436
|
+
// llms.txt so the two files travel together.
|
|
1042
1437
|
mkdirSync(join(outputDir, "public"), { recursive: true });
|
|
1043
|
-
|
|
1438
|
+
// _headers Link header points at the per-mount llms.txt at
|
|
1439
|
+
// <combined>/llms.txt — the URL agents would actually fetch.
|
|
1440
|
+
writeFileSync(join(outputDir, "public", "_headers"), buildHeadersFile(combinedPrefix(options)));
|
|
1441
|
+
}
|
|
1442
|
+
// Sitemap — emitted by Dogsbay (not @astrojs/sitemap) into
|
|
1443
|
+
// public/<basePath>/sitemap-{index,0}.xml so multi-mount deploys
|
|
1444
|
+
// don't collide at host root. Gated on a valid http(s) siteUrl
|
|
1445
|
+
// because <loc> entries must be absolute.
|
|
1446
|
+
if (options.siteUrl && /^https?:\/\//.test(options.siteUrl)) {
|
|
1447
|
+
emitSitemapFiles(outputDir, options, pages);
|
|
1044
1448
|
}
|
|
1045
1449
|
// src/middleware.ts — Tier 1 (always update). Drives both the
|
|
1046
1450
|
// `Accept: text/markdown` content-negotiation rewrite (via
|
|
@@ -1059,12 +1463,29 @@ export function emitAgentReadinessFiles(pages, outputNav, outputDir, siteName, o
|
|
|
1059
1463
|
const localeRedirectOn = options.defaultLocale !== undefined && knownLocales.length >= 2;
|
|
1060
1464
|
const axisRedirectOn = versionRedirectOn || localeRedirectOn;
|
|
1061
1465
|
if (mdMirrorOn || axisRedirectOn) {
|
|
1466
|
+
// Taxonomy index paths share a single global namespace across
|
|
1467
|
+
// locales / versions (one `/tags/` for the whole site, not one
|
|
1468
|
+
// per locale). The redirect helper has to know to skip them or
|
|
1469
|
+
// it will 302 chip hrefs to non-existent locale-prefixed routes.
|
|
1470
|
+
// Strip leading `/` and pull just the first segment so a config
|
|
1471
|
+
// like `/tags` becomes the global-prefix entry `tags`.
|
|
1472
|
+
const globalPrefixes = [];
|
|
1473
|
+
if (options.taxonomyIndexPaths) {
|
|
1474
|
+
for (const raw of Object.values(options.taxonomyIndexPaths)) {
|
|
1475
|
+
const first = raw.replace(/^\/+/, "").split("/")[0];
|
|
1476
|
+
if (first)
|
|
1477
|
+
globalPrefixes.push(first);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1062
1480
|
mkdirSync(join(outputDir, "src"), { recursive: true });
|
|
1063
1481
|
writeFileSync(join(outputDir, "src", "middleware.ts"), buildMiddlewareSource({
|
|
1064
1482
|
mdMirror: mdMirrorOn,
|
|
1065
1483
|
axisRedirect: axisRedirectOn
|
|
1066
1484
|
? {
|
|
1067
|
-
|
|
1485
|
+
// Middleware compares paths against the request URL,
|
|
1486
|
+
// which carries the host's served subpath — so use the
|
|
1487
|
+
// combined prefix here.
|
|
1488
|
+
basePath: combinedPrefix(options),
|
|
1068
1489
|
...(versionRedirectOn
|
|
1069
1490
|
? {
|
|
1070
1491
|
defaultVersion: options.defaultVersion,
|
|
@@ -1077,6 +1498,7 @@ export function emitAgentReadinessFiles(pages, outputNav, outputDir, siteName, o
|
|
|
1077
1498
|
knownLocales,
|
|
1078
1499
|
}
|
|
1079
1500
|
: {}),
|
|
1501
|
+
...(globalPrefixes.length > 0 ? { globalPrefixes } : {}),
|
|
1080
1502
|
}
|
|
1081
1503
|
: undefined,
|
|
1082
1504
|
}));
|
|
@@ -1137,21 +1559,37 @@ function buildRobotsTxt(options, hasSiteUrl) {
|
|
|
1137
1559
|
const aiInput = options.aiInput ?? "yes";
|
|
1138
1560
|
const aiTrain = options.aiTrain ?? "no";
|
|
1139
1561
|
const contentSignal = `Content-Signal: search=${search}, ai-input=${aiInput}, ai-train=${aiTrain}\n`;
|
|
1140
|
-
|
|
1141
|
-
|
|
1562
|
+
// Per-mount sitemap path: each Dogsbay site emits its sitemap
|
|
1563
|
+
// index under <basePath>/, so robots.txt must point there too.
|
|
1564
|
+
// (Multi-mount deploys end up with one robots.txt per site at
|
|
1565
|
+
// their respective hosts / paths; each correctly references its
|
|
1566
|
+
// own mount's sitemap-index.)
|
|
1567
|
+
// Sitemap URL = origin + combined + /sitemap-index.xml. Use the
|
|
1568
|
+
// origin (no path) from site.url and the combined prefix (urlBase
|
|
1569
|
+
// + basePath); siteUrl could itself include a path component when
|
|
1570
|
+
// hosting on a subpath (GH Pages project page), so we strip it
|
|
1571
|
+
// here to avoid double-counting.
|
|
1572
|
+
const { origin } = parseSiteUrl(options.siteUrl);
|
|
1573
|
+
const sitemap = hasSiteUrl && origin
|
|
1574
|
+
? `Sitemap: ${origin}${withBasePath(combinedPrefix(options), "/sitemap-index.xml")}\n`
|
|
1142
1575
|
: "";
|
|
1143
1576
|
return `User-agent: *\nAllow: /\n${contentSignal}${sitemap}`;
|
|
1144
1577
|
}
|
|
1145
1578
|
/**
|
|
1146
1579
|
* Build the contents of `public/_headers` (Cloudflare Pages / Workers
|
|
1147
1580
|
* Static Assets convention). Emits a global RFC 8288 Link header
|
|
1148
|
-
* pointing at
|
|
1581
|
+
* pointing at this mount's llms.txt index, so agents don't need to
|
|
1149
1582
|
* parse HTML to discover the LLM-friendly content listing.
|
|
1583
|
+
*
|
|
1584
|
+
* The Link target is basePath-prefixed (`</docs/llms.txt>` for a
|
|
1585
|
+
* `/docs` mount) — matches where the platform actually emits
|
|
1586
|
+
* llms.txt under the per-mount layout.
|
|
1150
1587
|
*/
|
|
1151
|
-
function buildHeadersFile() {
|
|
1588
|
+
function buildHeadersFile(basePath) {
|
|
1589
|
+
const llmsHref = withBasePath(basePath, "/llms.txt");
|
|
1152
1590
|
return [
|
|
1153
1591
|
"/*",
|
|
1154
|
-
|
|
1592
|
+
` Link: <${llmsHref}>; rel="describedby"; type="text/plain"`,
|
|
1155
1593
|
"",
|
|
1156
1594
|
].join("\n");
|
|
1157
1595
|
}
|
|
@@ -1160,20 +1598,28 @@ function buildMiddlewareSource(config) {
|
|
|
1160
1598
|
"// AUTO-GENERATED by `dogsbay site build` — do not edit.",
|
|
1161
1599
|
"// Composes the docs-layout middleware helpers.",
|
|
1162
1600
|
"//",
|
|
1163
|
-
"//
|
|
1164
|
-
"//
|
|
1165
|
-
"//
|
|
1166
|
-
"//
|
|
1167
|
-
"//
|
|
1168
|
-
"//
|
|
1169
|
-
"//
|
|
1170
|
-
"//
|
|
1171
|
-
"//
|
|
1172
|
-
"//
|
|
1173
|
-
"//
|
|
1601
|
+
"// Static-prerender guard:",
|
|
1602
|
+
"// In Astro's static output mode, this middleware is invoked",
|
|
1603
|
+
"// for every prerendered route at build time. Reading",
|
|
1604
|
+
"// `context.request.headers` there triggers an Astro warning",
|
|
1605
|
+
"// per page (\"Astro.request.headers was used during static",
|
|
1606
|
+
"// render\"), which floods `dogsbay site build` / `site preview`",
|
|
1607
|
+
"// output. Worse, the negotiation can't actually happen at",
|
|
1608
|
+
"// build time — there's no runtime client whose Accept header",
|
|
1609
|
+
"// we'd be honoring.",
|
|
1610
|
+
"//",
|
|
1611
|
+
"// We guard with `context.isPrerendered` so prerendered routes",
|
|
1612
|
+
"// short-circuit to `next()` immediately. At runtime in static",
|
|
1613
|
+
"// deploys, middleware doesn't fire at all (no server); at",
|
|
1614
|
+
"// runtime in SSR / hybrid deploys, only dynamic routes fire,",
|
|
1615
|
+
"// which is exactly when negotiation makes sense.",
|
|
1174
1616
|
"//",
|
|
1175
|
-
"//
|
|
1176
|
-
"//
|
|
1617
|
+
"// Markdown content negotiation:",
|
|
1618
|
+
"// For pure-static deploys, `Accept: text/markdown` is honored",
|
|
1619
|
+
"// by the platform (Cloudflare _headers + Worker, Netlify Edge",
|
|
1620
|
+
"// functions). Agents that can't send Accept headers should",
|
|
1621
|
+
"// follow the page's <link rel=\"alternate\" type=\"text/markdown\">",
|
|
1622
|
+
"// to fetch the .md mirror directly (e.g. /docs.md).",
|
|
1177
1623
|
'import { defineMiddleware } from "astro:middleware";',
|
|
1178
1624
|
];
|
|
1179
1625
|
if (config.mdMirror) {
|
|
@@ -1187,6 +1633,11 @@ function buildMiddlewareSource(config) {
|
|
|
1187
1633
|
lines.push(`const AXIS_REDIRECT_CONFIG = ${JSON.stringify(config.axisRedirect, null, 2)};`, "");
|
|
1188
1634
|
}
|
|
1189
1635
|
lines.push("export const onRequest = defineMiddleware((context, next) => {");
|
|
1636
|
+
// Skip prerendered routes — see file-top comment for the rationale.
|
|
1637
|
+
// Avoids per-page Astro.request.headers warnings during build, and
|
|
1638
|
+
// matches runtime semantics (middleware doesn't fire on prerendered
|
|
1639
|
+
// routes when deployed).
|
|
1640
|
+
lines.push(" if (context.isPrerendered) return next();");
|
|
1190
1641
|
lines.push(" const url = new URL(context.request.url);");
|
|
1191
1642
|
if (config.mdMirror) {
|
|
1192
1643
|
lines.push(' const accept = context.request.headers.get("accept");', " const mdTarget = shouldRewriteToMarkdown(accept, url.pathname);", " if (mdTarget) return context.rewrite(mdTarget);");
|
|
@@ -1218,8 +1669,18 @@ function buildMdEndpoint(page, sourceRel) {
|
|
|
1218
1669
|
].join("\n");
|
|
1219
1670
|
}
|
|
1220
1671
|
/**
|
|
1221
|
-
* Emit
|
|
1222
|
-
*
|
|
1672
|
+
* Emit per-mount llms.txt + llms-full.txt + per-section indexes.
|
|
1673
|
+
*
|
|
1674
|
+
* Files live under `public/<basePath>/...` so multiple Dogsbay sites
|
|
1675
|
+
* can mount on the same host (`/docs/llms.txt` + `/api/llms.txt` +
|
|
1676
|
+
* `/handbook/llms.txt`) without colliding at the root. When basePath
|
|
1677
|
+
* is empty, this collapses to `public/llms.txt` — the single-site
|
|
1678
|
+
* llmstxt.org-spec layout.
|
|
1679
|
+
*
|
|
1680
|
+
* The host root `/llms.txt` is intentionally NOT emitted by the
|
|
1681
|
+
* platform: it's the user's umbrella file, analogous to
|
|
1682
|
+
* `sitemap-index.xml`. Multi-mount deploys hand-write a top-level
|
|
1683
|
+
* `/llms.txt` that links to each per-mount index.
|
|
1223
1684
|
*
|
|
1224
1685
|
* Per-section files are written for every top-level nav group that
|
|
1225
1686
|
* resolves to a site directory (either via `group.href` or via the
|
|
@@ -1231,26 +1692,64 @@ function emitLlmsTxtFiles(outputDir, siteName, options, nav, pages) {
|
|
|
1231
1692
|
description: options.description,
|
|
1232
1693
|
siteUrl: options.siteUrl,
|
|
1233
1694
|
};
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1695
|
+
// hrefPrefix is the COMBINED prefix — used for the URL paths that
|
|
1696
|
+
// appear inside the llms.txt body (so agents fetch the correct
|
|
1697
|
+
// host-relative URLs). Filesystem layout uses basePath alone:
|
|
1698
|
+
// `public/<basePath>/llms.txt` matches the existing per-mount
|
|
1699
|
+
// delivery shape.
|
|
1700
|
+
const hrefPrefix = combinedPrefix(options);
|
|
1701
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
1702
|
+
const baseSegments = basePathSegments(basePath);
|
|
1703
|
+
const mountDir = join(outputDir, "public", ...baseSegments);
|
|
1704
|
+
mkdirSync(mountDir, { recursive: true });
|
|
1705
|
+
writeFileSync(join(mountDir, "llms.txt"), buildLlmsTxt(siteConfig, nav, pages, { hrefPrefix }));
|
|
1706
|
+
writeFileSync(join(mountDir, "llms-full.txt"), buildLlmsFullTxt(siteConfig, nav, pages, {
|
|
1239
1707
|
summary: "body",
|
|
1240
1708
|
serializePage: serializePageMd,
|
|
1241
1709
|
hrefPrefix,
|
|
1242
1710
|
}));
|
|
1711
|
+
// Per-section files: `deriveSectionDir` returns a host-absolute
|
|
1712
|
+
// path (already includes basePath because nav hrefs do). Join it to
|
|
1713
|
+
// public/ directly — joining onto mountDir would double-prefix into
|
|
1714
|
+
// `public/<basePath>/<basePath>/<section>/llms.txt`.
|
|
1243
1715
|
for (const group of nav) {
|
|
1244
1716
|
if (!group.children || group.children.length === 0)
|
|
1245
1717
|
continue;
|
|
1246
1718
|
const dir = deriveSectionDir(group);
|
|
1247
1719
|
if (!dir)
|
|
1248
1720
|
continue;
|
|
1249
|
-
const sectionPath = join(
|
|
1721
|
+
const sectionPath = join(outputDir, "public", dir, "llms.txt");
|
|
1250
1722
|
mkdirSync(dirname(sectionPath), { recursive: true });
|
|
1251
1723
|
writeFileSync(sectionPath, buildSectionLlmsTxt(siteConfig, group, pages, { hrefPrefix }));
|
|
1252
1724
|
}
|
|
1253
1725
|
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Emit per-mount sitemap files.
|
|
1728
|
+
*
|
|
1729
|
+
* Writes `public/<basePath>/sitemap-index.xml` + `sitemap-0.xml`.
|
|
1730
|
+
* The index lists the single sub-sitemap today; future splits add
|
|
1731
|
+
* more sub-sitemap entries as the page count grows past
|
|
1732
|
+
* sitemaps.org's 50K-URL recommendation.
|
|
1733
|
+
*
|
|
1734
|
+
* Caller has already guarded on a valid http(s) `siteUrl` — without
|
|
1735
|
+
* one, `<loc>` entries can't be absolute and crawlers reject the
|
|
1736
|
+
* file. Skip emission rather than write a broken sitemap.
|
|
1737
|
+
*/
|
|
1738
|
+
function emitSitemapFiles(outputDir, options, pages) {
|
|
1739
|
+
// Filesystem path uses basePath (sitemap files live in
|
|
1740
|
+
// public/<basePath>/sitemap-*.xml). The URL prefix encoded into
|
|
1741
|
+
// each <loc> uses combined so the absolute URLs resolve under the
|
|
1742
|
+
// host's served subpath. buildSitemap strips path off siteUrl
|
|
1743
|
+
// internally, so passing siteUrl + combined as basePath gives
|
|
1744
|
+
// origin + combined as the final URL.
|
|
1745
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
1746
|
+
const combined = combinedPrefix(options);
|
|
1747
|
+
const baseSegments = basePathSegments(basePath);
|
|
1748
|
+
const mountDir = join(outputDir, "public", ...baseSegments);
|
|
1749
|
+
mkdirSync(mountDir, { recursive: true });
|
|
1750
|
+
writeFileSync(join(mountDir, "sitemap-0.xml"), buildSitemap(pages, { siteUrl: options.siteUrl, basePath: combined }));
|
|
1751
|
+
writeFileSync(join(mountDir, "sitemap-index.xml"), buildSitemapIndex({ siteUrl: options.siteUrl, basePath: combined }));
|
|
1752
|
+
}
|
|
1254
1753
|
/**
|
|
1255
1754
|
* Pick a directory under `public/` for a top-level nav group. Prefers
|
|
1256
1755
|
* the group's own href (already a `/docs/x/y` path); otherwise falls
|
|
@@ -1328,6 +1827,11 @@ function copyComponents(outputDir) {
|
|
|
1328
1827
|
"response-tabs", "schema-viewer", "code-samples", "copy-button",
|
|
1329
1828
|
"markdown-example",
|
|
1330
1829
|
"accordion", "link-card", "avatar", "math",
|
|
1830
|
+
// Icon resolves @ui/icon/Icon.astro → built-time SVG inlining
|
|
1831
|
+
// via @dogsbay/icons. Used by `:::cards` `{icon=...}` and the
|
|
1832
|
+
// inline `:icon[name]` directive. Without this entry every page
|
|
1833
|
+
// emitting the icon import 500s with "module not found".
|
|
1834
|
+
"icon",
|
|
1331
1835
|
];
|
|
1332
1836
|
for (const name of needed) {
|
|
1333
1837
|
const src = join(componentsSource, name);
|
|
@@ -1382,6 +1886,32 @@ function copyAssets(sourceDir, outputDir, imageOptimization) {
|
|
|
1382
1886
|
catch { /* source may not exist */ }
|
|
1383
1887
|
}
|
|
1384
1888
|
// ── CSS generation (ported from import-mkdocs.ts) ───────
|
|
1889
|
+
/**
|
|
1890
|
+
* Build the `@source inline("...")` directive that pins the
|
|
1891
|
+
* grid-tone palette into the generated stylesheet.
|
|
1892
|
+
*
|
|
1893
|
+
* Why we need it: tone classes like `bg-primary/10` only appear in
|
|
1894
|
+
* `.astro` pages emitted by Dogsbay's grid-item serializer. When
|
|
1895
|
+
* Tailwind's content scanner doesn't pick them up — because the
|
|
1896
|
+
* page lives outside the default scan globs, or because a class
|
|
1897
|
+
* is composed at the boundary of an interpolation — they get
|
|
1898
|
+
* purged. Result observed in dogsbay-docs-markdown audit: half
|
|
1899
|
+
* the grid demo cells render with no background. Pinning forces
|
|
1900
|
+
* generation regardless of scanner reach.
|
|
1901
|
+
*
|
|
1902
|
+
* Single source of truth: derived from TONE_CLASSES so any new
|
|
1903
|
+
* tone added to the palette is automatically safelisted.
|
|
1904
|
+
*/
|
|
1905
|
+
function buildToneSafelist() {
|
|
1906
|
+
const seen = new Set();
|
|
1907
|
+
for (const classes of Object.values(TONE_CLASSES)) {
|
|
1908
|
+
for (const cls of classes.split(/\s+/)) {
|
|
1909
|
+
if (cls)
|
|
1910
|
+
seen.add(cls);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
return [...seen].sort().join(" ");
|
|
1914
|
+
}
|
|
1385
1915
|
function generateGlobalCss() {
|
|
1386
1916
|
return `@import "tailwindcss";
|
|
1387
1917
|
@import "./theme.css";
|
|
@@ -1390,6 +1920,14 @@ function generateGlobalCss() {
|
|
|
1390
1920
|
@source "../../node_modules/@dogsbay/ui/src";
|
|
1391
1921
|
@source "../../node_modules/@dogsbay/docs-layout/src";
|
|
1392
1922
|
|
|
1923
|
+
/* Pin the grid-tone palette. These classes are emitted into
|
|
1924
|
+
markdown-generated .astro pages by the grid-item serializer
|
|
1925
|
+
(TONE_CLASSES in @dogsbay/format-astro). Without inlining,
|
|
1926
|
+
opacity-modified utilities like bg-primary/10 get purged when
|
|
1927
|
+
Tailwind doesn't see them in the scanned globs, leaving grid
|
|
1928
|
+
demo cells with no visible background. */
|
|
1929
|
+
@source inline("${buildToneSafelist()}");
|
|
1930
|
+
|
|
1393
1931
|
/* Prose typography for rendered content */
|
|
1394
1932
|
.docs-prose {
|
|
1395
1933
|
line-height: 1.7;
|