• トップ
  • ブログ一覧
  • 【第10回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた【ブログ完成!!】
  • 【第10回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた【ブログ完成!!】

    広告メディア事業部広告メディア事業部
    2020.08.20

    IT技術

    第10回~モダンなフレームワークの使い方を学びながらブログシステムを構築~

    連載「Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた」も第10回目です。

    前回は、「ブログトップの作成」を行いました。

    featureImg2020.08.17【第9回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた【ブログトップを作る】第9回~モダンなフレームワークの使い方を学びながらブログシステムを構築~連載「Python Responder+Fir...

    ブログを完成させよう

    最終回となる今回は、ブログにとって重要なページを作ったり、システムの微調整をしていきましょう。

    具体的には、以下の3点を実装していきます。

    1. カテゴリごとのページ
    2. sitemap.xml の自動生成
    3. フッター整備

    カテゴリ(タグ)ごとのページ

    まずは、「カテゴリごとのページ」を作っていきましょう。

    カテゴリごとのページとは、「/category/{category}」のことです。

    表示するものは、前回作成した「index.html」を流用します。

    コントローラの作成

    では、コントローラを作成します。

    やることは以下の2つだけなので、とても簡単です!

    1. URL からカテゴリを抽出
    2. ページに反映

    スラッグからカテゴリ名を取得する関数を作る

    ここで、「スラッグからカテゴリ名を取得する関数」を作りましょう。

    これを使うと、ページに『「技術」の記事一覧』のような表示が出来るようになります。

    1# models.py
    2def get_category_name(slug):
    3    # slugからIDを取得
    4    c_id = [cat.id for cat in db.collection('categories').where('slug', '==', slug).stream()][0]
    5    
    6    # IDを用いてデータを取得
    7    category = db.collection('categories').document(c_id).get().to_dict()
    8
    9    return category['name']  # nameだけ返す

    ルーティング

    そうしたら、ルーティングしましょう。

    1# controllers.py
    2@api.route('/category/{category}')
    3def category(req, resp, *, category):
    4    articles = get_articles(unreleased=False)  # 公開済みのものだけ
    5
    6    # カテゴリ抽出
    7    articles = [
    8        art for art in articles
    9        if art['category'] == category
    10    ]
    11
    12    md = markdown.Markdown(extensions=['tables'])
    13    thumbnails = [
    14        md.convert(art['thumbnail']) for art in articles
    15    ]
    16
    17    resp.html = api.template('index.html',
    18                             title=f'「{get_category_name(category)}」の記事一覧',
    19                             thumbnails=thumbnails,
    20                             articles=articles, )

    ほとんどトップページと同じですね。

    ビューも一部変更

    ビューは、以下の部分だけ変更します。

    1<!--   <h2>ブログ一覧</h2>    -->
    2<h2>{{ title }}</h2>

    忘れずに、「/」のコントローラも変更しておきましょうね。

    1# controllers.py
    2@api.route('/')
    3def index(req, resp):
    4    #~~~ 省略 ~~~#
    5
    6    resp.html = api.template('index.html',
    7                             title='ブログ一覧',  # new
    8                             thumbnails=thumbnails,
    9                             articles=articles,)

    これで良さそうです!

    動作確認

    それでは、動作確認してみましょう。

    例: /category/technology

    良さそうですね!

    細かい部分を整える

    先ほど、「スラッグからカテゴリ名を取得する関数」を作成しましたね。

    さっそく、「記事個別ページ」や「記事一覧」に反映させてみましょう。

    記事個別ページ

    1resp.html = api.template('single.html',
    2                         title=article['title'],
    3                         cat_name=get_category_name(article['category']),  # new
    4                         article=article,
    5                         whats_new=whats_new,
    6                         categories=categories)

    ここで、article['category'] = get_category_name(article['category']) をやってしまうのは、絶対に NG です!

    カテゴリの URL も同じになってしまうので、注意してくださいね。

    記事一覧

    こちらも同様です。

    1@api.route('/')
    2def index(req, resp):
    3    articles = get_articles(unreleased=False)  # 公開済みのものだけ
    4
    5    # new
    6    art_categories = [
    7        get_category_name(art['category'])
    8        for art in articles
    9    ]
    10
    11    md = markdown.Markdown(extensions=['tables'])
    12    thumbnails = [
    13        md.convert(art['thumbnail']) for art in articles
    14    ]
    15
    16    resp.html = api.template('index.html',
    17                             title='ブログ一覧',
    18                             thumbnails=thumbnails,
    19                             art_categories=art_categories,  # new
    20                             articles=articles,)

    「/category/{category}」のコードを最適化

    「/category/{category}」も同様ですが、こちらは全て同じカテゴリ名なので、少しだけコードを最適化できます。

    1# controllers.py
    2@api.route('/category/{category}')
    3def category(req, resp, *, category):
    4    articles = get_articles(unreleased=False)  # 公開済みのものだけ
    5
    6    # カテゴリ抽出
    7    articles = [
    8        art for art in articles
    9        if art['category'] == category
    10    ]
    11
    12    md = markdown.Markdown(extensions=['tables'])
    13    thumbnails = [
    14        md.convert(art['thumbnail']) for art in articles
    15    ]
    16    
    17    # カテゴリ名 呼び出すのは一度だけ
    18    cat_name = get_category_name(category)
    19
    20    resp.html = api.template('index.html',
    21                             title=f'「{cat_name}」の記事一覧',
    22                             art_categories=[cat_name for _ in articles],  # 全て同じ
    23                             thumbnails=thumbnails,
    24                             articles=articles)

    関数の呼び出しは少なめに!

    今回の関数は、データベースとのやり取りを行うため、重めの仕様になっています。

    そのため、できるだけ関数の呼び出しが少なくなるよう心がけましょう。

    ちなみに、「タグページ」もこれとほとんど同じなので、割愛します。

    sitemap.xml の自動生成

    サイトマップは、ブログの SEO 的に超重要です。

    理想としては、記事が追加・更新・削除されるたびに、「sitemap.xml」を更新するのが良いですね。

    では、さっそく実装していきましょう!

    ルーティング

    「/sitemap.xml」でサイトマップをしっかり取得できないと、サイトマップを送信してもクロールされません。

    基本的には、以下のような仕様にすれば OK です。

    1. テキストとして「sitemap.xml」を開く
    2. ヘッダーの Content-type を「application/xml」にする
    1# controllers.py
    2@api.route('/sitemap.xml')
    3def sitemap(req, resp):
    4    resp.text = open('sitemap.xml').read()
    5    resp.headers['Content-type'] = 'application/xml'

    自動生成する関数を作る

    何かのファイルと一緒でも良いのですが、筆者はsitemap_generator.py を新たに作り実装しました。

    「sitemap.xml」の基本は、以下の通りです。

    1<?xml version="1.0" encoding="UTF-8"?>
    2<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    3    <url>
    4        <loc>https://sample.com/</loc>
    5        <lastmod>2020-06-10</lastmod>
    6        <changefreq>always</changefreq>
    7        <priority>1.0</priority>
    8    </url>
    9</urlset>

    記事の URL を追加

    これにプラスして、記事の URL を追加していきます。

    少しゴリ押しですが、以下のような感じでファイルを上書きしていきます。

    1from models import get_articles
    2from datetime import datetime
    3
    4
    5DOMAIN = 'https://sample.com'  # 適宜変える
    6
    7
    8def update_sitemap(changefreq='monthly', priority=0.7):
    9    """
    10    sitemapを全て更新する
    11    カテゴリページは含めない
    12    :param changefreq: 記事の更新頻度
    13    :param priority: 記事の優先度
    14    :return:
    15    """
    16    # 公開済みのものだけ取得
    17    articles = get_articles(False)
    18
    19    # URL
    20    locs = [
    21        '{}/category/{}/{}'.format(DOMAIN, article['category'], article['slug'])
    22        for article in articles
    23    ]
    24
    25    # 最終更新日
    26    lastmods = [
    27        article['last_update'].strftime('%Y-%m-%d')
    28        for article in articles
    29    ]
    30
    31    # 上書きする
    32    sitemap = open('sitemap.xml', 'w')
    33    sitemap.write('<?xml version="1.0" encoding="UTF-8"?>\n')
    34    sitemap.write('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n')
    35    sitemap.write('    <url>\n')
    36    sitemap.write('        <loc>{}/</loc>\n'.format(DOMAIN))
    37    sitemap.write('        <lastmod>{}</lastmod>\n'.format(datetime.now().strftime('%Y-%m-%d')))  # topは今日
    38    sitemap.write('        <changefreq>always</changefreq>\n')
    39    sitemap.write('        <priority>1.0</priority>\n')
    40    sitemap.write('    </url>\n')
    41
    42    # 記事のサイトマップ
    43    for loc, lastmod in zip(locs, lastmods):
    44        sitemap.write('    <url>\n')
    45        sitemap.write('        <loc>{}/</loc>\n'.format(loc))
    46        sitemap.write('        <lastmod>{}</lastmod>\n'.format(lastmod))
    47        sitemap.write('        <changefreq>{}</changefreq>\n'.format(changefreq))
    48        sitemap.write('        <priority>{}</priority>\n'.format(priority))
    49        sitemap.write('    </url>\n')
    50
    51    sitemap.write('</urlset>\n')
    52    sitemap.close()

    「更新」「新規追加」「削除」部分に追記

    そうしたら、上記ファイルを各所で呼びます。

    例として、記事の更新をするコントローラでは以下のような感じ。

    1@api.route('/admin/article/update')
    2async def article_update(req, resp):
    3    #~~~ 省略 ~~~#
    4
    5    # update
    6    update_article_by_slug(
    7        original_slug,
    8        title,
    9        thumbnail,
    10        description,
    11        slug,
    12        contents,
    13        category,
    14        tags,
    15        released
    16    )
    17
    18    if released:  # 公開になった時に更新
    19        update_sitemap()
    20
    21    api.redirect(resp, '/admin')
    22    return

    他にも、新規追加・削除の部分に追記しておきましょう。

    なお、カテゴリページを含めるかは、みなさん次第です。

    動作チェック

    試しに、何か記事を更新してみます。

    /sitemap.xml にアクセスしてみる

    上手く反映できていそうですね!

    フッターを作る

    最後に、フッターを作成しましょう。

    これは Python も Firestore も関係ありませんが、ブログの見た目的には大事です。

    「footer.html」を作ってインクルード

    「footer.html」を作成し、「layout.html」にインクルードします。

    1<footer>
    2    <br>
    3     <ul>
    4         <li><a href="/">Top</a></li>
    5         <li><a href="/about">About</a></li>
    6         <li><a href="/profile">Profile</a></li>
    7         <li><a href="https://twitter.com">Twitter</a></li>
    8         <li><a href="/contact">Contact</a></li>
    9         <li><a href="/policy">プライバシーポリシー</a></li>
    10
    11    </ul>
    12    <div class="logo"><a href="/"><img src="/static/theme/footer.png" alt="My Blog Name"></a></div>
    13    <p>Copyright&copy; My Blog Name all rights reserved.</p>
    14    <br>
    15</footer>
    1<!--
    2templates/layout.html
    3-->
    4
    5<!DOCTYPE html>
    6<html lang="ja">
    7<head>
    8    <title>My Blog Name {{ title }}</title>
    9    <meta charset="UTF-8">
    10    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    11    <meta name="format-detection" content="telephone=no">
    12    <meta name="viewport" content="width=device-width">
    13
    14    <link rel="stylesheet" href="/static/style.css">
    15</head>
    16<body>
    17<div class="container">
    18    {% block content %}
    19    <!-- メインコンテンツ -->
    20    {% endblock %}
    21</div>
    22{% include 'footer.html' %}
    23</body>
    24</html>

    ロゴと CSS を用意

    デザインは、白色のロゴと、CSS を用意します。

    1footer{
    2    background-color: #1e366a;
    3    color: white;
    4    text-align: center;
    5}
    6footer .logo{
    7    display: block;
    8    width: 25%;
    9    margin: auto;
    10}
    11footer ul{
    12    display: flex;
    13    justify-content: center;
    14}
    15footer ul li{
    16    display: table-cell;
    17    color: white;
    18}
    19footer ul li a{
    20    display: block;
    21    color: #fff;
    22}

    完成!

    こんな感じでどうでしょう!

    あくまでサンプルなので、みなさんは自由にこだわってくださいね!

    footerサンプル

    プライバシーポリシーは、アドセンスなどで必要になってくるので、しっかり作りましょうね。

    番外編へつづく!

    お疲れ様でした!

    Python Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる」これにて完結です!

    全10回にもなる長期連載でしたが、ようやくブログシステムの構築が終わりました。

    この記事を通して、「Responder (Python)」と「Cloud Firestore」「Firebase Auth」との連携について、しっかり理解できたと思います。

    オリジナルのブログシステムを作ってみよう

    この連載で作成したシステムはとてもシンプルなので、自分好みにカスタマイズするのがおすすめです。

    参考までに、いくつかアイディアを挙げておきますね。

    1. 記事の CSS デザイン (code, quote, ...) などを整える
    2. 関連記事を表示する機能を実装してみる (キーワードを指定したり、タイトルの類似度を計算する)
    3. SNS シェアボタンを作る
    4. 月別アーカイブを作る

    ぜひ、この連載を参考にして、自分色のブログシステムを構築してみてくださいね!

    最後になりますが、第10回に渡りご愛読いただき、ありがとうございました!

    次回の記事はこちら(番外編)

    featureImg2020.09.07【番外編】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた【Pythonでコードをハイライトする】番外編~モダンなフレームワークの使い方を学びながらブログシステムを構築~本連載「Python Responder + ...

    第1回はこちら

    featureImg2020.07.17【第1回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた【初期セットアップ編】モダンなフレームワークの使い方を学びながらブログシステムを構築今回より、また新たに Python WebAPI 関連の...

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...

    featureImg2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...

    広告メディア事業部

    広告メディア事業部

    おすすめ記事