Django Sitemap Tutorial

A vital part of modern SEO (Search Engine Optimization) is to have a sitemap, an XML file that tells a search engine how often a page is updated and how important it is in relation to other pages on the site. Fortunately, Django comes with a built-in sitemap framework to help in this task.

Generally, a sitemap exists at a single location as a sitemap.xml file such as https://learndjango.com/sitemap.xml. But for sites with over 50,000 URLs, a sitemap index, which creates multiple sitemap files, is appropriate.

Recipe app

Before we can generate a sitemap, we need a basic Django website! Let's create a recipes app for this purpose. I'm going to largely give the commands in this section without much explanation. If you find this part confusing, I cover Django basics at length and in detail in my book, Django for Beginners.

On the command line, install Django, create a new project called config, and a new app called recipe.

$ cd ~/Desktop
$ mkdir recipes && cd recipes
$ pipenv install django~=3.1.0
$ pipenv shell
(recipes) $ django-admin.py startproject config .
(recipes) $ python manage.py startapp recipes

Then within the config/settings.py file add our new recipes app.

# config/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth', 
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'recipes', # new
]

Run the first migrate command to initialize the database.

(recipes) $ python manage.py migrate 

Model

Our model will contain a title and description for each recipe. We'll also add str and get_absolute_url methods as well.

# recipes/models.py
from django.db import models
from django.urls import reverse


class Recipe(models.Model):
    title = models.CharField(max_length=50)
    description = models.TextField()

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('recipe_detail', args=[str(self.id)])

Now create a migrations file for this change and add it to our database via the migrate command.

(recipes) $ python manage.py makemigrations recipes
(recipes) $ python manage.py migrate

Admin

The Django admin is a convenient way to play around with our data so create a superuser account to log in.

(recipes) $ python manage.py createsuperuser 

But--and I always forget this step myself--we also need to update recipe/admin.py to display our new app in the admin.

# recipes/admin.py
from django.contrib import admin 

from .models import Recipe

class RecipeAdmin(admin.ModelAdmin):
    list_display = ('title', 'description',)

admin.site.register(Recipe, RecipeAdmin)

Start up the local server with the command python manage.py runserver and log in at http://127.0.0.1:8000/admin with your superuser account.

Admin Homepage

Click on the "+Add" next to the Recipe section and create two new recipes.

Hummus Recipe

PBJ Recipe

Awesome. Now we have content. What's missing is our URL paths, views, and templates.

URLs

There are two URL files to update: our top-level one at config/urls.py and then one within the recipes app that must be created. Let's create that new one now.

(recipes) $ touch recipes/urls.py

Now update config/urls.py.

# config/urls.py
from django.contrib import admin
from django.urls import path, include # new

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('recipes.urls')), # new
]

And then recipes/urls.py.

# recipes/urls.py
from django.urls import path

from .views import RecipeListView, RecipeDetailView

urlpatterns = [
    path('<int:pk>', RecipeDetailView.as_view(), name='recipe_detail'),
    path('', RecipeListView.as_view(), name='recipe_list'),
]

Views

We will use the built-in Generic Class-Based Views for list and detail pages.

# recipes/views.py
from django.views.generic import ListView, DetailView

from .models import Recipe

class RecipeListView(ListView):
    model = Recipe
    template_name = 'recipe_list.html'


class RecipeDetailView(DetailView):
    model = Recipe
    template_name = 'recipe_detail.html'

Templates

Finally, a corresponding template for each is needed as well. First, create the blank HTML files.

(recipes) $ mkdir recipe/templates
(recipes) $ touch recipe/templates/recipe_list.html
(recipes) $ touch reciple/templates/recipe_detail.html

The recipe_list.html page displays all recipes.

<!-- recipe_list.html -->
<h1>Recipes</h1>
{% for recipe in object_list %}
  <ul>
    <li><a href="{{ recipe.get_absolute_url }}">{{ recipe.title }}</a></li>
  </ul>
{% endfor %}

And recipe_detail.html displays an individual recipe.

<!-- recipe_detail.html -->
<div>
  <h2>{{ object.title }}</h2>
  <p>{{ object.body }}</p>
</div>

That's it! We now have 3 pages in our basic app: - homepage with all recipes - a detail page for each of our 2 recipes in the database

Homepage

Recipe 1

Recipe 2

What we want is a sitemap now!

Installation

To add a sitemap, we must install the sitemap app to our project as well as the sites framework which it relies upon.

# config/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth', 
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites', # new
    'django.contrib.sitemaps', # new 

    'recipes', 
]

SITE_ID = 1 # new

The sites framework requires an update to our database so run migrate at this time.

(recipes) $ python manage.py migrate

There are several ways to implement sitemaps, but the simplest approach is to use GenericSitemap, a convenience class that covers common sitemap usage. Here's how to add it to our config/urls.py file.

# config/urls.py
from django.contrib import admin
from django.contrib.sitemaps import GenericSitemap # new
from django.contrib.sitemaps.views import sitemap # new
from django.urls import path, include

from recipes.models import Recipe # new

info_dict = {
    'queryset': Recipe.objects.all(),
}

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('recipes.urls')),
    path('sitemap.xml', sitemap, # new
        {'sitemaps': {'blog': GenericSitemap(info_dict, priority=0.6)}},
        name='django.contrib.sitemaps.views.sitemap'),
]

Make sure the server is running and visit http://127.0.0.1:8000/sitemap.xml.

Sitemap

There's our sitemap! But notice that it's using example.com as the name of our site. This comes from the sites framework. If you go into the Django admin, there's a new section called Sites where you can change the name.

It's also possible to change the priority and changefreq. And while you might notice that the current version uses http rather than https, if your production site uses https then that will be reflected in the sitemap. Or you can change the protocol entirely, if so desired.