Qlitre's Blog

2022.05.01 Nuxt.js /Python

Nuxt3 Netlifyビルド時にsitemapを生成する方法

今回はNuxt3でNetlifyデプロイ時にsitemapを生成する方法についての記事です。
Nuxt3,microCMS,Netlifyの組み合わせです。

Nuxt2では、@nuxtjs/sitemapを使ってNetlifyでのnpm run generate時にsitemap.xmlを生成していました。
が、Githubのissueを見る限り、Nuxt3にはまだ対応していない気配です。

ここは、運用でカバーということになりますが、Pythonを使ってsitemapを生成する方法を試してみました。

処理の流れ

sitemapを生成するPythonスクリプトを配置してNetlifyデプロイ時にBuild commandから実行する、という形になります。

前準備

プロジェクトのルートディレクトリに、requirements.txtruntime.txtを配置します。
中身は以下のようにします。

requirements.txt

requests
python-dotenv


runtime.txt

3.8


こちらをルートディレクトリに配置しておくことで、Netlifyがデプロイ時に必要ライブラリの読み込みとPythonの実行環境をセットしてくれます。

次に.envファイルに必要な環境変数をセットしておきます。

BASE_URL=https://your-service-name.microcms.io/api/v1
API_KEY=YOUR MICROCMS API KEY
HOST_URL=https://your-website-domain.com


BASE_URLとAPI_KEYはmicroCMSにGETリクエストを投げる際に必要です。
HOST_URLはsitemapのベースとなるURLですね、ここは環境変数にしなくてもいいかもしれませんが、一応入れておきます。

sitemap生成スクリプトの作成

次にsitemapの作成を実行するPythonファイルを作っていきます。
名前は何でもいいですが、ルートディレクトリにpythonscriptsというディレクトリを作り、中にcreate_sitemap.pyというファイルを作成します。
中身は以下のようにします。

import xml.etree.ElementTree as et
import requests
import os
from dotenv import load_dotenv
from pathlib import Path
import datetime

BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv()

END_POINT = '/post'
BASE_URL = os.environ.get('BASE_URL')
API_KEY = os.environ.get('API_KEY')
HEADERS = {'X-MICROCMS-API-KEY': API_KEY}
HOST_URL = os.environ.get('HOST_URL')


def get_total_count():
    """
    投稿された記事数を返す
    """
    queries = {'fields': 'id'}
    res = requests.request('GET',
                           url=BASE_URL + END_POINT,
                           headers=HEADERS,
                           params=queries).json()

    return res['totalCount']


def get_jst_date_string(utc_date_string):
    """
    UTC時刻文字列をJST時刻の文字列に変換
    """
    datetime_utc = datetime.datetime.strptime(utc_date_string, "%Y-%m-%dT%H:%M:%S.%f%z")
    datetime_jst = datetime_utc.astimezone(datetime.timezone(datetime.timedelta(hours=+9)))
    jst_date_string = datetime.datetime.strftime(datetime_jst, '%Y-%m-%d')
    return jst_date_string


def get_sitemap_materials():
    """
    {loc:url,lastmod:更新日}という辞書のリストを返す
    """
    limit = get_total_count()
    queries = {'fields': 'id,updatedAt', 'limit': limit}
    res = requests.request('GET',
                           url=BASE_URL + END_POINT,
                           headers=HEADERS,
                           params=queries)
    materials = []
    for obj in res.json()['contents']:
        jst_date_string = get_jst_date_string(obj['updatedAt'])
        elm = {'loc': f'{HOST_URL}/{obj["id"]}',
               'lastmod': jst_date_string}
        materials.append(elm)
    return materials


