cat ./posts/refactoring-without-breaking.en.md
Refactoring Without Breaking: Guardrails From a Full-Codebase Cleanup
# Six rules that kept behavior intact through deploy hardening, a zero-lint cleanup, and an atomic-design restructure of this blog.
cat ./posts/refactoring-without-breaking.en.md
# Six rules that kept behavior intact through deploy hardening, a zero-lint cleanup, and an atomic-design restructure of this blog.
まだコメントはありません。
I just finished a sweeping cleanup of this blog's codebase: hardening the deploy script, adding security headers, driving lint errors and warnings to zero, splitting a 1,000+ line admin console, and reorganizing every component into atomic design.
This post is not about *what* changed — it is a memo on what I was careful about so nothing broke. The bigger the change, the more the working rules decide the quality.
When splitting the admin console, I decided never to rewrite logic and change structure at the same time. State and handlers moved into a hook, markup moved into panel components — copied, not rewritten. Even when a clever improvement was tempting, it went on a memo instead.
If the diff reads as "lines moved, nothing else," review cost and regression risk both collapse. Improvements come later, as separate changes.
The scariest code is code that fails quietly.
• After wrangler versions deploy, the deploy script now reads deployments list --json and verifies the new version is actually serving 100% of traffic — otherwise it throws
• Version ID extraction no longer trusts CLI text output alone; there is a JSON fallback
• A missing Turnstile site key logs loudly on both server and client instead of just rendering a quiet warning box
The worst pattern is "looks deployed, but the old version is still serving." Anything unverifiable is treated as a failure.
Static analysis claimed that SQL's MAX(likes - 1, 0) was undefined behavior — but SQLite's max() with two or more arguments is a scalar function and perfectly valid. Another report mistook the "id" in mail property-existence check for a bug.
"Fixing" a false positive creates a real bug. Verify against primary sources — official docs and the actual code — before touching anything. This matters even more when AI did the investigation.
React's setState-in-effect warning can be silenced by wrapping calls in setTimeout, but that defeats the warning's intent (preventing cascading renders right after mount). Mount detection and localStorage reads were remodeled as external-store subscriptions with useSyncExternalStore instead.
For the few deliberate exceptions (a full page reload after authentication), the rule is: one-line disable, with the reason written right above it. The next person can then judge whether the exception still holds.
D1 migrations are forward-only — there is no down-migration mechanism. So before touching the schema, I wrote the rollback SQL and a runbook first, and confirmed the backup path.
Depending on a canary release follows the same idea: exact-pin the version and document the upgrade procedure, so every step stays reversible. Confirm you can go back before you go forward.
Format, typecheck, lint, 91 unit tests, an admin-feature structural coverage check, an ad-slot audit, and an end-to-end run against a real server (login → create → publish → delete) — all of it ran after every change.
This blog has structural tests that assert on source file paths and content patterns, so moving a file breaks tests. It looks tedious, but updating the tests is part of the refactoring. A human's "probably fine" is not evidence.
• Don't change behavior (transplant, don't rewrite)
• Don't let anything fail silently (fail loud)
• Doubt your tools and yourself (verify with primary sources)
• Build the road back first (rollbacks and pins)
Follow these four and big changes stop being scary. Ignore them, and the casual "while I'm here" improvement becomes the thing that breaks production.
Turnstile site key が未設定のため、このフォームは送信できません。管理者は NEXT_PUBLIC_TURNSTILE_SITE_KEY を設定してください。