@cmo-so/cli 0.12.12 → 0.12.14
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/cmo.mjs +78 -22
- package/package.json +1 -1
package/dist/cmo.mjs
CHANGED
|
@@ -17013,7 +17013,29 @@ async function techAudit(site, o = {}) {
|
|
|
17013
17013
|
const add = (f) => findings.push(f);
|
|
17014
17014
|
const robotsR = await get(`${base}/robots.txt`, "follow");
|
|
17015
17015
|
const robots = robotsR?.status === 200 ? robotsR.text : "";
|
|
17016
|
-
const {
|
|
17016
|
+
const visible = await site.request({ path: "/wp/v2/settings", query: { _fields: "blog_public" } }).then((r) => r.status === 200 && r.data.blog_public != null ? !!Number(r.data.blog_public) : null).catch(() => null);
|
|
17017
|
+
const engagedOff = visible === false;
|
|
17018
|
+
let { urls, sitemap } = await sitemapUrls(base, robots);
|
|
17019
|
+
let discovered = "sitemap";
|
|
17020
|
+
if (!urls.length) {
|
|
17021
|
+
const rows = await Promise.all(
|
|
17022
|
+
["/wp/v2/pages", "/wp/v2/posts"].map(
|
|
17023
|
+
(path3) => all(site, path3, { status: "publish", _fields: "link", per_page: 100 }, limit).catch(() => [])
|
|
17024
|
+
)
|
|
17025
|
+
);
|
|
17026
|
+
urls = [...new Set(rows.flat().map((r) => r.link))];
|
|
17027
|
+
discovered = "wordpress";
|
|
17028
|
+
}
|
|
17029
|
+
if (engagedOff)
|
|
17030
|
+
add({
|
|
17031
|
+
id: "search-engines-off",
|
|
17032
|
+
priority: "p1",
|
|
17033
|
+
area: "settings",
|
|
17034
|
+
title: "Search engines are switched off for this site",
|
|
17035
|
+
why: "WordPress tells every search engine not to index it (noindex on every page, no sitemap). Right for a test or unfinished site; check MEMORY.md before changing it.",
|
|
17036
|
+
fix: "When the site is ready: wp_settings_set search_engines true.",
|
|
17037
|
+
tool: { name: "wp_settings_set", args: { search_engines: true } }
|
|
17038
|
+
});
|
|
17017
17039
|
if (!robotsR || robotsR.status !== 200)
|
|
17018
17040
|
add({
|
|
17019
17041
|
id: "robots-missing",
|
|
@@ -17033,7 +17055,7 @@ async function techAudit(site, o = {}) {
|
|
|
17033
17055
|
fix: "Turn search engines on (wp_settings_set search_engines true) or fix the robots.txt file.",
|
|
17034
17056
|
tool: { name: "wp_settings_set", args: { search_engines: true } }
|
|
17035
17057
|
});
|
|
17036
|
-
if (robots && !/^sitemap:/im.test(robots))
|
|
17058
|
+
if (robots && !engagedOff && !/^sitemap:/im.test(robots))
|
|
17037
17059
|
add({
|
|
17038
17060
|
id: "robots-sitemap",
|
|
17039
17061
|
priority: "p3",
|
|
@@ -17052,7 +17074,7 @@ async function techAudit(site, o = {}) {
|
|
|
17052
17074
|
why: "ChatGPT, Claude, Perplexity and Google's AI answers can't read or cite the site.",
|
|
17053
17075
|
fix: "Allow them in robots.txt unless the owner chose to block AI."
|
|
17054
17076
|
});
|
|
17055
|
-
if (!sitemap)
|
|
17077
|
+
if (!sitemap && !engagedOff)
|
|
17056
17078
|
add({
|
|
17057
17079
|
id: "sitemap-missing",
|
|
17058
17080
|
priority: "p1",
|
|
@@ -17074,6 +17096,7 @@ async function techAudit(site, o = {}) {
|
|
|
17074
17096
|
tool: { name: "wp_llms_txt_set", args: { generate: true } }
|
|
17075
17097
|
});
|
|
17076
17098
|
const crawl = urls.length ? urls.slice(0, limit) : [`${base}/`];
|
|
17099
|
+
if (!crawl.some((u) => sitePath(u, base) === "/")) crawl.unshift(`${base}/`);
|
|
17077
17100
|
const scans = await pool3(crawl, 6, async (u) => {
|
|
17078
17101
|
const r = await get(u);
|
|
17079
17102
|
if (!r) return null;
|
|
@@ -17093,7 +17116,7 @@ async function techAudit(site, o = {}) {
|
|
|
17093
17116
|
fix: p.status >= 400 ? "Publish the page again, or remove it (wp_post_delete / wp_page_publish)." : "List the final address; the redirect is fine for visitors."
|
|
17094
17117
|
});
|
|
17095
17118
|
for (const p of ok2) {
|
|
17096
|
-
if (/noindex/.test(p.robots))
|
|
17119
|
+
if (/noindex/.test(p.robots) && !engagedOff && discovered === "sitemap")
|
|
17097
17120
|
add({
|
|
17098
17121
|
id: `noindex-in-sitemap:${p.path}`,
|
|
17099
17122
|
priority: "p1",
|
|
@@ -17270,7 +17293,9 @@ async function techAudit(site, o = {}) {
|
|
|
17270
17293
|
summary: {
|
|
17271
17294
|
...Object.fromEntries(order2.map((p) => [p, findings.filter((f) => f.priority === p).length])),
|
|
17272
17295
|
sitemap,
|
|
17273
|
-
|
|
17296
|
+
pages_from: discovered,
|
|
17297
|
+
search_engines: visible,
|
|
17298
|
+
sitemap_urls: discovered === "sitemap" ? urls.length : 0,
|
|
17274
17299
|
crawled: pages.length,
|
|
17275
17300
|
live: ok2.length,
|
|
17276
17301
|
llms_txt: hasLlms,
|
|
@@ -17298,7 +17323,10 @@ async function pageSpeed(url) {
|
|
|
17298
17323
|
const r = await fetch(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?${q3}`, {
|
|
17299
17324
|
signal: AbortSignal.timeout(9e4)
|
|
17300
17325
|
}).catch(() => null);
|
|
17301
|
-
if (!r || !r.ok)
|
|
17326
|
+
if (!r || !r.ok)
|
|
17327
|
+
return {
|
|
17328
|
+
error: `PageSpeed Insights didn't answer (${r?.status ?? "timeout"})${r?.status === 429 && !key ? ": the server needs a PAGESPEED_API_KEY (Google Cloud, free)" : ""}.`
|
|
17329
|
+
};
|
|
17302
17330
|
const j = await r.json();
|
|
17303
17331
|
const f = j.loadingExperience?.metrics ?? {};
|
|
17304
17332
|
const a = j.lighthouseResult?.audits ?? {};
|
|
@@ -17415,8 +17443,24 @@ async function linksPlan(site, o = {}) {
|
|
|
17415
17443
|
const blogPage = docs.find((d) => d.id === settings2.page_for_posts && d.kind === "page");
|
|
17416
17444
|
const actions = [];
|
|
17417
17445
|
const findings = [];
|
|
17418
|
-
const
|
|
17419
|
-
|
|
17446
|
+
const sample = docs.filter(
|
|
17447
|
+
(d) => d.kind === "page" && (/\/sample-page\/$/.test(d.path) || d.title === "Sample Page") || d.kind === "post" && (/\/hello-world\/$/.test(d.path) || d.title === "Hello world!")
|
|
17448
|
+
);
|
|
17449
|
+
for (const d of sample)
|
|
17450
|
+
findings.push({
|
|
17451
|
+
id: `sample:${d.path}`,
|
|
17452
|
+
priority: "p2",
|
|
17453
|
+
area: "outside",
|
|
17454
|
+
title: `WordPress's sample ${d.kind === "page" ? "page" : "article"} \u201C${d.title}\u201D is still published`,
|
|
17455
|
+
why: "It tells visitors and search engines the site isn't finished, and dilutes the real pages.",
|
|
17456
|
+
fix: "Move it to the trash.",
|
|
17457
|
+
tool: {
|
|
17458
|
+
name: d.kind === "page" ? "wp_page_delete" : "wp_post_delete",
|
|
17459
|
+
args: d.kind === "page" ? { page: d.id } : { post: d.id }
|
|
17460
|
+
}
|
|
17461
|
+
});
|
|
17462
|
+
const pageDocs = docs.filter((d) => d.kind === "page" && d !== home && !sample.includes(d));
|
|
17463
|
+
const postDocs = docs.filter((d) => d.kind === "post" && !sample.includes(d));
|
|
17420
17464
|
const menuItems = flatMenu(menu?.items ?? []);
|
|
17421
17465
|
const top = (menu?.items ?? []).length;
|
|
17422
17466
|
for (const i of menuItems) {
|
|
@@ -17504,7 +17548,7 @@ async function linksPlan(site, o = {}) {
|
|
|
17504
17548
|
for (const post of postDocs) {
|
|
17505
17549
|
let best = null;
|
|
17506
17550
|
let score = 0;
|
|
17507
|
-
for (const pg of pageDocs) {
|
|
17551
|
+
for (const pg of home ? [...pageDocs, home] : pageDocs) {
|
|
17508
17552
|
if (pg === blogPage) continue;
|
|
17509
17553
|
const s = overlap(post.words, pg.words, idf);
|
|
17510
17554
|
if (s > score) [best, score] = [pg, s];
|
|
@@ -17536,7 +17580,7 @@ async function linksPlan(site, o = {}) {
|
|
|
17536
17580
|
}
|
|
17537
17581
|
const lonelyPosts = [];
|
|
17538
17582
|
for (const d of docs) {
|
|
17539
|
-
if (d === home || d === blogPage) continue;
|
|
17583
|
+
if (d === home || d === blogPage || sample.includes(d)) continue;
|
|
17540
17584
|
const ins = inbound.get(d.path);
|
|
17541
17585
|
if (ins && ins.size || chrome.has(d.path)) continue;
|
|
17542
17586
|
if (d.kind === "post") lonelyPosts.push(d);
|
|
@@ -17612,7 +17656,7 @@ async function llmsTxt(site, o = {}) {
|
|
|
17612
17656
|
const base = (site.url ?? "").replace(/\/+$/, "");
|
|
17613
17657
|
const s = await site.request({
|
|
17614
17658
|
path: "/wp/v2/settings",
|
|
17615
|
-
query: { _fields: "title,description,page_on_front,page_for_posts" }
|
|
17659
|
+
query: { _fields: "title,description,page_on_front,page_for_posts,blog_public" }
|
|
17616
17660
|
}).then((r) => r.status === 200 ? r.data : {});
|
|
17617
17661
|
const pages = await listPages(site, { status: "publish" });
|
|
17618
17662
|
const posts2 = await all(
|
|
@@ -17621,10 +17665,20 @@ async function llmsTxt(site, o = {}) {
|
|
|
17621
17665
|
{ status: "publish", per_page: 100, _fields: "link,title,excerpt,meta", orderby: "date", order: "desc" },
|
|
17622
17666
|
o.posts ?? 25
|
|
17623
17667
|
);
|
|
17624
|
-
const
|
|
17668
|
+
const clip3 = (t, n2 = 200) => {
|
|
17669
|
+
const x = t.replace(/\s+/g, " ").trim();
|
|
17670
|
+
if (x.length <= n2) return x;
|
|
17671
|
+
const cut = x.slice(0, n2);
|
|
17672
|
+
return `${cut.slice(0, Math.max(cut.lastIndexOf(" "), n2 - 30)).replace(/[\s,;:–-]+$/, "")}\u2026`;
|
|
17673
|
+
};
|
|
17674
|
+
const desc = (m, fallback = "") => clip3(String(m?.maggie_seo_description || fallback));
|
|
17675
|
+
const isSample = (link, title) => /\/(sample-page|hello-world)\/?$/.test(link) || title === "Sample Page" || title === "Hello world!";
|
|
17625
17676
|
const home = pages.find((p) => p.id === s.page_on_front);
|
|
17626
17677
|
const noise = /privacy|terms|cookie|thank|cart|checkout|my-account|login/i;
|
|
17627
|
-
const visible = pages.filter(
|
|
17678
|
+
const visible = pages.filter(
|
|
17679
|
+
(p) => p.id !== s.page_on_front && p.id !== s.page_for_posts && !noise.test(p.link) && !isSample(p.link, titleOf(p.title))
|
|
17680
|
+
);
|
|
17681
|
+
const realPosts = posts2.filter((p) => !isSample(p.link, strip(p.title.rendered ?? "")));
|
|
17628
17682
|
const byParent = /* @__PURE__ */ new Map();
|
|
17629
17683
|
for (const p of visible) byParent.set(p.parent, [...byParent.get(p.parent) ?? [], p]);
|
|
17630
17684
|
const line = (title, url, d) => `- [${title.replace(/[[\]]/g, "")}](${url})${d ? `: ${d}` : ""}`;
|
|
@@ -17632,25 +17686,27 @@ async function llmsTxt(site, o = {}) {
|
|
|
17632
17686
|
const summary = desc(home?.meta, s.description ?? "");
|
|
17633
17687
|
if (summary) out.push(`> ${summary}`, "");
|
|
17634
17688
|
const top = byParent.get(0) ?? [];
|
|
17635
|
-
if (top.length) {
|
|
17689
|
+
if (top.length || home) {
|
|
17636
17690
|
out.push("## Pages", "");
|
|
17691
|
+
if (home) out.push(line(titleOf(home.title) || "Home", home.link, summary));
|
|
17637
17692
|
for (const p of top) {
|
|
17638
17693
|
out.push(line(titleOf(p.title), p.link, desc(p.meta)));
|
|
17639
17694
|
for (const c of byParent.get(p.id) ?? []) out.push(` ${line(titleOf(c.title), c.link, desc(c.meta))}`);
|
|
17640
17695
|
}
|
|
17641
17696
|
out.push("");
|
|
17642
17697
|
}
|
|
17643
|
-
if (
|
|
17698
|
+
if (realPosts.length) {
|
|
17644
17699
|
out.push("## Articles", "");
|
|
17645
|
-
for (const p of
|
|
17646
|
-
out.push(
|
|
17647
|
-
line(strip(p.title.rendered ?? ""), p.link, desc(p.meta, strip(p.excerpt?.rendered ?? "").slice(0, 160)))
|
|
17648
|
-
);
|
|
17700
|
+
for (const p of realPosts)
|
|
17701
|
+
out.push(line(strip(p.title.rendered ?? ""), p.link, desc(p.meta, strip(p.excerpt?.rendered ?? ""))));
|
|
17649
17702
|
out.push("");
|
|
17650
17703
|
}
|
|
17651
17704
|
const legal = pages.filter((p) => /privacy|terms/i.test(p.link));
|
|
17652
|
-
|
|
17653
|
-
|
|
17705
|
+
const optional2 = [
|
|
17706
|
+
...s.blog_public === 0 ? [] : [line("Sitemap", `${base}/wp-sitemap.xml`, "every page and article")],
|
|
17707
|
+
...legal.map((p) => line(titleOf(p.title), p.link, ""))
|
|
17708
|
+
];
|
|
17709
|
+
if (optional2.length) out.push("## Optional", "", ...optional2);
|
|
17654
17710
|
return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trim()}
|
|
17655
17711
|
`;
|
|
17656
17712
|
}
|
|
@@ -23844,7 +23900,7 @@ var program = new Command();
|
|
|
23844
23900
|
// package.json
|
|
23845
23901
|
var package_default = {
|
|
23846
23902
|
name: "@cmo-so/cli",
|
|
23847
|
-
version: "0.12.
|
|
23903
|
+
version: "0.12.14",
|
|
23848
23904
|
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.",
|
|
23849
23905
|
type: "module",
|
|
23850
23906
|
license: "MIT",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.14",
|
|
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",
|