@cmo-so/cli 0.12.13 → 0.12.15

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.
Files changed (2) hide show
  1. package/dist/cmo.mjs +89 -18
  2. package/package.json +1 -1
package/dist/cmo.mjs CHANGED
@@ -15205,6 +15205,47 @@ async function partSet(site, slug, change) {
15205
15205
  counts.social = (counts.social ?? 0) + links2.length;
15206
15206
  }
15207
15207
  }
15208
+ if (change.footer_links && slug === "footer") {
15209
+ const isRow = (b) => String(b.attrs.className ?? "").split(/\s+/).includes("maggie-footer-links");
15210
+ const items3 = change.footer_links.filter((l) => l?.label && l?.url).slice(0, 20).map(
15211
+ (l) => `<!-- wp:paragraph {"fontSize":"small"} -->
15212
+ <p class="has-small-font-size"><a href="${escAttr(l.url)}">${escAttr(l.label)}</a></p>
15213
+ <!-- /wp:paragraph -->`
15214
+ ).join("\n\n");
15215
+ const row = items3 ? blockOf(
15216
+ `<!-- wp:group {"className":"maggie-footer-links","style":{"spacing":{"margin":{"bottom":"var:preset|spacing|40"}}},"layout":{"type":"flex","flexWrap":"wrap"}} -->
15217
+ <div class="wp-block-group maggie-footer-links" style="margin-bottom:var(--wp--preset--spacing--40)">${items3}</div>
15218
+ <!-- /wp:group -->`
15219
+ ) : null;
15220
+ const old = find(blocks, isRow);
15221
+ if (old) {
15222
+ const top = blocks.indexOf(old);
15223
+ if (top >= 0) blocks.splice(top, 1, ...row ? [row] : []);
15224
+ else {
15225
+ const parent = find(blocks, (b) => b.inner.includes(old));
15226
+ const k = parent.inner.indexOf(old);
15227
+ if (row) parent.inner[k] = row;
15228
+ else {
15229
+ parent.inner.splice(k, 1);
15230
+ let seen = -1;
15231
+ const at = parent.content.findIndex((c) => c === null && ++seen === k);
15232
+ parent.content.splice(at, parent.content[at + 1] === "\n\n" ? 2 : 1);
15233
+ }
15234
+ touchPath(blocks, parent);
15235
+ }
15236
+ } else if (row) {
15237
+ const outer = blocks.find((b) => b.name === "core/group");
15238
+ if (outer?.inner.length) {
15239
+ const k = outer.inner.length - 1;
15240
+ outer.inner.splice(k, 0, row);
15241
+ let seen = -1;
15242
+ const at = outer.content.findIndex((c) => c === null && ++seen === k);
15243
+ outer.content.splice(at, 0, null, "\n\n");
15244
+ touchPath(blocks, outer);
15245
+ } else blocks.push(row);
15246
+ } else throw new Error("footer_links: give at least one { label, url }.");
15247
+ counts.footer_links = change.footer_links.length;
15248
+ }
15208
15249
  const content = serialize(blocks);
15209
15250
  if (content === p.content) return { slug, changed: false, counts };
15210
15251
  await call(site, { method: "POST", path: `/wp/v2/template-parts/${p.id}`, body: { content } });
@@ -17433,6 +17474,11 @@ async function linksPlan(site, o = {}) {
17433
17474
  );
17434
17475
  const footer = await partGet(site, "footer").catch(() => null);
17435
17476
  const footerPaths = footer ? contentLinks(footer.content, base) : /* @__PURE__ */ new Set();
17477
+ const rowHtml = /maggie-footer-links[^>]*>([\s\S]*?)<\/div>\s*<!-- \/wp:group -->/.exec(footer?.content ?? "")?.[1] ?? "";
17478
+ const footerRow = [...rowHtml.matchAll(/<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g)].map((m) => ({
17479
+ url: m[1],
17480
+ label: strip(m[2])
17481
+ }));
17436
17482
  const header = await partGet(site, "header").catch(() => null);
17437
17483
  const headerPaths = header ? contentLinks(header.content, base) : /* @__PURE__ */ new Set();
17438
17484
  const chrome = /* @__PURE__ */ new Set([...menuPaths, ...footerPaths, ...headerPaths]);
@@ -17495,14 +17541,16 @@ async function linksPlan(site, o = {}) {
17495
17541
  type: "menu",
17496
17542
  why: "Main pages every visitor should reach in one click.",
17497
17543
  add: wantMenu.slice(0, 6).map((d) => ({ label: d.title, url: d.url })),
17498
- tool: "wp_menu_set"
17544
+ tool: "wp_menu_set",
17545
+ args: { add: wantMenu.slice(0, 6).map((d) => ({ label: d.title, url: d.path })) }
17499
17546
  });
