https://github.com/phalt/paulblish
Turn your markdown files into a static website
https://github.com/phalt/paulblish
Last synced: 29 days ago
JSON representation
Turn your markdown files into a static website
- Host: GitHub
- URL: https://github.com/phalt/paulblish
- Owner: phalt
- License: mit
- Created: 2026-03-31T12:09:14.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-05-22T19:47:47.000Z (2 months ago)
- Last Synced: 2026-05-22T22:32:42.165Z (2 months ago)
- Language: HTML
- Homepage: http://paulwrites.software/
- Size: 1.07 MB
- Stars: 5
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
# Paulblish 📖
[](https://www.python.org/downloads/)
[](https://opensource.org/licenses/MIT)
A CLI tool that converts an Obsidian vault (or any directory of markdown files) into a static HTML site with a cyberpunk aesthetic, ready for deployment to GitHub Pages.
## What is this?
Paulblish (`pb`) takes a directory of markdown files — such as an [Obsidian](https://obsidian.md) vault — and generates a complete static HTML site from it. The generated output is committed directly to the repo and deployed via GitHub Pages. Your source vault lives on your machine; only the HTML output is in version control.
The project follows the [file over app](https://stephango.com/file-over-app) philosophy. Your content stays in plain markdown, and the site generator is just a tool you run locally.
## Quick Start
```sh
git clone https://github.com/phalt/paulblish.git
cd paulblish
make install
uv run pb build --source ~/obsidian/blog --output ./_site
git add _site/
git commit -m "Rebuild site"
git push
```
## Installation
Clone the repo and install dependencies using `uv`:
```sh
git clone https://github.com/phalt/paulblish.git
cd paulblish
make install
```
## Usage
### `pb build`
Build the static site from a source directory.
```sh
uv run pb build --source ~/obsidian/blog --output ./_site
```
| Flag | Default | Description |
| --- | --- | --- |
| `--source`, `-s` | `.` (cwd) | Path to the markdown source directory. Must contain a `site.toml`. |
| `--output`, `-o` | `./_site` | Path to write generated HTML. |
| `--base-url` | _(from site.toml)_ | Base URL for absolute links (overrides `site.toml`). |
| `--templates` | _(bundled defaults)_ | Path to a custom Jinja2 templates directory. |
| `--drafts` | `false` | Include articles without `publish: true`. |
| `--incremental` | `false` | Only rebuild articles whose source file has changed since the last build. See [Incremental Builds](#incremental-builds). |
### `pb clean`
Remove the output directory.
```sh
uv run pb clean --output ./_site
```
| Flag | Default | Description |
| --- | --- | --- |
| `--output`, `-o` | `./_site` | Path to the built site directory to remove. |
### `pb serve`
Serve the built site locally for preview.
```sh
uv run pb serve --output ./_site
uv run pb serve --output ./_site --port 9000
```
| Flag | Default | Description |
| --- | --- | --- |
| `--output`, `-o` | `./_site` | Path to the built site directory to serve. |
| `--port`, `-p` | `8000` | Port to listen on. |
## Site Configuration
Site configuration is loaded from one of two sources, tried in order:
1. **`site.toml`** — a TOML file in the root of your source directory (preferred).
2. **`Home.md` frontmatter** — YAML frontmatter fields in `Home.md` at the source root (useful for Obsidian users where `.toml` files are inconvenient).
If `site.toml` is present it takes priority. If neither source is found, or the required fields are missing, `pb build` exits with an error:
```text
Error: No site configuration found in
Either create a site.toml file or add site config fields to your Home.md frontmatter:
title, base_url, description, author
```
### Method 1: `site.toml`
Create `site.toml` in the root of your source directory:
```toml
[site]
title = "My Blog"
base_url = "https://yourusername.github.io/yourrepo"
description = "A blog about things."
author = "Your Name"
cname = "" # optional — your custom domain, e.g. "blog.example.com"
avatar = "" # optional — relative path to a square image for the home page
```
### Method 2: `Home.md` frontmatter
Add the config fields to the YAML frontmatter of your `Home.md`:
```yaml
---
publish: true
title: "My Blog"
base_url: "https://yourusername.github.io/yourrepo"
description: "A blog about things."
author: "Your Name"
cname: "" # optional
avatar: "" # optional
---
```
### Fields
| Field | Required | Description |
| --- | --- | --- |
| `title` | yes | Site title shown in `` and the nav bar |
| `base_url` | yes | Absolute base URL (e.g. `https://user.github.io/repo`) |
| `description` | yes | Short site description for `` tags |
| `author` | yes | Author name shown in footer and meta |
| `cname` | no | Custom domain — writes a `CNAME` file to the output root |
| `avatar` | no | Path to a square image shown on the home page |
## Frontmatter Schema
Only files with `publish: true` in their frontmatter are included in the build.
```yaml
---
publish: true # required — must be true to be included
title: "Article Title" # optional — derived from first H1 or filename if absent
slug: "article-title" # required — used as the URL segment; also accepts `permalink`
date: 2026-03-15 # optional — used for sorting; falls back to file mtime
tags: [python, tooling] # optional — list of strings; generates /tags/{tag}/ pages
description: "A short summary." # optional — shown in article header and listings
---
```
Files missing a `slug` (or `permalink`) are skipped with a clear reason in the build output.
## Directory Structure
The source directory path of each file is preserved in the output URL:
| Source file | Output path | URL |
| --- | --- | --- |
| `foo.md` | `_site/foo/index.html` | `/foo/` |
| `articles/foo.md` | `_site/articles/foo/index.html` | `/articles/foo/` |
| `articles/deep/bar.md` | `_site/articles/deep/bar/index.html` | `/articles/deep/bar/` |
| `Home.md` | `_site/index.html` | `/` |
## The Home File
A file named `Home.md` (case-insensitive) at the root of your source directory becomes the site index page at `/`.
The home page renders with:
- An ASCII art "Hello" banner (`
`)
- An optional avatar image (configured via `site.avatar` in `site.toml`)
- The body content of `Home.md`
If no `Home.md` is present or it is not published, the index page falls back to a generated article listing.
## Deployment
The deployment workflow is: **build locally → commit `_site/` → push → GitHub Actions deploys**.
There is no build step in CI. You build the site on your machine and commit the generated HTML.
### Steps
1. Build the site locally:
```sh
uv run pb build --source ~/obsidian/blog --output ./_site
```
2. Commit the output:
```sh
git add _site/
git commit -m "Rebuild site"
git push
```
3. In your GitHub repo settings, go to **Settings → Pages** and set the source to **GitHub Actions**.
The included `deploy.yml` workflow triggers on any push to `main` that touches `_site/**` and deploys the directory to GitHub Pages.
### Base URL patterns
The `base_url` in your `site.toml` controls how all internal links and asset paths are generated. The correct value depends on how your site is hosted.
#### Pattern 1: GitHub Pages without a custom domain
Your site lives at `https://username.github.io/reponame/`. Set `base_url` to the full path including the repo name:
```toml
base_url = "https://username.github.io/reponame"
```
#### Pattern 2: GitHub Pages with a custom domain (CNAME)
When you use a CNAME, your site is served from the root of your domain. Set `base_url` to just the domain — no trailing slash, no path suffix:
```toml
base_url = "https://blog.example.com"
cname = "blog.example.com"
```
#### Testing locally
`pb serve` handles this automatically. On every `pb build`, a small metadata file (`.pb-meta.json`) is written to the output directory recording the `base_url` that was used. When you then run `pb serve`, the server reads that file and rewrites all occurrences of `base_url` in HTML and XML responses to an empty string before sending them to the browser. Internal paths like `/articles/foo/` are not affected.
```sh
uv run pb build --source ~/obsidian/blog --output ./_site # build once with production base_url
uv run pb serve # works locally without any rebuild
```
The production `_site/` files themselves are never modified; rewriting happens only in-flight during serving.
### Custom Domain
Set `cname` in your `site.toml`:
```toml
cname = "blog.example.com"
```
This writes a `CNAME` file to `_site/CNAME` on every build. GitHub Pages reads the CNAME from the published directory root — no manual setup needed beyond pointing your DNS.
## Incremental Builds
For large vaults, re-rendering every article on each build can be slow. Pass `--incremental` to skip articles whose source file has not been modified since the last build:
```sh
uv run pb build --source ~/obsidian/blog --output ./_site --incremental
```
How it works:
- At the end of every build (full or incremental) a manifest file `.pb-manifest.json` is written to the output directory. It records each article's source path and its modification time.
- On the next `--incremental` build, articles whose source `mtime` matches the manifest are skipped — their existing HTML files are left untouched.
- Articles that have been modified (or are new) are fully re-rendered.
- Source files that have been **deleted** since the last build have their output HTML removed automatically.
- Shared pages (all-articles listing, tag pages, RSS feed, `sitemap.xml`, `robots.txt`, `404.html`) are always regenerated — they reflect the full article set.
`--incremental` is compatible with `--drafts`. Draft articles are tracked in the manifest when `--drafts` is active.
## Development
```sh
make install # uv sync — install all dependencies
make test # uv run pytest
make lint # uv run ruff check .
make format # uv run ruff format .
make clean # remove _site/, __pycache__, and egg-info
```
## Fork Your Own Copy
Paulblish is designed so anyone can fork it and run their own blog. To set up your own:
1. Fork this repository (or use "Use this template" on GitHub).
2. Clone it locally and run `make install`.
3. Configure your site — pick whichever suits your workflow:
**Option A — `site.toml`** (create in the root of your Obsidian directory):
```toml
[site]
title = "My Blog"
base_url = "https://yourusername.github.io/yourrepo"
description = "A blog about things."
author = "Your Name"
cname = "" # set to your custom domain, or leave empty
avatar = "" # path to a square image, or leave empty
```
**Option B — `Home.md` frontmatter** (add fields to your existing `Home.md`):
```yaml
---
publish: true
title: "My Blog"
base_url: "https://yourusername.github.io/yourrepo"
description: "A blog about things."
author: "Your Name"
---
```
4. Ensure your markdown files have `publish: true` in their frontmatter.
5. Create a `Home.md` in the root of your content directory for your index page.
6. Build the site:
```sh
uv run pb build --source /path/to/your/obsidian/dir --output ./_site
```
7. Commit the `_site/` directory and push to `main`.
8. In your GitHub repo settings, enable Pages and set it to deploy from GitHub Actions.
The `pb` tool, templates, and styles are all included in the repo. Customise the templates in `templates/` and the CSS in `templates/static/style.css` to make it your own.