Setting Up Static and Dynamic Sitemaps in Django
Learn how to create static and dynamic sitemaps in Django using the built-in sitemap framework, including model-backed URLs, static views, location(), and common routing mistakes to avoid.
A sitemap is an XML file that tells search engines which URLs exist on your site and, optionally, how often they change, how important they are, and when they were last modified. Django includes a built-in sitemap framework for generating these XML files from Python classes. That way we don't have to maintain them manually.
Django sitemaps usually fall into two categories:
- Static sitemaps for pages that are not backed by a database model, such as your homepage, about page, contact page, or legal pages.
- Dynamic sitemaps for pages generated from model instances, such as blog posts, categories, projects, products, or resources.
This article shows how to set up both.
1. Enable Django’s sitemap framework
First, add the sitemap framework to INSTALLED_APPS:
settings.py
⧉
1 2 3 4 | |
2. Create a sitemaps.py file
You can place this file wherever it makes sense. A common approach is to put it in your project config app:
project/
settings.py
urls.py
sitemaps.py
Or inside individual apps:
apps/
blog/
models.py
sitemaps.py
For a small or medium project, a central sitemaps.py is often easier than having a sitemap file for each app. Once the singular file grows too large, split into app specific.
Static Sitemaps
Static pages are views that exist in your URLconf but are not represented by database rows.
Examples:
urls.py
⧉
1 2 3 4 5 | |
To add these pages to your sitemap:
- Create a Sitemap class
- Optional, but recommended:
- changefreq
- Tells the search engine how often content changes ('daily', 'weekly', 'monthly', ...)
- priority
- Tells the search engine how important you think these pages are (from 0 to 1)
- changefreq
- Define a method called items()
- Return all list of the names as specified inside urls.py (i.e. name='index')
- Define a method called location()
- It returns the URLs by resolving the names returned by location() with Django's reverse() function
sitemaps.py
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
If your URLs are namespaced, include the namespace:
⧉
1 2 3 4 5 6 | |
You can define as many of these classes as you want. However, it makes sense to group them if they share the same priority or changefreq.
Dynamic Sitemaps
Dynamic sitemaps are for model-backed pages and share the same base URL, with parts being dynamic.
For example:
apps/blog/models.py
⧉
1 2 3 4 5 6 7 8 | |
Suppose your URL pattern looks like this:
apps/blog/urls.py
⧉
1 2 3 4 5 6 7 | |
Then your dynamic sitemap has to resolve the base URL ('articles/' in this example) and the dynmic parts ('
So your dynamic sitemap for articles is going to look similar to this:
sitemaps.py
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
Again, we define changefreq and priority (still optional but recommended).
The following methods however have to be changed so they resolve URLs dynamically rather than fixed as in the static example.
The items() method changes: Instead of returning a fixed list of url names, we return PostModel objects (here filtered by active=True)
Here, we add a lastmod() method. It's argument (obj) references each model object. The it returns the objects publishd attribute. If your PostModel had an attribute last_updated, you would use that.
location() resolves the URL again. With dynamic URLs however, Django's reverse() function takes keyword arguments (kwargs). These are mapped to your dynamic url args (here: '
The items() method returns the objects that belong in the sitemap. Django passes each object to methods like location(), lastmod(), changefreq(), and priority().
Alternative: use get_absolute_url()
Instead of defining location() in the sitemap class, you can define get_absolute_url() on the model:
apps/blog/models.py
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Then your sitemap changes to:
⧉
1 2 3 4 5 6 7 8 9 | |
This shortens down the sitemap class by defining how the absolute url should be built inside each Model class individualy.
If no location() method is provided, Django calls get_absolute_url() on each object returned by items().
There is no real advantage over either method. It comes down to preference: do you want the sitemap to resolve the URL or do you want the model to resolve the URL
Make sure to only include paths in your sitemap that you want to be accessed by the public and search engines. Don't include links to i.e. closed login pages
A Reusable Base Sitemap for Active Models
If several models share the same pattern, create a base sitemap class:
sitemaps.py
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Then subclass it:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | |
This gives you one shared rule for filtering active content and setting lastmod, while still letting each model define its own URL structure.
Register the Sitemaps in urls.py
Once your sitemap classes exist, register them in your root URLconf. This makes them accessible from the browser.
main/urls.py
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
You import all sitemap classes (either from one central sitemaps.py file or from all app specific files) and map them to a name (you can choose one freely) inside the sitemaps dictionary to a short section name, such as "posts" or "static".
The the path() function takes these arguments:
- "sitemap.xml"
- The URL for your sitemap
- sitemap
- The imported sitemap function (line 1)
- {"sitemaps": sitemaps}
- The mapped sitemap classes
- name="django.contrib.sitemaps.views.sitemap"
- Reference to the sitemap package, so django knows how to handle / render the sitemap classes
After adding your sitemaps, visit:
/sitemap.xml
Django will generate an XML sitemap dynamically.
You will see something similar to this:
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | |
That's your sitemaps all set up and ready to be submitted to Google Search Console!
Common Mistakes
get_absolute_url() in the Wrong Class
This is wrong:
⧉
1 2 3 4 5 | |
Why? Because Django does not call get_absolute_url() on the sitemap class. It calls it on the model instance.
So either put it on the model:
⧉
1 2 3 4 5 | |
Or define location() on the sitemap:
⧉
1 2 3 4 5 | |
URL Keyword Mismatches
If your URL pattern is:
⧉
1 2 3 4 5 | |
Then your reverse() call must use the same names as specified in your URL:
⧉
1 2 3 4 5 | |
Not:
⧉
1 2 3 4 5 | |
The Python model field may be called sub_category, but the URL parameter is called subcategory. Django’s reverse() matches the URL pattern keyword, not the model field name.
Optional: Sitemap Index for Larger Sites
For larger sites, you can generate a sitemap index. This creates a main sitemap.xml that points to separate sitemap files like:
/sitemap-posts.xml /sitemap-categories.xml /sitemap-static.xml
Example:
urls.py
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Django’s sitemap framework supports sitemap indexes that reference one sitemap file per section. This is especially useful when you have many URLs; the sitemap protocol limit is 50,000 URLs per sitemap, and Django’s default sitemap limit is aligned with that limit.
Complete Example:
sitemaps.py
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | |
urls.py
⧉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
In this final example we define the sitemaps dict inside the sitemap.py file. That way we don't have to import each sitemap class into main/urls.py but only the sitemaps dict.
Final Checklist:
Before calling the sitemap finished, make sure you tick off this list:
[] "django.contrib.sitemaps" is in INSTALLED_APPS [] /sitemap.xml is registered in the root urls.py [] Static pages use reverse() inside location() [] Dynamic model objects either define get_absolute_url() or the sitemap defines location() [] URL kwargs match the URL pattern names exactly [] QuerySets filter out inactive, draft, private, or unpublished objects [] Only public pages are listed in the sitemap [] lastmod returns a datetime/date where possible [] Large sites use a sitemap index
That gives search engines one clean sitemap endpoint while keeping your sitemap logic explicit, testable, and close to your URL design.
Join the Newsletter
Practical insights on Django, backend systems, deployment, architecture, and real-world development — delivered without noise.
Get updates when new guides, learning paths, cheat sheets, and field notes are published.
No spam. Unsubscribe anytime.
There is no third-party involved so don't worry - we won't share your details with anyone.