• トップ
  • ブログ一覧
  • 【第6回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる【記事作成画面の実装】
  • 【第6回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみる【記事作成画面の実装】

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

    IT技術

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

    前回は、記事作成部分を作りました。

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

    前回は、記事個別ページを作成しました。

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

    記事作成画面を作ろう

    今回は、いよいよ「記事作成画面を実装」していきます。

    記事の作成画面は、ブログシステムにとって重要な役目を果たします。

    ユーザビリティを考慮しながら作成していきましょう。

    記事作成ページに必要なもの

    実装に入る前に、「記事作成ページに何が必要か」を考えてみましょう。

    1. タイトルやコンテンツを書く操作
    2. カテゴリを選ぶ操作
    3. 下書き保存
    4. プレビュー
    5. 公開

    上記に挙げた機能をひとつひとつ実装していきましょう。

    執筆形式はマークダウン形式で

    コンテンツ作成画面で 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つの処理を実装する

    各ボタンによって行われる処理は、整理すると以下の様になります。

    1. 下書き保存:released=False としてデータベースに保存
    2. プレビュー :データベースには保存せずに、新規ウィンドウとして「現在書かれている内容を反映させた一時的なページ」を表示
    3. 下書き保存: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            # マークダウンからHTML27            # 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| テーブル || 良い感じ |

    すると、新規ウィンドウで以下の様に表示されるはずです。

    完成!

    HTMLに変換された

    まだデザインが追いついていないところもありますが、うまくプレビュー画面が反映されましたね!

    第7回へつづく!

    第6回目は、ブログシステムの超重要機能である「記事作成画面」を実装しました。

    これだけで、かなりブログシステムっぽくなってきましたね。

    あとは、「メディアの投稿」や「カテゴリの作成」、また「記事デザインの整備」などを地道にやっていきます。

    次回もお楽しみに!

    次回の記事はこちら

    featureImg2020.08.12【第7回】Responder + Firestore でモダンかつサーバーレスなブログシステムを作ってみた【メディア管理と非同期処理】第7回~モダンなフレームワークの使い方を学びながらブログシステムを構築~連載「Python Responder + F...

    第1回はこちら

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

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

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

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

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

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

    採用情報へ

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    エンジニア大募集中!

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

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

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

    background