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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-seo-add-site
|
|
3
|
+
description: Add one site to an n-seo instance correctly — the Search Console property form, gscHost, the numeric GA4 id, a brand regex, and the two access grants. Use whenever someone wants a new site tracked.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add a site
|
|
7
|
+
|
|
8
|
+
One entry in `n-seo.config.json`. There are no per-script site lists, so this
|
|
9
|
+
is the only edit. `docs/ADDING-A-SITE.md` is the reference; this is the order
|
|
10
|
+
to do it in.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
E=<engine checkout>/bin/n-seo.mjs
|
|
14
|
+
I=<instance dir>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 1. Collect four facts
|
|
18
|
+
|
|
19
|
+
Ask for whatever you cannot see, and do not guess any of them.
|
|
20
|
+
|
|
21
|
+
**The property, exactly as Search Console shows it.** This is where most
|
|
22
|
+
mistakes happen:
|
|
23
|
+
|
|
24
|
+
| In Search Console | `gscProperty` |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Domain property `example.com` | `sc-domain:example.com` |
|
|
27
|
+
| URL-prefix property | `https://www.example.com/` — scheme and **trailing slash** included |
|
|
28
|
+
|
|
29
|
+
**Whether the property covers more than this site.** A `sc-domain:` property
|
|
30
|
+
covers every subdomain and both protocols. So:
|
|
31
|
+
|
|
32
|
+
- One site on its own domain property → `gscHost` can be omitted.
|
|
33
|
+
- Several sites sharing one domain property → every entry gets the **same**
|
|
34
|
+
`gscProperty` and a **different** `gscHost`. The pull happens once per
|
|
35
|
+
property; each site's rows are filtered by `gscHost` when read.
|
|
36
|
+
- The site serves on `www` and the apex redirects → `gscHost` is
|
|
37
|
+
`www.example.com` even though `host` is `example.com`. Getting this wrong
|
|
38
|
+
means the probe and the metadata audit look at the wrong hostname and the
|
|
39
|
+
site silently shows no data.
|
|
40
|
+
|
|
41
|
+
**The numeric GA4 property id.** GA4 → Admin → Property details → Property
|
|
42
|
+
ID, nine or ten digits. It is **not** the `G-XXXXXXX` measurement id. Leave
|
|
43
|
+
`ga4Property` empty for a site with no analytics; it will still be probed and
|
|
44
|
+
pulled from Search Console.
|
|
45
|
+
|
|
46
|
+
**A brand regex.** Case-insensitive, matching what people type when they mean
|
|
47
|
+
this site by name, used for the branded/generic split on `/insights`. For
|
|
48
|
+
`example.com` something like `example|exmpl`. Omit it only if the site has no
|
|
49
|
+
brand terms — a site with a `gscProperty` and no `brand` counts **all** of
|
|
50
|
+
its traffic as generic, which quietly skews that split.
|
|
51
|
+
|
|
52
|
+
## 2. Add the entry
|
|
53
|
+
|
|
54
|
+
Append to `sites` in `$I/n-seo.config.json`:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"host": "docs.example.com",
|
|
59
|
+
"label": "docs",
|
|
60
|
+
"gscProperty": "sc-domain:example.com",
|
|
61
|
+
"gscHost": "docs.example.com",
|
|
62
|
+
"ga4Property": "987654321",
|
|
63
|
+
"brand": "example|exmpl",
|
|
64
|
+
"repo": "../example-docs",
|
|
65
|
+
"hosting": "GitHub Pages"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`label`, `repo` and `hosting` are display-only. Keep the JSON valid — the
|
|
70
|
+
dashboard falls back to the last good config and logs a parse error otherwise.
|
|
71
|
+
|
|
72
|
+
## 3. Grant access to the service account
|
|
73
|
+
|
|
74
|
+
Both consoles, using the service-account email (`node $E doctor --instance
|
|
75
|
+
"$I"` prints it, or read `client_email` from the key file):
|
|
76
|
+
|
|
77
|
+
- **Search Console** → the new property → Settings → Users and permissions →
|
|
78
|
+
Add user → **Full**.
|
|
79
|
+
- **GA4** → Admin → Property access management → **Viewer**. If the service
|
|
80
|
+
account is already Viewer at the *account* level, a new property under that
|
|
81
|
+
account inherits it and there is nothing to do.
|
|
82
|
+
|
|
83
|
+
On `metadata` auth the same grants apply to the runtime service account.
|
|
84
|
+
|
|
85
|
+
If Search Console's Add-user dialog rejects a service-account address on a
|
|
86
|
+
domain property, the fallback is the Site Verification API and a DNS TXT
|
|
87
|
+
record, described in `docs/SETUP-GOOGLE.md`.
|
|
88
|
+
|
|
89
|
+
## 4. Verify
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
node $E doctor --instance "$I"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The new property must appear under `search console` with a permission level,
|
|
96
|
+
and its GA4 id must resolve. A missing property means the grant has not
|
|
97
|
+
landed yet (give it a few minutes) or the property string does not match.
|
|
98
|
+
|
|
99
|
+
Then let the next scheduled run pick it up, or run it now:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
node $E daily --instance "$I"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 5. Confirm it landed
|
|
106
|
+
|
|
107
|
+
- The sites strip on `/` lists it.
|
|
108
|
+
- `/site/<host>` exists and the probe pills render.
|
|
109
|
+
- `/trends` shows it — a brand-new site gets a flat "no traffic recorded yet"
|
|
110
|
+
row rather than being silently absent, so an empty row is a connected site,
|
|
111
|
+
not a broken one.
|
|
112
|
+
|
|
113
|
+
Zero rows from Search Console on a new site is correct, not a fault: there is
|
|
114
|
+
no search history yet.
|
|
115
|
+
|
|
116
|
+
## Optional: watch a page
|
|
117
|
+
|
|
118
|
+
`watchPages` is a flat list of URLs whose numbers appear in every daily-log
|
|
119
|
+
entry. Add pages you are about to change so the log tracks them.
|
|
120
|
+
|
|
121
|
+
## Removing a site
|
|
122
|
+
|
|
123
|
+
Delete the entry. Its `data/` stays until removed. Backlog items for that host
|
|
124
|
+
in `config/backlog.json` still render on `/actions` — retire them with the
|
|
125
|
+
Retire button or via `POST /api/backlog/<id>/retire`.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-seo-deploy
|
|
3
|
+
description: Deploy n-seo to a host that is always on — a VPS, a NAS, or a GCE VM — with the dashboard reachable only through a tunnel and the static mirror behind sign-in. Use when someone wants n-seo off their laptop, running on a schedule, or asks how to host it.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deploy n-seo to an always-on host
|
|
7
|
+
|
|
8
|
+
Read `docs/DEPLOY.md` for the reasoning. This is the procedure.
|
|
9
|
+
|
|
10
|
+
## Rules you may not break
|
|
11
|
+
|
|
12
|
+
1. **Never publish the dashboard's port to the internet, and never suggest
|
|
13
|
+
it.** It has write endpoints, and its Settings page sets the LLM command
|
|
14
|
+
the daily run executes — reaching it is running code on the host. Access
|
|
15
|
+
is a tunnel (IAP, `ssh -L`, Tailscale) or a proxy that authenticates
|
|
16
|
+
first. If the operator asks for a public dashboard, say what it exposes
|
|
17
|
+
and offer the mirror or the Caddy overlay instead.
|
|
18
|
+
2. **Never print, echo, cat or commit a secret** — API keys, service-account
|
|
19
|
+
JSON, `.env` contents. Reference paths, not values.
|
|
20
|
+
3. **Do not create paid cloud resources without explicit confirmation.** Run
|
|
21
|
+
`./setup.sh --dry-run` first and show the operator what it would create.
|
|
22
|
+
4. **Do not invent gcloud flags.** If unsure, `gcloud <group> <cmd> --help`.
|
|
23
|
+
|
|
24
|
+
## 1. Establish what they have
|
|
25
|
+
|
|
26
|
+
Ask only what you cannot detect. Detect what you can:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
command -v docker gcloud git node python3
|
|
30
|
+
gcloud config get-value project 2>/dev/null
|
|
31
|
+
uname -s
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
You need to know, in this order:
|
|
35
|
+
|
|
36
|
+
- **Where should it run?** An existing Linux box they can SSH to, a new GCE
|
|
37
|
+
VM, or something else with Docker.
|
|
38
|
+
- **Is this a fresh instance, or an existing one to move?** If moving, the
|
|
39
|
+
instance directory comes with them and step 4 becomes a copy, not an
|
|
40
|
+
`init`.
|
|
41
|
+
- **Is the mirror wanted?** A read-only static copy others can see. Needs a
|
|
42
|
+
bucket plus something to serve it.
|
|
43
|
+
- **On GCP already?** Decides `auth: "metadata"` (no key file) versus a
|
|
44
|
+
mounted key.
|
|
45
|
+
|
|
46
|
+
## 2. Pick the shape
|
|
47
|
+
|
|
48
|
+
| They said | Do |
|
|
49
|
+
|---|---|
|
|
50
|
+
| "I have a VPS / NAS / server" | `deploy/vm/startup.sh` on that box |
|
|
51
|
+
| "We're on GCP" | `deploy/gcp/setup.sh`, then the VM steps |
|
|
52
|
+
| "Cloud Run" | Explain why the engine cannot: `export_static.py` finishes with an atomic directory rename and GCS FUSE has none. Offer the VM, and Cloud Run for the mirror only. |
|
|
53
|
+
| "Kubernetes" | Not scaffolded. The image and the volume contract are in `docker/README.md`; one Deployment plus one CronJob sharing a PVC is the shape. |
|
|
54
|
+
|
|
55
|
+
## 3. Run the scaffolding
|
|
56
|
+
|
|
57
|
+
**Any Linux box** (as root on that box):
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
curl -fsSLO https://raw.githubusercontent.com/en-dash-consulting/n-seo/main/deploy/vm/startup.sh
|
|
61
|
+
sudo bash startup.sh
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**GCP** (from a checkout, on their workstation):
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
gcloud config set project THEIR_PROJECT
|
|
68
|
+
cd deploy/gcp
|
|
69
|
+
./setup.sh --dry-run # show them this output, get a yes
|
|
70
|
+
./setup.sh
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`setup.sh` creates the service account with Token Creator on itself, the
|
|
74
|
+
private mirror bucket, a data disk, a VM with no external IP, an IAP-only
|
|
75
|
+
firewall, and the Cloud Run mirror. It prints the manual steps at the end —
|
|
76
|
+
keep them for step 6.
|
|
77
|
+
|
|
78
|
+
## 4. The instance
|
|
79
|
+
|
|
80
|
+
Fresh:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
sudo -u n-seo /opt/n-seo/manage init
|
|
84
|
+
sudo -u n-seo vi /srv/n-seo/instance/n-seo.config.json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Moving an existing one: copy the directory to `/srv/n-seo/instance`
|
|
88
|
+
(excluding `data/`, which regenerates), keep ownership as the `n-seo` user,
|
|
89
|
+
and confirm `n-seo.config.json`, `config/`, `content/` arrived.
|
|
90
|
+
|
|
91
|
+
Config points to settle, in order of how often they are wrong:
|
|
92
|
+
|
|
93
|
+
- `sites[]` — `gscProperty` is `sc-domain:example.com` for a domain property
|
|
94
|
+
or `https://example.com/` for a url-prefix one. `ga4Property` is the
|
|
95
|
+
numeric id from GA4 Admin → Property details, not the `G-` tag.
|
|
96
|
+
- `google.auth` — `"metadata"` on GCP (no key). Anywhere else,
|
|
97
|
+
`"service-account-key"` with the key mounted read-only.
|
|
98
|
+
- `modules.llm` — on a headless host the `claude -p` CLI usually is not
|
|
99
|
+
there. Use the HTTP path with `apiKeyEnv` and put the key in the
|
|
100
|
+
instance's `.env`, or leave the module off and accept no proposals or
|
|
101
|
+
briefings.
|
|
102
|
+
- `modules.staticExport` plus an `afterRun` hook if they want the mirror.
|
|
103
|
+
- `port` — leave it. The container publishes to loopback only.
|
|
104
|
+
|
|
105
|
+
## 5. Verify, in this order
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
sudo -u n-seo /opt/n-seo/manage doctor
|
|
109
|
+
sudo -u n-seo /opt/n-seo/manage daily --only probe # no credentials needed
|
|
110
|
+
sudo -u n-seo /opt/n-seo/manage daily # the real run
|
|
111
|
+
sudo cat /srv/n-seo/instance/data/last-run.json # failures should be ""
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`doctor` failing on Search Console or GA4 before step 6 is expected — the
|
|
115
|
+
console grants have not happened yet. Everything else should pass.
|
|
116
|
+
|
|
117
|
+
Then confirm the dashboard, through a tunnel and never otherwise:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
# GCP
|
|
121
|
+
gcloud compute start-iap-tunnel n-seo 4600 --local-host-port=localhost:4600 --zone ZONE
|
|
122
|
+
# anywhere
|
|
123
|
+
ssh -N -L 4600:localhost:4600 user@host
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:4600/
|
|
128
|
+
curl -s http://localhost:4600/api/actions | head -c 200
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 6. Hand over what only they can do
|
|
132
|
+
|
|
133
|
+
These are console steps. List them explicitly; do not pretend they are done.
|
|
134
|
+
|
|
135
|
+
1. **Search Console** → Settings → Users and permissions → add the service
|
|
136
|
+
account address with **Full**. If the UI rejects it on a domain property,
|
|
137
|
+
point them at `docs/SETUP-GOOGLE.md` for the DNS-verification route.
|
|
138
|
+
2. **GA4** → Admin → Property access management → add the same address as
|
|
139
|
+
**Viewer**.
|
|
140
|
+
3. **IAP members**, if the mirror is deployed:
|
|
141
|
+
```sh
|
|
142
|
+
gcloud beta iap web add-iam-policy-binding \
|
|
143
|
+
--resource-type=cloud-run --service=n-seo-mirror --region=REGION \
|
|
144
|
+
--member='user:someone@example.com' \
|
|
145
|
+
--role='roles/iap.httpsResourceAccessor'
|
|
146
|
+
```
|
|
147
|
+
4. **Re-run `manage doctor`** once those land. It should be all green.
|
|
148
|
+
|
|
149
|
+
## 7. Leave it healthy
|
|
150
|
+
|
|
151
|
+
- Confirm the scheduler service is up: `sudo -u n-seo /opt/n-seo/manage ps`.
|
|
152
|
+
The daily time is `N_SEO_DAILY_AT` in `/srv/n-seo/.env` (default 07:00 in
|
|
153
|
+
the host's `TZ`).
|
|
154
|
+
- Tell them where to look when it breaks: `/logs` on the dashboard,
|
|
155
|
+
`data/last-run.json`, `manage logs`.
|
|
156
|
+
- Tell them the upgrade command: `sudo -u n-seo /opt/n-seo/manage upgrade`.
|
|
157
|
+
- Recommend the instance live in its own git repo (`docs/INSTANCE.md`) —
|
|
158
|
+
that is the backup, and `gitAutoCommit` keeps it current.
|
|
159
|
+
- If they want failure alerts on a server, `modules.notifications` is
|
|
160
|
+
macOS-only; offer the `afterRun` webhook hook in `docs/DEPLOY.md`.
|
|
161
|
+
|
|
162
|
+
## Troubleshooting
|
|
163
|
+
|
|
164
|
+
| Symptom | Cause |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `doctor` says the metadata token was rejected | the SA lacks `roles/iam.serviceAccountTokenCreator` **on itself**; `setup.sh` grants it, re-run that binding |
|
|
167
|
+
| Search Console 403 on every property | the SA was never added as a user in the console (step 6.1) |
|
|
168
|
+
| GA4 returns no rows | `ga4Property` is the `G-` tag instead of the numeric property id |
|
|
169
|
+
| Dashboard unreachable through the tunnel | container not up (`manage ps`), or the tunnel points at the wrong port |
|
|
170
|
+
| Scan and digests produce nothing | `modules.llm` is off, or the CLI is missing on a headless host — use the HTTP path |
|
|
171
|
+
| Mirror is empty | `staticExport` off, or the `afterRun` rsync hook missing or failing (check `data/last-run.json`) |
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-seo-review
|
|
3
|
+
description: The weekly n-seo pass — what shipped, what moved, judge each watching item succeeded/failed/still cooking, retire what is done, refresh the insights briefing. Use for a weekly or monthly review.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Weekly review
|
|
7
|
+
|
|
8
|
+
The loop only closes here. Shipped work sits in *watching* until someone
|
|
9
|
+
reads the data and says whether it worked.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
E=<engine checkout>/bin/n-seo.mjs
|
|
13
|
+
I=<instance dir>
|
|
14
|
+
PORT=$(python3 -c "import json;print(json.load(open('$I/n-seo.config.json'))['port'])" 2>/dev/null || echo 4600)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Read the outputs; do not re-run the pipeline to review it.
|
|
18
|
+
|
|
19
|
+
## 1. Gather
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
cat "$I/data/last-run.json" # were the runs healthy this week
|
|
23
|
+
sed -n '/^## /,$p' "$I/docs/daily-log.md" | tail -120 # the week's entries
|
|
24
|
+
curl -s "http://localhost:$PORT/api/actions" # or MCP list_actions status=watching
|
|
25
|
+
cat "$I/data/opportunity-proposals.json" # verdicts + proposals
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Also useful: MCP `trends_timeseries` for a page-level before/after, and
|
|
29
|
+
`git log --oneline --since='1 week ago'` in each site repo for what actually
|
|
30
|
+
shipped.
|
|
31
|
+
|
|
32
|
+
## 2. Judge every watching item
|
|
33
|
+
|
|
34
|
+
For each item with a `watching` note, find its page in the daily-log lines
|
|
35
|
+
across the week and compare against the note's own success criterion — CTR,
|
|
36
|
+
position, impressions, sessions, engagement, whichever the card named.
|
|
37
|
+
|
|
38
|
+
Assign one of three, and write the evidence next to it:
|
|
39
|
+
|
|
40
|
+
- **Succeeded** — the criterion is met. Say by how much.
|
|
41
|
+
- **Failed** — the window has passed and the number did not move, or moved the
|
|
42
|
+
wrong way. Say what the data shows and propose the next move; a failed
|
|
43
|
+
metadata change is a candidate for a different angle, not a repeat.
|
|
44
|
+
- **Still cooking** — the 28-day window has not closed, or impressions are too
|
|
45
|
+
thin to read. Leave it and note when to look again.
|
|
46
|
+
|
|
47
|
+
The scan's own verdicts in `opportunity-proposals.json` are *review triggers*,
|
|
48
|
+
not conclusions. Read them, then judge from the data yourself.
|
|
49
|
+
|
|
50
|
+
Beware a page whose demand collapsed for reasons unrelated to the change —
|
|
51
|
+
falling query volume in the trends file means the fix can be fine while the
|
|
52
|
+
number still drops. Say that rather than scoring it failed.
|
|
53
|
+
|
|
54
|
+
## 3. Retire what is done
|
|
55
|
+
|
|
56
|
+
Only after it is judged succeeded, and only when there is nothing left to
|
|
57
|
+
watch:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
curl -sX POST "http://localhost:$PORT/api/backlog/<id>/retire"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For a *failed* item, do not retire it. Either update its `watching` note with
|
|
64
|
+
what you learned and the next move, or replace it with a new card carrying
|
|
65
|
+
that evidence.
|
|
66
|
+
|
|
67
|
+
For rule-derived cards, remove or update the page's entry in `shippedWatch`
|
|
68
|
+
in `$I/config/backlog.json` — dropping the entry lets the card return as
|
|
69
|
+
active work if the underlying gap is still there, which is usually what you
|
|
70
|
+
want after a failure.
|
|
71
|
+
|
|
72
|
+
## 4. Sweep the rest
|
|
73
|
+
|
|
74
|
+
- **Proposals** — present each with its evidence; accept the good ones
|
|
75
|
+
(Accept on `/actions`, or `POST /api/backlog/accept` with `index`) and say
|
|
76
|
+
plainly which you are ignoring and why. Never accept silently.
|
|
77
|
+
- **Health** — probe regressions and index-coverage problems from the log.
|
|
78
|
+
Pages Google has never crawled are a different problem from pages nobody
|
|
79
|
+
searches for; the `/indexing` page separates them.
|
|
80
|
+
- **Freezes expiring** — list pages whose 28-day window closes in the coming
|
|
81
|
+
week. Those are next week's metadata budget.
|
|
82
|
+
- **Conversions** — if `conversions` is configured, whether events are
|
|
83
|
+
arriving (`conversions_status`).
|
|
84
|
+
|
|
85
|
+
## 5. Refresh the briefing if the picture changed
|
|
86
|
+
|
|
87
|
+
`$I/config/insights.json` is the hand-written narrative on `/insights`:
|
|
88
|
+
`{ "date": "YYYY-MM-DD", "insights": [ { "title", "verdict", "body": [], "move" } ] }`
|
|
89
|
+
with `verdict` one of `opportunity`, `warning`, `momentum`, `deprioritize`.
|
|
90
|
+
|
|
91
|
+
Rewrite it only when the picture actually changed — a new rising cluster, a
|
|
92
|
+
collapsing one, a bet that paid off. Update `date` when you do. Leave it alone
|
|
93
|
+
in a quiet week rather than churning it.
|
|
94
|
+
|
|
95
|
+
## 6. Write the summary
|
|
96
|
+
|
|
97
|
+
Short, for the owner:
|
|
98
|
+
|
|
99
|
+
- **Shipped this week** — what, and the verdict where the data allows one.
|
|
100
|
+
- **Moved** — the numbers that changed, with the caveat where demand shifted.
|
|
101
|
+
- **Next week** — the top few, with the metadata budget already applied.
|
|
102
|
+
- **Needs you** — proposals to accept, approvals, anything blocked.
|
|
103
|
+
|
|
104
|
+
Impact numbers order the queue; they are never reported as expected results.
|
|
105
|
+
Recalibrate them against what you just observed if a card's estimate was
|
|
106
|
+
clearly wrong.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-seo-setup
|
|
3
|
+
description: Take an n-seo install from a fresh clone to its first real daily run — config, Google service account, both console grants, doctor, first pull. Use when the dashboard is empty, doctor reports problems, or someone says they just cloned this.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Setup — clone to first real run
|
|
7
|
+
|
|
8
|
+
Goal: a green `doctor` and one completed `daily` run. Work top to bottom and
|
|
9
|
+
stop at each **ASK** — those are the owner's decisions, not yours.
|
|
10
|
+
|
|
11
|
+
Throughout, `E` is the engine and `I` is the instance:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
E=<engine checkout>/bin/n-seo.mjs # the directory holding src/ and bin/
|
|
15
|
+
I=<instance dir> # in-place mode: the same directory
|
|
16
|
+
node $E version --instance "$I" # prints engine, instance, and mode
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If `n-seo` is on PATH (`npm link` in the engine), `n-seo <cmd>` works instead.
|
|
20
|
+
|
|
21
|
+
## 1. Establish the layout
|
|
22
|
+
|
|
23
|
+
Run `node $E version --instance "$I"`. It prints `mode: in-place` or
|
|
24
|
+
`mode: instance`.
|
|
25
|
+
|
|
26
|
+
- **in-place** — the checkout is the instance. Config lives beside `src/`.
|
|
27
|
+
- **instance** — config lives in a separate directory. If that directory is
|
|
28
|
+
empty, scaffold it: `node $E init "$I"`, which never overwrites.
|
|
29
|
+
|
|
30
|
+
`docs/INSTANCE.md` in the engine explains the split if the owner asks.
|
|
31
|
+
|
|
32
|
+
## 2. Offer the demo first when there is no Google access yet
|
|
33
|
+
|
|
34
|
+
If the owner does not have a service account (or does not want to make one
|
|
35
|
+
right now), do this before anything else so they see the tool working:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
node $E demo --instance "$I" # synthetic dataset for the configured sites
|
|
39
|
+
node $E start --instance "$I" # dashboard on the configured port
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Every page populates. Say plainly that this is synthetic data, that
|
|
43
|
+
`node $E demo --instance "$I" -- --clean` removes it, and that the real setup
|
|
44
|
+
resumes at step 3. **ASK** whether to continue to real data now.
|
|
45
|
+
|
|
46
|
+
## 3. Write the config
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
cp <engine>/n-seo.config.example.json "$I/n-seo.config.json"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Fill in `sites` first. For each site the owner names, collect:
|
|
53
|
+
|
|
54
|
+
| Field | Where it comes from |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `host` | the canonical hostname |
|
|
57
|
+
| `gscProperty` | Search Console, **exactly** as shown: `sc-domain:example.com` or `https://www.example.com/` with the trailing slash |
|
|
58
|
+
| `gscHost` | only when the property covers more hosts than this site, or the site serves on `www` |
|
|
59
|
+
| `ga4Property` | GA4 Admin → Property details → the numeric Property ID, not the `G-…` measurement id |
|
|
60
|
+
| `brand` | a case-insensitive regex matching their branded queries |
|
|
61
|
+
|
|
62
|
+
Use `/n-seo-add-site` for each site if there are several — it covers the
|
|
63
|
+
domain-vs-URL-prefix and shared-property cases properly. Set `port` if 4600
|
|
64
|
+
is taken.
|
|
65
|
+
|
|
66
|
+
## 4. Service account and the two grants
|
|
67
|
+
|
|
68
|
+
Full detail is `docs/SETUP-GOOGLE.md`; the shape is:
|
|
69
|
+
|
|
70
|
+
1. A Google Cloud project with the Search Console API, Google Analytics Data
|
|
71
|
+
API and Google Analytics Admin API enabled.
|
|
72
|
+
2. A service account with **no project roles**. Create a JSON key and store
|
|
73
|
+
it outside the repo:
|
|
74
|
+
```sh
|
|
75
|
+
mkdir -p ~/.config/n-seo
|
|
76
|
+
mv ~/Downloads/<downloaded>.json ~/.config/n-seo/service-account.json
|
|
77
|
+
chmod 600 ~/.config/n-seo/service-account.json
|
|
78
|
+
```
|
|
79
|
+
Point `google.serviceAccountKey` at that path.
|
|
80
|
+
3. **Search Console** → property → Settings → Users and permissions → Add
|
|
81
|
+
user → the service-account email → **Full**.
|
|
82
|
+
4. **GA4** → Admin → Property access management → Add users → the same email
|
|
83
|
+
→ **Viewer**. Account-level Viewer covers every property under it.
|
|
84
|
+
|
|
85
|
+
**ASK** before creating cloud resources or a key on the owner's behalf, and
|
|
86
|
+
never paste a key's contents into the transcript. If the install runs on GCP,
|
|
87
|
+
`google.auth: "metadata"` avoids the key file entirely — see
|
|
88
|
+
`docs/SETUP-GOOGLE.md`.
|
|
89
|
+
|
|
90
|
+
## 5. Doctor, and fix what it says
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
node $E doctor --instance "$I"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Work its output item by item; it names the fix for each. The common ones:
|
|
97
|
+
|
|
98
|
+
- *key file not found* — wrong path in `serviceAccountKey`, or the file is
|
|
99
|
+
not readable by this user.
|
|
100
|
+
- *a configured property is missing from the accessible list* — the grant in
|
|
101
|
+
step 4 has not landed. It can take a few minutes; if it never lands, the
|
|
102
|
+
property string does not match Search Console exactly.
|
|
103
|
+
- *`no ga4Property`* — expected for sites you deliberately left without GA4.
|
|
104
|
+
- *dashboard not running* — expected until step 7.
|
|
105
|
+
|
|
106
|
+
Do not continue while a FAIL remains.
|
|
107
|
+
|
|
108
|
+
## 6. First run
|
|
109
|
+
|
|
110
|
+
Start narrow, then go wide:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
node $E daily --instance "$I" --only probe # no auth, proves the config's hosts
|
|
114
|
+
node $E daily --instance "$I" # the full run
|
|
115
|
+
cat "$I/data/last-run.json" # failures should be ""
|
|
116
|
+
tail -20 "$I/docs/daily-log.md" # today's entry
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The index-coverage sweep dominates the wall time; several minutes is normal.
|
|
120
|
+
|
|
121
|
+
## 7. Dashboard, then scheduling
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
node $E start --instance "$I"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Open the configured port. Confirm the sites strip lists every configured site
|
|
128
|
+
and the queue has cards.
|
|
129
|
+
|
|
130
|
+
Then hand off scheduling. On this machine that is `docs/SCHEDULING.md` —
|
|
131
|
+
launchd, cron, systemd, and the installer script under `ops/templates/`. If it
|
|
132
|
+
should run on a server instead of a laptop, use `/n-seo-deploy`.
|
|
133
|
+
|
|
134
|
+
## Rules
|
|
135
|
+
|
|
136
|
+
- Never print or commit a key, a token, or a `.env` value.
|
|
137
|
+
- Do not enable publishing modules (`indexNow`, `staticExport`,
|
|
138
|
+
`gitAutoCommit`) during setup. They belong to a deliberate decision later.
|
|
139
|
+
- Read `docs/OPERATING-RULES.md` before recommending any change to a site.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-seo-ship
|
|
3
|
+
description: Implement one card from the n-seo queue in the site's own repo and record it as watching. Use when someone picks an action to do. Enforces the 28-day metadata freeze and the weekly batch limit.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ship one queue card
|
|
7
|
+
|
|
8
|
+
One card, one branch, one PR, then record it. If the owner has not named a
|
|
9
|
+
card, run `/n-seo-triage` first and let them choose.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
E=<engine checkout>/bin/n-seo.mjs
|
|
13
|
+
I=<instance dir>
|
|
14
|
+
PORT=$(python3 -c "import json;print(json.load(open('$I/n-seo.config.json'))['port'])" 2>/dev/null || echo 4600)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 1. Read the whole card
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
curl -s "http://localhost:$PORT/api/actions" | python3 -m json.tool
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or MCP `get_action` with a title substring. Use `spec` — it names the page,
|
|
24
|
+
the target queries, the shape of the change and the success criterion. Note
|
|
25
|
+
`id`, `host`, `tag` and whether it is rule-derived or curated (`source`).
|
|
26
|
+
|
|
27
|
+
## 2. Freeze and batch check — STOP HERE IF EITHER FAILS
|
|
28
|
+
|
|
29
|
+
Only for cards that change a title or meta description (`tag` of `metadata`
|
|
30
|
+
or `ctr-gap`, or any card whose `how` rewrites metadata):
|
|
31
|
+
|
|
32
|
+
1. **28-day freeze.** Look up the page in `shippedWatch` and in the dated
|
|
33
|
+
`watching` notes in `$I/config/backlog.json`. If its metadata changed
|
|
34
|
+
within 28 days, **stop and ask.** Say when the window opens. Iterating
|
|
35
|
+
inside it destroys the measurement the last change was making.
|
|
36
|
+
2. **Weekly batch.** Count metadata changes already recorded this week across
|
|
37
|
+
every site. At roughly eight, **stop and ask** before adding another.
|
|
38
|
+
|
|
39
|
+
Content additions, internal links, schema, and hygiene fixes are not metadata
|
|
40
|
+
changes and are not limited by either rule — but a page inside its freeze
|
|
41
|
+
still gets no title or description edit while you are in there.
|
|
42
|
+
|
|
43
|
+
## 3. Work in the site's repo, never in the instance
|
|
44
|
+
|
|
45
|
+
The site's code is in its own repository (`repo` on the site's page and in
|
|
46
|
+
the config). The instance holds config and content only.
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
cd <site repo>
|
|
50
|
+
git status --porcelain # if dirty, use a worktree; never touch uncommitted work
|
|
51
|
+
git checkout main && git pull
|
|
52
|
+
git checkout -b seo-<short-slug>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
If the checkout is dirty, `git worktree add ../<repo>-seo-<slug> -b seo-<slug>`
|
|
56
|
+
and work there instead.
|
|
57
|
+
|
|
58
|
+
Make exactly what the spec describes. For the common kinds:
|
|
59
|
+
|
|
60
|
+
- **Metadata fix / CTR gap** — the `<title>` restates the ranking query as its
|
|
61
|
+
answer, front-loaded, under about 60 characters. The description is one
|
|
62
|
+
~150-character sentence that answers the query directly. Metadata only.
|
|
63
|
+
- **Striking distance ("push to page 1")** — a section that answers the target
|
|
64
|
+
queries verbatim: an H2 phrased as the query, then a 40–60 word direct
|
|
65
|
+
answer, then detail. Two or three internal links to the page from related
|
|
66
|
+
pages using the query as anchor text.
|
|
67
|
+
- **Engagement mismatch** — rewrite the opening so it delivers what the click
|
|
68
|
+
promised; keep the URL.
|
|
69
|
+
- **Hygiene** — robots.txt, sitemap, `llms.txt`, real 404s, unblocking AI
|
|
70
|
+
crawlers, server-rendering the shell. Verify with
|
|
71
|
+
`node $E daily --instance "$I" --only probe` after deploy.
|
|
72
|
+
|
|
73
|
+
## 4. Verify before opening the PR
|
|
74
|
+
|
|
75
|
+
Run whatever the site repo uses (build, tests, lint). Check the rendered page
|
|
76
|
+
locally. Confirm the change is actually in the served HTML, not only in
|
|
77
|
+
source — the metadata audit reads the live page.
|
|
78
|
+
|
|
79
|
+
Push and open a PR whose description carries the card's evidence: the queries,
|
|
80
|
+
the numbers, the success criterion. That description is how the next daily run
|
|
81
|
+
gets attributed. Never push to `main` directly.
|
|
82
|
+
|
|
83
|
+
## 5. Record it — the step people skip
|
|
84
|
+
|
|
85
|
+
After it is merged and live:
|
|
86
|
+
|
|
87
|
+
**Mark the card watching** with a dated note saying what changed and what you
|
|
88
|
+
are waiting for:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
curl -sX POST "http://localhost:$PORT/api/backlog/<id>/watch" \
|
|
92
|
+
--data-urlencode "note=title rewritten for the widgets cluster, frozen until <date>, watching CTR"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The endpoint prefixes today's date. Rule-derived cards have no backlog entry
|
|
96
|
+
to write to — for those, add the page to `shippedWatch` in
|
|
97
|
+
`$I/config/backlog.json` instead, with the same dated note, so the card shows
|
|
98
|
+
as watching rather than reappearing as new work:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
"shippedWatch": {
|
|
102
|
+
"https://example.com/pricing": "2026-05-04: title rewritten, frozen until 2026-06-01, watching CTR"
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Add the page to `watchPages`** in `n-seo.config.json` if it is not there, so
|
|
107
|
+
its numbers appear in every daily-log entry.
|
|
108
|
+
|
|
109
|
+
Do not delete the card. Shipped work becomes watching; deleting it is how the
|
|
110
|
+
same page gets "fixed" again in six weeks by someone reading the same gap.
|
|
111
|
+
|
|
112
|
+
## 6. Report
|
|
113
|
+
|
|
114
|
+
Say what shipped, the PR link, the success criterion, and the date the freeze
|
|
115
|
+
lifts. The next runs decide whether it worked.
|
|
116
|
+
|
|
117
|
+
## Never
|
|
118
|
+
|
|
119
|
+
- Never edit `config/backlog.json` except to add a `shippedWatch` entry or via
|
|
120
|
+
the accept / watch / retire endpoints.
|
|
121
|
+
- Never touch a page inside its freeze window without the owner saying so.
|
|
122
|
+
- Never commit a secret, and never push to a site's `main`.
|