
I expected the URL shortener to be the difficult part. It wasn’t. The PHP back-end was finished relatively quickly: database schema, shortening algorithm, QR generation, statistics, rate limiting, and a simple API. The real challenge turned out to be building and publishing browser extensions for Firefox and Chrome.
TL;DR
I built a PHP URL shortener I always wanted to build – mostly to explore the algorithm used for such a tool, along with browser extensions for Mozilla Firefox and Google Chrome, with AI assistance. Technologies used: PHP, JavaScript, TailwindCSS v4.
Some of the technical challenges included:
- Of course, the shortening algorithm: generating a 6-character alphanumeric code, yielding ~57 billion combinations (62^6)
- Building browser extensions for Firefox and Chrome using Manifest V3 (MV3)
- i18n support for the web application (the extensions remained English-only)
element.innerHTMLand content security restrictions in extension contexts- Extension permissions and execution contexts within the browser
Non-technical challenges included:
- Chrome/Firefox Store compliance – like writing a privacy policy (required for the Chrome Web Store)
- Extension submission workflows (Firefox was simpler using web-ext cli; Chrome was more verbose via the dashboard)
And of course the usual infrastructure work: Apache configuration, caching, and rate limiting.
Overview
While AI might be cumbersome at times, I used Google Antigravity during development, mainly for boilerplate generation and debugging edge cases in browser extension behavior – especially around background scripts, MV3 constraints, and differences between Firefox and Chrome APIs (browser vs chrome namespaces).
Requirements
- No additional expenses (hosted on existing PHP infrastructure)
- Explore simple shortening strategies
- Keep usage friction minimal (easy to use)
The back-end (PHP):
The back-end was straightforward. It included:
- Simple relational schema (PDO-based)
- URL shortening logic
- QR code generation
- Basic analytics (i.e. shortened urls and hits)
- Caching layer for performance
- Rate limiting for abuse prevention
- Lightweight API for the browser extension integration
Nothing particularly unusual here, except for the shortening algorithm
The shortening algorithm:
I considered a few approaches:
- Sequential auto-increment IDs (too predictable)
- Hashing (less readable, collision considerations)
- Random generation (best balance of simplicity and scalability)
I ultimately chose random 6-character alphanumeric strings.
That gives ~57 billion possible combinations, which is more than enough for the scale I expect, while keeping URLs short and easy to share or even type manually.
function generate_code(PDO $db): string {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$reserved = array_map('strtolower', RESERVED_CODES);
do {
$code = '';
for ($i = 0; $i < 6; $i++) {
$code .= $chars[random_int(0, 61)];
}
$stmt = $db->prepare('SELECT * FROM urls WHERE code = ?');
$stmt->execute([$code]);
} while ($stmt->fetchColumn() || in_array(strtolower($code), $reserved, true));
return $code;
}
Why six characters? Because five characters would significantly reduce the space (~916 million combinations), while seven would expand it to trillions, which was unnecessary for this use case. Six felt just right.
The extensions (JavaScript):
To meet the “ease of use” requirement, I decided to build browser extensions – for both Firefox and Chrome. I started with Firefox, since it’s my favorite, most flexible and privacy-first browser. The back-end API was already in place, so integration was simple: add an authenticated endpoint where the (long) URL goes and return a shortened URL. That part was easy. The rest was not.
To run an extension, of course you need to know your HTML, CSS and JS, however there are a few additional things to consider – of course the technical part, but also non-functional requirements.
A brief history on manifesting (or manifest.json)
Unfortunately manifesting won’t help here a lot. Here are the facts:
- Manifest v1 (MV1) – the original Chrome extension format, long retired.
- Manifest v2 (MV2) (~2012) – the format most of us grew up on. Persistent background pages, broad host permissions, blocking webRequest. Deprecated on Chrome as of 2025.
- Manifest v3 (MV3) (introduced 2019) – introduces service-worker backgrounds, declarativeNetRequest instead of blocking webRequest, tighter permission model – definitely go with this one if you need cross-browser compatibility – i.e. one extension codebase for multiple browsers
The technical challenges
- The codebase – Because of the browser-specific differences, I keep feature work in one primary branch and branch off of it to handle the per-browser differences
- manifest.json – small differences like if let’s say you want an item in the browser’s context menu, you will find that this permission for firefox is “menus”, but for chrome is “contextMenus”
- Browser-specific differences – Firefox uses
browserto access the browser API, while chrome does the same through thechromenamespace. There is a polyfill for that. - CORS limitations – there are such! In my case it was easier to reconfigure the server, but keep in mind there is no getting around them
- innerHTML code injections are not welcome. You will need to find a way around that. In my case for a few elements, I was able to programmatically get around it, but something important to keep in mind.
Non-technical complexity
The technical work was only part of the process. Shipping the extension also required:
- Writing an honest privacy policy (Chrome Web Store requirement)
- Developer verification (Chrome Web Store requirement)
- Preparing store assets (screenshots, descriptions, metadata)
- Navigating Chrome’s more verbose submission process – you need to call out why you need each permission you request.
The publication process itself ended up being a non-trivial part of the project.
Conclusion
Going into the project, I expected the URL shortener to be the interesting engineering challenge. Instead, the back-end became the most predictable part of the system. The real complexity came from the browsers themselves: Manifest V3, permissions, execution contexts, store requirements, and the subtle differences between Firefox and Chrome. If I were to build this again, I would still start with the back-end – but I would no longer assume that is where the hard part is.
Useful resources
- What are Extensions? on MDN
- Extension Workshop (for Firefox)
- Example Extensions – handy starting point
- web-ext – CLI tool for verifying your extension before submission in the Mozilla ecosystem
- Welcome to Extensions (for Chrome)