MeshWorld India LogoMeshWorld.
SEOIndexNowWeb-DevelopmentSearch-Indexing27 min read

IndexNow Implementation Guide: WordPress, Shopify, Wix & Custom

Vishnu
By Vishnu
IndexNow Implementation Guide: WordPress, Shopify, Wix & Custom

Table of Contents

  1. WordPress Implementation
  2. Shopify Implementation
  3. Wix Implementation
  4. Squarespace Implementation
  5. Joomla Implementation
  6. Drupal Implementation
  7. Magento Implementation
  8. Laravel Implementation
  9. Next.js Implementation
  10. Nuxt.js Implementation
  11. Express.js / Node.js Implementation
  12. ASP.NET Implementation
  13. PHP Implementation
  14. Python Implementation
  15. Java Implementation
  16. Go Implementation
  17. Static Websites Implementation
  18. Headless CMS Implementation
  19. Jamstack Implementation
  20. Implementation Checklist

CMS and Service Integrations Fig 7.1: Technical architect notebook sketch illustrating various CMS implementation options (WordPress plugins, Shopify apps, and Cloudflare workers) that consolidate and submit URLs to search engines.


WordPress Implementation

WordPress is the most popular CMS and has the most mature IndexNow ecosystem. Multiple plugins support IndexNow, making implementation trivial.

Method 1: Microsoft Start Plugin (Official)

Recommended for: All WordPress users who want a simple, official solution

Steps:

  1. Log in to your WordPress admin dashboard
  2. Go to Plugins > Add New
  3. Search for “Microsoft Start” or “IndexNow
  4. Install and activate the Microsoft Start plugin (or “IndexNow” by Microsoft)
  5. The plugin automatically generates a key and places the key file
  6. Go to Settings > IndexNow to verify configuration
  7. Enable automatic submission on publish/update

What the plugin does automatically:

  • Generates a secure API key
  • Places the key file at your domain root
  • Submits new posts on publish
  • Submits updated posts on modification
  • Submits deleted posts on trash/permanent delete
  • Logs all submissions for review

Configuration options:

  • Submit on post publish (enabled by default)
  • Submit on post update (enabled by default)
  • Submit on post delete (enabled by default)
  • Submit on page changes (enabled by default)
  • Submit custom post types (configurable)
  • Exclude specific post types (configurable)

Method 2: Rank Math SEO (Built-in)

Recommended for: Users already using Rank Math for SEO

Steps:

  1. Go to Rank Math > Dashboard
  2. Find the IndexNow module
  3. Toggle it ON
  4. Rank Math automatically generates the key and configures everything
  5. No additional setup required

Features:

  • Automatic submission on content changes
  • Integration with Rank Math’s existing SEO workflow
  • No separate plugin needed

Method 3: Yoast SEO Premium

Recommended for: Yoast SEO Premium users

Steps:

  1. Go to Yoast SEO > Settings > Site features
  2. Scroll to IndexNow
  3. Toggle Enable IndexNow
  4. Yoast handles key generation and file placement

Note: IndexNow is a Premium feature in Yoast SEO.

Method 4: All in One SEO

Recommended for: AIOSEO users

Steps:

  1. Go to All in One SEO > Settings
  2. Navigate to Webmaster Tools
  3. Find the IndexNow section
  4. Enable and configure

Method 5: Manual Implementation (Custom Code)

Recommended for: Developers who want full control

Add this to your theme’s functions.php or a custom plugin:

php
<?php
/**
 * IndexNow Integration for WordPress
 * Add to functions.php or custom plugin
 */

// Generate or retrieve key
function get_indexnow_key() {
    $key = get_option('indexnow_api_key');
    if (empty($key)) {
        $key = bin2hex(random_bytes(32));
        update_option('indexnow_api_key', $key);
    }
    return $key;
}

// Create key file on activation
function create_indexnow_key_file() {
    $key = get_indexnow_key();
    $file_path = ABSPATH . $key . '.txt';
    
    if (!file_exists($file_path)) {
        file_put_contents($file_path, $key);
    }
}
add_action('init', 'create_indexnow_key_file');

// Submit URL to IndexNow
function submit_to_indexnow($url) {
    $key = get_indexnow_key();
    $host = parse_url(home_url(), PHP_URL_HOST);
    $key_location = home_url('/' . $key . '.txt');
    
    $payload = [
        'host' => $host,
        'key' => $key,
        'keyLocation' => $key_location,
        'urlList' => [$url]
    ];
    
    $response = wp_remote_post('https://api.indexnow.org/indexnow', [
        'headers' => ['Content-Type' => 'application/json'],
        'body' => json_encode($payload),
        'timeout' => 30
    ]);
    
    if (is_wp_error($response)) {
        error_log('IndexNow submission failed: ' . $response->get_error_message());
        return false;
    }
    
    $status = wp_remote_retrieve_response_code($response);
    return $status === 200;
}

// Auto-submit on post publish
function auto_submit_indexnow($post_id, $post, $update) {
    if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
        return;
    }
    
    if ($post->post_status !== 'publish') {
        return;
    }
    
    $url = get_permalink($post_id);
    submit_to_indexnow($url);
}
add_action('wp_insert_post', 'auto_submit_indexnow', 10, 3);

// Auto-submit on post delete
function auto_submit_indexnow_delete($post_id) {
    $url = get_permalink($post_id);
    submit_to_indexnow($url);
}
add_action('deleted_post', 'auto_submit_indexnow_delete');

Shopify Implementation

Shopify doesn’t have a native IndexNow integration, but you can implement it through several methods.

Method 1: Cloudflare Crawler Hints (Easiest)

If you use Cloudflare with your Shopify store:

  1. Log in to Cloudflare dashboard
  2. Go to Speed > Optimization > Crawler Hints
  3. Toggle IndexNow to ON
  4. Cloudflare automatically handles everything—no code needed

