Link-in-bio pages you own — where the click history is a table in your own database, not a chart you rent back from a vendor.
One install holds every page you run. An agency puts one page per client, each on that client's own domain, signed in the footer with the agency's name. A single person runs one page and ignores the rest.
An open-source app template provided by Clawnify.com. An alternative to Linktree, Beacons, Bio.link and Campsite.
A link page is three lines of HTML. Nobody pays for the page — they pay for what accrues around it, and then they can't take it with them.
Two years of "which post actually sent people to the shop" lives in someone else's dashboard, exported as a CSV if you're lucky. The page is trivial and the history is the asset, so the history is what gets held.
So the redirect is the product:
visitor taps a row → /r/{block} → a row written here → 302 to the destination
│
└── your table: which link, when, from
which referrer, in your database
Every link is served through a counting redirect, so the record is yours before it is anyone's. You can query it, join it to your own data, and keep it when you stop using this.
The second thing it buys: the pages tell you when they rot. A checker walks every link and stores what it answered, and the overview sorts stalest first. Dead invites and 404'd shop links stop being something a client tells you about.
- Many pages, one install. Each on its own custom domain, each with its own look, all in one place with one overview.
- A page anyone can build. Drag a row to reorder it, or use the arrows — both are there, because a list that can only be reordered by dragging cannot be reordered from a keyboard at all. Create it, add rows, retitle them in place, reorder, hide one without losing its click history, pick a template, publish. The real page sits beside the editor and updates on every save, so the confirmation that a change worked is the change.
- Paste a URL and the row fills itself in. The destination's own title and image come back from its meta tags — a fetch, not a model call and not a screenshot. The image is copied into this app's own storage, so a page with ten thumbnails still makes zero requests to anyone else, and still tells nobody who is reading it.
- Eight templates, then your brand on top.
mono,ink,paper,sunset,brutal,slate,candy,forest— each a whole look, with its own typeface, button treatment, corner and canvas. - A photograph can be the page. Upload one and the header becomes a hero: the image full-bleed, the name over it.
- A page is also a business card. Fill in the contact details and a Save contact row hands over a vCard the phone opens in its contacts app, with a QR code pointing at the page for the printed side. The code is generated as SVG, so it prints at any size and costs no image request.
- A socials row that still counts. A line of platform marks under the links,
each one going through the same counting redirect, so a tap on Instagram is
recorded with the platform beside it rather than vanishing into a bare
<a href>. - Email capture that lands here. Addresses go into your own table, not a vendor's list.
Every look downloads as markdown — the fields in a frontmatter block, then prose saying what the look is for and which of its rules are load-bearing:
---
slug: paper
name: Paper
background: "#f6f1e7"
font: serif
button: outline
---
# Paper
Rules that matter:
- The buttons never fill. A filled button on this ground reads as a
web form dropped into a book.Edit it yourself or hand it to an agent — "make this colder", "this is for a law firm" — paste it back, and your pages can wear it. The prose round-trips untouched, so the reasoning outlives the person who had it.
Button text colour is computed from the accent rather than authored, and every template is tested against WCAG AA for body text, subtitle and button label. The first sunset gradient looked lovely and measured 2.32:1; the test is why it is not the one that shipped.
A photograph defeats measurement — the next upload is a white sky where the last was a dark wall — so the guarantee moves to the layer the app controls. It solves for the scrim opacity at which the worst possible pixel still clears 4.5:1, and refuses anything lighter. You can go darker; you cannot go thinner.
One self-contained HTML document. No web font, no external stylesheet, no analytics beacon, no request to any other host. It renders on the first paint in an in-app browser on a bad connection, which is where link pages are actually opened.
That constraint is why the typefaces are system stacks and the gradients are CSS, and why a link thumbnail is copied here rather than hotlinked.
| Path | What it is |
|---|---|
/ |
The overview: every page with its staleness, broken links and clicks |
/pages/{id} |
The editor, with the live page beside it |
/templates |
The gallery; download any look as markdown |
/p/{slug} |
The public page. Plain HTML, server rendered |
/r/{block} |
The counting redirect every link goes through |
/p/{slug}/contact.vcf |
The contact card, as vCard |
/p/{slug}/card |
The printable business card, sized to 54×86mm |
/p/{slug}/qr.svg |
A QR code pointing at the page |
/api/… |
The full API, documented at /api/openapi.json and /llms.txt |
A page is a list of blocks, and kind decides how each one renders:
| kind | Renders as |
|---|---|
link |
A button. The only kind whose clicks are counted |
header |
A quiet caption that groups the rows under it |
embed |
An inline player. YouTube and Spotify today |
email |
A signup field that posts to this app |
socials |
A row of platform marks, each counted separately |
contact |
A button that hands over the page's contact card |
Requires Node 22+ and pnpm.
pnpm install
pnpm dev # UI on :5173, API on :8792pnpm dev applies schema.sql to a local database and runs the UI and the API together. The dev server supplies an organisation header so the authenticated routes work on a fresh clone.
pnpm test # contrast rules, the template round trip, the metadata parser
pnpm typecheck
pnpm buildnpx clawnify deployOr use the button in the Clawnify app directory. Each deployment gets its own database and file storage — the pages, the subscribers and the click history stay inside your own organisation.
- A new template is one entry in
src/server/theme.ts, or a markdown document posted to/api/templates. The contrast tests run over every one automatically, so a look that cannot be read never merges. - A new block kind is a
kindvalue plus a branch inrenderBlock— the editor picks it up from the same list. - The link checker stores a status per row rather than a boolean, so "410 gone" and "429 rate limited" can be told apart later without a migration.
- Deliberately absent: a hosted analytics dashboard. The clicks are a table; use your own tools on it. That is the point of keeping them.
MIT.