| images | ||
| items | ||
| output | ||
| static | ||
| theme/default | ||
| .gitignore | ||
| config.py | ||
| Dockerfile | ||
| LICENSE | ||
| picopaper.py | ||
| README.md | ||
| requirements.txt | ||
picopaper
A minimal static site generator for blogs built with Python 3 and Jinja2
- Status: beta - expect many changes
- Issue Tracker
- Goals: keeping it simple and easy to understand
- Demo: picopaper.com
Show cases:
Features
Available:
- simple use, easy to understand and modify
- config file for settings
- Themes
- long- and short form content
- pages
- static files
- separate sections (used for categories, tagging, etc)
/section/{tag} - exclusion of sections from main index (drafts or system notes)
- HTML anchors for headers
- list random posts at the bottom
- optional RSS feeds
- automatic EXIF/metadata stripping from images (privacy)
Ideas:
- custom error pages (404, etc)
Not planned:
Usage
Creating a new page or article
Put markdown file into items dir. Important naming convention:
2025-10-03_long_building-a-static-site-generator.md
2025-10-05_short_quick-update_draft.md
Format: YYYY-MM-DD_type_slug[_section].md
2025-10-03- date of the article_long_- type of content:long,short, orpagebuilding-a-static-site-generator- slug/path for the URL_draft(optional) - section tag for categorization
The first # header is the title of the article - no frontmatter needed.
Types of content:
long- only title with link to articles will be displayed in the main indexshort- title and all content will be displayedpage- won't be displayed in the main index at all
Sections
Posts can be tagged with an optional section (e.g., _python, _webdev). Posts with section tags:
- Appear on the main page (unless excluded in config)
- Have their own section page at
/section/{tag}/
Configuration in config.py:
# Exclude specific sections from main page (they'll still have /section/name/ pages)
EXCLUDE_SECTIONS_FROM_MAIN = ['draft', 'private']
This is useful for draft posts or topic-specific content you want separated from the main index.
Images
Images placed in images/ are copied to output/images/. By default picopaper
strips metadata (EXIF, GPS location, XMP, PNG text chunks) from them during the
build so nothing like camera model or GPS coordinates is published. The source
files in images/ are never modified — only the copies in output/.
JPEGs are re-encoded with their original quantization tables (quality="keep"),
so there is no quality loss. Animated images (GIF/WebP) and unsupported formats
(e.g. SVG) are copied as-is.
Configuration in config.py:
STRIP_IMAGE_EXIF = True # set to False to copy images byte-for-byte
Metadata stripping uses Pillow. If Pillow is not installed, picopaper prints a warning and copies images unchanged.
Installation
- Create and activate virtual environment:
python3 -m venv venv
source venv/bin/activate # On Linux/Mac
# or: venv\Scripts\activate # On Windows
- Install dependencies:
pip install -r requirements.txt
- Configure your blog in
config.py:
BLOG_TITLE = "My Blog"
BLOG_DESCRIPTION = "A simple blog built with picopaper"
THEME = "default"
- Generate the site:
venv/bin/python picopaper.py
- Serve locally for testing:
cd output
python3 -m http.server 8000
Visit http://localhost:8000
Docker
Build and run with Docker:
# Build the image
docker build -t picopaper .
# Run the container
docker run --rm -v $(pwd):/app picopaper
For Podman (recommended for rootless):
# Build the image
podman build -t picopaper .
# Run with user namespace mapping
podman run --rm --userns=keep-id -v $(pwd):/app picopaper
The generated site will be available in the output/ directory.
Directory Structure
items/- Markdown content filestheme/- Theme directory containing templates and assetsdefault/- Default themetemplates/- Jinja2 templatesassets/- CSS and static assets
images/- Image files (copied to output)static/- Static files copied as-is (GPG keys, .well-known, etc.)output/- Generated site (do not edit)config.py- Blog configuration
Themes
Themes are organized in the theme/ directory. Each theme has its own subdirectory containing:
templates/- Jinja2 template filesassets/- CSS, JavaScript, and other static assets
To switch themes, change the THEME setting in config.py
Notes
Security
For security concerns or reports, please contact via hello a t uphillsecurity d o t com gpg.
License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
- ✅ Commercial use
- ✅ Modification
- ✅ Distribution
- ✅ Patent use
- ✅ Private use
- ✅ Limitations
- ❌Trademark use
- ❌Liability
- ❌Warranty
