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.
Files changed (88) hide show
  1. package/.env.example +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +184 -0
  4. package/bin/n-seo.mjs +310 -0
  5. package/docs/ADDING-A-SITE.md +82 -0
  6. package/docs/ARCHITECTURE.md +213 -0
  7. package/docs/DEPLOY.md +300 -0
  8. package/docs/FAQ.md +93 -0
  9. package/docs/INSTANCE.md +365 -0
  10. package/docs/MCP.md +104 -0
  11. package/docs/OPERATING-RULES.md +106 -0
  12. package/docs/PLAYBOOK.md +122 -0
  13. package/docs/PRD.md +249 -0
  14. package/docs/RELEASING.md +189 -0
  15. package/docs/SCHEDULING.md +104 -0
  16. package/docs/SETUP-GOOGLE.md +215 -0
  17. package/docs/examples/campaign.json +59 -0
  18. package/docs/examples/draft.md +43 -0
  19. package/docs/screenshots/overview.png +0 -0
  20. package/ingest/__pycache__/analyze_ga4.cpython-313.pyc +0 -0
  21. package/ingest/__pycache__/analyze_gsc.cpython-313.pyc +0 -0
  22. package/ingest/__pycache__/analyze_metadata.cpython-313.pyc +0 -0
  23. package/ingest/__pycache__/analyze_trends.cpython-313.pyc +0 -0
  24. package/ingest/__pycache__/google_auth.cpython-313.pyc +0 -0
  25. package/ingest/__pycache__/http_util.cpython-313.pyc +0 -0
  26. package/ingest/__pycache__/pull_ga4.cpython-313.pyc +0 -0
  27. package/ingest/__pycache__/pull_gsc.cpython-313.pyc +0 -0
  28. package/ingest/__pycache__/pull_index_status.cpython-313.pyc +0 -0
  29. package/ingest/__pycache__/pull_timeseries.cpython-313.pyc +0 -0
  30. package/ingest/__pycache__/seo_config.cpython-313.pyc +0 -0
  31. package/ingest/analyze_ga4.py +79 -0
  32. package/ingest/analyze_gsc.py +136 -0
  33. package/ingest/analyze_metadata.py +158 -0
  34. package/ingest/analyze_trends.py +145 -0
  35. package/ingest/google_auth.py +238 -0
  36. package/ingest/http_util.py +87 -0
  37. package/ingest/pull_ga4.py +107 -0
  38. package/ingest/pull_gsc.py +111 -0
  39. package/ingest/pull_index_status.py +179 -0
  40. package/ingest/pull_timeseries.py +130 -0
  41. package/ingest/seo_config.py +213 -0
  42. package/n-seo.config.example.json +110 -0
  43. package/ops/__pycache__/daily.cpython-313.pyc +0 -0
  44. package/ops/__pycache__/daily_diff.cpython-313.pyc +0 -0
  45. package/ops/__pycache__/demo_data.cpython-313.pyc +0 -0
  46. package/ops/__pycache__/doctor.cpython-313.pyc +0 -0
  47. package/ops/__pycache__/export_static.cpython-313.pyc +0 -0
  48. package/ops/__pycache__/hn_digest.cpython-313.pyc +0 -0
  49. package/ops/__pycache__/indexnow.cpython-313.pyc +0 -0
  50. package/ops/__pycache__/llm.cpython-313.pyc +0 -0
  51. package/ops/__pycache__/opportunity_scan.cpython-313.pyc +0 -0
  52. package/ops/__pycache__/publish.cpython-313.pyc +0 -0
  53. package/ops/__pycache__/reddit_digest.cpython-313.pyc +0 -0
  54. package/ops/daily.py +250 -0
  55. package/ops/daily_diff.py +151 -0
  56. package/ops/demo_data.py +529 -0
  57. package/ops/doctor.py +266 -0
  58. package/ops/export_static.py +125 -0
  59. package/ops/hn_digest.py +169 -0
  60. package/ops/indexnow.py +107 -0
  61. package/ops/install-launchd.sh +76 -0
  62. package/ops/llm.py +139 -0
  63. package/ops/mcp-smoke-stdio.mjs +61 -0
  64. package/ops/opportunity_scan.py +185 -0
  65. package/ops/publish.py +158 -0
  66. package/ops/reddit_digest.py +168 -0
  67. package/ops/templates/n-seo-daily.service +11 -0
  68. package/ops/templates/n-seo-daily.timer +11 -0
  69. package/ops/templates/n-seo-dashboard.service +15 -0
  70. package/ops/templates/n-seo.cron +3 -0
  71. package/ops/templates/n-seo.daily.plist +29 -0
  72. package/ops/templates/n-seo.dashboard.plist +22 -0
  73. package/package.json +77 -0
  74. package/probes/__pycache__/site_probe.cpython-313.pyc +0 -0
  75. package/probes/site_probe.py +201 -0
  76. package/public/favicon.svg +6 -0
  77. package/public/styles.css +632 -0
  78. package/src/actions.ts +255 -0
  79. package/src/backlog.ts +197 -0
  80. package/src/config.ts +220 -0
  81. package/src/data.ts +895 -0
  82. package/src/insights.ts +22 -0
  83. package/src/mcp-stdio.ts +21 -0
  84. package/src/mcp.ts +490 -0
  85. package/src/server.tsx +260 -0
  86. package/src/settings.tsx +329 -0
  87. package/src/views.tsx +1487 -0
  88. package/tsconfig.json +15 -0
@@ -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 |