At 3AM in a Dubai coworking space, I was live in the Vercel deployment logs of a real estate platform for a client in Abu Dhabi. The error count spiked as Arabic-speaking users from Saudi started hitting 404s. Classic App Router gotcha: nested loading states breaking dynamic routes. I stared at my 7th coffee cup and realized this migration was going to be worse than debugging PHP legacy code.
This post isn't some pristine step-by-step guide. It's about the actual pain points we hit while moving Reach Home Properties — a UAE real estate platform that serves both Dubai developers and Riyadh buyers — from Pages Router to App Router last fall. The project clocked 85 commits across two weeks, burned through $1,200 in Vercel compute hours, and left me with a permanent twitch in my right eye when I see (.) in route definitions.
The Ugly First Step: Folder Structure PTSD
I'd done this before in personal projects. But this was production traffic with 15K MAU expecting Arabic RTL support and fast load times for users on 3G connections in GCC cities.
We started by creating the /app directory structure alongside /pages. Next.js 14 lets you do that temporarily. Big mistake. The API routes in /pages/api still worked, but the new App Router APIs under /app/api couldn't share context. We spent 3 days rewriting API middleware to use the new NextRequest/NextResponse format. For future reference: req.cookies vs request.cookies.get() breaks things silently.
The client's content team — who I swear write their page meta tags by hand for SEO reasons — kept creating .md files in the wrong folders. We ended up writing a folder-guard.ts script that kills the build if content appears in unintended directories. Painfully specific? Yes. Worth it? Also yes.
The Real Fight Begins: Client Components vs Server Components
The marketing team added Arabic language tags in content. Big whoop, right? Except we learned the hard way that React Server Components don't play nice with i18next hydration. We ended up wrapping all translation providers in a use client component, even though the docs say it's discouraged.
Wait — remember that nested loading state issue I mentioned earlier? We had a layout.tsx with a Suspense boundary that tried to handle fallback content for both property listings and user reviews. In practice, the error boundary would catch one but not both. The fix involved splitting them into separate template files with individual loading states. Not elegant, but users stopped complaining about blinking skeletons.
Build-Time Drama in the UAE Desert
Building in Frankfurt, deploying to Abu Dhabi users? Nightmare. The initial App Router build clocked 6.2 seconds on Turbopack (we tried), which is great until you realize Dubai internet speeds average 40Mbps and every extra second loses 3.2% of mobile visitors. We shaved 40% off build time by locking next.config.js to { output: 'standalone' }. Don't ask me why it works.
One concrete number for the skeptics: The first App Router deployment reduced homepage LCP by 0.8s. But TTI actually increased 0.4s because of client component waterfalls. We fixed it by preloading critical fonts and using React.lazy with suspense for non-critical listing components. Now the metrics look less like a horror story.
The Arabic Dilemma (and Why Docs Don't Help)
Here's something the Next.js docs don't mention: Arabic diacritics break Turbopack when reading route names. We had a /properties/[slug] route where some slugs included tatdid (ًٌٍَ) characters. Turbopack spat out unhelpful errors that looked like syntax bugs but were actually encoding issues.
The real kicker? We fixed it by adding process.env.NEXT_DISABLE_TURBOLOGS=1 in the build command. Not related, but it helped somehow. Also, we added this to our _document.tsx:
<html lang="ar" dir="rtl">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1"/>
</html>But we had to override the default scroll restoration in _app.tsx. Arabic users scroll differently — more vertical, faster — and the router was fighting their gestures.
Did Anybody Mention Environment Variables?
Oh right, the move to App Router coincided with Vercel's edge runtime changes. We had a server action that connected to a legacy MySQL database hosted on AWS Bahrain. Suddenly $DATABASE_URL was undefined, and the app started throwing TypeError: fetch failed errors at midnight UAE time.
We found the cause in next.config.js — we were using an .env file inside /app, which doesn't work because of the build isolation. Moved all vars to the project root and added:
const runtimeConfig = {
server: {
dbUrl: process.env.DATABASE_URL,
}
}Then we passed it to createServerComponentSupabaseClient() with custom headers. Not the cleanest, but it works for now.
I'll be real — if you're starting a new Next.js project in 2026, skip the Pages Router. But if you're maintaining a UAE production app with a mix of Arabic clients and GCC traffic, prepare for a week-long deployment hangover.
This migration taught me one thing: App Router gives you power, then asks how badly you want it. We shipped, the client paid the invoice, and I took two days off just to sleep.
Need help keeping your Next.js stack sane? Ping me on sarahprofile.com/contact. I'm mostly recovered, and I know where all the landmines are around here.