Method 2: Shopify App

Search the Shopify App Store for “IndexNow” apps. As of 2026, several third-party apps offer IndexNow integration.

Method 3: Custom App / Private App

Create a private app to handle IndexNow submissions:

javascript
// Shopify webhook handler for IndexNow
// Deploy as a Shopify private app or external service

const express = require('express');
const axios = require('axios');
const crypto = require('crypto');

const app = express();
app.use(express.json());

const INDEXNOW_KEY = process.env.INDEXNOW_KEY;
const SHOP_DOMAIN = process.env.SHOP_DOMAIN;
const KEY_LOCATION = `https://${SHOP_DOMAIN}/${INDEXNOW_KEY}.txt`;

// Webhook: Product created/updated
app.post('/webhook/product-update', async (req, res) => {
    const product = req.body;
    const productUrl = `https://${SHOP_DOMAIN}/products/${product.handle}`;
    
    try {
        await submitToIndexNow([productUrl]);
        res.status(200).send('OK');
    } catch (error) {
        console.error('IndexNow submission failed:', error);
        res.status(500).send('Error');
    }
});

// Webhook: Collection updated
app.post('/webhook/collection-update', async (req, res) => {
    const collection = req.body;
    const collectionUrl = `https://${SHOP_DOMAIN}/collections/${collection.handle}`;
    
    try {
        await submitToIndexNow([collectionUrl]);
        res.status(200).send('OK');
    } catch (error) {
        console.error('IndexNow submission failed:', error);
        res.status(500).send('Error');
    }
});

async function submitToIndexNow(urls) {
    const payload = {
        host: SHOP_DOMAIN,
        key: INDEXNOW_KEY,
        keyLocation: KEY_LOCATION,
        urlList: urls
    };
    
    const response = await axios.post('https://api.indexnow.org/indexnow', payload, {
        headers: { 'Content-Type': 'application/json' }
    });
    
    return response.status === 200;
}

app.listen(3000, () => {
    console.log('Shopify IndexNow webhook server running');
});

Method 4: Key File Placement

Shopify doesn’t allow direct file uploads to the root. Workarounds:

  1. Use Cloudflare (recommended)
  2. Create a redirect: Upload the key file to Shopify’s Files section, then create a URL redirect from /{key}.txt to the file URL
  3. Use a proxy service: Route traffic through a service that can serve the key file

Wix Implementation

Wix offers native IndexNow support—one of the easiest implementations available.

Steps:

  1. Log in to your Wix dashboard
  2. Go to Settings > SEO
  3. Scroll to IndexNow
  4. Toggle Enable IndexNow to ON
  5. Wix automatically:
    • Generates a secure key
    • Places the key file at your domain root
    • Submits new and updated pages automatically

What Wix submits automatically:

  • New pages
  • Updated pages
  • Blog posts
  • Product pages (for Wix Stores)
  • Event pages

Manual submission: If you need to submit a specific URL manually:

  1. Go to SEO Tools > IndexNow
  2. Enter the URL
  3. Click Submit

Squarespace Implementation

Squarespace has limited native IndexNow support. As of 2026, implementation requires custom code or third-party services.

Method 1: Code Injection (Advanced)

Squarespace allows code injection in the header/footer, but this won’t help with IndexNow since you need server-side API calls and root file placement.

Method 2: External Service

Use a service like Cloudflare (if you use a custom domain with Cloudflare) or a third-party IndexNow service that handles submissions via webhook.

Method 3: Manual Submission

For occasional submissions, use a manual IndexNow submission tool or curl:

bash
curl -X POST https://api.indexnow.org/indexnow   -H "Content-Type: application/json"   -d '{
    "host": "your-site.com",
    "key": "your-key",
    "keyLocation": "https://your-site.com/your-key.txt",
    "urlList": ["https://your-site.com/page-url"]
  }'

Note: You’ll need to host the key file externally or use a workaround since Squarespace doesn’t allow root file uploads.


Joomla Implementation

Method 1: Extension

Search the Joomla Extensions Directory for “IndexNow” extensions. Several are available as of 2026.

Method 2: Manual Implementation

Add this to a custom plugin or your template’s index.php:

php
<?php
// IndexNow integration for Joomla
// Place in a custom system plugin

defined('_JEXEC') or die;

use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Factory;

class PlgSystemIndexNow extends CMSPlugin
{
    protected $autoloadLanguage = true;
    
    private function getKey()
    {
        $key = $this->params->get('api_key');
        if (empty($key)) {
            $key = bin2hex(random_bytes(32));
            $this->params->set('api_key', $key);
            // Save params
        }
        return $key;
    }
    
    public function onContentAfterSave($context, $article, $isNew)
    {
        if ($context !== 'com_content.article') {
            return;
        }
        
        $url = JRoute::_($article->slug, 1, true);
        $this->submitToIndexNow([$url]);
    }
    
    private function submitToIndexNow($urls)
    {
        $key = $this->getKey();
        $host = Factory::getApplication()->get('sitename'); // Adjust as needed
        $keyLocation = JURI::root() . $key . '.txt';
        
        $payload = json_encode([
            'host' => $host,
            'key' => $key,
            'keyLocation' => $keyLocation,
            'urlList' => $urls
        ]);
        
        $ch = curl_init('https://api.indexnow.org/indexnow');
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_exec($ch);
        curl_close($ch);
    }
}

Drupal Implementation

Method 1: Module

Search Drupal.org for IndexNow modules. Community modules exist for Drupal 9/10.

Method 2: Custom Module

Create a custom module indexnow_integration:

php
<?php
// indexnow_integration.module

use Drupal\Core\Entity\EntityInterface;
use Drupal\node\NodeInterface;
use GuzzleHttp\Client;

/**
 * Implements hook_ENTITY_TYPE_insert() for node entities.
 */
function indexnow_integration_node_insert(NodeInterface $node) {
    indexnow_integration_submit($node->toUrl()->setAbsolute()->toString());
}

/**
 * Implements hook_ENTITY_TYPE_update() for node entities.
 */
function indexnow_integration_node_update(NodeInterface $node) {
    if ($node->isPublished()) {
        indexnow_integration_submit($node->toUrl()->setAbsolute()->toString());
    }
}

/**
 * Submit URL to IndexNow.
 */
function indexnow_integration_submit($url) {
    $config = \Drupal::config('indexnow_integration.settings');
    $key = $config->get('api_key');
    
    if (empty($key)) {
        $key = bin2hex(random_bytes(32));
        \Drupal::configFactory()->getEditable('indexnow_integration.settings')
            ->set('api_key', $key)
            ->save();
    }
    
    $host = \Drupal::request()->getHost();
    $keyLocation = 'https://' . $host . '/' . $key . '.txt';
    
    // Ensure key file exists
    $file_path = \Drupal::root() . '/' . $key . '.txt';
    if (!file_exists($file_path)) {
        file_put_contents($file_path, $key);
    }
    
    $client = new Client();
    try {
        $response = $client->post('https://api.indexnow.org/indexnow', [
            'json' => [
                'host' => $host,
                'key' => $key,
                'keyLocation' => $keyLocation,
                'urlList' => [$url]
            ]
        ]);
        
        \Drupal::logger('indexnow_integration')->notice(
            'Submitted @url to IndexNow. Status: @status',
            ['@url' => $url, '@status' => $response->getStatusCode()]
        );
    } catch (\Exception $e) {
        \Drupal::logger('indexnow_integration')->error(
            'IndexNow submission failed: @message',
            ['@message' => $e->getMessage()]
        );
    }
}

Magento Implementation

Method 1: Extension

Search the Magento Marketplace for IndexNow extensions. Several are available for Magento 2.

Method 2: Custom Module

Create a custom module Vendor_IndexNow:

php
<?php
// app/code/Vendor/IndexNow/Observer/ProductSaveObserver.php

namespace Vendor\IndexNow\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\HTTP\Client\Curl;
use Magento\Store\Model\StoreManagerInterface;

class ProductSaveObserver implements ObserverInterface
{
    protected $curl;
    protected $storeManager;
    protected $key;
    
    public function __construct(Curl $curl, StoreManagerInterface $storeManager)
    {
        $this->curl = $curl;
        $this->storeManager = $storeManager;
        $this->key = $this->getOrCreateKey();
    }
    
    public function execute(Observer $observer)
    {
        $product = $observer->getEvent()->getProduct();
        $productUrl = $product->getProductUrl();
        
        $this->submitToIndexNow([$productUrl]);
    }
    
    private function getOrCreateKey()
    {
        // Retrieve from config or generate new
        $key = 'your-generated-key'; // Implement config retrieval
        return $key;
    }
    
    private function submitToIndexNow($urls)
    {
        $host = $this->storeManager->getStore()->getBaseUrl();
        $host = parse_url($host, PHP_URL_HOST);
        $keyLocation = 'https://' . $host . '/' . $this->key . '.txt';
        
        $payload = json_encode([
            'host' => $host,
            'key' => $this->key,
            'keyLocation' => $keyLocation,
            'urlList' => $urls
        ]);
        
        $this->curl->setHeaders(['Content-Type' => 'application/json']);
        $this->curl->post('https://api.indexnow.org/indexnow', $payload);
    }
}

Laravel Implementation

Laravel makes IndexNow implementation straightforward with its HTTP client and file storage.

Step 1: Generate Key and Create File

php
<?php
// app/Console/Commands/SetupIndexNow.php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;

class SetupIndexNow extends Command
{
    protected $signature = 'indexnow:setup';
    protected $description = 'Setup IndexNow key and key file';
    
    public function handle()
    {
        $key = bin2hex(random_bytes(32));
        
        // Store key in config or database
        config(['indexnow.key' => $key]);
        
        // Create key file in public directory
        $path = public_path($key . '.txt');
        file_put_contents($path, $key);
        
        $this->info("IndexNow key generated: {$key}");
        $this->info("Key file created: {$path}");
        $this->info("Key URL: " . url($key . '.txt'));
    }
}

Step 2: Create Service

php
<?php
// app/Services/IndexNowService.php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class IndexNowService
{
    protected $key;
    protected $host;
    protected $keyLocation;
    
    public function __construct()
    {
        $this->key = config('indexnow.key');
        $this->host = parse_url(config('app.url'), PHP_URL_HOST);
        $this->keyLocation = config('app.url') . '/' . $this->key . '.txt';
    }
    
    public function submit(array $urls)
    {
        if (empty($this->key)) {
            Log::error('IndexNow key not configured');
            return false;
        }
        
        $chunks = array_chunk($urls, 10000);
        
        foreach ($chunks as $chunk) {
            $response = Http::withHeaders([
                'Content-Type' => 'application/json; charset=utf-8'
            ])->post('https://api.indexnow.org/indexnow', [
                'host' => $this->host,
                'key' => $this->key,
                'keyLocation' => $this->keyLocation,
                'urlList' => $chunk
            ]);
            
            if (!$response->successful()) {
                Log::error('IndexNow submission failed', [
                    'status' => $response->status(),
                    'body' => $response->body(),
                    'urls' => $chunk
                ]);
                return false;
            }
            
            Log::info('IndexNow submission successful', [
                'urls_count' => count($chunk),
                'status' => $response->status()
            ]);
        }
        
        return true;
    }
    
    public function submitSingle(string $url)
    {
        return $this->submit([$url]);
    }
}

Step 3: Auto-Submit on Model Changes

php
<?php
// app/Observers/PostObserver.php

namespace App\Observers;

use App\Models\Post;
use App\Services\IndexNowService;

class PostObserver
{
    protected $indexNow;
    
    public function __construct(IndexNowService $indexNow)
    {
        $this->indexNow = $indexNow;
    }
    
    public function created(Post $post)
    {
        $this->indexNow->submitSingle($post->url);
    }
    
    public function updated(Post $post)
    {
        if ($post->isDirty('content') || $post->isDirty('title')) {
            $this->indexNow->submitSingle($post->url);
        }
    }
    
    public function deleted(Post $post)
    {
        $this->indexNow->submitSingle($post->url);
    }
}

Step 4: Register Observer

php
<?php
// app/Providers/AppServiceProvider.php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Models\Post;
use App\Observers\PostObserver;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        Post::observe(PostObserver::class);
    }
}

Next.js Implementation

Next.js makes IndexNow implementation easy with API routes and static file serving.

Step 1: Create Key File

Place the key file in the public directory:

plaintext
public/
  └── [your-key].txt    # Contains only the key string

Step 2: Create API Route

typescript
// app/api/indexnow/route.ts (App Router)
// or
// pages/api/indexnow.ts (Pages Router)

import { NextRequest, NextResponse } from 'next/server';

const INDEXNOW_KEY = process.env.INDEXNOW_KEY!;
const HOST = process.env.NEXT_PUBLIC_SITE_URL 
  ? new URL(process.env.NEXT_PUBLIC_SITE_URL).host 
  : '';

export async function POST(request: NextRequest) {
  try {
    const { urls } = await request.json();
    
    if (!Array.isArray(urls) || urls.length === 0) {
      return NextResponse.json(
        { error: 'urls must be a non-empty array' },
        { status: 400 }
      );
    }
    
    if (urls.length > 10000) {
      return NextResponse.json(
        { error: 'Maximum 10,000 URLs per request' },
        { status: 400 }
      );
    }
    
    const payload = {
      host: HOST,
      key: INDEXNOW_KEY,
      keyLocation: `https://${HOST}/${INDEXNOW_KEY}.txt`,
      urlList: urls
    };
    
    const response = await fetch('https://api.indexnow.org/indexnow', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json; charset=utf-8'
      },
      body: JSON.stringify(payload)
    });
    
    if (!response.ok) {
      const errorText = await response.text();
      console.error('IndexNow API error:', errorText);
      return NextResponse.json(
        { error: 'IndexNow submission failed', details: errorText },
        { status: response.status }
      );
    }
    
    return NextResponse.json({
      success: true,
      submitted: urls.length,
      status: response.status
    });
    
  } catch (error) {
    console.error('IndexNow error:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Step 3: Trigger on Content Changes

typescript
// lib/indexnow.ts

export async function notifyIndexNow(urls: string | string[]) {
  const urlArray = Array.isArray(urls) ? urls : [urls];
  
  try {
    const response = await fetch('/api/indexnow', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ urls: urlArray })
    });
    
    if (!response.ok) {
      throw new Error(`IndexNow failed: ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    console.error('IndexNow notification failed:', error);
    throw error;
  }
}

// Use in your CMS or content management
// Example: After creating a blog post
export async function publishPost(postData: PostData) {
  // Save post to database...
  
  // Revalidate Next.js cache
  await revalidatePath(`/blog/${postData.slug}`);
  
  // Notify IndexNow
  await notifyIndexNow(`https://yoursite.com/blog/${postData.slug}`);
  
  return postData;
}

Nuxt.js Implementation

typescript
// server/api/indexnow.post.ts

export default defineEventHandler(async (event) => {
  const body = await readBody(event);
  const { urls } = body;
  
  const config = useRuntimeConfig();
  const INDEXNOW_KEY = config.indexnowKey;
  const HOST = config.public.siteUrl 
    ? new URL(config.public.siteUrl).host 
    : '';
  
  if (!Array.isArray(urls) || urls.length === 0) {
    throw createError({
      statusCode: 400,
      statusMessage: 'urls must be a non-empty array'
    });
  }
  
  const payload = {
    host: HOST,
    key: INDEXNOW_KEY,
    keyLocation: `https://${HOST}/${INDEXNOW_KEY}.txt`,
    urlList: urls
  };
  
  const response = await $fetch('https://api.indexnow.org/indexnow', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json; charset=utf-8'
    },
    body: payload
  });
  
  return { success: true, submitted: urls.length };
});

Place the key file in the public/ directory for automatic serving.


Express.js / Node.js Implementation

javascript
// indexnow.js
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');
const fs = require('fs').promises;
const path = require('path');

class IndexNowClient {
  constructor(options = {}) {
    this.key = options.key || this.generateKey();
    this.host = options.host;
    this.endpoint = options.endpoint || 'https://api.indexnow.org/indexnow';
    this.keyFilePath = options.keyFilePath || path.join(process.cwd(), 'public', `${this.key}.txt`);
  }
  
  generateKey() {
    return crypto.randomBytes(32).toString('hex');
  }
  
  async setup() {
    // Create key file
    await fs.writeFile(this.keyFilePath, this.key);
    console.log(`IndexNow key file created: ${this.keyFilePath}`);
    return this;
  }
  
  async submit(urls) {
    const urlArray = Array.isArray(urls) ? urls : [urls];
    
    if (urlArray.length > 10000) {
      throw new Error('Maximum 10,000 URLs per request');
    }
    
    const payload = {
      host: this.host,
      key: this.key,
      keyLocation: `https://${this.host}/${this.key}.txt`,
      urlList: urlArray
    };
    
    try {
      const response = await axios.post(this.endpoint, payload, {
        headers: {
          'Content-Type': 'application/json; charset=utf-8'
        },
        timeout: 30000
      });
      
      return {
        success: response.status === 200,
        status: response.status,
        submitted: urlArray.length
      };
    } catch (error) {
      console.error('IndexNow submission failed:', error.message);
      throw error;
    }
  }
  
  async submitSingle(url) {
    return this.submit([url]);
  }
}

// Usage in Express app
const app = express();
app.use(express.json());

const indexNow = new IndexNowClient({
  host: 'example.com',
  keyFilePath: path.join(__dirname, 'public', 'indexnow-key.txt')
});

// Initialize on startup
indexNow.setup().catch(console.error);

// API endpoint for submitting URLs
app.post('/api/notify-indexnow', async (req, res) => {
  try {
    const { urls } = req.body;
    const result = await indexNow.submit(urls);
    res.json(result);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Auto-submit middleware example
app.post('/api/posts', async (req, res, next) => {
  // Save post logic...
  
  // Notify IndexNow
  try {
    await indexNow.submitSingle(`https://example.com/posts/${req.body.slug}`);
  } catch (error) {
    console.error('IndexNow notification failed:', error);
    // Don't fail the request if IndexNow fails
  }
  
  res.json({ success: true });
});

module.exports = { IndexNowClient };

ASP.NET Implementation

csharp
// Services/IndexNowService.cs
using System.Text;
using System.Text.Json;

public class IndexNowService
{
    private readonly HttpClient _httpClient;
    private readonly string _key;
    private readonly string _host;
    private readonly string _keyLocation;
    private readonly ILogger<IndexNowService> _logger;
    
    public IndexNowService(IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger<IndexNowService> logger)
    {
        _httpClient = httpClientFactory.CreateClient();
        _key = configuration["IndexNow:Key"] ?? GenerateKey();
        _host = configuration["IndexNow:Host"] ?? throw new ArgumentException("Host required");
        _keyLocation = $"https://{_host}/{_key}.txt";
        _logger = logger;
        
        // Ensure key file exists
        EnsureKeyFileExists();
    }
    
    private string GenerateKey()
    {
        var bytes = new byte[32];
        using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
        {
            rng.GetBytes(bytes);
        }
        return Convert.ToHexString(bytes).ToLower();
    }
    
    private void EnsureKeyFileExists()
    {
        var filePath = Path.Combine("wwwroot", $"{_key}.txt");
        if (!File.Exists(filePath))
        {
            File.WriteAllText(filePath, _key);
            _logger.LogInformation($"IndexNow key file created: {filePath}");
        }
    }
    
    public async Task<bool> SubmitAsync(List<string> urls)
    {
        if (urls.Count > 10000)
        {
            throw new ArgumentException("Maximum 10,000 URLs per request");
        }
        
        var payload = new
        {
            host = _host,
            key = _key,
            keyLocation = _keyLocation,
            urlList = urls
        };
        
        var json = JsonSerializer.Serialize(payload);
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        
        try
        {
            var response = await _httpClient.PostAsync("https://api.indexnow.org/indexnow", content);
            
            if (response.IsSuccessStatusCode)
            {
                _logger.LogInformation($"IndexNow submission successful: {urls.Count} URLs");
                return true;
            }
            else
            {
                var errorBody = await response.Content.ReadAsStringAsync();
                _logger.LogError($"IndexNow submission failed: {response.StatusCode} - {errorBody}");
                return false;
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "IndexNow submission exception");
            return false;
        }
    }
    
    public async Task<bool> SubmitSingleAsync(string url)
    {
        return await SubmitAsync(new List<string> { url });
    }
}

// Program.cs registration
builder.Services.AddHttpClient();
builder.Services.AddSingleton<IndexNowService>();

// Controller usage
public class PostsController : ControllerBase
{
    private readonly IndexNowService _indexNow;
    
    public PostsController(IndexNowService indexNow)
    {
        _indexNow = indexNow;
    }
    
    [HttpPost]
    public async Task<IActionResult> CreatePost([FromBody] PostDto post)
    {
        // Save post...
        
        // Notify IndexNow
        var url = $"https://example.com/posts/{post.Slug}";
        await _indexNow.SubmitSingleAsync(url);
        
        return Ok(new { success = true });
    }
}

PHP Implementation

php
<?php
// IndexNow.php - Standalone PHP implementation

class IndexNowClient
{
    private string $key;
    private string $host;
    private string $keyLocation;
    private string $endpoint;
    
    public function __construct(string $host, ?string $key = null)
    {
        $this->host = $host;
        $this->key = $key ?? $this->generateKey();
        $this->keyLocation = "https://{$host}/{$this->key}.txt";
        $this->endpoint = 'https://api.indexnow.org/indexnow';
        
        $this->ensureKeyFile();
    }
    
    private function generateKey(): string
    {
        return bin2hex(random_bytes(32));
    }
    
    private function ensureKeyFile(): void
    {
        $filePath = $_SERVER['DOCUMENT_ROOT'] . "/{$this->key}.txt";
        
        if (!file_exists($filePath)) {
            file_put_contents($filePath, $this->key);
        }
    }
    
    public function submit(array $urls): bool
    {
        if (count($urls) > 10000) {
            throw new InvalidArgumentException('Maximum 10,000 URLs per request');
        }
        
        $payload = [
            'host' => $this->host,
            'key' => $this->key,
            'keyLocation' => $this->keyLocation,
            'urlList' => $urls
        ];
        
        $ch = curl_init($this->endpoint);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode($payload),
            CURLOPT_HTTPHEADER => ['Content-Type: application/json; charset=utf-8'],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30
        ]);
        
        $response = curl_exec($ch);
        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($statusCode !== 200) {
            error_log("IndexNow submission failed: HTTP {$statusCode} - {$response}");
            return false;
        }
        
        return true;
    }
    
    public function submitSingle(string $url): bool
    {
        return $this->submit([$url]);
    }
    
    public function getKey(): string
    {
        return $this->key;
    }
    
    public function getKeyLocation(): string
    {
        return $this->keyLocation;
    }
}

// Usage
$indexNow = new IndexNowClient('example.com');
$indexNow->submitSingle('https://example.com/new-page');
$indexNow->submit([
    'https://example.com/page1',
    'https://example.com/page2',
    'https://example.com/page3'
]);

Python Implementation

python
# indexnow_client.py
import os
import secrets
import requests
from typing import List, Optional
from urllib.parse import urlparse

class IndexNowClient:
    def __init__(self, host: str, key: Optional[str] = None, 
                 key_file_dir: Optional[str] = None):
        self.host = host
        self.key = key or self._generate_key()
        self.key_location = f"https://{host}/{self.key}.txt"
        self.endpoint = "https://api.indexnow.org/indexnow"
        self.key_file_dir = key_file_dir or os.getcwd()
        
        self._ensure_key_file()
    
    def _generate_key(self) -> str:
        return secrets.token_hex(32)
    
    def _ensure_key_file(self):
        file_path = os.path.join(self.key_file_dir, f"{self.key}.txt")
        if not os.path.exists(file_path):
            with open(file_path, 'w') as f:
                f.write(self.key)
    
    def submit(self, urls: List[str]) -> bool:
        if len(urls) > 10000:
            raise ValueError("Maximum 10,000 URLs per request")
        
        payload = {
            "host": self.host,
            "key": self.key,
            "keyLocation": self.key_location,
            "urlList": urls
        }
        
        try:
            response = requests.post(
                self.endpoint,
                json=payload,
                headers={"Content-Type": "application/json; charset=utf-8"},
                timeout=30
            )
            
            if response.status_code == 200:
                print(f"IndexNow: Submitted {len(urls)} URLs successfully")
                return True
            else:
                print(f"IndexNow failed: HTTP {response.status_code} - {response.text}")
                return False
                
        except requests.RequestException as e:
            print(f"IndexNow request failed: {e}")
            return False
    
    def submit_single(self, url: str) -> bool:
        return self.submit([url])

# Django integration example
# models.py
from django.db import models
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver

class BlogPost(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    content = models.TextField()
    updated_at = models.DateTimeField(auto_now=True)
    
    def get_absolute_url(self):
        return f"/blog/{self.slug}/"

# signals.py
@receiver(post_save, sender=BlogPost)
def notify_indexnow_on_save(sender, instance, created, **kwargs):
    from django.conf import settings
    from django.contrib.sites.models import Site
    
    site = Site.objects.get_current()
    client = IndexNowClient(host=site.domain)
    
    url = f"https://{site.domain}{instance.get_absolute_url()}"
    client.submit_single(url)

@receiver(post_delete, sender=BlogPost)
def notify_indexnow_on_delete(sender, instance, **kwargs):
    from django.contrib.sites.models import Site
    
    site = Site.objects.get_current()
    client = IndexNowClient(host=site.domain)
    
    url = f"https://{site.domain}{instance.get_absolute_url()}"
    client.submit_single(url)

# Flask integration example
from flask import Flask, request, jsonify
from indexnow_client import IndexNowClient

app = Flask(__name__)
indexnow = IndexNowClient(host='example.com')

@app.route('/api/posts', methods=['POST'])
def create_post():
    data = request.json
    # Save post logic...
    
    # Notify IndexNow
    url = f"https://example.com/posts/{data['slug']}"
    indexnow.submit_single(url)
    
    return jsonify({"success": True})

Java Implementation

java
// IndexNowClient.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.SecureRandom;
import java.util.List;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import com.fasterxml.jackson.databind.ObjectMapper;

public class IndexNowClient {
    private final String key;
    private final String host;
    private final String keyLocation;
    private final String endpoint;
    private final HttpClient httpClient;
    private final ObjectMapper objectMapper;
    
    public IndexNowClient(String host) {
        this(host, null, null);
    }
    
    public IndexNowClient(String host, String key, String keyFileDir) {
        this.host = host;
        this.key = key != null ? key : generateKey();
        this.keyLocation = "https://" + host + "/" + this.key + ".txt";
        this.endpoint = "https://api.indexnow.org/indexnow";
        this.httpClient = HttpClient.newHttpClient();
        this.objectMapper = new ObjectMapper();
        
        ensureKeyFile(keyFileDir);
    }
    
    private String generateKey() {
        byte[] bytes = new byte[32];
        new SecureRandom().nextBytes(bytes);
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
    
    private void ensureKeyFile(String keyFileDir) {
        String dir = keyFileDir != null ? keyFileDir : System.getProperty("user.dir");
        Path filePath = Paths.get(dir, key + ".txt");
        
        if (!Files.exists(filePath)) {
            try {
                Files.writeString(filePath, key);
            } catch (IOException e) {
                throw new RuntimeException("Failed to create IndexNow key file", e);
            }
        }
    }
    
    public boolean submit(List<String> urls) throws IOException, InterruptedException {
        if (urls.size() > 10000) {
            throw new IllegalArgumentException("Maximum 10,000 URLs per request");
        }
        
        IndexNowPayload payload = new IndexNowPayload(host, key, keyLocation, urls);
        String json = objectMapper.writeValueAsString(payload);
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(endpoint))
            .header("Content-Type", "application/json; charset=utf-8")
            .POST(HttpRequest.BodyPublishers.ofString(json))
            .build();
        
        HttpResponse<String> response = httpClient.send(request, 
            HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() == 200) {
            System.out.println("IndexNow: Submitted " + urls.size() + " URLs");
            return true;
        } else {
            System.err.println("IndexNow failed: HTTP " + response.statusCode() + 
                             " - " + response.body());
            return false;
        }
    }
    
    public boolean submitSingle(String url) throws IOException, InterruptedException {
        return submit(List.of(url));
    }
    
    // Inner class for JSON payload
    private static class IndexNowPayload {
        public String host;
        public String key;
        public String keyLocation;
        public List<String> urlList;
        
        public IndexNowPayload(String host, String key, String keyLocation, 
                               List<String> urlList) {
            this.host = host;
            this.key = key;
            this.keyLocation = keyLocation;
            this.urlList = urlList;
        }
    }
}

// Spring Boot integration
@Service
public class PostService {
    @Autowired
    private IndexNowClient indexNowClient;
    
    @Autowired
    private PostRepository postRepository;
    
    public Post createPost(PostDto dto) {
        Post post = new Post();
        post.setTitle(dto.getTitle());
        post.setSlug(dto.getSlug());
        post.setContent(dto.getContent());
        
        postRepository.save(post);
        
        // Notify IndexNow
        try {
            String url = "https://example.com/posts/" + post.getSlug();
            indexNowClient.submitSingle(url);
        } catch (Exception e) {
            // Log but don't fail the request
            System.err.println("IndexNow notification failed: " + e.getMessage());
        }
        
        return post;
    }
}

Go Implementation

go
// indexnow.go
package indexnow

import (
    "bytes"
    "crypto/rand"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "path/filepath"
)

// Client represents an IndexNow client
type Client struct {
    Key         string
    Host        string
    KeyLocation string
    Endpoint    string
    KeyFileDir  string
}

// Payload represents the IndexNow API request payload
type Payload struct {
    Host        string   `json:"host"`
    Key         string   `json:"key"`
    KeyLocation string   `json:"keyLocation"`
    URLList     []string `json:"urlList"`
}

// NewClient creates a new IndexNow client
func NewClient(host string, opts ...func(*Client)) (*Client, error) {
    client := &Client{
        Host:     host,
        Endpoint: "https://api.indexnow.org/indexnow",
    }
    
    for _, opt := range opts {
        opt(client)
    }
    
    if client.Key == "" {
        key, err := generateKey()
        if err != nil {
            return nil, fmt.Errorf("failed to generate key: %w", err)
        }
        client.Key = key
    }
    
    client.KeyLocation = fmt.Sprintf("https://%s/%s.txt", host, client.Key)
    
    if client.KeyFileDir == "" {
        client.KeyFileDir = "."
    }
    
    if err := client.ensureKeyFile(); err != nil {
        return nil, fmt.Errorf("failed to create key file: %w", err)
    }
    
    return client, nil
}

// WithKey sets a custom key
func WithKey(key string) func(*Client) {
    return func(c *Client) {
        c.Key = key
    }
}

// WithKeyFileDir sets the directory for the key file
func WithKeyFileDir(dir string) func(*Client) {
    return func(c *Client) {
        c.KeyFileDir = dir
    }
}

func generateKey() (string, error) {
    bytes := make([]byte, 32)
    if _, err := rand.Read(bytes); err != nil {
        return "", err
    }
    return hex.EncodeToString(bytes), nil
}

func (c *Client) ensureKeyFile() error {
    filePath := filepath.Join(c.KeyFileDir, fmt.Sprintf("%s.txt", c.Key))
    
    if _, err := os.Stat(filePath); os.IsNotExist(err) {
        return os.WriteFile(filePath, []byte(c.Key), 0644)
    }
    
    return nil
}

// Submit sends URLs to IndexNow
func (c *Client) Submit(urls []string) error {
    if len(urls) > 10000 {
        return fmt.Errorf("maximum 10,000 URLs per request")
    }
    
    payload := Payload{
        Host:        c.Host,
        Key:         c.Key,
        KeyLocation: c.KeyLocation,
        URLList:     urls,
    }
    
    jsonData, err := json.Marshal(payload)
    if err != nil {
        return fmt.Errorf("failed to marshal payload: %w", err)
    }
    
    req, err := http.NewRequest("POST", c.Endpoint, bytes.NewBuffer(jsonData))
    if err != nil {
        return fmt.Errorf("failed to create request: %w", err)
    }
    
    req.Header.Set("Content-Type", "application/json; charset=utf-8")
    
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return fmt.Errorf("request failed: %w", err)
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK {
        body, _ := io.ReadAll(resp.Body)
        return fmt.Errorf("IndexNow API returned %d: %s", resp.StatusCode, string(body))
    }
    
    return nil
}

// SubmitSingle submits a single URL
func (c *Client) SubmitSingle(url string) error {
    return c.Submit([]string{url})
}

// Gin framework integration example
/*
package main

import (
    "github.com/gin-gonic/gin"
    "yourmodule/indexnow"
)

func main() {
    r := gin.Default()
    
    client, err := indexnow.NewClient("example.com")
    if err != nil {
        panic(err)
    }
    
    r.POST("/api/posts", func(c *gin.Context) {
        var post Post
        if err := c.ShouldBindJSON(&post); err != nil {
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        
        // Save post...
        
        // Notify IndexNow
        url := fmt.Sprintf("https://example.com/posts/%s", post.Slug)
        go client.SubmitSingle(url) // Async, don't block response
        
        c.JSON(200, gin.H{"success": true})
    })
    
    r.Run()
}
*/

Static Websites Implementation

For static sites (Jekyll, Hugo, Gatsby, 11ty, etc.), you have several options:

Option 1: Build-Time Submission

Submit URLs during the build process:

bash
#!/bin/bash
# submit-indexnow.sh

# After building your static site
# Generate a list of changed URLs

SITE_URL="https://example.com"
KEY="your-api-key"
KEY_FILE="public/$KEY.txt"

# Create key file
echo "$KEY" > "$KEY_FILE"

# Get list of all pages
URLS=$(find public -name "*.html" | sed 's|public/||' | sed "s|^|$SITE_URL/|" | sed 's|/index.html$|/|')

# Submit to IndexNow
curl -X POST https://api.indexnow.org/indexnow   -H "Content-Type: application/json"   -d "{
    "host": "example.com",
    "key": "$KEY",
    "keyLocation": "$SITE_URL/$KEY.txt",
    "urlList": [$(echo "$URLS" | sed 's/.*/"&",/' | sed '$s/,$//')]
  }"

Option 2: Netlify Functions

javascript
// netlify/functions/indexnow.js
exports.handler = async (event) => {
  if (event.httpMethod !== 'POST') {
    return { statusCode: 405, body: 'Method Not Allowed' };
  }
  
  const { urls } = JSON.parse(event.body);
  const KEY = process.env.INDEXNOW_KEY;
  const HOST = process.env.SITE_URL.replace('https://', '');
  
  const response = await fetch('https://api.indexnow.org/indexnow', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      host: HOST,
      key: KEY,
      keyLocation: `${process.env.SITE_URL}/${KEY}.txt`,
      urlList: urls
    })
  });
  
  return {
    statusCode: response.status,
    body: JSON.stringify({ success: response.ok })
  };
};

Option 3: Vercel Edge Function

typescript
// app/api/indexnow/route.ts
export const runtime = 'edge';

export async function POST(request: Request) {
  const { urls } = await request.json();
  const KEY = process.env.INDEXNOW_KEY;
  const HOST = process.env.VERCEL_URL || 'your-domain.com';
  
  const response = await fetch('https://api.indexnow.org/indexnow', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      host: HOST,
      key: KEY,
      keyLocation: `https://${HOST}/${KEY}.txt`,
      urlList: urls
    })
  });
  
  return new Response(JSON.stringify({ success: response.ok }), {
    status: response.status
  });
}

Headless CMS Implementation

For headless CMS setups (Contentful, Strapi, Sanity, etc.), implement IndexNow in your frontend or API layer:

Strapi Webhook Example

javascript
// config/server.js
module.exports = ({ env }) => ({
  // ... other config
  webhooks: {
    populateRelations: true,
  },
});

// Create a webhook in Strapi admin that hits your API
// Your API then submits to IndexNow

Contentful Webhook Handler

javascript
// serverless function handling Contentful webhooks
exports.handler = async (event) => {
  const payload = JSON.parse(event.body);
  
  if (payload.sys.contentType.sys.id === 'blogPost') {
    const slug = payload.fields.slug['en-US'];
    const url = `https://example.com/blog/${slug}`;
    
    // Submit to IndexNow
    await submitToIndexNow([url]);
  }
  
  return { statusCode: 200, body: 'OK' };
};

Jamstack Implementation

For Jamstack architectures, integrate IndexNow into your build and deployment pipeline:

GitHub Actions Example

yaml
# .github/workflows/deploy.yml
name: Deploy and Index

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build site
        run: npm run build
      
      - name: Deploy
        run: npm run deploy
      
      - name: Submit to IndexNow
        run: |
          # Generate list of all URLs from sitemap
          URLS=$(curl -s https://example.com/sitemap.xml |             grep -oP '(?<=<loc>)[^<]+')
          
          # Submit to IndexNow
          curl -X POST https://api.indexnow.org/indexnow             -H "Content-Type: application/json"             -d "{
              "host": "example.com",
              "key": "${{ secrets.INDEXNOW_KEY }}",
              "keyLocation": "https://example.com/${{ secrets.INDEXNOW_KEY }}.txt",
              "urlList": [$(echo "$URLS" | sed 's/.*/"&",/' | sed '$s/,$//')]
            }"

Implementation Checklist

Use this checklist for any IndexNow implementation:

Pre-Implementation

  • Determine which platform/CMS you’re using
  • Check if native plugin support exists
  • Identify all content types that need indexing (posts, products, pages, etc.)
  • Plan key management strategy (environment variables, secrets manager)
  • Verify hosting allows root file placement (or plan CDN workaround)

