Pelican
Python製の成熟したSSG。12k超のGitHubスターを持ち、Pythonエコシステムでのブログ構築に最適。
トレンド・動向
Pythonコミュニティでのブログ構築標準として安定した地位。データサイエンス系ブログで人気。
# SSG
Pelican
## 概要
Pelicanは「Python製の成熟した静的サイトジェネレータ」として開発された、Pythonエコシステムで最も信頼性の高いSSGの一つです。MarkdownとreStructuredTextの両方をサポートし、Jinja2テンプレートエンジンによる柔軟なカスタマイズが可能。12k超のGitHubスターを誇り、Pythonコミュニティでのブログ構築標準として安定した地位を確立。豊富なプラグインエコシステムと成熟したドキュメント、長期サポートにより、データサイエンティストや研究者の技術ブログから企業のドキュメントサイトまで幅広く採用されています。
## 詳細
Pelican 2025年版はPython静的サイト生成の決定版として10年以上の開発実績を誇ります。シンプルな設定ファイル(pelicanconf.py)によるPythonicな設定管理と、強力なJinja2テンプレートシステムによる高度なカスタマイズが特徴。MarkdownとreStructuredTextの両フォーマット対応により、技術文書作成で好まれる記法を選択可能。タグ・カテゴリによる分類機能、RSSフィード生成、サイトマップ自動生成、多言語サイト対応など、ブログ運営に必要な機能を標準搭載。プラグインシステムにより検索機能、コメントシステム、画像最適化など拡張機能も豊富です。
### 主な特徴
- **Pythonベース**: Pythonの豊富なエコシステムと統合可能
- **柔軟なテンプレート**: Jinja2による高度なカスタマイズ
- **マルチフォーマット対応**: Markdown、reStructuredText、HTML対応
- **豊富なメタデータ**: 記事の詳細な分類・管理機能
- **テーマシステム**: 豊富なテーマとカスタマイズ性
- **プラグイン拡張**: 活発なプラグインエコシステム
## メリット・デメリット
### メリット
- Pythonエコシステムとの完全統合による高い拡張性
- 成熟したドキュメントと豊富なチュートリアル・学習リソース
- reStructuredTextサポートによる高度な技術文書作成
- Jinja2テンプレートによる柔軟なサイト設計とカスタマイズ
- 豊富なプラグインによるSEO最適化と機能拡張
- Python開発者にとって学習コストが低く直感的な操作
### デメリット
- Python環境の構築が必要で初心者には環境設定が複雑
- 大規模サイトでのビルド時間が他のSSGより長い傾向
- 他の言語エコシステムとの統合に制約がある
- フロントエンドの最新トレンドへの対応が限定的
- モダンなJavaScriptフレームワークとの連携に制約
- Node.js系SSGと比較してビルド速度で劣る場合がある
## 参考ページ
- [Pelican 公式サイト](https://getpelican.com/)
- [Pelican ドキュメント](https://docs.getpelican.com/)
- [Pelican GitHub リポジトリ](https://github.com/getpelican/pelican)
## 書き方の例
### インストールと基本設定
```bash
# Pelicanのインストール
pip install pelican[markdown]
# Markdownサポート付きインストール(推奨)
pip install pelican[markdown] typogrify
# プロジェクトの初期化
mkdir my-blog
cd my-blog
pelican-quickstart
# 基本的なディレクトリ構造の確認
ls -la
# content/ # 記事ファイル
# output/ # 生成されたサイト
# pelicanconf.py # 設定ファイル
# publishconf.py # 本番用設定
```
### プロジェクト構造とページ作成
```bash
# 記事ディレクトリの作成
mkdir content/articles
mkdir content/pages
mkdir content/images
# 最初の記事作成
cat > content/articles/first-post.md << 'EOF'
Title: 私の最初の記事
Date: 2025-01-01 10:00
Category: Tech
Tags: pelican, python, blog
Slug: first-post
Author: Your Name
Summary: Pelicanで作成する最初の記事です。
# はじめに
Pelicanを使って静的サイトを作成しました。
## 特徴
- Python製のSSG
- Markdownサポート
- 豊富なテーマ
[続きを読む...](/articles/second-post.html)
EOF
# 固定ページの作成
cat > content/pages/about.md << 'EOF'
Title: About
Date: 2025-01-01
Slug: about
このサイトについての説明ページです。
## プロフィール
技術ブログを運営しています。
## 連絡先
- Email: [email protected]
- Twitter: @example
EOF
# サイトの生成とプレビュー
pelican content
pelican --listen # http://localhost:8000でプレビュー
```
### 設定カスタマイズ(pelicanconf.py)
```python
#!/usr/bin/env python
# -*- coding: utf-8 -*- #
AUTHOR = 'Your Name'
SITENAME = 'My Tech Blog'
SITEURL = ''
PATH = 'content'
TIMEZONE = 'Asia/Tokyo'
DEFAULT_LANG = 'ja'
# フィード設定
FEED_DOMAIN = SITEURL
FEED_ALL_ATOM = 'feeds/all.atom.xml'
CATEGORY_FEED_ATOM = 'feeds/category_%s.atom.xml'
TRANSLATION_FEED_ATOM = None
AUTHOR_FEED_ATOM = None
AUTHOR_FEED_RSS = None
# ページネーション設定
DEFAULT_PAGINATION = 10
PAGINATION_PATTERNS = (
(1, '{base_name}/', '{base_name}/index.html'),
(2, '{base_name}/page/{number}/', '{base_name}/page/{number}/index.html'),
)
# テーマとスタイル設定
THEME = 'themes/elegant' # カスタムテーマ
THEME_STATIC_DIR = 'theme'
CSS_FILE = 'main.css'
# URLとパス設定
ARTICLE_URL = 'articles/{slug}.html'
ARTICLE_SAVE_AS = 'articles/{slug}.html'
PAGE_URL = '{slug}.html'
PAGE_SAVE_AS = '{slug}.html'
CATEGORY_URL = 'category/{slug}.html'
CATEGORY_SAVE_AS = 'category/{slug}.html'
TAG_URL = 'tag/{slug}.html'
TAG_SAVE_AS = 'tag/{slug}.html'
# 静的ファイル設定
STATIC_PATHS = ['images', 'extra/CNAME', 'extra/robots.txt']
EXTRA_PATH_METADATA = {
'extra/CNAME': {'path': 'CNAME'},
'extra/robots.txt': {'path': 'robots.txt'},
}
# ソーシャルリンク
SOCIAL = (('GitHub', 'https://github.com/username'),
('Twitter', 'https://twitter.com/username'),
('LinkedIn', 'https://linkedin.com/in/username'))
# メニュー設定
MENUITEMS = (
('Archives', '/archives.html'),
('Categories', '/categories.html'),
('Tags', '/tags.html'),
)
# SEO設定
SITEMAP = {
'format': 'xml',
'priorities': {
'articles': 0.5,
'indexes': 0.5,
'pages': 0.5
},
'changefreqs': {
'articles': 'monthly',
'indexes': 'daily',
'pages': 'monthly'
}
}
# 記事メタデータのデフォルト値
DEFAULT_METADATA = {
'status': 'draft',
}
# Markdown設定
MARKDOWN = {
'extension_configs': {
'markdown.extensions.codehilite': {'css_class': 'highlight'},
'markdown.extensions.extra': {},
'markdown.extensions.meta': {},
'markdown.extensions.toc': {},
},
'output_format': 'html5',
}
```
### プラグインシステムの活用
```python
# pelicanconf.py - プラグイン設定
PLUGIN_PATHS = ['plugins']
PLUGINS = [
'sitemap', # サイトマップ生成
'tag_cloud', # タグクラウド
'related_posts', # 関連記事
'tipue_search', # 検索機能
'neighbors', # 前後記事ナビ
'representative_image', # 代表画像
'liquid_tags.img', # 画像タグ
'render_math', # 数式レンダリング
]
# 検索機能の設定
DIRECT_TEMPLATES = ['index', 'tags', 'categories', 'archives', 'search']
# タグクラウド設定
TAG_CLOUD_STEPS = 4
TAG_CLOUD_MAX_ITEMS = 100
TAG_CLOUD_SORTING = 'random'
# 関連記事設定
RELATED_POSTS_MAX = 5
RELATED_POSTS_SKIP_SAME_CATEGORY = True
# 数式設定
MATH_JAX = {
'align': 'left',
'indent': '2em',
'show_menu': 'true',
'process_escapes': 'true',
'latex_preview': 'tex2jax_preview',
'color': 'black',
'linebreaks': 'false',
}
# 画像処理設定
IMAGE_PROCESS = {
'article-image': ["scale_in 300 300 True"],
'thumb': ["crop 0 0 50% 50%", "scale_out 150 150 True", "crop 0 0 150 150"],
}
# カスタムプラグイン関数
def my_custom_filter(text):
"""カスタムJinja2フィルター"""
return text.replace('old', 'new')
JINJA_FILTERS = {
'my_filter': my_custom_filter
}
```
### テーマとテンプレートカスタマイズ
```html
<!-- themes/custom/templates/base.html -->
<!DOCTYPE html>
<html lang="{{ DEFAULT_LANG }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ SITENAME }}{% endblock title %}</title>
<!-- SEOメタタグ -->
<meta name="description" content="{% block description %}{{ SITEDESCRIPTION }}{% endblock %}">
<meta name="keywords" content="{% block keywords %}{{ SITEKEYWORDS }}{% endblock %}">
<meta name="author" content="{{ AUTHOR }}">
<!-- OGP設定 -->
<meta property="og:title" content="{% block og_title %}{{ SITENAME }}{% endblock %}">
<meta property="og:description" content="{% block og_description %}{{ SITEDESCRIPTION }}{% endblock %}">
<meta property="og:type" content="{% block og_type %}website{% endblock %}">
<meta property="og:url" content="{{ SITEURL }}{% block og_url %}{% endblock %}">
<meta property="og:image" content="{% block og_image %}{{ SITEURL }}/images/default-og.png{% endblock %}">
<!-- スタイルシート -->
<link rel="stylesheet" href="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/css/{{ CSS_FILE }}">
<link rel="stylesheet" href="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/css/pygments.css">
<!-- ファビコン -->
<link rel="icon" type="image/x-icon" href="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/images/favicon.ico">
<!-- フィード -->
{% if FEED_ALL_ATOM %}
<link href="{{ SITEURL }}/{{ FEED_ALL_ATOM }}" type="application/atom+xml" rel="alternate" title="{{ SITENAME }} Atom Feed" />
{% endif %}
</head>
<body>
<header class="site-header">
<div class="container">
<nav class="site-nav">
<a href="{{ SITEURL }}/" class="site-title">{{ SITENAME }}</a>
<ul class="nav-menu">
{% for title, link in MENUITEMS %}
<li><a href="{{ link }}">{{ title }}</a></li>
{% endfor %}
</ul>
</nav>
</div>
</header>
<main class="site-main">
<div class="container">
{% block content %}{% endblock %}
</div>
</main>
<footer class="site-footer">
<div class="container">
<p>© {{ date.strftime('%Y') }} {{ AUTHOR }}. All rights reserved.</p>
{% if SOCIAL %}
<div class="social-links">
{% for name, link in SOCIAL %}
<a href="{{ link }}" target="_blank">{{ name }}</a>
{% endfor %}
</div>
{% endif %}
</div>
</footer>
<!-- JavaScript -->
<script src="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/js/main.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
<!-- themes/custom/templates/article.html -->
{% extends "base.html" %}
{% block title %}{{ article.title }} - {{ super() }}{% endblock %}
{% block description %}{{ article.summary|striptags|truncate(160) }}{% endblock %}
{% block og_title %}{{ article.title }}{% endblock %}
{% block og_description %}{{ article.summary|striptags|truncate(160) }}{% endblock %}
{% block og_type %}article{% endblock %}
{% block og_url %}{{ SITEURL }}/{{ article.url }}{% endblock %}
{% block content %}
<article class="post">
<header class="post-header">
<h1 class="post-title">{{ article.title }}</h1>
<div class="post-meta">
<time datetime="{{ article.date.isoformat() }}">{{ article.locale_date }}</time>
{% if article.category %}
<span class="post-category">
<a href="{{ SITEURL }}/{{ article.category.url }}">{{ article.category }}</a>
</span>
{% endif %}
{% if article.tags %}
<div class="post-tags">
{% for tag in article.tags %}
<a href="{{ SITEURL }}/{{ tag.url }}" class="tag">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
</div>
</header>
<div class="post-content">
{{ article.content }}
</div>
<footer class="post-footer">
{% if article.modified %}
<p class="post-modified">
Last updated: <time datetime="{{ article.modified.isoformat() }}">{{ article.modified.strftime('%Y-%m-%d') }}</time>
</p>
{% endif %}
<!-- 関連記事 -->
{% if article.related_posts %}
<section class="related-posts">
<h3>関連記事</h3>
<ul>
{% for related in article.related_posts %}
<li><a href="{{ SITEURL }}/{{ related.url }}">{{ related.title }}</a></li>
{% endfor %}
</ul>
</section>
{% endif %}
</footer>
</article>
<!-- 前後記事ナビ -->
{% if article.prev_article or article.next_article %}
<nav class="post-nav">
{% if article.prev_article %}
<a href="{{ SITEURL }}/{{ article.prev_article.url }}" class="post-nav-prev">
← {{ article.prev_article.title }}
</a>
{% endif %}
{% if article.next_article %}
<a href="{{ SITEURL }}/{{ article.next_article.url }}" class="post-nav-next">
{{ article.next_article.title }} →
</a>
{% endif %}
</nav>
{% endif %}
{% endblock %}
```
### 本番環境でのデプロイ設定
```python
# publishconf.py - 本番用設定
import os
import sys
sys.path.append(os.curdir)
from pelicanconf import *
# 本番用URL設定
SITEURL = 'https://yourdomain.com'
RELATIVE_URLS = False
# フィード設定
FEED_DOMAIN = SITEURL
FEED_ALL_ATOM = 'feeds/all.atom.xml'
CATEGORY_FEED_ATOM = 'feeds/category_%s.atom.xml'
# 削除する開発用ファイル
DELETE_OUTPUT_DIRECTORY = True
# Google Analytics
GOOGLE_ANALYTICS = 'UA-XXXXXXXX-1'
# Disqus コメント
DISQUS_SITENAME = 'your-disqus-sitename'
# 本番用プラグイン追加
PLUGINS.extend([
'optimize_images',
'gzip_cache',
'webassets',
])
# アセット最適化
WEBASSETS = True
WEBASSETS_CONFIG = [
('SASS_LOAD_PATHS', ['themes/custom/sass']),
('BABEL_PRESETS', 'es2015'),
]
# 画像最適化
OPTIMIZE_IMAGES = True
# SSL設定
FORCE_SSL = True
```
### 自動化とGitHub Actions連携
```yaml
# .github/workflows/deploy.yml
name: Deploy Pelican Site
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pelican[markdown] typogrify
pip install -r requirements.txt
- name: Build site
run: |
pelican content -s publishconf.py
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./output
cname: yourdomain.com
# Makefile - 開発用タスク
PY?=python3
PELICAN?=pelican
PELICANOPTS=
BASEDIR=$(CURDIR)
INPUTDIR=$(BASEDIR)/content
OUTPUTDIR=$(BASEDIR)/output
CONFFILE=$(BASEDIR)/pelicanconf.py
PUBLISHCONF=$(BASEDIR)/publishconf.py
DEBUG ?= 0
ifeq ($(DEBUG), 1)
PELICANOPTS += -D
endif
RELATIVE ?= 0
ifeq ($(RELATIVE), 1)
PELICANOPTS += --relative-urls
endif
help:
@echo 'Makefile for Pelican Website '
@echo ' '
@echo 'Usage: '
@echo ' make html (re)generate the web site '
@echo ' make clean remove the generated files '
@echo ' make regenerate regenerate files upon modification'
@echo ' make publish generate using production settings'
@echo ' make serve [PORT=8000] serve site at http://localhost:8000'
@echo ' make devserver [PORT=8000] start/restart develop_server.sh '
html:
$(PELICAN) $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS)
clean:
[ ! -d $(OUTPUTDIR) ] || rm -rf $(OUTPUTDIR)
regenerate:
$(PELICAN) -r $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS)
serve:
ifdef PORT
$(PELICAN) -l $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS) -p $(PORT)
else
$(PELICAN) -l $(INPUTDIR) -o $(OUTPUTDIR) -s $(CONFFILE) $(PELICANOPTS)
endif
publish:
$(PELICAN) $(INPUTDIR) -o $(OUTPUTDIR) -s $(PUBLISHCONF) $(PELICANOPTS)
.PHONY: html help clean regenerate serve publish
```