【第5回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる【記事個別ページ作成】
IT技術
第5回~モダンなフレームワークの使い方を学びながらブログシステムを構築~
連載「Python Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる」第5回目です。
前回は、「ブログシステムのデータベース」を作成しました。
記事個別ページを作ろう!
今回は、「記事個別ページ」を作成していきます。
記事の個別ページは、ブログにとって命です。
SEO にも深くかかわってくるので、慎重かつ丁寧に作っていきましょう。
必要な関数を作る
まずは、「個別ページに必要なものを取得する関数」をあらかじめ作っておきましょう。
models.py に加筆
モデル関連なので、models.py に加筆していきます。
なお、個別ページのパーマリンクは「/category/{category}/{slug}」という形をとるようにしました。
3つの関数を用意
取り急ぎ用意するものは、以下の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 %}
エラーページ完成!
デザインやレイアウトは、好みに合わせて変更してください。
第6回へつづく!
今回は、ブログの超大事な要素である「記事ページを作成」してみました。
これで、記事の表示は出来ますが、やるべきことはたくさん残っています。
次回は「記事作成画面」を作っていきます。
まだまだ頑張っていきましょう!
次回の記事はこちら
第1回はこちら
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.07.30Python 特集実装編※最新記事順Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた!P...
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit