Djangoで、サイトマップを自動生成する

Twitterでシェア FaceBookでシェア はてなブックマークでシェア

Python - Django
2018年12月8日4:44に更新(約5日前)
2018年11月5日23:53に作成(約38日前)

旧ブログ移行記事です。

概要

Djangoでサイトマップを自動生成します。

配信フィードもよければご覧ください。

アプリケーションの準備

Djangoアプリケーションを作らないと試せないので、作成しておきます。

models.py

from django.db import models
from django.utils import timezone


class Post(models.Model):
    title = models.CharField('タイトル', max_length=255)
    text = models.TextField('本文')
    created_at = models.DateTimeField('作成日', default=timezone.now)

    def __str__(self):
        return self.title

urls.py

記事の一覧と記事の詳細ページを定義します。

from django.urls import path
from . import views

app_name = 'app'

urlpatterns = [
    path('', views.PostIndex.as_view(), name='list'),
    path('detail/<int:pk>/', views.PostDetail.as_view(), name='detail'),
]

reverseresolve_urlといった逆引きでこのurls.pyが参照されるので、定義しておく必要があります。ビュー自体の内容は適当に作成しておきましょう。適当で良いです。

settings.py

まず、settings.pyINSTALLED_APPSに3つ追加します。 django.contrib.sitesdjango.contrib.sitemaps、そしてあなたのアプリケーションです。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites',  # 追加
    'django.contrib.sitemaps',  # 追加
    'app.apps.AppConfig',  # 追加
]

また、siteフレームワークを使うので、SITE_IDの指定も必要になります。SITE_ID=1settings.pyのどこかに書いておきましょう。

SITE_ID = 1

プロジェクトのurls.py

ここにサイトマップ用の定義をすべて書いていますが、見づらい場合は他のファイルに分割しましょう。

from django.contrib import admin
from django.contrib.sitemaps import Sitemap
from django.contrib.sitemaps.views import sitemap
from django.shortcuts import resolve_url
from django.urls import path, include
from app.models import Post


class PostSitemap(Sitemap):
    changefreq = "never"
    priority = 0.5

    def items(self):
        return Post.objects.all()

    def location(self, obj):
        return resolve_url('app:detail', pk=obj.pk)

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


sitemaps = {
    'posts': PostSitemap,
}

urlpatterns = [
    path('admin/', admin.site.urls),
    path('sitemap.xml/', sitemap, {'sitemaps': sitemaps},  name='sitemap'),
    path('', include('app.urls')),
]

サイトマップを作成する流れとしては、(プロジェクトの)urlpatternsリストにサイトマップ用の定義を書きます。

path('sitemap.xml/', sitemap, {'sitemaps': sitemaps},  name='sitemap'),

次に、sitemaps辞書を定義します。 辞書の各要素はサイトマップに登録するデータの種類を表します。posts: PostSitemap は記事のサイトマップという意味で定義しました。

sitemaps = {
    'posts': PostSitemap,
}

そして、実際にPostSitemapクラスを作ります。

class PostSitemap(Sitemap):
    changefreq = "never"
    priority = 0.5

    def items(self):
        return Post.objects.all()

    def location(self, obj):
        return resolve_url('app:detail', pk=obj.pk)

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

Sitemapクラス

Sitemapクラスを継承したクラスを作り、いくつかの属性を上書きしていきます。

まず重要なのがitemsメソッドで、このメソッドは必ず定義する必要があります。

    def items(self):
        return Post.objects.all()

このメソッドはリストやQuerysetを返し、locationlastmodといったメソッドを定義していれば、それらに1つずつ渡されていきます。 イメージとしては、以下のような処理を内部で行っています。

sitemap = PostSitemap()
for item in sitemap.items():
    location = sitemap.location(item)
    lastmod = sitemap.lastmod(item)
    ...

location, lastmod, changefreq, priorityはメソッドでもクラス属性でも、どちらで定義してもいいのですが、モデルインスタンスによって値が動的に変わるようなケース...locationで例えるとpk=1のPostは/detail/1 になりますし、最近作成されたばかりの記事はpriority(優先度)を上げたいかもしれません。このような場合は、メソッドにする必要があるでしょう。

locationですが、モデルにget_absolute_urlメソッドを定義していない場合は、このクラス内に必ず定義する必要があります。上でも書きましたが記事URLはモデルインスタンスによって変わるので、メソッドにするのが良いでしょう。

    def location(self, obj):
        return resolve_url('app:detail', pk=obj.pk)

location/detail/1/ のような、プロトコル(http)とドメイン(narito.ninja)を除いた完全なURLの文字列を返すようにします。

次に、lastmodは最後に変更された日付ですね。これも記事によって変わるので、メソッドにしています。

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

changefreqpriorityは、全記事同じ値でいいかなーと思ったのでクラス属性にしました。

    changefreq = "never"
    priority = 0.5

見た目

example.comというドメインになっていますが、管理画面に既に作られているSiteモデルの内容を更新すれば、好きなドメインにできます。

一覧ページや他のメニューのURLも含める

一覧ページのURL、今回のアプリケーションで言えば「/」もサイトマップに追加したいはずです。 プロジェクトのurls.pyを変更します。

まず、sitemaps辞書に新しい定義をつけます。

sitemaps = {
    'posts': PostSitemap,
    'static': StaticSitemap,
}

そして、StaticSitemapクラスを作ります。 考え方は単純で、itemsメソッドで返すリストの中身を'app:list' のようにresolveが評価できる文字列にします。aboutとかそういったページができたら、'app:about' と足すだけです。

class StaticSitemap(Sitemap):
    changefreq = "never"
    priority = 0.5

    def items(self):
        return ['app:list']

    def location(self, obj):
        return resolve_url(obj)  # objには'app:list' が渡される

Googleにpingを打つ方法

sitemapに関連して、こちらも紹介しておきます。 Googleクローラーに「更新したから見に来て」と伝える方法です。

django.contrib.sitemapsping_google関数を使うだけですが、使う場所は自由です。 例えば、Modelのsaveメソッドを上書きする方法があります。

from django.db import models
from django.contrib.sitemaps import ping_google


class Post(models.Model):
    title = models.CharField('タイトル', max_length=255)
    text = models.TextField('本文')
    category = models.ForeignKey(Category, verbose_name='カテゴリ', on_delete=models.PROTECT)
    created_at = models.DateTimeField('作成日', default=timezone.now)

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        try:
            ping_google()
        except Exception:
            pass

以下のようなviewを作り、あるURLにアクセスするとpingが打てるようにする、等も良いでしょう。

@login_required
def ping(request):
    try:
        url = resolve('sitemap')
        ping_google(sitemap_url=url)
    except Exception:
        raise
    else:
        return redirect(....)

他にも、Djangoのシグナルを利用してモデルの保存処理(save)を検知する方法もあります。

Django公式ドキュメントには以下のようにあります。

とはいえ、 ping_google() は Google のサーバに HTTP リクエストを送信 するので、 save() のたびにネットワークアクセスのオーバヘッドが生じます。 もっと効率的にやりたければ、 cron 化されたスクリプトなど、一定の時点で実行 するようスケジュールしたタスクの中で ping_google() を呼び出すとよい でしょう。

私のブログではping用のビューを作る方法をとっています。任意のタイミングで使えるというのが良いですね。 それにあわせてcron等を利用すると更によさそうです。

ちなみにですが、Djangoのコマンドでもpingが打てます。

python manage.py ping_google [/sitemap.xml]

アプリケーション内に定義する

Djangoのサイトマップ定義は、原則プロジェクトのurls.pyで定義する必要があります。 すべてをそこに書くと見づらいですし、Djangoアプリケーション毎の再利用もできませんので、少し分割してみます

アプリケーション内にsitemap.pyを作り、中身を以下のようにします。

from django.contrib.sitemaps import Sitemap
from django.shortcuts import resolve_url
from app.models import Post


class PostSitemap(Sitemap):
    changefreq = "never"
    priority = 0.5

    def items(self):
        return Post.objects.all()

    def location(self, obj):
        return resolve_url('app:detail', pk=obj.pk)

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


class StaticSitemap(Sitemap):
    changefreq = "never"
    priority = 0.5

    def items(self):
        return ['app:list']

    def location(self, obj):
        return resolve_url(obj)

そして、プロジェクトのurls.pyでそれらを読み込みます。以前のに比べるとすっきりしました。

from django.contrib import admin
from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from app.sitemap import PostSitemap, StaticSitemap

sitemaps = {
    'posts': PostSitemap,
    'static': StaticSitemap,
}

urlpatterns = [
    path('admin/', admin.site.urls),
    path('sitemap.xml/', sitemap, {'sitemaps': sitemaps},  name='sitemap'),
    path('', include('app.urls')),
]
Twitterでシェア FaceBookでシェア はてなブックマークでシェア

記事にコメントする