DEV Community

Cover image for ChatGPT Can't Cite What Bing Hasn't Indexed: Setting Up IndexNow in Next.js
Furqan Khalid
Furqan Khalid

Posted on AI-assisted

ChatGPT Can't Cite What Bing Hasn't Indexed: Setting Up IndexNow in Next.js

Most developers treat Bing as a rounding error. It's a small share of classic search, so "submit the sitemap once and forget it" felt fine.

In 2026 that's a mistake. ChatGPT search and Microsoft Copilot both draw on Bing's index to find pages to cite. If Bing hasn't crawled your new docs page, or still has last month's version of your pricing page, those assistants are working from stale or missing information.

Bing supports a simple fix: IndexNow, a protocol that lets you push "this URL changed" instead of waiting for a crawler to notice. It takes about 30 minutes to wire into a Next.js deploy.


What IndexNow is (and isn't)

  • Is: an open protocol (indexnow.org). You POST a list of changed URLs, and participating search engines (Bing, Yandex, Seznam, Naver and others) share the notification with each other.
  • Isn't: a ranking boost, or a way into Google. Google doesn't use IndexNow, so keep your sitemap and Search Console set up for Google.
  • Isn't: a guarantee. It tells Bing to look. Bing still decides whether and when to index.

On the AI side, be precise about what you're getting. Faster Bing indexing makes it possible for Bing-grounded assistants to find your update sooner. No vendor has published proof that IndexNow by itself makes ChatGPT cite a page faster. It removes one delay. It doesn't create demand.

![Deploy pipeline from content change to IndexNow ping to Bing index to AI answers, with notes on Google, lastmod and Bing's AI Performance report]

Step 1: Create and host your key

Generate a key (8–128 characters, letters, numbers and dashes):

node -e "console.log(crypto.randomUUID().replaceAll('-', ''))"
# e.g. 4f9c2b7e1a0d4c6b8e3f5a7c9d1b2e4f
Enter fullscreen mode Exit fullscreen mode

Host it as a text file at your site root whose name and content are both the key. In Next.js, the simplest way is the public/ folder:

echo -n "4f9c2b7e1a0d4c6b8e3f5a7c9d1b2e4f" > public/4f9c2b7e1a0d4c6b8e3f5a7c9d1b2e4f.txt
Enter fullscreen mode Exit fullscreen mode

Check it's live:

curl https://example.com/4f9c2b7e1a0d4c6b8e3f5a7c9d1b2e4f.txt
Enter fullscreen mode Exit fullscreen mode

The key isn't a secret (it's public by design). It proves you control the host.

Step 2: A small submit helper

// lib/indexnow.ts
const KEY = process.env.INDEXNOW_KEY!;
const HOST = process.env.SITE_HOST!; // e.g. "example.com"

export async function submitToIndexNow(urls: string[]) {
  const urlList = [...new Set(urls)].filter(u => new URL(u).host === HOST);
  if (urlList.length === 0) return { skipped: true };

  // Max 10,000 URLs per request
  for (let i = 0; i < urlList.length; i += 10_000) {
    const res = await fetch("https://api.indexnow.org/indexnow", {
      method: "POST",
      headers: { "Content-Type": "application/json; charset=utf-8" },
      body: JSON.stringify({
        host: HOST,
        key: KEY,
        keyLocation: `https://${HOST}/${KEY}.txt`,
        urlList: urlList.slice(i, i + 10_000),
      }),
    });
    // 200 = OK, 202 = accepted (key validation pending). Anything else: log it.
    if (![200, 202].includes(res.status)) {
      throw new Error(`IndexNow ${res.status}: ${await res.text()}`);
    }
  }
  return { submitted: urlList.length };
}
Enter fullscreen mode Exit fullscreen mode

Option A: Call it when content changes (CMS webhook)

If your content lives in a headless CMS, submit on publish:

// app/api/cms-webhook/route.ts
import { revalidatePath } from "next/cache";
import { submitToIndexNow } from "@/lib/indexnow";

export async function POST(req: Request) {
  if (req.headers.get("x-webhook-secret") !== process.env.CMS_WEBHOOK_SECRET) {
    return new Response("Unauthorized", { status: 401 });
  }
  const { slug } = await req.json();
  const path = `/blog/${slug}`;

  revalidatePath(path);
  await submitToIndexNow([`https://${process.env.SITE_HOST}${path}`]);

  return Response.json({ ok: true });
}
Enter fullscreen mode Exit fullscreen mode

Revalidate first, then ping. If you ping before the new version is live, Bing may fetch the old one.

Option B: Submit changed pages on deploy (Markdown/MDX in git)

If your content is files in the repo, let git tell you what changed:

// scripts/indexnow-changed.mjs
import { execSync } from "node:child_process";

const HOST = process.env.SITE_HOST;
const KEY = process.env.INDEXNOW_KEY;
const base = process.env.BASE_SHA || "HEAD~1";

const changed = execSync(`git diff --name-only ${base} HEAD -- content/`, { encoding: "utf8" })
  .split("\n")
  .filter(f => /\.(md|mdx)$/.test(f));

// Map file paths to URLs. Adjust to your routing.
// content/blog/my-post.mdx -> /blog/my-post
const urls = changed.map(f =>
  `https://${HOST}/` + f.replace(/^content\//, "").replace(/\/index\.mdx?$|\.mdx?$/, "")
);

if (!urls.length) {
  console.log("No content changes.");
  process.exit(0);
}

const res = await fetch("https://api.indexnow.org/indexnow", {
  method: "POST",
  headers: { "Content-Type": "application/json; charset=utf-8" },
  body: JSON.stringify({ host: HOST, key: KEY, keyLocation: `https://${HOST}/${KEY}.txt`, urlList: urls }),
});
console.log(res.status, urls);
if (![200, 202].includes(res.status)) process.exit(1);
Enter fullscreen mode Exit fullscreen mode
# .github/workflows/indexnow.yml
name: IndexNow
on:
  deployment_status:   # fires when Vercel/Netlify report a deploy
jobs:
  ping:
    if: github.event.deployment_status.state == 'success' && github.event.deployment.environment == 'Production'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 2 }
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: node scripts/indexnow-changed.mjs
        env:
          SITE_HOST: example.com
          INDEXNOW_KEY: ${{ secrets.INDEXNOW_KEY }}
Enter fullscreen mode Exit fullscreen mode

Waiting for deployment_status: success matters for the same reason as before: only ping once the new content is actually live.

Step 3: Make lastmod honest

Your sitemap's <lastmod> is the other freshness signal. Bing and Google both say they use it when it's accurate, and they learn to ignore sites that set every page to "today" on every build.

Use the last git commit that touched the content file:

// app/sitemap.ts
import type { MetadataRoute } from "next";
import { execSync } from "node:child_process";
import { getAllPosts } from "@/lib/posts"; // returns { slug, file }

function gitLastModified(file: string): Date | undefined {
  try {
    const iso = execSync(`git log -1 --format=%cI -- "${file}"`, { encoding: "utf8" }).trim();
    return iso ? new Date(iso) : undefined;
  } catch {
    return undefined;
  }
}

export default function sitemap(): MetadataRoute.Sitemap {
  return getAllPosts().map(({ slug, file }) => ({
    url: `https://example.com/blog/${slug}`,
    lastModified: gitLastModified(file),
  }));
}
Enter fullscreen mode Exit fullscreen mode

Vercel and many CI systems do shallow clones, which make git log wrong for older files. Fetch full history in the build (VERCEL_DEEP_CLONE=true on Vercel, fetch-depth: 0 in Actions), or compute dates at commit time and store them in front matter.

Keep dateModified in your Article JSON-LD consistent with this date, and only bump it for meaningful changes. Fixing a typo isn't an update.

Step 4: Verify in Bing Webmaster Tools

  1. Verify your site in Bing Webmaster Tools (you can import from Google Search Console in a couple of clicks).
  2. IndexNow report: confirms Bing is receiving your submissions and lists any errors.
  3. URL Inspection: check that a specific page is indexed and see which version Bing has.
  4. AI Performance report: in public preview since February 2026, this shows how often your pages are cited in Copilot and Bing's AI answers, and the grounding queries behind those citations. It's CSV export only, sampled, and has no click data, but it's the most direct first-party view of AI citations you can get from any search engine right now.

The grounding queries are the hidden gem: they show the short background searches Copilot actually ran. Compare them with your page headings. If the phrasing doesn't match, you've found a content gap (Part 4 of this series shows how to look for these systematically).

Bing-indexed is step one, not the finish line

Getting indexed only makes you eligible to be cited. To find out whether ChatGPT, Copilot and Perplexity actually cite your pages, and which pages, you have to check the answers themselves. Vista AI's citation tracking does that across engines. Even without a tool, a weekly spreadsheet of 10 important prompts and the URLs each assistant cites will tell you whether your IndexNow work is paying off.


Quick checklist

  • [ ] Key file live at /{key}.txt
  • [ ] Ping after publish/deploy, not before
  • [ ] Sitemap lastmod comes from real content changes
  • [ ] Site verified in Bing Webmaster Tools, IndexNow report clean
  • [ ] AI Performance report exported monthly

Next in the series: Should you serve Markdown to AI agents?

Top comments (3)

Collapse
 
mujtbabbas profile image
Syed Mujtaba •

The "ping after it is live" rule is the part most teams get wrong in a migration. When hundreds of product pages change at once, a deploy-time batch feels right, but a premature ping just teaches Bing to fetch the old version. The honest lastmod point matters even more there: a catalog rebuild should not mark every page as changed. How do you handle a bulk change — one submission with all changed URLs, or a throttled queue so a large migration does not look like a burst of noise?

Collapse
 
furqank729 profile image
Furqan Khalid •

For bulk changes, I prefer a queue-based approach. First, confirm the updated pages are live and returning the correct content, then submit only URLs that have genuinely changed. A throttled queue helps manage large batches and makes failures easier to retry. I’d also update lastmod only when a page’s meaningful content changes, not simply because a deployment occurred. The goal is to notify Bing about real changes, not generate unnecessary crawl signals.

Collapse
 
mujtbabbas profile image
Syed Mujtaba •

That lines up with what I have seen in practice. Confirming the pages are actually live before pinging is the part that saves you from Bing fetching the old version, and keeping lastmod honest is what keeps the signal useful. I also like the retry angle: with a queue, a failed batch is just a retry, not a lost migration. Thanks for breaking down the queue approach, that is the way I would run a large catalog change too.