<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Level Up Digital Marketing Group]]></title><description><![CDATA[Level Up Digital Marketing Group]]></description><link>https://levelupdigitalmarketinggroup.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 31 Oct 2026 11:38:13 GMT</lastBuildDate><atom:link href="https://levelupdigitalmarketinggroup.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Your Sitemap's lastmod Is Lying to Google. Generate It From the CMS in Next.js]]></title><description><![CDATA[Open the sitemap on almost any marketing site built in the last few years and look at the lastmod column. Every URL has today's date. Or every URL has the…]]></description><link>https://levelupdigitalmarketinggroup.hashnode.dev/your-sitemap-s-lastmod-is-lying-to-google-generate-it-from-the-cms-in-next-js</link><guid isPermaLink="true">https://levelupdigitalmarketinggroup.hashnode.dev/your-sitemap-s-lastmod-is-lying-to-google-generate-it-from-the-cms-in-next-js</guid><category><![CDATA[SEO]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[Sanity ]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[accounts Level Up Digital Marketing Group]]></dc:creator><pubDate>Tue, 20 Oct 2026 21:29:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aa988be6dc9fe478a6f0f23/b22434c2-5a4a-43c8-94c9-944a56ae4cdf.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Open the sitemap on almost any marketing site built in the last few years and look at the <code>lastmod</code> column. Every URL has today's date. Or every URL has the date of the last deploy. Or there is no <code>lastmod</code> at all. All three mean the same thing to Google: the sitemap is not telling it anything about which pages changed, so it will decide for itself what to recrawl and when.</p>
<p>That matters more for a small site than a large one. A 40 page service business site does not get crawled constantly. If you fix a price on a service page or rewrite a post, you want Google to come back for that page soon, not for the homepage it already has. An honest <code>lastmod</code> is the cheapest signal you can send, and most Next.js sites throw it away.</p>
<h2>What Google does with lastmod</h2>
<p>Google has said plainly that it uses <code>lastmod</code> when it is consistently accurate, and ignores it on sites where it is not. The other sitemap fields, <code>changefreq</code> and <code>priority</code>, are ignored outright. So the entire value of the file comes down to two things: a complete list of canonical URLs, and a date per URL that actually reflects when the content changed.</p>
<p>"Actually reflects" is the hard part. A build timestamp is not a content change. A git commit that touched a layout component is not a content change for every page that uses it. The only source that knows when the content changed is the CMS.</p>
<h2>Pull the date from the CMS, not the build</h2>
<p>On a Sanity and Next.js stack this is a small query. Every Sanity document carries <code>_updatedAt</code>, which moves when the document is edited and published. That is the date you want. Here is a sitemap route for the App Router that reads it directly.</p>
<pre><code class="language-ts">// app/sitemap.ts
import type { MetadataRoute } from "next";
import { client } from "@/sanity/client";

const BASE = "https://www.example.com";

const query = `{
  "posts": *[_type == "post" &amp;&amp; defined(slug.current) &amp;&amp; !(_id in path("drafts.**"))]{
    "slug": slug.current, _updatedAt
  },
  "services": *[_type == "service" &amp;&amp; defined(slug.current) &amp;&amp; !(_id in path("drafts.**"))]{
    "slug": slug.current, _updatedAt
  },
  "home": *[_type == "homePage"][0]{ _updatedAt }
}`;

export const revalidate = 3600;

export default async function sitemap(): Promise&lt;MetadataRoute.Sitemap&gt; {
  const { posts, services, home } = await client.fetch(query);

  return [
    { url: `${BASE}/`, lastModified: home?._updatedAt ?? new Date() },
    ...services.map((s: any) =&gt; ({
      url: `${BASE}/services/${s.slug}`,
      lastModified: s._updatedAt,
    })),
    ...posts.map((p: any) =&gt; ({
      url: `${BASE}/journal/${p.slug}`,
      lastModified: p._updatedAt,
    })),
  ];
}
</code></pre>
<p>Three details in that file do the real work. The draft filter keeps unpublished documents out, which also keeps their URLs from leaking before they exist. The <code>revalidate</code> export caches the sitemap for an hour so the route is not hitting Sanity on every crawler request. And nothing in the file uses <code>new Date()</code> except the one fallback, because a date you make up is worse than no date.</p>
<h2>Do not let a layout change bump every page</h2>
<p>The trap on the other side is being too clever. Some teams compute <code>lastmod</code> from the git history of the page file, or from the deployment time, so that a change to the global header makes every URL look fresh. Google notices within a few crawls that the dates do not correspond to visible changes and stops trusting the file. You are back to zero, with extra code.</p>
<p>If a layout change really does alter the content of every page, say you added a new section to every service page, that is a content change, and the right move is to touch the documents in Sanity so <code>_updatedAt</code> moves. A single migration script does it.</p>
<pre><code class="language-ts">// scripts/touch-services.ts
import { createClient } from "@sanity/client";

const client = createClient({
  projectId: process.env.SANITY_PROJECT_ID!,
  dataset: "production",
  token: process.env.SANITY_WRITE_TOKEN!,
  apiVersion: "2025-01-01",
  useCdn: false,
});

const ids: string[] = await client.fetch(`*[_type == "service"]._id`);
const tx = ids.reduce((t, id) =&gt; t.patch(id, { set: { touched: new Date().toISOString() } }), client.transaction());
await tx.commit();
</code></pre>
<p>Setting any field, even one the frontend never reads, moves <code>_updatedAt</code>. It is deliberate, it is reviewable, and it only happens when you decide the content changed.</p>
<h2>Keep the sitemap complete and canonical</h2>
<p>The second half of the job is the URL list itself. A few checks worth running once:</p>
<p>Every URL should be the canonical form. If the site serves <code>www</code>, the sitemap uses <code>www</code>. No trailing slash mismatches, no <code>http</code>. If a page has a <code>canonical</code> tag pointing somewhere else, it does not belong in the sitemap at all.</p>
<p>Paginated listing pages, tag pages, and search result pages usually should not be in there. They dilute the signal and most of them are <code>noindex</code> anyway. The sitemap is for pages you want ranked, which on a service site means the homepage, the service pages, the location pages, and the posts. That is also the set of pages your internal links should concentrate on, and if you have never audited that, <a href="https://www.levelupdigitalmarketinggroup.com/journal/internal-linking-graph-site-structure-google">the internal link graph Google actually uses</a> is the place to start, because a sitemap can list a page but it cannot make it matter.</p>
<p>Above about 50,000 URLs you need a sitemap index. Almost no service business is near that, but if you run a large catalog or city by service matrix, the App Router supports it through <code>generateSitemaps</code>, which splits the file by id and serves <code>/sitemap/0.xml</code>, <code>/sitemap/1.xml</code>, and so on. The same <code>_updatedAt</code> approach applies to each chunk.</p>
<h2>Make it update itself</h2>
<p>The last piece is not letting the sitemap go stale between deploys. With <code>revalidate</code> set, the route refreshes on its own schedule. For a tighter loop, point a Sanity webhook at a revalidation route that calls <code>revalidatePath("/sitemap.xml")</code> whenever a document publishes. Then the sitemap updates within seconds of an editor hitting Publish, with the right date, and nobody touched a deploy.</p>
<p>This is the same pattern we use when we <a href="https://www.levelupdigitalmarketinggroup.com/services/build-your-own-ai-system">build a content system that publishes and revalidates on its own</a> for a client: the CMS is the source of truth for what changed and when, the frontend reads that truth, and the sitemap is one more consumer of it rather than a file someone remembers to regenerate.</p>
<h2>A five minute check for your own site</h2>
<p>Open <code>/sitemap.xml</code>. If every <code>lastmod</code> matches, or is missing, the file is not helping you. Open Search Console, go to Sitemaps, and confirm the submitted file is the one you think it is and that the discovered URL count matches what you expect. Then pick one page you edited last week and confirm its date in the sitemap matches the edit. If all three pass, you have a sitemap Google can use. If not, the fix above is about thirty lines.</p>
<p><em>Level Up Digital Marketing Group builds Next.js and Sanity sites for service businesses in San Diego and runs the SEO behind them. More at <a href="https://www.levelupdigitalmarketinggroup.com">levelupdigitalmarketinggroup.com</a>.</em></p>
]]></content:encoded></item></channel></rss>