Minimal Python static site generator. Markdown + Jinja2, Tailwind CSS built-in, zero configuration, no frontmatter.
Project description
medusa-ssg
Minimal Python static site generator. Markdown + Jinja2, Tailwind CSS built-in, zero configuration, no frontmatter.
Quick Start
pip install medusa-ssg
medusa new mysite
cd mysite
medusa serve
Open http://localhost:4000 to see your site with live reload.
Features
- Zero frontmatter — title, date, tags, and description derived from filenames and content
- Markdown + Jinja2 — write content in Markdown, layouts in Jinja2
- Tailwind CSS — built-in pipeline with typography plugin
- Live reload — dev server with WebSocket-based hot reload
- Pretty URLs —
/posts/hello/not/posts/hello.html - Automatic sitemap & RSS — generated on build
- Custom 404 pages — static HTML served with proper status codes
Requirements
- Python 3.10+
- Node.js 16+ (for Tailwind CSS)
Installation
# Install Medusa
pip install medusa-ssg
# Or install from source
git clone https://github.com/yourname/medusa.git
cd medusa
pip install -e .
CLI Commands
medusa new NAME # Create a new project
medusa build # Build site to output/
medusa build --drafts # Include draft content
medusa serve # Dev server at localhost:4000
medusa serve --port 3000 # Custom port
medusa serve --drafts # Include drafts in dev
Project Structure
mysite/
├── site/ # Content and templates
│ ├── _layouts/ # Page layouts
│ │ └── default.html.jinja
│ ├── _partials/ # Reusable components
│ │ ├── header.html.jinja
│ │ └── footer.html.jinja
│ ├── posts/ # Blog posts
│ │ └── 2024-01-15-hello-world.md
│ ├── index.jinja # Home page
│ ├── about.md # Static page
│ └── 404.html # Custom error page
├── assets/
│ ├── css/main.css # Tailwind entry point
│ ├── js/main.js
│ └── images/
├── data/ # YAML data files
│ ├── site.yaml # Site metadata
│ └── nav.yaml # Navigation links
├── output/ # Generated site (git-ignored)
├── medusa.yaml # Configuration
├── tailwind.config.js
└── package.json
Configuration
Create medusa.yaml in your project root:
# Output directory (default: output)
output_dir: output
# Base URL used to absolutize all links in output (default: empty).
# medusa serve always uses http://localhost:<port> for dev builds.
root_url: https://example.com
# Dev server port (default: 4000)
port: 4000
# WebSocket port for live reload (default: port + 1)
ws_port: 4001
Writing Content
Markdown Pages
Create .md files anywhere in site/:
# My Page Title
Content goes here. The first `# heading` becomes the page title.
Use #hashtags for tags. #python #tutorial
Dated Posts
Name files with a date prefix for automatic date extraction:
site/posts/2024-01-15-my-post.md → /posts/my-post/
site/posts/2024-02-20-another.md → /posts/another/
Custom Layouts
Specify a layout with brackets in the filename:
site/about[minimal].md → Uses _layouts/minimal.html.jinja
site/contact.md → Uses _layouts/default.html.jinja
Drafts
Prefix files or folders with _ to mark as draft:
site/posts/_work-in-progress.md # Draft post
site/_experiments/test.md # Draft folder
Drafts are excluded from builds unless you use --drafts.
Templates
Available Variables
| Variable | Description |
|---|---|
current_page |
The current page object |
pages |
Collection of all pages |
tags |
Map of tag names to page collections |
data |
Merged YAML from data/ directory |
url_for(path) |
Generate URLs with base path |
Page Object
current_page.title # Page title
current_page.content # Rendered HTML
current_page.description # First paragraph
current_page.url # URL path (/posts/hello/)
current_page.date # Publication date
current_page.tags # List of tags
current_page.draft # Is draft?
current_page.layout # Layout name
current_page.group # Folder group (posts, pages, etc.)
Collections API
{# Get posts #}
{% for post in pages.group("posts").sorted() %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
{# Filter by tag #}
{% for post in pages.with_tag("python").latest(5) %}
{{ post.title }}
{% endfor %}
{# Published only (excludes drafts) #}
{% for page in pages.published() %}
{{ page.title }}
{% endfor %}
Partials
{# site/_partials/card.html.jinja #}
<div class="card">
<h3>{{ title }}</h3>
<p>{{ description }}</p>
</div>
{# Include in any template #}
{% include "card.html.jinja" %}
Data Files
YAML files in data/ are available in templates:
# data/site.yaml - merged into data root
title: My Site
author: Jane Doe
# data/social.yaml - available as data.social
- platform: GitHub
url: https://github.com/username
- platform: Twitter
url: https://twitter.com/username
<h1>{{ data.title }}</h1>
<p>By {{ data.author }}</p>
{% for link in data.social %}
<a href="{{ link.url }}">{{ link.platform }}</a>
{% endfor %}
Static Files
404 Page
Create site/404.html for a custom error page. This file is copied directly to output without layout processing and served with a 404 status code by the dev server.
Other Static HTML
Any .html file (not .html.jinja) in site/ is copied as-is to output, preserving the path structure.
Deployment
Netlify
Create netlify.toml:
[build]
command = "pip install -e . && medusa build"
publish = "output"
[[redirects]]
from = "/*"
to = "/404.html"
status = 404
Vercel
Create vercel.json:
{
"buildCommand": "pip install -e . && medusa build",
"outputDirectory": "output"
}
GitHub Pages
Create .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: pip install medusa-ssg
- run: npm install
- run: medusa build
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./output
Manual / Any Host
medusa build
# Upload contents of output/ to your server
Development
# Clone and install
git clone https://github.com/yourname/medusa.git
cd medusa
pip install -e ".[dev]"
# Run tests
pytest
# Run with coverage
pytest --cov=medusa --cov-report=term-missing
License
MIT License. See LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file medusa_ssg-0.1.5.tar.gz.
File metadata
- Download URL: medusa_ssg-0.1.5.tar.gz
- Upload date:
- Size: 50.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e15d47638719d05f95e2d5e5b0b492a4e8efbdb9f8dc239487c40088f0e8d4a
|
|
| MD5 |
530561df789f4ab6920607c2e59fcb12
|
|
| BLAKE2b-256 |
98cac106299219eca0ca030892f0dc3a2972478c9298d8309064a5f17b9fbcd0
|
File details
Details for the file medusa_ssg-0.1.5-py3-none-any.whl.
File metadata
- Download URL: medusa_ssg-0.1.5-py3-none-any.whl
- Upload date:
- Size: 45.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
67b9a41cec6c579d02ecd4ecd2f7f87d0a4920c86b1aae57e0182b728d373ea8
|
|
| MD5 |
ef08d172e5ea093b18547a59957f68c7
|
|
| BLAKE2b-256 |
8ab5012fd5c619e02cb126ed2507f630c5f88e4bf5625953abdde4833eac632b
|