ha.mr is the project that makes the whole URL-shortener series more interesting.
YOURLS, Shlink, Kutt, Snapp, and Dub all revolve around the same basic idea: store a mapping somewhere, then redirect a short slug to a long target. ha.mr does something different. It compresses the target URL into the generated link itself.
That means there is no database. There are no stored slugs. There are no accounts. There is no analytics pipeline. It is closer to a URL codec than a traditional shortener.
What is ha.mr?
ha.mr is a static URL compressor and QR code optimizer.
I inspected commit:
af23d13ed488df891f65cf4df18c474859cb5e64
The project description is direct:
Compresses links and optimizes QR codes entirely in the browser, without a back-end database.
The implementation is plain static JavaScript served through a small Nginx container.
How Normal Link Shorteners Work
Most link shorteners are redirect databases.
The usual flow looks like this:
Long URL -> shortener stores mapping -> short code
Visitor opens /abc123 -> server looks up abc123 -> server redirects to long URL
That is how YOURLS, Shlink, Kutt, Snapp, and Dub broadly work, even though their stacks differ a lot.
This model gives you useful features:
- editable destinations;
- custom slugs;
- click analytics;
- expiration rules;
- password protection;
- teams and dashboards;
- API-based management;
- abuse controls.
But it also requires state. Somewhere, a service must remember that abc123 means https://example.com/some/long/path.
How ha.mr Is Different
ha.mr removes the lookup table.
The generated link contains the compressed destination:
Long URL -> compression algorithm -> encoded payload inside ha.mr link
Visitor opens ha.mr link -> browser decodes payload -> browser asks before redirecting
For normal text links, ha.mr puts the payload after #:
http://ha.mr#...
That fragment is handled by the browser. It is not sent to the server in the HTTP request.
For QR mode, ha.mr uses a path payload:
HTTP://HA.MR/...
That lets the QR encoder use an alphanumeric character set and reduce QR overhead.
What the Compression Does
The compressor is not just Base64.
From the code and README, ha.mr uses several layers:
- common URL parts like protocol,
www., andindex.htmlare encoded as compact flags; - ports are encoded numerically;
- common second-level domains and top-level domains use Huffman-coded dictionaries;
- remaining URL parts are split into path, query, and hash segments;
- each segment is encoded using the smallest matching alphabet when possible;
- fallback path/query characters can be Huffman-coded;
- output can target ASCII URL characters, QR alphanumeric characters, or emoji.
That last point is why this project is useful for thinking about QR codes. A QR code is not only about character count. The QR mode matters too. A string that fits the QR alphanumeric mode can require less QR capacity than an arbitrary byte-mode string.
Architecture
The runtime architecture is tiny:
Browser -> static Nginx site -> JavaScript decodes locally
The repo contains:
docs/404.html: the app shell;docs/main.js: UI, validation, QR generation, and redirect prompt;docs/compress.js: compression and decompression;docs/alphabets.js: ASCII, QR, and emoji output alphabets;docs/lean-qr/lean-qr.js: QR generation library;standalone.js: a small CLI-style wrapper around the compressor;Dockerfile,compose.yml, andnginx.conf: static deployment pieces.
There is no API server and no backend storage.
Important Behavior
ha.mr does not immediately redirect silently.
When a compressed link is opened, the browser decodes the destination and shows a confirmation prompt:
Proceed to target?
That is a sensible tradeoff. Traditional shorteners hide the destination until the server sends the redirect. ha.mr can reveal the destination before navigation because the browser decodes it locally.
The app supports only http and https URLs and rejects credentials in URLs. The UI also warns when a URL has multiple query parameters, because many long links are long mostly because of tracking parameters.
Self-Hosting with Docker
The reusable Home-Lab compose file is here:
The site includes the same Compose file from the local snippets folder:
assets/snippets/ha-mr/docker-compose.yml
Pre-Requisites - Docker
Install Docker on your system before proceeding:
- Linux: Official Docker Engine install guide
- Windows / Mac: Docker Desktop
Verify installation: docker --version && docker compose version
Prepare the environment:
cd assets/snippets/ha-mr
cp .env.sample .env
Start the stack:
docker compose up -d --build
The default local URL is:
http://localhost:8790
Why the Home-Lab Dockerfile Is Slightly Different
The upstream Dockerfile built successfully, but the smoke test exposed a QR-library path issue.
The app imports:
./lean-qr/lean-qr.js
In the upstream Docker build I tested, lean-qr.js was copied to the web root instead of /lean-qr/lean-qr.js. That meant:
/lean-qr/lean-qr.js
returned the HTML fallback, not JavaScript.
The Home-Lab Dockerfile fixes that by preserving:
site/lean-qr/lean-qr.js
inside the Nginx web root.
Static Hosting Notes
ha.mr can be deployed as a static site.
For full functionality, the static host needs:
404.htmlor equivalent single-page fallback;/main.js,/compress.js,/alphabets.js;/lean-qr/lean-qr.js;- arbitrary path fallback to the app shell for QR payload links.
Cloudflare Pages, GitHub Pages, Nginx, Caddy, and many static hosts can serve this kind of app. The key is path fallback: QR links use the path, so /SOME-PAYLOAD must still serve the app shell.
Field Test
I tested three paths.
First, the upstream Docker image built, but QR mode had the lean-qr path issue described above.
Second, the standalone CLI wrapper worked when Node was forced into ES-module mode:
node --experimental-default-type=module tmp/foss-post/ha-mr/standalone.js \
'https://github.com/p2r3/ha.mr?utm_source=test#readme' ascii encode
It produced a ha.mr link, and decoding it returned:
https://github.com/p2r3/ha.mr?utm_source=test#readme
Running the same file directly under Node 18 failed because the repository has ES-module syntax but no package.json declaring "type": "module".
Third, the patched Home-Lab Docker setup passed:
GET / 200 text/html
GET /main.js 200 application/javascript
GET /compress.js 200 application/javascript
GET /lean-qr/lean-qr.js 200 application/javascript
GET /ABC123 200 text/html
Then I removed the smoke-test container:
docker compose -p hamr_smoke down --remove-orphans
ha.mr vs YOURLS vs Shlink vs Dub
ha.mr is not a replacement for a managed short-link service.
Pick ha.mr when:
- you want a static, no-backend link compressor;
- you want QR-friendly encoding;
- you do not need analytics;
- you do not need editable links;
- you like that the destination is recoverable from the link itself.
Pick YOURLS, Shlink, Kutt, or Snapp when:
- you want custom slugs;
- you want a database of links;
- you want admin dashboards;
- you want click statistics;
- you want to edit a short link after sharing it.
Pick Dub when:
- you want heavier attribution, analytics, workspaces, and a cloud-native data plane.
Related posts:
FAQ
Is ha.mr a URL shortener?
Sort of, but not in the usual database-backed sense. It is more accurate to call it a static URL compressor. The generated link contains the destination in compressed form.
Is ha.mr the most different and minimal option in this series?
Yes.
ha.mr is the most different from YOURLS, Shlink, Kutt, Snapp, and Dub because it is not built around a backend redirect database.
It has:
no database
no accounts
no admin dashboard
no stored slugs
no analytics
no editable links
no backend API
no Redis, Postgres, MySQL, ClickHouse, S3, or queue service
That makes it the least cluttered and most minimalistic option from an infrastructure perspective.
The tradeoff is that you also lose normal shortener features: editable destinations, click tracking, custom branded slugs, expiration rules, teams, API management, and dashboards.
In this series:
- Most minimal and different: ha.mr.
- Classic simple self-hosted shortener: YOURLS.
- Mature API-first backend: Shlink.
- Modern app-style shortener: Kutt or Snapp.
- Heaviest SaaS-style attribution platform: Dub.
Does ha.mr need a database?
No. There is no Postgres, MySQL, MariaDB, SQLite, Redis, ClickHouse, or object storage requirement.
What additional services does ha.mr use?
The app itself uses no backend services.
Optional infrastructure:
- Static host: required to serve the files. Nginx, Caddy, Apache, GitHub Pages, and Cloudflare Pages can all serve static files. Nginx, Caddy, and Apache are open source and self-hostable.
- Reverse proxy: optional for HTTPS and public routing. Caddy, Nginx, Traefik, and Apache are open source and self-hostable.
- Cloudflare Pages: optional static hosting. It is convenient, but it is a managed service rather than something you self-host.
There is no analytics backend unless your web server or CDN logs requests.
Can I deploy ha.mr statically to Cloudflare Pages?
Yes, as long as arbitrary paths fall back to the app shell. Normal hash links need only the static app, but QR payload links use the path, so /SOME-PAYLOAD must still serve 404.html or the equivalent page shell.
Is the destination private?
No. The destination is encoded, not encrypted. Anyone with the link can decode it.
For normal hash links, the payload after # is not sent to the server. For QR path links, the payload is in the URL path and may appear in web server or CDN logs.
Can I edit a ha.mr link after sharing it?
No. There is no backend mapping to edit. If the destination changes, generate a new ha.mr link.
Does it track clicks?
No. Core ha.mr does not track clicks. Only your static host or CDN logs would see requests, and hash payloads are not sent to the server.
Does it support non-HTTP protocols?
No. The UI rejects protocols other than http and https.
Is it a PWA?
No. There is no service worker or web app manifest in the repo.
Does it use WASM or Pyodide?
No. The compressor is plain JavaScript using browser APIs and BigInt.
Verdict
ha.mr is the clever outlier in this series.
It is not the best choice if you want a dashboard, analytics, editable links, custom slugs, or team workflows. It is excellent if you want to understand what “shortening” can mean without a server-side lookup table.
That makes it a useful companion to YOURLS and Shlink: they show the traditional redirect-database model, while ha.mr shows the self-contained compression model.
Comments