Frozen-FlaskとLektor


# TL;DR

- LektorはFlaskライクな簡易CMS
- ただしCMSとしての機能はかなり限定的で、かっこよいサイトを簡単に作れる感じではない
- が、一度もpyファイル触らずにそれなりのものはできる。研究室のメンバー紹介とか作れそう。
- Flask(+Frozen-Flask)使うよりはずっと簡単

# はじめに
Github Pagesにホストさせるウェブサイトを作ろうとした際に、ウェブアプリをFlaskで作成した経験があるので、それベースで作ろうと考えました。ただし、Github Pageはアプリケーション(pyファイル)はホストできないため、少し調べたところ、以下の2点が見つかりました。

[Frozen-Flask](https://pythonhosted.org/Frozen-Flask/)
Frozen-Flaskとは、Flaskで作成したアプリケーションを、静的なファイルに変換するFlaskの拡張ライブラリ。

[Lektor](https://www.getlektor.com)
Lektorとは、CMSの機能も持った静的なウェブサイトのジェネレータ。Flaskと作者が同じであり、Jinja2がテンプレートエンジンである点など使用感が似ているらしい。
[ここ](http://lucumr.pocoo.org/2015/12/21/introducing-lektor/)に作者の思いが書かれていますが、両親でも使えるような、つまり非プログラマでも使える純粋なCMSを作りたかったようです。

やったこととしては、
1. Lektorを使ってみた。
2. 同じ内容をFrozen-Flaskで作ってみた

# Lektor
基本的には上記公式にある、[Installation](https://www.getlektor.com/docs/installation/)、[Quickstart](https://www.getlektor.com/docs/quickstart/)の項目通りでOKです。今回はPIPでインストールしています。ちなみにPython3には対応していました。

```shell-session
C:\Users\ikedak2\PycharmProjects\Lektor\venv\Scripts>activate
(venv) C:\Users\ikedak2\PycharmProjects\Lektor\venv\Scripts>cd ../../
(venv) C:\Users\ikedak2\PycharmProjects\Lektor>pip install lektor
# quickstartというコマンドでテスト用のプロジェクトを作成できる
(venv) C:\Users\ikedak2\PycharmProjects\Lektor>lektor quickstart
Lektor Quickstart
=================

This wizard will generate a new basic project with some sensible defaults for
getting started quickly.  We just need to go through a few questions so that
the project is set up correctly for you.

Step 1:
| A project needs a name.  The name is primarily used for the admin UI and
| some other places to refer to your project to not get confused if multiple
| projects exist.  You can change this at any later point.
> Project Name: Test

Step 2:
| Your name.  This is used in a few places in the default template to refer to
| in the default copyright messages.
> Author Name [ikedak2]:

Step 3:
| This is the path where the project will be located.  You can move a project
| around later if you do not like the path.  If you provide a relative path it
| will be relative to the working directory.
> Project Path [C:\Users\ikedak2\PycharmProjects\Lektor\Test]:

Step 4:
| Do you want to generate a basic blog module?  If you enable this the models
| for a very basic blog will be generated.
> Add Basic Blog [Y/n]: n

That's all. Create project? [Y/n] Y

(venv) C:\Users\ikedak2\PycharmProjects\Lektor>
```

実行することで以下のようなフォルダ構成のサイトが構成されました。

    
```shell-session
(venv) C:\Users\ikedak2\PycharmProjects\Lektor>tree /F ..\Lektor\Test
C:\USERS\IKEDAK2\PYCHARMPROJECTS\LEKTOR\TEST
│  Test.lektorproject
│
├─assets
│  └─static
│          style.css
│
├─content
│  │  contents.lr
│  │
│  ├─about
│  │      contents.lr
│  │
│  └─projects
│          contents.lr
│
├─models
│      page.ini
│
└─templates
    │  layout.html
    │  page.html
    │
    └─macros
            pagination.html

```

ウェブサーバとして起動すると、

```shell-session
(venv) C:\Users\ikedak2\PycharmProjects\Lektor\Test>lektor server
 * Project path: C:\Users\ikedak2\PycharmProjects\Lektor\Test\Test.lektorproject
 * Output path: C:\Users\ikedak2\AppData\Local\Lektor\Cache\builds\145e3dcb732c0335bf9e1a4ef38f0217
Started source info update
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Finished source info update in 0.12 sec
Started build
U index.html
U about/index.html
U projects/index.html
U static/style.css
Finished build in 0.24 sec
Started prune
Finished prune in 0.02 sec
```

![image](https://qiita-image-store.s3.amazonaws.com/0/159980/e508efd0-0a27-da1a-6ca1-d1da7de099cb.png)

こんな感じになります。右上の鉛筆マークをクリックすると、CMSらしく管理画面が表示され、ページの追加や編集が可能です。
※この時点ではまだ「動的」なページであり、HTMLファイルもJinjaのテンプレートとなっています。

![image](https://qiita-image-store.s3.amazonaws.com/0/159980/c08e6e04-aaab-4023-d74d-821e969502e7.png)

これをbuildコマンドで静的なファイルに変換します。

```shell-session
# このコマンドでどこにプロジェクトファイルが生成されるか確認する
(venv) C:\Users\ikedak2\PycharmProjects\Lektor\Test>lektor project-info --output-path
C:\Users\ikedak2\AppData\Local\Lektor\Cache\builds\145e3dcb732c0335bf9e1a4ef38f0217
# 静的なHTMLファイルを生成
(venv) C:\Users\ikedak2\PycharmProjects\Lektor\Test>lektor build
Started build
Finished build in 0.02 sec
Started prune
Finished prune in 0.02 sec

# 生成されたファイルの内容確認
(venv) C:\Users\ikedak2\PycharmProjects\Lektor\Test>tree /F C:\Users\ikedak2\AppData\Local\Lektor\Cache\builds\145e3dcb732c0335bf9e1a4ef38f0217
フォルダー パスの一覧:  ボリューム OS
ボリューム シリアル番号は F4DB-7972 です
C:\USERS\IKEDAK2\APPDATA\LOCAL\LEKTOR\CACHE\BUILDS\145E3DCB732C0335BF9E1A4EF38F0217
│  index.html
│
├─.lektor
│      buildstate
│
├─about
│      index.html
│
├─projects
│      index.html
│
└─static
        style.css

```

## Data Modeling
最低限はここまでとなりますが、1つだけ応用チックなこともやってみます。
Adminページから新規ページを作成可能ですが、デフォルトではpageというモデルしか選べません。モデルとはページに含める情報(の型)だけをまとめたものです。

![image](https://qiita-image-store.s3.amazonaws.com/0/159980/1dc28af8-dc92-5458-8d0b-4f289f5a021f.png)

これはmodelsディレクトリに(model name).iniというファイルを作成することで追加できます。例として、チームメンバーの自己紹介ページを作るイメージで下記のようにしてみました。

```ini:models/member.ini
[model]
name = Member Page
label = {{ this.title }}

[fields.name]
label = Name
type = string
size = large
width = 1/2

[fields.joined]
description = Date when the member joined this team
label = JoinedDate
type = date
width = 1/4


[fields.comment]
label = Comment
type = text

[fields.skills]
label = Skill
type = checkboxes
choices = Python,Java,Ruby,Go,C
```

モデル用iniの記載方法は下記を参考にしています。
[Data Modelingについて](https://www.getlektor.com/docs/models/)
[Fieldとして何が使えるか](https://www.getlektor.com/docs/api/db/types/)

また公式には詳しく書いていないようなのですが、モデルはページに含める情報(の型)だけしか持たないので、本当の意味でのHTMLテンプレートを別に作成する必要があります。ファイル名はデフォルトではモデルの名前と同じものを探すようです。

```html:member.html
{% extends "layout.html" %}
{% block title %}{{ this.name }}{% endblock %}
{% block body %}
  <h2>{{ this.name }}</h2>
  - Joined Date: <br>
  {{ this.joined }}<br>
  - Skills: <br>
  {% for skill in this.skills %}
    {{ skill }} <br>
  {% endfor %}
  - Comment:<br>
  {{ this.comment }}<br>
{% endblock %}
```

ここまで作成して、サーバを起動しなおすと、以下の通り作成したテンプレートを使うことができます。

![image](https://qiita-image-store.s3.amazonaws.com/0/159980/dc106dba-0f37-a165-f2cf-2f0f0602771c.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/159980/44ee185a-7ede-fdc0-65bb-74343a385592.png)

見た目はこんな感じになります。※リンクは自動生成されないようです。

![image](https://qiita-image-store.s3.amazonaws.com/0/159980/30a2487a-49fd-885a-308a-0f07447a4248.png)


# Frozen-Flask
Lektorで作成したものと同じようなウェブサイトをFlaskで作成すると、どうなるか?
こちらは例がたくさんあると思うので、結果だけ。

ディレクトリ構造は下記の通り。(Tomページなしの状態です)

```shell-session
(venv) C:\Users\ikedak2\PycharmProjects\Lektor>tree /F Test-Flask
C:\USERS\IKEDAK2\PYCHARMPROJECTS\LEKTOR\TEST-FLASK
│  freeze.py
│  myapp.py
│
└─app
    │  routes.py
    │  __init__.py
    │
    ├─assets
    │  └─static
    │          style.css
    │
    ├─templates
              layout.html
              page.html

```

アプリケーションの大元。appディレクトリのapp(`__init__.py`)を実行してウェブサーバ起動するだけ。

```py3:myapp.py
#!/usr/bin/env python
# -*- encoding: utf-8 -*-

from app import app

app.run(debug=True)
```

Frozen-Flaskの本体はコレ。Freezerというクラスにappインスタンスを入れてfreeze()実行するだけ。

```py3:freeze.py
#!/usr/bin/env python
# -*- encoding: utf-8 -*-

from flask_frozen import Freezer
from app import app

freezer = Freezer(app)

if __name__ == '__main__':
    freezer.freeze()
```

Flaskをインスタンス化して、ルーティングを呼び出す。

```py3:app/__init__.py
#!/usr/bin/env python
# -*- encoding: utf-8 -*-

from flask import Flask
import os
app = Flask(__name__, static_folder=os.path.join(os.getcwd(), 'app', 'assets', 'static'))

from app import routes
```

ルーティング。

```py3:app/routes.py
#!/usr/bin/env python
# -*- encoding: utf-8 -*-

from flask import render_template,url_for
from app import app

@app.route('/')
def index():
    title = 'Test'
    body = 'This is test content.'
    return render_template('page.html', title=title, body=body)

@app.route('/about/') #最後に/を入れないと、Frozenした時にhtmlにならない
def about():
    title = 'About this Website'
    body = '''
    This is a website that was made with the Lektor quickstart.

    And it does not contain a lot of information.
    '''

    return render_template('page.html', title=title, body=body)

@app.route('/projects/') #最後に/を入れないと、Frozenした時にhtmlにならない
def projects():
    title = 'Projects'
    body = '''
    This is a list of the projects:
    
    * Project 1
    * Project 2
    * Project 3
    '''

    return render_template('page.html', title=title, body=body)

```

```html:templates/layout.html
<!doctype html>
<meta charset="utf-8">
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}"> <!-- static/style.cssを読み込む -->
<title>{% block title %}Welcome{% endblock %} — Test</title>
<body>
  <header>
    <h1>Test</h1>
    <nav>
      <ul class="nav navbar-nav">
        <a href="{{ url_for('index') }}"> Home </a></li>
        <a href="{{ url_for('about') }}"> About </a></li>
        <a href="{{ url_for('projects') }}"> Projects </a></li>
      </ul>
    </nav>
  </header>
  <div class="page">
    {% block body %}{% endblock %}
  </div>
  <footer>
    &copy; Copyright 2019 by ikedak2.
  </footer>
</body>
```

```html:templates/page.html
{% extends "layout.html" %}
{% block title %}{{ title }}{% endblock %}
{% block body %}
  <h2>{{ title }}</h2>
  {{ body }}
{% endblock %}
```

この状態でfreeze.pyを実行すると、app配下にbuildというディレクトリが作成され、中に生成された静的ファイルが保存されます。
※buildは毎回上書きされるので注意!

```shell-session
(venv) C:\Users\ikedak2\PycharmProjects\Lektor>tree /F Test-Flask\app\build
C:\USERS\IKEDAK2\PYCHARMPROJECTS\LEKTOR\TEST-FLASK\APP\BUILD
│  index.html
│
├─about
│      index.html
│
├─projects
│      index.html
│
└─static
        style.css

```

Frozenの部分は簡単だが、一通り書くのはそれなりに大変。

## 注意点
### Flaskのcss配置場所指定
本質的な問題ではないですが、Lektorはasset/statis/という階層にcssを配置していますが、Flaskはデフォルトではおおもとのpy(myapp.py)と同じ階層のstaticというフォルダをCSSの配置場所とみなします。(url_for('static')が返すのがこれ)
これを変更するには、Flaskアプリケーションをインスタンス化する際に、static_folderというパラメータを入れる必要がある。
`app = Flask(__name__, static_folder=os.path.join(os.getcwd(), 'app', 'assets', 'static'))`

### Flaskのdecorator記載方法
`@app.route('/projects')`のように、最後に/が入っていないと、Frozenした時にhtmlとして出力されません。これまであまり気にしていませんでしたが、重要なポイントのようです。

# まとめ
最初に記載した通り。Flask好きなら…という感じ。でもCMSとしてはかなり簡単では??
ただテーマとか少ないし、かっこよくするには努力がいる。あとリンクとかも自分で張ったりするの大変。この辺は拡張とかうまく使えばクリアできるかもしれない。
Frozen-Flaskは最初から静的なサイトを作る目的で使うようなものではないと思われる。

# 参考

[routes.pyでのスラッシュ不足](https://www.reddit.com/r/flask/comments/335cyi/a_problem_with_frozenflask/)

実行時に以下のエラーが表示される。

```shell-session
MimetypeMismatchWarning: Filename extension of 'about' (type application/octet-stream) does not match Content-Type: text/html; charset=utf-8   return set(page.url for page in self.freeze_yield())
```