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

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

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

    採用情報へ

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    エンジニア大募集中!

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

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

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

    background