@omniharmonic/openspace-cli 0.1.1 → 0.1.2

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/bin.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire as __osCreateRequire } from "node:module"; const require
3
3
  import {
4
4
  processIo,
5
5
  run
6
- } from "./chunk-LTJEM3DN.js";
6
+ } from "./chunk-R3SYHQ5J.js";
7
7
  import "./chunk-3F6TAUAT.js";
8
8
 
9
9
  // src/bin.ts
@@ -7435,6 +7435,223 @@ var CliError = class extends Error {
7435
7435
  import { readdir as readdir2, readFile as readFile2, stat } from "fs/promises";
7436
7436
  import { join as join2 } from "path";
7437
7437
 
7438
+ // ../schema/src/links.ts
7439
+ var COLLECTION_SUBPATHS = /* @__PURE__ */ new Set(["page", "category", "categories", "tag", "tags"]);
7440
+ var MARKDOWN_LINK_RE = /\]\((\/[^)\s]*)\)|href="(\/[^"\s]*)"/g;
7441
+ var trimSlashes = (s) => s.replace(/^\/+|\/+$/g, "");
7442
+ var LinkTargets = class {
7443
+ constructor(doc) {
7444
+ this.doc = doc;
7445
+ this.slugs = new Set(Object.values(doc.pages).map((p) => p.slug));
7446
+ this.redirectFroms = new Set((doc.site.redirects ?? []).map((r) => trimSlashes(r.from)));
7447
+ }
7448
+ doc;
7449
+ slugs;
7450
+ redirectFroms;
7451
+ hasCollection(name) {
7452
+ return name in this.doc.collections || name in (this.doc.collectionConfigs ?? {});
7453
+ }
7454
+ /** Does a site-internal path (`/about/`, `/journal/hello`) lead somewhere? */
7455
+ pathResolves(raw) {
7456
+ const path = raw.split(/[?#]/)[0] ?? "";
7457
+ const slug = trimSlashes(path);
7458
+ const last = slug.split("/").pop() ?? "";
7459
+ if (/\.[A-Za-z0-9]+$/.test(last)) return true;
7460
+ if (slug === "404" || slug.startsWith("_os/")) return true;
7461
+ if (this.slugs.has(slug) || this.redirectFroms.has(slug)) return true;
7462
+ const [head, second, ...rest] = slug.split("/");
7463
+ if (head && this.hasCollection(head)) {
7464
+ if (second === void 0) return true;
7465
+ if (this.doc.collections[head]?.[second]) return rest.length === 0;
7466
+ return COLLECTION_SUBPATHS.has(second);
7467
+ }
7468
+ return false;
7469
+ }
7470
+ };
7471
+ var isInternalPath = (s) => /^\/(?!\/)/.test(s);
7472
+ var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
7473
+ function findBrokenLinks(doc) {
7474
+ const targets = new LinkTargets(doc);
7475
+ const out = [];
7476
+ const walk5 = (value, scope, path, where) => {
7477
+ const add = (kind, target, message, severity = "error") => out.push({ scope, path, ...where, kind, target, severity, message });
7478
+ if (typeof value === "string") {
7479
+ if (path[path.length - 1] === "body")
7480
+ for (const m of value.matchAll(MARKDOWN_LINK_RE)) {
7481
+ const target = m[1] ?? m[2];
7482
+ if (target && !targets.pathResolves(target))
7483
+ add("markdown", target, `Link to ${target} does not lead to any page`);
7484
+ }
7485
+ return;
7486
+ }
7487
+ if (Array.isArray(value)) {
7488
+ for (const [i, v] of value.entries()) walk5(v, scope, [...path, i], where);
7489
+ return;
7490
+ }
7491
+ if (!isObject(value)) return;
7492
+ const o = value;
7493
+ if (o.type === "page" && typeof o.page === "string") {
7494
+ if (!(o.page in doc.pages))
7495
+ add("page", o.page, `Link to page ${o.page} but no such page exists`);
7496
+ } else if (o.type === "item" && typeof o.collection === "string" && typeof o.slug === "string") {
7497
+ if (!doc.collections[o.collection]?.[o.slug])
7498
+ add(
7499
+ "item",
7500
+ `${o.collection}/${o.slug}`,
7501
+ `Link to ${o.collection}/${o.slug} but no such item exists`
7502
+ );
7503
+ } else if (o.type === "collection" && typeof o.collection === "string") {
7504
+ if (!targets.hasCollection(o.collection))
7505
+ add(
7506
+ "collection",
7507
+ o.collection,
7508
+ `Navigation points at collection "${o.collection}" which does not exist`
7509
+ );
7510
+ } else if (o.type === "url" && typeof o.url === "string" && isInternalPath(o.url)) {
7511
+ if (!targets.pathResolves(o.url))
7512
+ add("url", o.url, `Link to ${o.url} does not lead to any page`);
7513
+ } else if (o.type === "link" && isObject(o.attrs) && typeof o.attrs.href === "string") {
7514
+ const href = o.attrs.href;
7515
+ if (isInternalPath(href) && !targets.pathResolves(href))
7516
+ add("richtext", href, `Text link to ${href} does not lead to any page`, "warning");
7517
+ }
7518
+ for (const [k, v] of Object.entries(o)) walk5(v, scope, [...path, k], where);
7519
+ };
7520
+ const walkSection = (section, scope, base, container2) => {
7521
+ for (const [k, v] of Object.entries(section))
7522
+ if (k !== "blocks") walk5(v, scope, [...base, k], { container: container2, section: section.id });
7523
+ section.blocks.forEach((b, j) => {
7524
+ const at = { container: container2, section: section.id, block: b.id };
7525
+ for (const [k, v] of Object.entries(b)) {
7526
+ if (k !== "fields") walk5(v, scope, [...base, "blocks", j, k], at);
7527
+ else if (isObject(v))
7528
+ for (const [field, fv] of Object.entries(v))
7529
+ walk5(fv, scope, [...base, "blocks", j, "fields", field], { ...at, field });
7530
+ }
7531
+ });
7532
+ };
7533
+ const site2 = { kind: "site" };
7534
+ for (const [k, v] of Object.entries(doc.site))
7535
+ if (k === "footer") walkSection(doc.site.footer, site2, ["footer"], "footer");
7536
+ else walk5(v, site2, [k], {});
7537
+ for (const page of Object.values(doc.pages)) {
7538
+ const scope = { kind: "page", page: page.id };
7539
+ for (const [k, v] of Object.entries(page)) if (k !== "sections") walk5(v, scope, [k], {});
7540
+ for (const [i, s] of page.sections.entries()) walkSection(s, scope, ["sections", i], page.id);
7541
+ }
7542
+ for (const [collection, items] of Object.entries(doc.collections))
7543
+ for (const item of Object.values(items))
7544
+ walk5(item, { kind: "item", collection, slug: item.slug }, [], {});
7545
+ return out;
7546
+ }
7547
+ var CONTACT_WORDS = [
7548
+ "contact",
7549
+ "book",
7550
+ "booking",
7551
+ "bookings",
7552
+ "reserve",
7553
+ "reservations",
7554
+ "enquire",
7555
+ "enquiry",
7556
+ "enquiries",
7557
+ "inquire",
7558
+ "inquiry",
7559
+ "get-in-touch",
7560
+ "hello",
7561
+ "appointments",
7562
+ "visit"
7563
+ ];
7564
+ var words = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2);
7565
+ var related = (a, b) => {
7566
+ if (a === b) return true;
7567
+ const [s, l] = a.length < b.length ? [a, b] : [b, a];
7568
+ return s.length >= 4 && l.startsWith(s) && l.length - s.length <= 3;
7569
+ };
7570
+ var publicPages = (doc) => Object.values(doc.pages).filter(
7571
+ (p) => !p.access?.password && !p.access?.members && p.slug !== "404"
7572
+ );
7573
+ var pagePath = (p) => p.slug ? `/${p.slug}` : "/";
7574
+ function suggestLinkTarget(doc, target) {
7575
+ const wanted = words(trimSlashes(target.split(/[?#]/)[0] ?? ""));
7576
+ const pages = publicPages(doc);
7577
+ const matches = (p, list3) => {
7578
+ const own = [...words(p.slug), ...words(p.title)];
7579
+ return own.some((w) => list3.some((x) => related(w, x)));
7580
+ };
7581
+ const bySlugWord = pages.find((p) => p.slug && wanted.some((w) => words(p.slug).includes(w)));
7582
+ if (bySlugWord) return { page: bySlugWord, path: pagePath(bySlugWord) };
7583
+ const byWord = pages.find((p) => p.slug && matches(p, wanted));
7584
+ if (byWord) return { page: byWord, path: pagePath(byWord) };
7585
+ const collection = Object.keys(doc.collections).find(
7586
+ (c) => wanted.some((w) => related(w, c.toLowerCase()))
7587
+ );
7588
+ if (collection) return { path: `/${collection}/` };
7589
+ const contact = CONTACT_WORDS.map(
7590
+ (w) => pages.find((p) => p.slug && (words(p.slug).includes(w) || words(p.title).includes(w)))
7591
+ ).find(Boolean);
7592
+ if (contact) return { page: contact, path: pagePath(contact) };
7593
+ const home = pages.find((p) => p.slug === "") ?? pages[0];
7594
+ return home ? { page: home, path: pagePath(home) } : void 0;
7595
+ }
7596
+ function suggestionLink(s, newTab) {
7597
+ if (s.page) return { type: "page", page: s.page.id };
7598
+ return { type: "url", url: s.path, ...newTab ? { newTab } : {} };
7599
+ }
7600
+ function resolveInternalLinks(value, doc) {
7601
+ const targets = new LinkTargets(doc);
7602
+ const changes = [];
7603
+ const cache = /* @__PURE__ */ new Map();
7604
+ const suggest = (target) => {
7605
+ if (!cache.has(target)) cache.set(target, suggestLinkTarget(doc, target));
7606
+ return cache.get(target);
7607
+ };
7608
+ const fix = (v, path) => {
7609
+ if (typeof v === "string") {
7610
+ if (path[path.length - 1] !== "body") return v;
7611
+ return v.replace(MARKDOWN_LINK_RE, (m, md, html) => {
7612
+ const target = md ?? html;
7613
+ if (!target || targets.pathResolves(target)) return m;
7614
+ const s = suggest(target);
7615
+ if (!s) return m;
7616
+ changes.push({ path, from: target, to: s.path });
7617
+ return md ? `](${s.path})` : `href="${s.path}"`;
7618
+ });
7619
+ }
7620
+ if (Array.isArray(v)) return v.map((x, i) => fix(x, [...path, i]));
7621
+ if (!isObject(v)) return v;
7622
+ if (v.type === "url" && typeof v.url === "string" && isInternalPath(v.url)) {
7623
+ if (targets.pathResolves(v.url)) return v;
7624
+ const s = suggest(v.url);
7625
+ if (!s) return v;
7626
+ changes.push({ path, from: v.url, to: s.path });
7627
+ return suggestionLink(s, v.newTab === true ? true : void 0);
7628
+ }
7629
+ if (v.type === "page" && typeof v.page === "string" && !(v.page in doc.pages)) {
7630
+ const home = suggest("/");
7631
+ if (!home?.page) return v;
7632
+ changes.push({ path, from: v.page, to: home.path });
7633
+ return { ...v, page: home.page.id };
7634
+ }
7635
+ if (v.type === "link" && isObject(v.attrs) && typeof v.attrs.href === "string") {
7636
+ const href = v.attrs.href;
7637
+ if (!isInternalPath(href) || targets.pathResolves(href)) return v;
7638
+ const s = suggest(href);
7639
+ if (!s) return v;
7640
+ changes.push({ path, from: href, to: s.path });
7641
+ return { ...v, attrs: { ...v.attrs, href: s.path } };
7642
+ }
7643
+ const out = {};
7644
+ for (const [k, x] of Object.entries(v)) out[k] = fix(x, [...path, k]);
7645
+ return out;
7646
+ };
7647
+ return { value: fix(value, []), changes };
7648
+ }
7649
+ function withPages(doc, pages) {
7650
+ const all = { ...doc.pages };
7651
+ for (const p of pages) all[p.id] = p;
7652
+ return { ...doc, pages: all };
7653
+ }
7654
+
7438
7655
  // ../schema/src/secrets.ts
7439
7656
  function shannonEntropy(value) {
7440
7657
  if (!value) return 0;
@@ -8151,39 +8368,6 @@ function fileDepth2(path) {
8151
8368
  }
8152
8369
  var IMAGE_EXT_RE = /\.(?:jpe?g|png|gif|webp|avif|svg)$/i;
8153
8370
  var MEDIA_REF_RE = /(?<![\w/.:-])media\/[A-Za-z0-9_\-./]+\.[A-Za-z0-9]+/g;
8154
- var MD_LINK_RE = /\]\((\/[^)\s]*)\)|href="(\/[^"\s]*)"/g;
8155
- var COLLECTION_SUBPATHS = /* @__PURE__ */ new Set(["page", "category", "categories", "tag", "tags"]);
8156
- var Resolver = class {
8157
- constructor(doc) {
8158
- this.doc = doc;
8159
- this.slugs = new Set(Object.values(doc.pages).map((p) => p.slug));
8160
- this.redirectFroms = new Set(
8161
- (doc.site.redirects ?? []).map((r) => r.from.replace(/^\/+|\/+$/g, ""))
8162
- );
8163
- }
8164
- doc;
8165
- slugs;
8166
- redirectFroms;
8167
- hasCollection(name) {
8168
- return name in this.doc.collections || name in (this.doc.collectionConfigs ?? {});
8169
- }
8170
- /** Does a site-internal path (`/about/`, `/journal/hello`) lead somewhere? */
8171
- pathResolves(raw) {
8172
- const path = raw.split(/[?#]/)[0] ?? "";
8173
- const slug = path.replace(/^\/+|\/+$/g, "");
8174
- const last = slug.split("/").pop() ?? "";
8175
- if (/\.[A-Za-z0-9]+$/.test(last)) return true;
8176
- if (slug === "404" || slug.startsWith("_os/")) return true;
8177
- if (this.slugs.has(slug) || this.redirectFroms.has(slug)) return true;
8178
- const [head, second, ...rest] = slug.split("/");
8179
- if (head && this.hasCollection(head)) {
8180
- if (second === void 0) return true;
8181
- if (this.doc.collections[head]?.[second]) return rest.length === 0;
8182
- return COLLECTION_SUBPATHS.has(second);
8183
- }
8184
- return false;
8185
- }
8186
- };
8187
8371
  async function runCheck(root, options = {}) {
8188
8372
  const budgets = { ...DEFAULT_BUDGETS, ...options.budgets };
8189
8373
  const findings = [];
@@ -8321,7 +8505,7 @@ async function runCheck(root, options = {}) {
8321
8505
  return { ok: errors2 === 0, errors: errors2, warnings, findings, stats: stats2 };
8322
8506
  }
8323
8507
  function checkReferences(doc, pageFiles2, mediaOnDisk, budgets, add) {
8324
- const resolver = new Resolver(doc);
8508
+ const resolver = new LinkTargets(doc);
8325
8509
  const reportedImages = /* @__PURE__ */ new Set();
8326
8510
  const checkImage = (ref, file, path) => {
8327
8511
  if (!isMediaPath(ref)) return;
@@ -8349,67 +8533,23 @@ function checkReferences(doc, pageFiles2, mediaOnDisk, budgets, add) {
8349
8533
  if (typeof value === "string") {
8350
8534
  const here = dotPath(path);
8351
8535
  for (const m of value.matchAll(MEDIA_REF_RE)) checkImage(m[0], file, here);
8352
- if (path[path.length - 1] === "body") {
8353
- for (const m of value.matchAll(MD_LINK_RE)) {
8354
- const target = m[1] ?? m[2];
8355
- if (target && !resolver.pathResolves(target))
8356
- add({
8357
- severity: "error",
8358
- rule: "link",
8359
- file,
8360
- path: here,
8361
- message: `Link to ${target} does not lead to any page`
8362
- });
8363
- }
8364
- }
8365
8536
  return;
8366
8537
  }
8367
8538
  if (Array.isArray(value)) {
8368
8539
  for (const [i, v] of value.entries()) walk5(v, file, [...path, i]);
8369
8540
  return;
8370
8541
  }
8371
- if (value && typeof value === "object") {
8372
- const o = value;
8373
- const here = dotPath(path);
8374
- if (o.type === "page" && typeof o.page === "string" && !(o.page in doc.pages))
8375
- add({
8376
- severity: "error",
8377
- rule: "link",
8378
- file,
8379
- path: here || void 0,
8380
- message: `Link to page ${o.page} but no such page exists`
8381
- });
8382
- else if (o.type === "item" && typeof o.collection === "string" && typeof o.slug === "string") {
8383
- if (!doc.collections[o.collection]?.[o.slug])
8384
- add({
8385
- severity: "error",
8386
- rule: "link",
8387
- file,
8388
- path: here || void 0,
8389
- message: `Link to ${o.collection}/${o.slug} but no such item exists`
8390
- });
8391
- } else if (o.type === "collection" && typeof o.collection === "string") {
8392
- if (!resolver.hasCollection(o.collection))
8393
- add({
8394
- severity: "error",
8395
- rule: "link",
8396
- file,
8397
- path: here || void 0,
8398
- message: `Navigation points at collection "${o.collection}" which does not exist`
8399
- });
8400
- } else if (o.type === "url" && typeof o.url === "string" && o.url.startsWith("/")) {
8401
- if (!resolver.pathResolves(o.url))
8402
- add({
8403
- severity: "error",
8404
- rule: "link",
8405
- file,
8406
- path: here || void 0,
8407
- message: `Link to ${o.url} does not lead to any page`
8408
- });
8409
- }
8410
- for (const [k, v] of Object.entries(o)) walk5(v, file, [...path, k]);
8411
- }
8542
+ if (value && typeof value === "object")
8543
+ for (const [k, v] of Object.entries(value)) walk5(v, file, [...path, k]);
8412
8544
  };
8545
+ for (const l of findBrokenLinks(doc))
8546
+ add({
8547
+ severity: l.severity,
8548
+ rule: "link",
8549
+ file: l.scope.kind === "site" ? SITE_FILE : l.scope.kind === "page" ? pageFiles2[l.scope.page] ?? PAGES_DIR : `${COLLECTIONS_DIR}/${l.scope.collection}/${l.scope.slug}${ITEM_EXT}`,
8550
+ path: dotPath(l.path) || void 0,
8551
+ message: l.message
8552
+ });
8413
8553
  walk5(doc.site, SITE_FILE, []);
8414
8554
  for (const page of Object.values(doc.pages)) walk5(page, pageFiles2[page.id] ?? PAGES_DIR, []);
8415
8555
  for (const [name, items] of Object.entries(doc.collections))
@@ -12472,6 +12612,104 @@ function sortIssues(issues) {
12472
12612
  return issues.map((i, n) => [i, n]).sort(([a, na], [b, nb]) => rank[a.severity] - rank[b.severity] || na - nb).map(([i]) => i);
12473
12613
  }
12474
12614
 
12615
+ // ../lint/src/links.ts
12616
+ var titleOf = (p) => p.title || (p.slug === "" ? "Home" : p.slug);
12617
+ function linkPlace(doc, link) {
12618
+ const s = link.scope;
12619
+ if (link.block) {
12620
+ const container2 = link.container === "footer" ? doc.site.footer : doc.pages[link.container]?.sections.find((x) => x.id === link.section);
12621
+ const block = container2?.blocks.find((b) => b.id === link.block);
12622
+ const label = block && blocksRegistry().get(block.type)?.meta.label || "block";
12623
+ const what = `A ${label.toLowerCase()}`;
12624
+ if (link.container === "footer") return `${what} in the footer`;
12625
+ const page = doc.pages[link.container];
12626
+ return page ? `${what} on ${titleOf(page)}` : what;
12627
+ }
12628
+ if (s.kind === "site") {
12629
+ const head = link.path[0];
12630
+ if (head === "nav") return "Your menu";
12631
+ if (head === "header") return "The header";
12632
+ if (head === "announcement") return "The announcement bar";
12633
+ if (head === "popup") return "The pop-up";
12634
+ return "Your site settings";
12635
+ }
12636
+ if (s.kind === "page") {
12637
+ const page = doc.pages[s.page];
12638
+ return page ? `The page ${titleOf(page)}` : "A page";
12639
+ }
12640
+ const item = doc.collections[s.collection]?.[s.slug];
12641
+ const title = item?.frontmatter?.title ?? s.slug;
12642
+ return `\u201C${title}\u201D in ${s.collection}`;
12643
+ }
12644
+ function describeBrokenLink(doc, link) {
12645
+ const where = linkPlace(doc, link);
12646
+ switch (link.kind) {
12647
+ case "page":
12648
+ return `${where} links to a page that no longer exists.`;
12649
+ case "item":
12650
+ return `${where} links to ${link.target}, which isn't on your site any more.`;
12651
+ case "collection":
12652
+ return `${where} points at the \u201C${link.target}\u201D collection, which doesn't exist.`;
12653
+ default:
12654
+ return `${where} links to ${link.target}, which isn't a page on your site.`;
12655
+ }
12656
+ }
12657
+ function linkFix(doc, link, meta) {
12658
+ const target = link.kind === "page" ? "/" : link.target;
12659
+ const s = suggestLinkTarget(doc, target);
12660
+ if (!s) return void 0;
12661
+ const label = `Link to ${s.page ? titleOf(s.page) : s.path} instead`;
12662
+ if (link.block && link.field) {
12663
+ const container2 = link.container === "footer" ? doc.site.footer : doc.pages[link.container]?.sections.find((x) => x.id === link.section);
12664
+ const block = container2?.blocks.find((b) => b.id === link.block);
12665
+ if (!block) return void 0;
12666
+ const { value, changes } = resolveInternalLinks(block.fields[link.field], doc);
12667
+ if (!changes.length) return void 0;
12668
+ return {
12669
+ label,
12670
+ ops: [
12671
+ {
12672
+ op: "block.update",
12673
+ ...meta(),
12674
+ page: link.container,
12675
+ block: block.id,
12676
+ set: { [link.field]: value }
12677
+ }
12678
+ ]
12679
+ };
12680
+ }
12681
+ if (link.scope.kind === "site" && link.path[0] === "nav") {
12682
+ const { value, changes } = resolveInternalLinks(doc.site.nav, doc);
12683
+ if (!changes.length) return void 0;
12684
+ return { label, ops: [{ op: "site.update", ...meta(), set: { nav: value } }] };
12685
+ }
12686
+ return void 0;
12687
+ }
12688
+ function linkIssues(doc, pageId, options = {}, meta = opMeta(options)) {
12689
+ const out = [];
12690
+ for (const link of findBrokenLinks(doc)) {
12691
+ const s = link.scope;
12692
+ if (pageId && s.kind === "page" && s.page !== pageId) continue;
12693
+ if (pageId && s.kind === "item") continue;
12694
+ const page = link.container ?? (s.kind === "page" ? s.page : void 0);
12695
+ const autofix = linkFix(doc, link, meta);
12696
+ out.push(
12697
+ issue({
12698
+ rule: "broken-link",
12699
+ severity: link.severity,
12700
+ ...page ? { page } : {},
12701
+ ...link.section ? { section: link.section } : {},
12702
+ ...link.block ? { block: link.block } : {},
12703
+ key: `${s.kind === "item" ? `${s.collection}/${s.slug}:` : ""}${link.path.join(".")}`,
12704
+ message: describeBrokenLink(doc, link),
12705
+ fix: link.severity === "error" ? "Point the link at a page that exists, or add the missing page. The site can't be published until then." : "Point the link at a page that exists, or add the missing page.",
12706
+ ...autofix ? { autofix } : {}
12707
+ })
12708
+ );
12709
+ }
12710
+ return out;
12711
+ }
12712
+
12475
12713
  // ../lint/src/mobile.ts
12476
12714
  var ROW_UNIT = { d: 24, m: 20 };
12477
12715
  var MOBILE_WIDTH = 350;
@@ -12733,6 +12971,7 @@ function designCheck(doc, pageId, options = {}) {
12733
12971
  if (pageId && pages.length === 0) throw new Error(`Page ${pageId} not found`);
12734
12972
  for (const page of pages) ctx.page(page);
12735
12973
  ctx.container(FOOTER, [doc.site.footer], { footer: true });
12974
+ ctx.issues.push(...linkIssues(doc, pageId, options, ctx.meta));
12736
12975
  return sortIssues(ctx.issues);
12737
12976
  }
12738
12977
  var LIGHT = /* @__PURE__ */ new Set(["light", "light-accent"]);
@@ -13530,12 +13769,12 @@ function seoCheck(page, doc, options = {}) {
13530
13769
  } : {}
13531
13770
  });
13532
13771
  }
13533
- const words = blocks.map(blockText).join(" ").split(/\s+/).filter(Boolean).length;
13534
- if (!page.seo?.noindex && words < 80) {
13772
+ const words2 = blocks.map(blockText).join(" ").split(/\s+/).filter(Boolean).length;
13773
+ if (!page.seo?.noindex && words2 < 80) {
13535
13774
  add({
13536
13775
  rule: "seo-thin-content",
13537
13776
  severity: "info",
13538
- message: `The page has about ${words} words of text; search engines have little to go on.`,
13777
+ message: `The page has about ${words2} words of text; search engines have little to go on.`,
13539
13778
  fix: "Add a short paragraph that explains what's on the page in your own words."
13540
13779
  });
13541
13780
  }
@@ -14237,10 +14476,10 @@ var listSectionPresets = defineTool({
14237
14476
  let list3 = presetCatalog.slice();
14238
14477
  if (input.category) list3 = list3.filter((p) => p.category === input.category);
14239
14478
  if (input.query) {
14240
- const words = input.query.toLowerCase().split(/\s+/).filter(Boolean);
14479
+ const words2 = input.query.toLowerCase().split(/\s+/).filter(Boolean);
14241
14480
  list3 = list3.filter((p) => {
14242
14481
  const hay = `${p.id} ${p.label} ${p.description} ${p.tags.join(" ")} ${p.ai.whenToUse}`.toLowerCase();
14243
- return words.every((w) => hay.includes(w));
14482
+ return words2.every((w) => hay.includes(w));
14244
14483
  });
14245
14484
  }
14246
14485
  const full = input.detail === "full";
@@ -14882,14 +15121,23 @@ var insertSection = defineTool({
14882
15121
  if (input.preset) {
14883
15122
  const preset = findPreset(input.preset);
14884
15123
  const opts = { colorTheme: input.colorTheme, collection: input.collection, ...links };
15124
+ const linkNote = (changes2) => changes2.length ? {
15125
+ linksChanged: changes2.map((c) => ({ from: c.from, to: c.to })),
15126
+ linkNote: "Example links that lead nowhere on this site were pointed at existing pages; change them with update_block."
15127
+ } : {};
14885
15128
  if (preset.target === "header") {
14886
- const header = preset.build(opts);
15129
+ const { value: header, changes: changes2 } = resolveInternalLinks(preset.build(opts), doc);
14887
15130
  await ctx.apply(siteId, [{ op: "site.update", set: { header } }]);
14888
15131
  return {
14889
- data: { applied: preset.id, header, note: "The site header now uses this preset." }
15132
+ data: {
15133
+ applied: preset.id,
15134
+ header,
15135
+ note: "The site header now uses this preset.",
15136
+ ...linkNote(changes2)
15137
+ }
14890
15138
  };
14891
15139
  }
14892
- const section2 = preset.build(opts);
15140
+ const { value: section2, changes } = resolveInternalLinks(preset.build(opts), doc);
14893
15141
  if (input.label) section2.label = input.label;
14894
15142
  if (preset.target === "footer") {
14895
15143
  await ctx.apply(siteId, [{ op: "site.update", set: { footer: section2 } }]);
@@ -14897,7 +15145,8 @@ var insertSection = defineTool({
14897
15145
  data: {
14898
15146
  replaced: "footer",
14899
15147
  section: sectionSummary(ctx.registry, section2),
14900
- note: 'Edit footer blocks with page "footer".'
15148
+ note: 'Edit footer blocks with page "footer".',
15149
+ ...linkNote(changes)
14901
15150
  }
14902
15151
  };
14903
15152
  }
@@ -14912,7 +15161,8 @@ var insertSection = defineTool({
14912
15161
  page: page2,
14913
15162
  section: sectionSummary(ctx.registry, section2),
14914
15163
  copyGuidance: preset.copy.map((c) => `${c.label}: ${c.words[0]}\u2013${c.words[1]} words`),
14915
- next: "Replace the demo copy with the owner's content (update_block), then run design_check."
15164
+ next: "Replace the demo copy with the owner's content (update_block), then run design_check.",
15165
+ ...linkNote(changes)
14916
15166
  }
14917
15167
  };
14918
15168
  }
@@ -15017,7 +15267,10 @@ var insertBlock = defineTool({
15017
15267
  const doc = await ctx.doc(siteId);
15018
15268
  const page = container(doc, input.page);
15019
15269
  requireSection2(doc, page, input.section);
15020
- const block = makeBlock(ctx.registry, input.block);
15270
+ const made = makeBlock(ctx.registry, input.block);
15271
+ const block = resolveInternalLinks(made, doc).value;
15272
+ for (const k of Object.keys(input.block.fields ?? {}))
15273
+ if (k in made.fields) block.fields[k] = made.fields[k];
15021
15274
  const body = {
15022
15275
  op: "block.insert",
15023
15276
  page,
@@ -15263,7 +15516,6 @@ var createPage = defineTool({
15263
15516
  const colorTheme = input.themes?.[i];
15264
15517
  sections.push(preset.build(colorTheme ? { colorTheme } : {}));
15265
15518
  });
15266
- for (const raw of input.sections ?? []) sections.push(makeSection(ctx.registry, raw));
15267
15519
  const page = {
15268
15520
  id: newId("pg"),
15269
15521
  title: input.title,
@@ -15271,6 +15523,9 @@ var createPage = defineTool({
15271
15523
  seo: input.description ? { description: input.description } : {},
15272
15524
  sections
15273
15525
  };
15526
+ const linked = resolveInternalLinks(sections, withPages(doc, [page]));
15527
+ page.sections = linked.value;
15528
+ for (const raw of input.sections ?? []) page.sections.push(makeSection(ctx.registry, raw));
15274
15529
  if (input.hidden) page.hidden = true;
15275
15530
  const body = { op: "page.create", page };
15276
15531
  if (input.addToNav !== false && !input.hidden)
@@ -15279,8 +15534,12 @@ var createPage = defineTool({
15279
15534
  return {
15280
15535
  data: {
15281
15536
  page: { id: page.id, title: page.title, slug: page.slug, path: pagePathOf(page) },
15282
- sections: sections.map((s) => sectionSummary(ctx.registry, s)),
15283
- inNav: Boolean(body.nav)
15537
+ sections: page.sections.map((s) => sectionSummary(ctx.registry, s)),
15538
+ inNav: Boolean(body.nav),
15539
+ ...linked.changes.length ? {
15540
+ linksChanged: linked.changes.map((c) => ({ from: c.from, to: c.to })),
15541
+ linkNote: "Example links that lead nowhere on this site were pointed at existing pages; change them with update_block."
15542
+ } : {}
15284
15543
  }
15285
15544
  };
15286
15545
  }
@@ -16589,10 +16848,10 @@ Unpublished work saved as ${tag}`,
16589
16848
  });
16590
16849
  return this.preview;
16591
16850
  }
16592
- async previewUrl(siteId, pagePath = "/") {
16851
+ async previewUrl(siteId, pagePath2 = "/") {
16593
16852
  this.check(siteId);
16594
16853
  const { url } = await this.previewServer();
16595
- return `${url}${pagePath.startsWith("/") ? pagePath : `/${pagePath}`}`;
16854
+ return `${url}${pagePath2.startsWith("/") ? pagePath2 : `/${pagePath2}`}`;
16596
16855
  }
16597
16856
  async uploadImage(siteId, input, ctx) {
16598
16857
  this.check(siteId);
@@ -17158,4 +17417,4 @@ export {
17158
17417
  createProgram,
17159
17418
  run
17160
17419
  };
17161
- //# sourceMappingURL=chunk-LTJEM3DN.js.map
17420
+ //# sourceMappingURL=chunk-R3SYHQ5J.js.map