Moving /wisata: a 308 Redirect and a Menu That Survives
Renaming a public route is three moves at once: relocate the route tree, install a permanent 308, and migrate menu rows without stomping admin edits.
TL;DR
Renaming the public /wisata route to /pariwisata took more than a file move. The team added permanent 308 redirects for the old path and wildcard subpaths to preserve bookmarks and POST requests. A guarded MySQL migration renamed only untouched menu rows, shifted ordering, and left CMS-edited content intact.
At a glance, moving a route looks like a two-minute job: git mv the wisata folder to pariwisata, one UPDATE in the database, deploy. While reassembling the portal into the four DISPORAPAR pillars, that was exactly the plan in my head. Reality: portal menus still spoke the old name, people's bookmarks could go dead, and the hero title on the front page kept greeting visitors with an identity that is no longer in use.
A public route rename turns out to be three steps that must land together: move the route tree, teach the server the old address so existing visitors still connect, and move the data rows that point at the old path. The twist is that the data is alive: menus may already have been edited by admins through the CMS, so the migration must not simply stomp over them.
308: the old address keeps working
The glue toward the outside world is redirects in next.config.js: /wisata heading to /pariwisata, plus every subpath through the wildcard form, both installed permanently, which means 308 [12]. I deliberately picked 308 over 301: 308 says the resource moved for good and the client must not alter the request method and body [13]. A POST bookmark stuck on the old path still POSTs at the new address. The redirect is checked before the filesystem, and the query string rides along to the destination [12]. The admin area lives on a different path and is not covered by these rules, so it never redirects.
Menu data: shift, never overwrite
The second glue lives in MySQL, migration 000061. The opposite of the redirect: this one is about the inside. Header and bottom_nav menus are seeded by migrations, but their content may already have been edited by admins through the CMS. So the Wisata to Pariwisata rename is wrapped in a tight guard: only rows still matching the seed values exactly (location, label, href all matching) get renamed [14].
-- rename only rows still at their seed values
UPDATE menu
SET label = 'Pariwisata',
href = CONCAT('/pariwisata', SUBSTRING(href, 8))
WHERE location = 'header'
AND label = 'Wisata'
AND href LIKE '/wisata%';Derived addresses, for example wisata prefixes carrying a category parameter or a slug, are rewritten to the new prefix via SUBSTRING(href, 8), so they point straight at the final destination and never detour through the redirect [14]. Three new pillar items are INSERTed right after Beranda, every other header item shifts sort_order +3 behind a NOT IN guard, and Pariwisata is pinned at position 4. The identity rows site_name plus hero_title only swap to DISPORAPAR when the value is still the legacy MuDe; anything edited through the CMS survives.
The doctrine sits in that migration's own comment: menu items may already have been edited by admins, so the migration shifts the order of other items rather than overwriting them. The down migration mirrors everything: DELETE the three pillar rows, shift back -3, rename back, and restore derived prefixes via SUBSTRING(href, 12). On the frontend side, the app router folder moved whole, plus three new pillar pages and an extended portal e2e spec. One commit, two stores (Next.js config and MySQL data), one point of glue in the form of a redirect. Whoever knocks on the old address still arrives; whatever already sits at the new address had its data moved cleanly.