Implementation

  • Generate secure API key (32+ hex characters)
  • Place key file at domain root (https://example.com/{key}.txt)
  • Verify key file is accessible and returns Content-Type: text/plain
  • Implement API submission logic
  • Add auto-submit triggers (on publish, update, delete)
  • Test with a single URL using curl or Postman
  • Verify HTTP 200 response from API

Post-Implementation

  • Monitor server logs for crawler visits within 24 hours
  • Check Bing Webmaster Tools for indexing status
  • Set up error logging and alerting
  • Implement retry logic for failed submissions
  • Add deduplication to prevent duplicate submissions
  • Document the implementation for team members
  • Schedule key rotation (annually recommended)

Ongoing Maintenance

  • Monitor submission success rates
  • Review logs weekly for anomalies
  • Update key file after any key rotation
  • Test implementation after CMS/platform updates
  • Stay informed about protocol updates from indexnow.org

Key Takeaways

WordPress: Use Microsoft Start plugin, Rank Math, or Yoast Premium for 1-click setup
Wix: Native integration—just toggle on in Settings
Shopify: Use Cloudflare Crawler Hints or custom app/webhook
Laravel: Create service + observers for elegant integration
Next.js/Nuxt: API route + static key file in public directory
Express/Node: Simple HTTP client with retry logic
ASP.NET: Service class with HttpClient + dependency injection
PHP: Standalone class or framework-specific integration
Python: requests library + Django/Flask signals
Java: HttpClient with Jackson for JSON serialization
Go: Custom client with standard library HTTP
Static sites: Build-time submission or serverless functions
Headless CMS: Webhook handlers that trigger IndexNow API
Always verify key file accessibility before going live


Previous: Part 6: Which Search Engines Support IndexNow?
Next: Part 8: API Deep Dive

Share_This Twitter / X
Vishnu
Written By

Vishnu

Founder & Principal Architect at MeshWorld. Senior engineer and instructor specializing in AI agent systems, scalable web architecture, and modern development workflows.

Enjoyed this article?

Support MeshWorld and help us create more technical content