【第6回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる【記事作成画面の実装】
IT技術
第6回~モダンなフレームワークの使い方を学びながらブログシステムを構築~
前回は、記事作成部分を作りました。
連載「Python Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる」第6回目です。
前回は、記事個別ページを作成しました。
記事作成画面を作ろう
今回は、いよいよ「記事作成画面を実装」していきます。
記事の作成画面は、ブログシステムにとって重要な役目を果たします。
ユーザビリティを考慮しながら作成していきましょう。
記事作成ページに必要なもの
実装に入る前に、「記事作成ページに何が必要か」を考えてみましょう。
- タイトルやコンテンツを書く操作
- カテゴリを選ぶ操作
- 下書き保存
- プレビュー
- 公開
上記に挙げた機能をひとつひとつ実装していきましょう。
執筆形式はマークダウン形式で
コンテンツ作成画面で HTML を直打ちするのは、ユーザビリティに欠けるので避けた方がいいでしょう。
今回は、「マークダウン形式で記事を執筆」できるようにします。
ルーティングおよびビューを作る
最初に「記事作成画面への遷移」と「ビュー」を作っていきます。
ルーティング
ルーティングは「/admin/new」としておきます。
1# controllers.py
2@api.route('/admin/new')
3def new_article(req, resp):
4 if req.cookies.get('session') is None:
5 api.redirect(resp, '/login')
6
7 # ログイン済み
8 else:
9 categories = get_categories()
10 tags = get_tags()
11 resp.html = api.template('new.html',
12 name=req.cookies.get('username'),
13 categories=categories,
14 tags=tags)
ビュー
ビューは、以下の様にデザインしてみました。
1<!--
2templates/edit.html
3記事編集ページ
4-->
5
6{% extends "layout.html" %}
7{% block content %}
8
9<br>
10<h1>My Blog Name | 管理者ページ</h1>
11<p>こんにちは,{{ name }} さん</p>
12
13<div class="main-container">
14 <div class="admin-main-menu">
15 <h2>新規投稿</h2>
16 <div class="edit">
17 <form action="/admin/add" method="post">
18 <h3>タイトル</h3>
19 <input type="text" name="title" value="">
20 <h3>スラッグ</h3>
21 <input type="text" name="slug" value="">
22 <h3>内容</h3>
23 <textarea name="contents" id="contents"></textarea>
24 <h3>カテゴリ</h3>
25 {% for cat in categories %}
26 <input type="radio" name="category" value="{{cat['slug']}}"> {{cat['name']}}
27 {% endfor %}
28 <h3>タグ</h3>
29 {% for tag in tags %}
30 <input type="checkbox" name="tags[]" value="{{tag['slug']}}"> {{tag['name']}}
31 {% endfor %}
32 <h3>詳細・抜粋</h3>
33 <textarea name="description" id="description"></textarea>
34 <p>
35 <button type="submit" name="draft" value="draft" id="draft">下書き保存</button>
36 <button type="submit" name="preview" value="preview" id="preview" formtarget="_blank">プレビュー </button>
37 <button type="submit" name="release" value="release" id="release">公開</button>
38 </p>
39 </form>
40 </div>
41 </div>
42
43 <div class="admin-side-menu">
44 <h2>Menu</h2>
45 <ul>
46 <li><a href="/admin/profile">プロフィール確認・編集</a></li>
47 <li><a href="/admin/new">新規追加</a></li>
48 <li><a href="/admin">投稿一覧</a></li>
49 <li><a href="/admin/category">カテゴリ</a></li>
50 <li><a href="/admin/tag">タグ</a></li>
51 <li><a href="/logout">ログアウト</a></li>
52 </ul>
53 </div>
54
55</div>
56
57{% endblock %}
1.edit input[type=text]{
2 width: 90%;
3 padding: 10px 15px;
4 font-size: 16px;
5 border-radius: 3px;
6 border: 2px solid #ddd;
7 box-sizing: border-box;
8}
9.edit #contents{
10 width: 90%;
11 min-height: 700px;
12 height: auto;
13 /*padding: 10px 15px;*/
14 font-size: 16px;
15 border-radius: 3px;
16 border: 2px solid #ddd;
17 box-sizing: border-box;
18}
19.edit #description{
20 width: 90%;
21 min-height: 200px;
22 height: auto;
23 /*padding: 10px 15px;*/
24 font-size: 16px;
25 border-radius: 3px;
26 border: 2px solid #ddd;
27 box-sizing: border-box;
28}
29.edit button{
30 display: inline-block;
31 margin: 1.0em 1.0em;
32}
33.edit #draft{
34 background-color: #555555;
35 color: #fff;
36 border-radius: 5px;
37}
38.edit #draft:hover{
39 opacity: 0.7;
40}
41.edit #preview{
42 background-color: #2b52cd;
43 color: #fff;
44 border-radius: 5px;
45}
46.edit #preview:hover{
47 opacity: 0.7;
48}
49.edit #release{
50 background-color: #2d8b58;
51 color: #fff;
52 border-radius: 5px;
53}
54.edit #release:hover{
55 opacity: 0.7;
56}
完成!
こんな感じになりました。
下部にある3つのボタンのうち、「プレビューだけは新規ウィンドウで開く」ようにしています。
また、いずれのボタンでも、POST は「/admin/add」に投げる様にしているので、早速これを実装していきます。
3つの処理を実装する
各ボタンによって行われる処理は、整理すると以下の様になります。
- 下書き保存:released=False としてデータベースに保存
- プレビュー :データベースには保存せずに、新規ウィンドウとして「現在書かれている内容を反映させた一時的なページ」を表示
- 下書き保存:released=True としてデータベースに保存
HTML に変換しよう
プレビューや実際に記事を表示するときには、「Markdown => HTML」の変換が必要です。
データベースへの保存は、マークダウン形式のままで問題ありません。
1# controllers.py
2@api.route('/admin/add')
3class Add:
4 async def on_get(self, req, resp):
5 # ログインしてなければ
6 if req.cookies.get('session') is None:
7 api.redirect(resp, '/login')
8
9 # ログイン済み
10 else:
11 resp.html = api.template('new.html',
12 name=req.cookies.get('username'))
13
14 async def on_post(self, req, resp):
15 data = await req.media()
16 title = data.get('title', '')
17 thumbnail = ''
18 description = data.get('description', '')
19 slug = data.get('slug', '').lower().replace(' ', '-') # 全て小文字かつ空白は置換
20 contents = data.get('contents', '')
21 category = data.get('category', '')
22 tags = data.get_list('tags')
23
24 # プレビュー の場合
25 if data.get('preview', None) is not None:
26 # マークダウンからHTMLへ
27 # tableはデフォルトでは変換してくれないのでここで指定する
28 md = markdown.Markdown(extensions=['tables'])
29 html = md.convert(contents)
30
31 whats_new = get_whats_new()
32 categories = get_categories()
33 resp.html = api.template('preview.html',
34 title=title,
35 contents=html,
36 thumbnail=thumbnail,
37 category=category,
38 tags=tags,
39 whats_new=whats_new,
40 categories=categories
41 )
42 return
43
44 # それ以外
45 released = False if data.get('draft', None) is not None else True
46
47 article = Article(
48 title=title,
49 thumbnail=thumbnail,
50 description=description,
51 author=req.cookies.get('username'),
52 slug=slug,
53 contents=contents,
54 category=category,
55 tags=tags,
56 released=released,
57 )
58
59 article.add()
60 api.redirect(resp, '/admin')
エラー処理とサムネイルはまた後で
エラー処理などはここでは実装しません、割愛します。
なお、サムネイルはまた後程実装します。
ひとまず今は空文字列で代用するので、ご了承ください。
プレビューのビューを作る
前回作ったsingle.html をもとにpreview.html を作成します。
日付などは必要ないので、ダミーを入れます。
1<!--
2templates/single.html
3記事個別ページ
4-->
5
6{% extends "layout.html" %}
7{% block content %}
8
9<br>
10<h1>My Blog Name</h1>
11
12<div class="main-container">
13 <div class="article-main">
14
15 <!-- メイン -->
16 <div class="title"><h2>{{ title }}</h2></div>
17 <div class="contents">
18 <div class="info">
19 YYYY.mm.dd<br>
20 Author: {{ author }}
21 </div>
22 <a href="/category/{{category}}"><span class="category">🏷 {{ category }}</span></a>
23
24 <div class="thumbnail"><img src="{{ thumbnail }}" alt="{{ title }}"></div>
25 </div>
26
27 <div class="contents">
28 {% autoescape false%}
29 {{ contents }}
30 {% endautoescape%}
31 </div>
32 </div>
33
34 <!-- サイドメニュー -->
35 {% include 'sidemenu.html' %}
36</div>
37{% endblock %}
動作確認
プレビュー
では、プレビューの動作確認をしてみましょう。
試しに、以下のマークダウンを入れてみます。(他は適当に埋めてください)
1## h2タイトル
2何かしらの文章
3
4### h3のタイトル
5リストを作成してみる。
6
7* リスト1
8* リスト2
9* リスト3
10
11> 引用はこう
12
13文章を書いていて、 **太字で強調したり**、*斜体で強調したり*する 。
14
15コードブロックも入るけどまだダサい
16
17```
18print('e.g. python code')
19```
20
21| hd1 | hd2 | hd3 |
22|:-----------|------------:|:------------:|
23| 左寄せ | 右寄せ | 中央寄せ |
24| テーブル | も | 良い感じ |
すると、新規ウィンドウで以下の様に表示されるはずです。
完成!
まだデザインが追いついていないところもありますが、うまくプレビュー画面が反映されましたね!
第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
おすすめ記事
immichを知ってほしい
2024.10.31