はじめに
今回から数回に分けて,実際にWagtailで簡単なCMSを構築していってみようと思う.ゴールとしては,複数の研究プロジェクトや研究グループのページをホストするサイトをイメージしてみた.
各研究プロジェクトや研究グループのトップページをTopPage
と名付けよう.その下には,プロジェクトの概要説明,メンバー紹介などのページなどを置くことになりそうだが,それらのために使うページをPlainPage
と呼ぶことにする.さらに,ブログポスト的なページPostPage
と,関連するブログポストをまとめてリスト表示するページListPage
を用意することにしよう.
ひとまず,デフォルトで用意されているHomePage
はそのままにしておいて,その下に複数のTopPage
をぶら下げられるようにする.そして,TopPage
の下にはPlainPage
,もしくはListPage
をぶら下げられるようにし,ListPage
の下にはさらに別のListPage
,あるいはPostPage
をぶら下げられるようにする.
今回は,TopPage
を題材にして,実際にページを定義し,編集し,それを表示させるまでの大まかな流れをみていこう.
ページの定義
上で導入したHomePage
,TopPage
,PlainPage
,ListPage
,PostPage
などはCMSで管理するページの種類を表している.Wagtailでは,これらのページを,Page
クラスを継承したサブクラスとして定義していく.
デフォルトで用意されているHomePage
の定義はhome/models.pyに下記のように記述されている.
from wagtail.core.models import Page
class HomePage(Page):
pass
すなわち,HomePage
は,デフォルトではPage
クラスに何も追加されていない.
スーパーユーザでWagtailの管理サイトにログインすると,"Welcome to your new Wagtail site!"というタイトルのページが1つだけ登録済みになっているはずである.これは,HomePage
クラスのインスタンスに対応している.このページの編集画面を開いてみると,CONTENT,PROMOTE,SETTINGSという3つのタブがあり,各タブからそれぞれいくつかの情報を追加できるようになっている.これらの情報は,Page
クラスが保持できる情報の例である.
自作のページを定義していく際には,Page
クラスを継承することによって,Page
クラスにもともと用意されている以外の情報(フィールドやメソッド)を追加していくことになる.この際に検討すべきことは次の2つである.
- そのページのために編集者が管理サイトから投入すべき情報はなにか?
- そのページを表示する(レンダリングする)ためにテンプレートに渡すべき情報はなにか?
これらの項目への答えは必ずしも一致しない.後者の項目の一部は,編集者が投入しなくとも,別の方法で自動的に取得できる可能性があるからである.ここではまずは,前者の項目に対応する情報をフィールドとしてPage
クラスに追加した上で,管理サイトからそれらを投入できるように編集画面を拡張していこう.
自作ページの定義は,前回追加したcmsアプリケーションの中のmodelモジュール,すなわちcms/models.pyの中に記述していく.ここでは,下のようにTopPage
クラスを定義した.
from django.db import models
from wagtail.core.models import Page
from wagtail.core.fields import RichTextField
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.images.edit_handlers import ImageChooserPanel
class TopPage(Page):
cover_image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
intro = models.CharField(max_length=255)
main_body = RichTextField(blank=True)
side_image = models.ForeignKey(
'wagtailimages.Image',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
side_title = models.CharField(blank=True, max_length=255)
side_body = RichTextField(blank=True)
content_panels = Page.content_panels + [
ImageChooserPanel('cover_image'),
FieldPanel('intro'),
FieldPanel('main_body', classname="full"),
ImageChooserPanel('side_image'),
FieldPanel('side_title'),
FieldPanel('side_body', classname="full"),
]
intro
(ページの紹介文の情報),side_title
(サイドバーのタイトル)の2つがDjangoのmodels.CharField
として追加されていることがわかる.また,Wagtail独自のフィールドであるRichTextFIeld
を用いて,main_body
(ページの本文)とside_body
(サイドバーの本文)の2つが追加されている.RichTextFIeld
は,リッチテキスト形式で編集画面から投入した文書を,適切なhtmlタグ付きでテンプレートに渡すためのものであり,CMSには欠かせないフィールドであると言える.
また,cover_image
やside_image
を見ると,画像の情報を追加する方法もわかるだろう.具体的には,Djangoのmodels.ForeignKey
を利用してwagtailimages.Image
クラスと紐付けすればよい.
最後にcontent_panels
のリストに,新たに追加したフィールドに関するパネルの情報が追加されていることも見て取れる.これは,管理サイトのページ編集画面のCONTENTタブから,それらのフィールドに関する情報を投入するための入力フォームの形式を指定するものである.画像についてはImageChooserPanel
,それ以外にはFieldPanel
が指定されている(ひとまず,画像以外はFieldPanel
を指定しておけば,フィールドのタイプに応じて適切な入力フォームが用意されると考えておけばいいだろう).
ページの編集
続いて,上で定義したページを管理サイトのページ編集画面で編集してみよう.そうすることで,content_panels
に追加した指定が編集画面にどのように反映されたのかもわかる(例えば,classname="full"
の指定を外すと編集画面がどう変わるかなどを確めてみるといいだろう).
具体的には,ページ一覧から先ほどの"Welcome to your new Wagtail site!"を選択し,EDITではなく,ADD CHILD PAGEをクリックし,TopPage
を選択すれば,新しいTopPage
インスタンスの編集画面が開く.編集自体は直感的に行えるので説明は不要と思う.自由に情報を追加してサンプルとなるインスタンスを作成してみよう.
なお,PROMOTEタブのSlugには自動生成された文字列が入るが,これを変更すれば,このページが公開される際のURL(の末尾)を変更することができる.
ページの表示
以上で,自作のTopPage
クラスを定義し,そのインスタンスを生成するところまで進んだ.次に,これをブラウザで表示させてみることにしよう.ページのURLは上のSlagの文字列に基づいて決まる.
通常のDjangoアプリケーションの場合,所定のアドレスにアクセスするとurls.pyでそれに紐付けられたview関数が呼ばれる.そして,view関数が適切なcontextを生成しそれをテンプレートに組み込むことによってページがレンダリングされるという流れが一般的である.したがって,ページを表示できるようにするためには,urls.pyにルーティングの指定を追加し,view関数とテンプレートを用意する必要があった.
これに対して,Wagtailでは,ルーティングは自動的に行われるためurls.pyの編集は不要であり,標準的な使い方では,view関数のようなものも用意する必要はない.テンプレートだけ準備しておけば,自動的にそれに従ってインスタンスの情報を元にページのレンダリングが行われる仕組みになっている.
ということで,まずはテンプレートを用意しよう.cms/models.pyに定義されたTopPage
クラスのページが利用するテンプレートは,デフォルトではtemplates/cms/top_page.htmlという名称になる(クラス名称のパスカルケースがテンプレートではスネークケースになる点に注意しよう).通常はそのままデフォルトの名称を利用すればいいだろう.
templates/cms/top_page.htmlをゼロから作成するのは大変なので,Wagtailがデフォルトで用意してくれているtemplates/base.htmlから継承していくことにする.templates/base.htmlには,スタイルファイルを追加するためのextra_css,コンテンツを追加するためのcontent,javaScriptを追加するためのexstra_jsというブロックが含まれているので,それらのブロックを利用する.
テンプレートの継承関係の見通しをよくするために,まずtemplates/cms/base.htmlを次のように作成した.
{% extends 'base.html' %}
{% load static %}
{% block extra_css %}
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
crossorigin="anonymous"
/>
<link rel="stylesheet" type="text/css" href="{% static 'css/cms/base.css' %}">
{% endblock %}
{% block content %}
<div id="nav_bar">{% block nav_bar %}{% endblock %}</div>
<div id="header">{% block header %}{% endblock %}</div>
<div id="main">{% block main %}{% endblock %}</div>
<div id="footer">{% block footer %}{% endblock %}</div>
{% endblock %}
{% block extra_js %}
<script src="https://unpkg.com/react@17/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js" crossorigin></script>
<script
src="https://unpkg.com/react-bootstrap@next/dist/react-bootstrap.min.js"
crossorigin></script>
{% endblock %}
extra_cssのブロックでは,Bootstarapのためのcssと,自作のcss/cms/base.cssを読み込んでいる.contentブロックでは,<body>
タグの中身をさらにnav_bar,header,main,footerというidの4つの<div>
に細分している.また,extra_jsのブロックでは,ReactとReact Bootstrapのためのスクリプトを読み込んでいる(React Bootstrapを使うのは,他の箇所でReactを利用したいからである.単に,Bootstrapを使うだけなら,普通にjQueryを取り込めばよいだろう).
なお,自作のcssの記述は最小限に抑えた.css/cms/base.cssの中身は下記の通りである.
body {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
padding: 50px 0 50px 0;
}
.jumbotron {
height: 400px;
color: #ffffff;
background-size: cover;
background-position: center center;
background-color: rgba(0, 0, 0, 0.5);
background-blend-mode: multiply;
}
#main > .container {
max-width: 992px;
}
#footer {
font-size: 90%;
}
/* これ以下の記述は,埋込みメディアをレスポンシブルにするための指定 */
.rich-text img {
max-width: 100%;
height: auto;
}
.responsive-object {
position: relative;
}
.responsive-object iframe,
.responsive-object object,
.responsive-object embed {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
後半の記述は画像やその他の埋込みメディアをレスポンシブルにするための指定である(詳しくはここを参照).あわせて,config/settings/base.pyに以下の記述を追加しておく.
WAGTAILEMBEDS_RESPONSIVE_HTML = True
続いて,これらを元に,templates/cms/top_page.htmlを下のように作成した.
{% extends 'cms/base.html' %}
{% load static wagtailcore_tags wagtailimages_tags %}
{% block nav_bar %}
<nav class="navbar fixed-top navbar-dark bg-dark mb-0">
<span class="navbar-text mx-auto">
Say something at the begining.
</span>
</nav>
{% endblock %}
{% block header %}
{% image page.cover_image fill-1000x400 as my_image %}
<div class="jumbotron jumbotron-fluid" style="background-image: url('{{ my_image.url }}');">
<div class="container">
<h1 class="display-4">{{ page.title }}</h1>
<p class="lead">{{ page.intro }}</p>
</div>
</div>
{% endblock %}
{% block main %}
<div class="container">
<div class="row">
<div class="col-md-8">
{% block main_body %}
<div class="rich-text my-5">
{{ page.main_body|richtext }}
</div>
{% endblock %}
</div>
<div class="col-md-4">
{% block side_bar %}
<div class="card my-4">
<h4 class="card-header">{{ page.side_title}}</h4>
<div class="card-body">
{% image page.side_image fill-200x200 class="img-fluid rounded-circle d-block mx-auto" alt="" %}
</div>
<div class="card-body">
<div class="rich-text">
{{ page.side_body|richtext }}
</div>
</div>
</div>
{% endblock %}
</div>
</div>
</div>
{% endblock %}
{% block footer %}
<nav class="navbar navbar-dark bg-dark fixed-bottom">
<span class="navbar-text mx-auto py-0">
Say something at the end.
</span>
</nav>
{% endblock %}
{% block extra_js %}
{{ block.super }}
{% endblock %}
wagtailcore_tagsとwagtailimages_tagsをloadすることで,Wagtailが用意しているテンプレートタグが使えるようになる(上の例では,imageタグやrichtextフィルタ).また,ページインスタンスのフィールド情報にはpage.フィールド名
でアクセスできていることがわかる.RichTextフィールドに格納された文書ではhtmlタグがエスケープされているので,richtextフィルタを通して元に戻している.また,その部分を<div class="rich-text">
で囲むことによって,css/cms/base.cssに記述した下記の指定が効くようにしてある(詳しくはここを参照).
.rich-text img {
max-width: 100%;
height: auto;
}
ここでは,imageタグを用いた画像の扱い方には立ち入らない(が,詳しくはここを参照してほしい).nav_barブロックとfooterブロックはダミーである.headerブロックには,cover_imageで指定した画像を背景にしたジャンボトロンを表示させるようにした.サイドバーにはBootstrapのcardを利用している.
おわりに
今回は,Wagtailでのページの定義,編集,表示までの流れをざっと紹介した.素のDjangoで同様のページを作成するよりはかなり簡単だと思う.また,個人的には,管理サイトの使い勝手も気に入っている.見栄えの調整は少し面倒かもしれないが,公開されているBootstrapのテンプレートを利用する手もある(その方法はこのチュートリアルが詳しい).
次回以降は,少しずつ細かな話題に触れていきたいと思う.