Skip to content

Post-release hardening: SEO correctness, reliability, chrome autosave - #2

Merged
MarsherSusanin merged 5 commits into
mainfrom
fix/post-release-hardening
Jul 3, 2026
Merged

MarsherSusanin merged 5 commits into
mainfrom
fix/post-release-hardening

Conversation

@MarsherSusanin

Copy link
Copy Markdown
Owner

Summary

Closes the actionable items from the post-v1.1.0 project review (3-agent verified backlog). Four commits, each a reviewed cluster:

SEO correctness (29344f1)

  • Full-page cache preserves X-Robots-Tag — a noindex page could previously be served indexable from a cache HIT (only Content-Type was stored).
  • Sitemap/llms.txt invalidation — every content mutation (flushAll()) now also drops the SEO endpoint caches via the new SeoCacheKeys registry; previously a just-published page stayed out of the sitemap for the full TTL. The sitemap index advertises the real latest content change per locale instead of now() on every crawl.
  • Meta length guardsmeta_description capped (max:1000) in all six admin input paths; SeoResolverService clamps title/description (255/500) at render time. The per-entity SeoOverride lookup is now cached (busted by model events).

Reliability (0b83807)

  • Per-type block schemas for the five landing blocks (hero/stats/features/testimonial/pricing) — malformed payloads are rejected server-side.
  • CMS_UPDATE_HEALTH_STRICT — opt-in: an unreachable health endpoint counts as failed and triggers rollback (default stays tolerant for hosts with blocked loopback). The previously-untested failure paths are covered: 5xx → auto-rollback, strict unreachable → rollback, default unreachable → tolerated.
  • DB restore — a recorded dump whose file is missing now fails the restore loudly instead of silently restoring code without its DB; the sqlite dump+restore path is exercised end-to-end for the first time.

Chrome builder autosave (da409d0)

The last editor without draft protection: snapshots to localStorage, restore prompt on load, beforeunload flush, and stale-snapshot drop after a successful save (chrome_saved flash → boot.justSaved). JS passed an adversarial scope/TDZ review (7/7).

Quick wins (33a0150)

Idempotent index migration (Schema::hasIndex guards), per-build memo for menu link targets, first dedicated auth-gate coverage (AuthSmokeTest).

Consciously skipped (with rationale): platform.php bump to 8.3 (repo supports 8.2 per require ^8.2 + CI matrix), SeoSetting::global() caching (one PK query, memo risks test isolation), admin RU strings i18n (admin is RU-first throughout — needs a dedicated i18n pass), phpstan baseline cleanup / image pipeline / vitest de-IIFE (dedicated sessions per prior agreement).

Test plan

  • Full suite: 187 passed (1082 assertions) — 17 new tests across SeoCacheHardeningTest, LandingBlockSchemaTest, FilesystemUpdaterSharedHostingTest (+3), CoreBackupRestoreTest, ChromeAutosaveSignalTest, AuthSmokeTest.
  • composer analyse (phpstan L5) clean, Pint clean, node --check clean on the edited JS.

🤖 Generated with Claude Code

MarsherSusanin and others added 5 commits July 2, 2026 23:57
…uards

Three post-release review fixes for the SEO-critical path:

- Full-page cache now preserves X-Robots-Tag: PageCacheService stored only
  Content-Type, so a noindex page could be served indexable from a HIT.
- Sitemap/llms.txt caches are invalidated on content mutations: every
  flushAll() (create/update/publish/delete/scheduled publish) now also
  drops the SEO endpoint caches via the new SeoCacheKeys registry —
  previously a just-published page stayed out of the sitemap for the full
  TTL. The sitemap index is cached too and advertises the real latest
  content change per locale instead of now() on every crawl.
- Meta length guards: meta_description capped at max:1000 in all six admin
  input paths, and SeoResolverService clamps title/description at render
  time (255/500) so no data source can ship multi-kilobyte meta tags. The
  per-entity SeoOverride lookup is also cached now (busted by model events
  on save/delete), removing a query from every public render.

