Setting Up Pelican Like a Developer (Not a Beginner) – Static, But Smarter
Most Pelican tutorials teach the same sequence: run pelican-quickstart, tweak a few settings, and start writing.
It works — until it doesn’t.
The failure mode is predictable: as soon as the project outgrows “a few posts”, configuration spreads, Python environments drift, plugins are added opportunistically, and the folder becomes a grab-bag of generated output and source files. The site still builds… right up until the day it doesn’t, and you can’t explain why.
In this article, we’re not “setting up Pelican”. We’re building a reproducible content system:
deterministic environment → clean project scaffold → split configuration → disciplined dependencies → automation from day one → a first-run page
If you’re a beginner: you’ll get a path that works now and keeps working later. If you’re a power user: you’ll get a structure you can version, automate, deploy, and extend without regret.
1. Introduction – Why “Beginner” Setups Break
Beginner setups break for one reason: they optimize for getting a page on screen, not for building a maintainable pipeline.
A production-grade static site isn’t “a folder of Markdown” — it’s a system with:
• a pinned interpreter and reproducible dependencies
• a clean separation between source and build output
• configuration that behaves differently in dev vs production
• automation that makes the “right thing” the easiest thing
Let’s build that foundation first.
2. Environment Isolation: Controlled Python, Not Global Chaos
The first rule of a maintainable Pelican setup is simple: your Python environment must be predictable.
We’ll do this in three layers:
• pin the Python interpreter (pyenv)
• isolate dependencies (venv)
• separate “what we want” from “what gets installed” (pip-tools)
2.1 Pin the interpreter with pyenv
If you already manage Python another way (asdf, mise, containers), keep your tool — the goal is still the same: pin the interpreter.
Here’s the pyenv approach:
pyenv install 3.12.2
pyenv local 3.12.2
That creates .python-version. Anyone cloning your repo gets the exact same Python version.
2.2 Create a local virtual environment
python -m venv .venv
source .venv/bin/activate
At this point: no global packages, no “works on my laptop” surprises.
2.3 Use pip-tools for deterministic installs
A common trap is freezing everything too early. Instead, keep two files:
• requirements.in = your direct dependencies (intent)
• requirements.txt = fully pinned dependency graph (resolution)
pip install pip-tools
# Define only direct dependencies
echo "pelican\nmarkdown" > requirements.in
# Compile a fully pinned lockfile
pip-compile requirements.in
# Install exactly what was compiled
pip-sync requirements.txt
Key idea: You’re not setting up “a blog”. You’re defining a build environment.
From now on, your environment is deterministic: Python version locked, dependencies reproducible, no global pollution.
3. The Professional Project Scaffold
The default Pelican layout is minimal. Minimal is fine for a weekend experiment. For a real site, you want a directory structure that makes it obvious what is:
• source
• output
• UI/theme
• extensions
• automation
project/
│
├── content/ # source: articles, pages, images, extra files
│ ├── articles/
│ ├── pages/
│ ├── images/
│ └── extra/
├── output/ # generated site (never hand-edit)
├── theme/ # templates + CSS/JS
├── plugins/ # optional, controlled extensions
├── tasks/ # build/deploy scripts (later)
├── tools/ # custom generators (later)
├── pelicanconf.py # dev config
├── publishconf.py # production overrides
├── requirements.in # intent
├── requirements.txt # lockfile
└── Makefile # automation
This separation is what turns Pelican into a content pipeline, not just a static site generator.
3.1 Bootstrap the structure (one command)
You can create everything manually, but a bootstrap script gives you repeatability. Keep it simple, and don’t be afraid to parameterize it.
#!/usr/bin/env bash
set -euo pipefail
PROJECT_NAME="pelican-project"
PYTHON_VERSION="3.12.2"
echo "[+] Creating project structure..."
mkdir -p "$PROJECT_NAME"/{content/{articles,pages,images,extra},output,theme,plugins,tasks,tools}
touch "$PROJECT_NAME"/pelicanconf.py
touch "$PROJECT_NAME"/publishconf.py
touch "$PROJECT_NAME"/Makefile
touch "$PROJECT_NAME"/requirements.in
touch "$PROJECT_NAME"/content/articles/.keep
touch "$PROJECT_NAME"/content/pages/.keep
echo "[+] Initializing Python environment..."
cd "$PROJECT_NAME"
pyenv local "$PYTHON_VERSION"
python -m venv .venv
source .venv/bin/activate
pip install pip-tools
echo -e "pelican\nmarkdown" > requirements.in
pip-compile requirements.in
pip-sync requirements.txt
echo "[✓] Project ready."
Now your Pelican project can be created in seconds — consistently, every time.
4. Configuration Strategy – Split Dev vs Production (and keep secrets out of git)
Treat configuration like code: explicit, modular, reviewable. Pelican gives you two natural layers:
• pelicanconf.py → development defaults
• publishconf.py → production overrides
4.1 A clean development config
Keep the baseline boring and stable. This article’s goal is to get structure correct, not to introduce features.
# pelicanconf.py
PATH = "content"
TIMEZONE = "UTC"
DEFAULT_LANG = "en"
THEME = "theme"
STATIC_PATHS = ["images", "extra"]
PAGE_PATHS = ["pages"]
# Optional hardening for dev feeds
FEED_ALL_ATOM = None
CATEGORY_FEED_ATOM = None
4.2 Minimal production overrides
# publishconf.py
from pelicanconf import *
SITEURL = "https://example.com"
DELETE_OUTPUT_DIRECTORY = True
4.3 Optional: use a .env file (only if you need it)
If you want environment-specific values (different SITEURL, feature flags, etc.), you can use a .env.
Just remember: a .env is a convenience, not a requirement.
SITEURL=http://localhost:8002
If you do this, add python-dotenv to your dependencies and load it explicitly:
echo "python-dotenv" >> requirements.in
pip-compile requirements.in
pip-sync requirements.txt
# pelicanconf.py
from dotenv import load_dotenv
load_dotenv()
And crucially: don’t commit secrets. Add .env to .gitignore.
5. Plugins: Intentional Selection (Not “plugin-first”)
Pelican plugins are powerful — and easy to overuse. Every plugin becomes part of your build pipeline. So we follow a strict rule: plugins must earn their place.
In the early stage, you should be able to build a working site with zero plugins. That forces you to understand:
• where data comes from (content + metadata)
• how it flows through templates
• how URLs and structure are generated
When you do add plugins, add them like normal dependencies: pinned, documented, and reproducible.
# example: sitemap (only when you’re ready)
pip install pelican-sitemap
Then enable them explicitly (and only where needed):
PLUGIN_PATHS = ["plugins"]
PLUGINS = ["sitemap"]
If you need custom behavior, prefer implementing it in tools/ or your theme once you fully understand the pipeline.
6. Version Control Hygiene: What to Commit vs What to Regenerate
A clean Pelican project is defined by what you don’t keep. Generated output, caches, and virtualenvs should never mix with source content.
Here’s a minimal .gitignore that keeps your repo clean:
# Python
.venv/
__pycache__/
*.py[cod]
# Pelican
output/
.pelican-cache/
# Environment
.env
# OS / editor
.DS_Store
.vscode/
As a rule of thumb:
• Commit: content/, theme/, configs, requirements.in, requirements.txt, Makefile
• Never commit: output/, .venv/, caches, generated artifacts
Golden rule: Only source files matter. Everything else can be regenerated.
Optional but useful: add .editorconfig to prevent formatting drift across machines.
7. The Build System: A Makefile from Day One
Before writing content, define how the system runs. Automation isn’t “extra”; it’s how you make good practices frictionless.
.PHONY: dev build publish serve clean
PORT ?= 8002
# Live-reload development server
serve:
pelican -r -l -p $(PORT)
# Deterministic build (uses pelicanconf.py)
build:
pelican content -s pelicanconf.py -o output
# Production build (uses publishconf.py)
publish:
pelican content -s publishconf.py -o output
# Serve the generated output directory
serve:
cd output && python -m http.server $(PORT)
clean:
rm -rf output __pycache__ .pelican-cache
This gives you a stable interface:
• make serve → live reload
• make build → local build
• make publish → production output
• make clean → wipe generated artifacts
8. First Light: “Hello World” Page
Before you write “real” content, prove the pipeline works end-to-end. We’ll create a page (not an article) because it’s the fastest way to validate output without date ordering or taxonomies.
mkdir -p content/pages
cat > content/pages/home.md << 'EOF'
Title: Hello, World
Slug: home
# Hello, World
This page was generated by Pelican.
EOF
Start the development server:
make serve
Open http://localhost:8002 in your browser.
If you see “Hello, World”, your environment, config, and directory layout are all aligned.
Congratulations — the machine works. Now we can safely add complexity without ever breaking the “works out of the box” contract.
9. What We Just Built (and Why It Matters)
You now have a build pipeline, not just a blog folder. From here onward, you’ll repeatedly apply a simple three-layer pattern:
• Config: tell Pelican what to load and how to structure output
• Theme: render that data intentionally (templates, includes, layout)
• Metadata conventions: define the shape of the content so it stays queryable at build time
This pattern scales: it keeps the project understandable at 5 posts and at 500.
Next: in the next article we’ll design a clean content architecture — file naming, front matter vocabulary, and a directory strategy that keeps your site navigable and extensible as it grows.