• トップ
  • ブログ一覧
  • 【第5回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる【記事個別ページ作成】
  • 【第5回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる【記事個別ページ作成】

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

    IT技術

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

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

    前回は、「ブログシステムのデータベース」を作成しました。

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

    記事個別ページを作ろう!

    今回は、「記事個別ページ」を作成していきます。

    記事の個別ページは、ブログにとって命です。

    SEO にも深くかかわってくるので、慎重かつ丁寧に作っていきましょう。

    必要な関数を作る

    まずは、「個別ページに必要なものを取得する関数」をあらかじめ作っておきましょう。

    models.py に加筆

    モデル関連なので、models.py に加筆していきます。

    なお、個別ページのパーマリンクは「/category/{category}/{slug}」という形をとるようにしました。

    3つの関数を用意

    取り急ぎ用意するものは、以下の3つです。

    1. スラッグから記事情報を取得する関数
    2. 最新記事を数件取得する関数
    3. カテゴリ一覧を取得する関数

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

    1# models.py
    2def get_article(slug: str):
    3    """
    4    Get an articles with slug
    5    :param slug:
    6    :return:
    7    """
    8    article = [art.to_dict() for art in db.collection('articles').where('slug', '==', slug).stream()]
    9    if len(article) == 0:
    10        return None
    11
    12    return article[0]
    13
    14def get_whats_new(num: int = 5):
    15    """ get What's New  """
    16    articles = [art.to_dict() for art in db.collection('articles').where('released', '==', True).limit(num).stream()]
    17    
    18    # datetimeでソート
    19    articles = sorted(articles, key=lambda art: art['last_update'], reverse=True)
    20
    21    return articles
    22
    23def get_categories():
    24    """ Get all categories """
    25    return [cat.to_dict() for cat in db.collection('categories').stream()]

    リストの最初の記事だけ取り出すようにする

    今回、スラッグは「一意に定まるもの」として実装しています。

    そのため、article[0] のように、記事は「リストの最初のものだけ取り出す」形にしました。

    なければ、None を返しておきます。

    個別ページのルーティング

    次に、コントローラを実装します。

    今作った関数をもとに、ビューページの「single.html」に投げるだけですが…。

    記事がマークダウン形式なので、まずは HTML に変換しなければいけません。

    Python のライブラリを使う

    Python のライブラリを使いたいと思います。

    1$ pip install markdown

    使い方は、以下のコードを見てください。

    それでは、パーマリンクに沿って実装していきます。

    1# controllers.py
    2@api.route('/category/{category}/{slug}')
    3def single(req, resp, *, category, slug):
    4    """ 記事 """
    5    article = get_article(slug)
    6    categories = get_categories()
    7    whats_new = get_whats_new()
    8    
    9    # markdown => html
    10    md = markdown.Markdown(extensions=['tables'])  # tableも補完
    11    article['thumbnail'] = md.convert(article['thumbnail'])
    12    article['contents'] = md.convert(article['contents'])
    13
    14    resp.html = api.template('single.html',
    15                             title=article['title'],
    16                             article=article,
    17                             whats_new=whats_new,
    18                             categories=categories)

    Responder は動的なルーティングも可能

    このように、動的なルーティングも Responder なら可能です!

    それらのデータを受け取るときは、「関数の引数名を一致させて」受け取ります。

    とてもシンプルですね!

    ビューページ「single.html」の作成

    さて、ここからが今回の肝です。

    正直に言うと、これからやる作業は Responder も Firestore もあまり関係ありません。

    しかし、しっかりブログを構築するために、手を抜かずに作っていきましょう。

    パンくずリストを作る

    まずは、「パンくずリスト」を作ります。

    記事個別ページに必要な上、SEO にも大きな役割を果たすものです。

    パンくずリストの実装は、「schema.org」にしたがっているので、以下を参照してください。

    【HTML Microdata】
    https://www.w3.org/TR/microdata/

    実装

    実装したビューページは、以下の様になります!

    1<!--
    2templates/single.html
    3記事個別ページ
    4
    5Copyright (c) RightCode Inc. All rights reserved.
    6-->
    7
    8{% extends "layout.html" %}
    9{% block content %}
    10
    11<br>
    12<h1>My Blog Name</h1>
    13
    14<div class="main-container">
    15    <div class="article-main">
    16
    17        <!--   パンくずリスト   -->
    18        <div class="breadcrumb">
    19            <ol itemscope itemtype="https://schema.org/BreadcrumbList">
    20                <li itemprop="itemListElement" itemscope
    21                    itemtype="https://schema.org/ListItem">
    22                    <a itemprop="item" href="/">
    23                        <span itemprop="name">ホーム</span>
    24                    </a>
    25                    <meta itemprop="position" content="1" />
    26                </li>
    27                <li itemprop="itemListElement" itemscope
    28                    itemtype="https://schema.org/ListItem">
    29                    <a itemprop="item" href="/category/{{ article['category'] }}">
    30                        <span itemprop="name">{{ article['category'] }}</span>
    31                    </a>
    32                    <meta itemprop="position" content="2" />
    33                </li>
    34                <li itemprop="itemListElement" itemscope
    35                    itemtype="https://schema.org/ListItem">
    36                    <a itemprop="item" href="./{{ article['slug'] }}">
    37                        <span itemprop="name">{{ article['title'] }}</span>
    38                    </a>
    39                    <meta itemprop="position" content="3" />
    40                </li>
    41            </ol>
    42        </div>
    43
    44        <!--   メイン   -->
    45        <div class="title"><h2>{{ article['title'] }}</h2></div>
    46            <div class="contents">
    47                <div class="info">
    48                {{ article['last_update'].strftime('%Y.%m.%d') }} <br>
    49                Author: {{ article['author'] }}
    50                </div>
    51                <a href="/category/{{article['category']}}"><span class="category">🏷 {{ article['category'] }}</span></a>
    52
    53                {% autoescape false%}
    54                <div class="thumbnail">{{ article['thumbnail'] }}</div>
    55            </div>
    56
    57        <div class="contents">
    58            {{ article['contents'] }}
    59            {% endautoescape%}
    60        </div>
    61    </div>
    62
    63    <!--   サイドメニュー  -->
    64    {% include 'sidemenu.html' %}
    65</div>
    66{% endblock %}

    注意!

    HTMLタグが自動でエスケープされないよう{% autoescape false%} を忘れないでください。

    サイドメニューも作ろう

    サイドメニューは、別に作っています。

    1<div class="article-side">
    2    <h3>What's New</h3>
    3    <ul>
    4        {% for wn in whats_new %}
    5        <li><a href="/category/{{ wn['category'] }}/{{ wn['slug'] }}">📄 {{ wn['title'] }} - {{ wn['last_update'].strftime('%Y.%m.%d') }}</a></li>
    6        {% endfor %}
    7    </ul>
    8    <h3>Category</h3>
    9    <ul>
    10        {% for cat in categories %}
    11        <li><a href="/category/{{ cat['slug'] }}">🏷 {{ cat['name'] }}</a></li>
    12        {% endfor %}
    13    </ul>
    14</div>

    CSS

    少し長いですが、CSS は以下のようにしています。

    1.article-main{
    2    width: 70%;
    3    margin: 2.0em 0;
    4}
    5/* パンくずリスト*/
    6.article-main .breadcrumb{
    7    margin: 0;
    8    padding: 0;
    9    list-style: none;
    10}
    11.article-main .breadcrumb li{
    12    display: inline;
    13    list-style: none;
    14    padding: 0;
    15    margin: 0;
    16}
    17.article-main .breadcrumb li:after{
    18    content: '>';
    19    padding: 0 0.2em;
    20    color: #555;
    21}
    22.article-main .breadcrumb li:last-child:after{
    23    content: '';
    24}
    25.article-main .breadcrumb a{
    26    text-decoration: none;
    27    color: #555555;
    28}
    29.article-main .breadcrumb a:hover{
    30    opacity: 0.7;
    31}
    32.article-main .thumbnail {
    33    max-width: 600px;
    34    margin: auto;
    35}
    36.article-main .title h2{
    37    position: relative;
    38    padding-left: 25px;
    39}
    40.article-main .title h2:before{
    41     position: absolute;
    42     content: '';
    43     bottom: -3px;
    44     left: 0;
    45     width: 0;
    46     height: 0;
    47     border: none;
    48     border-left: solid 15px transparent;
    49     border-bottom: solid 15px rgb(119, 195, 223);
    50}
    51.article-main .title h2:after{
    52    position: absolute;
    53    content: '';
    54    bottom: -3px;
    55    left: 10px;
    56    width: 100%;
    57    border-bottom: solid 3px rgb(119, 195, 223);
    58}
    59.article-main .info{
    60    color: gray;
    61}
    62.article-main .contents{
    63    margin: 2.0em 1.5em;
    64}
    65.article-main .contents h2{
    66    position: relative;
    67    padding: 0.6em;
    68    color: #fff;
    69    background: #688cde;
    70}
    71.article-main .contents h3{
    72    padding: 0.25em 0.5em;/*上下 左右の余白*/
    73    color: #494949;/*文字色*/
    74    background: transparent;/*背景透明に*/
    75    border-left: solid 5px #7db4e6;/*左線*/
    76}
    77.article-main .category{
    78    color: white;
    79    background-color: #2b52cd;
    80    border-radius: 5px;
    81    padding: 2px 5px;
    82}
    83.article-main .category:hover{
    84    opacity: 0.7;
    85}
    86.article-main table{
    87    border-collapse:collapse;
    88    width: 90%;
    89    margin:0 auto;
    90}
    91.article-main th{
    92    color:#fff;
    93    background-color: #005ab3;
    94    border:1px solid #999;
    95}
    96.article-main td{
    97    border:1px solid #999;
    98}
    99.article-main td,th{
    100    padding:10px;
    101}
    102.article-side{
    103    width: 20%;
    104    margin: 2.0em auto;
    105}
    106.article-side h3{
    107    background-color: #727b86;
    108    padding: 5px 10px;
    109    color: white;
    110}
    111.article-side ul{
    112    padding: 0;
    113    position: relative;
    114}
    115.article-side ul li{
    116    line-height: 1.5;
    117    padding: 0.5em;
    118    list-style-type: none!important;/*ポチ消す*/
    119}
    120.article-side a{
    121    font-size: 16px;
    122    color: #314a89;
    123    text-decoration: none;
    124}
    125.article-side a:hover{
    126    opacity: 0.7;
    127}

    HTML 側では「受け取ったデータを展開している」だけなので、コード自体はさほど難しくありません。

    個別ページ完成!

    サムネは無いが、個別ページができた

    とりあえずは、これで個別ページの出来上がりです。

    使いやすくなるよう整えよう

    まだ仮なのでシンプルなデザインですが、また後ほど整えていきます。

    「カテゴリごとの記事表示」も実装するので、カテゴリにもしっかりリンクしておいてください。

    今回は省いていますが、著者へのリンクを追加しても良いですね。

    404 Error: Not Foundページを作る

    今作った記事個別ページは、たとえ未公開でも URL さえ知っていれば閲覧できてしまいます

    「未公開記事へのアクセスを避ける」ためと、「URL が間違っている」場合に備えて、404ページを準備しておきましょう。

    コード

    1@api.route('/category/{category}/{slug}')
    2def single(req, resp, *, category, slug):
    3    """ 記事 """
    4    article = get_article(slug)
    5    if article is None or not article['released']:
    6        resp.html = api.template('404.html', title='お探しの記事は見つかりませんでした。')
    7        return
    8
    9    categories = get_categories()
    10    whats_new = get_whats_new()
    11
    12    #~~~ 省略 ~~~#
    1<!--
    2templates/404.html
    3404 Error: Not Found
    4-->
    5
    6{% extends "layout.html" %}
    7{% block content %}
    8
    9<br>
    10<h1>My Blog Name</h1>
    11<h2>404 Error: Not Found</h2>
    12<p>申し訳ありません。お探しのURLは削除されたか、間違っております。</p>
    13<p><a href="/">トップへ戻る</a></p>
    14
    15{% endblock %}

    エラーページ完成!

    404エラーページの例

    デザインやレイアウトは、好みに合わせて変更してください。

    第6回へつづく!

    今回は、ブログの超大事な要素である「記事ページを作成」してみました。

    これで、記事の表示は出来ますが、やるべきことはたくさん残っています。

    次回は「記事作成画面」を作っていきます。

    まだまだ頑張っていきましょう!

    次回の記事はこちら

    featureImg2020.08.11【第6回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる【記事作成画面の実装】第6回~モダンなフレームワークの使い方を学びながらブログシステムを構築~前回は、記事作成部分を作りました。連載「Pyt...

    第1回はこちら

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

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

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

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

    広告メディア事業部

    広告メディア事業部

    おすすめ記事