Minimal Python static site generator. Markdown + Jinja2, Tailwind CSS built-in, zero configuration, optional frontmatter.
pip install medusa-ssg
medusa new mysite
cd mysite
medusa serveOpen http://localhost:4000 to see your site with live reload.
- Zero frontmatter required — title, date, tags, and description derived from filenames and content
- Optional YAML frontmatter — add custom metadata when you need it
- Markdown + Jinja2 — write content in Markdown, layouts in Jinja2
- Tailwind CSS — built-in pipeline with automatic processing
- 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
- Syntax highlighting — Pygments-powered code blocks
- Table of contents — auto-generated from headings
- Python 3.10+
- Node.js 16+ (for Tailwind CSS)
# Install Medusa
pip install medusa-ssg
# Or install from source
git clone https://github.com/yourname/medusa.git
cd medusa
pip install -e .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
medusa md # Interactive markdown file creator
medusa --version # Show versionmysite/
├── 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
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: 4001Medusa processes these file types in the site/ directory:
- Markdown (
.md) — Content files with full Markdown support - HTML (
.html) — Plain HTML files, processed as pages with pretty URLs - Jinja templates (
.html.jinja,.jinja) — Templates with Jinja2 syntax
All other file types are ignored.
Create .md files anywhere in site/:
# My Page Title
Content goes here. The first `# heading` becomes the page title.
Use #hashtags for tags. #python #tutorialName 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/
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.
Add optional YAML frontmatter for custom metadata:
---
author: Jane Doe
featured: true
category: tutorials
---
# My Post Title
Content goes here.Access frontmatter in templates with {{ frontmatter.author }}.
| 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 |
frontmatter |
Current page's YAML frontmatter |
url_for(path) |
Generate URLs with base path |
render_toc(page) |
Generate nested <ul> table of contents |
css_path(name) |
Path to CSS file in assets/css/ |
js_path(name) |
Path to JS file in assets/js/ |
img_path(name) |
Path to image in assets/images/ (auto-detects extension) |
font_path(name) |
Path to font in assets/fonts/ (auto-detects extension) |
pygments_css() |
CSS for syntax highlighting |
current_page.title # Page title
current_page.content # Rendered HTML
current_page.body # Raw markdown/source text
current_page.description # First paragraph (for SEO)
current_page.excerpt # Full first paragraph
current_page.url # URL path (/posts/hello/)
current_page.slug # URL slug (hello)
current_page.date # Publication date
current_page.tags # List of tags
current_page.toc # List of headings for TOC
current_page.draft # Is draft?
current_page.layout # Layout name
current_page.group # Folder group (posts, pages, etc.)
current_page.frontmatter # YAML frontmatter dict{# 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 %}Pages are sorted by three criteria in order:
- Date — Newest first (from filename or file modification time)
- Number prefix — If dates are equal, by number prefix in filename
- Filename — If dates and numbers are equal, alphabetically
Examples of number prefixes:
site/docs/01-introduction.md → Sorted first
site/docs/02-getting-started.md → Sorted second
site/docs/03-advanced.md → Sorted third
You can also combine date and number prefixes:
site/posts/2024-01-15-01-part-one.md
site/posts/2024-01-15-02-part-two.md
{# site/_partials/card.html.jinja #}
<div class="card">
<h3>{{ title }}</h3>
<p>{{ description }}</p>
</div>
{# Include in any template #}
{% include "card.html.jinja" %}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 %}HTML files (.html) in site/ are processed as pages with pretty URLs. For example:
site/about.html→/about/site/404.html→/404/
The content is wrapped in your layout template like Markdown pages.
Create site/404.html for a custom error page. It will be processed as a page at /404/ and served with a 404 status code by the dev server.
Reference assets in your templates:
<link rel="stylesheet" href="{{ css_path('main') }}">
<script src="{{ js_path('app') }}"></script>
<img src="{{ img_path('logo') }}" alt="Logo">All helpers respect root_url for CDN support. Image and font helpers auto-detect file extensions when omitted.
Code blocks with language identifiers get Pygments highlighting:
```python
def hello():
print("Hello!")
```Include the CSS in your layout:
<style>{{ pygments_css() }}</style>Create netlify.toml:
[build]
command = "pip install -e . && medusa build"
publish = "output"
[[redirects]]
from = "/*"
to = "/404.html"
status = 404Create vercel.json:
{
"buildCommand": "pip install -e . && medusa build",
"outputDirectory": "output"
}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: ./outputmedusa build
# Upload contents of output/ to your server# Clone and install
git clone https://github.com/yourname/medusa.git
cd medusa
make setup # Installs dev dependencies and configures pre-commit hook
# Run tests
pytest
# Run with coverage
pytest --cov=medusa --cov-report=term-missingMIT License. See LICENSE for details.