Build your own link shortener
Build your own link shortener as a short URL, a redirect, and a click count. Publish a domain you own and skip Bitly's $35–$300 a month platform.
You can build your own link shortener in 90 minutes. Bitly is a short URL. The catalog calls it tiny by definition, and that is the whole app most people use.
You pay $35–$300 a month for it. That is $420–$3,600 a year. The basic replacement is 90 minutes: short links, redirects, and click counts.
I would ship this next if I still rented redirects. It is marked Tonight. The verdict on the card is “Tiny by definition”.
Yesterday I wrote about a Typeform alternative that is a form and a table. A shortener is the next small bill. Open the product page, copy the prompt, and send a real short URL before you start decorating a workspace.
What you need to build your own link shortener
The Bitly page splits the work into three rows.
The first row is the one you want tonight. Short links, redirects, click counts. A slug you pick or generate. A destination you can edit. A number that goes up when someone hits the URL.
The second row is 5 hours. Custom slugs, campaign tags, QR codes, expiry. Those can wait until you have sent the first link.
The third row is a week. Global analytics, deep links, enterprise permissions. The table labels that row as the part you should skip.
If you tell an agent to “clone Bitly”, it aims at the third row. Use the prompt on the page. It is written for a useful v1, and you still have to keep it on the first row.
Here is the shape of the data you are building. Nothing else has to exist at 90 minutes.
const link = {
domain: 'go.flaviocopes.com',
slug: 'js-course',
destination: 'https://flaviocopes.com/javascript/',
enabled: true,
clicksToday: 0,
}
A visitor hits https://go.flaviocopes.com/js-course and lands on the current destination. You change the destination later. The tweet still works.
Who a Bitly alternative is for
The audience line on the page is specific. A marketing team managing campaign links on its own domain.
The goal is to create branded redirects and inspect basic traffic without surveillance. Tonight the done state is one live short URL and a count that moves when you click it.
If you need deep links into mobile apps, or a permission matrix for a company, keep paying. That is the honest rebuild. The site puts it at a week on purpose.
The same v1 also replaces Dub, Rebrandly, and Short.io. They solve the same job. One focused build covers whichever of those you currently pay for.
A QR code that stays editable after you print it is a different job. That is QR Tiger, $15–$49 a month, about 90 minutes. The 5-hour Bitly row already includes a QR download. Build the squares if the poster is the rent. Build this if the tweet is.
Read the non-goals before you copy anything
Open the guide. Read the audience and the non-goals before you touch the prompt.
The non-goal is the useful sentence: do not add ad retargeting, deep links, link-in-bio pages, or enterprise permissions.
The guardrail sits next to it. Block dangerous protocols. Rate-limit redirect creation. Minimize stored request data.
Then pick the stack you already write. Every prompt ships in four flavors: AHA (Astro, HTMX, Alpine.js), Next.js, Laravel, and Rails. The AHA version for this product uses Workers, D1, and Astro. This site is an Astro app on Cloudflare, so that is the path I default to. Use Next.js if that is your weekday language. The scope matters more than the framework.
Copy the prompt. Before you run it, add one extra line at the top: ship the 90-minute row only. Short links, redirects, click counts. Stop.
Walk the first short-link journey
You create a branded short link. Custom slug or generated. A destination. You confirm the redirect from the detail screen.
A visitor requests the short URL. They land on the current safe destination. One privacy-minimized event lands in the daily count.
Keep that slug stable when you edit the destination later, so the tweet, the newsletter, and the printed card keep working.
The screens the prompt asks for are a links dashboard, a link editor, a bulk import, and an analytics view. For 90 minutes you can live with the first two. Bulk import and the campaign chart can wait.
The dashboard needs the slug, the destination, the click total, and a create action. It does not need a date-range comparison.
The editor is the whole app for the owner. Domain, slug, destination, enabled state, a test-redirect button. Campaign, note, expiry, and QR preview belong to the 5-hour row.
The data model on the page is ShortLink, ClickEvent, Campaign. Tonight you need ShortLink. ClickEvent can be a (link_id, day) counter. Campaign waits.
One more rule from the prompt, and it is a good one. Destination edits must invalidate cached redirect state promptly, while historical counts stay on the same link. The identity is the slug record, so the totals stay when the destination changes.
Code the destination check yourself
Agents invent URL fields that accept anything. Your domain then becomes an open redirect with a short slug on it.
The prompt already says to accept only valid HTTP or HTTPS destinations, reject embedded credentials and control characters, and never fetch the destination as part of redirect handling. The mini-prompt on the page, “The URL validator that refuses politely”, is the longer version of that rule.
Humans paste flaviocopes.com/javascript/ with no scheme. Attackers paste javascript: or https://user:pass@host. You assume https for the first case and reject everything that is not http or https.
function normalizeDestination(raw) {
const trimmed = raw.trim()
const withScheme = trimmed.includes('://') ? trimmed : `https://${trimmed}`
const url = new URL(withScheme)
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('Only http and https destinations are allowed')
}
if (url.username !== '' || url.password !== '') {
throw new Error('Destinations cannot include credentials')
}
return url.href
}
normalizeDestination('flaviocopes.com/javascript/?utm_source=newsletter')
//https://flaviocopes.com/javascript/?utm_source=newsletter
Never fetch the destination to check that it loads. Fetching turns your shortener into an SSRF proxy and slows every save. You are validating shape, not liveness. On redirect, pass the destination through untouched so the UTM tags stay byte-for-byte.
Rate-limit creation so a script cannot turn your domain into a spam-link factory. I would start at 30 new links an hour per account. The same function should run for a single create and for a CSV import later.
A failed save should leave the rest of the draft intact. Do not wipe the slug because the destination is missing a slash.
The slug has to be unique for real
Two people hitting create on launch at the same time is the other failure agents miss.
The mini-prompt is “Two people, one slug, exactly one winner”. Normalize consistently. Trim. Compare case-insensitively. Enforce uniqueness in the creation transaction itself, a unique constraint, not a check-then-insert.
Reserve the routes your app already serves. dashboard, api, health. A user must not be able to shadow your admin page.
Generated slugs need a boring alphabet. Skip 0 and O. Skip l and 1. If the random one collides, generate again instead of failing the request.
const SLUG_ALPHABET = 'abcdefghijkmnopqrstuvwxyz23456789'
function generateSlug(length = 7) {
let slug = ''
for (let i = 0; i < length; i++) {
slug += SLUG_ALPHABET[Math.floor(Math.random() * SLUG_ALPHABET.length)]
}
return slug
}
generateSlug()
//k4n7p2m
That is enough for tonight. Bulk CSV with reserved-slug errors is the 5-hour version of the same uniqueness rule.
Count clicks without storing people
The catalog is explicit about what to keep. The short link, the day, a coarse country, a referrer domain, a device class. Discard full IP addresses, query secrets, and raw user agents.
Tonight you can skip country and device. Increment a (link_id, day) row. Filter obvious bots by user agent before counting. Write the increment after the visitor is already gone.
function clickKey(linkId, requestedAt) {
const day = requestedAt.toISOString().slice(0, 10)
return `${linkId}:${day}`
}
clickKey('js-course', new Date('2026-10-12T18:04:00.000Z'))
//js-course:2026-10-12
Disabled and expired links do not increment anything. They show a branded unavailable page.
Cache the redirect if you want it fast. Invalidate that cache in the same write that changes the destination or disables the link. Store the expiry timestamp inside the cached entry and check it at request time, so a promo dies on schedule without a cron job.
What not to build in a link shortener
Leave out ad retargeting, deep links, a link-in-bio page, and anything that looks like enterprise permissions.
Campaign tags are on the feature list. A string on the row is enough later. QR download is a library call on the 5-hour row. Neither of those is a second product.
If the agent starts a workspace with roles, stop it. Paste the non-goal again.
The 5-hour row, after you send a link
Use the shortener as the real tool for a week. Then look at the second estimate.
Custom slugs you already have if tonight went well. Campaign tags, so you can filter a launch from a newsletter. QR codes, so a printed card can point at the same slug. Expiry, so a promo dies on schedule.
The mini-prompt for that cache work is “Fast redirects that notice when you edit them”. Historical analytics stay attached to the slug through every destination edit. The link’s identity is the record, not the URL it currently points at.
That is the solid v1. The catalog says 5 hours. Stop there.
The week-long row is global analytics, deep links, and enterprise permissions. You do not need that.
Host it, point the links, cancel
The product pages put hosting this way: a focused replacement costs your build time plus close-to-zero hosting.
Put the shortener on a subdomain you already own, go.flaviocopes.com. Update the next tweet first, then the other places you still point at the old Bitly domain.
Use it for a week as the real tool, not as a demo. If you miss expiry or a QR download, add them from the 5-hour row. You will not miss a permission matrix.
Then cancel Bitly, or Dub, or Rebrandly, or Short.io, whichever one you pay for.
A page of links is a different job. That is Linktree, $9–$24 a month, about 30 minutes. You already have that post if a bio page is the rent.
If you want another Tonight card after this one, the audit is still the index. Statuspage is about 3 hours. TinyPNG Pro is about 60 minutes and stays in the browser.
Start now
Go to Build your own Bitly. Copy the AHA prompt, or the stack you already write.
Tell the agent the first estimate only: short links, redirects, click counts. Ninety minutes.
Publish the short URL. Point one real campaign at it tonight.