Fergell Murphy

Blog
Home
About
Contact

SEO-friendly sitemaps in Next.js App Router

On a content site, a sitemap is not a ceremonial SEO checkbox. It is how I tell crawlers which URLs matter, especially blog posts that are not yet heavily interlinked. My personal site uses the App Router metadata route convention: a sitemap.ts file that returns static pages plus every entry from blogPosts.

What the sitemap needs to cover

For a personal developer site I include the homepage, about, blog index, privacy policy, terms, contact, and each blog post. I do not include utility routes, preview URLs, or anything blocked by robots. If a page should not be indexed, it should not appear in the sitemap either.

  1. Canonical absolute URLs with a consistent base domain.
  2. Static pages listed explicitly so they cannot be forgotten.
  3. Blog posts generated from the same data source the UI uses.
  4. Sensible lastModified values when content changes.

The sitemap.ts pattern I use

Next.js will serve this as /sitemap.xml. The important idea is single-source blog data: if a post exists in lib/data.tsx, it automatically gets a sitemap entry.

import { blogPosts } from "../../lib/data";
export default async function sitemap() {
const baseUrl = "https://fergellmurphy.co.za/";
const staticPages = [
"",
"about",
"blog",
"privacy-policy",
"terms-and-conditions",
"contact",
].map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date().toISOString(),
changeFrequency: "weekly" as const,
priority: 1,
}));
const blogRoutes = blogPosts.map((post) => ({
url: `${baseUrl}blog/${post.slug}`,
lastModified: post.updatedAt || new Date().toISOString(),
changeFrequency: "weekly" as const,
priority: 1,
}));
return [...staticPages, ...blogRoutes];
}

I keep priority simple. Search engines do not treat those numbers as a ranking cheat code. Consistency and completeness matter more than micro-optimizing priority fields.

Why updatedAt belongs on blog posts

When I substantially rewrite a post, I set updatedAt on that entry. The sitemap then emits a fresher lastModified for that URL. That does not force a recrawl overnight, but it gives crawlers a clear signal that the document changed. For thin typo fixes I usually leave the date alone. For full rewrites aimed at better reader value, I update it.

Using the same field for display logic later is a bonus. One date in the content model beats scattered hard-coded strings.

robots.txt that points at the sitemap

My robots file is intentionally short. Allow crawling, declare the sitemap, avoid clever disallow rules unless there is a real reason.

User-Agent: *
Allow: /
Sitemap: https://fergellmurphy.co.za/sitemap.xml

In the App Router you can keep a static src/app/robots.txt or use a robots.ts metadata route. Either works. The critical part is that the sitemap URL is absolute and reachable in production.

Google Search Console steps I actually run

  1. Verify the property for the canonical domain.
  2. Open Sitemaps, submit https://fergellmurphy.co.za/sitemap.xml.
  3. Confirm the discovered URL count matches static pages plus blog posts.
  4. Use URL Inspection on a new post after deploy to request indexing when needed.
  5. Watch for soft 404s, redirects to the wrong host, or excluded pages.

If Search Console shows fewer URLs than expected, I check for a missing static route in the array or a blog slug that never made it into blogPosts. If it shows more, I look for duplicate trailing-slash variants or staging domains leaking into production metadata.

Common mistakes that waste the sitemap

Shipping relative URLs, mixing www and apex hosts, listing noindex pages, and forgetting new legal pages are the usual ones. Another subtle issue: generating blog URLs from a different list than the blog UI. If the homepage reads one array and the sitemap reads another, drift is inevitable.

A sitemap will not fix thin content, slow pages, or missing metadata. It will make good pages discoverable faster. Pair it with real titles, descriptions, internal links, and content worth ranking. Then keep the generator boring and automatic so you never hand-edit XML again.

Keeping static routes honest as the site grows

Every time I add a marketing page, I update three places in the same change whenever possible: the page itself, navigation/footer links, and the static array in sitemap.ts. Forgetting the third is how About pages exist for humans and remain invisible in Search Console's sitemap count.

I include privacy and terms because they matter for trust and for monetization setups, not because they are glamorous. Contact and blog index are obvious. The homepage uses an empty route string so the canonical URL stays clean.

If you introduce trailing slash differences or wwwaliases, fix redirects first. A sitemap cannot heal a split-host problem; it can only advertise it more efficiently.

Validating locally before you submit

After deploy to a preview or production environment, open /sitemap.xml and skim for missing or duplicated paths. Compare the URL count to staticPages.length + blogPosts.length. If they diverge, debug the generator before submitting in Search Console.

I also click a few blog URLs from the XML to ensure slugs match real routes. A renamed slug that still appears in an old sitemap entry is a soft 404 waiting to happen if you did not ship redirects.

For robots, fetch /robots.txt and confirm the Sitemap line uses HTTPS and the canonical host. Mixed content or HTTP sitemap references are unnecessary friction.

What a sitemap will not fix

Thin excerpts, duplicate titles, slow LCP, and missing internal links will still limit performance in search. The sitemap is discovery infrastructure. It amplifies the quality of what you publish; it does not create quality.

That is why I tie sitemap work to content work. When I expand posts for reader value, I update updatedAt, verify the XML, and only then ask Search Console to notice. Process over superstition.

If you maintain one content array, one sitemap function, and one robots pointer, you already have a more reliable SEO foundation than many larger brochure sites with hand-edited XML files.

changeFrequency and priority without superstition

I often leave changeFrequency at weekly for a personal site because it is an honest average across pages that update when I publish. Search engines do not treat these hints as commands. Spending hours tuning priority floats from 0.7 to 0.8 is theater.

What is not theater is correctness: absolute URLs, stable slugs, updated dates on real rewrites, and noindex pages excluded from the feed. Get those right and you have done the high-leverage work.

If a section of the site is truly volatile, you can specialize later. Start with one boring generator that never drifts from your content module. Complexity is optional; consistency is not.

Revisit the sitemap whenever you change domain strategy, add a language path, or move the blog to a new base route. Those moments are when silent SEO regressions appear.

Operational cadence after launch

A sitemap is not a one-time artifact. Each time I publish or deeply revise a post, I confirm updatedAt, deploy, open the XML once, and spot-check the new URL. When Search Console reports discovery issues, I compare its counts to my local array lengths before I invent exotic theories.

I also keep redirects intentional. If a slug must change, I ship a redirect and remove the old URL from content data so the sitemap stops advertising a dead path. Leaving both live without a plan creates duplicate or soft-404 noise.

For a personal Next.js site, this operational rhythm matters more than advanced SEO tooling. Boring consistency wins.

Closing thoughts

If you maintain static routes, map blog posts from the same module your UI uses, point robots at the XML, and verify in Search Console, you have a durable sitemap practice for a Next.js personal site.

© 2025-2026 Fergell MurphyTerms And ConditionsPrivacy Policy