n-seo 0.1.0 → 0.2.0
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/README.md +132 -46
- package/bin/n-seo.mjs +14 -4
- package/docs/FAQ.md +14 -3
- package/docs/MCP.md +25 -9
- package/docs/PRD.md +6 -4
- package/docs/RELEASING.md +21 -0
- package/docs/SCHEDULING.md +42 -5
- package/docs/SETUP-GOOGLE.md +1 -1
- package/ingest/__pycache__/analyze_metadata.cpython-312.pyc +0 -0
- package/ingest/__pycache__/google_auth.cpython-312.pyc +0 -0
- package/ingest/__pycache__/http_util.cpython-312.pyc +0 -0
- package/ingest/__pycache__/seo_config.cpython-312.pyc +0 -0
- package/ingest/analyze_ga4.py +2 -2
- package/ingest/analyze_gsc.py +2 -2
- package/ingest/analyze_metadata.py +2 -2
- package/ingest/analyze_trends.py +2 -2
- package/ingest/google_auth.py +53 -20
- package/ingest/pull_ga4.py +1 -1
- package/ingest/pull_gsc.py +1 -1
- package/ingest/pull_index_status.py +1 -1
- package/ingest/pull_timeseries.py +3 -3
- package/ingest/seo_config.py +19 -3
- package/ops/__pycache__/daily.cpython-312.pyc +0 -0
- package/ops/__pycache__/daily_diff.cpython-312.pyc +0 -0
- package/ops/__pycache__/demo_data.cpython-312.pyc +0 -0
- package/ops/__pycache__/export_static.cpython-312.pyc +0 -0
- package/ops/__pycache__/llm.cpython-312.pyc +0 -0
- package/ops/__pycache__/publish.cpython-312.pyc +0 -0
- package/ops/daily.py +2 -2
- package/ops/daily_diff.py +9 -8
- package/ops/demo_data.py +17 -17
- package/ops/doctor.py +21 -7
- package/ops/export_static.py +4 -4
- package/ops/hn_digest.py +1 -1
- package/ops/indexnow.py +3 -3
- package/ops/llm.py +23 -1
- package/ops/opportunity_scan.py +4 -4
- package/ops/py.mjs +40 -0
- package/ops/reddit_digest.py +1 -1
- package/ops/templates/n-seo-daily-task.xml +65 -0
- package/package.json +16 -9
- package/probes/__pycache__/site_probe.cpython-312.pyc +0 -0
- package/probes/site_probe.py +1 -1
- package/src/actions.ts +1 -1
- package/src/config.ts +1 -1
- package/src/mcp.ts +2 -2
- package/src/settings.tsx +3 -3
- package/src/views.tsx +9 -9
- package/ingest/__pycache__/analyze_ga4.cpython-313.pyc +0 -0
- package/ingest/__pycache__/analyze_gsc.cpython-313.pyc +0 -0
- package/ingest/__pycache__/analyze_metadata.cpython-313.pyc +0 -0
- package/ingest/__pycache__/analyze_trends.cpython-313.pyc +0 -0
- package/ingest/__pycache__/google_auth.cpython-313.pyc +0 -0
- package/ingest/__pycache__/http_util.cpython-313.pyc +0 -0
- package/ingest/__pycache__/pull_ga4.cpython-313.pyc +0 -0
- package/ingest/__pycache__/pull_gsc.cpython-313.pyc +0 -0
- package/ingest/__pycache__/pull_index_status.cpython-313.pyc +0 -0
- package/ingest/__pycache__/pull_timeseries.cpython-313.pyc +0 -0
- package/ingest/__pycache__/seo_config.cpython-313.pyc +0 -0
- package/ops/__pycache__/daily.cpython-313.pyc +0 -0
- package/ops/__pycache__/daily_diff.cpython-313.pyc +0 -0
- package/ops/__pycache__/demo_data.cpython-313.pyc +0 -0
- package/ops/__pycache__/doctor.cpython-313.pyc +0 -0
- package/ops/__pycache__/export_static.cpython-313.pyc +0 -0
- package/ops/__pycache__/hn_digest.cpython-313.pyc +0 -0
- package/ops/__pycache__/indexnow.cpython-313.pyc +0 -0
- package/ops/__pycache__/llm.cpython-313.pyc +0 -0
- package/ops/__pycache__/opportunity_scan.cpython-313.pyc +0 -0
- package/ops/__pycache__/publish.cpython-313.pyc +0 -0
- package/ops/__pycache__/reddit_digest.cpython-313.pyc +0 -0
- package/probes/__pycache__/site_probe.cpython-313.pyc +0 -0
package/ops/opportunity_scan.py
CHANGED
|
@@ -36,7 +36,7 @@ RISE_MIN_RATIO = 2.5 # and have grown at least this much vs prior 84d
|
|
|
36
36
|
|
|
37
37
|
def latest_trends():
|
|
38
38
|
files = sorted(DATA.glob("trends-*.json"))
|
|
39
|
-
return json.loads(files[-1].read_text()) if files else None
|
|
39
|
+
return json.loads(files[-1].read_text(encoding="utf-8")) if files else None
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def queue():
|
|
@@ -51,7 +51,7 @@ def queue():
|
|
|
51
51
|
except json.JSONDecodeError:
|
|
52
52
|
pass
|
|
53
53
|
try:
|
|
54
|
-
return json.loads((seo_config.INSTANCE / "config" / "backlog.json").read_text()).get("actions", [])
|
|
54
|
+
return json.loads((seo_config.INSTANCE / "config" / "backlog.json").read_text(encoding="utf-8")).get("actions", [])
|
|
55
55
|
except (OSError, json.JSONDecodeError):
|
|
56
56
|
return []
|
|
57
57
|
|
|
@@ -84,7 +84,7 @@ def _host_resolver(prop):
|
|
|
84
84
|
best = {}
|
|
85
85
|
qp = seo_config.DATA / "gsc" / seo_config.gsc_data_slug(prop) / "query_page_90d.json"
|
|
86
86
|
try:
|
|
87
|
-
for r in json.loads(qp.read_text()).get("rows", []):
|
|
87
|
+
for r in json.loads(qp.read_text(encoding="utf-8")).get("rows", []):
|
|
88
88
|
q, page = r["keys"][0], r["keys"][1]
|
|
89
89
|
h = page.split("/")[2] if page.count("/") >= 2 else ""
|
|
90
90
|
if h in by_gsc_host and r["impressions"] > best.get(q, (0, ""))[0]:
|
|
@@ -175,7 +175,7 @@ def main():
|
|
|
175
175
|
print("llm module off — candidates recorded without proposals")
|
|
176
176
|
|
|
177
177
|
DATA.mkdir(parents=True, exist_ok=True)
|
|
178
|
-
(DATA / "opportunity-proposals.json").write_text(json.dumps(out, indent=1))
|
|
178
|
+
(DATA / "opportunity-proposals.json").write_text(json.dumps(out, indent=1), encoding="utf-8")
|
|
179
179
|
print(f"candidates: {len(candidates)} | proposals: {len(out['proposals'])} | "
|
|
180
180
|
f"verdicts: {len(out['verdicts'])} | inference: {out['inference_ran']}")
|
|
181
181
|
return 0
|
package/ops/py.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* Run a Python script with whichever interpreter this machine actually has.
|
|
3
|
+
*
|
|
4
|
+
* The npm scripts used to hardcode `python3`, which does not exist on
|
|
5
|
+
* Windows: Python installs there as `python`, or only behind the `py`
|
|
6
|
+
* launcher. Worse, Windows ships an App Execution Alias at `python3.exe`
|
|
7
|
+
* that opens the Microsoft Store instead of running anything, so a hardcoded
|
|
8
|
+
* `python3` fails in a way that looks like Python is missing when it is not.
|
|
9
|
+
* Probing for one that answers `--version` is the only reliable answer.
|
|
10
|
+
*
|
|
11
|
+
* node ops/py.mjs ops/doctor.py --offline
|
|
12
|
+
*
|
|
13
|
+
* $PYTHON overrides, which is also how the n-seo CLI behaves.
|
|
14
|
+
*/
|
|
15
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
|
|
18
|
+
const CANDIDATES = process.platform === "win32"
|
|
19
|
+
? ["python", "python3", "py"]
|
|
20
|
+
: ["python3", "python"];
|
|
21
|
+
|
|
22
|
+
export function pythonBin() {
|
|
23
|
+
if (process.env.PYTHON) return process.env.PYTHON;
|
|
24
|
+
for (const c of CANDIDATES) {
|
|
25
|
+
const probe = spawnSync(c, ["--version"], { stdio: "ignore" });
|
|
26
|
+
if (!probe.error && probe.status === 0) return c;
|
|
27
|
+
}
|
|
28
|
+
return CANDIDATES[0]; // let the real command fail with a useful message
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Only act as a launcher when invoked directly, not when imported.
|
|
32
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
33
|
+
const child = spawn(pythonBin(), process.argv.slice(2), { stdio: "inherit" });
|
|
34
|
+
child.on("error", (err) => {
|
|
35
|
+
console.error(`could not start ${pythonBin()}: ${err.message}`);
|
|
36
|
+
console.error("install Python 3.10+, or set $PYTHON to its path");
|
|
37
|
+
process.exit(127);
|
|
38
|
+
});
|
|
39
|
+
child.on("exit", (code, signal) => process.exit(signal ? 1 : code ?? 1));
|
|
40
|
+
}
|
package/ops/reddit_digest.py
CHANGED
|
@@ -151,7 +151,7 @@ def main():
|
|
|
151
151
|
seo_config.DATA.mkdir(parents=True, exist_ok=True)
|
|
152
152
|
(seo_config.DATA / "reddit-digest.json").write_text(json.dumps(
|
|
153
153
|
{"generated": datetime.now(timezone.utc).isoformat(timespec="minutes"),
|
|
154
|
-
"user": user or None, "auth": bool(oauth_token()), "picks": picks}, indent=1))
|
|
154
|
+
"user": user or None, "auth": bool(oauth_token()), "picks": picks}, indent=1), encoding="utf-8")
|
|
155
155
|
for p in picks:
|
|
156
156
|
print(f"- r/{p['sub']}: {p['title']} ({p['comments']}c/{p['score']}pts, {p['age_days']}d) {p['url']}")
|
|
157
157
|
if not picks:
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-16"?>
|
|
2
|
+
<!--
|
|
3
|
+
n-seo daily run at 07:00 local, for Windows Task Scheduler.
|
|
4
|
+
|
|
5
|
+
Register it (from an elevated or normal prompt, your choice — this is a
|
|
6
|
+
per-user task):
|
|
7
|
+
|
|
8
|
+
schtasks /create /tn "n-seo daily" /xml ops\templates\n-seo-daily-task.xml
|
|
9
|
+
|
|
10
|
+
Before you do, replace the two placeholders below:
|
|
11
|
+
__NSEO__ the n-seo command, e.g. C:\Users\you\AppData\Roaming\npm\n-seo.cmd
|
|
12
|
+
(find it with: where n-seo)
|
|
13
|
+
__INSTANCE__ your instance directory, e.g. C:\Users\you\my-sites
|
|
14
|
+
|
|
15
|
+
StartWhenAvailable is the Windows equivalent of launchd's catch-up
|
|
16
|
+
behaviour: a machine that was asleep at 07:00 runs the job when it wakes,
|
|
17
|
+
rather than skipping the day the way cron does.
|
|
18
|
+
-->
|
|
19
|
+
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
20
|
+
<RegistrationInfo>
|
|
21
|
+
<Description>n-seo daily run: pulls Search Console and GA4, probes the sites, refreshes the queue.</Description>
|
|
22
|
+
<URI>\n-seo daily</URI>
|
|
23
|
+
</RegistrationInfo>
|
|
24
|
+
<Triggers>
|
|
25
|
+
<CalendarTrigger>
|
|
26
|
+
<StartBoundary>2026-01-01T07:00:00</StartBoundary>
|
|
27
|
+
<Enabled>true</Enabled>
|
|
28
|
+
<ScheduleByDay>
|
|
29
|
+
<DaysInterval>1</DaysInterval>
|
|
30
|
+
</ScheduleByDay>
|
|
31
|
+
</CalendarTrigger>
|
|
32
|
+
</Triggers>
|
|
33
|
+
<Principals>
|
|
34
|
+
<Principal id="Author">
|
|
35
|
+
<LogonType>InteractiveToken</LogonType>
|
|
36
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
37
|
+
</Principal>
|
|
38
|
+
</Principals>
|
|
39
|
+
<Settings>
|
|
40
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
41
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
42
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
43
|
+
<AllowHardTerminate>true</AllowHardTerminate>
|
|
44
|
+
<StartWhenAvailable>true</StartWhenAvailable>
|
|
45
|
+
<RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
|
|
46
|
+
<IdleSettings>
|
|
47
|
+
<StopOnIdleEnd>false</StopOnIdleEnd>
|
|
48
|
+
<RestartOnIdle>false</RestartOnIdle>
|
|
49
|
+
</IdleSettings>
|
|
50
|
+
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
51
|
+
<Enabled>true</Enabled>
|
|
52
|
+
<Hidden>false</Hidden>
|
|
53
|
+
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
54
|
+
<WakeToRun>false</WakeToRun>
|
|
55
|
+
<ExecutionTimeLimit>PT2H</ExecutionTimeLimit>
|
|
56
|
+
<Priority>7</Priority>
|
|
57
|
+
</Settings>
|
|
58
|
+
<Actions Context="Author">
|
|
59
|
+
<Exec>
|
|
60
|
+
<Command>__NSEO__</Command>
|
|
61
|
+
<Arguments>daily --instance "__INSTANCE__"</Arguments>
|
|
62
|
+
<WorkingDirectory>__INSTANCE__</WorkingDirectory>
|
|
63
|
+
</Exec>
|
|
64
|
+
</Actions>
|
|
65
|
+
</Task>
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n-seo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Agentic, local-first SEO / AEO / GEO control plane: pulls Search Console + GA4, probes your sites, and ranks the next moves. Your LLM writes the proposals; your coding agent works the queue over MCP.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "En Dash Consulting (https://endash.us)",
|
|
7
|
-
"homepage": "https://
|
|
7
|
+
"homepage": "https://n-seo.dev/",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/en-dash-consulting/n-seo.git"
|
|
@@ -16,12 +16,19 @@
|
|
|
16
16
|
"seo",
|
|
17
17
|
"aeo",
|
|
18
18
|
"geo",
|
|
19
|
+
"agent",
|
|
20
|
+
"ai-agent",
|
|
21
|
+
"agentic",
|
|
22
|
+
"llm",
|
|
23
|
+
"claude",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"mcp",
|
|
26
|
+
"mcp-server",
|
|
19
27
|
"search-console",
|
|
20
28
|
"google-analytics",
|
|
21
29
|
"ga4",
|
|
22
30
|
"llms-txt",
|
|
23
31
|
"ai-crawlers",
|
|
24
|
-
"mcp",
|
|
25
32
|
"dashboard",
|
|
26
33
|
"local-first",
|
|
27
34
|
"cli"
|
|
@@ -53,13 +60,13 @@
|
|
|
53
60
|
"typecheck": "tsc --noEmit",
|
|
54
61
|
"mcp": "tsx src/mcp-stdio.ts",
|
|
55
62
|
"mcp:smoke": "node ops/mcp-smoke-stdio.mjs",
|
|
56
|
-
"doctor": "
|
|
57
|
-
"demo": "
|
|
58
|
-
"daily": "
|
|
63
|
+
"doctor": "node ops/py.mjs ops/doctor.py",
|
|
64
|
+
"demo": "node ops/py.mjs ops/demo_data.py",
|
|
65
|
+
"daily": "node ops/py.mjs ops/daily.py",
|
|
59
66
|
"test": "tsx --test tests/ts/*.test.ts",
|
|
60
|
-
"test:py": "
|
|
67
|
+
"test:py": "node ops/py.mjs -m unittest discover -s tests/py -v",
|
|
61
68
|
"check": "npm run typecheck && npm test && npm run test:py",
|
|
62
|
-
"export": "
|
|
69
|
+
"export": "node ops/py.mjs ops/export_static.py",
|
|
63
70
|
"docker:build": "docker build -t n-seo:latest .",
|
|
64
71
|
"docker:up": "docker compose -f docker/compose.yml up -d"
|
|
65
72
|
},
|
|
Binary file
|
package/probes/site_probe.py
CHANGED
|
@@ -173,7 +173,7 @@ def main():
|
|
|
173
173
|
out_dir = seo_config.DATA / "probes"
|
|
174
174
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
175
175
|
out = out_dir / f"probe-{datetime.now(timezone.utc):%Y%m%d-%H%M%S}.json"
|
|
176
|
-
out.write_text(json.dumps(snapshot, indent=2))
|
|
176
|
+
out.write_text(json.dumps(snapshot, indent=2), encoding="utf-8")
|
|
177
177
|
|
|
178
178
|
for s in snapshot["sites"]:
|
|
179
179
|
h = s["homepage"]
|
package/src/actions.ts
CHANGED
|
@@ -118,7 +118,7 @@ function probeActions(site: SiteCfg): Action[] {
|
|
|
118
118
|
const mk = (key: string, title: string, how: string): Action => ({
|
|
119
119
|
id: `gen-hygiene-${slug(site.host)}-${key}`,
|
|
120
120
|
host: site.host, title, kind: "Config change", why: "Live probe failing on this check", how,
|
|
121
|
-
spec: [how, `Verify with:
|
|
121
|
+
spec: [how, `Verify with: n-seo daily --only probe after deploy.`],
|
|
122
122
|
impact: 5, effort: "S", tag: "hygiene", source: "rule",
|
|
123
123
|
});
|
|
124
124
|
if (!p.robots.exists) out.push(mk("robots", "Add robots.txt", "Serve robots.txt with a Sitemap: pointer and explicit AI-crawler allowances."));
|
package/src/config.ts
CHANGED
|
@@ -80,7 +80,7 @@ export interface Hooks {
|
|
|
80
80
|
beforeRun: string[];
|
|
81
81
|
/** … after the last step (and after last-run.json is written) */
|
|
82
82
|
afterRun: string[];
|
|
83
|
-
/** … after a named step, e.g. { "daily-diff": ["
|
|
83
|
+
/** … after a named step, e.g. { "daily-diff": ["node my_sync.mjs"] } */
|
|
84
84
|
afterStep: Record<string, string[]>;
|
|
85
85
|
}
|
|
86
86
|
|
package/src/mcp.ts
CHANGED
|
@@ -273,7 +273,7 @@ export function createMcpServer(): McpServer {
|
|
|
273
273
|
},
|
|
274
274
|
async ({ host }) => {
|
|
275
275
|
const audit = data.metadataAudit();
|
|
276
|
-
if (!audit) return fail("No metadata audit on disk yet — run
|
|
276
|
+
if (!audit) return fail("No metadata audit on disk yet — run `n-seo daily --only metadata-audit`.");
|
|
277
277
|
if (!host) return json(audit);
|
|
278
278
|
const findings = audit.sites[host];
|
|
279
279
|
if (!findings) return fail(`No audit entry for "${host}". Known: ${Object.keys(audit.sites).join(", ")}`);
|
|
@@ -379,7 +379,7 @@ export function createMcpServer(): McpServer {
|
|
|
379
379
|
},
|
|
380
380
|
async () => {
|
|
381
381
|
const scan = data.opportunityScan();
|
|
382
|
-
if (!scan) return fail("No opportunity scan on disk yet — run
|
|
382
|
+
if (!scan) return fail("No opportunity scan on disk yet — run `n-seo daily --only opportunity-scan`.");
|
|
383
383
|
return json(scan);
|
|
384
384
|
}
|
|
385
385
|
);
|
package/src/settings.tsx
CHANGED
|
@@ -105,7 +105,7 @@ export const SettingsPage: FC<{ saved?: boolean; error?: string }> = ({ saved, e
|
|
|
105
105
|
<div class="s-title">Mode</div>
|
|
106
106
|
<div class="mono">{cfg.google.auth}</div>
|
|
107
107
|
<p class="sub">{cfg.google.auth === "service-account-key"
|
|
108
|
-
? "A service-account JSON key
|
|
108
|
+
? "A service-account JSON key. The JWT is signed locally with node's crypto module — no gcloud, no openssl, nothing to install."
|
|
109
109
|
: cfg.google.auth === "metadata"
|
|
110
110
|
? "The runtime service account from the GCE / Cloud Run / GKE metadata server — no key file. It mints its own scoped tokens, which needs Token Creator on itself."
|
|
111
111
|
: cfg.google.auth === "gcloud-impersonate"
|
|
@@ -122,7 +122,7 @@ export const SettingsPage: FC<{ saved?: boolean; error?: string }> = ({ saved, e
|
|
|
122
122
|
<div class="s-card">
|
|
123
123
|
<div class="s-title">Grant access to</div>
|
|
124
124
|
<div class="mono">{sa.email ?? cfg.google.impersonate ?? (cfg.google.auth === "metadata" ? "the runtime service account" : "—")}</div>
|
|
125
|
-
<p class="sub">Add this email as a <b>Full</b> user on each Search Console property and a <b>Viewer</b> on each GA4 property. Then <code>
|
|
125
|
+
<p class="sub">Add this email as a <b>Full</b> user on each Search Console property and a <b>Viewer</b> on each GA4 property. Then <code>n-seo doctor</code> confirms it can see them.</p>
|
|
126
126
|
</div>
|
|
127
127
|
</div>
|
|
128
128
|
|
|
@@ -254,7 +254,7 @@ export const SettingsPage: FC<{ saved?: boolean; error?: string }> = ({ saved, e
|
|
|
254
254
|
<div class="mono">{lr.ts}</div>
|
|
255
255
|
<p class="sub">{lr.failures.trim() ? <span class="chip bad">failed: {lr.failures}</span> : <span class="chip good">all steps OK</span>}</p>
|
|
256
256
|
</>
|
|
257
|
-
) : <p class="sub">never — run <code>
|
|
257
|
+
) : <p class="sub">never — run <code>n-seo daily</code></p>}
|
|
258
258
|
</div>
|
|
259
259
|
<div class="s-card">
|
|
260
260
|
<div class="s-title">Data freshness</div>
|
package/src/views.tsx
CHANGED
|
@@ -215,7 +215,7 @@ export const SplitQueue: FC<{ actions: Action[]; idPrefix: string }> = ({ action
|
|
|
215
215
|
{active.length ? (
|
|
216
216
|
<ActionQueue actions={active} showHost idPrefix={`${idPrefix}-a`} />
|
|
217
217
|
) : (
|
|
218
|
-
<p class="empty">Nothing active. Either the data has no findings yet (run <code>
|
|
218
|
+
<p class="empty">Nothing active. Either the data has no findings yet (run <code>n-seo daily</code>) or the queue is clear — add strategic items in <code>config/backlog.json</code>.</p>
|
|
219
219
|
)}
|
|
220
220
|
{watching.length > 0 && (
|
|
221
221
|
<>
|
|
@@ -526,7 +526,7 @@ export const SiteDetail: FC<{ site: SiteCfg; days?: number }> = ({ site, days =
|
|
|
526
526
|
) : (
|
|
527
527
|
<p class="empty">
|
|
528
528
|
{site.ga4Property || site.gscProperty
|
|
529
|
-
? <>no time series yet — run <code>
|
|
529
|
+
? <>no time series yet — run <code>n-seo daily --only timeseries</code></>
|
|
530
530
|
: "no Search Console or GA4 property configured for this site"}
|
|
531
531
|
</p>
|
|
532
532
|
)}
|
|
@@ -614,11 +614,11 @@ export const SiteDetail: FC<{ site: SiteCfg; days?: number }> = ({ site, days =
|
|
|
614
614
|
|
|
615
615
|
export const Probes: FC = () => {
|
|
616
616
|
const probe = data.latestProbe();
|
|
617
|
-
if (!probe) return <><h1>Live-site probe</h1><p class="empty">No probe snapshots yet — run <code>
|
|
617
|
+
if (!probe) return <><h1>Live-site probe</h1><p class="empty">No probe snapshots yet — run <code>n-seo daily --only probe</code></p></>;
|
|
618
618
|
return (
|
|
619
619
|
<>
|
|
620
620
|
<h1>Live-site probe</h1>
|
|
621
|
-
<p class="sub">Snapshot {probe.probed_at} · no-auth checks of what crawlers actually see. Re-run any time: <code>
|
|
621
|
+
<p class="sub">Snapshot {probe.probed_at} · no-auth checks of what crawlers actually see. Re-run any time: <code>n-seo daily --only probe</code></p>
|
|
622
622
|
<div class="tbl-wrap"><table>
|
|
623
623
|
<thead>
|
|
624
624
|
<tr><th>Site</th><th>robots</th><th>AI crawlers</th><th>sitemap</th><th>llms.txt</th><th>404</th><th>JSON-LD</th><th>visible text</th><th>Title</th></tr>
|
|
@@ -704,7 +704,7 @@ export const IndexingPage: FC = () => {
|
|
|
704
704
|
<>
|
|
705
705
|
<h1>Indexing</h1>
|
|
706
706
|
<p class="empty">
|
|
707
|
-
No index snapshot yet — enable the <b>Index coverage sweep</b> module and run <code>
|
|
707
|
+
No index snapshot yet — enable the <b>Index coverage sweep</b> module and run <code>n-seo daily --only index-status</code>
|
|
708
708
|
</p>
|
|
709
709
|
</>
|
|
710
710
|
);
|
|
@@ -890,7 +890,7 @@ export const InsightsPage: FC = () => {
|
|
|
890
890
|
})()}
|
|
891
891
|
</>
|
|
892
892
|
) : (
|
|
893
|
-
<p class="empty">No trend analysis yet — run <code>
|
|
893
|
+
<p class="empty">No trend analysis yet — run <code>n-seo daily --only opportunity-scan</code>, which refreshes it (the daily run does this for you when the module is on).</p>
|
|
894
894
|
)}
|
|
895
895
|
</>
|
|
896
896
|
);
|
|
@@ -976,7 +976,7 @@ export const ContentPage: FC = () => {
|
|
|
976
976
|
))}
|
|
977
977
|
</div>
|
|
978
978
|
) : (
|
|
979
|
-
<p class="empty">No fresh threads — the daily run refreshes this list (<code>
|
|
979
|
+
<p class="empty">No fresh threads — the daily run refreshes this list (<code>n-seo daily --only hn-digest</code> any time).</p>
|
|
980
980
|
)}
|
|
981
981
|
</>
|
|
982
982
|
)}
|
|
@@ -1216,7 +1216,7 @@ export const LogsPage: FC = () => {
|
|
|
1216
1216
|
<h2>{sec.heading}</h2>
|
|
1217
1217
|
<ul class="log-lines">{sec.lines.map(logLine)}</ul>
|
|
1218
1218
|
</div>
|
|
1219
|
-
)) : <p class="empty">No daily-log entries yet — <code>
|
|
1219
|
+
)) : <p class="empty">No daily-log entries yet — <code>n-seo daily</code> writes <code>docs/daily-log.md</code>.</p>}
|
|
1220
1220
|
|
|
1221
1221
|
<h2>Raw run output <small>(data/daily-ops.log, last 150 lines — look here if a run failed)</small></h2>
|
|
1222
1222
|
<div class="tbl-wrap"><pre class="ops-log">{data.opsLogTail()}</pre></div>
|
|
@@ -1403,7 +1403,7 @@ export const TrendsPage: FC<{ days: number }> = ({ days }) => {
|
|
|
1403
1403
|
<span class="domain">{site.host}</span>
|
|
1404
1404
|
<span class="chip">{site.gscProperty ? "Search Console connected" : "no Search Console property"}</span>
|
|
1405
1405
|
<span class="chip">{site.ga4Property ? "GA4 connected" : "no GA4 property"}</span>
|
|
1406
|
-
<span class="trend-empty-note">no time series yet — run <code>
|
|
1406
|
+
<span class="trend-empty-note">no time series yet — run <code>n-seo daily --only timeseries</code></span>
|
|
1407
1407
|
</div>
|
|
1408
1408
|
);
|
|
1409
1409
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|