Tests: SeoCacheHardeningTest covers the HIT robots header, service-level
cache busting, honest lastmod, clamping, and override-cache invalidation.
Full suite green (170); phpstan clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- BlockSchemaValidator now has per-type data schemas for the five landing
  leaf blocks (stats/features/testimonial/pricing items + hero fields), so
  malformed payloads are rejected server-side instead of stored as-is.
- Self-update health check gains CMS_UPDATE_HEALTH_STRICT: when on, an
  unreachable health endpoint counts as failed and triggers rollback (off
  by default — hosts with blocked loopback must not roll back healthy
  updates). An explicitly configured health_check_url now also works under
  the testing env, making the rollback path exercisable.
- The previously-untested failure paths are now covered: a 5xx health
  check auto-rolls back, strict-mode unreachable rolls back, default
  unreachable is tolerated (FilesystemUpdaterSharedHostingTest).
- DB restore: restoreSnapshot no longer silently skips a recorded dump
  whose file is missing — that's a data trap (code restored without its
  DB); it throws instead. CoreBackupRestoreTest exercises the sqlite
  dump+restore end-to-end through the public API for the first time.

Full suite green (181); phpstan clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The theme/chrome builder was the last editor without draft protection: a
crashed tab lost every unsaved header/footer/search edit. It now snapshots
state to localStorage (debounced through the existing syncPayload funnel,
gated until init completes), offers to restore a differing draft on load,
force-saves on beforeunload, and — mirroring the page/post editors — drops
the stale snapshot on the load right after a successful save via a
chrome_saved flash surfaced as boot.justSaved.

Tests: ChromeAutosaveSignalTest covers the flash → boot signal round-trip;
the JS block passed an adversarial scope/TDZ/flow review (7/7).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The composite status/published_at index migration now guards with
  Schema::hasIndex both ways, so a hand-created index (or a kept index
  with a lost ledger row) no longer hard-fails a deploy.
- ResolvedChromeViewModelFactory memoizes menu link-target lookups per
  build: header/footer/legal menus referencing the same page cost one
  query instead of one per occurrence.
- AuthSmokeTest gives the admin login gate its first dedicated coverage:
  guest redirect, wrong password, success + last_login_at, blocked
  account, logout.

Full suite green (187); phpstan clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A follow-up audit of PR #2's own diff surfaced defects the fixes introduced;
all are corrected here before merge:

- [HIGH] Rollback mislabeled as failed: apply()'s try wrapped both
  restoreSnapshot() and the post-restore health check, so under strict mode
  a whole-window endpoint outage marked a SUCCESSFUL restore as
  status=failed/restore_status=failed — panicking an operator over a healthy
  site. rollback() now fails only when restoreSnapshot() itself throws; a
  post-restore health-check throw yields status=rolled_back /
  restore_status=health_unverified and still resyncs installed_version to the
  reverted code. (Also fixes the installed_version desync + double-jeopardy.)
- [HIGH] Stale SeoOverride cache: ContentEntityCleanupObserver purges
  seo_overrides via a query-builder delete, which fires no model event, so
  the new resolver override-cache was never busted on parent delete — a
  reused id could serve a deleted override's meta/robots for the full TTL.
  The observer now forgets the override keys across locales.
- [MED] Meta clamp used Str::limit (display width), over-truncating CJK
  titles/descriptions that passed max:N validation; now clamps by character
  count (mb_substr). META_DESCRIPTION_MAX raised 500→1000 to match the
  write-path validation ceiling so saved values aren't silently cut.
- [MED] Landing block schemas now cap item count (60) and per-field string
  length (2000) — an authenticated writer could otherwise store a
  multi-megabyte block expanded into every cached render.

Tests: whole-window-outage rollback, override-cache bust on delete,
char-vs-width clamp, block count/length caps. Full suite green (193);
phpstan clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MarsherSusanin
MarsherSusanin merged commit 3f6f1b4 into main Jul 3, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant