django-indexnow is a minimal Django app that submits updated URLs to IndexNow-compatible search engines.
It is designed to stay small: Django + Python standard library only.
pip install django-indexnowAdd the app and middleware:
INSTALLED_APPS = [
# ...
"django.contrib.sites",
"indexnow",
]Expose the app endpoint:
# urls.py
from django.urls import include, path
urlpatterns = [
# ...
path("indexnow/", include("indexnow.urls")),
]Supported settings:
INDEXNOW_API_KEY = "your-32-char-hex-key" # optional; missing/empty disables app
INDEXNOW_ENDPOINT = "https://api.indexnow.org/indexnow" # optional
INDEXNOW_TIMEOUT = 5 # optional
INDEXNOW_USER_AGENT = "django-indexnow/0.1.0" # optional
INDEXNOW_DEDUPE_SECONDS = 60 # optional; 0 disables dedupeLogging uses Python's standard logging. Configure the indexnow logger in your Django LOGGING settings to control emitted levels.
Behavior is automatic:
- If
INDEXNOW_API_KEYis set and non-empty, endpoints and submissions are active. - If it is missing or empty, endpoints return 404 and signal submission silently no-ops.
When enabled, the package serves:
/indexnow/key.txtviaindexnow.urls/<INDEXNOW_API_KEY>.txtonly if middleware is enabled
Both return plain text with exactly:
<your_key>\n
The root key file endpoint (/<INDEXNOW_API_KEY>.txt) is provided by middleware.
If you need that endpoint, add:
MIDDLEWARE = [
# ...
"indexnow.middleware.IndexNowKeyFileMiddleware",
]If you do not add this middleware, /indexnow/key.txt still works.
Dispatch indexnow.signals.indexnow whenever a URL should be submitted:
from indexnow.signals import indexnow
indexnow.send(sender=BlogPost, url=blog_post.get_absolute_url())Relative URLs are resolved against https://<site.domain> from Site.objects.get_current().
Absolute URLs are submitted as-is.
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from indexnow.signals import indexnow
from .models import BlogPost
@receiver(post_save, sender=BlogPost)
def on_blogpost_saved(sender, instance, **kwargs):
indexnow.send(sender=sender, url=instance.get_absolute_url())
@receiver(post_delete, sender=BlogPost)
def on_blogpost_deleted(sender, instance, **kwargs):
indexnow.send(sender=sender, url=instance.get_absolute_url())Generate a key:
python manage.py indexnow_generate_keyGenerate and print settings assignment:
python manage.py indexnow_generate_key --setFrom the repository root:
make testOr directly with Django:
env/python/bin/python -m django test --settings=tests.test_settingsGitHub Actions workflow: .github/workflows/ci-cd.yml
- On pull requests and pushes to
main: runs tests and builds/checks artifacts. - On version tags (examples:
0.1.1,v0.1.1,0.1.1-rc1,v0.1.1-rc1) or published releases:- uploads
dist/*.whlanddist/*.tar.gzto the GitHub Release - publishes a package image to GitHub Container Registry:
ghcr.io/<owner>/django-indexnow:<tag>
- uploads
Note: GitHub Packages does not provide a Python package registry endpoint. This project publishes Python artifacts to Releases and uses GHCR for registry publishing.
IndexNow is currently supported by engines including Bing, Yandex, Seznam, and Naver.
MIT. See LICENSE.