Note
For the PTBR version, access the README.pt.md.
For the webv version (using flask) access the README_WEB.md version.
A minimal static blog system that publishes Markdown posts directly to Fastmail Files via WebDAV. No build step, no server, no framework — just Python and your writing.
Markdown file (.md)
↓
publish.py
↓
HTML page (with inline CSS)
↓
Fastmail WebDAV (your domain)
Posts are converted from Markdown to self-contained HTML pages and uploaded to your Fastmail Files storage, which can be served as a static website through your custom domain.
- Python 3.10+
- A Fastmail account with Files & Storage enabled
- A custom domain pointed to Fastmail (optional but recommended)
Install dependencies:
pip install requests markdown pyyamlOr with uv:
uv pip install requests markdown pyyaml1. Copy the config template:
cp .blog-config.example.json .blog-config.json2. Fill in your credentials:
{
"webdav_base_url": "https://myfiles.fastmail.com/",
"webdav_username": "you@fastmail.com",
"webdav_app_password": "YOUR_APP_PASSWORD",
"blog_path": "blog/",
"site_title": "My Blog",
"site_description": "A short description",
"author": "Your Name",
"base_url": "https://yourdomain.com/blog/",
"posts_per_page": 10
}To generate a Fastmail app password: Settings → Privacy & Security → Manage app passwords
To find your WebDAV URL: Settings → Files & Storage
.blog-config.jsonis in.gitignore— never commit your credentials.
Posts are plain Markdown files with YAML frontmatter:
---
title: My first post
date: 2024-01-15
description: A short summary shown in the index.
tags: [writing, personal]
---
Your content here.| Field | Required | Description |
|---|---|---|
title |
No | Defaults to the filename |
date |
No | Defaults to today |
description |
No | Shown in the post index |
tags |
No | List or comma-separated string |
slug |
No | URL slug; auto-generated from title |
Supported Markdown extensions: fenced code blocks, tables, footnotes, table of contents, syntax highlighting.
# Publish a single post
python publish.py my-post.md
# Rebuild the entire index (after editing multiple posts)
python publish.py --rebuild-index
# List all published posts
python publish.py --list
# Delete a post by its slug
python publish.py --delete my-post-slugscrap_blog.py scrapes an existing blog and saves each post as a Markdown file compatible with publish.py:
# Save to current directory
python scrap_blog.py
# Save to a specific folder
python scrap_blog.py posts_antigos/Edit BLOG_INDEX_URL inside the script to point to your source blog.
.
├── publish.py # Main publisher
├── scrap_blog.py # Blog scraper / importer
├── .blog-config.json # Your credentials (gitignored)
├── .blog-config.example.json
├── .blog-posts.json # Local post registry (auto-managed)
├── preview-index.html # Local preview of the index page
├── preview-post.html # Local preview of a post page
├── posts_antigos/ # Imported/archived posts
└── posts_novos/ # New posts to publish
The generated HTML is self-contained with inline CSS. The default theme uses:
- Fonts: Josefin Sans (headings), Lora (body), JetBrains Mono (code)
- Colors: Dark background (
#1a2332) with teal accents - Layout: Single-column, max 720px, responsive
To customize the design, edit the CSS constant in publish.py.
MIT