dogsbay 0.2.0-beta.41 → 0.2.0-beta.43

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.
@@ -99,7 +99,15 @@ export async function migrateMkdocs(source, options) {
99
99
  // snippets, macros, autodoc, variants — see @dogsbay/format-
100
100
  // mkdocs's CLAUDE.md) runs identically to import-mkdocs; only
101
101
  // the final write target differs.
102
- const { pageCount, lossy } = await collectAndWriteContent(sourceDir, fullDocsDir, outputDir, config, { inlineAutodoc: options.inlineAutodoc ?? false });
102
+ // Lift the mkdocstrings Python handler options from mkdocs.yml.
103
+ // These become the project-level `autodoc.python` config block and
104
+ // also feed `--inline-autodoc`'s parser globalOptions — see
105
+ // plans/autodoc-mkdocstrings-options.md.
106
+ const autodocPythonOptions = extractAutodocOptions(config);
107
+ const { pageCount, lossy } = await collectAndWriteContent(sourceDir, fullDocsDir, outputDir, config, {
108
+ inlineAutodoc: options.inlineAutodoc ?? false,
109
+ autodocPythonOptions,
110
+ });
103
111
  console.log(pc.green(`Wrote`) + ` ${pageCount} pages to ./content/ as Dogsbay-MD`);
104
112
  // 3. Convert MkDocs nav: → ./content/nav.yml in canonical
105
113
  // single-key-map shape (- Label: file.md / nested children).
@@ -131,15 +139,28 @@ export async function migrateMkdocs(source, options) {
131
139
  // makes the migrated config self-documenting. NO `output:`
132
140
  // field — the default (./astro) is what we want; flat layout
133
141
  // (`output: "."`) triggers a rebuild loop in `site dev`.
134
- const siteUrl = config.site_url || undefined;
142
+ // site.url is NOT inherited from the source mkdocs.yml. A
143
+ // migration almost always moves to a new host, so copying the
144
+ // source's site_url silently bakes the wrong origin into
145
+ // robots.txt, sitemap, canonical tags, and llms.txt. The user
146
+ // supplies the destination explicitly with --site-url; absent,
147
+ // site.url is left unset and the build emits relative URLs.
148
+ // sourceSiteUrl is kept only to warn the user it was dropped.
149
+ const sourceSiteUrl = config.site_url || undefined;
150
+ const siteUrl = options.siteUrl || undefined;
135
151
  const repoUrl = config.repo_url || undefined;
152
+ // --base-path controls where docs sit under the host. Default
153
+ // /docs (matches every scaffolded dogsbay site); pass --base-path
154
+ // "" to serve at the host root — common for a dedicated repo or
155
+ // GitHub Pages project site.
156
+ const basePath = options.basePath !== undefined ? options.basePath : "/docs";
136
157
  const siteDescription = config.site_description || undefined;
137
158
  const dogsbayConfig = {
138
159
  schemaVersion: 1,
139
160
  site: {
140
161
  name: siteName,
141
162
  url: siteUrl,
142
- basePath: "/docs",
163
+ basePath,
143
164
  description: siteDescription,
144
165
  repoUrl,
145
166
  },
@@ -150,9 +171,26 @@ export async function migrateMkdocs(source, options) {
150
171
  llmsTxt: true,
151
172
  mdMirror: true,
152
173
  },
174
+ // Project-level autodoc render options lifted from the source
175
+ // mkdocs.yml's mkdocstrings handler. resolve-autodoc applies
176
+ // these under per-directive props so migrated reference pages
177
+ // render with the same signature/member-order behaviour the
178
+ // MkDocs site configured. Omitted when the source declared no
179
+ // renderer-relevant options.
180
+ ...(autodocPythonOptions
181
+ ? { autodoc: { python: autodocPythonOptions } }
182
+ : {}),
153
183
  };
154
184
  writeFileSync(join(outputDir, "dogsbay.config.yml"), serializeConfig(dogsbayConfig, "yaml"));
155
185
  console.log(pc.green(`Wrote`) + ` dogsbay.config.yml`);
186
+ if (sourceSiteUrl && !siteUrl) {
187
+ console.log(pc.yellow(`Note:`) +
188
+ ` source site_url (${sourceSiteUrl}) was not carried over —` +
189
+ ` a migration moves hosts.`);
190
+ console.log(` Set site.url in dogsbay.config.yml (or re-run with` +
191
+ ` --site-url) so robots.txt,`);
192
+ console.log(` sitemap, and llms.txt point at your host.`);
193
+ }
156
194
  // 6. Scaffold the Astro project under ./astro/ (theme,
157
195
  // package.json, astro.config.mjs, tsconfig, copied UI
158
196
  // components). Same emitter `dogsbay site init` uses, just
@@ -160,7 +198,7 @@ export async function migrateMkdocs(source, options) {
160
198
  emitSiteScaffold(astroDir, siteName, {
161
199
  siteName,
162
200
  siteUrl,
163
- basePath: "/docs",
201
+ basePath,
164
202
  repoUrl,
165
203
  llmsTxt: true,
166
204
  mdMirror: true,
@@ -187,6 +225,9 @@ export async function migrateMkdocs(source, options) {
187
225
  pageCount,
188
226
  assetCount,
189
227
  lossy,
228
+ siteUrl,
229
+ sourceSiteUrl,
230
+ basePath,
190
231
  }));
191
232
  console.log(pc.green(`Wrote`) + ` MIGRATION.md`);
192
233
  if (!options.quiet) {
@@ -280,8 +321,16 @@ async function collectAndWriteContent(sourceDir, fullDocsDir, outputDir, mkdocsC
280
321
  if (opts.inlineAutodoc && autodocSourceRootAbs) {
281
322
  // Snapshot mode: resolve at migration time. The resolved api-*
282
323
  // TreeNodes serialize to Dogsbay-MD `:::api-*` directives via
283
- // format-dogsbay-md's Phase 4a cases.
284
- parseOpts.autodoc = { sourceRoot: autodocSourceRootAbs };
324
+ // format-dogsbay-md's Phase 4a cases. globalOptions carries the
325
+ // mkdocstrings handler options so the snapshot matches what the
326
+ // MkDocs site configured (members_order, merge_init_into_class,
327
+ // …) rather than the bare mkdocstrings defaults.
328
+ parseOpts.autodoc = {
329
+ sourceRoot: autodocSourceRootAbs,
330
+ ...(opts.autodocPythonOptions
331
+ ? { globalOptions: opts.autodocPythonOptions }
332
+ : {}),
333
+ };
285
334
  }
286
335
  // Preserve mode (default): no `autodoc` in parseOpts. The `:::`
287
336
  // paragraphs stay as raw text; we walk them in post-process and
@@ -429,6 +478,60 @@ function extractMkdocstringsConfig(mkdocsConfig) {
429
478
  }
430
479
  return null;
431
480
  }
481
+ /**
482
+ * snake_case mkdocstrings handler option → camelCase Dogsbay
483
+ * `AutodocPythonOptions` key. Options the autodoc renderer doesn't
484
+ * understand (e.g. `extensions`, `preload_modules`,
485
+ * `docstring_section_style`) are intentionally absent — they fall
486
+ * through and are dropped. Mirrors the map in
487
+ * `format-mkdocs/src/importer.ts` so `migrate-mkdocs` and
488
+ * `import-mkdocs` honour the same handler options.
489
+ */
490
+ const MKDOCSTRINGS_OPTION_MAP = {
491
+ show_source: "showSource",
492
+ show_bases: "showBases",
493
+ show_signature: "showSignature",
494
+ unwrap_annotated: "unwrapAnnotated",
495
+ group_by_category: "groupByCategory",
496
+ show_root_full_path: "showRootFullPath",
497
+ show_root_heading: "showRootHeading",
498
+ show_if_no_docstring: "showIfNoDocstring",
499
+ inherited_members: "inheritedMembers",
500
+ merge_init_into_class: "mergeInitIntoClass",
501
+ separate_signature: "separateSignature",
502
+ members_order: "membersOrder",
503
+ filters: "filters",
504
+ heading_level: "headingLevel",
505
+ };
506
+ /**
507
+ * Lift the mkdocstrings Python handler `options:` block out of
508
+ * mkdocs.yml and map it to `AutodocPythonOptions`.
509
+ *
510
+ * A MkDocs site configures the handler once; `import-mkdocs` threads
511
+ * those options into the autodoc renderer as `globalOptions`. Without
512
+ * this, migrated `:::autodoc` directives fall back to the bare
513
+ * mkdocstrings defaults (`merge_init_into_class: false`,
514
+ * `members_order: alphabetical`) — the reference pages lose the
515
+ * expanded constructor signature and the document-order member list.
516
+ *
517
+ * Returns null when mkdocstrings isn't declared or sets no
518
+ * renderer-relevant options. See plans/autodoc-mkdocstrings-options.md.
519
+ */
520
+ function extractAutodocOptions(mkdocsConfig) {
521
+ const mkdocstrings = extractMkdocstringsConfig(mkdocsConfig);
522
+ if (!mkdocstrings)
523
+ return null;
524
+ const raw = mkdocstrings.handlers?.python?.options;
525
+ if (!raw || typeof raw !== "object")
526
+ return null;
527
+ const src = raw;
528
+ const out = {};
529
+ for (const [snake, camel] of Object.entries(MKDOCSTRINGS_OPTION_MAP)) {
530
+ if (src[snake] !== undefined)
531
+ out[camel] = src[snake];
532
+ }
533
+ return Object.keys(out).length > 0 ? out : null;
534
+ }
432
535
  /**
433
536
  * Extract macros plugin data file mappings from mkdocs.yml.
434
537
  * Returns { varName: absolutePath } for each `include_yaml` entry,
@@ -785,7 +888,7 @@ function labelFromPath(path) {
785
888
  }
786
889
  // ── MIGRATION.md ────────────────────────────────────────
787
890
  function buildMigrationNotes(args) {
788
- const { sourceDir, outputDir, siteName, pageCount, assetCount, lossy } = args;
891
+ const { sourceDir, outputDir, siteName, pageCount, assetCount, lossy, siteUrl, sourceSiteUrl, basePath, } = args;
789
892
  const lines = [
790
893
  `# Migration: ${siteName}`,
791
894
  "",
@@ -811,19 +914,22 @@ function buildMigrationNotes(args) {
811
914
  "npx dogsbay site dev # watch + preview",
812
915
  "```",
813
916
  "",
814
- "## Re-running the migration",
815
- "",
816
- "If you find a regression and we ship a fix, re-run with `--force`:",
817
- "",
818
- "```bash",
819
- `npx dogsbay migrate-mkdocs ${sourceDir} --output ${outputDir} --force`,
820
- "```",
821
- "",
822
- "Note that `--force` overwrites all generated files; any hand edits",
823
- "to `./content/*.md` since the last migration will be lost. Commit",
824
- "your work first.",
825
- "",
826
917
  ];
918
+ // Site URL section — the destination URL is not inherited from the
919
+ // source mkdocs.yml, so spell out what was written and what (if
920
+ // anything) was dropped.
921
+ lines.push("## Site URL", "");
922
+ if (siteUrl) {
923
+ lines.push(`\`site.url\` is set to \`${siteUrl}\` (from \`--site-url\`).`, `\`site.basePath\` is \`${basePath || '""'}\`. robots.txt, the`, "sitemap, canonical tags, and llms.txt all derive from these —", "edit `dogsbay.config.yml` and rebuild to change them.", "");
924
+ }
925
+ else {
926
+ lines.push("`site.url` was left **unset** — a migration moves hosts, so the", "source's `site_url` is intentionally not carried over. The build", "emits relative URLs until you set it.", "");
927
+ if (sourceSiteUrl) {
928
+ lines.push(`The source \`mkdocs.yml\` had \`site_url: ${sourceSiteUrl}\` —`, "that points at the *old* host and was dropped on purpose.", "");
929
+ }
930
+ lines.push("Set the destination URL in `dogsbay.config.yml`:", "", "```yaml", "site:", " url: https://you.github.io/your-repo # drives sitemap, llms.txt, canonical", ` basePath: ${basePath || '""'}`, "```", "", "Or re-run the migration with `--site-url` (and optionally", "`--base-path`).", "");
931
+ }
932
+ lines.push("## Re-running the migration", "", "If you find a regression and we ship a fix, re-run with `--force`:", "", "```bash", `npx dogsbay migrate-mkdocs ${sourceDir} --output ${outputDir} --force`, "```", "", "Note that `--force` overwrites all generated files; any hand edits", "to `./content/*.md` since the last migration will be lost. Commit", "your work first.", "");
827
933
  if (lossy.length > 0) {
828
934
  lines.push("## Known limitations", "");
829
935
  lines.push("The following files have content that didn't fully round-trip", "into Dogsbay-MD. Review each one and fix manually:", "");
@@ -176,6 +176,10 @@ export async function siteBuild(cwd, options) {
176
176
  const { resolveAutodocRefs } = await import("../resolve-autodoc.js");
177
177
  const autodocResult = await resolveAutodocRefs(pages, {
178
178
  configDir: dirname(configPath),
179
+ // Project-level autodoc render options (lifted from the source
180
+ // mkdocs.yml by migrate-mkdocs). Layered under per-directive
181
+ // props — see plans/autodoc-mkdocstrings-options.md.
182
+ pythonOptions: config.autodoc?.python,
179
183
  });
180
184
  if (autodocResult.resolved > 0 || autodocResult.failed > 0) {
181
185
  const parts = [];
@@ -94,6 +94,7 @@ function validate(parsed, sourcePath) {
94
94
  throw new Error(`analytics must be an object in ${sourcePath}`);
95
95
  }
96
96
  const agent = validateAgent(obj.agent, sourcePath);
97
+ const autodoc = validateAutodoc(obj.autodoc, sourcePath);
97
98
  const taxonomies = validateTaxonomies(obj.taxonomies, sourcePath);
98
99
  const propSchema = validatePropSchema(obj.propSchema, sourcePath);
99
100
  const plugins = validatePlugins(obj.plugins, sourcePath);
@@ -108,11 +109,66 @@ function validate(parsed, sourcePath) {
108
109
  deploy: obj.deploy,
109
110
  analytics: obj.analytics,
110
111
  agent,
112
+ autodoc,
111
113
  taxonomies,
112
114
  propSchema,
113
115
  plugins,
114
116
  };
115
117
  }
118
+ /**
119
+ * Validate the optional `autodoc:` block. One sub-block per language
120
+ * handler — only `python` exists today. Field names mirror
121
+ * `AutodocPythonOptions`; type-checked individually so a typo'd
122
+ * boolean doesn't silently flip a render default.
123
+ */
124
+ function validateAutodoc(raw, sourcePath) {
125
+ if (raw === undefined)
126
+ return undefined;
127
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
128
+ throw new Error(`autodoc must be an object in ${sourcePath}`);
129
+ }
130
+ const a = raw;
131
+ if (a.python === undefined)
132
+ return {};
133
+ if (typeof a.python !== "object" || a.python === null || Array.isArray(a.python)) {
134
+ throw new Error(`autodoc.python must be an object in ${sourcePath}`);
135
+ }
136
+ const p = a.python;
137
+ const booleanKeys = [
138
+ "mergeInitIntoClass",
139
+ "separateSignature",
140
+ "unwrapAnnotated",
141
+ "inheritedMembers",
142
+ "showIfNoDocstring",
143
+ "showRootHeading",
144
+ "showRootFullPath",
145
+ "showBases",
146
+ "showSource",
147
+ "showSignature",
148
+ "groupByCategory",
149
+ ];
150
+ for (const key of booleanKeys) {
151
+ if (p[key] !== undefined && typeof p[key] !== "boolean") {
152
+ throw new Error(`autodoc.python.${key} must be a boolean in ${sourcePath}`);
153
+ }
154
+ }
155
+ if (p.membersOrder !== undefined &&
156
+ p.membersOrder !== "source" &&
157
+ p.membersOrder !== "alphabetical") {
158
+ throw new Error(`autodoc.python.membersOrder must be "source" or "alphabetical" ` +
159
+ `in ${sourcePath}; got ${JSON.stringify(p.membersOrder)}`);
160
+ }
161
+ if (p.headingLevel !== undefined && typeof p.headingLevel !== "number") {
162
+ throw new Error(`autodoc.python.headingLevel must be a number in ${sourcePath}`);
163
+ }
164
+ if (p.filters !== undefined) {
165
+ if (!Array.isArray(p.filters) ||
166
+ p.filters.some((f) => typeof f !== "string")) {
167
+ throw new Error(`autodoc.python.filters must be an array of strings in ${sourcePath}`);
168
+ }
169
+ }
170
+ return { python: p };
171
+ }
116
172
  function validatePlugins(raw, sourcePath) {
117
173
  if (raw === undefined)
118
174
  return undefined;
package/dist/index.js CHANGED
@@ -184,6 +184,13 @@ program
184
184
  .argument("<source>", "Path to MkDocs project (containing mkdocs.yml)")
185
185
  .option("-o, --output <dir>", "Output directory (default: {source}-dogsbay)")
186
186
  .option("--force", "Overwrite an existing Dogsbay site at the output dir")
187
+ .option("--site-url <url>", "Canonical URL of the destination site (e.g. " +
188
+ "https://you.github.io/repo). Drives robots.txt, sitemap, " +
189
+ "canonical tags, and llms.txt. NOT inherited from the source " +
190
+ "mkdocs.yml — a migration moves hosts.")
191
+ .option("--base-path <path>", 'Path under the host where docs are served (default "/docs"). ' +
192
+ 'Pass --base-path "" to serve at the host root — common for a ' +
193
+ "dedicated repo / GitHub Pages project site.")
187
194
  .option("--inline-autodoc", "Resolve mkdocstrings ::: directives at migration time (snapshot). " +
188
195
  "Default is :::autodoc directives that site-build resolves on " +
189
196
  "every build (live re-rendering).")
@@ -75,6 +75,7 @@ export async function resolveAutodocRefs(pages, options) {
75
75
  for (const page of pages) {
76
76
  await walkAndResolve(page.tree, {
77
77
  configDir: options.configDir,
78
+ pythonOptions: options.pythonOptions,
78
79
  md,
79
80
  onResolved: () => {
80
81
  resolved++;
@@ -187,16 +188,21 @@ async function resolveOne(node, ctx) {
187
188
  // Resolve against configDir so the resolver doesn't depend on
188
189
  // process.cwd().
189
190
  const sourceRoot = resolvePath(ctx.configDir, sourceRootRaw);
190
- // Pull rendering options from props, layered on top of the
191
- // mkdocstrings-compatible defaults. tree-builder.ts uses
192
- // truthy-checks for several of these (e.g. `if (options.showSource
193
- // && symbol.sourceText) { ... }`) — without the defaults the
194
- // collapsible source-code block, attribute signatures, and class
195
- // bases all silently drop. The defaults mirror format-mkdocs/
196
- // src/loader.ts processAutodocDirectives so the migrated
197
- // output matches the legacy import-mkdocs output by default.
191
+ // Layer rendering options, lowest highest precedence:
192
+ // 1. AUTODOC_RENDER_DEFAULTS — bare mkdocstrings defaults.
193
+ // tree-builder.ts truthy-checks several of these, so without
194
+ // them the collapsible source block, attribute signatures,
195
+ // and class bases silently drop.
196
+ // 2. ctx.pythonOptions project-level `config.autodoc.python`,
197
+ // lifted by migrate-mkdocs from the source mkdocs.yml's
198
+ // mkdocstrings handler (members_order, merge_init_into_class,
199
+ // …). This is what makes a migrated reference page match the
200
+ // original instead of the bare-defaults render.
201
+ // 3. props — the per-directive `:::autodoc` YAML body, so an
202
+ // individual symbol can still override the project default.
198
203
  const renderOptions = {
199
204
  ...AUTODOC_RENDER_DEFAULTS,
205
+ ...(ctx.pythonOptions ?? {}),
200
206
  ...props,
201
207
  };
202
208
  delete renderOptions.ref;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dogsbay",
3
- "version": "0.2.0-beta.41",
3
+ "version": "0.2.0-beta.43",
4
4
  "description": "CLI for Dogsbay — scaffold, build, and serve documentation sites with markdown / MkDocs / Obsidian / OpenAPI sources",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,15 +32,15 @@
32
32
  "picocolors": "^1.1.0",
33
33
  "prompts": "^2.4.2",
34
34
  "yaml": "^2.8.3",
35
- "@dogsbay/autodoc-python": "0.2.0-beta.41",
36
- "@dogsbay/format-mkdocs": "0.2.0-beta.41",
37
- "@dogsbay/format-astro": "0.2.0-beta.41",
38
- "@dogsbay/format-obsidian": "0.2.0-beta.41",
39
- "@dogsbay/format-mdx": "0.2.0-beta.41",
40
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.41",
41
- "@dogsbay/format-starlight": "0.2.0-beta.41",
42
- "@dogsbay/format-openapi": "0.2.0-beta.41",
43
- "@dogsbay/types": "0.2.0-beta.41"
35
+ "@dogsbay/autodoc-python": "0.2.0-beta.43",
36
+ "@dogsbay/format-mkdocs": "0.2.0-beta.43",
37
+ "@dogsbay/format-astro": "0.2.0-beta.43",
38
+ "@dogsbay/format-obsidian": "0.2.0-beta.43",
39
+ "@dogsbay/format-mdx": "0.2.0-beta.43",
40
+ "@dogsbay/format-starlight": "0.2.0-beta.43",
41
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.43",
42
+ "@dogsbay/format-openapi": "0.2.0-beta.43",
43
+ "@dogsbay/types": "0.2.0-beta.43"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/markdown-it": "^14.1.0",
@@ -62,6 +62,39 @@ This was learned the hard way during Phase 3 of
62
62
  emitted `output: "."` and `dogsbay site dev` rebuild-looped on the
63
63
  result. The fix was to revert to the standard `output: "./astro"`.
64
64
 
65
+ ### Destination URL — supplied, not inherited
66
+
67
+ **Do NOT copy the source format's site URL into `site.url`.** A
68
+ migration almost always moves the docs to a *new* host — GitHub
69
+ Pages, a Cloudflare project, a custom domain. The source's
70
+ `site_url` (MkDocs), `url` (Jekyll `_config.yml`), `baseURL`
71
+ (Hugo), etc. points at the *old* host.
72
+
73
+ `site.url` feeds `robots.txt`, the sitemap XML, the `Sitemap:` /
74
+ `Llms-Txt:` lines, canonical tags, `llms.txt` absolute URLs, and
75
+ Astro's `site` / `base`. Inheriting the wrong origin silently
76
+ breaks every one of those — and the breakage only shows up after
77
+ a build, in generated files the user doesn't normally read.
78
+
79
+ **Required:**
80
+ - Migration commands take a `--site-url <url>` flag. Its value
81
+ goes to `dogsbay.config.yml`'s `site.url` *and* the scaffold
82
+ emitter's `siteUrl` option. The URL may carry a path component
83
+ (`https://you.github.io/repo`); `format-astro`'s `parseSiteUrl`
84
+ splits it into Astro `site` (origin) + `base` (urlBase) — this
85
+ is exactly how GitHub Pages **project** sites work.
86
+ - Take a `--base-path <path>` flag too. Default `/docs`; `""`
87
+ serves at the host root. Check `!== undefined` (not truthiness)
88
+ so the empty string is honoured.
89
+ - When `--site-url` is absent, leave `site.url` **unset** (the
90
+ build degrades cleanly to relative URLs) — do NOT fall back to
91
+ the source value.
92
+ - Keep the dropped source URL only to (a) print a one-line note
93
+ after writing the config and (b) record it in `MIGRATION.md`'s
94
+ "Site URL" section so the user knows what to set.
95
+ - The *code* repo URL (`repo_url` etc.) **is** inherited — that
96
+ doesn't change in a docs migration.
97
+
65
98
  ## 2. Asset folder (`content/_assets/`)
66
99
 
67
100
  All images, diagrams, screenshots, icons, PDFs, and downloadable
@@ -247,6 +280,7 @@ Every migration command MUST have a fixture test asserting all of:
247
280
  | `<output>/content/nav.json` does NOT exist | Avoid loader precedence trap |
248
281
  | `<output>/dogsbay.config.yml` has `sources: [{ path: ./content, from: dogsbay-md }]` | Config wires content to source |
249
282
  | `<output>/dogsbay.config.yml` does NOT set `output:` | Use default `./astro` |
283
+ | `site.url` reflects `--site-url`, NOT the source's URL; absent → unset | Destination URL not inherited |
250
284
  | `<output>/astro/package.json` exists | Scaffold under astro/ |
251
285
  | `<output>/astro/astro.config.mjs` exists | Scaffold under astro/ |
252
286
  | `<output>/astro/src/styles/theme.css` exists | Scaffold under astro/ |