# Link Shortener with Click Stats (Python, nothing to install)

> Your own short links on your own server: custom aliases, fast redirects, and a stats page with clicks per day, top referrers and devices. Python only.

- Kind: Plan
- Page: https://ownlyplans.com/bhaskar/link-shortener-with-click-stats-python-nothing-to-install/
- Licence: CC BY 4.0 (reuse with credit)

## Steps

### 1. Store links in SQLite

One file holds everything. Short codes are random, so nobody can guess the next one.

Uses: CLAUDE.md

Prompt:

```text
Read CLAUDE.md. Create shortener.py with a Store class over sqlite3: a links table (code primary key, url, created) and a clicks table (code, at, referrer, agent) with an index on (code, at). Add create(url, alias=None) that accepts only http and https URLs with a host (max 2048 chars), makes a random 6 character code with secrets.choice from letters and digits, and supports an optional alias of 3-32 letters, digits, - or _. Block reserved words (api, stats, static, admin, new) and raise on a taken alias. Use one lock around database calls so threads are safe.
```

Done when: in a Python shell, s = Store(":memory:") then s.create("https://example.com") returns a 6 character code, and s.create("javascript:alert(1)") raises ValueError

### 2. Create links with a JSON API

A tiny HTTP server built on http.server. Creating links can be locked with a token.

Uses: CLAUDE.md

Prompt:

```text
Add a ThreadingHTTPServer handler. POST /api/links takes JSON {"url", "alias"} and answers 201 with {code, url, created, short}. Bad JSON or a bad URL is 400 with {"error"}, a taken alias is 409, a body over 10 KB is 413. If the SHORTENER_TOKEN environment variable is set, require "Authorization: Bearer <token>" and answer 401 otherwise. Add --host, --port and --db flags (defaults 127.0.0.1, 8000, links.db).
```

Done when: curl -X POST localhost:8000/api/links -H "Content-Type: application/json" -d "{\"url\":\"https://python.org\"}" returns 201 with a short link

### 3. Redirect and count clicks

Visiting a short link sends a 302 redirect and saves one click: time, referring site and device type.

Uses: CLAUDE.md

Prompt:

```text
GET /<code> looks up the link and answers 302 with the Location header, Cache-Control no-store and Referrer-Policy no-referrer. Save a click with the UTC time, only the host name of the Referer header (never the full URL), and the device type from User-Agent: bot (bot, crawler, spider, preview, curl), mobile, desktop or other. Unknown codes get a friendly 404 page. HEAD works the same as GET.
```

Done when: opening the short link in a browser lands on the long URL, and a made-up code shows the 404 page

### 4. Add stats as JSON and a page

See total clicks, a 30 day series with empty days filled in, top referrers and device types.

Uses: CLAUDE.md

Prompt:

```text
Add stats(code) returning total, days (the last 30 UTC days, each {day, clicks}, zeros included), the top 10 referrers (empty referrer shown as "(direct)") and clicks per device type. Serve it at GET /api/links/<code>. Add GET /stats/<code>: an HTML page with the target URL, the total, a bar per day for the last 14 days and a referrer table. Escape every value with html.escape.
```

Done when: after three clicks /api/links/ shows total 3, and /stats/ shows the bars and the referrer table

### 5. Add the home page form

A plain form for people who do not want to use curl. It works on phones and in dark mode.

Uses: CLAUDE.md

Prompt:

```text
Serve GET / as a small HTML page with a URL field, an optional alias field (pattern 3-32 letters, digits, - or _) and a Shorten button. Submit with fetch to /api/links, then show the short link and a link to its stats page, or the error text, in an aria-live region. Build those links with DOM methods, not innerHTML. Use color-scheme light dark and a 16px side margin.
```

Done when: on a phone sized window the form makes a short link, shows it, and a taken alias shows "alias already taken"

### 6. Test everything offline

The tests start the server on a free port with a temporary database, so they need no network.

Uses: CLAUDE.md

Prompt:

```text
Add test_shortener.py using unittest only. Start make_server(temp_db, port=0) in a thread for each test. Test: create and redirect, bad URLs and bad JSON give 400, alias rules (taken is 409; too short, reserved and "../etc" are 400), three clicks with different Referer and User-Agent give the right total, referrers and device counts, unknown codes are 404, the stats page escapes a URL containing <script>, the token is enforced, and links survive a server restart.
```

Done when: python -m unittest passes all 8 tests

## Files

- CLAUDE.md: https://ownlyplans.com/bhaskar/link-shortener-with-click-stats-python-nothing-to-install/claude/ (Project rules for your AI tool. Put it in the empty project folder before step 1.)

## About

Build a small link shortener you fully own. Paste a long URL, get a short one like yoursite/launch, and see how many people clicked it, on which days, from which sites, and on phone or desktop. It has a JSON API (POST /api/links), a simple home page form, a stats page per link, custom aliases with reserved words blocked, an optional token so only you can create links, and safe HTML everywhere.

Who it is for: makers, newsletter writers and small teams who want branded short links and simple click counts without a paid service or tracking scripts.

What you need: Python 3.10 or newer and an AI coding tool. Nothing to install: it uses only Python's standard library (http.server, sqlite3, secrets, json, html). Data lives in one SQLite file.

How to use this plan: put CLAUDE.md (asset 1) in an empty folder, then paste each step's prompt into your AI tool in order. Check the "done when" line before moving on. We built and tested every step of this plan ourselves (8 automated tests plus a browser check).

---
Also available as JSON: https://ownlyplans.com/bhaskar/link-shortener-with-click-stats-python-nothing-to-install/?format=json
MCP server: https://ownlyplans.com/mcp
