A statically hosted URL shortener for GitHub Pages.
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file paulias-0.1.0.tar.gz.
File metadata
- Download URL: paulias-0.1.0.tar.gz
- Upload date:
- Size: 47.9 kB
- Tags: Source
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e63f37e138fecb6a9986d13bde71fd20c657c1d102cdbea26e8a6a80aa045bc
|
|
| MD5 |
469917d95e8b80e7e8dfed3aabced4af
|
|
| BLAKE2b-256 |
2dea47cb68336aad70777dd67f712dfb73c6f8ea37afa76b5366b5064e0553a3
|
File details
Details for the file paulias-0.1.0-py3-none-any.whl.
File metadata
- Download URL: paulias-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.8 kB
- Tags: Python 3
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cd8d6910d2baa1c99fc56b8dd27268fc07e7c25c581bed4572da7463976220da
|
|
| MD5 |
21a46a90f2cfbc7f6e80218d6dc054d7
|
|
| BLAKE2b-256 |
9e458092ac4c8b2f086c82198dd045fa23382612541d1e028492ecf02a7aee26
|