medusa-ssg
Minimal Python static site generator. Markdown + Jinja2, Tailwind CSS built-in, zero configuration, optional 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 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
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
medusa md # Interactive markdown file creator
medusa --version # Show version
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
Supported File Types
Medusa 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.
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/
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.
Frontmatter
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 }}.
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 |
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 |
Page Object
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
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 %}
Sorting
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
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
HTML Pages
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.
404 Page
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.
Asset Helpers
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.
Syntax Highlighting
Code blocks with language identifiers get Pygments highlighting:
```python
def hello():
print("Hello!")
```
Include the CSS in your layout:
<style>{{ pygments_css() }}</style>
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.
Metadata
Release files for medusa-ssg 0.1.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| medusa_ssg-0.1.9.tar.gz | 65.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| medusa_ssg-0.1.9-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 119.3 kB
Release files / medusa_ssg-0.1.9.tar.gz
| Download URL | medusa_ssg-0.1.9.tar.gz |
|---|---|
| Size | 65.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7d7e1072d1cda4caa8b030d8de8a10da65b4ab66f33f6e908876a00bbba45c53
|
|
BLAKE2b-256 checksum How to use checksums |
5105d9943b876f71a9aacafc1233ef35d5fafddde7843c05efd53dede024d6be
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|
Release files / medusa_ssg-0.1.9-py3-none-any.whl
| Download URL | medusa_ssg-0.1.9-py3-none-any.whl |
|---|---|
| Size | 53.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3d65a1e734473f178091d5341d739f8e80ff3b1ef5f37df5de50dfa3f48030bd
|
|
BLAKE2b-256 checksum How to use checksums |
326d827f3946c5c6483e5703ac45eb94b8741537f581f1e466efd8b040c30387
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|