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 instead of maintaining them manually.

Django sitemaps usually fall into two categories:

  1. Static sitemaps for pages that are not backed by a database model, such as your homepage, about page, contact page, or legal pages.
  2. 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

python

1
2
3
4
INSTALLED_APPS = [
    # ...
    "django.contrib.sitemaps",
]

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 easiest.

Static Sitemaps

Static pages are views that exist in your URLconf but are not represented by database rows.

Examples:

urls.py

python

1
2
3
4
5
urlpatterns = [
    path("", views.index, name="index"),
    path("about/", views.about, name="about"),
    path("contact/", views.contact, name="contact"),
]

To add these pages to your sitemap, create a Sitemap class that returns URL names from items() and resolves them with reverse() in location().

sitemaps.py

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from django.contrib.sitemaps import Sitemap
from django.urls import reverse


class StaticViewSitemap(Sitemap):
    changefreq = "weekly"
    priority = 0.8

    def items(self):
        return [
            "index",
            "about",
            "contact",
        ]

    def location(self, item):
        return reverse(item)

If your URLs are namespaced, include the namespace:

python

1
2
3
4
5
6
def items(self):
    return [
        "core:index",
        "core:about",
        "core:contact",
    ]

Dynamic Sitemaps

Dynamic sitemaps are for model-backed pages.

For example:

apps/blog/models.py

python

1
2
3
4
5
6
7
8
class PostModel(models.Model):
    title = models.CharField(max_length=150)
    slug = models.SlugField(unique=True)
    active = models.BooleanField(default=False)
    published = models.DateTimeField(auto_now_add=True)

    category = models.ForeignKey("CategoryModel", on_delete=models.CASCADE)
    sub_category = models.ForeignKey("SubCategoryModel", on_delete=models.CASCADE)

Suppose your URL pattern looks like this:

apps/blog/urls.py

python

1
2
3
4
5
6
7
urlpatterns = [
    path(
        "articles/<slug:category>/<slug:subcategory>/<slug:slug>/",
        views.post,
        name="post",
    ),
]

Then your dynamic sitemap can look like this:

sitemaps.py

python

 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
from django.contrib.sitemaps import Sitemap
from django.urls import reverse

from apps.blog.models import PostModel


class PostSitemap(Sitemap):
    changefreq = "weekly"
    priority = 0.7

    def items(self):
        return PostModel.objects.filter(active=True)

    def lastmod(self, obj):
        return obj.published

    def location(self, obj):
        return reverse(
            "blog:post",
            kwargs={
                "category": obj.category.slug,
                "subcategory": obj.sub_category.slug,
                "slug": obj.slug,
            },
        )

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

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from django.urls import reverse


class PostModel(models.Model):
    # fields...

    def get_absolute_url(self):
        return reverse(
            "blog:post",
            kwargs={
                "category": self.category.slug,
                "subcategory": self.sub_category.slug,
                "slug": self.slug,
            },
        )

Then your sitemap changes to:

python

1
2
3
4
5
6
7
8
9
class PostSitemap(Sitemap):
    changefreq = "weekly"
    priority = 0.7

    def items(self):
        return PostModel.objects.filter(active=True)

    def lastmod(self, obj):
        return obj.published

If no location() method is provided, Django calls get_absolute_url() on each object returned by items().

A Reusable Base Sitemap for Active Models

If several models share the same pattern, create a base sitemap class:

sitemaps.py

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from django.contrib.sitemaps import Sitemap


class _BaseActiveSitemap(Sitemap):
    changefreq = "weekly"
    priority = 0.7

    model = None

    def items(self):
        return self.model.objects.filter(active=True)

    def lastmod(self, obj):
        return getattr(obj, "updated", None) or getattr(obj, "published", None)

Then subclass it:

python

 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
from django.urls import reverse

from apps.blog.models import CategoryModel, PostModel
from apps.field_notes.models import FieldNoteModel
from apps.projects.models import ProjectModel


class PostSitemap(_BaseActiveSitemap):
    model = PostModel

    def location(self, obj):
        return reverse(
            "blog:post",
            kwargs={
                "category": obj.category.slug,
                "subcategory": obj.sub_category.slug,
                "slug": obj.slug,
            },
        )


class CategorySitemap(_BaseActiveSitemap):
    model = CategoryModel

    def location(self, obj):
        return reverse(
            "blog:category",
            kwargs={"slug": obj.slug},
        )


class FieldNoteSitemap(_BaseActiveSitemap):
    model = FieldNoteModel

    def location(self, obj):
        return reverse(
            "field_notes:detail",
            kwargs={"slug": obj.slug},
        )


