Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

Bing hasn't seen your new pages yet. Ping IndexNow from your Next.js deploy

Last week we opened Bing Webmaster Tools for our own site and found 103 URLs in the sitemap, 77 indexed, and a red banner at the top: "Set up IndexNow". The sitemap was fine. Bing just hadn't come back to read it. Sitem

Last week we opened Bing Webmaster Tools for our own site and found 103 URLs in the sitemap, 77 indexed, and a red banner at the top: "Set up IndexNow". The sitemap was fine. Bing just hadn't come back to read it.

Sitemaps are a pull model: you publish a list and wait for crawlers to visit. IndexNow is push: when a URL changes, you tell the search engines yourself. Bing, Yandex, Seznam, Naver and Yep share submissions with each other, so one ping reaches all of them. Google doesn't take part, so keep your sitemap. This is an addition, not a replacement.

Here's how we're adding it to a Next.js site in about 40 lines, with no plugin and no vendor.

1. Create a key and host it

IndexNow checks you own the domain with a key file. The key is any 8 to 128 character string of letters, numbers and dashes. Generate one:

node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"

Put it in public/ as a text file whose name and contents are both the key:

public/3f9c2e...b71a.txt   ->  contains: 3f9c2e...b71a

Deploy, then check that https://yoursite.com/<key>.txt returns the key as plain text. If your middleware rewrites or redirects unknown paths, exclude .txt files, or the key check will fail.

2. Work out which URLs actually changed

Don't ping every URL on every deploy. IndexNow is a signal that something changed, and resubmitting unchanged pages dilutes it.

The simplest honest source of "what changed" is git. If your pages live in app/ and your articles in content/, map changed files to routes:

// scripts/changed-urls.mjs
import { execSync } from 'node:child_process';

const SITE = 'https://yoursite.com';

export function changedUrls(base = 'HEAD~1') {
  const files = execSync(`git diff --name-only ${base} HEAD`, { encoding: 'utf8' })
    .split('\n')
    .filter(Boolean);

  const urls = new Set();
  for (const f of files) {
    // app/(marketing)/services/web/page.tsx -> /services/web
    let m = f.match(/^app\/(.*)\/page\.(tsx|jsx|mdx)$/);
    if (m) urls.add(`${SITE}/${m[1].replace(/\([^)]*\)\/?/g, '')}`);
    if (f === 'app/page.tsx') urls.add(`${SITE}/`);

    // content/insights/my-post.mdx -> /insights/my-post
    m = f.match(/^content\/insights\/(.+)\.mdx$/);
    if (m) urls.add(`${SITE}/insights/${m[1]}`);
  }
  return [...urls];
}

Adjust the patterns to your folder structure. Route groups like (marketing) are stripped because they never appear in the URL. Dynamic segments like [slug] can't be mapped from a file name, which is what the content rule is for.

3. Send the ping

// scripts/indexnow.mjs
import { changedUrls } from './changed-urls.mjs';

const HOST = 'yoursite.com';
const KEY = process.env.INDEXNOW_KEY;

const urlList = changedUrls(process.env.GIT_BASE || 'HEAD~1');
if (urlList.length === 0) {
  console.log('IndexNow: nothing changed');
  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,
  }),
});

console.log(`IndexNow: ${res.status} for ${urlList.length} URL(s)`);
if (![200, 202].includes(res.status)) process.exitCode = 1;

What the status codes mean:

  • 200 or 202: accepted. 202 means the key is still being validated, which is normal on your first submission.
  • 400: the request is malformed.
  • 403: the key file wasn't found or doesn't match the key.
  • 422: a URL doesn't belong to host. Check www against non-www.
  • 429: you're sending too often.

4. Run it after the deploy, not before

The ping has to go out once the new pages are live, otherwise the crawler fetches the old version. On Vercel, trigger it from a GitHub Action when the production deployment succeeds:

# .github/workflows/indexnow.yml
name: IndexNow
on: deployment_status
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: 20
      - run: node scripts/indexnow.mjs
        env:
          INDEXNOW_KEY: ${{ secrets.INDEXNOW_KEY }}

fetch-depth: 2 matters: the default shallow clone has no previous commit to diff against. If you merge several commits in one deploy, set GIT_BASE to the last deployed commit instead of HEAD~1.

5. Check it worked

In Bing Webmaster Tools, open the IndexNow section. Submitted URLs appear there within minutes. A day or two later, run URL Inspection on one of them to confirm it was crawled.

What it won't do

IndexNow gets a crawler to your page sooner. It won't make Bing index a page it considers thin or duplicate, and it does nothing for Google. If a page sits at "discovered but not indexed" for weeks, that's a content problem, not a discovery problem.

It matters more than it used to, though. Microsoft Copilot answers from Bing's index, so a page Bing hasn't crawled is a page Copilot can't cite. If AI search visibility is on your list, we wrote about what to look for in a GEO agency, including the claims to be sceptical of.

Over to you: how do you tell search engines about new pages today? Sitemap and wait, or something faster?

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.