Two independent questions, measured separately: is csupiautomentes.hu actually in
good shape, and are the six seo-* skills safe to point at the next client. Every claim below traces
to a command; raw outputs in .tmp/audit-2026-07-27/.
29 of 33 checklist lines PASS. Zero critical defects found live. The four gaps are one measurement blind spot (Search Console), one blocked item (Google cégprofil), and two slow pages.
The extraction is genuinely sound: the skill scripts rebuilt 44 committed artefacts byte-for-byte. But two of them crash on their first run against a fresh project.
Files reproduced byte-identically by the parameterised skill scripts from
seo.config.json + committed data alone — 15 service pages and 29 images.
Scored against the master checklist in ~/.claude/skills/seo/SKILL.md. "It exists" never
counts — each line names the measurement that decided it.
| Checklist line | Verdict | Evidence |
|---|---|---|
| Foundations | ||
seo.config.json in the project root | FAIL | Absent. Every skill script anchors on it; verify_redirects.py died with
FileNotFoundError: seo-data\gsc-url-status.json until one was supplied. |
One canonical host, 301 from the other; .html → extensionless | PASS | www → apex 301 · /index.html → 308 / · both single-hop. |
| Junk URL returns a real 404 | PASS | curl -sI /totally-random-nonexistent-xyz → 404. The soft-404 cascade is gone. |
| robots.txt present, blocks nothing real | PASS | Allow: / + sitemap declared. Fetched live. |
| Per page — 37 indexable pages | ||
Unique <title> ≤ 62 chars | 1 FAIL | 35/37 pass. /furgonmentes = 66 chars. (m31.html at 68 is a dead
file, unreachable behind a 301 — see finding 5.) |
| Unique meta description ≤ 158 chars | 1 FAIL | 35/37 pass. /furgonmentes = 197 chars — Google truncates it. |
| Self-referencing canonical, exactly one | PASS | 37/37 present, self-referencing and unique. The historic bug (9 pages canonicalised to
/m31) has not returned. |
| One H1; OG + Twitter in sync with title/desc | PASS | Sweep of all 37 pages for og:title/description/url/image + twitter:* mismatches and H1 count: 0 problems. |
| JSON-LD parses; fields localised per page | PASS | LocalBusiness 37, Service 37, BreadcrumbList 37, FAQPage 36, WebSite 1 — all parse.
37 distinct Service nodes and 37 distinct breadcrumb trails; the "generator forgot to
localise areaServed" failure is absent. |
No internal links ending in .html | PASS | audit.py → 0. (Was 80 pointless 308 hops.) |
| Favicon set + OG share image | PASS | 4 favicon files live 200; og-share.jpg measured at exactly 1200×630. |
| Site-wide files | ||
| sitemap.xml lists every real page, nothing retired | PASS | All 37/37 URLs return 200 live, cache-busted, no hop. |
| llms.txt present, every link resolves | PASS | 39/39 unique links return 200. |
| Content uniqueness measured vs own baseline | PASS | Templated locations: max 0.297 / mean 0.176 (site baseline: 0.295 acceptable, 0.35 = delete). Rebuilt topic pages: max 0.164 / mean 0.133. Neither family is doorway-risk. |
| Redirects & history | ||
| GSC export crossed with a live crawl | PASS | seo-tools/gsc-url-status.json, 265 URLs, dated 2026-07-22. 60% of 16-month
clicks sat on URLs that were 404 at crawl time. |
| Every traffic-earning dead URL mapped, all 301 | PASS | 190 of 193 return 301, routing 4 837 clicks/16m to a real page. The 3 exceptions are instrument artefacts, checked by hand: two are pages since rebuilt (308 → 200 live), one is a Windows console encoding crash on a mangled Unicode path. |
| Map in a Pages Function if >150 rules; gclid preserved | PASS | 191 rules in functions/legacy-map.js (past the ~200 _redirects
ceiling). ?gclid=TEST123 survives the hop intact. |
| Search Console | ||
| Correct property confirmed, access working | NOT CHECKED | No live access this session (agreed scope). Only committed export available. |
| Sitemap submitted, stale entries removed | NOT CHECKED | Same. |
| Index-coverage buckets read and reconciled | NOT CHECKED | The committed chart ends 2026-06-12 — before every fix. It cannot speak to today. See finding 1. |
| No manual actions / security issues | NOT CHECKED | Same. |
| Performance | ||
| Images shipped at 2× render box, SSIM-gated; re-run is a no-op | PASS | Rebuilt from originals in a clean clone: 29/29 files byte-identical to what is deployed. Every SSIM ≥ 0.99 at painted size. |
| LCP element identified; no CSS-background LCP | PASS | LCP is a real HEADER img on all 36 measured pages, and it is preloaded:
<link rel="preload" as="image" href="assets/hero-bg.webp" fetchpriority="high">.
No preload-scanner trap. |
| Throttled-mobile sweep of all sitemap URLs, median < 2.5 s | PASS | slow-4G + 4× CPU at 390 px: median LCP 2 168 ms. The 9 392 ms "worst" was a cold start — re-measured 3× at a median of 2 488 ms. Two genuine laggards remain (findings 3–4). |
_headers caching + _routes.json exclusion | PASS | Second fetch of styles.css → cf-cache-status: HIT; images HIT.
_routes.json excludes all 12 static paths from the Function. |
| CrUX field data | NOT CHECKED | PageSpeed API returned 429 — Quota exceeded … Queries per day on both
attempts. Needs an API key. All speed numbers here are lab-only, on a deliberately harsh profile. |
| Local | ||
| Live review count feeding schema | PASS | /api/reviews returns userRatingCount: 337; schema asserts
337 on all 37 pages. Exact match, live-sourced. |
| NAP identical across footer, schema, copy | PASS | One address string on 38/38 pages; one identical LocalBusiness name/telephone/address
block on 38/38. tel:+36706262777 on all 391 phone links. |
| GBP assessed, risk register put to the owner | BLOCKED | No manager access since 2026-07-19. Plan written, cannot execute. See finding 2. |
| Ship & close | ||
| Deployed to the production branch | PASS | Live HTML matches the repo; dist/ and root are in sync (0 drift across 39 files). |
| Invariant greps live | PASS | Live homepage carries 8 gfn-track-phone (the documented expectation);
all 39 pages carry phone_conversion_number: '06 (70) 62 62 777', matching the displayed
string exactly; GTM-TH67S8M and AW-871296311 present live. |
| Browser QA at 1280 / 768 / 390 incl. mobile menu | PASS-WITH-NOTES | No horizontal overflow anywhere; hamburger opens all 6 items and closes; hero photo visible and legible; review carousel renders live data; gallery loads. One real cosmetic overlap (finding 6). |
| Expectations written for the client | PASS | SEO-OSSZEFOGLALO.md §5 sets them, and explicitly forbids promising a
ranking by a date. |
complete, at natural sizes 1110×833 / 1110×833 / 1024×768, filling their 200 px
box with object-fit: cover and a 0 px gap, and all three return 200 live. The agent screenshotted
before the loading="lazy" images painted. Reported here rather than silently dropped, because it
is the same lesson the audit skill already carries: an instrument is a hypothesis until you validate it.
search-console-issues/Diagram.csv)
ends 2026-06-12 and shows 2 indexed / 69 not-indexed — a snapshot from before the
canonical fix, the soft-404 fix, the 13 rebuilt pages and the 191-URL redirect map.www — that mix-up already
cost a diagnosis once), export the 16-month Pages report, re-run
verify_redirects.py against it.[e-mail cím eltávolítva] (checked 2026-07-19/20);
unchanged. Plan exists at plans/seo-expansion-round2/gbp-optimization-plan.md.business.google.com; then a day of work./furgonmentes is the weakest page on the site MEDIUMapply_copy_fixes.py's page list, which is exactly
why it escaped the sitewide copy pass that fixed everything else./bikazas-hideginditas (1 472 ms, 35 KB, 2 images) does not — it carries 5 images and 52 KB of HTML.audit.py → 0 problems; 3× re-measure median < 2 500 ms./automentes-buda LCP median 3 408 ms LOW-MEDm31.html still ships as a stale duplicate LOWindex, follow, self-canonical to /m31, identical H1
to /automentes-m31, with an out-of-date 68-char title and 176-char description. It is not
in the sitemap, and /m31 301s to /automentes-m31 live — so the edge redirect is the
only thing preventing a duplicate.noindex, and correct the CLAUDE.md line.audit.py reports 38 → 37 pages and 2 fewer problems.www + a legacy URL LOWwww.csupiautomentes.hu/automentes-melygarazsbol → 301 to apex → 301 to
/melygarazsbol-mentes. The middleware rewrites the host and returns before consulting the
legacy map.www traffic is small. Noted for
completeness, not for action._middleware.js — resolve the legacy path before returning the
host redirect.curl -sIL shows one 301, not two.gclid. Do not migrate it back into _redirects — that file stops honouring rules
past ~200 and the tail silently 404s.<head> must assert this count afterwards.dist/.gtag without a brief.429). Needs an API
key. Every speed number here is lab-only on a deliberately harsh profile; field data outranks all of it.gclid;
confirming it needs a real ad click, visible in Google Ads, not synthetic testing.Not assessed by reading them. Tested: a clean copy of the repo was made in a scratch directory, given the
seo.config.json the family assumes exists (and which this project never had), and every skill
script was run against it — output diffed byte-for-byte against what is deployed. Nothing in the client repo
was modified.
seo.config.json plus
committed data. wire_pages.py reported 0 sitemap changes, 0 redirect rules dropped, 0 retargeted;
apply_copy_fixes.py reported "0 file(s) would change"; seo_enrich.py and
add_favicons.py were clean no-ops. Idempotency is real, not claimed.
apply_copy_fixes.py crashes on its own documented config value BLOCKERseo/SKILL.md documents "reviews_api": "/api/reviews". The script
does REVIEWS_API = cfg("reviews_api", "") and passes it straight to
urllib.request.Request → ValueError: unknown url type: '/api/reviews'. Setting it to
an absolute URL made the same run succeed and report "0 file(s) would change".site when the value starts with / — one line — or change
the documented example to an absolute URL.optimize_images.py --dry-run cannot run on a fresh project BLOCKER.tmp probe files into dist/images/ while measuring,
but only creates that directory when not in dry-run → FileNotFoundError: …\dist\images\
blue_mercedes_g_wagon.webp.tmp. Creating the directory by hand made the identical command succeed.dist/images/ does not yet exist. The failure looks like a broken script, not a missing
directory.os.makedirs(OUT, exist_ok=True) unconditionally.optimize_images.py fails open into the exact bug it exists to prevent MEDIUMBOXES is a hardcoded table of Csupi's filenames and measured render boxes.
An image not in it is silently shutil.copy2'd at full original size (the comment justifies this
for og-share.jpg). On a new project no filename matches, so every original is copied
whole.BOXES from measure_boxes.js output in the project (it already
ships in seo-ship/scripts/), and make an unmeasured, non-excluded image a loud error.--help:
wire_pages.py executed and rewrote the sitemap; seo_enrich.py and
add_favicons.py executed; gen_*_pages.py silently treated it as a page slug and did
nothing. Only the exact string --dry-run is honoured, by substring match on
sys.argv.--dryrun, -n — writes to the project instead of
previewing. On a client site that is a live edit.argparse, or at minimum reject unknown flags.seo/SKILL.md invariant #3 requires every transform to be idempotent, support
--dry-run, and "run its selftest on every invocation". Measured: gen_service_pages.py
and gen_location_pages.py have neither a dry-run nor a selftest; only
apply_copy_fixes.py has a selftest.--dry-run to both generators. Either add selftests or soften the invariant to
what is actually enforced.| Item | Severity | Detail |
|---|---|---|
B6 · LEARNINGS.md | MEDIUM | seo/SKILL.md line 11 says the full record with numbers lives in
LEARNINGS.md at the skills repo root. Indexed every file under all eight seo*
skills — it does not exist. The router's stated provenance is a dead pointer. |
B7 · seo-setup | MEDIUM | Superseded by seo-onpage (which says so), but still present, still listed, and
its description covers the same ground almost word for word — so it can win the trigger. It also still
references scripts at seo-tools/… paths that now live in seo-onpage/scripts/.
It ships a SKILL.md and nothing else. Retire it. |
| B8 · misfiled script path | LOW | seo-audit/SKILL.md §5 cites scripts/speed_sweep.js, implying its
own directory; it lives in seo-ship/scripts/. A fresh session follows that path and finds
nothing. |
| B9 · location-page data missing | MEDIUM | Project-side, not skill-side, but it breaks the same promise. seo-tools/locations/
holds exactly one file (automento-paty.json) while 23 location pages are live.
gen_location_pages.py can only update pages already on disk — run with no arguments it
printed skip automento-paty — no page on disk and touched nothing. The 23-page location
family is not reproducible from the repo. The invariant "generators are source, they get committed"
holds for the code and fails for the data. |
B10 · hardcoded dist/ | LOW | wire_pages.py hardcodes dist/_redirects and dist/sitemap.xml;
not in the config spec. Fine for Cloudflare Pages, wrong for any other layout. |
| B11 · cosmetic | NIT | optimize_images.py prints 0 KB -> 2802 KB (-280100% smaller)
when the baseline is empty — i.e. on every first run. |
READY-WITH-FIXES. The method and the scripts are real: they rebuilt this site's artefacts byte-for-byte from config plus data, which is a far stronger result than a code review could have given. What is not yet proven is the first-run path, because it has never been walked — and both blockers (B1, B2) live exactly there. Fix B1 and B2, then B3 and B4 for safety, and the family is safe to point at a new client. B6–B8 are half an hour of tidying. B9 is a Csupi data debt worth paying before anyone tries to regenerate a location page.
The strongest recommendation from this exercise: the next project should start by writing
seo.config.json and running every script in dry-run before any real work — that single
step surfaced both blockers in under an hour.