{"id":51303895,"url":"https://github.com/phalt/paulblish","last_synced_at":"2026-06-30T22:32:22.600Z","repository":{"id":348790415,"uuid":"1197208139","full_name":"phalt/paulblish","owner":"phalt","description":"Turn your markdown files into a static website","archived":false,"fork":false,"pushed_at":"2026-05-22T19:47:47.000Z","size":1121,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T22:32:42.165Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"http://paulwrites.software/","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/phalt.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-31T12:09:14.000Z","updated_at":"2026-05-22T19:47:51.000Z","dependencies_parsed_at":"2026-05-12T17:01:01.146Z","dependency_job_id":null,"html_url":"https://github.com/phalt/paulblish","commit_stats":null,"previous_names":["phalt/paulblish"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/phalt/paulblish","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phalt%2Fpaulblish","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phalt%2Fpaulblish/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phalt%2Fpaulblish/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phalt%2Fpaulblish/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/phalt","download_url":"https://codeload.github.com/phalt/paulblish/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phalt%2Fpaulblish/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34986248,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-30T02:00:05.919Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-30T22:32:18.367Z","updated_at":"2026-06-30T22:32:22.567Z","avatar_url":"https://github.com/phalt.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Paulblish 📖\n\n[![Python 3.14](https://img.shields.io/badge/python-3.14-blue.svg)](https://www.python.org/downloads/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nA 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.\n\n## What is this?\n\nPaulblish (`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.\n\nThe 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.\n\n## Quick Start\n\n```sh\ngit clone https://github.com/phalt/paulblish.git\ncd paulblish\nmake install\nuv run pb build --source ~/obsidian/blog --output ./_site\ngit add _site/\ngit commit -m \"Rebuild site\"\ngit push\n```\n\n## Installation\n\nClone the repo and install dependencies using `uv`:\n\n```sh\ngit clone https://github.com/phalt/paulblish.git\ncd paulblish\nmake install\n```\n\n## Usage\n\n### `pb build`\n\nBuild the static site from a source directory.\n\n```sh\nuv run pb build --source ~/obsidian/blog --output ./_site\n```\n\n| Flag | Default | Description |\n| --- | --- | --- |\n| `--source`, `-s` | `.` (cwd) | Path to the markdown source directory. Must contain a `site.toml`. |\n| `--output`, `-o` | `./_site` | Path to write generated HTML. |\n| `--base-url` | _(from site.toml)_ | Base URL for absolute links (overrides `site.toml`). |\n| `--templates` | _(bundled defaults)_ | Path to a custom Jinja2 templates directory. |\n| `--drafts` | `false` | Include articles without `publish: true`. |\n| `--incremental` | `false` | Only rebuild articles whose source file has changed since the last build. See [Incremental Builds](#incremental-builds). |\n\n### `pb clean`\n\nRemove the output directory.\n\n```sh\nuv run pb clean --output ./_site\n```\n\n| Flag | Default | Description |\n| --- | --- | --- |\n| `--output`, `-o` | `./_site` | Path to the built site directory to remove. |\n\n### `pb serve`\n\nServe the built site locally for preview.\n\n```sh\nuv run pb serve --output ./_site\nuv run pb serve --output ./_site --port 9000\n```\n\n| Flag | Default | Description |\n| --- | --- | --- |\n| `--output`, `-o` | `./_site` | Path to the built site directory to serve. |\n| `--port`, `-p` | `8000` | Port to listen on. |\n\n## Site Configuration\n\nSite configuration is loaded from one of two sources, tried in order:\n\n1. **`site.toml`** — a TOML file in the root of your source directory (preferred).\n2. **`Home.md` frontmatter** — YAML frontmatter fields in `Home.md` at the source root (useful for Obsidian users where `.toml` files are inconvenient).\n\nIf `site.toml` is present it takes priority. If neither source is found, or the required fields are missing, `pb build` exits with an error:\n\n```text\nError: No site configuration found in \u003csource directory\u003e\n       Either create a site.toml file or add site config fields to your Home.md frontmatter:\n\n         title, base_url, description, author\n```\n\n### Method 1: `site.toml`\n\nCreate `site.toml` in the root of your source directory:\n\n```toml\n[site]\ntitle       = \"My Blog\"\nbase_url    = \"https://yourusername.github.io/yourrepo\"\ndescription = \"A blog about things.\"\nauthor      = \"Your Name\"\ncname       = \"\"   # optional — your custom domain, e.g. \"blog.example.com\"\navatar      = \"\"   # optional — relative path to a square image for the home page\n```\n\n### Method 2: `Home.md` frontmatter\n\nAdd the config fields to the YAML frontmatter of your `Home.md`:\n\n```yaml\n---\npublish: true\ntitle: \"My Blog\"\nbase_url: \"https://yourusername.github.io/yourrepo\"\ndescription: \"A blog about things.\"\nauthor: \"Your Name\"\ncname: \"\"    # optional\navatar: \"\"   # optional\n---\n```\n\n### Fields\n\n| Field | Required | Description |\n| --- | --- | --- |\n| `title` | yes | Site title shown in `\u003ctitle\u003e` and the nav bar |\n| `base_url` | yes | Absolute base URL (e.g. `https://user.github.io/repo`) |\n| `description` | yes | Short site description for `\u003cmeta\u003e` tags |\n| `author` | yes | Author name shown in footer and meta |\n| `cname` | no | Custom domain — writes a `CNAME` file to the output root |\n| `avatar` | no | Path to a square image shown on the home page |\n\n## Frontmatter Schema\n\nOnly files with `publish: true` in their frontmatter are included in the build.\n\n```yaml\n---\npublish: true                    # required — must be true to be included\ntitle: \"Article Title\"           # optional — derived from first H1 or filename if absent\nslug: \"article-title\"            # required — used as the URL segment; also accepts `permalink`\ndate: 2026-03-15                 # optional — used for sorting; falls back to file mtime\ntags: [python, tooling]          # optional — list of strings; generates /tags/{tag}/ pages\ndescription: \"A short summary.\"  # optional — shown in article header and listings\n---\n```\n\nFiles missing a `slug` (or `permalink`) are skipped with a clear reason in the build output.\n\n## Directory Structure\n\nThe source directory path of each file is preserved in the output URL:\n\n| Source file | Output path | URL |\n| --- | --- | --- |\n| `foo.md` | `_site/foo/index.html` | `/foo/` |\n| `articles/foo.md` | `_site/articles/foo/index.html` | `/articles/foo/` |\n| `articles/deep/bar.md` | `_site/articles/deep/bar/index.html` | `/articles/deep/bar/` |\n| `Home.md` | `_site/index.html` | `/` |\n\n## The Home File\n\nA file named `Home.md` (case-insensitive) at the root of your source directory becomes the site index page at `/`.\n\nThe home page renders with:\n\n- An ASCII art \"Hello\" banner (`\u003cpre class=\"ascii-banner\" aria-hidden=\"true\"\u003e`)\n- An optional avatar image (configured via `site.avatar` in `site.toml`)\n- The body content of `Home.md`\n\nIf no `Home.md` is present or it is not published, the index page falls back to a generated article listing.\n\n## Deployment\n\nThe deployment workflow is: **build locally → commit `_site/` → push → GitHub Actions deploys**.\n\nThere is no build step in CI. You build the site on your machine and commit the generated HTML.\n\n### Steps\n\n1. Build the site locally:\n\n   ```sh\n   uv run pb build --source ~/obsidian/blog --output ./_site\n   ```\n\n2. Commit the output:\n\n   ```sh\n   git add _site/\n   git commit -m \"Rebuild site\"\n   git push\n   ```\n\n3. In your GitHub repo settings, go to **Settings → Pages** and set the source to **GitHub Actions**.\n\nThe included `deploy.yml` workflow triggers on any push to `main` that touches `_site/**` and deploys the directory to GitHub Pages.\n\n### Base URL patterns\n\nThe `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.\n\n#### Pattern 1: GitHub Pages without a custom domain\n\nYour site lives at `https://username.github.io/reponame/`. Set `base_url` to the full path including the repo name:\n\n```toml\nbase_url = \"https://username.github.io/reponame\"\n```\n\n#### Pattern 2: GitHub Pages with a custom domain (CNAME)\n\nWhen 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:\n\n```toml\nbase_url    = \"https://blog.example.com\"\ncname       = \"blog.example.com\"\n```\n\n#### Testing locally\n\n`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.\n\n```sh\nuv run pb build --source ~/obsidian/blog --output ./_site  # build once with production base_url\nuv run pb serve                                            # works locally without any rebuild\n```\n\nThe production `_site/` files themselves are never modified; rewriting happens only in-flight during serving.\n\n### Custom Domain\n\nSet `cname` in your `site.toml`:\n\n```toml\ncname = \"blog.example.com\"\n```\n\nThis 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.\n\n## Incremental Builds\n\nFor 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:\n\n```sh\nuv run pb build --source ~/obsidian/blog --output ./_site --incremental\n```\n\nHow it works:\n\n- 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.\n- On the next `--incremental` build, articles whose source `mtime` matches the manifest are skipped — their existing HTML files are left untouched.\n- Articles that have been modified (or are new) are fully re-rendered.\n- Source files that have been **deleted** since the last build have their output HTML removed automatically.\n- Shared pages (all-articles listing, tag pages, RSS feed, `sitemap.xml`, `robots.txt`, `404.html`) are always regenerated — they reflect the full article set.\n\n`--incremental` is compatible with `--drafts`. Draft articles are tracked in the manifest when `--drafts` is active.\n\n## Development\n\n```sh\nmake install    # uv sync — install all dependencies\nmake test       # uv run pytest\nmake lint       # uv run ruff check .\nmake format     # uv run ruff format .\nmake clean      # remove _site/, __pycache__, and egg-info\n```\n\n## Fork Your Own Copy\n\nPaulblish is designed so anyone can fork it and run their own blog. To set up your own:\n\n1. Fork this repository (or use \"Use this template\" on GitHub).\n2. Clone it locally and run `make install`.\n3. Configure your site — pick whichever suits your workflow:\n\n   **Option A — `site.toml`** (create in the root of your Obsidian directory):\n\n   ```toml\n   [site]\n   title = \"My Blog\"\n   base_url = \"https://yourusername.github.io/yourrepo\"\n   description = \"A blog about things.\"\n   author = \"Your Name\"\n   cname = \"\"   # set to your custom domain, or leave empty\n   avatar = \"\"  # path to a square image, or leave empty\n   ```\n\n   **Option B — `Home.md` frontmatter** (add fields to your existing `Home.md`):\n\n   ```yaml\n   ---\n   publish: true\n   title: \"My Blog\"\n   base_url: \"https://yourusername.github.io/yourrepo\"\n   description: \"A blog about things.\"\n   author: \"Your Name\"\n   ---\n   ```\n\n4. Ensure your markdown files have `publish: true` in their frontmatter.\n5. Create a `Home.md` in the root of your content directory for your index page.\n6. Build the site:\n\n   ```sh\n   uv run pb build --source /path/to/your/obsidian/dir --output ./_site\n   ```\n\n7. Commit the `_site/` directory and push to `main`.\n8. In your GitHub repo settings, enable Pages and set it to deploy from GitHub Actions.\n\nThe `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.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphalt%2Fpaulblish","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fphalt%2Fpaulblish","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphalt%2Fpaulblish/lists"}