<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: NxFold Technology LLC</title>
    <description>The latest articles on DEV Community by NxFold Technology LLC (@nxfold_9a37c0ceb3a755db04).</description>
    <link>https://dev.to/nxfold_9a37c0ceb3a755db04</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3994459%2F7159bb20-92dc-484d-a617-02531ad0a9b4.png</url>
      <title>DEV Community: NxFold Technology LLC</title>
      <link>https://dev.to/nxfold_9a37c0ceb3a755db04</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nxfold_9a37c0ceb3a755db04"/>
    <language>en</language>
    <item>
      <title>Building a Sitemap That Can't Go Stale: A Config-Driven Architecture for Next.js App Router</title>
      <dc:creator>NxFold Technology LLC</dc:creator>
      <pubDate>Mon, 27 Jul 2026 12:34:29 +0000</pubDate>
      <link>https://dev.to/nxfold_9a37c0ceb3a755db04/building-a-sitemap-that-cant-go-stale-a-config-driven-architecture-for-nextjs-app-router-2djc</link>
      <guid>https://dev.to/nxfold_9a37c0ceb3a755db04/building-a-sitemap-that-cant-go-stale-a-config-driven-architecture-for-nextjs-app-router-2djc</guid>
      <description>&lt;h1&gt;
  
  
  Building a Sitemap That Can't Go Stale: A Config-Driven Architecture for Next.js App Router
&lt;/h1&gt;

&lt;p&gt;Every team that ships a marketing site on Next.js eventually writes the same file: &lt;code&gt;sitemap.ts&lt;/code&gt;. It loops over some blog posts, hardcodes a few static paths, and gets shipped without much thought. Six months later, someone notices Google has indexed &lt;code&gt;/dashboard/settings&lt;/code&gt;, three new landing pages never made it into the sitemap, and nobody can explain why the homepage and a random FAQ page both have &lt;code&gt;priority: 0.8&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This isn't a hypothetical. It's the natural failure mode of treating a sitemap as a one-off script instead of a piece of infrastructure. After running into exactly this problem on a bilingual (English/Arabic), App Router-based Next.js app with a large admin console living in the same codebase as the public marketing site, I want to walk through the architecture that fixed it: colocated route metadata, a build-time drift check, locale-aware hreflang generation, and a human-readable sitemap rendered through XSLT.&lt;/p&gt;

&lt;p&gt;None of this is exotic. It's mostly discipline, encoded so that the discipline can't be skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with the "loop over content" sitemap
&lt;/h2&gt;

&lt;p&gt;The default pattern most tutorials show looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/sitemap.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MetadataRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MetadataRoute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sitemap&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getAllPosts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/about&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://example.com/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lastModified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works fine for a five-page site. It breaks down once you have three categories of pain happening at once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Route sprawl.&lt;/strong&gt; A real app router tree has dozens of routes across marketing pages, a blog, case studies, service/industry landing pages, and — critically — a private admin console that lives under the same domain and the same &lt;code&gt;[locale]&lt;/code&gt; segment as the public pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locale multiplication.&lt;/strong&gt; Every public route exists in at least two languages. A sitemap that only emits English URLs is silently telling search engines that half your site doesn't exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata drift.&lt;/strong&gt; Priority, change frequency, and indexability decisions get made once, in someone's head, and never revisited when new pages are added.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix isn't a smarter loop. It's moving sitemap metadata out of a single centralized file and into the same place as the route it describes, then verifying at build time that nothing was left out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;

&lt;p&gt;At a high level, the system has four moving parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Route files in app/ directory] --&amp;gt; B[Colocated sitemapConfig metadata]
    B --&amp;gt; C[Build-time manifest generator]
    C --&amp;gt; D{Drift check}
    D --&amp;gt;|New page missing config| E[Build warning / CI failure]
    D --&amp;gt;|All routes accounted for| F[routes.manifest.json]
    F --&amp;gt; G["/sitemap.xml route handler"]
    G --&amp;gt; H[Per-locale URL + hreflang expansion]
    H --&amp;gt; I[XML response + XSL stylesheet]
    I --&amp;gt; J[Search engines / browsers]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important design decision here is &lt;strong&gt;where the metadata lives&lt;/strong&gt;. Instead of a central &lt;code&gt;sitemap.ts&lt;/code&gt; that has to know about every route in the app, each route file exports a small, typed object describing itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/[locale]/blog/[slug]/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sitemapConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;changeFrequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;monthly&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPublishedPosts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A static page that never changes gets something simpler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/[locale]/privacy/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sitemapConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;changeFrequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yearly&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;noIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And an admin page — anything under a route group like &lt;code&gt;(admin)&lt;/code&gt; — simply doesn't export &lt;code&gt;sitemapConfig&lt;/code&gt; at all, because it isn't supposed to be in the sitemap in the first place.&lt;/p&gt;

