Moving a Section Between Pages in the Next.js App Router
A UI section never moves alone in the App Router: its data fetches, fallbacks, and the origin page's dead data path all have to move too.
TL;DR
The KotaPortal homepage had a legacy hero section. It was moved to the tourism page. The section's data fetches were also relocated.
The KotaPortal homepage used to carry two heroes at once: a CMS-managed hero slider, and below it a legacy hero section holding a badge, a city title block, a promo carousel, and a row of quick links. The tourism page at /pariwisata showed only its slider, then jumped straight to breadcrumbs and content lists. The request was short: move the legacy hero section to the tourism page, right below its slider, with no visual redesign.
The obvious fix looked like a cut and paste: lift the section's JSX block out of the home page file, drop it into the tourism page file, save. The code compiled with zero errors. Then the page opened and the section came up empty: no promo slides, the title block falling back to defaults, the quick links row gone. The JSX was identical. The data had stayed behind.
Tracing where a section's data comes from
In the Next.js App Router, layouts and pages are Server Components: the page file runs on the server and does its data fetching before the HTML is rendered [2]. A section rendered inside a page never stands alone; it is fed by the fetches written in the file that renders it. The legacy hero was fed by three fetches living in the home page file: promo items from getEntities with limit 3, page settings from getSettings("homepage") for the badge and title copy, and the quick links from getMenus with the related_links location.
URL segments follow the folder tree: folders define URL segments, and nesting folders nests segments [3]. Moving the section from app/page.tsx (route /) to app/pariwisata/page.tsx (route /pariwisata) is a move between two different route segments, not a refactor inside one file. The display moved, and the entire data path behind it had to move with it.
Moving the fetches to the destination page
The three fetches joined the tourism page's existing Promise.all, each with a .catch returning an empty fallback so the page still renders when an individual endpoint is down.
const [promoRaw, settingsRaw, menusRaw] = await Promise.all([
getEntities({ limit: 3 }).catch(() => []),
getSettings("homepage").catch(() => []),
getMenus("related_links").catch(() => []),
]);
This per-page shape is the shape the App Router encourages. Identical fetch requests in one React component tree are memoized per request, so the component that needs the data can fetch it itself instead of receiving props from above [1]. The trade-off deserves a sentence: the same endpoints are now called from two different pages, and that memoization only spans a single page render, not across routes. Each page owns its data. The tourism page also has to tolerate empty results, because an uncached fetch blocks its page's render until it completes [1]; the .catch fallbacks keep the page upright when a request fails.
Deleting the dead data path on the origin page
With the render block gone, the home page file still carried the section's leftovers: the promoSlides state, two fetch slots inside Promise.all, the static quick-links constant with its merge logic, and the now-unused getMenus and HeroSection imports. None of it errored. The page simply kept fetching data that nothing rendered anymore, invisible to any visual review and visible only on a re-read of the file.
That cleanup doubled as the first verification gate: npx tsc --noEmit finished clean and catches straggling imports or variables. Last came the layout proof: headless screenshots of both routes showed the home page now running slider into its four-pillar section, and the tourism page running slider, legacy hero, then breadcrumbs and content.
A checklist for moving a section between pages
One small request distilled into a reusable pattern for the next section move in the App Router:
- Move the render block to the destination page file and confirm the destination route segment is the intended one [3].
- Audit which fetches feed the section and move all of them into the destination page's data loading, each with its own failure fallback [2].
- Delete the origin page's dead data path until
tscruns clean, so no fetch survives whose result never renders. - Verify both pages: typecheck, the affected component tests, and screenshots of both routes.
A section never moves alone. It drags its data path along, and the quality of the move is measured by what came with it, not by the block of JSX that successfully landed.