17500
17547
  if (postDocs.length && blogPage && !chrome.has(blogPage.path))
17501
17548
  actions.push({
17502
17549
  type: "menu",
17503
17550
  why: "The blog isn't reachable from the menu.",
17504
17551
  add: [{ label: blogPage.title || "Blog", url: blogPage.url }],
17505
- tool: "wp_menu_set"
17552
+ tool: "wp_menu_set",
17553
+ args: { add: [{ label: blogPage.title || "Blog", url: blogPage.path }] }
17506
17554
  });
17507
17555
  const legal = pageDocs.filter(
17508
17556
  (d) => /privacy|terms|cookie|legal|impressum|datenschutz|accessib/i.test(`${d.path} ${d.title}`)
@@ -17514,7 +17562,14 @@ async function linksPlan(site, o = {}) {
17514
17562
  type: "footer",
17515
17563
  why: "Legal, contact and about pages belong in the footer of every page (trust, and Google's quality raters look for them).",
17516
17564
  add: footerMissing.slice(0, 8).map((d) => ({ label: d.title, url: d.url })),
17517
- tool: "wp_footer_set"
17565
+ tool: "wp_footer_set",
17566
+ // The footer's own row (if this tool made one) plus the missing pages, ready for wp_footer_set.
17567
+ args: {
17568
+ footer_links: [
17569
+ ...footerRow,
17570
+ ...footerMissing.slice(0, 8).filter((d) => !footerRow.some((r) => sitePath(r.url, base) === d.path)).map((d) => ({ label: d.title, url: d.path }))
17571
+ ]
17572
+ }
17518
17573
  });
