n-seo 0.2.0 → 0.3.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/.claude/skills/n-seo-add-site/SKILL.md +125 -0
- package/.claude/skills/n-seo-deploy/SKILL.md +171 -0
- package/.claude/skills/n-seo-review/SKILL.md +106 -0
- package/.claude/skills/n-seo-setup/SKILL.md +139 -0
- package/.claude/skills/n-seo-ship/SKILL.md +122 -0
- package/.claude/skills/n-seo-triage/SKILL.md +90 -0
- package/.claude/skills/orient/SKILL.md +54 -0
- package/README.md +93 -15
- package/bin/n-seo.mjs +163 -6
- package/docs/ADDING-A-SITE.md +4 -0
- package/docs/DEPLOY.md +4 -0
- package/docs/FAQ.md +32 -3
- package/docs/INSTANCE.md +54 -0
- package/docs/PRD.md +23 -5
- package/docs/SCHEDULING.md +4 -0
- package/docs/SETUP-GOOGLE.md +7 -0
- 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/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/demo_data.py +6 -0
- package/ops/doctor.py +9 -0
- package/ops/export_static.py +5 -0
- package/package.json +10 -2
- package/probes/__pycache__/site_probe.cpython-312.pyc +0 -0
- package/public/fonts/OFL.txt +105 -0
- package/public/fonts/montserrat-latin-var.woff2 +0 -0
- package/public/styles.css +18 -1
- package/src/server.tsx +22 -1
- package/src/views.tsx +70 -6
package/docs/PRD.md
CHANGED
|
@@ -102,6 +102,10 @@ a fresh checkout runs.
|
|
|
102
102
|
- Acceptance: date × page series for 180 days from both sources.
|
|
103
103
|
- Acceptance: URL Inspection verdict for every sitemap URL (sitemap indexes
|
|
104
104
|
followed one level, ≤400 URLs/host), with sitemap submission state.
|
|
105
|
+
- Acceptance: each coverage state renders with what it means, what to do
|
|
106
|
+
about it, and whether Request Indexing helps — true only where Google has
|
|
107
|
+
formed no judgement on the content (unknown, discovered-never-crawled),
|
|
108
|
+
false where it fetched and declined (soft 404, crawled-not-indexed).
|
|
105
109
|
- Acceptance: metadata audit flags title/query mismatch, CTR below position
|
|
106
110
|
expectation, missing/short/duplicate descriptions, long titles.
|
|
107
111
|
- Acceptance: trend file with branded/generic split, rising/falling queries
|
|
@@ -148,7 +152,9 @@ the config with the current values as defaults.
|
|
|
148
152
|
be marked watching or retired; rule-derived items expose no write buttons.
|
|
149
153
|
- Acceptance: the server binds 127.0.0.1 by default and rejects cross-origin
|
|
150
154
|
POSTs.
|
|
151
|
-
- Acceptance: light and dark themes; no external fonts
|
|
155
|
+
- Acceptance: light and dark themes; no external fonts — the one webfont is
|
|
156
|
+
served from the app's own origin, so the dashboard renders identically on a
|
|
157
|
+
host with no outbound internet and leaks no request to a font CDN.
|
|
152
158
|
|
|
153
159
|
## Feature: Settings page [shipped]
|
|
154
160
|
|
|
@@ -206,14 +212,22 @@ instead of a CLI, with the key read from `.env`.
|
|
|
206
212
|
mode is unchanged when it is unset.
|
|
207
213
|
- Acceptance: `n-seo init|start|dev|daily|doctor|demo|mcp|check|export|
|
|
208
214
|
upgrade|version` behave as documented in `docs/INSTANCE.md`.
|
|
215
|
+
- Acceptance: an instance is a standalone project. `n-seo init` exits 2
|
|
216
|
+
without writing anything when the target holds an application marker, or
|
|
217
|
+
when the directory it would create falls inside a git repository that is
|
|
218
|
+
not an instance; `--force` overrides, re-running on an existing instance
|
|
219
|
+
succeeds, and in-place mode in the engine checkout is unaffected.
|
|
220
|
+
- Acceptance: `init` installs the seven instance-facing skills and a
|
|
221
|
+
`CLAUDE.md` with the operating rules, substituting the real engine and
|
|
222
|
+
instance paths, and leaves the contributor (`ndx-*`) skills behind.
|
|
209
223
|
- Acceptance: `n-seo upgrade` refuses to leave the engine on a commit that
|
|
210
224
|
fails `npm run check` without printing the rollback command.
|
|
211
225
|
|
|
212
|
-
## Feature: Publish the engine to npm [
|
|
226
|
+
## Feature: Publish the engine to npm [shipped]
|
|
213
227
|
|
|
214
228
|
- Acceptance: `npm i n-seo` installs a working engine; `npx n-seo init`
|
|
215
229
|
scaffolds an instance; the `files` list excludes tests and the marketing
|
|
216
|
-
site.
|
|
230
|
+
site, and includes the instance-facing skills.
|
|
217
231
|
|
|
218
232
|
## Feature: Scheduled upgrade with gate [planned]
|
|
219
233
|
|
|
@@ -246,6 +260,10 @@ runs doctor.
|
|
|
246
260
|
promises (local data, evidence first, briefs-not-acts) and the operating
|
|
247
261
|
rules.
|
|
248
262
|
|
|
249
|
-
## Feature: Custom domain [
|
|
263
|
+
## Feature: Custom domain [shipped]
|
|
250
264
|
|
|
251
|
-
`www/CNAME`
|
|
265
|
+
`www/CNAME` carries n-seo.dev; analytics are guarded on the hostname so a
|
|
266
|
+
fork's Pages deploy never reports into the project's property.
|
|
267
|
+
|
|
268
|
+
- Acceptance: typography is self-hosted (latin-subset variable woff2 from the
|
|
269
|
+
page's own origin), so the site makes no third-party request.
|
package/docs/SCHEDULING.md
CHANGED
|
@@ -4,6 +4,10 @@ Two things should always be running: the **dashboard** (a long-lived process
|
|
|
4
4
|
serving the configured port) and the **daily run** (`ops/daily.py`, once a
|
|
5
5
|
morning). Both are plain processes; pick the scheduler your OS already has.
|
|
6
6
|
|
|
7
|
+
> Or say *"schedule the morning run"* in the instance directory — the
|
|
8
|
+
> `n-seo-deploy` skill covers scheduling on each platform, and `n-seo-setup`
|
|
9
|
+
> ends here.
|
|
10
|
+
|
|
7
11
|
## macOS (launchd)
|
|
8
12
|
|
|
9
13
|
The install script fills in the templates under `ops/templates/`, copies them
|
package/docs/SETUP-GOOGLE.md
CHANGED
|
@@ -7,6 +7,13 @@ account with a JSON key** — a machine identity you add as a read-only user in
|
|
|
7
7
|
each console. No gcloud install, no browser login, no token that expires when
|
|
8
8
|
your session does.
|
|
9
9
|
|
|
10
|
+
> **You can have this done for you.** `n-seo init` installed the
|
|
11
|
+
> `n-seo-setup` skill in your instance. Open the directory in Claude Code and
|
|
12
|
+
> say *"set this up for my sites"*: it works through everything below, runs
|
|
13
|
+
> `n-seo doctor` between steps, and stops at each decision that is yours (the
|
|
14
|
+
> Cloud project, the property ids, whether to grant access). This page is the
|
|
15
|
+
> reference behind it, and what to read when something does not fit.
|
|
16
|
+
|
|
10
17
|
## 1. Pick a Google Cloud project
|
|
11
18
|
|
|
12
19
|
Any project works; a dedicated one keeps it tidy. In the
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/ops/demo_data.py
CHANGED
|
@@ -522,6 +522,12 @@ def main():
|
|
|
522
522
|
else:
|
|
523
523
|
print("\nnow run: n-seo start (then open the dashboard)")
|
|
524
524
|
print("remove with: n-seo demo --clean")
|
|
525
|
+
# The demo answers "what is this"; the next question is always "how do I
|
|
526
|
+
# point it at my own sites", and the answer is a sentence, not a doc.
|
|
527
|
+
if (seo_config.INSTANCE / ".claude" / "skills" / "n-seo-setup" / "SKILL.md").exists():
|
|
528
|
+
print("\nwhen you want it on your own sites, you can just ask:")
|
|
529
|
+
print(f" cd {seo_config.INSTANCE} && claude")
|
|
530
|
+
print(' "set this up for my sites" → /n-seo-setup')
|
|
525
531
|
return 0
|
|
526
532
|
|
|
527
533
|
|
package/ops/doctor.py
CHANGED
|
@@ -273,6 +273,15 @@ def main():
|
|
|
273
273
|
check_dashboard()
|
|
274
274
|
check_modules(cfg)
|
|
275
275
|
print(f"\n{'all good' if not FAILS else f'{FAILS} problem(s) to fix'}")
|
|
276
|
+
# Most people should not work through the setup docs by hand. If the
|
|
277
|
+
# skills are installed, say so exactly when it is useful: after a report
|
|
278
|
+
# that lists things to fix.
|
|
279
|
+
if FAILS and (seo_config.INSTANCE / ".claude" / "skills" / "n-seo-setup" / "SKILL.md").exists():
|
|
280
|
+
print(
|
|
281
|
+
"\nnot sure what to do with these? this instance ships the fixes as skills.\n"
|
|
282
|
+
f" cd {seo_config.INSTANCE} && claude\n"
|
|
283
|
+
' "set this up for my sites" → /n-seo-setup works through every item above'
|
|
284
|
+
)
|
|
276
285
|
return 1 if FAILS else 0
|
|
277
286
|
|
|
278
287
|
|
package/ops/export_static.py
CHANGED
|
@@ -106,6 +106,11 @@ def main():
|
|
|
106
106
|
dest.write_text(html, encoding="utf-8")
|
|
107
107
|
|
|
108
108
|
(out / "styles.css").write_text(fetch("/styles.css"), encoding="utf-8")
|
|
109
|
+
# styles.css declares @font-face against /fonts/, so the file has to
|
|
110
|
+
# travel with it or every heading in the mirror falls back silently.
|
|
111
|
+
fonts = ROOT / "public" / "fonts"
|
|
112
|
+
if fonts.is_dir():
|
|
113
|
+
shutil.copytree(fonts, out / "fonts")
|
|
109
114
|
(out / "favicon.svg").write_text(fetch("/favicon.svg"), encoding="utf-8")
|
|
110
115
|
(out / "robots.txt").write_text("User-agent: *\nDisallow: /\n", encoding="utf-8")
|
|
111
116
|
except BaseException:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n-seo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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)",
|
|
@@ -47,6 +47,14 @@
|
|
|
47
47
|
"public/",
|
|
48
48
|
"n-seo.config.example.json",
|
|
49
49
|
".env.example",
|
|
50
|
+
".claude/skills/orient/",
|
|
51
|
+
".claude/skills/n-seo-setup/",
|
|
52
|
+
".claude/skills/n-seo-add-site/",
|
|
53
|
+
".claude/skills/n-seo-triage/",
|
|
54
|
+
".claude/skills/n-seo-ship/",
|
|
55
|
+
".claude/skills/n-seo-review/",
|
|
56
|
+
".claude/skills/n-seo-deploy/",
|
|
57
|
+
"!.claude/skills/README.md",
|
|
50
58
|
"docs/",
|
|
51
59
|
"README.md",
|
|
52
60
|
"LICENSE"
|
|
@@ -71,7 +79,7 @@
|
|
|
71
79
|
"docker:up": "docker compose -f docker/compose.yml up -d"
|
|
72
80
|
},
|
|
73
81
|
"dependencies": {
|
|
74
|
-
"@hono/node-server": "^1.
|
|
82
|
+
"@hono/node-server": "^2.1.1",
|
|
75
83
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
76
84
|
"hono": "^4.6.14",
|
|
77
85
|
"tsx": "^4.19.2",
|
|
Binary file
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
The two fonts in this directory are used under the SIL Open Font License 1.1.
|
|
2
|
+
The full license text follows. Both are redistributed unmodified, as the
|
|
3
|
+
latin-subset variable woff2 files Google Fonts serves.
|
|
4
|
+
|
|
5
|
+
montserrat-latin-var.woff2
|
|
6
|
+
Copyright 2024 The Montserrat Project Authors
|
|
7
|
+
https://github.com/JulietaUla/Montserrat
|
|
8
|
+
|
|
9
|
+
merriweather-latin-var.woff2
|
|
10
|
+
Copyright 2020 The Merriweather Project Authors, with Reserved Font Name
|
|
11
|
+
"Merriweather". https://github.com/EbenSorkin/Merriweather4
|
|
12
|
+
|
|
13
|
+
--------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
16
|
+
This license is copied below, and is also available with a FAQ at:
|
|
17
|
+
https://openfontlicense.org
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
-----------------------------------------------------------
|
|
21
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
22
|
+
-----------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
PREAMBLE
|
|
25
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
26
|
+
development of collaborative font projects, to support the font creation
|
|
27
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
28
|
+
open framework in which fonts may be shared and improved in partnership
|
|
29
|
+
with others.
|
|
30
|
+
|
|
31
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
32
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
33
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
34
|
+
redistributed and/or sold with any software provided that any reserved
|
|
35
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
36
|
+
however, cannot be released under any other type of license. The
|
|
37
|
+
requirement for fonts to remain under this license does not apply
|
|
38
|
+
to any document created using the fonts or their derivatives.
|
|
39
|
+
|
|
40
|
+
DEFINITIONS
|
|
41
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
42
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
43
|
+
include source files, build scripts and documentation.
|
|
44
|
+
|
|
45
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
46
|
+
copyright statement(s).
|
|
47
|
+
|
|
48
|
+
"Original Version" refers to the collection of Font Software components as
|
|
49
|
+
distributed by the Copyright Holder(s).
|
|
50
|
+
|
|
51
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
52
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
53
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
54
|
+
new environment.
|
|
55
|
+
|
|
56
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
57
|
+
writer or other person who contributed to the Font Software.
|
|
58
|
+
|
|
59
|
+
PERMISSION & CONDITIONS
|
|
60
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
61
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
62
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
63
|
+
Software, subject to the following conditions:
|
|
64
|
+
|
|
65
|
+
1) Neither the Font Software nor any of its individual components,
|
|
66
|
+
in Original or Modified Versions, may be sold by itself.
|
|
67
|
+
|
|
68
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
69
|
+
redistributed and/or sold with any software, provided that each copy
|
|
70
|
+
contains the above copyright notice and this license. These can be
|
|
71
|
+
included either as stand-alone text files, human-readable headers or
|
|
72
|
+
in the appropriate machine-readable metadata fields within text or
|
|
73
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
74
|
+
|
|
75
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
76
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
77
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
78
|
+
presented to the users.
|
|
79
|
+
|
|
80
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
81
|
+
Software shall not be used to promote, endorse or advertise any
|
|
82
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
83
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
84
|
+
permission.
|
|
85
|
+
|
|
86
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
87
|
+
must be distributed entirely under this license, and must not be
|
|
88
|
+
distributed under any other license. The requirement for fonts to
|
|
89
|
+
remain under this license does not apply to any document created
|
|
90
|
+
using the Font Software.
|
|
91
|
+
|
|
92
|
+
TERMINATION
|
|
93
|
+
This license becomes null and void if any of the above conditions are
|
|
94
|
+
not met.
|
|
95
|
+
|
|
96
|
+
DISCLAIMER
|
|
97
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
98
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
99
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
100
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
101
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
102
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
103
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
104
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
105
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
package/public/styles.css
CHANGED
|
@@ -62,6 +62,19 @@
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/* Montserrat, self-hosted: one variable file, latin subset, from this origin.
|
|
66
|
+
Headings and the brand mark only — the tables, chips and numbers stay on the
|
|
67
|
+
system UI font, which is what a dense data view is designed around. The
|
|
68
|
+
fallback stack is what renders if the file is missing, so nothing breaks. */
|
|
69
|
+
@font-face {
|
|
70
|
+
font-family: "Montserrat";
|
|
71
|
+
src: url("/fonts/montserrat-latin-var.woff2") format("woff2-variations"),
|
|
72
|
+
url("/fonts/montserrat-latin-var.woff2") format("woff2");
|
|
73
|
+
font-weight: 100 900;
|
|
74
|
+
font-style: normal;
|
|
75
|
+
font-display: swap;
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
* { box-sizing: border-box; }
|
|
66
79
|
body {
|
|
67
80
|
margin: 0;
|
|
@@ -72,7 +85,7 @@ body {
|
|
|
72
85
|
line-height: 1.55;
|
|
73
86
|
}
|
|
74
87
|
main { max-width: 1480px; margin: 0 auto; padding: 28px 24px 80px; }
|
|
75
|
-
h1, h2, h3 { line-height: 1.2; letter-spacing: -0.01em; }
|
|
88
|
+
h1, h2, h3 { font-family: "Montserrat", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.2; letter-spacing: -0.01em; }
|
|
76
89
|
h1 { font-size: 1.8rem; font-weight: 800; margin: 0.5em 0; }
|
|
77
90
|
h2 { font-size: 1.05rem; font-weight: 700; margin: 2em 0 0.6em; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
78
91
|
h2 small { font-weight: 500; color: var(--muted); font-size: 0.75rem; text-transform: none; letter-spacing: 0; }
|
|
@@ -544,6 +557,10 @@ footer { max-width: 1480px; margin: 0 auto; padding: 16px 24px 40px; color: var(
|
|
|
544
557
|
.idx-help { color: var(--muted); font-size: 0.82rem; margin-left: 8px; }
|
|
545
558
|
.idx-stale { color: var(--muted); font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
546
559
|
.idx-sitemap { margin: -4px 0 8px; font-size: 0.78rem; color: var(--muted); }
|
|
560
|
+
.idx-fix { margin: 6px 0 2px 8px; font-size: 0.8rem; color: var(--muted); line-height: 1.55; max-width: 96ch; }
|
|
561
|
+
.idx-ri { display: inline-block; font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; border-radius: 4px; padding: 1px 6px; margin-right: 4px; white-space: nowrap; }
|
|
562
|
+
.idx-ri.yes { background: var(--good-soft); color: var(--good); }
|
|
563
|
+
.idx-ri.no { background: var(--bad-soft); color: var(--bad); }
|
|
547
564
|
|
|
548
565
|
/* ---- settings ---- */
|
|
549
566
|
.settings-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; }
|
package/src/server.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { ROOT, PORT, siteByHost, dotEnv } from "./config.js";
|
|
8
|
+
import { ROOT, INSTANCE, PORT, siteByHost, dotEnv } from "./config.js";
|
|
9
9
|
import { draftBySlug, campaignBySlug } from "./data.js";
|
|
10
10
|
import { allActions } from "./actions.js";
|
|
11
11
|
import { acceptProposal, setWatching, retire } from "./backlog.js";
|
|
@@ -94,6 +94,20 @@ app.get("/styles.css", (c) => {
|
|
|
94
94
|
return c.text(css, 200, { "Content-Type": "text/css; charset=utf-8" });
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
// The one webfont the dashboard uses, served from this origin. Self-hosting
|
|
98
|
+
// keeps the dashboard free of third-party requests — it renders on machines
|
|
99
|
+
// with no outbound internet, and it never tells Google when someone opens it.
|
|
100
|
+
app.get("/fonts/:file", (c) => {
|
|
101
|
+
const file = c.req.param("file");
|
|
102
|
+
if (!/^[\w.-]+\.woff2$/.test(file)) return c.notFound();
|
|
103
|
+
const abs = path.join(ROOT, "public", "fonts", file);
|
|
104
|
+
if (!fs.existsSync(abs)) return c.notFound();
|
|
105
|
+
return c.body(fs.readFileSync(abs), 200, {
|
|
106
|
+
"Content-Type": "font/woff2",
|
|
107
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
97
111
|
app.get("/favicon.svg", (c) => {
|
|
98
112
|
const svg = fs.readFileSync(path.join(ROOT, "public", "favicon.svg"), "utf8");
|
|
99
113
|
return c.text(svg, 200, { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=86400" });
|
|
@@ -257,4 +271,11 @@ app.post("/api/backlog/:id/retire", (c) => {
|
|
|
257
271
|
const HOST = process.env.SEO_HOST ?? "127.0.0.1";
|
|
258
272
|
serve({ fetch: app.fetch, port: PORT, hostname: HOST }, (info) => {
|
|
259
273
|
console.log(`n-seo → http://${HOST === "0.0.0.0" ? "localhost" : HOST}:${info.port}`);
|
|
274
|
+
// Printed once per start, and only where it is actionable: an instance that
|
|
275
|
+
// has the skills installed. A running dashboard says nothing about how to
|
|
276
|
+
// get the next thing done, and most people should be asking rather than
|
|
277
|
+
// reading.
|
|
278
|
+
if (fs.existsSync(path.join(INSTANCE, ".claude", "skills", "n-seo-triage", "SKILL.md"))) {
|
|
279
|
+
console.log(` ask it: cd ${INSTANCE} && claude → "what should I work on today?"`);
|
|
280
|
+
}
|
|
260
281
|
});
|
package/src/views.tsx
CHANGED
|
@@ -652,13 +652,60 @@ const COVERAGE_ORDER = [
|
|
|
652
652
|
"Crawled - currently not indexed",
|
|
653
653
|
];
|
|
654
654
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
655
|
+
/** What each verdict means and — separately — what to actually do about it.
|
|
656
|
+
*
|
|
657
|
+
* These are not the same action, and treating them as one is the common
|
|
658
|
+
* mistake. "Request Indexing" only helps where Google has *not yet judged*
|
|
659
|
+
* the page: unknown or discovered-never-crawled. Once Google has fetched a
|
|
660
|
+
* URL and declined it (soft 404, crawled-not-indexed), another request
|
|
661
|
+
* re-runs the same judgement on the same content and spends a slot of a
|
|
662
|
+
* quota that is roughly ten URLs a day. Those need a page change first. */
|
|
663
|
+
const COVERAGE_HELP: Record<string, { means: string; fix: string }> = {
|
|
664
|
+
"URL is unknown to Google": {
|
|
665
|
+
means: "Not discovered at all — Google has never seen this URL.",
|
|
666
|
+
fix: "Discovery problem, not a content one: confirm it is in the sitemap Search Console actually read, and link to it from a page that is already indexed. Then request indexing.",
|
|
667
|
+
},
|
|
668
|
+
"Discovered - currently not indexed": {
|
|
669
|
+
means: "Known but never fetched — it is sitting in the crawl queue.",
|
|
670
|
+
fix: "Request Indexing (manual, in the Search Console UI) is the right move here and usually works within hours. Spend the daily quota on these first.",
|
|
671
|
+
},
|
|
672
|
+
"Soft 404": {
|
|
673
|
+
means: "Google fetched it, got a 200, and read the response as an error or an empty page.",
|
|
674
|
+
fix: "Do NOT just request indexing — Google already judged this content and would judge it the same way again. Fetch the URL and find the cause: content rendered only in JavaScript (the crawler sees an empty shell), a genuinely thin page, or an error page returning 200 that should return 404 or 410 and leave the sitemap. Fix it, then request indexing.",
|
|
675
|
+
},
|
|
676
|
+
"Crawled - currently not indexed": {
|
|
677
|
+
means: "Google fetched it and declined to index it — a judgement about value.",
|
|
678
|
+
fix: "Requesting indexing re-submits the same page for the same verdict. Either make it substantially better and more distinct from your other pages, or accept it as not worth indexing and drop it from the sitemap.",
|
|
679
|
+
},
|
|
680
|
+
"Page with redirect": {
|
|
681
|
+
means: "The URL redirects, so the destination is what gets indexed.",
|
|
682
|
+
fix: "Expected for an old URL. If it is in your sitemap, remove it — sitemaps should list final URLs only.",
|
|
683
|
+
},
|
|
684
|
+
"Alternate page with proper canonical tag": {
|
|
685
|
+
means: "Correctly pointing at a canonical elsewhere. Not a problem.",
|
|
686
|
+
fix: "No action. Remove it from the sitemap if you want the report clean.",
|
|
687
|
+
},
|
|
688
|
+
"Duplicate without user-selected canonical": {
|
|
689
|
+
means: "Google picked a different URL as canonical because it reads as a duplicate.",
|
|
690
|
+
fix: "Set an explicit canonical, or differentiate the content. Requesting indexing does not change a duplicate judgement.",
|
|
691
|
+
},
|
|
692
|
+
"Blocked by robots.txt": {
|
|
693
|
+
means: "Crawling is disallowed, so it cannot be indexed.",
|
|
694
|
+
fix: "Either remove the Disallow rule, or remove the URL from the sitemap — asking for a URL you block is a contradiction.",
|
|
695
|
+
},
|
|
696
|
+
"Excluded by 'noindex' tag": {
|
|
697
|
+
means: "The page asks not to be indexed and Google is obeying.",
|
|
698
|
+
fix: "If that is intentional, drop it from the sitemap. If not, remove the noindex tag, then request indexing.",
|
|
699
|
+
},
|
|
660
700
|
};
|
|
661
701
|
|
|
702
|
+
/** True where re-submitting the URL is a reasonable use of the daily quota:
|
|
703
|
+
* Google has not yet formed a verdict on the content. */
|
|
704
|
+
const REQUEST_INDEXING_HELPS = new Set([
|
|
705
|
+
"URL is unknown to Google",
|
|
706
|
+
"Discovered - currently not indexed",
|
|
707
|
+
]);
|
|
708
|
+
|
|
662
709
|
const STALE_DAYS = 90;
|
|
663
710
|
|
|
664
711
|
/** A verdict is only as current as the crawl behind it. A URL can read as
|
|
@@ -760,7 +807,24 @@ export const IndexingPage: FC = () => {
|
|
|
760
807
|
<tr class="idx-group">
|
|
761
808
|
<td colspan={2}>
|
|
762
809
|
<strong>{header}</strong>{" "}
|
|
763
|
-
<span class="idx-help">{COVERAGE_HELP[header] ?? p.detail ?? ""}</span>
|
|
810
|
+
<span class="idx-help">{COVERAGE_HELP[header]?.means ?? p.detail ?? ""}</span>
|
|
811
|
+
{COVERAGE_HELP[header] && (
|
|
812
|
+
<div class="idx-fix">
|
|
813
|
+
<span
|
|
814
|
+
class={`idx-ri ${REQUEST_INDEXING_HELPS.has(header) ? "yes" : "no"}`}
|
|
815
|
+
title={
|
|
816
|
+
REQUEST_INDEXING_HELPS.has(header)
|
|
817
|
+
? "Google has not judged this content yet — a request moves it"
|
|
818
|
+
: "Google already fetched and judged this page; re-requesting repeats the same verdict and spends quota"
|
|
819
|
+
}
|
|
820
|
+
>
|
|
821
|
+
{REQUEST_INDEXING_HELPS.has(header)
|
|
822
|
+
? "Request Indexing helps"
|
|
823
|
+
: "Request Indexing will not fix this"}
|
|
824
|
+
</span>{" "}
|
|
825
|
+
{COVERAGE_HELP[header].fix}
|
|
826
|
+
</div>
|
|
827
|
+
)}
|
|
764
828
|
</td>
|
|
765
829
|
</tr>
|
|
766
830
|
)}
|