
【第5回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる【記事個別ページ作成】
2021.12.20
第5回~モダンなフレームワークの使い方を学びながらブログシステムを構築~

連載「Python Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる」第5回目です。
前回は、「ブログシステムのデータベース」を作成しました。
記事個別ページを作ろう!
今回は、「記事個別ページ」を作成していきます。
記事の個別ページは、ブログにとって命です。
SEO にも深くかかわってくるので、慎重かつ丁寧に作っていきましょう。
必要な関数を作る
まずは、「個別ページに必要なものを取得する関数」をあらかじめ作っておきましょう。
models.py に加筆
モデル関連なので、 models.py に加筆していきます。
なお、個別ページのパーマリンクは「/category/{category}/{slug}」という形をとるようにしました。
3つの関数を用意
取り急ぎ用意するものは、以下の3つです。
- スラッグから記事情報を取得する関数
- 最新記事を数件取得する関数
- カテゴリ一覧を取得する関数
さっそく実装していきましょう!
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 | # models.py def get_article(slug: str): """ Get an articles with slug :param slug: :return: """ article = [art.to_dict() for art in db.collection('articles').where('slug', '==', slug).stream()] if len(article) == 0: return None return article[0] def get_whats_new(num: int = 5): """ get What's New """ articles = [art.to_dict() for art in db.collection('articles').where('released', '==', True).limit(num).stream()] # datetimeでソート articles = sorted(articles, key=lambda art: art['last_update'], reverse=True) return articles def get_categories(): """ Get all categories """ return [cat.to_dict() for cat in db.collection('categories').stream()] |
リストの最初の記事だけ取り出すようにする
今回、スラッグは「一意に定まるもの」として実装しています。
そのため、 article[0] のように、記事は「リストの最初のものだけ取り出す」形にしました。
なければ、 None を返しておきます。
個別ページのルーティング
次に、コントローラを実装します。
今作った関数をもとに、ビューページの「single.html」に投げるだけですが…。
記事がマークダウン形式なので、まずは HTML に変換しなければいけません。
Python のライブラリを使う
Python のライブラリを使いたいと思います。
1 | $ pip install markdown |
使い方は、以下のコードを見てください。
それでは、パーマリンクに沿って実装していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # controllers.py @api.route('/category/{category}/{slug}') def single(req, resp, *, category, slug): """ 記事 """ article = get_article(slug) categories = get_categories() whats_new = get_whats_new() # markdown => html md = markdown.Markdown(extensions=['tables']) # tableも補完 article['thumbnail'] = md.convert(article['thumbnail']) article['contents'] = md.convert(article['contents']) resp.html = api.template('single.html', title=article['title'], article=article, whats_new=whats_new, categories=categories) |
Responder は動的なルーティングも可能
このように、動的なルーティングも Responder なら可能です!
それらのデータを受け取るときは、「関数の引数名を一致させて」受け取ります。
とてもシンプルですね!
ビューページ「single.html」の作成
さて、ここからが今回の肝です。
正直に言うと、これからやる作業は Responder も Firestore もあまり関係ありません。
しかし、しっかりブログを構築するために、手を抜かずに作っていきましょう。
パンくずリストを作る
まずは、「パンくずリスト」を作ります。
記事個別ページに必要な上、SEO にも大きな役割を果たすものです。
パンくずリストの実装は、「schema.org」にしたがっているので、以下を参照してください。
【HTML Microdata】
https://www.w3.org/TR/microdata/
実装
実装したビューページは、以下の様になります!
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | <!-- templates/single.html 記事個別ページ Copyright (c) RightCode Inc. All rights reserved. --> {% extends "layout.html" %} {% block content %} <br> <h1>My Blog Name</h1> <div class="main-container"> <div class="article-main"> <!-- パンくずリスト --> <div class="breadcrumb"> <ol itemscope itemtype="https://schema.org/BreadcrumbList"> <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"> <a itemprop="item" href="/"> <span itemprop="name">ホーム</span> </a> <meta itemprop="position" content="1" /> </li> <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"> <a itemprop="item" href="/category/{{ article['category'] }}"> <span itemprop="name">{{ article['category'] }}</span> </a> <meta itemprop="position" content="2" /> </li> <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"> <a itemprop="item" href="./{{ article['slug'] }}"> <span itemprop="name">{{ article['title'] }}</span> </a> <meta itemprop="position" content="3" /> </li> </ol> </div> <!-- メイン --> <div class="title"><h2>{{ article['title'] }}</h2></div> <div class="contents"> <div class="info"> {{ article['last_update'].strftime('%Y.%m.%d') }} <br> Author: {{ article['author'] }} </div> <a href="/category/{{article['category']}}"><span class="category">🏷 {{ article['category'] }}</span></a> {% autoescape false%} <div class="thumbnail">{{ article['thumbnail'] }}</div> </div> <div class="contents"> {{ article['contents'] }} {% endautoescape%} </div> </div> <!-- サイドメニュー --> {% include 'sidemenu.html' %} </div> {% endblock %} |
注意!
HTMLタグが自動でエスケープされないよう、 {% autoescape false%} を忘れないでください。
サイドメニューも作ろう
サイドメニューは、別に作っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <div class="article-side"> <h3>What's New</h3> <ul> {% for wn in whats_new %} <li><a href="/category/{{ wn['category'] }}/{{ wn['slug'] }}">📄 {{ wn['title'] }} - {{ wn['last_update'].strftime('%Y.%m.%d') }}</a></li> {% endfor %} </ul> <h3>Category</h3> <ul> {% for cat in categories %} <li><a href="/category/{{ cat['slug'] }}">🏷 {{ cat['name'] }}</a></li> {% endfor %} </ul> </div> |
CSS
少し長いですが、CSS は以下のようにしています。
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | .article-main{ width: 70%; margin: 2.0em 0; } /* パンくずリスト*/ .article-main .breadcrumb{ margin: 0; padding: 0; list-style: none; } .article-main .breadcrumb li{ display: inline; list-style: none; padding: 0; margin: 0; } .article-main .breadcrumb li:after{ content: '>'; padding: 0 0.2em; color: #555; } .article-main .breadcrumb li:last-child:after{ content: ''; } .article-main .breadcrumb a{ text-decoration: none; color: #555555; } .article-main .breadcrumb a:hover{ opacity: 0.7; } .article-main .thumbnail { max-width: 600px; margin: auto; } .article-main .title h2{ position: relative; padding-left: 25px; } .article-main .title h2:before{ position: absolute; content: ''; bottom: -3px; left: 0; width: 0; height: 0; border: none; border-left: solid 15px transparent; border-bottom: solid 15px rgb(119, 195, 223); } .article-main .title h2:after{ position: absolute; content: ''; bottom: -3px; left: 10px; width: 100%; border-bottom: solid 3px rgb(119, 195, 223); } .article-main .info{ color: gray; } .article-main .contents{ margin: 2.0em 1.5em; } .article-main .contents h2{ position: relative; padding: 0.6em; color: #fff; background: #688cde; } .article-main .contents h3{ padding: 0.25em 0.5em;/*上下 左右の余白*/ color: #494949;/*文字色*/ background: transparent;/*背景透明に*/ border-left: solid 5px #7db4e6;/*左線*/ } .article-main .category{ color: white; background-color: #2b52cd; border-radius: 5px; padding: 2px 5px; } .article-main .category:hover{ opacity: 0.7; } .article-main table{ border-collapse:collapse; width: 90%; margin:0 auto; } .article-main th{ color:#fff; background-color: #005ab3; border:1px solid #999; } .article-main td{ border:1px solid #999; } .article-main td,th{ padding:10px; } .article-side{ width: 20%; margin: 2.0em auto; } .article-side h3{ background-color: #727b86; padding: 5px 10px; color: white; } .article-side ul{ padding: 0; position: relative; } .article-side ul li{ line-height: 1.5; padding: 0.5em; list-style-type: none!important;/*ポチ消す*/ } .article-side a{ font-size: 16px; color: #314a89; text-decoration: none; } .article-side a:hover{ opacity: 0.7; } |
HTML 側では「受け取ったデータを展開している」だけなので、コード自体はさほど難しくありません。
個別ページ完成!

サムネは無いが、個別ページができた
とりあえずは、これで個別ページの出来上がりです。
使いやすくなるよう整えよう
まだ仮なのでシンプルなデザインですが、また後ほど整えていきます。
「カテゴリごとの記事表示」も実装するので、カテゴリにもしっかりリンクしておいてください。
今回は省いていますが、著者へのリンクを追加しても良いですね。
404 Error: Not Foundページを作る
今作った記事個別ページは、たとえ未公開でも URL さえ知っていれば閲覧できてしまいます。
「未公開記事へのアクセスを避ける」ためと、「URL が間違っている」場合に備えて、404ページを準備しておきましょう。
コード
1 2 3 4 5 6 7 8 9 10 11 12 | @api.route('/category/{category}/{slug}') def single(req, resp, *, category, slug): """ 記事 """ article = get_article(slug) if article is None or not article['released']: resp.html = api.template('404.html', title='お探しの記事は見つかりませんでした。') return categories = get_categories() whats_new = get_whats_new() #~~~ 省略 ~~~# |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!-- templates/404.html 404 Error: Not Found --> {% extends "layout.html" %} {% block content %} <br> <h1>My Blog Name</h1> <h2>404 Error: Not Found</h2> <p>申し訳ありません。お探しのURLは削除されたか、間違っております。</p> <p><a href="/">トップへ戻る</a></p> {% endblock %} |
エラーページ完成!

404エラーページの例
デザインやレイアウトは、好みに合わせて変更してください。
第6回へつづく!
今回は、ブログの超大事な要素である「記事ページを作成」してみました。
これで、記事の表示は出来ますが、やるべきことはたくさん残っています。
次回は「記事作成画面」を作っていきます。
まだまだ頑張っていきましょう!
次回の記事はこちら
第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世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン