ha.mr compresses a URL well when the domain is already popular
Static URL compressor and QR code optimizer
At a glance
- What is it?
- A browser-side URL compressor and QR optimizer served as static files by nginx, where the compression ratio is a function of a Huffman dictionary built from public link datasets, and the author documents his own alphabet mistake in the README.
- Who is it for?
- The compression here is real, and its limits are structural rather than temporary. Short output depends on the destination already being in the dictionary, which means popular domains compress well and unusual ones do not, and the QR path restricts the output alphabet further.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 37 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- p2r3/ha.mr1,077 ★ · +81
| Repository | 2026-09-21 | 2026-11-13 |
|---|---|---|
| p2r3/ha.mr | 996 | 1.1k |
81 Stars in the last 53 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The compression ratio is a function of how popular the domain is
The scheme is described in five steps, and the second one explains everything about how well a given link compresses.
Step one handles structure rather than statistics. Protocol, a `www.` prefix and `index.html` are detected and reduced to individual bits, and a port, if present, is written as a raw numeric value. That is a fixed saving per URL, not a proportional one.
Step two is where the ratio comes from. Second-level and top-level domains are matched against a Huffman-coded dictionary of the most common websites and TLDs, so a domain that appears often in that corpus gets a short code and a domain that appears once gets a long one. The corpus is named in the acknowledgements: a Reddit links dataset and the Ada URL dataset. Both are link collections, so the dictionary is a picture of what gets shared, which means the compression is best exactly where sharing is most common and worst for a URL nobody has ever posted.
Step three handles the remainder by splitting the rest of the link into parts, each of which is either fitted to a predefined character set or Huffman coded. So the path and query string get their own treatment rather than being appended raw.
The practical consequence is that a compressed link is not a fixed-width encoding. The same tool produces a short code for a well-known site and a long one for a page nobody has linked, and the ratio cannot be predicted from the URL length alone.
Square brackets are in the output alphabet, and the author says so
For plain links the output is encoded in what the project calls the full character set of a URL. The parenthetical note that follows is the most quotable line in the README: the author has been informed that square brackets are not supposed to be part of that set, but it is too late to change it now.
Two costs follow, and they are different in kind. The first is size. Every character in the output table consumes bits whether or not it appears in a given URL, so a table with a few unnecessary entries makes every compressed link slightly longer than it needs to be. That is a fixed tax and it is small.
The second is correctness, and it is the reason the note is worth reading twice. If brackets are not valid URL characters, then a decompressed string that relies on them is not a well-formed URL. The question of whether the brackets are ever emitted is not answered here, and the difference matters: an unused table entry is waste, while an emitted one produces an output that other tools may reject or misparse.
The decision to keep it is defensible for a project whose stated scope is small and whose author writes that one day it might reach a state where nothing has to be changed. It is also the kind of thing a fork could fix cheaply, and the note is precise enough that a forker knows exactly where to look.
QR output drops to the alphanumeric set, which is uppercase only
The fifth step is where the QR path diverges from the link path, and the divergence is the whole reason QR codes come out shorter than other generators produce them.
QR codes have several encoding modes, and the alphanumeric mode is the most compact one because it packs two characters into a smaller number of bits than the byte mode does. The catch is what alphanumeric mode allows: digits, uppercase letters and a short list of punctuation. No lowercase.
So the compressor's output alphabet is not one set but two, chosen by target. Links get the full URL character set. QR codes get the alphanumeric set, which removes overhead relative to generators that default to byte mode. And step three, which fits each remaining segment to a predefined character set, is therefore constrained differently depending on which of the two you are producing.
That constraint propagates backwards. Since lowercase cannot appear in QR output, the per-segment fitting has to map mixed-case path segments into an uppercase-only alphabet, and the Huffman tables have to be built to suit whichever set is in play. It is a reasonable engineering decision and it explains why the project ships alphabet definitions as a separate concern, in a file the image copies alongside the compressor itself.
The user-visible consequence is that a QR produced here is denser than a byte-mode QR of the same URL, at the cost of a shorter alphabet for the payload to fit into.
There is no build step, so the QR library is committed to the repository
The container recipe is three instructions long. It starts from `nginx:alpine`, copies `nginx.conf` over the default site configuration, copies a single `404.html`, and then copies the application's own files into the nginx document root.
What that last copy contains is the informative part. The command lists `docs/alphabets.js`, `docs/compress.js` and `docs/main.js`, the directory `docs/lean-qr/`, and `standalone.js` from the repository root, all into the same destination. Three first-party scripts, one vendored third-party library directory, and the standalone build all land in one folder with no separation.
There is no `npm install` and no build command anywhere in it. Nothing is compiled, bundled or minified. That means the copy of lean-qr in `docs/lean-qr/` has to be committed to the repository, because there is no step that would fetch it. The same applies to the rest of the application: the browser receives the source files as they sit in `docs/`.
Both published tags are titled with the words standalone tool, v1 and then v1.1, which lines up with that layout. The standalone file and the web page share the same assets, and the deployment simply publishes both into one directory.
Compose serves plain HTTP on 8080 and confirms there is no database
The compose file is a single service. It builds from the repository root and maps host port 8080 to container port 80. There is no database service, no application server, no cache and no environment block.
That is the deployment proof of the headline claim. The project describes itself as compressing links and optimizing QR codes entirely in the browser without a back-end database, and the container that runs it is nginx serving files. There is nothing behind it that could hold a log of what was compressed, because there is no process behind it at all.
The gap between the container and the hosted site is transport. The compose file exposes port 80 and terminates no TLS, so a local copy answers plain HTTP on 8080, while the hosted address is reached over HTTPS. A self-hosted operator therefore has to supply the certificate and the redirect themselves, and nothing in the shipped configuration does it for them.
The repository also carries `flake.nix`, `flake.lock` and `package.nix` alongside the Dockerfile and compose file, so there is a Nix packaging path as well as a container path, and at least three ways to run this that the README never explains.
Three Nix files, a Dockerfile and a compose file, none of them documented
The top-level entries are Dockerfile, LICENSE, README.md, compose.yml, `docs/`, `flake.lock`, `flake.nix`, `nginx.conf`, `package.nix` and `standalone.js`. There is no build directory, no source directory, no test directory and no CI configuration at the root.
The README covers two subjects and no others. The first is the five-step compression algorithm. The second is the contributing policy. There is no section on installation, no section on running the project, no section on deployment, no configuration reference and no troubleshooting. Nothing explains what `nginx.conf` contains beyond the fact that the Dockerfile overwrites the default site with it.
That is a coherent choice for a project whose author explicitly asks contributors to keep the scope small and not to refactor everything into a newer web framework, and whose stated ambition is a state where nothing has to be changed. It is also the reason the release tags and the branch have drifted apart: the tags are v1 on 2026-08-09 and v1.1 on 2026-08-16, while the last commit on the default branch is dated 2026-09-13, so the branch sits several weeks past the newest release with no release notes covering the difference.
For an operator, the practical route is the hosted site, or reading three packaging files to work out what the container does.
The contributing policy offers a compromise on generated code and threatens a block
Three rules govern contributions, and the third is the most unusual thing in the repository.
The first is scope. Bug fixes and standard maintenance are welcome; features stacked on features and a rewrite into a newer framework are not. The stated reason is that this is a simple project and should stay that way, and the author adds that one day the project might reach a state where nothing has to be changed, and that this would be fine.
The second is process. Search the existing issues or open one, settle whether the change is necessary, and only then write code. Undiscussed, non-trivial pull requests will be closed, and the author states plainly that having written a lot of code does not create an obligation to merge it.
The third is about generated code, and the reasoning is worth reading in full rather than paraphrasing. The project was written by hand as a learning exercise and the author would prefer it stay that way, but a ban would only push people to hide the use of tools, so the compromise is disclosure on three conditions: you say where AI was used, the generated code fits the existing style, and you can explain the change in your own words. Passing off generated work as your own is grounds for a block.
That is a more workable rule than either extreme, and it is enforced by judgement rather than by a mechanism, which is the honest limitation of any such policy.
A compressed link is opaque by design, and no abuse policy is stated
The output of this service is a Huffman-coded string. That is the point of it, and it has a consequence worth naming before anyone builds a link shortener on top of it: the destination of a ha.mr link is not readable by a human, by a mail client, or by a URL preview service. Someone clicking it has to trust the operator of the service.
Nothing in the visible README addresses that. There is no blocklist of domains, no reporting path for an abusive link, no statement about how long a compressed link lives, and no disclosure mechanism for the destination. The privacy claim is about the absence of a database, which is a real property and also means there is no server-side record that could be used to revoke a link or trace its history.
That is a normal property of URL compressors rather than an objection specific to this one. It becomes worth naming here because the project is MIT licensed and ships a compose file, so anyone can run an instance, and because the standalone build means a copy can exist without the operator knowing who else is using the service.
The design is otherwise honest about its own tradeoffs: a small static footprint, a popular-domain dictionary, two output alphabets, and one acknowledged mistake in the character set.
Editorial conclusion
The compression here is real, and its limits are structural rather than temporary. Short output depends on the destination already being in the dictionary, which means popular domains compress well and unusual ones do not, and the QR path restricts the output alphabet further. Anyone building on this should know that the character set carries square brackets that do not belong to a URL, which the author has decided to live with. Two operational notes for a self-hosted copy. The compose file serves plain HTTP on port 8080 with no certificate, so put a reverse proxy in front of it before it is reachable from anywhere but your own machine. And because output is deliberately opaque and no abuse policy is stated, treat the service as a general-purpose link compressor and decide your own policy about what you are willing to compress.
Frequently asked questions
What does ha.mr do and does it need a server?
It compresses links and optimizes QR codes entirely in the browser, with no back-end database. The deployment confirms this: the compose file runs a single container built from nginx:alpine that serves static files and maps port 8080 to port 80.
How does ha.mr compress a URL?
In five steps. Protocol, a www. prefix and index.html are reduced to individual bits and a port is written as a raw numeric value, then second-level and top-level domains are matched against a Huffman-coded dictionary of common websites and TLDs, then the rest is split into parts that are fitted to a predefined character set or Huffman coded. Links use the full URL character set, QR codes use the alphanumeric set.
Why is a ha.mr link shorter for some URLs than others?
Because the domain dictionary is a Huffman code built from public link corpora, named in the acknowledgements as a Reddit links dataset and the Ada URL dataset. A domain that appears often gets a short code and one that appears once gets a long code, so the ratio depends on how widely the destination has been linked.
Why are ha.mr QR codes smaller than other generators produce?
The output uses the alphanumeric character set, which packs characters into fewer bits than byte mode, removing overhead relative to generators that default to byte encoding. The cost is a restricted uppercase-only alphabet, which in turn constrains how each remaining segment of the URL can be fitted.
How do I run ha.mr myself?
The repository contains a Dockerfile, a compose file mapping 8080 to 80, and Nix packaging files, but the README has no installation, run or deployment section. The container is nginx:alpine with no build step, so the application files and the vendored lean-qr library in docs/ are copied into the document root as they are committed.
What does the ha.mr project say about AI-generated contributions?
It does not ban them. The author writes that the project was written by hand as a learning exercise, but that a ban would only push people to hide tool use, so the compromise is disclosure: say where AI was used, keep the style consistent, and be able to explain the change in your own words. Passing generated work off as your own is grounds for a block.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/p2r3-ha-mr)Star history chart for your README
[](https://hysenlabs.com/projects/p2r3-ha-mr)Was this useful?