Paulias
A statically hosted URL shortener. Paulias takes a markdown file of short paths and target URLs, generates a directory of HTML redirect files, and pushes them to a GitHub Pages repo. No server, no JavaScript required, no database.
What is this?
Paulias turns a single human-readable markdown file into a fully working URL shortener hosted for free on GitHub Pages. You manage shortlinks by editing paulias.md; Paulias builds and deploys the static HTML.
- File over app. The list of shortlinks lives in a single markdown file with YAML frontmatter and link references. Edit it anywhere — GitHub, your phone, Obsidian, or the CLI.
- No server. Each shortlink is a tiny static HTML file containing a meta refresh redirect. GitHub Pages serves it for free.
- The config is the source of truth. The generated HTML is derived; the markdown file is what matters.
Quick start
# install
uv tool install paulias
# create a new shortener repo
gh repo create your-username/my-links --public --clone
cd my-links
# initialise
paulias init
# add some links
paulias add gh https://github.com/your-username
paulias add blog https://yourblog.com
# build and publish
paulias deploy
Your shortlinks are now live at https://your-username.github.io/my-links/gh, etc.
Installation
uv tool install paulias
Requires Python 3.14+. After install, run paulias from inside any directory containing a paulias.md file.
Commands
paulias init
Write a starter paulias.md to the current working directory.
paulias init
Auto-detects the repo field from git remote get-url origin if the cwd is a git repo pointing at GitHub. Errors if paulias.md already exists unless --force is passed.
| Flag | Description |
|---|---|
--force |
Overwrite an existing paulias.md. |
--repo |
Set the repo field explicitly instead of auto-detecting. |
paulias add
Append a new shortlink to paulias.md.
paulias add <path> <url>
Validates the path and URL before writing. Errors if <path> already exists. Does not build or push.
| Flag | Description |
|---|---|
--force |
Overwrite an existing entry with the same path. |
--deploy |
Run paulias deploy immediately after adding. |
paulias delete
Remove a shortlink from paulias.md.
paulias delete <path>
Errors if <path> does not exist. Does not build or push.
| Flag | Description |
|---|---|
--deploy |
Run paulias deploy immediately after deleting. |
paulias list
Print the current shortlinks as a formatted table.
paulias list
Reads only from paulias.md — does not look at docs/.
| Flag | Description |
|---|---|
--json |
Print as JSON instead of a table. |
paulias open
Open a shortlink's target URL in the default browser.
paulias open <path>
Looks up <path> in paulias.md and opens its target URL directly — useful for quick verification without typing the full domain.
| Flag | Description |
|---|---|
--print |
Print the target URL to stdout instead of opening it. |
paulias deploy
Build the site and push to GitHub Pages.
paulias deploy
In order: validate paulias.md → wipe and regenerate docs/ → stage files → commit → push. The commit message is generated automatically: Deploy N shortlinks (M added, K removed).
deploy is idempotent — running it twice with no changes produces no commit.
| Flag | Description |
|---|---|
--dry-run |
Build to docs/ but do not commit or push. |
--no-push |
Commit but do not push. |
--message, -m |
Override the generated commit message. |
--force |
Skip the validation step (not recommended). |
paulias.md format
The config lives at paulias.md in the root of your shortener repo.
---
cname: paulias.dev
repo: your-username/my-links
branch: main
title: "My shortlinks"
about: "A personal collection of short links."
footer: "Made by [You](https://yoursite.com) with [Paulias](https://github.com/phalt/paulias)."
---
[gh]: https://github.com/your-username
[blog]: https://yourblog.com
Shortlinks are standard markdown link reference definitions. Each line maps a short path to its target URL. The order of entries is preserved on disk so the file diffs cleanly.
Frontmatter fields
| Field | Required | Description |
|---|---|---|
repo |
yes | GitHub repo in owner/name form. Used by deploy. |
cname |
no | Custom domain. Writes a CNAME file to docs/CNAME. |
branch |
no | Branch to push to. Default main. |
title |
no | Title shown on the index page. Default Paulias. |
about |
no | Short description shown on the index page. |
footer |
no | Footer text. Supports inline markdown for links and emphasis. |
Path rules
- Lowercase alphanumerics, hyphens, and underscores only.
- Must start with an alphanumeric character.
- Maximum 64 characters.
- Must not collide with reserved paths:
cname,404,index,style,docs,assets,static,templates,paulias.
Custom domain setup
Set cname in your frontmatter to your custom domain:
cname: links.yourdomain.com
paulias deploy will write a CNAME file to docs/CNAME. Then in your DNS provider, add a CNAME record pointing links.yourdomain.com to your-username.github.io.
Finally, in your GitHub repo settings under Pages, set the custom domain.
Deployment workflow
# initialise once
paulias init
# daily usage
paulias add gh https://github.com/your-username
paulias add f1 https://www.formula1.com
paulias deploy
# editing by hand also works
vim paulias.md
paulias deploy
paulias add and paulias delete only edit paulias.md. Use paulias deploy to build and ship. This separation lets you batch edits, edit the config by hand, and review the diff before publishing.
Template customisation
Create a templates/ directory next to paulias.md to override any bundled template:
my-links/
├── paulias.md
└── templates/
├── base.html.j2
├── index.html.j2
└── 404.html.j2
Local templates take precedence over the bundled defaults. Available Jinja2 context variables:
| Variable | Type | Description |
|---|---|---|
title |
str | Site title from frontmatter. |
about |
str | About text from frontmatter. |
footer |
str | Footer HTML, already rendered from markdown. |
cname |
str | Custom domain or empty string. |
shortlinks |
list | List of {"short": ..., "target": ...}. |
Development
git clone https://github.com/phalt/paulias
cd paulias
make install # uv sync
make test # pytest
make lint # ruff check
make format # ruff format
Fork your own copy
Paulias is designed to be self-hosted on GitHub Pages with zero running costs. To set up your own shortener:
- Create a new public GitHub repo (e.g.
your-username/my-links). - In repo Settings → Pages, set source to the
mainbranch, folder/docs. - Install Paulias:
uv tool install paulias. - Clone your repo, run
paulias init, add links, andpaulias deploy.
License
MIT — see LICENSE.
Metadata
Release files for paulias 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| paulias-0.1.0.tar.gz | 47.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| paulias-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 65.6 kB
Release files / paulias-0.1.0.tar.gz
| Download URL | paulias-0.1.0.tar.gz |
|---|---|
| Size | 47.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7e63f37e138fecb6a9986d13bde71fd20c657c1d102cdbea26e8a6a80aa045bc
|
|
BLAKE2b-256 checksum How to use checksums |
2dea47cb68336aad70777dd67f712dfb73c6f8ea37afa76b5366b5064e0553a3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / paulias-0.1.0-py3-none-any.whl
| Download URL | paulias-0.1.0-py3-none-any.whl |
|---|---|
| Size | 17.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cd8d6910d2baa1c99fc56b8dd27268fc07e7c25c581bed4572da7463976220da
|
|
BLAKE2b-256 checksum How to use checksums |
9e458092ac4c8b2f086c82198dd045fa23382612541d1e028492ecf02a7aee26
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.12 {"installer":{"name":"uv","version":"0.11.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|