seo-platform-detect
The profile decides three things no module can decide on its own: which platform-conditional check
ids fire, which findings are
because the platform owns the thing, and which fix
adapter
will reach for. Run it once per audit, right after the snapshot, before dispatch.
Detection is orthogonal to the vertical:
answers
what kind of site is this,
this skill answers
what is it built on. Both feed one
.
When to run
In the normal
and
flow you do
not run this yourself:
already calls the
same
over the run's homepage snapshot and persists the result as
.
Read that file first. Run the script directly only when you need a signal the headless pass could not
use —
for repo/package signals,
for the WordPress plugin verdict — or when
you are working outside the runner. Either way it is one run per audit, before the dispatch envelopes
are built; every subagent reads the same profile.
How to run
bash
node "${CLAUDE_PLUGIN_ROOT}/scripts/detect-platform.mjs" --snapshot "<run_dir>/pages/<home-slug>.json"
Add flags when they apply, and only then:
- when the user gave a local project (). This is the only way repo
and package signals are consulted; without it, Payload is undetectable and Next.js router
detection falls back to DOM markers. It is also what turns on, which is
what makes local file fixes possible at all.
- when the target is reachable and a WordPress plugin verdict matters. It requests
, , and — four GETs, no more. The
REST namespace list is the only reliable way to tell Yoast from Rank Math from AIOSEO; HTML
comments name a plugin that printed a comment, not one that is active.
--out "<run_dir>/profile.platform.json"
to persist it instead of piping the stdout JSON.
Offline / fixture mode (used by the tests, and by you when there is no network):
--html <file> [--headers <json>] [--cookies <json>] [--probes <json>] [--url <u>]
.
Exit codes: 0 ok · 1 bad invocation or unreadable input · 2 fetch failure.
What comes back
{ version: "detect-platform/1", target, platform, framework, cms_plugins[], hosting, environment, capabilities, write_targets[], candidates, vertical_hints[], cards[], sources, notes[] }
.
Each of
/
/
/
carries its own
(
≥ 8 with ≥ 2 signal kinds,
≥ 5,
≥ 2) and the
that produced it.
A layer with less evidence than that is
—
an unresolved platform is a result, not a gap.
Report it as unresolved and let the run continue on
; never round a
up in prose.
Four layers are scored independently, so headless setups resolve correctly: a WordPress backend with
a Next.js front end yields
and , and
becomes
. Follow
whenever the cards disagree.
Merging into profile.json
is the union of this output and
's.
writes it: it
refreshes every key
owns and preserves the ones you added, so a
block already in the
file survives a re-run (a
flag overrides it). Shape:
json
{ "version": "detect-platform/1", "platform": {…}, "framework": {…}, "cms_plugins": [],
"hosting": {…}, "environment": {…}, "capabilities": {…}, "write_targets": [],
"candidates": {…}, "vertical_hints": [], "cards": [], "sources": {…}, "notes": [],
"vertical": { "primary": "ecommerce", "also": [], "multilingual": false, "locales": [] } }
Pass
to
as
signals, never as verdicts — it must still cite
its own evidence. Pass
to the scorer
(
score.mjs --environment preview|staging|local
), which suppresses noindex-type severity-5 caps on
a non-production host and reports them as expected instead.
Which card goes to which subagent
already lists the paths (
references/platforms/<id>.md
). Put them in every
dispatch envelope as
, alongside the
field (the absolute
path) and a one-line summary
(
"shopify (high) · theme head · staging theme available · environment production"
), with these two
instructions verbatim:
- emit the card's §10 check ids when the rule matches;
- card §4 items ⇒ naming the owner in — never a silent pass,
never a for something the platform will not let anyone change.
Hand out the platform card, the framework card and any plugin card together; a headless site legibly
needs all three. When
capabilities.instructions_only
is true, also tell the agents that every fix
they propose will be delivered as a click-path, so
must be human-readable prose rather
than a diff.
Honesty
- in lists credential key names. Presence is checked for
CLAUDE_PLUGIN_OPTION_<KEY>
/ — values are never read, printed or logged.
means "not configured", never "wrong key".
- Cookie names are evidence; cookie values are never recorded.
- Repo signals are absent without , probe signals absent without , and the profile's
says so. Repeat that in the report rather than presenting a partial profile as complete.
- A signal marked in a card's §12 may support a detection verdict but must never
support a write. When a fix depends on an unverified API detail, the answer is .