• トップ
  • ブログ一覧
  • 【第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...

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    メディアチーム
    メディアチーム
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background