Restructuring the Blog URL Scheme
Migrating from flat posts/*.html to folder-per-post posts/slug/index.html. Zero migration cost because no links had been shared publicly. And a Safari caching lesson about why meta-refresh is never the right redirect.
The Decision
The original blog structure stored each post as a flat file: posts/slug-name.html. That works, but it has a limitation — each post lives at a URL with a .html extension, and there's no clean way to co-locate supporting assets (images, attachments) alongside a post without naming gymnastics.
The alternative is folder-per-post: posts/slug-name/index.html. Each post gets its own directory. The URL becomes /posts/slug-name — no extension, cleaner, and the post's folder can contain anything else the post needs.
Two options:
- Option A: Migrate to folder-per-post; retire the old flat URLs
- Option B: Keep the flat structure to avoid breaking existing links
The answer was immediate: no links had been shared publicly. The blog existed but hadn't been promoted anywhere. Zero migration cost → Option A, immediately.
What the Migration Covered
A few dozen posts existed. Each one needed to move from posts/slug.html to posts/slug/index.html. A few posts had accompanying image assets — those moved into their respective post folders. The journey.html root post became journey/index.html.
The homepage redirect pointed to /journey — that URL now works correctly under the new structure. The old flat files were deleted.
The blog root at blog.banavu.com serves as the homepage. The /journey URL path was retired at this point; the homepage is just the root.
The Safari Caching Problem
After the migration, navigating to some old URLs surfaced a problem: Safari had cached old <meta http-equiv="refresh"> redirect pages. These are client-side HTML redirects — a <meta> tag in the <head> that tells the browser to navigate to a different URL after a short delay.
Safari aggressively caches these. If you visit a page that contains a meta-refresh, Safari stores the page in its cache. Later visits load the cached version — including the redirect — even if the server no longer sends that page at all. The fix for a user is a hard refresh (hold the reload button in Safari → Reload Without Content Blockers, or clear site data). Not a great user experience to ask for.
The deeper lesson: meta-refresh should never be used for permanent redirects. Firebase Hosting supports HTTP 301 redirects configured in firebase.json. A 301 is an HTTP-level signal — the browser understands it as "this URL has permanently moved" and doesn't cache the redirect HTML itself. For any redirect that needs to persist reliably, HTTP 301 is the right tool.
// firebase.json
"redirects": [
{
"source": "/old-slug",
"destination": "/posts/new-slug",
"type": 301
}
]
Why Timing Zero-Migration Cost Matters
Structural decisions like URL scheme get exponentially harder to change as adoption grows. When the blog had no external links, the cost to migrate was essentially zero — rename some files, delete some old ones. Once those URLs are in bookmarks, search engine indexes, newsletters, social shares, the cost becomes much higher: you either maintain redirects indefinitely or you accept broken links.
The right time to make a structural improvement is before anyone depends on the old structure. This applied the same logic I use for the app's data pipeline: refactor when the risk is low, not after the system has production load on it.
What I Took From It
- Meta-refresh is not a real redirect. It's an HTML trick that happens to cause navigation. Browsers cache it like any other page, which means you can't reliably remove it once it's out there. HTTP 301 from the server is the only correct answer for permanent redirects.
- No external links = free migration. Before promoting any blog post URLs, the structural choice was consequence-free. Now that the folder-per-post structure is in place, adding supporting assets to any post is trivial — they go in the post's folder and are served at relative paths.
- The right URL structure makes future work easier. The flat file scheme would have required increasingly awkward workarounds as posts accumulated supporting content. Making the structural fix early avoided those workarounds entirely.