17519
17574
  for (const hub of hubs) {
17520
17575
  const kids = pageDocs.filter((c) => c.parent === hub.id);
@@ -17637,8 +17692,8 @@ function relatedBlock(heading2, links2) {
17637
17692
  const items3 = links2.map((l) => `<!-- wp:list-item -->
17638
17693
  <li><a href="${esc(l.url)}">${esc(l.label)}</a></li>
17639
17694
  <!-- /wp:list-item -->`).join("\n");
17640
- return `<!-- wp:group {"metadata":{"name":"Related links","maggie":{"type":"related-links","version":1}},"align":"wide","className":"maggie-related-links","layout":{"type":"constrained"}} -->
17641
- <div class="wp-block-group alignwide maggie-related-links"><!-- wp:heading {"level":2} -->
17695
+ return `<!-- wp:group {"metadata":{"name":"Related links","maggie":{"type":"related-links","version":1}},"align":"wide","className":"maggie-related-links","style":{"spacing":{"padding":{"top":"var:preset|spacing|60","bottom":"var:preset|spacing|60"}}},"layout":{"type":"constrained","contentSize":"var(--wp--style--global--wide-size)"}} -->
17696
+ <div class="wp-block-group alignwide maggie-related-links" style="padding-top:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--60)"><!-- wp:heading {"level":2} -->
17642
17697
  <h2 class="wp-block-heading">${esc(heading2)}</h2>
17643
17698
  <!-- /wp:heading -->
17644
17699
 
@@ -17656,7 +17711,7 @@ async function llmsTxt(site, o = {}) {
17656
17711
  const base = (site.url ?? "").replace(/\/+$/, "");
17657
17712
  const s = await site.request({
17658
17713
  path: "/wp/v2/settings",
17659
- query: { _fields: "title,description,page_on_front,page_for_posts" }
17714
+ query: { _fields: "title,description,page_on_front,page_for_posts,blog_public" }
17660
17715
  }).then((r) => r.status === 200 ? r.data : {});
17661
17716
  const pages = await listPages(site, { status: "publish" });
17662
17717
  const posts2 = await all(
@@ -17665,10 +17720,20 @@ async function llmsTxt(site, o = {}) {
17665
17720
  { status: "publish", per_page: 100, _fields: "link,title,excerpt,meta", orderby: "date", order: "desc" },
17666
17721
  o.posts ?? 25
17667
17722
  );
17668
- const desc = (m, fallback = "") => String(m?.maggie_seo_description || fallback).replace(/\s+/g, " ").trim();
17723
+ const clip3 = (t, n2 = 200) => {
17724
+ const x = t.replace(/\s+/g, " ").trim();
17725
+ if (x.length <= n2) return x;
17726
+ const cut = x.slice(0, n2);
17727
+ return `${cut.slice(0, Math.max(cut.lastIndexOf(" "), n2 - 30)).replace(/[\s,;:–-]+$/, "")}\u2026`;
17728
+ };
17729
+ const desc = (m, fallback = "") => clip3(String(m?.maggie_seo_description || fallback));
17730
+ const isSample = (link, title) => /\/(sample-page|hello-world)\/?$/.test(link) || title === "Sample Page" || title === "Hello world!";
17669
17731
  const home = pages.find((p) => p.id === s.page_on_front);
17670
17732
  const noise = /privacy|terms|cookie|thank|cart|checkout|my-account|login/i;
17671
- const visible = pages.filter((p) => p.id !== s.page_on_front && p.id !== s.page_for_posts && !noise.test(p.link));
17733
+ const visible = pages.filter(
17734
+ (p) => p.id !== s.page_on_front && p.id !== s.page_for_posts && !noise.test(p.link) && !isSample(p.link, titleOf(p.title))
17735
+ );
17736
+ const realPosts = posts2.filter((p) => !isSample(p.link, strip(p.title.rendered ?? "")));
17672
17737
  const byParent = /* @__PURE__ */ new Map();
17673
17738
  for (const p of visible) byParent.set(p.parent, [...byParent.get(p.parent) ?? [], p]);
17674
17739
  const line = (title, url, d) => `- [${title.replace(/[[\]]/g, "")}](${url})${d ? `: ${d}` : ""}`;
@@ -17676,25 +17741,27 @@ async function llmsTxt(site, o = {}) {
17676
17741
  const summary = desc(home?.meta, s.description ?? "");
17677
17742
  if (summary) out.push(`> ${summary}`, "");
17678
17743
  const top = byParent.get(0) ?? [];
17679
- if (top.length) {
17744
+ if (top.length || home) {
17680
17745
  out.push("## Pages", "");
17746
+ if (home) out.push(line(titleOf(home.title) || "Home", home.link, summary));
17681
17747
  for (const p of top) {
17682
17748
  out.push(line(titleOf(p.title), p.link, desc(p.meta)));
17683
17749
  for (const c of byParent.get(p.id) ?? []) out.push(` ${line(titleOf(c.title), c.link, desc(c.meta))}`);
17684
17750
  }
17685
17751
  out.push("");
17686
17752
  }
17687
- if (posts2.length) {
17753
+ if (realPosts.length) {
17688
17754
  out.push("## Articles", "");
17689
- for (const p of posts2)
17690
- out.push(
17691
- line(strip(p.title.rendered ?? ""), p.link, desc(p.meta, strip(p.excerpt?.rendered ?? "").slice(0, 160)))
17692
- );
17755
+ for (const p of realPosts)
17756
+ out.push(line(strip(p.title.rendered ?? ""), p.link, desc(p.meta, strip(p.excerpt?.rendered ?? ""))));
17693
17757
  out.push("");
17694
17758
  }
17695
17759
  const legal = pages.filter((p) => /privacy|terms/i.test(p.link));
17696
- out.push("## Optional", "", line("Sitemap", `${base}/wp-sitemap.xml`, "every page and article"));
17697
- for (const p of legal) out.push(line(titleOf(p.title), p.link, ""));
17760
+ const optional2 = [
17761
+ ...s.blog_public === 0 ? [] : [line("Sitemap", `${base}/wp-sitemap.xml`, "every page and article")],
17762
+ ...legal.map((p) => line(titleOf(p.title), p.link, ""))
17763
+ ];
17764
+ if (optional2.length) out.push("## Optional", "", ...optional2);
17698
17765
  return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trim()}
17699
17766
  `;
17700
17767
  }
@@ -19589,12 +19656,16 @@ var elementTools = [
19589
19656
  }),
19590
19657
  tool({
19591
19658
  name: "wp_footer_set",
19592
- description: "Change the footer: any text (address, hours, copyright), link or picture, social profile links by service; reset goes back to the theme's own footer.",
19659
+ description: "Change the footer: any text (address, hours, copyright), link or picture, social profile links by service, the row of page links (footer_links); reset goes back to the theme's own footer.",
19593
19660
  parameters: {
19594
19661
  type: "object",
19595
19662
  properties: {
19596
19663
  ...EDITS.properties ?? {},
19597
19664
  social: obj('Social links by service: { "facebook": "https://\u2026", "instagram": "" (remove) }.'),
19665
+ footer_links: arr(
19666
+ "The footer's row of page links, in order (replaces the row set before; [] removes it): legal, contact, about, main pages.",
19667
+ obj("", { label: str2(""), url: str2("Relative for own pages: /contact/.") })
19668
+ ),
19598
19669
  reset: bool("Drop the site's changes: back to the theme's footer.")
19599
19670
  }
19600
19671
  },
@@ -23888,7 +23959,7 @@ var program = new Command();
23888
23959
  // package.json
23889
23960
  var package_default = {
23890
23961
  name: "@cmo-so/cli",
23891
- version: "0.12.13",
23962
+ version: "0.12.15",
23892
23963
  description: "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
23893
23964
  type: "module",
23894
23965
  license: "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/cli",
3
- "version": "0.12.13",
3
+ "version": "0.12.15",
4
4
  "description": "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
5
5
  "type": "module",
6
6
  "license": "MIT",