
【第10回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた【ブログ完成!!】
2021.12.20
第10回~モダンなフレームワークの使い方を学びながらブログシステムを構築~

連載「Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた」も第10回目です。
前回は、「ブログトップの作成」を行いました。
ブログを完成させよう
最終回となる今回は、ブログにとって重要なページを作ったり、システムの微調整をしていきましょう。
具体的には、以下の3点を実装していきます。
- カテゴリごとのページ
- sitemap.xml の自動生成
- フッター整備
カテゴリ(タグ)ごとのページ
まずは、「カテゴリごとのページ」を作っていきましょう。
カテゴリごとのページとは、「/category/{category}」のことです。
表示するものは、前回作成した「index.html」を流用します。
コントローラの作成
では、コントローラを作成します。
やることは以下の2つだけなので、とても簡単です!
- URL からカテゴリを抽出
- ページに反映
スラッグからカテゴリ名を取得する関数を作る
ここで、「スラッグからカテゴリ名を取得する関数」を作りましょう。
これを使うと、ページに『「技術」の記事一覧』のような表示が出来るようになります。
1 2 3 4 5 6 7 8 9 | # models.py def get_category_name(slug): # slugからIDを取得 c_id = [cat.id for cat in db.collection('categories').where('slug', '==', slug).stream()][0] # IDを用いてデータを取得 category = db.collection('categories').document(c_id).get().to_dict() return category['name'] # nameだけ返す |
ルーティング
そうしたら、ルーティングしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # controllers.py @api.route('/category/{category}') def category(req, resp, *, category): articles = get_articles(unreleased=False) # 公開済みのものだけ # カテゴリ抽出 articles = [ art for art in articles if art['category'] == category ] md = markdown.Markdown(extensions=['tables']) thumbnails = [ md.convert(art['thumbnail']) for art in articles ] resp.html = api.template('index.html', title=f'「{get_category_name(category)}」の記事一覧', thumbnails=thumbnails, articles=articles, ) |
ほとんどトップページと同じですね。
ビューも一部変更
ビューは、以下の部分だけ変更します。
1 2 | <!-- <h2>ブログ一覧</h2> --> <h2>{{ title }}</h2> |
忘れずに、「/」のコントローラも変更しておきましょうね。
1 2 3 4 5 6 7 8 9 | # controllers.py @api.route('/') def index(req, resp): #~~~ 省略 ~~~# resp.html = api.template('index.html', title='ブログ一覧', # new thumbnails=thumbnails, articles=articles,) |
これで良さそうです!
動作確認
それでは、動作確認してみましょう。

