Django + microCMSでつくるブログサイト ⑥サイト内ページを静的生成する

「Django + microCMSでつくるブログサイト」シリーズの6番目の記事です。
前回までで、ある程度ブログの形はできてきたので、デプロイの準備をしていきます。
今回は公開にあたり、サイト内のすべてのページを事前に静的なhtmlファイルとして書き出しておく処理の説明になります。

djangoで使える静的ジェネレーター

django-distillというライブラリを使用しました。

django-distill

こちらを使うと設定したURLパターンに応じて、サイトのhtmlファイルを網羅的に作成してくれます。

つかいかた

まずは仮想環境を立ち上げた後に、pipでinstallします。

myvenv\\scripts\\activate
pip install django-distill


settings.pyを編集します。

# project/settings.py
...

INSTALLED_APPS = [
    ...
	'django_distill',
]

...

STATIC_ROOT = Path(BASE_DIR, 'static')
# 静的ファイルの配信path
DISTILL_DIR = Path(BASE_DIR, 'dist')


設定の準備としてはこれだけです。
次にアプリ内のurls.pyの全文を以下のように書き換えます。

# blog/urls.py

from django_distill import distill_path
from . import views
from django.conf import settings
import requests
import math

app_name = 'blog'

limit = 2  # 一覧ページに表示する記事数
url = getattr(settings, "BASE_URL", None)
api_key = getattr(settings, "API_KEY", None)
headers = {'X-API-KEY': api_key}

def _post_total_count():
    """ポストAPIのトータル数を返す"""
    return requests.request(method='GET',
                            url=url + '/post',
                            headers=headers).json()['totalCount']

def _tag_total_count():
    """タグAPIのトータル数を返す"""
    return requests.request(method='GET',
                            url=url + '/tag',
                            headers=headers).json()['totalCount']

def get_index():
    """
    トップページ
    引数はないので、Noneを返す
    """
    return None

def get_posts():
    """
    記事詳細ページを生成するためのpost idを返す

    """
    post_total_count = _post_total_count()
    end_point = f'/post?limit={post_total_count}&fields=id'
    res = requests.request('GET', url=url + end_point, headers=headers)
    for data in res.json()['contents']:
        yield data['id']

def get_pages():
    """
    ページ数を指定した一覧ページを生成するためのページ数を返す
    """
    post_total_count = _post_total_count()
    num_page = math.ceil(post_total_count / limit)
    for page_num in range(1, num_page + 1):
        yield {'page': str(page_num)}

def get_tags():
    """
    タグとページ数を指定した一覧ページを生成するための
    タグ+ページ数を返す
    """
    tag_total_count = _tag_total_count()
    post_total_count = _post_total_count()
    # タグの一覧を取得
    end_point = f'/tag?limit={tag_total_count}&fields=id'
    tag_res = requests.request(method='GET',
                               url=url + end_point,
                               headers=headers)

    for data in tag_res.json()['contents']:
        # タグIDを取得
        tag_id = data['id']
        # タグに紐づく記事が何個あるか?を取得
        res = requests.request(method='GET',
                               url=url + f'/post?limit={post_total_count}&filters=tag[contains]{tag_id}',
                               headers=headers)
        post_total_count_with_tag = res.json()['totalCount']
        # 一ページあたりの記事数で割り出して、何ページあるか?を計算
        num_page = math.ceil(post_total_count_with_tag / limit)
        # タグIDとページ数をyield
        for page_num in range(1, num_page + 1):
            yield {'tag_id': tag_id, 'page': str(page_num)}

# urlパターン
urlpatterns = [
    # トップページの普通の記事一覧
    distill_path('',
                 views.post_list,
                 name='index',
                 distill_func=get_index,
                 distill_file='index.html'),
    # 記事詳細ページ
    distill_path('post/<slug:slug>/',
                 views.post_detail,
                 name='post_detail',
                 distill_func=get_posts),
    # ページを指定した記事一覧
    distill_path('page/<str:page>/',

                 views.post_list,
                 name='index_with_page',
                 distill_func=get_pages,
                 ),
    # タグを指定した記事一覧
    distill_path('tag/<str:tag_id>/page/<str:page>/',
                 views.post_list,
                 name='index_with_tag',
                 distill_func=get_tags
                 ),
]


そして以下のようにコマンドラインからmanage.pyで呼び出します。
collect staticを最初に行い、staticフォルダを事前に作っておくのが注意です。

python manage.py collectstatic
python ./manage.py distill-local

>>>
You have requested to create a static version of
this site into the output path directory:

    Source static path:  C:\\Users\\your\\DjangoProject\\static
    Distill output path: C:\\Users\\your\\DjangoProject\\dist

Does not exist, create it? (YES/no):


settings.pyで設定した配信pathDISTILL_PATHに配信しますがいいですか?と聞かれているわけです。
yesとすると静的ファイルの生成が始まります。

yes

>>>
Creating directory...

Generating static site into directory: C:\\Users\\your\\DjangoProject\\dist
Loading site URLs
...省略

Site generation complete


うまくいきますとこのような階層構造でdist配下にhtmlファイルが生成されます。



htmlファイルを開くと、このような表示です。
開発環境だとcssとリンクが効いていないですが、本番環境にデプロイすると、ちゃんとした見た目で動作します。


挙動の説明

distill-localコマンドを呼び出すと、distill_path内で指定しているパターンに沿ってhtmlファイルが生成されます。

distill_path('post/<slug:slug>/',
                 views.post_detail,
                 name='post_detail',
                 distill_func=get_posts),


この場合、postフォルダの中にslugのフォルダを作り、その中にindex.htmlを生成するように解釈されます。
slugがhogehogeだったらpostフォルダ→hogehogeフォルダ→index.htmlという構造になるということです。

views.post_detailname='post_detail'の部分は通常のurlパターンを書く時と同じです。

distill_func=get_postsの部分が特殊です。 こちらが生成の際にviews.py内のpost_detailに引数slugを渡す役割を担っています。
get_posts関数は以下のようになっています。

def get_posts():
    """
    記事詳細ページを生成するためのpost idを返す
    """
    post_total_count = _post_total_count()
    end_point = f'/post?limit={post_total_count}&fields=id'
    res = requests.request('GET', url=url + end_point, headers=headers)
    for data in res.json()['contents']:
        yield data['id']


難しいことは考えずに作成する全てのパターンをyieldする処理をここで書けば動きます。

views.py内のpost_detail関数はslug(=postのid)を指定することでレンダリングする処理でした。
なので、postのidを網羅的にyieldする処理をするわけです。

ページ番号を指定したページ

/page/{ページ番号}のパターンです。 これは簡単で、ページ数を取得して渡すだけです。

def get_pages():
    """
    ページ数を指定した一覧ページを生成するためのページ数を返す
    """
    post_total_count = _post_total_count()
    num_page = math.ceil(post_total_count / limit)
    for page_num in range(1, num_page + 1):
        yield {'page': str(page_num)}


タグを指定したページ

/tag/{タグID}/page/{ページ番号}のパターンです。 ここがちょっと複雑ですが以下のような形で全パターンを網羅的に渡せます。

  1. タグのIDを全て取得する
  2. タグIDごとに何ページあるか?ということを計算する。
  3. タグIDとページ番号をyieldする
def get_tags():
    """
    タグとページ数を指定した一覧ページを生成するための
    タグ+ページ数を返す
    """
    tag_total_count = _tag_total_count()
    post_total_count = _post_total_count()
    # タグの一覧を取得
    end_point = f'/tag?limit={tag_total_count}&fields=id'
    tag_res = requests.request(method='GET',
                               url=url + end_point,
                               headers=headers)

    for data in tag_res.json()['contents']:
        # タグIDを取得
        tag_id = data['id']
        # タグに紐づく記事が何個あるか?を取得
        res = requests.request(method='GET',
                               url=url + f'/post?limit={post_total_count}&filters=tag[contains]{tag_id}',
                               headers=headers)
        post_total_count_with_tag = res.json()['totalCount']
        # 一ページあたりの記事数で割り出して、何ページあるか?を計算
        num_page = math.ceil(post_total_count_with_tag / limit)
        # タグIDとページ数をyield
        for page_num in range(1, num_page + 1):
            yield {'tag_id': tag_id, 'page': str(page_num)}


以上です。
次回は実際に生成した静的なhtmlファイルをNetlifyにデプロイします。

TOPページ