class ProjectSitemap(_BaseActiveSitemap):
    model = ProjectModel

    def location(self, obj):
        return reverse(
            "projects:detail",
            kwargs={"slug": obj.slug},
        )

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.

urls.py

python

 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
from django.contrib.sitemaps.views import sitemap
from django.urls import path

from .sitemaps import (
    StaticViewSitemap,
    PostSitemap,
    CategorySitemap,
    FieldNoteSitemap,
    ProjectSitemap,
)


sitemaps = {
    "static": StaticViewSitemap,
    "posts": PostSitemap,
    "categories": CategorySitemap,
    "field_notes": FieldNoteSitemap,
    "projects": ProjectSitemap,
}


urlpatterns = [
    # your normal URLs...

    path(
        "sitemap.xml",
        sitemap,
        {"sitemaps": sitemaps},
        name="django.contrib.sitemaps.views.sitemap",
    ),
]

Now visit:

/sitemap.xml

Django will generate an XML sitemap dynamically.

The sitemaps dictionary maps a short section name, such as "posts" or "static", to a Sitemap class or instance.

Common Mistake: get_absolute_url() in the Wrong Class

This is wrong:

python

1
2
3
4
5
class CategorySitemap(BaseActiveSitemap):
    model = CategoryModel

    def get_absolute_url(self):
        return reverse("blog:category", kwargs={"slug": self.slug})

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:

python

1
2
3
4
5
class CategoryModel(models.Model):
    slug = models.SlugField(unique=True)

    def get_absolute_url(self):
        return reverse("blog:category", kwargs={"slug": self.slug})

Or define location() on the sitemap:

python

1
2
3
4
5
class CategorySitemap(BaseActiveSitemap):
    model = CategoryModel

    def location(self, obj):
        return reverse("blog:category", kwargs={"slug": obj.slug})

Common Mistake: URL Keyword Mismatches

If your URL pattern is:

python

1
2
3
4
5
path(
    "articles/<slug:category>/<slug:subcategory>/<slug:slug>/",
    views.post,
    name="post",
)

Then your reverse() call must use:

python

1
2
3
4
5
kwargs={
    "category": obj.category.slug,
    "subcategory": obj.sub_category.slug,
    "slug": obj.slug,
}

Not:

python

1
2
3
4
5
kwargs={
    "category": obj.category.slug,
    "sub_category": obj.sub_category.slug,
    "slug": obj.slug,
}

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

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from django.contrib.sitemaps import views as sitemap_views
from django.urls import path

from .sitemaps import sitemaps


urlpatterns = [
    path(
        "sitemap.xml",
        sitemap_views.index,
        {"sitemaps": sitemaps},
        name="django.contrib.sitemaps.views.index",
    ),
    path(
        "sitemap-<section>.xml",
        sitemap_views.sitemap,
        {"sitemaps": sitemaps},
        name="django.contrib.sitemaps.views.sitemap",
    ),
]

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

python

 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
from django.contrib.sitemaps import Sitemap
from django.urls import reverse

from apps.blog.models import PostModel, CategoryModel
from apps.field_notes.models import FieldNoteModel
from apps.projects.models import ProjectModel


# --- Static
class StaticViewSitemap(Sitemap):
    changefreq = "weekly"
    priority = 0.8

    def items(self):
        return [
            "index",
            "about",
            "contact",
        ]

    def location(self, item):
        return reverse(item)



# --- Dynamic
class _BaseActiveSitemap(Sitemap):
    changefreq = "weekly"
    priority = 0.7

    model = None

    def items(self):
        return self.model.objects.filter(active=True)

    def lastmod(self, obj):
        return getattr(obj, "updated", None) or getattr(obj, "published", None)


class PostSitemap(_BaseActiveSitemap):
    model = PostModel

    def location(self, obj):
        return reverse(
            "blog:post",
            kwargs={
                "category": obj.category.slug,
                "subcategory": obj.sub_category.slug,
                "slug": obj.slug,
            },
        )


sitemaps = {
    "static": StaticViewSitemap,
    "posts": PostSitemap,
    "categories": CategorySitemap,
}

urls.py

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from django.contrib.sitemaps.views import sitemap
from django.urls import path, include

from .sitemaps import sitemaps


urlpatterns = [
    path("", include("apps.blog.urls")),
    path("", include("apps.field_notes.urls")),
    path("", include("apps.projects.urls")),

    path(
        "sitemap.xml",
        sitemap,
        {"sitemaps": sitemaps},
        name="django.contrib.sitemaps.views.sitemap",
    ),
]

Final Checklist:

Before calling the sitemap finished, verify these points:

[] "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 [] 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.