n-seo 0.1.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/.env.example +13 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/bin/n-seo.mjs +310 -0
- package/docs/ADDING-A-SITE.md +82 -0
- package/docs/ARCHITECTURE.md +213 -0
- package/docs/DEPLOY.md +300 -0
- package/docs/FAQ.md +93 -0
- package/docs/INSTANCE.md +365 -0
- package/docs/MCP.md +104 -0
- package/docs/OPERATING-RULES.md +106 -0
- package/docs/PLAYBOOK.md +122 -0
- package/docs/PRD.md +249 -0
- package/docs/RELEASING.md +189 -0
- package/docs/SCHEDULING.md +104 -0
- package/docs/SETUP-GOOGLE.md +215 -0
- package/docs/examples/campaign.json +59 -0
- package/docs/examples/draft.md +43 -0
- package/docs/screenshots/overview.png +0 -0
- 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/ingest/analyze_ga4.py +79 -0
- package/ingest/analyze_gsc.py +136 -0
- package/ingest/analyze_metadata.py +158 -0
- package/ingest/analyze_trends.py +145 -0
- package/ingest/google_auth.py +238 -0
- package/ingest/http_util.py +87 -0
- package/ingest/pull_ga4.py +107 -0
- package/ingest/pull_gsc.py +111 -0
- package/ingest/pull_index_status.py +179 -0
- package/ingest/pull_timeseries.py +130 -0
- package/ingest/seo_config.py +213 -0
- package/n-seo.config.example.json +110 -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/ops/daily.py +250 -0
- package/ops/daily_diff.py +151 -0
- package/ops/demo_data.py +529 -0
- package/ops/doctor.py +266 -0
- package/ops/export_static.py +125 -0
- package/ops/hn_digest.py +169 -0
- package/ops/indexnow.py +107 -0
- package/ops/install-launchd.sh +76 -0
- package/ops/llm.py +139 -0
- package/ops/mcp-smoke-stdio.mjs +61 -0
- package/ops/opportunity_scan.py +185 -0
- package/ops/publish.py +158 -0
- package/ops/reddit_digest.py +168 -0
- package/ops/templates/n-seo-daily.service +11 -0
- package/ops/templates/n-seo-daily.timer +11 -0
- package/ops/templates/n-seo-dashboard.service +15 -0
- package/ops/templates/n-seo.cron +3 -0
- package/ops/templates/n-seo.daily.plist +29 -0
- package/ops/templates/n-seo.dashboard.plist +22 -0
- package/package.json +77 -0
- package/probes/__pycache__/site_probe.cpython-313.pyc +0 -0
- package/probes/site_probe.py +201 -0
- package/public/favicon.svg +6 -0
- package/public/styles.css +632 -0
- package/src/actions.ts +255 -0
- package/src/backlog.ts +197 -0
- package/src/config.ts +220 -0
- package/src/data.ts +895 -0
- package/src/insights.ts +22 -0
- package/src/mcp-stdio.ts +21 -0
- package/src/mcp.ts +490 -0
- package/src/server.tsx +260 -0
- package/src/settings.tsx +329 -0
- package/src/views.tsx +1487 -0
- package/tsconfig.json +15 -0
package/docs/PLAYBOOK.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Search and answer-engine growth playbook
|
|
2
|
+
|
|
3
|
+
How to grow organic traffic across one or more sites with this tool: **SEO**
|
|
4
|
+
(classic search), **AEO** (answer engines — featured snippets, People Also
|
|
5
|
+
Ask, AI Overviews) and **GEO** (generative engines — ChatGPT, Claude,
|
|
6
|
+
Perplexity citing or recommending your pages). No paid ads assumed.
|
|
7
|
+
|
|
8
|
+
## The operating loop: four planes
|
|
9
|
+
|
|
10
|
+
1. **Awareness (data in).** Pull Search Console and GA4 on a schedule; probe
|
|
11
|
+
the live sites. Snapshot everything — rankings and AI-crawler behavior only
|
|
12
|
+
make sense as trends. This is `ops/daily.py`.
|
|
13
|
+
2. **Analysis.** Turn the snapshots into questions with answers: which
|
|
14
|
+
queries earn impressions but not clicks (title/description problem)? Which
|
|
15
|
+
pages rank 5–15 (small changes move them to page one)? Which sitemap URLs
|
|
16
|
+
has Google never fetched? Are AI crawlers allowed in? This is the action
|
|
17
|
+
engine, the metadata audit, the indexing sweep, and the trend analysis.
|
|
18
|
+
3. **Action.** Ship the change in the site's own repo: a rewritten title, an
|
|
19
|
+
answer-formatted section, structured data, internal links, an `llms.txt`.
|
|
20
|
+
Ping IndexNow, resubmit the sitemap, request indexing where it matters.
|
|
21
|
+
4. **Measurement.** The next day's pull scores the last change. Every
|
|
22
|
+
metadata change is dated so movement can be attributed, and stays in view
|
|
23
|
+
as *watching* until the data speaks.
|
|
24
|
+
|
|
25
|
+
The dashboard is the awareness plane; you (optionally with an AI agent reading
|
|
26
|
+
the MCP server) are analysis and action.
|
|
27
|
+
|
|
28
|
+
## What actually moves each discipline
|
|
29
|
+
|
|
30
|
+
### SEO — Google and Bing organic
|
|
31
|
+
|
|
32
|
+
- **Crawlability first.** Correct `robots.txt`, a complete `sitemap.xml` with
|
|
33
|
+
honest `lastmod`, real 404s (unknown paths return 404, not 200), one
|
|
34
|
+
canonical host (https, and one of www/apex), canonical tags. The probe
|
|
35
|
+
checks these daily; regressions are the cheapest thing to fix and the most
|
|
36
|
+
common way SEO dies on a deploy.
|
|
37
|
+
- **Server-rendered content.** If the raw HTML is a JavaScript shell, Google
|
|
38
|
+
mostly copes but Bing and every AI crawler see nothing. The probe reports
|
|
39
|
+
visible text bytes on the homepage; under ~500 bytes usually means a shell.
|
|
40
|
+
Static or server rendering is the single highest-leverage technical fix for
|
|
41
|
+
a client-rendered site.
|
|
42
|
+
- **Indexed before ranked.** A page can sit in the sitemap for months as
|
|
43
|
+
"Discovered — currently not indexed" and look identical to a page nobody
|
|
44
|
+
searches for (both show zero impressions). The `/indexing` page separates
|
|
45
|
+
the two. Spend manual "Request Indexing" quota on never-crawled pages that
|
|
46
|
+
matter; a resubmitted sitemap and internal links move the rest.
|
|
47
|
+
- **Striking distance.** Queries at position 5–15 with real impressions are
|
|
48
|
+
the best return per hour in SEO: add a section that answers the query
|
|
49
|
+
verbatim (H2 phrased as the query, then a 40–60 word direct answer), add
|
|
50
|
+
two or three internal links with the query as anchor text, update the
|
|
51
|
+
modified date.
|
|
52
|
+
- **Title and description CTR.** High impressions and low CTR at a good
|
|
53
|
+
position means the snippet is losing the click. Rewrite the title as the
|
|
54
|
+
answer to the query (front-load the query words, stay under ~60
|
|
55
|
+
characters); make the description one ~150-character sentence that
|
|
56
|
+
answers it. Then leave it alone for 28 days.
|
|
57
|
+
- **Internal linking.** Most small sites barely link within themselves or to
|
|
58
|
+
their sibling sites. Related-content blocks and footer cross-links pass
|
|
59
|
+
authority from the pages that have it to the ones that need it.
|
|
60
|
+
- **Core Web Vitals.** Matters at the margin. Check occasionally; don't
|
|
61
|
+
obsess.
|
|
62
|
+
|
|
63
|
+
### AEO — featured snippets, People Also Ask, AI Overviews
|
|
64
|
+
|
|
65
|
+
- **Answer-formatted pages.** A question as an H2, a 40–60 word direct
|
|
66
|
+
answer immediately below it, then elaboration. This is the shape snippets
|
|
67
|
+
and AI Overviews lift.
|
|
68
|
+
- **Structured data that fits.** `FAQPage`, `HowTo`, `Article`,
|
|
69
|
+
`SoftwareApplication`, `Dataset` — whichever the page honestly is. The
|
|
70
|
+
probe lists the JSON-LD types on each homepage.
|
|
71
|
+
- **Tables, ordered lists, definitions.** These get extracted far more often
|
|
72
|
+
than prose walls. A comparison table between the hero and the body of a
|
|
73
|
+
"X vs Y" page is a snippet magnet.
|
|
74
|
+
|
|
75
|
+
### GEO — being cited by ChatGPT, Claude, Perplexity
|
|
76
|
+
|
|
77
|
+
- **Let AI crawlers in.** `robots.txt` must not block GPTBot, ClaudeBot,
|
|
78
|
+
Claude-Web, PerplexityBot, Google-Extended, CCBot, Bytespider,
|
|
79
|
+
OAI-SearchBot — the probe flags any that are. Blocked crawlers cannot cite
|
|
80
|
+
you.
|
|
81
|
+
- **`llms.txt` and `llms-full.txt`.** A markdown summary of the site with
|
|
82
|
+
links to the pages that matter (and, in the full version, the content
|
|
83
|
+
itself). Regenerate on every publish. The probe checks both exist.
|
|
84
|
+
- **Be the canonical explainer for a niche.** Language models cite pages
|
|
85
|
+
that define or explain a thing plainly, with concrete numbers, code and
|
|
86
|
+
steps. Generic content does not get cited; firsthand specifics do.
|
|
87
|
+
- **Freshness.** Generative engines over-select recently updated pages. Real
|
|
88
|
+
content changes that move the modified date compound.
|
|
89
|
+
- **Measure it.** GA4 referral sessions from chatgpt.com, perplexity.ai,
|
|
90
|
+
claude.ai, copilot.microsoft.com, gemini.google.com — the dashboard shows
|
|
91
|
+
them per site and per month. This is the GEO scoreboard.
|
|
92
|
+
|
|
93
|
+
## Data plane
|
|
94
|
+
|
|
95
|
+
| Source | API | Auth | Gives you |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| Search Console | `searchconsole.googleapis.com` | service account (Full user on the property) | queries, impressions, CTR, position; sitemap state; per-URL index coverage |
|
|
98
|
+
| GA4 | Analytics Data API | service account (Viewer on the property) | sessions, sources (incl. AI referrals), landing-page engagement, conversion events |
|
|
99
|
+
| Live sites | plain HTTP | none | robots / sitemap / llms.txt / meta / SSR-ness / 404 behavior, daily |
|
|
100
|
+
| IndexNow | `api.indexnow.org` | a key you host at `/{key}.txt` | instant Bing/Copilot/Yandex pings on publish (optional module) |
|
|
101
|
+
| Hacker News, Reddit | public APIs | HN: none; Reddit: a free script app | threads where your expertise applies (optional modules; briefings only) |
|
|
102
|
+
|
|
103
|
+
## Content loops that work
|
|
104
|
+
|
|
105
|
+
- **Query gap → answer page.** The trend analysis surfaces rising queries no
|
|
106
|
+
page targets. Draft an answer-formatted page in the site repo, review,
|
|
107
|
+
ship, ping, watch.
|
|
108
|
+
- **Publish hook.** When a page ships: regenerate sitemap and `llms.txt`,
|
|
109
|
+
add internal links from related pages, ping IndexNow, request indexing if
|
|
110
|
+
it is a page that matters.
|
|
111
|
+
- **Regression sentinel.** The daily probe alerts when robots, sitemap,
|
|
112
|
+
canonical, `llms.txt` or server-rendered content silently breaks. Deploys
|
|
113
|
+
are where SEO dies; this is the smoke alarm.
|
|
114
|
+
- **Weekly review.** Read the *watching* list against the daily log. Items
|
|
115
|
+
whose 28 days are up either graduate (data moved: note it, retire it) or
|
|
116
|
+
become a new action (data didn't: what else could explain it?).
|
|
117
|
+
|
|
118
|
+
## What this tool will not do
|
|
119
|
+
|
|
120
|
+
It does not edit your sites, post to communities, send outreach, or promote
|
|
121
|
+
its own proposals into your queue. Those are the steps where judgment and
|
|
122
|
+
authenticity live, and they stay with you.
|
package/docs/PRD.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# n-seo — Product Requirements
|
|
2
|
+
|
|
3
|
+
This is the product definition the PRD tree in `.rex/` is generated from and
|
|
4
|
+
kept in sync with. It states what n-seo is for, who it serves, what it
|
|
5
|
+
deliberately does not do, and the capabilities that exist or are planned.
|
|
6
|
+
Each feature lists acceptance criteria in the form a test or a reviewer can
|
|
7
|
+
check. Status markers: **[shipped]**, **[planned]**, **[idea]**.
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
n-seo is a local-first control plane for organic growth across one or more
|
|
12
|
+
websites: classic search (SEO), answer engines (AEO) and generative engines
|
|
13
|
+
that cite sources (GEO). It replaces the monthly agency read-out with a loop
|
|
14
|
+
that runs every morning on the owner's machine: pull Search Console and GA4,
|
|
15
|
+
probe the live sites, turn the data into a ranked queue of concrete actions
|
|
16
|
+
with evidence attached, and measure yesterday's changes.
|
|
17
|
+
|
|
18
|
+
## Users
|
|
19
|
+
|
|
20
|
+
- **A site owner or small team** who wants to hone their own SEO practice
|
|
21
|
+
without paying an agency: developers, indie makers, consultancies, small
|
|
22
|
+
businesses with a technical person.
|
|
23
|
+
- **An operator running several sites** (a portfolio, an agency serving its
|
|
24
|
+
own clients) who needs one queue across all of them.
|
|
25
|
+
- **An AI agent** (Claude Code, Claude Desktop, any MCP client) acting for
|
|
26
|
+
the owner, which needs the same data in machine-readable form and must not
|
|
27
|
+
be able to bypass the owner's approval gates.
|
|
28
|
+
|
|
29
|
+
## Principles (non-negotiable)
|
|
30
|
+
|
|
31
|
+
1. **Local-first.** All data lives in files on the owner's machine. The only
|
|
32
|
+
outbound calls are to Google APIs the owner authorized and to a local LLM
|
|
33
|
+
command the owner configured.
|
|
34
|
+
2. **It briefs; it never acts on the owner's behalf.** No module posts,
|
|
35
|
+
sends, publishes, or edits a site. Community modules produce briefings,
|
|
36
|
+
never comment text.
|
|
37
|
+
3. **Evidence before advice.** Every action shows the numbers behind it,
|
|
38
|
+
then the move, then a spec and a success criterion.
|
|
39
|
+
4. **Encoded operating rules.** 28-day metadata freeze; ≈8 metadata changes
|
|
40
|
+
per week; decisions on the trailing 90 days, 16 months for history; impact
|
|
41
|
+
orders the queue and is never reported as a forecast; shipped work becomes
|
|
42
|
+
*watching*, never deleted; machine proposals never self-promote.
|
|
43
|
+
5. **Upgradeable.** An instance (config, queue, content, data) is separable
|
|
44
|
+
from the engine so an upgrade is a pull, not a merge.
|
|
45
|
+
6. **Stdlib Python, no-bundler TypeScript, no pip installs**, so "clone and
|
|
46
|
+
run" is literally true.
|
|
47
|
+
|
|
48
|
+
## Non-goals
|
|
49
|
+
|
|
50
|
+
- Not a rank tracker or keyword-research SaaS; it reads what Google already
|
|
51
|
+
reports about the owner's own sites.
|
|
52
|
+
- Not an auto-publisher, link builder, or comment bot.
|
|
53
|
+
- Not a hosted product; there is no account and no server-side component
|
|
54
|
+
beyond an optional static mirror the owner hosts.
|
|
55
|
+
- Windows is not a target (WSL likely works; untested).
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
# Epic: Configuration and Google access [shipped]
|
|
60
|
+
|
|
61
|
+
## Feature: One config file for every component [shipped]
|
|
62
|
+
|
|
63
|
+
`n-seo.config.json` (path overridable by `N_SEO_CONFIG`) is read by the
|
|
64
|
+
TypeScript app and every Python script; the example file is the fallback so
|
|
65
|
+
a fresh checkout runs.
|
|
66
|
+
|
|
67
|
+
- Acceptance: adding a site to `sites[]` makes it appear in every pull,
|
|
68
|
+
probe, audit, page and export with no other edit.
|
|
69
|
+
- Acceptance: both loaders derive the same `data/gsc/<slug>` from a property
|
|
70
|
+
(`sc-domain:` and url-prefix forms).
|
|
71
|
+
- Acceptance: unknown keys are preserved when the Settings page saves.
|
|
72
|
+
|
|
73
|
+
## Feature: Service-account auth without gcloud [shipped]
|
|
74
|
+
|
|
75
|
+
- Acceptance: with only a service-account JSON key and `openssl` on PATH,
|
|
76
|
+
`python3 ingest/google_auth.py` mints a token and lists accessible
|
|
77
|
+
properties.
|
|
78
|
+
- Acceptance: gcloud impersonation and gcloud user modes remain selectable.
|
|
79
|
+
- Acceptance: `ops/doctor.py` reports the exact email to add in Search
|
|
80
|
+
Console and GA4 and which configured properties are not yet accessible.
|
|
81
|
+
|
|
82
|
+
# Epic: Ingest [shipped]
|
|
83
|
+
|
|
84
|
+
## Feature: Search Console pulls [shipped]
|
|
85
|
+
|
|
86
|
+
- Acceptance: per property, `queries`, `pages`, `query_page`, `dates` for the
|
|
87
|
+
16-month window and `*_90d` for the trailing 90 days; transient failures
|
|
88
|
+
retried; a 4xx fails loudly.
|
|
89
|
+
- Acceptance: `gscExtraProperties` are pulled into `data/gsc/<slug>` without
|
|
90
|
+
appearing in the UI.
|
|
91
|
+
|
|
92
|
+
## Feature: GA4 pulls [shipped]
|
|
93
|
+
|
|
94
|
+
- Acceptance: `daily`, `sources`, `landing` per configured property id; a
|
|
95
|
+
`funnel` report only for `conversions.site`, falling back when the custom
|
|
96
|
+
dimension is unregistered.
|
|
97
|
+
|
|
98
|
+
## Feature: Time series, index coverage, metadata audit, trend analysis [shipped]
|
|
99
|
+
|
|
100
|
+
- Acceptance: date × page series for 180 days from both sources.
|
|
101
|
+
- Acceptance: URL Inspection verdict for every sitemap URL (sitemap indexes
|
|
102
|
+
followed one level, ≤400 URLs/host), with sitemap submission state.
|
|
103
|
+
- Acceptance: metadata audit flags title/query mismatch, CTR below position
|
|
104
|
+
expectation, missing/short/duplicate descriptions, long titles.
|
|
105
|
+
- Acceptance: trend file with branded/generic split, rising/falling queries
|
|
106
|
+
(84d vs prior 84d), monthly trajectory and AI-referral sessions by month.
|
|
107
|
+
|
|
108
|
+
# Epic: Live-site probe [shipped]
|
|
109
|
+
|
|
110
|
+
- Acceptance: robots.txt (incl. AI-crawler disallows), sitemap.xml, llms.txt
|
|
111
|
+
and llms-full.txt, homepage metadata/JSON-LD/visible-text bytes, and a real
|
|
112
|
+
404 check, per configured host, written as a timestamped snapshot.
|
|
113
|
+
- Acceptance: the daily diff raises an ALERT line on any regression versus
|
|
114
|
+
the previous snapshot.
|
|
115
|
+
|
|
116
|
+
# Epic: Action engine [shipped]
|
|
117
|
+
|
|
118
|
+
- Acceptance: six rules over the 90-day window (metadata findings, CTR
|
|
119
|
+
gaps, striking distance, probe hygiene, engagement mismatch, traffic drop)
|
|
120
|
+
produce actions with id, host, title, kind, why, how, spec[], impact,
|
|
121
|
+
effort, tag, source.
|
|
122
|
+
- Acceptance: ranking is impact / effort weight (S=1, M=2.5, L=5).
|
|
123
|
+
- Acceptance: metadata findings suppress CTR-gap cards on the same page.
|
|
124
|
+
- Acceptance: pages in `shippedWatch` render as *watching*.
|
|
125
|
+
- Acceptance: `config/backlog.json` is merged and hot-reloaded without a
|
|
126
|
+
restart, and a syntax error keeps the last good queue.
|
|
127
|
+
|
|
128
|
+
## Feature: Rule thresholds in config [planned]
|
|
129
|
+
|
|
130
|
+
Move the numeric thresholds (min impressions, position band, engagement
|
|
131
|
+
floor, drop percentage, effort weights) into an optional `rules` block of
|
|
132
|
+
the config with the current values as defaults.
|
|
133
|
+
|
|
134
|
+
- Acceptance: every threshold has a documented default and a config
|
|
135
|
+
override; tests cover an override changing a rule's output.
|
|
136
|
+
|
|
137
|
+
# Epic: Dashboard [shipped]
|
|
138
|
+
|
|
139
|
+
- Acceptance: routes `/`, `/actions`, `/insights`, `/trends[/N]`,
|
|
140
|
+
`/content`, `/drafts/:slug`, `/campaigns/:slug`, `/site/:host`,
|
|
141
|
+
`/indexing`, `/probes`, `/logs`, `/settings`, `/api/actions` all render
|
|
142
|
+
from demo data with no server error.
|
|
143
|
+
- Acceptance: Today board groups Fix / Approve / Publish / Comment / Ship;
|
|
144
|
+
Comment appears only when a participation module is enabled.
|
|
145
|
+
- Acceptance: proposals can be accepted into the backlog; backlog items can
|
|
146
|
+
be marked watching or retired; rule-derived items expose no write buttons.
|
|
147
|
+
- Acceptance: the server binds 127.0.0.1 by default and rejects cross-origin
|
|
148
|
+
POSTs.
|
|
149
|
+
- Acceptance: light and dark themes; no external fonts.
|
|
150
|
+
|
|
151
|
+
## Feature: Settings page [shipped]
|
|
152
|
+
|
|
153
|
+
- Acceptance: toggles every module, edits digest topics, expertise, watch
|
|
154
|
+
pages and conversions, and writes the config file (creating it from the
|
|
155
|
+
example on first save); invalid topic lines are rejected with a message.
|
|
156
|
+
|
|
157
|
+
## Feature: Custom pages and rules in instance mode [idea]
|
|
158
|
+
|
|
159
|
+
Allow an instance to register extra routes and rules without forking the
|
|
160
|
+
engine (e.g. `instance/src/extensions.ts`).
|
|
161
|
+
|
|
162
|
+
# Epic: Modules (opt-in) [shipped]
|
|
163
|
+
|
|
164
|
+
- Acceptance: each of indexStatus, metadataAudit, opportunityScan, llm,
|
|
165
|
+
hackerNews, reddit, indexNow, staticExport, gitAutoCommit, notifications is
|
|
166
|
+
gated by `modules.<key>.enabled`; a disabled digest exits 0 with a message.
|
|
167
|
+
- Acceptance: the LLM module runs any command that reads a prompt on stdin;
|
|
168
|
+
when unavailable, the scan records candidates only and digests skip
|
|
169
|
+
briefings.
|
|
170
|
+
- Acceptance: briefing prompts forbid generating comment text.
|
|
171
|
+
|
|
172
|
+
## Feature: HTTP LLM adapter [planned]
|
|
173
|
+
|
|
174
|
+
Optional adapter that calls an OpenAI-compatible or Anthropic HTTP endpoint
|
|
175
|
+
instead of a CLI, with the key read from `.env`.
|
|
176
|
+
|
|
177
|
+
## Feature: Bing Webmaster ingest [idea]
|
|
178
|
+
|
|
179
|
+
## Feature: Core Web Vitals via CrUX [idea]
|
|
180
|
+
|
|
181
|
+
# Epic: Daily run and scheduling [shipped]
|
|
182
|
+
|
|
183
|
+
- Acceptance: `ops/daily.py` waits for the network, runs the enabled steps
|
|
184
|
+
in order, retries a failed step once, tees output to `data/daily-ops.log`,
|
|
185
|
+
writes `data/last-run.json` with per-step timing, honours `--only`,
|
|
186
|
+
`--skip`, `--list`, `--no-network-wait`, and exits 1 on any failure.
|
|
187
|
+
- Acceptance: hooks (`beforeRun`, `afterRun`, `afterStep`) run in the
|
|
188
|
+
instance directory and are logged like steps without aborting the run.
|
|
189
|
+
- Acceptance: launchd, cron and systemd templates plus an installer script.
|
|
190
|
+
|
|
191
|
+
# Epic: MCP server [shipped]
|
|
192
|
+
|
|
193
|
+
- Acceptance: stdio transport with no secret; HTTP transport that returns
|
|
194
|
+
503 with no token configured and compares tokens in constant time.
|
|
195
|
+
- Acceptance: every tool is annotated read-only; tools cover the queue, a
|
|
196
|
+
single action, sites, per-site report, top queries, striking distance, CTR
|
|
197
|
+
gaps, metadata audit, time series, ops status, daily log, proposals,
|
|
198
|
+
conversions, campaigns, settings and engine info; docs exposed as
|
|
199
|
+
resources.
|
|
200
|
+
|
|
201
|
+
# Epic: Engine and instance [shipped]
|
|
202
|
+
|
|
203
|
+
- Acceptance: `N_SEO_INSTANCE` relocates every instance-owned path; in-place
|
|
204
|
+
mode is unchanged when it is unset.
|
|
205
|
+
- Acceptance: `n-seo init|start|dev|daily|doctor|demo|mcp|check|export|
|
|
206
|
+
upgrade|version` behave as documented in `docs/INSTANCE.md`.
|
|
207
|
+
- Acceptance: `n-seo upgrade` refuses to leave the engine on a commit that
|
|
208
|
+
fails `npm run check` without printing the rollback command.
|
|
209
|
+
|
|
210
|
+
## Feature: Publish the engine to npm [planned]
|
|
211
|
+
|
|
212
|
+
- Acceptance: `npm i n-seo` installs a working engine; `npx n-seo init`
|
|
213
|
+
scaffolds an instance; the `files` list excludes tests and the marketing
|
|
214
|
+
site.
|
|
215
|
+
|
|
216
|
+
## Feature: Scheduled upgrade with gate [planned]
|
|
217
|
+
|
|
218
|
+
A documented weekly job (`n-seo upgrade`) with notification on failure, so
|
|
219
|
+
instances exercise the upgrade path routinely.
|
|
220
|
+
|
|
221
|
+
# Epic: Quality [shipped]
|
|
222
|
+
|
|
223
|
+
- Acceptance: `npm run check` runs typecheck, TypeScript tests (sandboxed
|
|
224
|
+
copy) and Python tests; CI runs both suites, a demo-data dashboard smoke,
|
|
225
|
+
`doctor --offline`, and a grep that fails on any private name.
|
|
226
|
+
|
|
227
|
+
# Epic: Onboarding and docs [shipped]
|
|
228
|
+
|
|
229
|
+
- Acceptance: `npm run demo` populates every page with synthetic data before
|
|
230
|
+
any Google setup.
|
|
231
|
+
- Acceptance: README quickstart, SETUP-GOOGLE, SCHEDULING, ADDING-A-SITE,
|
|
232
|
+
INSTANCE, MCP, OPERATING-RULES, PLAYBOOK, FAQ, CONTRIBUTING, SECURITY,
|
|
233
|
+
CHANGELOG exist and match the CLI contracts.
|
|
234
|
+
|
|
235
|
+
## Feature: Interactive setup wizard [idea]
|
|
236
|
+
|
|
237
|
+
`n-seo init --guided`: asks for hosts, property ids and the key path, then
|
|
238
|
+
runs doctor.
|
|
239
|
+
|
|
240
|
+
# Epic: Marketing site [shipped]
|
|
241
|
+
|
|
242
|
+
- Acceptance: `www/` is a single static page deployed by the Pages workflow,
|
|
243
|
+
with OG image, `llms.txt`, robots and sitemap; copy names the three
|
|
244
|
+
promises (local data, evidence first, briefs-not-acts) and the operating
|
|
245
|
+
rules.
|
|
246
|
+
|
|
247
|
+
## Feature: Custom domain [planned]
|
|
248
|
+
|
|
249
|
+
`www/CNAME` and URL updates once a domain is chosen.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
n-seo publishes to npm as **`n-seo`**, unscoped. The package name and the
|
|
4
|
+
binary name match, which is why `npm i n-seo` gives you `npx n-seo` and not
|
|
5
|
+
something longer.
|
|
6
|
+
|
|
7
|
+
Releases are driven by tags. Pushing `v<version>` runs
|
|
8
|
+
[`.github/workflows/release.yml`](../.github/workflows/release.yml), which
|
|
9
|
+
refuses rather than guesses at every step: a tag that disagrees with
|
|
10
|
+
`package.json`, or a version with no changelog section, stops the run before
|
|
11
|
+
anything reaches the registry.
|
|
12
|
+
|
|
13
|
+
## What counts as a breaking change
|
|
14
|
+
|
|
15
|
+
The public contract is larger than the exported functions, because most people
|
|
16
|
+
never import this package — they run it. Treat these as **major**:
|
|
17
|
+
|
|
18
|
+
- **`n-seo.config.json`** — removing a key, renaming one, or changing what a
|
|
19
|
+
value means. Adding an optional key with a safe default is minor.
|
|
20
|
+
- **The instance layout** — where `config/`, `content/`, `data/` and
|
|
21
|
+
`docs/daily-log.md` live, and which of them an upgrade may touch. Someone's
|
|
22
|
+
instance is a directory they back up; moving it silently is a breaking
|
|
23
|
+
change even though no code signature moved.
|
|
24
|
+
- **The CLI verbs** and their arguments (`init`, `start`, `daily`, `doctor`,
|
|
25
|
+
`demo`, `mcp`, `export`, `check`, `upgrade`, `version`).
|
|
26
|
+
- **The data-file shapes** in `docs/ARCHITECTURE.md`, since instances and
|
|
27
|
+
outside scripts read them.
|
|
28
|
+
- **The MCP tool names and their inputs**, which agents call.
|
|
29
|
+
|
|
30
|
+
Minor: new modules, new rules in the action engine, new pages, new data files.
|
|
31
|
+
Patch: fixes that leave all of the above alone.
|
|
32
|
+
|
|
33
|
+
The engine and an instance are versioned together — an instance pins an engine
|
|
34
|
+
version, so a major bump is the signal to read the changelog before upgrading.
|
|
35
|
+
|
|
36
|
+
## Pre-flight
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
git switch main && git pull
|
|
40
|
+
git status --porcelain # must be empty
|
|
41
|
+
npm ci
|
|
42
|
+
npm run check # typecheck + TypeScript tests + Python tests
|
|
43
|
+
.github/scripts/pack-smoke.sh # packs, installs, and drives the tarball
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
CI must be green on `main`. The `package` job is the one that matters most
|
|
47
|
+
here: it installs the tarball into an empty project and drives the result,
|
|
48
|
+
which is the only place packaging bugs are visible. Two shipped undetected
|
|
49
|
+
before it existed — `tsx` sitting in `devDependencies`, and `tsconfig.json`
|
|
50
|
+
missing from `files` — and a git checkout hides both.
|
|
51
|
+
|
|
52
|
+
## Cut the release
|
|
53
|
+
|
|
54
|
+
1. **Write the changelog.** Rename `## [Unreleased]` to
|
|
55
|
+
`## [<version>] - <YYYY-MM-DD>`, add a fresh empty `## [Unreleased]` above
|
|
56
|
+
it, and update the link definitions at the bottom of the file.
|
|
57
|
+
|
|
58
|
+
2. **Bump the version.** `npm version` writes `package.json`, commits, and
|
|
59
|
+
tags in one step:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npm version patch # or minor, or major
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
To keep the changelog commit and the version commit together, stage the
|
|
66
|
+
changelog first and use `npm version --no-git-tag-version`, then commit and
|
|
67
|
+
tag by hand:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npm version 0.2.0 --no-git-tag-version
|
|
71
|
+
git add package.json package-lock.json CHANGELOG.md
|
|
72
|
+
git commit -m "Release 0.2.0"
|
|
73
|
+
git tag v0.2.0
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
3. **Push the tag.**
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
git push origin main
|
|
80
|
+
git push origin v0.2.0
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The workflow then checks the tag against `package.json`, checks the changelog
|
|
84
|
+
has that section, runs the full suite, packs and drives the tarball,
|
|
85
|
+
publishes with provenance, and opens a GitHub Release whose body is that
|
|
86
|
+
changelog section.
|
|
87
|
+
|
|
88
|
+
## The first release is different
|
|
89
|
+
|
|
90
|
+
Two things are only true once.
|
|
91
|
+
|
|
92
|
+
**Provenance needs a public repository.** `npm publish --provenance` attaches a
|
|
93
|
+
signed attestation linking the tarball to the workflow run that built it.
|
|
94
|
+
GitHub will not issue the OIDC token for that on a private repo, so the repo
|
|
95
|
+
must be public before the first tag is pushed.
|
|
96
|
+
|
|
97
|
+
**Trusted publishing cannot be configured until the package exists.** npm
|
|
98
|
+
binds a trusted publisher to a package, and the settings page for it lives on
|
|
99
|
+
the package — which does not exist before the first publish. PyPI allows
|
|
100
|
+
configuring a publisher for a not-yet-existing project; npm does not
|
|
101
|
+
([npm/cli#8544](https://github.com/npm/cli/issues/8544)).
|
|
102
|
+
|
|
103
|
+
So something has to make version 0.1.0 exist. The best option is **not** a CI
|
|
104
|
+
token: publish the first version by hand, from a machine, with interactive
|
|
105
|
+
2FA. No long-lived credential is created and nothing is ever stored in the
|
|
106
|
+
repository.
|
|
107
|
+
|
|
108
|
+
1. Make the repository public (provenance requires it).
|
|
109
|
+
2. From a clean checkout at the tagged commit:
|
|
110
|
+
```sh
|
|
111
|
+
npm login # interactive, with 2FA
|
|
112
|
+
npm run check # the suite
|
|
113
|
+
.github/scripts/pack-smoke.sh # install the tarball and drive it
|
|
114
|
+
npm publish --access public
|
|
115
|
+
```
|
|
116
|
+
Skip `--provenance` here: a local publish has no OIDC token to sign
|
|
117
|
+
against. Only the workflow can attach provenance.
|
|
118
|
+
3. On npmjs.com, open the package → Settings → Trusted Publisher, and point it
|
|
119
|
+
at `en-dash-consulting/n-seo` with workflow `.github/workflows/release.yml`.
|
|
120
|
+
4. `npm logout`, so the local session token dies too.
|
|
121
|
+
|
|
122
|
+
Every release after that is just a tag. The workflow authenticates over OIDC,
|
|
123
|
+
signs with provenance, and no secret exists in the repository at all.
|
|
124
|
+
|
|
125
|
+
If you would rather automate even the first publish, a **granular access
|
|
126
|
+
token** scoped to publish, with the shortest expiry npm offers, added as
|
|
127
|
+
`NPM_TOKEN` and restored to the publish step as
|
|
128
|
+
`NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}`, will do it. Delete the secret
|
|
129
|
+
immediately afterwards. Prefer the manual route: npm is actively moving away
|
|
130
|
+
from long-lived tokens, and 2FA-bypassing automation tokens for account
|
|
131
|
+
management are being retired.
|
|
132
|
+
|
|
133
|
+
### Why the workflow upgrades npm
|
|
134
|
+
|
|
135
|
+
Node 22 ships npm 10, and trusted publishing needs **npm 11.5.1 or later**.
|
|
136
|
+
Without the upgrade step the OIDC exchange never happens and the publish
|
|
137
|
+
fails with a 404 that has nothing to do with the package being missing. The
|
|
138
|
+
workflow also deliberately omits `registry-url` from `setup-node`, because it
|
|
139
|
+
writes an `.npmrc` auth line interpolating `NODE_AUTH_TOKEN`, and an empty
|
|
140
|
+
value there produces the same misleading 404.
|
|
141
|
+
|
|
142
|
+
## Verify what was published
|
|
143
|
+
|
|
144
|
+
Do not trust the workflow's own output. Install from the registry, into an
|
|
145
|
+
empty directory, as a stranger would:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
cd "$(mktemp -d)"
|
|
149
|
+
npm init -y >/dev/null
|
|
150
|
+
npm i n-seo
|
|
151
|
+
npx n-seo version
|
|
152
|
+
npx n-seo init ./inst
|
|
153
|
+
npx n-seo demo --instance ./inst
|
|
154
|
+
npx n-seo start --instance ./inst # then open the port it prints
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Check the npm page shows the provenance badge, the README renders, and
|
|
158
|
+
`repository` and `homepage` resolve.
|
|
159
|
+
|
|
160
|
+
## When a release is wrong
|
|
161
|
+
|
|
162
|
+
**Prefer publishing a fix.** A new patch version is almost always better than
|
|
163
|
+
removing one, because anything that already installed the bad version keeps
|
|
164
|
+
working.
|
|
165
|
+
|
|
166
|
+
Mark a bad version so nobody new installs it:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
npm deprecate n-seo@0.2.0 "Broken packaging; use 0.2.1 or later."
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
If `latest` is pointing at the wrong version, move it:
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
npm dist-tag add n-seo@0.1.0 latest
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Unpublishing is a last resort and npm's policy limits it to a 72-hour window
|
|
179
|
+
for a version that nothing depends on:
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
npm unpublish n-seo@0.2.0
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
A version number is never reusable once published, even after unpublishing.
|
|
186
|
+
Bump and move on.
|
|
187
|
+
|
|
188
|
+
If a secret ever reaches the registry, treat the secret as compromised and
|
|
189
|
+
rotate it first. Removing the tarball does not un-leak it.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Scheduling the daily run and the dashboard
|
|
2
|
+
|
|
3
|
+
Two things should always be running: the **dashboard** (a long-lived process
|
|
4
|
+
serving the configured port) and the **daily run** (`ops/daily.py`, once a
|
|
5
|
+
morning). Both are plain processes; pick the scheduler your OS already has.
|
|
6
|
+
|
|
7
|
+
## macOS (launchd)
|
|
8
|
+
|
|
9
|
+
The install script fills in the templates under `ops/templates/`, copies them
|
|
10
|
+
to `~/Library/LaunchAgents`, and loads them:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
ops/install-launchd.sh # installs n-seo.dashboard and n-seo.daily
|
|
14
|
+
ops/install-launchd.sh --uninstall
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
What it installs:
|
|
18
|
+
|
|
19
|
+
| Label | Does | Log |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `n-seo.dashboard` | `npx tsx src/server.tsx`, KeepAlive, starts at login | `data/dashboard.log` |
|
|
22
|
+
| `n-seo.daily` | `python3 ops/daily.py` at 07:00 local | `data/daily-launchd.log` (launchd), `data/daily-ops.log` (the run itself) |
|
|
23
|
+
|
|
24
|
+
Manual equivalent: copy the two plists from `ops/templates/`, replace
|
|
25
|
+
`__REPO__`, `__NODE_BIN__` and `__HOME__`, then
|
|
26
|
+
`launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.n-seo.*.plist`.
|
|
27
|
+
|
|
28
|
+
**Runs on wake.** A `StartCalendarInterval` job that misses its slot because
|
|
29
|
+
the machine was asleep fires as soon as the machine wakes. On wake the network
|
|
30
|
+
is usually not up yet, which is why `ops/daily.py` waits (up to five minutes)
|
|
31
|
+
for connectivity before it starts. A machine that stays shut all day skips
|
|
32
|
+
that day; there is no catch-up.
|
|
33
|
+
|
|
34
|
+
**After code changes** the dashboard service does not reload itself:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
launchctl kickstart -k gui/$(id -u)/n-seo.dashboard
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For iterative development use `npm run dev` in a terminal — but stop it
|
|
41
|
+
before relying on the service again, since both bind the same port.
|
|
42
|
+
|
|
43
|
+
## Linux (cron)
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
crontab -e
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Paste the line from `ops/templates/n-seo.cron`, adjusting the path:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
0 7 * * * cd /path/to/n-seo && /usr/bin/python3 ops/daily.py >> data/daily-cron.log 2>&1
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Cron does not run missed jobs; if the machine is off at 07:00 that day is
|
|
56
|
+
skipped. Run the dashboard with the systemd unit below, or with any process
|
|
57
|
+
supervisor you already use.
|
|
58
|
+
|
|
59
|
+
## Linux (systemd user units)
|
|
60
|
+
|
|
61
|
+
`ops/templates/` has three units:
|
|
62
|
+
|
|
63
|
+
| File | Purpose |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `n-seo-dashboard.service` | the dashboard, `Restart=always` |
|
|
66
|
+
| `n-seo-daily.service` | one run of `ops/daily.py` |
|
|
67
|
+
| `n-seo-daily.timer` | `OnCalendar=*-*-* 07:00:00`, `Persistent=true` (runs a missed slot on next boot) |
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
mkdir -p ~/.config/systemd/user
|
|
71
|
+
sed "s|__REPO__|$PWD|g" ops/templates/n-seo-dashboard.service > ~/.config/systemd/user/n-seo-dashboard.service
|
|
72
|
+
sed "s|__REPO__|$PWD|g" ops/templates/n-seo-daily.service > ~/.config/systemd/user/n-seo-daily.service
|
|
73
|
+
cp ops/templates/n-seo-daily.timer ~/.config/systemd/user/
|
|
74
|
+
systemctl --user daemon-reload
|
|
75
|
+
systemctl --user enable --now n-seo-dashboard.service n-seo-daily.timer
|
|
76
|
+
loginctl enable-linger "$USER" # keep user units running when you are logged out
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Check on them with `systemctl --user status n-seo-dashboard` and
|
|
80
|
+
`journalctl --user -u n-seo-daily`.
|
|
81
|
+
|
|
82
|
+
## Running by hand
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
python3 ops/daily.py # everything
|
|
86
|
+
python3 ops/daily.py --list # the step names
|
|
87
|
+
python3 ops/daily.py --only gsc,ga4 # a subset
|
|
88
|
+
python3 ops/daily.py --skip index-status
|
|
89
|
+
python3 ops/daily.py --no-network-wait
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Each run appends to `data/daily-ops.log`, writes `data/last-run.json` (which
|
|
93
|
+
the dashboard shows as the run status chip), and appends a dated entry to
|
|
94
|
+
`docs/daily-log.md`. Re-running on the same day replaces that day's entry
|
|
95
|
+
rather than stacking a second one.
|
|
96
|
+
|
|
97
|
+
## Logs, in one place
|
|
98
|
+
|
|
99
|
+
| File | What |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `data/daily-ops.log` | stdout of every daily run, appended |
|
|
102
|
+
| `data/last-run.json` | timestamp, failed step names, per-step timing |
|
|
103
|
+
| `docs/daily-log.md` | the human-readable daily entry (also on `/logs`) |
|
|
104
|
+
| `data/dashboard.log` | dashboard stdout/stderr under launchd |
|