def job():
    """
    sitemapを生成する処理
    """
    url_set = et.Element('urlset')
    url_set.set("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9")
    url_set.set("xmlns:news", "http://www.google.com/schemas/sitemap-news/0.9")
    url_set.set("xmlns:xhtml", "http://www.w3.org/1999/xhtml")
    url_set.set("xmlns:mobile", "http://www.google.com/schemas/sitemap-mobile/1.0")
    url_set.set("xmlns:image", "http://www.google.com/schemas/sitemap-image/1.1")
    url_set.set("xmlns:video", "http://www.google.com/schemas/sitemap-video/1.1")
    tree = et.ElementTree(element=url_set)

    # 記事を順番に書きこむ
    for elm in get_sitemap_materials():
        url_element = et.SubElement(url_set, 'url')
        loc = et.SubElement(url_element, 'loc')
        loc.text = elm['loc']
        lastmod = et.SubElement(url_element, 'lastmod')
        lastmod.text = elm['lastmod']
    # 最後にトップURL
    url_element = et.SubElement(url_set, 'url')
    loc = et.SubElement(url_element, 'loc')
    loc.text = HOST_URL

    # Nuxt3プロジェクトのpublicディレクトリに保存
    tree.write(f'{BASE_DIR}/client/public/sitemap.xml', encoding='utf-8', xml_declaration=True)
    # ビルド時に分かりやすくするため、メッセージを出しておく
    success_message = """
    ============
    Successfully create sitemap
    ============
    """
    print(success_message)


if __name__ == '__main__':
    job()


まずここの部分でmicroCMSに投稿された記事数を取得するようにしています。

def get_total_count():
    """
    投稿された記事数を返す
    """
    queries = {'fields': 'id'}
    res = requests.request('GET',
                           url=BASE_URL + END_POINT,
                           headers=HEADERS,
                           params=queries).json()
    return res['totalCount']


microCMSではデフォルトで10件しか記事を取得できないので、一度記事数を取得するためにリクエストを投げています。
responseは以下のような形で返ります。

{'contents': [{'id': 'nuxt3-microcms-blog-tag-filter'},
              # 省略              
              {'id': 'aws-lambda-line-bot'}],
 'limit': 10,
 'offset': 0,
 'totalCount': 66}


ここのtotalCountだけを返しているというわけです。

そうして取得した記事数をlimitに指定して再びGETリクエストを投げればすべての記事が取得できます。

def get_sitemap_materials():
    """
    {loc:url,lastmod:更新日}という辞書のリストを返す
    """
    limit = get_total_count()
    queries = {'fields': 'id,updatedAt', 'limit': limit}
    res = requests.request('GET',
                           url=BASE_URL + END_POINT,
                           headers=HEADERS,
                           params=queries)
    # 省略



あとは順番にURLを作ってリストにすることで素材を作ります。

materials = []
for obj in res.json()['contents']:
        elm = {'loc': f'{HOST_URL}/{obj["id"]}',
               'lastmod': obj['updatedAt']}
        materials.append(elm)


ここの部分ですね。
私のサイトの場合はhttps://qlitre-weblog.com/some-slugというURL構成なので、HOST_URLにidを加えるだけでした。
ここは構成によって変わる部分かと思います。

あとはxml生成ライブラリを使用して、xml形式で書き出していくという流れです。

xmlファイルの作り方ですが、公式ライブラリページを参考にテストコードを書いてみました。
xml.etree.ElementTree
SubElementメソッドを使って階層構造が作れるみたいです。

def test_element_tree():
    url_set = et.Element('urlset')
    url_set.set("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9")
    tree = et.ElementTree(element=url_set)
    url_element = et.SubElement(url_set, 'url')
    loc = et.SubElement(url_element, 'loc')
    loc.text = 'https://example.com/my-pretty-slug'
    lastmod = et.SubElement(url_element, 'lastmod')
    lastmod.text = '2022-05-01'
    tree.write(f'test.xml', encoding='utf-8', xml_declaration=True)


このように出力されます。

<?xml version='1.0' encoding='utf-8'?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <url>
        <loc>https://example.com/my-pretty-slug</loc>
        <lastmod>2022-05-01</lastmod>
    </url>
</urlset>


書き出しのディレクトリですが、Nuxtアプリ内のpublicディレクトリを指定するようにします。

tree.write(f'{BASE_DIR}/client/public/sitemap.xml', encoding='utf-8', xml_declaration=True)


私の場合、プロジェクトのルートディレクトリに配信用のclientディレクトリを作るようにしているので、上記のような記述となります。

コマンドテスト

一度、ルートディレクトリからコマンドを実行してsitemapが生成されるか試してみましょう。

python pythonscripts/create_sitemap.py
>>>
    ============
    Successfully create sitemap
    ============





上の写真のようにsitemap.xmlがpublicフォルダに入っていれば準備オッケーです。

Netlifyデプロイの設定


まず、環境変数でNetlifyにセットしていないものがある場合は追加しておくようにしましょう。
この3つでした。

BASE_URL=https://your-service-name.microcms.io/api/v1
API_KEY=YOUR MICROCMS API KEY
HOST_URL=https://your-domain.com


次に、NetlifyのBuild settingsのBuild commandに下記のように入力します。

python pythonscripts/create_sitemap.py&&npm run build




ポイントはsitemapを生成するpython scriptを先に実行するよう指定することです。
npm run build時にNuxtの機能でpublic配下のsitemapを配置するので、この順番が逆だとうまくいきません。
うまくいけば、以下のようにローカルで実行した場合と同様にSuccesメッセージがNetlifyのデプロイログに表示されます。

...
6:04:11 PM: ────────────────────────────────────────────────────────────────
6:04:11 PM:   1. Build command from Netlify app                             
6:04:11 PM: ────────────────────────────────────────────────────────────────
6:04:11 PM: ​
6:04:11 PM: $ python pythonscripts/create_sitemap.py&&npm run build
6:04:13 PM:     ============
6:04:13 PM:     Successfully create sitemap
6:04:13 PM:     ============
6:04:13 PM: > build
6:04:13 PM: > nuxt build
6:04:13 PM: [log] Nuxt CLI v3.0.0-rc.1
6:04:24 PM: [info] Vite client warmed up in 5336ms
...


うまくいかない場合


場合によって、初期の段階でこのようなエラーが出て止まることがあるかもしれません。

5:52:16 PM: /opt/build-bin/run-build-functions.sh: line 223: /opt/buildhome/python3.8/bin/activate: No such file or directory
5:52:16 PM: Error setting python version from runtime.txt
5:52:16 PM: Creating deploy upload records
5:52:16 PM: Please see https://github.com/netlify/build-image/blob/xenial/included_software.md for current versions
5:52:16 PM: Build was terminated: Build script returned non-zero exit code: 1
5:52:16 PM: Failing build: Failed to build site
5:52:16 PM: Failed during stage 'building site': Build script returned non-zero exit code: 1 (https://ntl.fyi/exit-code-1)
5:52:16 PM: Finished processing build request in 15.884826356s


これはruntime.txtでPythonの3.8を実行環境に指定しているのですが、Netlifyに用意されていない場合に起こります。

その場合Deploy SettingsのBuild image selectionを確認してみてください。
私も一度はまったのですが、古いバージョンだとPython3.8が実行できないみたいです。
バージョン20.0.4を選択するようにします。



ソースコード、例

運営している日記サイトで同じものを使用してデプロイしてみました。
ソースコード
https://github.com/qlitre/qlitre-dialy
Sitemap
https://tranquil-maamoul-e09715.netlify.app/sitemap.xml

追記

2022/05/01
sitemapは作れたのですが、Google Search Consoleで送信したところエラーが起きました。
正常にうごいていたNuxt2でのsitemapジェネレーターを使ってデプロイをしてみましたが、同様の現象が起きたので、sitemapではなくてアプリの動作環境の方に問題があるのかもしれません。



原因調査中です。

TOPページへ