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

前回は、記事作成部分を作りました。
連載「Python Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる」第6回目です。
前回は、記事個別ページを作成しました。
記事作成画面を作ろう
今回は、いよいよ「記事作成画面を実装」していきます。
記事の作成画面は、ブログシステムにとって重要な役目を果たします。
ユーザビリティを考慮しながら作成していきましょう。
記事作成ページに必要なもの
実装に入る前に、「記事作成ページに何が必要か」を考えてみましょう。
- タイトルやコンテンツを書く操作
- カテゴリを選ぶ操作
- 下書き保存
- プレビュー
- 公開
上記に挙げた機能をひとつひとつ実装していきましょう。
執筆形式はマークダウン形式で
コンテンツ作成画面で HTML を直打ちするのは、ユーザビリティに欠けるので避けた方がいいでしょう。
今回は、「マークダウン形式で記事を執筆」できるようにします。
ルーティングおよびビューを作る
最初に「記事作成画面への遷移」と「ビュー」を作っていきます。
ルーティング
ルーティングは「/admin/new」としておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # controllers.py @api.route('/admin/new') def new_article(req, resp): if req.cookies.get('session') is None: api.redirect(resp, '/login') # ログイン済み else: categories = get_categories() tags = get_tags() resp.html = api.template('new.html', name=req.cookies.get('username'), categories=categories, tags=tags) |
ビュー
ビューは、以下の様にデザインしてみました。
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 | <!-- templates/edit.html 記事編集ページ --> {% extends "layout.html" %} {% block content %} <br> <h1>My Blog Name | 管理者ページ</h1> <p>こんにちは,{{ name }} さん</p> <div class="main-container"> <div class="admin-main-menu"> <h2>新規投稿</h2> <div class="edit"> <form action="/admin/add" method="post"> <h3>タイトル</h3> <input type="text" name="title" value=""> <h3>スラッグ</h3> <input type="text" name="slug" value=""> <h3>内容</h3> <textarea name="contents" id="contents"></textarea> <h3>カテゴリ</h3> {% for cat in categories %} <input type="radio" name="category" value="{{cat['slug']}}"> {{cat['name']}} {% endfor %} <h3>タグ</h3> {% for tag in tags %} <input type="checkbox" name="tags[]" value="{{tag['slug']}}"> {{tag['name']}} {% endfor %} <h3>詳細・抜粋</h3> <textarea name="description" id="description"></textarea> <p> <button type="submit" name="draft" value="draft" id="draft">下書き保存</button> <button type="submit" name="preview" value="preview" id="preview" formtarget="_blank">プレビュー </button> <button type="submit" name="release" value="release" id="release">公開</button> </p> </form> </div> </div> <div class="admin-side-menu"> <h2>Menu</h2> <ul> <li><a href="/admin/profile">プロフィール確認・編集</a></li> <li><a href="/admin/new">新規追加</a></li> <li><a href="/admin">投稿一覧</a></li> <li><a href="/admin/category">カテゴリ</a></li> <li><a href="/admin/tag">タグ</a></li> <li><a href="/logout">ログアウト</a></li> </ul> </div> </div> {% endblock %} |
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 | .edit input[type=text]{ width: 90%; padding: 10px 15px; font-size: 16px; border-radius: 3px; border: 2px solid #ddd; box-sizing: border-box; } .edit #contents{ width: 90%; min-height: 700px; height: auto; /*padding: 10px 15px;*/ font-size: 16px; border-radius: 3px; border: 2px solid #ddd; box-sizing: border-box; } .edit #description{ width: 90%; min-height: 200px; height: auto; /*padding: 10px 15px;*/ font-size: 16px; border-radius: 3px; border: 2px solid #ddd; box-sizing: border-box; } .edit button{ display: inline-block; margin: 1.0em 1.0em; } .edit #draft{ background-color: #555555; color: #fff; border-radius: 5px; } .edit #draft:hover{ opacity: 0.7; } .edit #preview{ background-color: #2b52cd; color: #fff; border-radius: 5px; } .edit #preview:hover{ opacity: 0.7; } .edit #release{ background-color: #2d8b58; color: #fff; border-radius: 5px; } .edit #release:hover{ opacity: 0.7; } |
完成!

記事編集画面:上部

記事編集画面:下部
こんな感じになりました。
下部にある3つのボタンのうち、「プレビューだけは新規ウィンドウで開く」ようにしています。
また、いずれのボタンでも、POST は「/admin/add」に投げる様にしているので、早速これを実装していきます。
3つの処理を実装する
各ボタンによって行われる処理は、整理すると以下の様になります。
- 下書き保存: released=False としてデータベースに保存
- プレビュー :データベースには保存せずに、新規ウィンドウとして「現在書かれている内容を反映させた一時的なページ」を表示
- 下書き保存: released=True としてデータベースに保存
HTML に変換しよう
プレビューや実際に記事を表示するときには、「Markdown => HTML」の変換が必要です。
データベースへの保存は、マークダウン形式のままで問題ありません。
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 | # controllers.py @api.route('/admin/add') class Add: async def on_get(self, req, resp): # ログインしてなければ if req.cookies.get('session') is None: api.redirect(resp, '/login') # ログイン済み else: resp.html = api.template('new.html', name=req.cookies.get('username')) async def on_post(self, req, resp): data = await req.media() title = data.get('title', '') thumbnail = '' description = data.get('description', '') slug = data.get('slug', '').lower().replace(' ', '-') # 全て小文字かつ空白は置換 contents = data.get('contents', '') category = data.get('category', '') tags = data.get_list('tags') # プレビュー の場合 if data.get('preview', None) is not None: # マークダウンからHTMLへ # tableはデフォルトでは変換してくれないのでここで指定する md = markdown.Markdown(extensions=['tables']) html = md.convert(contents) whats_new = get_whats_new() categories = get_categories() resp.html = api.template('preview.html', title=title, contents=html, thumbnail=thumbnail, category=category, tags=tags, whats_new=whats_new, categories=categories ) return # それ以外 released = False if data.get('draft', None) is not None else True article = Article( title=title, thumbnail=thumbnail, description=description, author=req.cookies.get('username'), slug=slug, contents=contents, category=category, tags=tags, released=released, ) article.add() api.redirect(resp, '/admin') |
エラー処理とサムネイルはまた後で
エラー処理などはここでは実装しません、割愛します。
なお、サムネイルはまた後程実装します。
ひとまず今は空文字列で代用するので、ご了承ください。
プレビューのビューを作る
前回作った single.html をもとに preview.html を作成します。
日付などは必要ないので、ダミーを入れます。
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 | <!-- templates/single.html 記事個別ページ --> {% extends "layout.html" %} {% block content %} <br> <h1>My Blog Name</h1> <div class="main-container"> <div class="article-main"> <!-- メイン --> <div class="title"><h2>{{ title }}</h2></div> <div class="contents"> <div class="info"> YYYY.mm.dd<br> Author: {{ author }} </div> <a href="/category/{{category}}"><span class="category">🏷 {{ category }}</span></a> <div class="thumbnail"><img src="{{ thumbnail }}" alt="{{ title }}"></div> </div> <div class="contents"> {% autoescape false%} {{ contents }} {% endautoescape%} </div> </div> <!-- サイドメニュー --> {% include 'sidemenu.html' %} </div> {% endblock %} |
動作確認
プレビュー
では、プレビューの動作確認をしてみましょう。
試しに、以下のマークダウンを入れてみます。(他は適当に埋めてください)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | ## h2タイトル 何かしらの文章 ### h3のタイトル リストを作成してみる。 * リスト1 * リスト2 * リスト3 > 引用はこう 文章を書いていて、 **太字で強調したり**、*斜体で強調したり*する 。 コードブロックも入るけどまだダサい ``` print('e.g. python code') ``` | hd1 | hd2 | hd3 | |:-----------|------------:|:------------:| | 左寄せ | 右寄せ | 中央寄せ | | テーブル | も | 良い感じ | |
すると、新規ウィンドウで以下の様に表示されるはずです。
完成!

HTMLに変換された
まだデザインが追いついていないところもありますが、うまくプレビュー画面が反映されましたね!
第7回へつづく!
第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世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン