Next.js Integration

Push URLs to SitemapHost from your Next.js app during build, on-demand, or via ISR

This guide covers how to integrate SitemapHost with a Next.js application. You will learn how to collect URLs from your pages and push them to SitemapHost automatically during builds, via API routes, or through Incremental Static Regeneration (ISR).

Prerequisites: A SitemapHost account, a configured domain, and an API key. See the Quick Start guide if you haven't set these up yet.

Why Use SitemapHost with Next.js?

Next.js generates pages at build time (SSG), on request (SSR), or incrementally (ISR). The built-in next-sitemap package works for static builds, but has limitations:

  • ISR pages are not included until a full rebuild
  • Dynamic routes require custom logic to enumerate
  • Large sites (100k+ pages) hit build memory limits generating XML
  • No search engine notifications -- you still need to submit to Google Search Console and Bing manually

SitemapHost handles all of this. Push your URLs via the API, and we generate, host, split, and notify search engines automatically.

Option 1: Post-Build Script

The simplest approach. After next build, run a script that collects all generated pages and pushes them to SitemapHost.

Create the script

Create a file at scripts/push-sitemap.mjs in your project root:

// scripts/push-sitemap.mjs
import fs from "fs/promises";
import path from "path";

const SITEMAPHOST_API_KEY = process.env.SITEMAPHOST_API_KEY;
const SITEMAPHOST_DOMAIN = process.env.SITEMAPHOST_DOMAIN; // e.g., sitemap.yoursite.com
const SITE_URL = process.env.SITE_URL; // e.g., https://yoursite.com

async function collectPages(dir, base = "") {
  const entries = await fs.readdir(dir, { withFileTypes: true });
  const urls = [];

  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    const urlPath = path.join(base, entry.name);

    if (entry.isDirectory()) {
      urls.push(...(await collectPages(fullPath, urlPath)));
    } else if (entry.name.endsWith(".html")) {
      // Convert file path to URL
      let loc = urlPath.replace(/\.html$/, "").replace(/\/index$/, "/");
      if (!loc.startsWith("/")) loc = "/" + loc;
      if (loc === "/index") loc = "/";

      urls.push({
        loc: `${SITE_URL}${loc}`,
        lastmod: new Date().toISOString().split("T")[0],
      });
    }
  }

  return urls;
}

async function pushToSitemapHost(urls) {
  const response = await fetch(
    "https://dash.sitemaphost.app/api/sitemap/generate",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-API-Key": SITEMAPHOST_API_KEY,
      },
      body: JSON.stringify({
        domain: SITEMAPHOST_DOMAIN,
        sizeLimit: "gsc-optimized",
        urls,
      }),
    }
  );

  const result = await response.json();

  if (!result.success) {
    console.error("SitemapHost error:", result.error);
    process.exit(1);
  }

  console.log(`Pushed ${urls.length} URLs to SitemapHost`);
  console.log(`Sitemap: https://${SITEMAPHOST_DOMAIN}/sitemap.xml`);
}

const buildDir = path.resolve(".next/server/pages");
const urls = await collectPages(buildDir);
await pushToSitemapHost(urls);

Add to your build pipeline

Update package.json:

{
  "scripts": {
    "build": "next build",
    "postbuild": "node scripts/push-sitemap.mjs"
  }
}

Set environment variables in your hosting platform (Vercel, Netlify, etc.):

SITEMAPHOST_API_KEY=sk_live_xxxxxxxxxxxxxxxx
SITEMAPHOST_DOMAIN=sitemap.yoursite.com
SITE_URL=https://yoursite.com

Option 2: API Route for On-Demand Updates

For sites using ISR or server-side rendering, create an API route that pushes updated URLs to SitemapHost when content changes.

Next.js App Router (app directory)

// app/api/sitemap/push/route.ts
import { NextRequest, NextResponse } from "next/server";

const SITEMAPHOST_API_KEY = process.env.SITEMAPHOST_API_KEY!;
const SITEMAPHOST_DOMAIN = process.env.SITEMAPHOST_DOMAIN!;

export async function POST(request: NextRequest) {
  // Verify the request is authorized (e.g., from your CMS webhook)
  const authHeader = request.headers.get("authorization");
  if (authHeader !== `Bearer ${process.env.WEBHOOK_SECRET}`) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { urls } = await request.json();

  const response = await fetch(
    "https://dash.sitemaphost.app/api/sitemap/generate",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-API-Key": SITEMAPHOST_API_KEY,
      },
      body: JSON.stringify({
        domain: SITEMAPHOST_DOMAIN,
        sizeLimit: "gsc-optimized",
        urls,
      }),
    }
  );

  const result = await response.json();
  return NextResponse.json(result);
}

Triggering from a CMS webhook

When your CMS publishes new content, call this API route:

curl -X POST https://yoursite.com/api/sitemap/push \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_WEBHOOK_SECRET" \
  -d '{
    "urls": [
      {
        "loc": "https://yoursite.com/blog/new-post",
        "lastmod": "2024-01-15"
      }
    ]
  }'

Option 3: Named Sitemaps for Different Content Types

For larger sites, use named sitemaps to organize URLs by content type. This gives you better Google Search Console insights.

// lib/sitemaphost.ts
const SITEMAPHOST_API_KEY = process.env.SITEMAPHOST_API_KEY!;
const SITEMAPHOST_DOMAIN = process.env.SITEMAPHOST_DOMAIN!;

type SitemapUrl = {
  loc: string;
  lastmod?: string;
  priority?: number;
  changefreq?: string;
  images?: Array<{
    loc: string;
    title?: string;
    caption?: string;
  }>;
};

export async function pushSitemap(
  sitemapName: string,
  urls: SitemapUrl[]
) {
  const response = await fetch(
    "https://dash.sitemaphost.app/api/sitemap/generate",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-API-Key": SITEMAPHOST_API_KEY,
      },
      body: JSON.stringify({
        domain: SITEMAPHOST_DOMAIN,
        sitemapName,
        sizeLimit: "gsc-optimized",
        urls,
      }),
    }
  );

  if (!response.ok) {
    throw new Error(`SitemapHost error: ${response.status}`);
  }

  return response.json();
}

Use it in your build script:

import { pushSitemap } from "../lib/sitemaphost";

// Push blog URLs
const blogPosts = await getBlogPosts(); // Your CMS query
await pushSitemap(
  "blog",
  blogPosts.map((post) => ({
    loc: `https://yoursite.com/blog/${post.slug}`,
    lastmod: post.updatedAt,
    images: post.featuredImage
      ? [{ loc: post.featuredImage.url, title: post.title }]
      : undefined,
  }))
);

// Push product URLs
const products = await getProducts();
await pushSitemap(
  "products",
  products.map((product) => ({
    loc: `https://yoursite.com/products/${product.slug}`,
    lastmod: product.updatedAt,
    priority: 0.8,
    images: product.images.map((img) => ({
      loc: img.url,
      title: img.alt,
    })),
  }))
);

ISR Considerations

If you use Incremental Static Regeneration, pages are generated on-demand and cached. This means a full build may not include all pages.

Recommended approach: Fetch URLs from your data source (CMS, database) rather than scanning the build output.

// scripts/push-all-urls.mjs
// Fetch all published content from your CMS
const allPosts = await cms.getAllPosts({ status: "published" });
const allProducts = await cms.getAllProducts({ status: "active" });
const allPages = await cms.getAllPages({ status: "published" });

// Combine and push
const urls = [
  ...allPages.map((p) => ({
    loc: `https://yoursite.com/${p.slug}`,
    lastmod: p.updatedAt,
  })),
  ...allPosts.map((p) => ({
    loc: `https://yoursite.com/blog/${p.slug}`,
    lastmod: p.updatedAt,
  })),
  ...allProducts.map((p) => ({
    loc: `https://yoursite.com/products/${p.slug}`,
    lastmod: p.updatedAt,
    priority: 0.8,
  })),
];

await pushToSitemapHost(urls);

CI/CD Integration

GitHub Actions

# .github/workflows/sitemap.yml
name: Update Sitemap
on:
  push:
    branches: [main]
  schedule:
    - cron: "0 */6 * * *" # Every 6 hours

jobs:
  update-sitemap:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: node scripts/push-sitemap.mjs
        env:
          SITEMAPHOST_API_KEY: $
          SITEMAPHOST_DOMAIN: sitemap.yoursite.com
          SITE_URL: https://yoursite.com

Vercel Deploy Hook

If you deploy on Vercel, add a post-deploy script in vercel.json:

{
  "buildCommand": "next build && node scripts/push-sitemap.mjs"
}

Disabling Next.js Built-In Sitemap

If you were using next-sitemap or a custom sitemap.xml route, disable it to avoid conflicts:

  1. Remove the next-sitemap package if installed
  2. Delete any app/sitemap.ts or pages/sitemap.xml.ts files
  3. Update robots.txt to point to your SitemapHost URL:
User-agent: *
Allow: /

Sitemap: https://sitemap.yoursite.com/sitemap.xml

Next Steps