&lt;p&gt;This colocation matters more than it looks. When a developer adds a new marketing page, the sitemap behavior for that page is decided in the same pull request, by the same person, sitting right next to the code that renders it. There's no separate "update the sitemap" step to forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  The drift check
&lt;/h2&gt;

&lt;p&gt;Colocated metadata alone doesn't solve the problem — it just moves it. You still need something that notices when a page exists on disk but has no &lt;code&gt;sitemapConfig&lt;/code&gt;, or when &lt;code&gt;sitemapConfig&lt;/code&gt; exists but the route was deleted. That's the job of a small build-time script that walks the &lt;code&gt;app/&lt;/code&gt; directory, matches page files against a manifest, and either updates the manifest or fails loudly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/generate-route-manifest.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;globSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;glob&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ManifestEntry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;hasSitemapConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="na"&gt;isDynamic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="na"&gt;isAdmin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildManifest&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;ManifestEntry&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;globSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/app/[locale]/**/page.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pageFiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toRoutePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;hasSitemapConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;fileExportsSitemapConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;isDynamic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;isAdmin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(admin)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reportDrift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ManifestEntry&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isAdmin&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasSitemapConfig&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`⚠ &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; public route(s) have no sitemapConfig:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output of this script is a static JSON manifest checked into the repo (or generated in CI). Crucially, it's not a runtime dependency — it runs once at build time, and the sitemap route handler simply reads the result. This keeps the actual &lt;code&gt;/sitemap.xml&lt;/code&gt; request fast; it's not walking the filesystem on every crawl.&lt;/p&gt;

&lt;p&gt;The warning-instead-of-hard-failure choice matters too. A hard failure on missing metadata sounds appealing until a designer adds a one-off internal tools page and the entire build breaks over a sitemap entry nobody cares about. A warning that shows up in build logs and CI output gets seen without blocking shipping — and if a team wants stricter enforcement, turning it into a failure for specifically the &lt;code&gt;(public)&lt;/code&gt; and default route groups is a one-line change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Locale-aware sitemap generation
&lt;/h2&gt;

&lt;p&gt;This is the part most sitemap tutorials skip entirely, and it's the part that actually causes indexing problems in production. If your app uses &lt;code&gt;[locale]&lt;/code&gt; segment routing (the standard pattern with &lt;code&gt;next-intl&lt;/code&gt; or similar libraries), every URL in your sitemap needs to exist &lt;strong&gt;once per locale&lt;/strong&gt;, and each locale variant needs to point at its siblings via &lt;code&gt;hreflang&lt;/code&gt; alternates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;graph TD
    subgraph "One logical page"
        EN["/en/blog/scaling-postgres"]
        AR["/ar/blog/scaling-postgres"]
    end
    EN -- "hreflang=ar" --&amp;gt; AR
    AR -- "hreflang=en" --&amp;gt; EN
    EN -- "hreflang=x-default" --&amp;gt; EN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get this wrong in either direction and you get one of two failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only the default locale is in the sitemap.&lt;/strong&gt; Search engines discover the other locale through links eventually, but crawl budget and indexing lag suffer, and international search visibility is worse than it should be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locales are listed with no hreflang alternates.&lt;/strong&gt; Google may treat &lt;code&gt;/en/blog/x&lt;/code&gt; and &lt;code&gt;/ar/blog/x&lt;/code&gt; as unrelated pages competing for the same query, instead of understanding they're translations of one another.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sitemap protocol handles this through &lt;code&gt;&amp;lt;xhtml:link rel="alternate" hreflang="..."&amp;gt;&lt;/code&gt; entries inside each &lt;code&gt;&amp;lt;url&amp;gt;&lt;/code&gt; block. Expanding the manifest into locale variants looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;expandForLocales&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ManifestEntry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alternates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;locales&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;altLocale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;hreflang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;altLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;altLocale&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;hreflang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/en&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;changeFrequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changeFrequency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For dynamic routes (blog posts, service pages, case studies), this expansion happens against each item returned from &lt;code&gt;generateStaticParams&lt;/code&gt;, not against the route pattern itself — so a blog post that's only published in English so far doesn't get a broken Arabic sitemap entry pointing at a 404.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a route handler instead of the built-in &lt;code&gt;sitemap.ts&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Next.js ships a built-in &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap" rel="noopener noreferrer"&gt;Metadata Files API&lt;/a&gt; for sitemaps, including support for splitting large sitemaps via &lt;code&gt;generateSitemaps&lt;/code&gt;. For a lot of sites, that's the right tool and you should reach for it first. The custom route handler approach earns its complexity in specific circumstances:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;app/sitemap.ts&lt;/code&gt; (built-in)&lt;/th&gt;
&lt;th&gt;Custom route handler&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple, single-locale site&lt;/td&gt;
&lt;td&gt;Ideal&lt;/td&gt;
&lt;td&gt;Overkill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-locale hreflang alternates&lt;/td&gt;
&lt;td&gt;Possible, but you build the XML shape yourself either way&lt;/td&gt;
&lt;td&gt;Same effort, more control over structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human-readable output via XSL&lt;/td&gt;
&lt;td&gt;Not supported natively&lt;/td&gt;
&lt;td&gt;Full control — serve a linked &lt;code&gt;.xsl&lt;/code&gt; stylesheet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Splitting across multiple sitemap files (10,000+ URL limit)&lt;/td&gt;
&lt;td&gt;Built-in via &lt;code&gt;generateSitemaps&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Manual, but straightforward with a sitemap index file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Excluding whole route groups (e.g. admin) automatically&lt;/td&gt;
&lt;td&gt;Manual filtering in the function body&lt;/td&gt;
&lt;td&gt;Manual filtering, but colocated with route metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caching / revalidation control&lt;/td&gt;
&lt;td&gt;Framework-managed&lt;/td&gt;
&lt;td&gt;Full control (route handler &lt;code&gt;revalidate&lt;/code&gt; export, or on-demand)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The deciding factor, in this case, was the combination of hreflang complexity and wanting a sitemap that's actually pleasant to open in a browser. Both are achievable with the built-in API, but a route handler makes the XML structure and the accompanying stylesheet explicit rather than inferred.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/sitemap.xml/route.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revalidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="c1"&gt;// seconds&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadRouteManifest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;expandForLocales&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SITE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderSitemapXml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;stylesheet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/sitemap.xsl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/xml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public, max-age=0, s-maxage=3600&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The XSL stylesheet: a small, underrated detail
&lt;/h2&gt;

&lt;p&gt;Anyone who has opened a raw &lt;code&gt;sitemap.xml&lt;/code&gt; in a browser knows the experience: an unstyled wall of angle brackets that Chrome renders as a barely-readable tree. Browsers support attaching an XSLT stylesheet to XML documents via a processing instruction, which turns that same file into an actual HTML table — clickable links, priority and last-modified columns, locale badges — with zero JavaScript and zero extra requests beyond the stylesheet itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;?xml-stylesheet type="text/xsl" href="/sitemap.xsl"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;urlset&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.sitemaps.org/schemas/sitemap/0.9"&lt;/span&gt;
        &lt;span class="na"&gt;xmlns:xhtml=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/1999/xhtml"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;url&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;loc&amp;gt;&lt;/span&gt;https://example.com/en/blog/scaling-postgres&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;xhtml:link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"alternate"&lt;/span&gt; &lt;span class="na"&gt;hreflang=&lt;/span&gt;&lt;span class="s"&gt;"ar"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://example.com/ar/blog/scaling-postgres"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;lastmod&amp;gt;&lt;/span&gt;2026-06-12&lt;span class="nt"&gt;&amp;lt;/lastmod&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;changefreq&amp;gt;&lt;/span&gt;monthly&lt;span class="nt"&gt;&amp;lt;/changefreq&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;priority&amp;gt;&lt;/span&gt;0.6&lt;span class="nt"&gt;&amp;lt;/priority&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/urlset&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stylesheet itself is served from its own route handler (&lt;code&gt;app/sitemap.xsl/route.ts&lt;/code&gt;) as &lt;code&gt;application/xslt+xml&lt;/code&gt;, and browsers apply it automatically when they load the XML document directly. Search engine crawlers ignore the stylesheet entirely and parse the raw XML — it's purely a debugging and QA convenience for humans, but it's the kind of detail that makes reviewing sitemap changes in a pull request preview actually usable instead of dreadful. See MDN's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/XSLT" rel="noopener noreferrer"&gt;XSLT documentation&lt;/a&gt; for the underlying mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the admin console out of the index
&lt;/h2&gt;

&lt;p&gt;The other half of this problem is exclusion, not inclusion. A large Next.js app that hosts both a marketing site and an authenticated admin console under the same domain has to be deliberate about which routes are even eligible to appear in a sitemap or be indexed at all. In practice this means three overlapping layers of defense, not one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Route grouping.&lt;/strong&gt; Admin routes live under a &lt;code&gt;(admin)&lt;/code&gt; route group, which never exports a &lt;code&gt;sitemapConfig&lt;/code&gt;, so the manifest generator excludes them structurally rather than by convention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;generateMetadata&lt;/code&gt; sets &lt;code&gt;robots: { index: false }&lt;/code&gt;&lt;/strong&gt; on every admin page's metadata, independent of the sitemap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;robots.txt&lt;/code&gt; disallow rule&lt;/strong&gt; for the admin path prefix, as defense in depth in case a page is ever linked externally before its metadata ships.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/[locale]/(admin)/layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateMetadata&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;robots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;follow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# app/robots.txt
&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;-&lt;span class="n"&gt;agent&lt;/span&gt;: *
&lt;span class="n"&gt;Disallow&lt;/span&gt;: /*/&lt;span class="n"&gt;dashboard&lt;/span&gt;/
&lt;span class="n"&gt;Sitemap&lt;/span&gt;: &lt;span class="n"&gt;https&lt;/span&gt;://&lt;span class="n"&gt;example&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;/&lt;span class="n"&gt;sitemap&lt;/span&gt;.&lt;span class="n"&gt;xml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relying on just one of these is how &lt;code&gt;/dashboard/settings&lt;/code&gt; ends up in Google's index. &lt;code&gt;noindex&lt;/code&gt; meta tags only work if the crawler is allowed to fetch the page in the first place, and &lt;code&gt;robots.txt&lt;/code&gt; disallow rules alone don't remove a URL that's already indexed — they just stop future crawling of it. Google's own &lt;a href="https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag" rel="noopener noreferrer"&gt;guidance on robots meta tags&lt;/a&gt; is worth reading closely on this exact interaction; it's more subtle than "add one directive and move on."&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoding the sitemap in one file that nobody updates when routes change.&lt;/strong&gt; This is the root cause of almost every stale-sitemap problem; colocated metadata with a drift check is the direct fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emitting only one locale's URLs.&lt;/strong&gt; Half your content becomes invisible to search engines in other languages, even though the pages exist and are perfectly crawlable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hreflang without reciprocity.&lt;/strong&gt; If &lt;code&gt;/en/x&lt;/code&gt; points to &lt;code&gt;/ar/x&lt;/code&gt; but &lt;code&gt;/ar/x&lt;/code&gt; doesn't point back, search engines are explicit that they may ignore the annotation entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;noindex&lt;/code&gt; and expecting it to remove already-indexed pages instantly.&lt;/strong&gt; It stops future indexing; removal takes a crawl-and-recrawl cycle, sometimes accelerated by Search Console's removal tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting the 50,000 URL / 50MB per-sitemap limit.&lt;/strong&gt; Large content sites need a sitemap index file that references multiple child sitemaps, not one gigantic file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;lastmod&lt;/code&gt;, or a &lt;code&gt;lastmod&lt;/code&gt; that's just "today" on every build.&lt;/strong&gt; This actively wastes crawl budget — a &lt;code&gt;lastmod&lt;/code&gt; that's always current is worse than no &lt;code&gt;lastmod&lt;/code&gt; at all, because it signals constant change where there is none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating priority as universally meaningful.&lt;/strong&gt; Google has stated for years that it largely ignores the &lt;code&gt;priority&lt;/code&gt; field for ranking purposes; it's still useful internally for relative ordering and for other crawlers, but don't design around the idea that &lt;code&gt;1.0&lt;/code&gt; gets you ranked higher.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;Sitemap correctness is a process problem disguised as a technical one. The actual XML is trivial to generate; the hard part is making sure the metadata describing each page stays true as the app grows across quarters and multiple contributors. Three things made the biggest difference in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Colocating metadata with the route&lt;/strong&gt; turns "did we update the sitemap" into "did the file compile," which is a much harder thing to skip by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A visible drift warning beats a silent gap.&lt;/strong&gt; Nobody reads a changelog entry about sitemap coverage; everybody sees a build warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locale handling has to be designed in from the start.&lt;/strong&gt; Retrofitting hreflang into a sitemap that was built single-locale-first is more work than building it multilingual from day one, because every downstream assumption (URL shape, canonical tags, alternate link generation) has to be revisited.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep sitemap metadata as close as possible to the route it describes — ideally in the same file.&lt;/li&gt;
&lt;li&gt;Automate a build-time or CI check that flags public routes with no sitemap metadata, even if it's just a warning.&lt;/li&gt;
&lt;li&gt;Generate one sitemap entry per locale, with full hreflang reciprocity including an &lt;code&gt;x-default&lt;/code&gt; entry.&lt;/li&gt;
&lt;li&gt;Exclude private/admin surfaces at the route-group level, the metadata level, and the &lt;code&gt;robots.txt&lt;/code&gt; level — don't rely on any single layer.&lt;/li&gt;
&lt;li&gt;Use a sitemap index file once you approach the 50,000 URL limit rather than waiting until you hit it.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;lastmod&lt;/code&gt; from actual content update timestamps, not the build time.&lt;/li&gt;
&lt;li&gt;Attach an XSL stylesheet if you want humans to be able to sanity-check the sitemap without piping it through a formatter.&lt;/li&gt;
&lt;li&gt;Treat &lt;code&gt;priority&lt;/code&gt; and &lt;code&gt;changefreq&lt;/code&gt; as hints for your own internal consistency, not as ranking levers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap" rel="noopener noreferrer"&gt;Next.js — Sitemap Metadata File API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/building-your-application/routing/route-handlers" rel="noopener noreferrer"&gt;Next.js — Route Handlers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap" rel="noopener noreferrer"&gt;Google Search Central — Build and Submit a Sitemap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag" rel="noopener noreferrer"&gt;Google Search Central — Robots Meta Tag&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/search/docs/specialty/international/localized-versions" rel="noopener noreferrer"&gt;Google Search Central — Managing Multi-Regional and Multilingual Sites&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/XSLT" rel="noopener noreferrer"&gt;MDN — XSLT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sitemaps.org/protocol.html" rel="noopener noreferrer"&gt;sitemaps.org — Protocol Specification&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you've built a sitemap system that automatically kept itself honest — or one that quietly rotted for a year before anyone noticed — I'd like to hear how it happened. What's the smallest process change that would have caught it earlier?&lt;/p&gt;

</description>
      <category>seo</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>5 Performance Mistakes Quietly Slowing Down Your Next.js Site (and How to Fix Each One)</title>
      <dc:creator>NxFold Technology LLC</dc:creator>
      <pubDate>Sat, 20 Jun 2026 17:53:10 +0000</pubDate>
      <link>https://dev.to/nxfold_9a37c0ceb3a755db04/5-performance-mistakes-quietly-slowing-down-your-nextjs-site-and-how-to-fix-each-one-1138</link>
      <guid>https://dev.to/nxfold_9a37c0ceb3a755db04/5-performance-mistakes-quietly-slowing-down-your-nextjs-site-and-how-to-fix-each-one-1138</guid>
      <description>&lt;p&gt;Next.js gives you a fast site almost for free. The framework does a lot of heavy lifting out of the box, which is exactly why most performance problems aren't dramatic. There's no error, nothing crashes. Your Lighthouse score just sits at 72 and you're not sure why.&lt;/p&gt;

&lt;p&gt;Below are five mistakes I see constantly in real client codebases, ordered roughly by how often they show up. Each one is small on its own. Stacked together, they're the difference between a site that feels instant and one that feels sluggish. All examples use the App Router.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Turning whole pages into Client Components
&lt;/h2&gt;

&lt;p&gt;In the App Router, every component is a Server Component by default. The moment you add &lt;code&gt;"use client"&lt;/code&gt; at the top of a file, that component and everything it imports gets bundled and shipped to the browser. The common mistake is slapping &lt;code&gt;"use client"&lt;/code&gt; on an entire page just because one small piece needs interactivity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ The whole page ships to the client just for one button&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;

      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to push &lt;code&gt;"use client"&lt;/code&gt; down to the leaves. Keep the page on the server and isolate only the interactive part.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Page stays server-rendered; only the button is a Client Component&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;

      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// AddToCartButton.tsx&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AddToCartButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;addToCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Add to cart;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page's content renders on the server and arrives as HTML. Only the tiny button carries JavaScript. Multiply this across a whole app and your bundle shrinks dramatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Using &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; instead of &lt;code&gt;next/image&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This one is everywhere. A plain &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag ships the full-size file, blocks rendering, and does nothing clever about format or screen size. On image-heavy pages it's usually the single biggest drag on load time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Full-size file, no lazy loading, no modern format&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;next/image&lt;/code&gt; handles resizing, lazy loading, and modern formats like WebP automatically. It also reserves space for the image so your layout doesn't jump while it loads, which directly helps your Cumulative Layout Shift score.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Resized, lazy-loaded, modern format, no layout shift&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Image&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;
  &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/hero.jpg"&lt;/span&gt;
  &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Hero"&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;priority&lt;/span&gt; &lt;span class="c1"&gt;// use only for above-the-fold images like a hero&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One note: &lt;code&gt;priority&lt;/code&gt; tells Next.js to load the image immediately instead of lazily. Use it for the hero or anything visible on first paint, and leave it off everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Importing entire libraries for one function
&lt;/h2&gt;

&lt;p&gt;It's easy to write &lt;code&gt;import _ from "lodash"&lt;/code&gt; and reach for one helper. The problem is that depending on your setup, you can pull a big chunk of the library into your bundle to use a single function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Risks bundling far more than you use&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lodash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Import the specific function instead. Your bundler only includes what you actually reference.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Only the function you need&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;debounce&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lodash/debounce&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;@next/bundle-analyzer&lt;/code&gt; once and you'll usually find one or two libraries quietly eating most of your bundle. It takes ten minutes to set up and it's the fastest way to find easy wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Building data-fetching waterfalls
&lt;/h2&gt;

&lt;p&gt;Async/await reads cleanly, which is exactly why this mistake hides so well. Each &lt;code&gt;await&lt;/code&gt; pauses until the previous one finishes, so three independent fetches run one after another instead of together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Sequential — each request waits for the one before it&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reviews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getReviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If none of these depend on each other, fire them all at once with &lt;code&gt;Promise.all&lt;/code&gt;. The total time drops from the sum of all three to roughly the slowest single one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Parallel — all three start immediately&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;getOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;getReviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only keep things sequential when a later request genuinely needs data from an earlier one. Otherwise, parallelize.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Not lazy-loading heavy components
&lt;/h2&gt;

&lt;p&gt;Some components are expensive: charts, rich text editors, maps, video players, big modals. If they sit at the top of your import list, they load with the rest of the page even when the user can't see them yet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Heavy chart loads with the initial page&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;HeavyChart&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/HeavyChart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;next/dynamic&lt;/code&gt; lets you load a component only when it's actually needed, keeping it off the critical path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Loads on demand, with a fallback while it arrives&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/dynamic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HeavyChart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/HeavyChart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Loading&lt;/span&gt; &lt;span class="nx"&gt;chart&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially worth it for anything below the fold or behind an interaction, like a modal that only opens on click. There's no reason to pay for that JavaScript on first load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;None of these require a rewrite. Most are a few lines changed in files you already have. A realistic order to tackle them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the bundle analyzer to see what's actually heavy.&lt;/li&gt;
&lt;li&gt;Swap every &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; for &lt;code&gt;next/image&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fix any obvious data-fetching waterfalls with &lt;code&gt;Promise.all&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pull &lt;code&gt;"use client"&lt;/code&gt; down to the smallest components that need it.&lt;/li&gt;
&lt;li&gt;Lazy-load the expensive stuff with &lt;code&gt;next/dynamic&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do those five and re-run Lighthouse. A 70-something score usually jumps well into the 90s, and more importantly the site just &lt;em&gt;feels&lt;/em&gt; quicker.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by the team at &lt;a href="https://nxfold.com" rel="noopener noreferrer"&gt;NxFold&lt;/a&gt;, a Dubai-based studio building fast, custom websites and web apps on Next.js and React. We're always happy to talk shop — find us at &lt;a href="https://nxfold.com" rel="noopener noreferrer"&gt;nxfold.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>performance</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How Much Does a Website Cost in Dubai? (2026 Guide)</title>
      <dc:creator>NxFold Technology LLC</dc:creator>
      <pubDate>Sat, 20 Jun 2026 17:50:11 +0000</pubDate>
      <link>https://dev.to/nxfold_9a37c0ceb3a755db04/how-much-does-a-website-cost-in-dubai-2026-guide-4dkk</link>
      <guid>https://dev.to/nxfold_9a37c0ceb3a755db04/how-much-does-a-website-cost-in-dubai-2026-guide-4dkk</guid>
      <description>&lt;p&gt;"How much does a website cost in Dubai?" is the first question almost every business owner asks us — and the honest answer is: it depends on what you actually need. But "it depends" isn't helpful when you're trying to budget, so here are real ranges and the factors that move them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;For a custom-built website in Dubai in 2026, most projects fall into three bands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Brochure / marketing site (5–8 pages):&lt;/strong&gt; a professional, conversion-focused site for a service business. Fast to ship, the most common starting point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E-commerce store:&lt;/strong&gt; product catalog, checkout, payments and inventory. More moving parts, more cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web app or platform:&lt;/strong&gt; custom dashboards, bookings, user accounts, internal tools. Priced by complexity, not page count.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anyone quoting you a fixed number before understanding your goals is guessing. A good studio scopes the price to what you're trying to achieve.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually drives the cost
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Custom design vs. template.&lt;/strong&gt; A templated theme is cheap up front but looks like everyone else and is harder to optimise for conversions. Custom design costs more because someone is actually designing around &lt;em&gt;your&lt;/em&gt; brand and customers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page count and content.&lt;/strong&gt; More pages, more design and build. Having your copy and images ready speeds things up and lowers cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Functionality.&lt;/strong&gt; A contact form is simple. Online payments, booking systems, multi-language, user logins and integrations each add real engineering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance and SEO.&lt;/strong&gt; Sites built to load fast and rank — clean code, &lt;a href="https://dev.to/blog/core-web-vitals-uae"&gt;Core Web Vitals&lt;/a&gt;, structured data — take more care than a quick build. It pays back in traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who builds it.&lt;/strong&gt; A freelancer, an offshore shop and a local studio price very differently — and so does the result, the communication, and what happens when something breaks.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Cheap isn't cheap
&lt;/h2&gt;

&lt;p&gt;The lowest quote often costs the most over time: slow load speeds, no SEO foundation, a template every competitor also uses, and a rebuild within a year. We've replaced plenty of "bargain" sites that never brought in a single enquiry. The right question isn't "what's the cheapest site," it's "what's the site that pays for itself."&lt;/p&gt;

&lt;h2&gt;
  
  
  How to budget sensibly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start with the goal, not the page count.&lt;/strong&gt; More bookings? More qualified leads? Online sales? The goal decides what you actually need to build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate must-haves from nice-to-haves.&lt;/strong&gt; Launch with what drives revenue; add the rest in phase two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask what's included.&lt;/strong&gt; Hosting, SEO setup, revisions, training, post-launch support — these belong in the quote, not as surprise add-ons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get it in writing.&lt;/strong&gt; A clear scope, timeline and fixed price upfront protects both sides.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The NxFold approach
&lt;/h2&gt;

&lt;p&gt;We scope every project to what you actually need and send an itemized, fixed quote within 24 hours — no obligation. Custom design, custom code, and a site built to convert and rank, not just to exist. If you'd rather talk it through first, &lt;a href="https://dev.to/apply"&gt;tell us about your project&lt;/a&gt; and we'll give you a straight answer.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
