Sculpinは静的サイトジェネレータ(Static Site Generator, SSG)と呼ばれるもので、PHPで動きます。Markdown、Twigを使って静的ページを作成することができます。
日本語の記事があまり無いようだったので、紹介を兼ねて一通りのページを作成してみました。
またテンプレートエンジンのTwigの動きもなんとなくわかるように、{% block xxx %}
や{{ include() }}
を使った内容にしてあります。
普段Symfonyを使っている自分には馴染みやすく、使いやすいものでした。
準備
インストールにはcomposerを使用するので、composerが使える環境が必要です。
$ php -v
PHP 7.3.4 (cli) (built: Apr 2 2019 13:48:50) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.4, Copyright (c) 1998-2018 Zend Technologies
$ curl -S https://getcomposer.org/installer | php
インストール
公式で紹介されているものは以下の2つ。
- sculpin/sculpin-blog-skeleton
- sculpin/sculpin
主な違いは sculpin/sculpin-blog-skeleton の場合ブログなどを作るのに便利なwebpackなどが同時にインストールされます。
今回は sculpin/sculpin を使って進めます。
sculpin/sculpin
$ mkdir ~/sculpin && cp ./composer.phar ~/sculpin/composer.phar
$ cd ~/sculpin
$ php composer.phar require sculpin/sculpin
$ php vendor/bin/sculpin init
初期状態の構成は以下のようになっています。
project_root/
|-- app/
| |-- config/
| | |-- sculpin_kernel.yml
| | `-- sculpin_site.yml
| `-- SculpinKernel.php
|-- composer.json
|-- composer.lock
|-- composer.phar
|-- source/
| |-- index.md
| `-- _views/
| `-- default.html
`-- vendor/
|-- bin/
| |-- sculpin
| `-- ...
`-- ...
あとで作るので、 source/*
は削除。
$ rm -r source/*
設定
ref: https://sculpin.io/documentation/
ref: https://sculpin.io/documentation/bundles/SculpinPostsBundle/
初期状態では以下のような設定になっています。
-
project_root/source/_layout/
にページの基本レイアウトファイルを配置。Twig形式 -
project_root/source/_views/
に各レイアウトファイルを配置。Twig形式。layout: xxx
で指定する際はここのレイアウトファイルを指定する -
project_root/source/assets/
にcss、js、画像等のファイルを配置 -
project_root/source/_*
内に記事ファイルを配置。source/_blog
だったり、source/_posts
だったり
project_root/source/_*/
内の各ファイルはTwig形式またはMarkdown形式で記述します。
Markdown形式で記述された場合、本文は page.blocks.content
として扱われます。
app/config/sculpin_kernel.yml
Symfony4.*系と構造を近づけたかったので、以下のような設定にします。
sculpin_twig.extensions
で拡張子を指定する際の注意として、空(''
)を指定しておかないと後述のlayout: xxx
によるレイアウトの指定ができなくなります。
sculpin_twig:
source_view_paths: ['templates'] # sourceからの相対パス。レイアウトファイルはここに配置
extensions: ['', 'twig.html'] # Twigファイルの拡張子を指定
sculpin_markdown:
extensions: ['md'] # Markdownファイルの拡張子を指定
sculpin_site_[ENV]
の形式で設定ファイルをわけることもできます。
これも後述しますが、 php vendor/bin/sculpin generate
でファイルを生成する際に指定したenvで自動的に使い分けられます。
imports:
- sculpin_site.yml
locale: ja
imports:
- sculpin_site.yml
locale: ja
content-type
次にcontent-typeを設定します。
今回はdevelopments
としました。
...
sculpin_content_types:
posts: # sculpin/sculpinに同梱されている
enabled: false # 使用しないcontent-typeは無効化できる
developments:
enabled: true
type: path
path: developments # 記事ファイルを配置する場所。sourceからの相対パス
singular_name: developments
layout: default # デフォルトのレイアウト。拡張子を除いて指定する
permalink: :year/:month/:day/:basename/index.html # 出力時のディレクトリ構成を指定
taxonomies:
- tags
app/config/sculpin_site_dev.yml
ここで設定した値はTwig形式で記述した際に {{ site.[KEY] }}
でページ内から参照できます。
...
title: the DEVELOPMENTS
subtitle: ~
url: http://localhost # ここで指定せず引数で渡すこともできる
app/config/sculpin_services.yml
TwigExtensionを使えるようにします。TwigExtension自体は sculpin/sculpin に含まれているので、設定するだけで使えるようになります。
parameters:
services:
twig.extension.text:
class: Twig_Extensions_Extension_Text
tags:
- { name: twig.extension }
レイアウトの作成
source/templates/base.twig.html
まずは基礎になるレイアウトを作成します。
<!doctype html>
<html lang="{{ site.locale }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
{% block meta %}{% endblock %}
<title>{% if page.title is not empty %}{{ page.title }} | {% endif %}{{ site.title }}{% if site.subtitle is not empty %} - {{ site.subtitle }}{% endif %}</title>
{% block stylesheets %}{% endblock %}
</head>
<body>
{% block header %}{% endblock %}
{% block main %}{% endblock %}
{% block footer %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>
source/templates/default.twig.html
base.twig.html
を引き継いだテンプレートを作成します。
{% extends 'base' %}
{% block stylesheets %}
<style>
:root {
--header-back-color: #fff;
--header-fore-color: #444;
--header-border-color: #ddd;
--header-height: auto;
}
header {
height: auto;
color: var(--header-fore-color);
background: var(--header-back-color);
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
}
header.sticky {
height: var(--header-height);
border-bottom: var(--header-border-color) solid 0.0625rem;
position: sticky;
z-index: 1101;
top: 0;
}
nav > .nav-list {
list-style: none;
margin-bottom: 0;
}
.nav-list > .nav-item {
position: relative;
float: left;
margin-bottom: 0;
}
.nav-item > .nav-link {
font-size: 1.6rem;
font-weight: 600;
letter-spacing: .2rem;
line-height: 6.5rem;
margin-right: 4rem;
text-decoration: none;
text-transform: uppercase;
}
</style>
{% endblock %}
{% block header %}
<header class="sticky">
<h1 style="text-align: center;">{{ site.title }}</h1>
<nav>
<ul class="nav-list">
<li class="nav-item"><a class="nav-link" href="{{ site.url }}">home</a>
<li class="nav-item"><a class="nav-link" href="/archives">archives</a>
<li class="nav-item"><a class="nav-link" href="/tags">tags</a>
<li class="nav-item"><a class="nav-link" href="/about">about</a>
</ul>
</nav>
</header>
{% endblock %}
{% block main %}
<article>
<header>
<h2>{{ page.title }}</h2>
{% if page.date is defined %}
{{ include('_publishedOn.twig.html', {
timestamp: page.date,
}, with_context = false) }}
{% endif %}
<br>
{% if page.tags is not empty %}
{{ include('_tags.twig.html', {
tags: page.tags,
}, with_context = false) }}
{% endif %}
</header>
<main>
{% block content %}{% endblock %}
</main>
</article>
<section class="paginator">
{% if page.previous_developments or page.next_developments is not empty %}
{% if page.next_developments is not empty %}
Next: <a href="{{ page.next_developments.url }}">{{ page.next_developments.title }}</a>
{% endif %}
{% if page.previous_developments is not empty %}
Prev: <a href="{{ page.previous_developments.url }}">{{ page.previous_developments.title }}</a>
{% endif %}
{% endif %}
</section>
{% endblock %}
{% block javascripts %}
<script>
(function() {
const headerHeight = document.querySelector('header').offsetHeight;
document.documentElement.style.setProperty('--header-height', headerHeight + 'px');
}());
</script>
{% endblock %}
<time datetime="{{ timestamp|date("Y-m-d") }}T{{ timestamp|date("H") }}:{{ timestamp|date("i") }}+0900">{{ timestamp|date("F") }} {{ timestamp|date("j") }}, {{ timestamp|date("Y") }}</time>
Tags:
{% for tag in tags %}
<a href="/tags/{{ tag|url_encode(true) }}">{{ tag }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
各ページの作成
一覧ページと個別の投稿を作成。
各ファイルの冒頭でYAML形式のデータを指定すると、 {{ page.[KEY] }}
で参照できます。
source/index.twig.html
最初に表示されるページ。
今回は最新5件の投稿が表示されるページとして作成。
---
title: Home
layout: default # source/developments 配下なら指定はいらない
use: [developments] # sculpin_kernel.ymlのsculpin_content_typesのキー
---
{% block main %}
<main>
<h2>Recent Posts</h2>
{% for post in data.developments|slice(0, 5) %}
<article>
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
{% if post.tags is not empty %}
{{ include('_tags.twig.html', {
tags: post.tags,
}, with_context = false) }}
{% endif %}
<p>{{ post.blocks.content|striptags|truncate(140) }} {# truncate()がTwigExtensionに含まれます #}
</article>
{% endfor %}
</main>
{% endblock %}
source/archives.twig.html
過去の記事を一覧表示するページ。
少し複雑ですが、それぞれの記事の date
を読み込んでリンクの前に表示するようにしてあります。
---
title: Archives
layout: default
generator: pagination
pagination:
provider: data.developments
max_per_page: 30
use: [developments]
---
{% block main %}
<h2>{{ page.title }}</h2>
{% set year = '' %}
{% for post in page.pagination.items %}
<section>
<time datetime="{{ post.date|date("Y-m-d") }}T{{ post.date|date("H") }}:{{ post.date|date("i") }}+0900">
{% set postedYear %}{{ post.date|date("Y") }}{% endset %}
<span{% if year == postedYear %} style="visibility: hidden;"{% endif %}>
{% if year != postedYear %}
{% set month = '' %}
{% set year = postedYear %}
{% endif %}
{{ year }}
</span>
{% set postedMonth %}{{ post.date|date("m") }}{% endset %}
<span{% if month == postedMonth %} style="visibility: hidden;"{% endif %}>
{% if month != postedMonth %}
{% set day = '' %}
{% set month = postedMonth %}
{% endif %}
{{ month }}
</span>
{% set postedDay %}{{ post.date|date("d") }}{% endset %}
<span{% if day == postedDay %} style="visibility: hidden;"{% endif %}>
{% if day != postedDay %}
{% set day = postedDay %}
{% endif %}
{{ day }}
</span>
</time>
<a href="{{ post.url }}">{{ post.title }}</a>
</section>
{% endfor %}
<section class="paginator">
{% if page.pagination.previous_page or page.pagination.next_page is not empty %}
{% if page.pagination.previous_page is not empty is not empty %}
<a href="{{ page.pagination.previous_page.url }}">Newer</a>
{% endif %}
{% if page.pagination.next_page is not empty is not empty %}
<a href="{{ page.pagination.next_page.url }}">Older</a>
{% endif %}
{% endif %}
</section>
{% endblock %}
source/tags.twig.html
タグ一覧のページ。
---
title: Tags
layout: default
generator: pagination
pagination:
provider: data.developments_tags
max_per_page: 30
use: [developments_tags]
---
{% block main %}
<h2>{{ page.title }}</h2>
<ul>
{% for tag, posts in page.pagination.items %}
<li><a href="/tags/{{ tag|url_encode(true) }}">{{ tag }}</a> ({{ max(posts|keys) + 1 }})
{% endfor %}
</ul>
<section class="paginator">
{% if page.pagination.previous_page or page.pagination.next_page is not empty %}
{% if page.pagination.previous_page is not empty %}
<a href="{{ page.pagination.previous_page.url }}">Previous</a>
{% endif %}
{% if page.pagination.next_page is not empty %}
<a href="{{ page.pagination.next_page.url }}">Next</a>
{% endif %}
{% endif %}
</section>
{% endblock %}
source/tags/tag.twig.html
そのタグがつけられている記事の一覧ページ。
source/tags.twig.html
から生成されるページからここに飛んできます。
---
title: Tag
layout: developments
generator: [developments_tag_index, pagination]
pagination:
provider: page.tag_developments
max_per_page: 30
---
{% block main %}
<h2>{{ page.tag }}</h2>
<ul>
{% for post in page.pagination.items %}
<li><a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
</ul>
<section>
{% if page.pagination.previous_page or page.pagination.next_page is not empty %}
{% if page.pagination.previous_page is not empty %}
<a href="{{ page.pagination.previous_page.url }}">Previous</a>
{% endif %}
{% if page.pagination.next_page is not empty %}
<a href="{{ page.pagination.next_page.url }}">Next</a>
{% endif %}
{% endif %}
</section>
{% endblock %}
source/about.md
よくあるアバウトページ。
内容が簡単なものであればMarkdown形式でも書けます。
またTwig形式のファイルと同様、冒頭でYAML形式のデータを指定できます。
---
title: About this site
layout: default
---
generated by [sculpin](https://github.com/sculpin/sculpin)
記事ファイルの作成
一覧ページではない、個別の投稿は sculpin_kernel.yml
の sculpin_content_types.[CONTENT_TYPE].path
で指定したディレクトリ内に配置します。
$ touch source/developments/1st.md && vim $_
---
title: 1st post
date: 2019-05-31 12:34
tags: [initial]
---
hi.
this is my first post.
thanks.
出力
最後に、ページを生成します。
$ php vendor/bin/sculpin cache:clear && \
php vendor/bin/sculpin generate --env=dev --verbose --clean --watch
-
cache:clear
でキャッシュクリア -
--env
で[ENV]
を指定 -
--clean
でoutput_[ENV]
の中身を消去 -
--watch
でファイルに変更があるたび自動で生成
php vendor/bin/sculpin generate --url=[URL]
を指定すると、ファイル生成時に {{ site.url }}
で参照する値を渡すことができます( sculpin_site.yml
で指定がある場合はそちらが優先されます)。
また php vendor/bin/sculpin generate --port=[PORT]
でポートも指定できます。
おまけ
簡易サーバーを使用する
すでに生成されたファイルがあるなら、簡易サーバーを利用できます。
URLは http://localhost
固定(?)のようです。
$ php vendor/bin/sculpin serve --port=8000 --env=dev
テストファイルを作成する
paginationの動きを確認するためのテストファイルを作りたいときは以下のコマンドで作成できます。が、bash以外で動くかはわかりません……。
# source/developments/dummy_xxx.md を100個作る
$ for i in `seq -w 001 100`; do cp source/developments/1st.md source/developments/dummy_$i.md; done
変数表
一部ですが、使用頻度の高そうなものを書いておきます。
Twigファイルからアクセスするときは {{ page.title }}
のように参照します。
- page
- title (そのページで設定していれば)
- date (そのページで設定していれば)
- tag (そのページで設定していれば)
- generator (そのページで設定していれば)
- pagination (そのページで設定していれば)
- provider
- items
- FileSource:FilesystemDataSource:...
- page
- total_pages
- total_items
- previous_page
- next_page
- use (そのページで設定していれば)
- ?
- calculated_date
- dateとおなじ?
- layout
- permalink
- next_[CONTENT_TYPE]
- 該当タイプの次の記事
- title
- url
- previous_[CONTENT_TYPE]
- 該当タイプの前の記事
- title
- url
- url
- relative_pathname
- project_root/sourceからの相対パス
- filename
- blocks
- twig形式で指定した
{% block ... %}
- twig形式で指定した
- [KEY]
- その他yaml形式で埋め込んだ変数
titleなど
- その他yaml形式で埋め込んだ変数
- data
- page.useが設定されていれば使える
- [CONTENT_TYPE]
- FileSource:FilesystemDataSource:...
- page.pagination.itemsとおなじ?