例: /category/technology
良さそうですね!
細かい部分を整える
先ほど、「スラッグからカテゴリ名を取得する関数」を作成しましたね。
さっそく、「記事個別ページ」や「記事一覧」に反映させてみましょう。
記事個別ページ
1 2 3 4 5 6 | resp.html = api.template('single.html', title=article['title'], cat_name=get_category_name(article['category']), # new article=article, whats_new=whats_new, categories=categories) |
ここで、 article['category'] = get_category_name(article['category']) をやってしまうのは、絶対に NG です!
カテゴリの URL も同じになってしまうので、注意してくださいね。
記事一覧
こちらも同様です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @api.route('/') def index(req, resp): articles = get_articles(unreleased=False) # 公開済みのものだけ # new art_categories = [ get_category_name(art['category']) for art in articles ] md = markdown.Markdown(extensions=['tables']) thumbnails = [ md.convert(art['thumbnail']) for art in articles ] resp.html = api.template('index.html', title='ブログ一覧', thumbnails=thumbnails, art_categories=art_categories, # new articles=articles,) |
「/category/{category}」のコードを最適化
「/category/{category}」も同様ですが、こちらは全て同じカテゴリ名なので、少しだけコードを最適化できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # controllers.py @api.route('/category/{category}') def category(req, resp, *, category): articles = get_articles(unreleased=False) # 公開済みのものだけ # カテゴリ抽出 articles = [ art for art in articles if art['category'] == category ] md = markdown.Markdown(extensions=['tables']) thumbnails = [ md.convert(art['thumbnail']) for art in articles ] # カテゴリ名 呼び出すのは一度だけ cat_name = get_category_name(category) resp.html = api.template('index.html', title=f'「{cat_name}」の記事一覧', art_categories=[cat_name for _ in articles], # 全て同じ thumbnails=thumbnails, articles=articles) |
関数の呼び出しは少なめに!
今回の関数は、データベースとのやり取りを行うため、重めの仕様になっています。
そのため、できるだけ関数の呼び出しが少なくなるよう心がけましょう。
ちなみに、「タグページ」もこれとほとんど同じなので、割愛します。
sitemap.xml の自動生成
サイトマップは、ブログの SEO 的に超重要です。
理想としては、記事が追加・更新・削除されるたびに、「sitemap.xml」を更新するのが良いですね。
では、さっそく実装していきましょう!
ルーティング
「/sitemap.xml」でサイトマップをしっかり取得できないと、サイトマップを送信してもクロールされません。
基本的には、以下のような仕様にすれば OK です。
- テキストとして「sitemap.xml」を開く
- ヘッダーの Content-type を「application/xml」にする
1 2 3 4 5 | # controllers.py @api.route('/sitemap.xml') def sitemap(req, resp): resp.text = open('sitemap.xml').read() resp.headers['Content-type'] = 'application/xml' |
自動生成する関数を作る
何かのファイルと一緒でも良いのですが、筆者は sitemap_generator.py を新たに作り実装しました。
「sitemap.xml」の基本は、以下の通りです。
1 2 3 4 5 6 7 8 9 | <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>https://sample.com/</loc> <lastmod>2020-06-10</lastmod> <changefreq>always</changefreq> <priority>1.0</priority> </url> </urlset> |
記事の URL を追加
これにプラスして、記事の URL を追加していきます。
少しゴリ押しですが、以下のような感じでファイルを上書きしていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | from models import get_articles from datetime import datetime DOMAIN = 'https://sample.com' # 適宜変える def update_sitemap(changefreq='monthly', priority=0.7): """ sitemapを全て更新する カテゴリページは含めない :param changefreq: 記事の更新頻度 :param priority: 記事の優先度 :return: """ # 公開済みのものだけ取得 articles = get_articles(False) # URL locs = [ '{}/category/{}/{}'.format(DOMAIN, article['category'], article['slug']) for article in articles ] # 最終更新日 lastmods = [ article['last_update'].strftime('%Y-%m-%d') for article in articles ] # 上書きする sitemap = open('sitemap.xml', 'w') sitemap.write('<?xml version="1.0" encoding="UTF-8"?>\n') sitemap.write('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n') sitemap.write(' <url>\n') sitemap.write(' <loc>{}/</loc>\n'.format(DOMAIN)) sitemap.write(' <lastmod>{}</lastmod>\n'.format(datetime.now().strftime('%Y-%m-%d'))) # topは今日 sitemap.write(' <changefreq>always</changefreq>\n') sitemap.write(' <priority>1.0</priority>\n') sitemap.write(' </url>\n') # 記事のサイトマップ for loc, lastmod in zip(locs, lastmods): sitemap.write(' <url>\n') sitemap.write(' <loc>{}/</loc>\n'.format(loc)) sitemap.write(' <lastmod>{}</lastmod>\n'.format(lastmod)) sitemap.write(' <changefreq>{}</changefreq>\n'.format(changefreq)) sitemap.write(' <priority>{}</priority>\n'.format(priority)) sitemap.write(' </url>\n') sitemap.write('</urlset>\n') sitemap.close() |
「更新」「新規追加」「削除」部分に追記
そうしたら、上記ファイルを各所で呼びます。
例として、記事の更新をするコントローラでは以下のような感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @api.route('/admin/article/update') async def article_update(req, resp): #~~~ 省略 ~~~# # update update_article_by_slug( original_slug, title, thumbnail, description, slug, contents, category, tags, released ) if released: # 公開になった時に更新 update_sitemap() api.redirect(resp, '/admin') return |
他にも、新規追加・削除の部分に追記しておきましょう。
なお、カテゴリページを含めるかは、みなさん次第です。
動作チェック
試しに、何か記事を更新してみます。

/sitemap.xml にアクセスしてみる
上手く反映できていそうですね!
フッターを作る
最後に、フッターを作成しましょう。
これは Python も Firestore も関係ありませんが、ブログの見た目的には大事です。
「footer.html」を作ってインクルード
「footer.html」を作成し、「layout.html」にインクルードします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <footer> <br> <ul> <li><a href="/">Top</a></li> <li><a href="/about">About</a></li> <li><a href="/profile">Profile</a></li> <li><a href="https://twitter.com">Twitter</a></li> <li><a href="/contact">Contact</a></li> <li><a href="/policy">プライバシーポリシー</a></li> </ul> <div class="logo"><a href="/"><img src="/static/theme/footer.png" alt="My Blog Name"></a></div> <p>Copyright© My Blog Name all rights reserved.</p> <br> </footer> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <!-- templates/layout.html --> <!DOCTYPE html> <html lang="ja"> <head> <title>My Blog Name {{ title }}</title> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="format-detection" content="telephone=no"> <meta name="viewport" content="width=device-width"> <link rel="stylesheet" href="/static/style.css"> </head> <body> <div class="container"> {% block content %} <!-- メインコンテンツ --> {% endblock %} </div> {% include 'footer.html' %} </body> </html> |
ロゴと CSS を用意
デザインは、白色のロゴと、CSS を用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | footer{ background-color: #1e366a; color: white; text-align: center; } footer .logo{ display: block; width: 25%; margin: auto; } footer ul{ display: flex; justify-content: center; } footer ul li{ display: table-cell; color: white; } footer ul li a{ display: block; color: #fff; } |
完成!
こんな感じでどうでしょう!
あくまでサンプルなので、みなさんは自由にこだわってくださいね!

footerサンプル
プライバシーポリシーは、アドセンスなどで必要になってくるので、しっかり作りましょうね。
番外編へつづく!
お疲れ様でした!
「Python Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる」これにて完結です!
全10回にもなる長期連載でしたが、ようやくブログシステムの構築が終わりました。
この記事を通して、「Responder (Python)」と「Cloud Firestore」「Firebase Auth」との連携について、しっかり理解できたと思います。
オリジナルのブログシステムを作ってみよう
この連載で作成したシステムはとてもシンプルなので、自分好みにカスタマイズするのがおすすめです。
参考までに、いくつかアイディアを挙げておきますね。
- 記事の CSS デザイン (code, quote, ...) などを整える
- 関連記事を表示する機能を実装してみる (キーワードを指定したり、タイトルの類似度を計算する)
- SNS シェアボタンを作る
- 月別アーカイブを作る
ぜひ、この連載を参考にして、自分色のブログシステムを構築してみてくださいね!
最後になりますが、第10回に渡りご愛読いただき、ありがとうございました!
次回の記事はこちら(番外編)
第1回はこちら
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ライトコードの日常12月 1, 2023ライトコードクエスト〜東京オフィス歴史編〜
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン