← Build logs
BackendMay 10, 2026

Boosting Blog Post Visibility: Building an Automation System with the IndexNow API

I'm sure many of you have experienced the frustration of publishing a new blog post only to find it's not immediately visible in search engine results. I recently learned that search engines like Bing and Yandex offer a way to quickly notify them of new posts via the IndexNow API. So, I decided to integrate this feature into my blog.

Attempts and Pitfalls

Initially, I created helper functions in services/indexnow_service.py to call the IndexNow API when a post was published. I structured the code to use asyncio.create_task to send a ping asynchronously whenever the post status changed to 'published' in the BlogRepository.update_status method.

# services/indexnow_service.py (partial)
import asyncio
import httpx

async def ping_urls(urls: list[str], api_key: str): async with httpx.AsyncClient() as client: for url in urls: try: response = await client.post( "https://api.indexnow.org/submit-url", json={"url": url, "key": api_key} ) response.raise_for_status() print(f"Successfully pinged {url}") except httpx.HTTPStatusError as e: print(f"Error pinging {url}: {e}") except Exception as e: print(f"An unexpected error occurred for {url}: {e}")

async def ping_blog_post(post_url: str, api_key: str): await ping_urls([post_url], api_key)

BlogRepository.update_status (partial)

async def update_status(self, post_id: int, new_status: str): # ... existing logic ... if new_status == 'published' and INDEXNOW_KEY: post = await self.get_post_by_id(post_id) # In reality, you'd get the URL from the post object asyncio.create_task(ping_blog_post(post.url, INDEXNOW_KEY)) # ...

I also created an admin API endpoint to manually trigger pings. I set up the public/<KEY>.txt file and even configured middleware. But to my surprise, the pings just wouldn't go through, no matter what I tried. After about three hours of debugging, I discovered that the ownership verification file required by the IndexNow API had a different path than I expected. Sometimes, it needed to be accessed not as /public/<KEY>.txt, but simply as /KEY.txt.

The Cause

Ultimately, the problem lay in how the IndexNow API verifies ownership via the verification file. My setup placed the file inside the public/ directory, but IndexNow prefers it directly in the root directory, or it has stricter requirements for specific path configurations. Additionally, the INDEXNOW_KEY environment variable might not have been set correctly, disabling the feature.

The Solution

To resolve this, I made a few adjustments:

  1. Corrected Ownership File Path: I removed the public/ directory and changed the configuration to place the KEY.txt file directly in the root directory. I configured the web framework's middleware to serve this file directly.
  2. Enhanced Environment Variable Check: I added logic to explicitly check if the INDEXNOW_KEY environment variable was set and if it contained a valid value.
  3. Improved Asynchronous Ping Logic: In BlogRepository.update_status, I continued to use asyncio.create_task to ensure the ping request wouldn't block the main request flow.
# services/indexnow_service.py (after modification)
import asyncio
import httpx
import os

INDEXNOW_KEY = os.environ.get("INDEXNOW_KEY")

async def ping_urls(urls: list[str]): if not INDEXNOW_KEY: print("INDEXNOW_KEY is not set. Skipping ping.") return

async with httpx.AsyncClient() as client:
    for url in urls:
        try:
            response = await client.post(
                &quot;https://api.indexnow.org/submit-url&quot;,
                json={&quot;url&quot;: url, &quot;key&quot;: INDEXNOW_KEY}
            )
            response.raise_for_status()
            print(f&quot;Successfully pinged {url}&quot;)
        except httpx.HTTPStatusError as e:
            print(f&quot;Error pinging {url}: {e}&quot;)
        except Exception as e:
            print(f&quot;An unexpected error occurred for {url}: {e}&quot;)

async def ping_blog_post(post_url: str): await ping_urls([post_url])

main.py or app.py (example middleware setup)

from fastapi import FastAPI

from fastapi.staticfiles import StaticFiles

app = FastAPI()

# Configure to serve KEY.txt file directly from the root directory

app.mount("/", StaticFiles(directory=".", html=True), name="static")

# BlogRepository.update_status (after modification)

async def update_status(self, post_id: int, new_status: str):

# ... existing logic ...

if new_status == 'published' and INDEXNOW_KEY:

post = await self.get_post_by_id(post_id)

asyncio.create_task(ping_blog_post(post.url))

# ...

Example admin API endpoint

@router.post("/blog/indexnow-ping-all")

async def indexnow_ping_all():

all_posts = await blog_repository.get_all_published_posts()

for post in all_posts:

asyncio.create_task(ping_blog_post(post.url))

return {"message": "Initiated ping for all published posts."}

Results

  • The time it takes for posts to appear in search engine results after publication has noticeably decreased.
  • The ability to enable or disable the feature at any time via the INDEXNOW_KEY environment variable allows for secure management.
  • Thanks to the admin API, initial setup scenarios and batch pinging of any missed posts have become much easier.
  • asyncio.create_task ensures that pings are handled in the background, having no impact on the user experience.

Summary — Avoiding the Same Pitfalls

  • [ ] When using the IndexNow API, always double-check the exact path configuration for the ownership verification file (KEY.txt). You need to verify your web framework's static file serving settings.
  • [ ] The INDEXNOW_KEY environment variable is mandatory; manage it securely for enabling/disabling the feature.
  • [ ] Process IndexNow pings for post publications asynchronously (asyncio.create_task) to avoid degrading user experience.
  • [ ] Building an admin API to add a batch ping function for all posts is extremely useful during initial setup and for re-processing.