update url shortener example. add click tracking. add api sub app.

Commit c517868 · patx · 2026-01-04T05:13:20-05:00

Changeset
c51786881518fc4092da3bf81f8d7d8118b15789
Parents
431d5fbb6feff81e9b6151fba088a70804b1313c

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/examples/url_shortener/Procfile b/examples/url_shortener/Procfile
new file mode 100644
index 0000000..278ec11
--- /dev/null
+++ b/examples/url_shortener/Procfile
@@ -0,0 +1,2 @@
+web: uvicorn main:app --host=0.0.0.0 --port=${PORT}
+
diff --git a/examples/url_shortener/main.py b/examples/url_shortener/main.py
index 2d7aef2..06ceb43 100644
--- a/examples/url_shortener/main.py
+++ b/examples/url_shortener/main.py
@@ -1,77 +1,229 @@
+"""
+URL Shortener using MicroPie and PyMongo. Live at https://erd.sh/
+
+
+Copyright 2025 Harrison Erd
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, 
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, 
+this list of conditions and the following disclaimer in the documentation 
+and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its 
+contributors may be used to endorse or promote products derived from this 
+software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
+OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
+OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
 from string import ascii_letters, digits
 from secrets import choice
+from datetime import datetime
 
 from micropie import App
-from mongokv import Mkv
+from pymongo import AsyncMongoClient, ReturnDocument
 
-# Import middlewares and session backends
 from middlewares.rate_limit import MongoRateLimitMiddleware
 from middlewares.csrf import CSRFMiddleware
+from middlewares.sub_app import SubAppMiddleware
 from sessions.mongo_session import MkvSessionBackend
 
 
-# EXAMPLE KEYS/URI, in production use/generate your own and save it as an 
-# environment variables, do not hard code them like these demos HINT: You 
-# can use `secrets.token_urlsafe(64)` to generate your CSRF secret key
-URL_ROOT = "http://localhost:8000/"
+URL_ROOT = "https://localhost:8000/"
 MONGO_URI = "mongodb://localhost:27017"
 DB_NAME = "shorty"
-CSRF_KEY = "wzWf0CsZr3LfrgPVc9RqHFVUmyXsYT-k8hnGt41bMGU"
-
-# Create an mongoKV instance using our URI
-db = Mkv(MONGO_URI, db_name=DB_NAME, collection_name="urls")
+COLLECTION = "urls"
+CSRF_KEY = "wzDf0CcZr3LgrgPVc2RqHFVUmyXsYT-k8kjGt41bMGU"
 
+mongo = AsyncMongoClient(MONGO_URI)
+urls = mongo[DB_NAME][COLLECTION]
 
-# Our main app class
+def _generate_id(length: int = 6) -> str:
+    return "".join(choice(ascii_letters + digits) for _ in range(length))
+        
 class Shorty(App):
 
-    def _generate_id(self, length: int = 8) -> str:
-        return "".join(choice(ascii_letters + digits) for _ in range(length))
-
     async def index(self, url_str: str | None = None):
         if url_str:
             if self.request.method == "POST":
-                while True:
-                    short_id = self._generate_id()
-                    try:
-                        await db.get(short_id)
-                    except KeyError:
-                        break
-
-                await db.set(short_id, url_str)
-                return await self._render_template(
-                    "success.html",
-                    url_id=url_str,
-                    short_id=short_id,
-                    url_root=URL_ROOT,
-                )
-
-            real_url = await db.get(url_str, "/")
-            if not isinstance(real_url, str) or not real_url.startswith(("http://", "https://")):
-                return self._redirect("/")
-            return self._redirect(real_url)
+                return await self._create_short_link(url_str)
+
+            if url_str.endswith("+"):
+                return await self._stats_page(url_str)
+
+            return await self._redirect_link(url_str)
 
         return await self._render_template("index.html", request=self.request)
 
+    async def _create_short_link(self, url_str: str):
+        if not isinstance(url_str, str) or not url_str.startswith(("http://", "https://")):
+            return self._redirect("/")
+
+        while True:
+            short_id = _generate_id()
+            exists = await urls.find_one({"_id": short_id})
+            if not exists:
+                break
+
+        await urls.insert_one({
+            "_id": short_id,
+            "url": url_str,
+            "clicks": 0,
+            "created_at": datetime.utcnow(),
+            "last_clicked_at": None,
+        })
+
+        return await self._render_template(
+            "success.html",
+            url_id=url_str,
+            short_id=short_id,
+            url_root=URL_ROOT,
+        )
+
+
+    async def _redirect_link(self, url_str: str):
+        doc = await urls.find_one_and_update(
+            {"_id": url_str},
+            {
+                "$inc": {"clicks": 1},
+                "$set": {"last_clicked_at": datetime.utcnow()},
+            },
+            return_document=ReturnDocument.AFTER,
+        )
+        if not doc:
+            return self._redirect("/")
+        return self._redirect(doc["url"])
+    
+
+    async def _stats_page(self, url_str: str):
+        short_code = url_str[:-1]
+        doc = await urls.find_one({"_id": short_code})
+        
+        if not doc:
+            return self._redirect("/")
+            
+        return await self._render_template(
+            "stats.html",
+            short_code=short_code,
+            url=doc.get("url"),
+            clicks=int(doc.get("clicks", 0)),
+            created_at=doc.get("created_at"),
+            last_clicked_at=doc.get("last_clicked_at"),
+            url_root=URL_ROOT,
+        )
+
+
+class ApiApp(App):
+
+    async def index(self):
+        return await self._render_template("api.html")
 
-app = Shorty(session_backend=MkvSessionBackend(
-    mongo_uri=MONGO_URI, 
-    db_name=DB_NAME
+    async def shorten(self):
+        if self.request.method != "POST":
+            return 405, {"error": "Method Not Allowed"}
+
+        data = self.request.get_json
+        url_str = data.get("url")
+
+        if not isinstance(url_str, str):
+            return 400, {"error": "Invalid JSON or missing 'url' key"}
+
+        if not url_str.startswith(("http://", "https://")):
+            return 400, {"error": "Invalid URL"}
+
+        while True:
+            short_id = _generate_id()
+            exists = await urls.find_one({"_id": short_id})
+            if not exists:
+                break
+
+        await urls.insert_one({
+            "_id": short_id,
+            "url": url_str,
+            "clicks": 0,
+            "created_at": datetime.utcnow(),
+            "last_clicked_at": None,
+        })
+
+        return {
+            "status": "success",
+            "long_url": url_str,
+            "short_id": short_id,
+            "short_url": f"{URL_ROOT}{short_id}",
+        }
+
+    async def stats(self, short_id: str):
+        if self.request.method != "GET":
+            return 405, {"error": "Method Not Allowed"}
+
+        if not isinstance(short_id, str) or not short_id:
+            return 400, {"error": "Missing short_id"}
+
+        doc = await urls.find_one({"_id": short_id})
+        if not doc:
+            return 404, {"error": "Not Found"}
+
+        created_at = doc.get("created_at")
+        last_clicked_at = doc.get("last_clicked_at")
+
+        return {
+            "status": "success",
+            "short_id": short_id,
+            "short_url": f"{URL_ROOT}{short_id}",
+            "long_url": doc.get("url"),
+            "clicks": int(doc.get("clicks", 0)),
+            "created_at": created_at.isoformat() + "Z" if created_at else None,
+            "last_clicked_at": last_clicked_at.isoformat() + "Z" if last_clicked_at else None,
+        }
+
+
+
+app = Shorty(
+    session_backend=MkvSessionBackend(
+        mongo_uri=MONGO_URI,
+        db_name=DB_NAME,
     )
 )
+
 app.middlewares.append(
     MongoRateLimitMiddleware(
         mongo_uri=MONGO_URI,
         db_name=DB_NAME,
-        allowed_hosts=None,          # don't enforce host allowlist, change in prod
-        trust_proxy_headers=False,   # change in prod
+        allowed_hosts=None,
+        trust_proxy_headers=False,
         require_cf_ray=False,
     )
 )
+
 app.middlewares.append(
     CSRFMiddleware(
         app=app,
-        secret_key=CSRF_KEY
+        secret_key=CSRF_KEY,
+        exempt_paths=[
+            "/api/v1/shorten",
+            "/api/v1/stats",
+        ],
     )
 )
 
+app.middlewares.append(
+    SubAppMiddleware(
+        mount_path="/api/v1",
+        subapp=ApiApp(),
+    )
+)
diff --git a/examples/url_shortener/middlewares/__init__.py b/examples/url_shortener/middlewares/__init__.py
index c5a3e14..deecfc7 100644
--- a/examples/url_shortener/middlewares/__init__.py
+++ b/examples/url_shortener/middlewares/__init__.py
@@ -1,4 +1,5 @@
 from .rate_limit import MongoRateLimitMiddleware
 from .csrf import CSRFMiddleware
+from .sub_app import SubAppMiddleware
 
-__all__ = ["MongoRateLimitMiddleware", "CSRFMiddleware"]
+__all__ = ["MongoRateLimitMiddleware", "CSRFMiddleware", "SubAppMiddlware"]
diff --git a/examples/url_shortener/middlewares/rate_limit.py b/examples/url_shortener/middlewares/rate_limit.py
index 29b93fd..4fd0fa6 100644
--- a/examples/url_shortener/middlewares/rate_limit.py
+++ b/examples/url_shortener/middlewares/rate_limit.py
@@ -2,32 +2,98 @@ from __future__ import annotations
 
 import ipaddress
 from datetime import datetime, timedelta
-from typing import Set
+from typing import Set, Iterable
 
 from pymongo import AsyncMongoClient, ReturnDocument
 from micropie import HttpMiddleware
 
 
+# ---------------------------------------------------------------------------
+# IP helpers
+# ---------------------------------------------------------------------------
+
 def _valid_ip(value: str | None) -> str | None:
+    """Parse and normalize an IP string, returning canonical string form or None."""
     try:
+        if not value:
+            return None
         return str(ipaddress.ip_address(value.strip()))
     except Exception:
         return None
 
 
+# Cloudflare IPv4 + IPv6 ranges
+# Source: https://www.cloudflare.com/ips/
+_CLOUDFLARE_IP_RANGES: list[str] = [
+    # IPv4
+    "173.245.48.0/20",
+    "103.21.244.0/22",
+    "103.22.200.0/22",
+    "103.31.4.0/22",
+    "141.101.64.0/18",
+    "108.162.192.0/18",
+    "190.93.240.0/20",
+    "188.114.96.0/20",
+    "197.234.240.0/22",
+    "198.41.128.0/17",
+    "162.158.0.0/15",
+    "104.16.0.0/13",
+    "104.24.0.0/14",
+    "172.64.0.0/13",
+    "131.0.72.0/22",
+    # IPv6
+    "2400:cb00::/32",
+    "2606:4700::/32",
+    "2803:f800::/32",
+    "2405:b500::/32",
+    "2405:8100::/32",
+    "2a06:98c0::/29",
+    "2c0f:f248::/32",
+]
+
+_CLOUDFLARE_NETWORKS = [ipaddress.ip_network(n) for n in _CLOUDFLARE_IP_RANGES]
+
+
+def _is_cloudflare_socket_ip(socket_ip: str | None) -> bool:
+    """
+    Returns True if the connecting socket IP belongs to Cloudflare's published ranges.
+    This is the key anti-spoof check before trusting CF/XFF headers.
+    """
+    try:
+        if not socket_ip:
+            return False
+        addr = ipaddress.ip_address(socket_ip)
+        return any(addr in net for net in _CLOUDFLARE_NETWORKS)
+    except Exception:
+        return False
+
+
+# ---------------------------------------------------------------------------
+# Middleware
+# ---------------------------------------------------------------------------
+
 class MongoRateLimitMiddleware(HttpMiddleware):
     """
-    Global MongoDB-based rate limiter (Heroku + Cloudflare safe).
+    Global MongoDB-based rate limiter with Cloudflare anti-spoofing.
 
-    - One document per client IP
+    - One document per client key (IP, optionally IP+route bucket)
     - Fixed window counter
     - Escalating temporary blocks
     - Permanent block based on 24h violation history
     - Fully atomic (single DB op per request)
     - PyMongo Async API (no Motor)
 
-    Requires:
-    - MongoDB 4.2+ (aggregation pipeline updates)
+    Security:
+    - Only trusts CF/XFF headers if:
+        1) trust_proxy_headers=True
+        2) (optionally) CF-Ray is present when require_cf_ray=True
+        3) the connecting socket IP (ASGI scope['client'][0]) is in Cloudflare IP ranges
+      Otherwise, proxy headers are ignored.
+
+    Notes:
+    - Host allow-list is a sanity check, NOT sufficient to prevent origin bypass if the
+      origin is publicly reachable.
+    - Best defense: make origin only reachable from Cloudflare (Tunnel/firewall).
     """
 
     # --- rate config ---
@@ -49,36 +115,56 @@ class MongoRateLimitMiddleware(HttpMiddleware):
         allowed_hosts: Set[str] | None = None,
         trust_proxy_headers: bool = True,
         require_cf_ray: bool = True,
+        # Optional: include METHOD+PATH in key (lets you set different limits by route)
+        bucket_by_route: bool = False,
+        # If we can't reliably identify the client, return 403 (recommended)
+        fail_closed: bool = True,
     ):
         self.client = AsyncMongoClient(mongo_uri)
         self.db = self.client[db_name]
         self.collection = self.db[collection_name]
 
-        # Security / proxy config
-        self.allowed_hosts = allowed_hosts or set()
+        self.allowed_hosts = set(h.lower() for h in (allowed_hosts or set()))
         self.trust_proxy_headers = trust_proxy_headers
         self.require_cf_ray = require_cf_ray
 
+        self.bucket_by_route = bucket_by_route
+        self.fail_closed = fail_closed
+
     # ---------------------------------------------------------
-    # Real client IP resolution (for Cloudflare + Heroku or similar setups)
+    # Client identification
     # ---------------------------------------------------------
 
-    def _client_ip(self, request) -> str:
+    def _host_allowed(self, headers: dict) -> bool:
+        if not self.allowed_hosts:
+            return True
+        host = (headers.get("host") or "").split(":", 1)[0].lower()
+        return bool(host) and host in self.allowed_hosts
+
+    def _socket_ip(self, request) -> str | None:
+        client = (request.scope or {}).get("client") or (None, 0)
+        return _valid_ip(client[0])
+
+    def _can_trust_proxy_headers(self, headers: dict, socket_ip: str | None) -> bool:
+        if not self.trust_proxy_headers:
+            return False
+        if self.require_cf_ray and not headers.get("cf-ray"):
+            return False
+        # Critical anti-spoof: only trust if the peer socket IP is Cloudflare
+        return _is_cloudflare_socket_ip(socket_ip)
+
+    def _client_ip(self, request) -> str | None:
         headers = getattr(request, "headers", {}) or {}
 
-        # 1) Optional host allow-list (prevents origin bypass)
-        if self.allowed_hosts:
-            host = (headers.get("host") or "").split(":", 1)[0].lower()
-            if host and host not in self.allowed_hosts:
-                return "unknown"
+        # Optional host sanity check
+        if not self._host_allowed(headers):
+            return None
 
-        # 2) Only trust proxy headers if allowed
-        can_trust = self.trust_proxy_headers
-        if can_trust and self.require_cf_ray:
-            can_trust = bool(headers.get("cf-ray"))
+        socket_ip = self._socket_ip(request)
+        can_trust = self._can_trust_proxy_headers(headers, socket_ip)
 
         if can_trust:
-            # Cloudflare headers (best)
+            # Cloudflare headers (preferred)
             for h in ("cf-connecting-ip", "true-client-ip"):
                 ip = _valid_ip(headers.get(h))
                 if ip:
@@ -96,9 +182,15 @@ class MongoRateLimitMiddleware(HttpMiddleware):
             if ip:
                 return ip
 
-        # 3) ASGI scope fallback (Heroku router)
-        client = request.scope.get("client") or ("unknown", 0)
-        return _valid_ip(client[0]) or "unknown"
+        # Untrusted path: fall back to socket peer IP (Heroku router, etc.)
+        return socket_ip
+
+    def _key(self, client_ip: str, request) -> str:
+        if not self.bucket_by_route:
+            return client_ip
+        method = (getattr(request, "method", None) or (request.scope or {}).get("method") or "GET").upper()
+        path = (request.scope or {}).get("path") or "/"
+        return f"{client_ip}|{method}|{path}"
 
     # ---------------------------------------------------------
     # Middleware hook
@@ -106,12 +198,19 @@ class MongoRateLimitMiddleware(HttpMiddleware):
 
     async def before_request(self, request):
         client_ip = self._client_ip(request)
+
+        # Avoid collapsing unknowns into a shared key (DoS vector)
+        if not client_ip:
+            if self.fail_closed:
+                return {"status_code": 403, "body": "Forbidden.", "headers": []}
+            return None  # fail open (not recommended)
+
         now = datetime.utcnow()
 
         window_start_cutoff = now - timedelta(seconds=self.WINDOW_SECONDS)
         perma_window_cutoff = now - timedelta(hours=self.PERMA_WINDOW_HOURS)
 
-        key = client_ip
+        key = self._key(client_ip, request)
 
         doc = await self.collection.find_one_and_update(
             {"_id": key},
@@ -161,7 +260,7 @@ class MongoRateLimitMiddleware(HttpMiddleware):
                     }
                 },
 
-                # 4) Update window/count atomically (only if not blocked)
+                # 4) Update window/count (only if not blocked)
                 {
                     "$set": {
                         "_window_expired": {
diff --git a/examples/url_shortener/middlewares/sub_app.py b/examples/url_shortener/middlewares/sub_app.py
new file mode 100644
index 0000000..d7c1008
--- /dev/null
+++ b/examples/url_shortener/middlewares/sub_app.py
@@ -0,0 +1,18 @@
+from micropie import HttpMiddleware
+
+class SubAppMiddleware(HttpMiddleware):
+    def __init__(self, mount_path: str, subapp):
+        self.mount_path = mount_path.lstrip("/")
+        self.subapp = subapp
+
+    async def before_request(self, request):
+        path = request.scope["path"].lstrip("/")
+        if path.startswith(self.mount_path):
+            request._subapp = self.subapp
+            request._subapp_path = path[len(self.mount_path):].lstrip("/") or "/"
+            request._subapp_mount_path = self.mount_path
+        return None
+
+    async def after_request(self, request, status_code, response_body, extra_headers):
+        return None
+
diff --git a/examples/url_shortener/requirements.txt b/examples/url_shortener/requirements.txt
new file mode 100644
index 0000000..a19382a
--- /dev/null
+++ b/examples/url_shortener/requirements.txt
@@ -0,0 +1,4 @@
+micropie[all]
+mongokv
+itsdangerous
+
diff --git a/examples/url_shortener/templates/api.html b/examples/url_shortener/templates/api.html
new file mode 100644
index 0000000..7521c7f
--- /dev/null
+++ b/examples/url_shortener/templates/api.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>API Documentation</title>
+
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
+
+<style>
+  :root{
+    color-scheme: dark;
+    --bg: #070a0f;
+    --card: rgba(255,255,255,.06);
+    --border: rgba(255,255,255,.10);
+    --text: rgba(255,255,255,.92);
+    --muted: rgba(255,255,255,.60);
+    --focus: rgba(255,255,255,.22);
+    --shadow: 0 18px 55px rgba(0,0,0,.55);
+    --radius: 16px;
+  }
+
+  *{ box-sizing:border-box; margin:0; padding:0; }
+
+  body{
+    min-height:100vh;
+    display:grid;
+    place-items:center;
+    font-family:"JetBrains Mono", ui-monospace, monospace;
+    background:
+      radial-gradient(900px 500px at 20% 15%, rgba(255,255,255,.05), transparent 55%),
+      radial-gradient(800px 500px at 80% 85%, rgba(255,255,255,.04), transparent 55%),
+      var(--bg);
+    color:var(--text);
+    padding:24px;
+  }
+
+  .card{
+    width:100%;
+    max-width:680px;
+    padding:22px;
+  }
+
+  h1{
+    font-size:1.05rem;
+    font-weight:600;
+    margin-bottom:12px;
+  }
+
+  .section{
+    margin-top:18px;
+  }
+
+  .label{
+    font-size:.75rem;
+    color:var(--muted);
+    margin-bottom:6px;
+  }
+
+  pre{
+    background:rgba(0,0,0,.35);
+    border:1px solid var(--border);
+    border-radius:12px;
+    padding:14px;
+    font-size:.85rem;
+    overflow-x:auto;
+    line-height:1.5;
+  }
+
+  .note{
+    font-size:.75rem;
+    color:var(--muted);
+    margin-top:10px;
+  }
+
+  a{
+    color:var(--muted);
+    text-decoration:none;
+  }
+
+  a:hover{
+    color:var(--text);
+  }
+</style>
+</head>
+
+<body>
+  <main class="card">
+    <h1>Create short link</h1>
+
+        <div class="section">
+            <pre>
+POST /api/v1/shorten
+Content-Type: application/json
+
+{
+  "url": "https://google.com"
+}
+            </pre>
+        </div>
+        
+        <div class="section">
+            <div class="label">Response</div>
+            <pre>
+{
+  "status": "success",
+  "long_url": "https://harrisonerd.com",
+  "short_id": "Ab3kQ9",
+  "short_url": "https://erd.sh/Ab3kQ9"
+}
+            </pre>
+        </div>
+
+      <h1 style="margin-top:48px;">Link stats</h1>
+      <div class="section">
+        <pre>GET /api/v1/stats/Ab3kQ9</pre>
+      </div>
+      
+      <div class="section">
+        <div class="label">Response</div>
+        <pre>
+{
+  "status": "success",
+  "short_id": "Ab3kQ9",
+  "short_url": "https://erd.sh/Ab3kQ9",
+  "long_url": "https://harrisonerd.com",
+  "clicks": 12,
+  "created_at": "2026-01-04T08:41:12Z",
+  "last_clicked_at": "2026-01-04T09:02:55Z"
+}
+        </pre>
+      </div>
+
+    <div class="note" style="margin-top:48px;">
+      <a href="/">&larr; Back</a>
+    </div>
+
+  </main>
+</body>
+</html>
+
diff --git a/examples/url_shortener/templates/index.html b/examples/url_shortener/templates/index.html
index 9cb2d1a..c3a7bbf 100644
--- a/examples/url_shortener/templates/index.html
+++ b/examples/url_shortener/templates/index.html
@@ -1,64 +1,158 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-<meta charset="UTF-8">
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<title>Shorten URL - Shorty</title>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>Shorten URL</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
 <style>
-* {
-  margin: 0;
-  padding: 0;
-  box-sizing: border-box;
-}
+  :root{
+    color-scheme: dark;
+    --bg: #070a0f;
+    --card: rgba(255,255,255,.06);
+    --card2: rgba(255,255,255,.04);
+    --border: rgba(255,255,255,.10);
+    --text: rgba(255,255,255,.92);
+    --muted: rgba(255,255,255,.60);
+    --focus: rgba(255,255,255,.22);
+    --shadow: 0 18px 55px rgba(0,0,0,.55);
+    --radius: 16px;
+  }
 
-body {
-  height: 100vh;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-family: system-ui, -apple-system, sans-serif;
-  background:
-    radial-gradient(1200px 600px at 15% 20%, rgba(99,102,241,.25), transparent 60%),
-    radial-gradient(900px 500px at 85% 75%, rgba(34,197,94,.18), transparent 55%),
-    linear-gradient(180deg, #070b14, #0b1220 35%, #070b14);
-}
+  * { box-sizing: border-box; margin: 0; padding: 0; }
 
-form {
-  width: 90%;
-  max-width: 600px;
-  display: flex;
-  flex-direction: column;
-  gap: 1rem;
-}
+  body{
+    min-height: 100vh;
+    display: grid;
+    place-items: center;
+    font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+    background:
+      radial-gradient(900px 500px at 20% 15%, rgba(255,255,255,.05), transparent 55%),
+      radial-gradient(800px 500px at 80% 85%, rgba(255,255,255,.04), transparent 55%),
+      var(--bg);
+    color: var(--text);
+    padding: 24px;
+  }
 
-input {
-  padding: 1rem;
-  font-size: 1.1rem;
-  border: 2px solid #ddd;
-  border-radius: 8px;
-}
+  .card{
+    width: 100%;
+    max-width: 640px;
+    padding: 22px;
+  }
 
-button {
-  padding: 1rem;
-  font-size: 1.1rem;
-  background: linear-gradient(135deg, rgba(99,102,241,.95), rgba(34,197,94,.75));
-  color: #fff;
-  border: none;
-  border-radius: 8px;
-  cursor: pointer;
-}
+  h1{
+    font-size: 1.05rem;
+    letter-spacing: .2px;
+    font-weight: 600;
+    margin-bottom: 6px;
+  }
+
+  p{
+    font-size: .95rem;
+    color: var(--muted);
+    line-height: 1.35;
+  }
+
+  form{
+    display: flex;
+    gap: 10px;
+    margin-top: 16px;
+  }
+
+  .field{
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+  }
+
+  input[type="url"]{
+    width: 100%;
+    padding: 12px 14px;
+    font-size: 1rem;
+    color: var(--text);
+    background: rgba(0,0,0,.30);
+    border: 1px solid var(--border);
+    border-radius: 12px;
+    outline: none;
+    transition: border-color .15s ease, box-shadow .15s ease;
+    font-family: inherit;
+  }
 
-button:hover{
-  filter: brightness(1.06);
-  box-shadow: 0 16px 42px rgba(0,0,0,.42);
+  input[type="url"]::placeholder{
+    color: rgba(255,255,255,.38);
+  }
+
+  input[type="url"]:focus{
+    border-color: var(--focus);
+    box-shadow: 0 0 0 4px rgba(255,255,255,.06);
+  }
+
+  button{
+    flex: 0 0 auto;
+    padding: 12px 16px;
+    font-size: 0.98rem;
+    font-weight: 600;
+    color: var(--text);
+    background: rgba(255,255,255,.10);
+    border: 1px solid var(--border);
+    border-radius: 12px;
+    cursor: pointer;
+    transition: transform .08s ease, background .15s ease, border-color .15s ease;
+    user-select: none;
+    white-space: nowrap;
+    font-family: inherit;
+  }
+
+  button:hover{
+    background: rgba(255,255,255,.14);
+    border-color: rgba(255,255,255,.14);
+  }
+
+  button:active{
+    transform: scale(.98);
+  }
+  
+  .below{
+    position: fixed;
+    bottom: 18px;
+    left: 50%;
+    transform: translateX(-50%);
+    text-align: center;
 }
+
+  .back-link{
+    display: inline-block;
+    font-size: .9rem;
+    color: var(--muted);
+    text-decoration: none;
+  }
+
+  .back-link:hover{
+    color: var(--text);
+  }
+  
+  /* mobile: stack */
+  @media (max-width: 560px){
+    form{ flex-direction: column; }
+    button{ width: 100%; }
+  }
 </style>
 </head>
+
 <body>
-<form method="POST" action="/">
-  <input type="url" name="url_str" placeholder="https://yourlongurl.com/here" required>
-  <input type="hidden" name="csrf_token" value="{{ request.session.csrf_token }}">
-  <button type="submit">Shorten</button>
-</form>
+  <main class="card">
+    <form method="POST" action="/">
+        <input id="url_str" type="url" name="url_str" placeholder="https://example.com/something/long" required autocomplete="off" autofocus/>
+      <input type="hidden" name="csrf_token" value="{{ request.session.csrf_token }}">
+      <button type="submit">Shorten</button>
+    </form>
+    <div class="below">
+      <a href="https://harrisonerd.com" target="_blank" class="back-link">erd.sh</a> &middot; <a href="/api/v1" class="back-link">API Docs (v1)</a>
+    </div>
+  </main>
 </body>
 </html>
+
diff --git a/examples/url_shortener/templates/stats.html b/examples/url_shortener/templates/stats.html
new file mode 100644
index 0000000..86c16f4
--- /dev/null
+++ b/examples/url_shortener/templates/stats.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>{{ url }}</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
+<style>
+  :root{
+    color-scheme: dark;
+    --bg: #070a0f;
+    --card: rgba(255,255,255,.06);
+    --card2: rgba(255,255,255,.04);
+    --border: rgba(255,255,255,.10);
+    --text: rgba(255,255,255,.92);
+    --muted: rgba(255,255,255,.60);
+    --focus: rgba(255,255,255,.22);
+    --shadow: 0 18px 55px rgba(0,0,0,.55);
+    --radius: 16px;
+  }
+
+  * { box-sizing: border-box; margin: 0; padding: 0; }
+
+  body{
+    min-height: 100vh;
+    display: grid;
+    place-items: center;
+    font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+    background:
+      radial-gradient(900px 500px at 20% 15%, rgba(255,255,255,.05), transparent 55%),
+      radial-gradient(800px 500px at 80% 85%, rgba(255,255,255,.04), transparent 55%),
+      var(--bg);
+    color: var(--text);
+    padding: 24px;
+  }
+
+  .card{
+    width: 100%;
+    max-width: 640px;
+    padding: 22px;
+  }
+
+  form{
+    display: flex;
+    gap: 10px;
+    margin-top: 0;
+  }
+
+  input[type="text"]{
+    width: 100%;
+    padding: 12px 14px;
+    font-size: 1rem;
+    color: var(--text);
+    background: rgba(0,0,0,.30);
+    border: 1px solid var(--border);
+    border-radius: 12px;
+    outline: none;
+    transition: border-color .15s ease, box-shadow .15s ease;
+    font-family: inherit;
+  }
+
+  input[type="text"]::placeholder{
+    color: rgba(255,255,255,.38);
+  }
+
+  input[type="text"]:focus{
+    border-color: var(--focus);
+    box-shadow: 0 0 0 4px rgba(255,255,255,.06);
+  }
+
+  button{
+    flex: 0 0 auto;
+    padding: 12px 16px;
+    font-size: 0.98rem;
+    font-weight: 600;
+    color: var(--text);
+    background: rgba(255,255,255,.10);
+    border: 1px solid var(--border);
+    border-radius: 12px;
+    cursor: pointer;
+    transition: transform .08s ease, background .15s ease, border-color .15s ease;
+    user-select: none;
+    white-space: nowrap;
+    font-family: inherit;
+  }
+
+  button:hover{
+    background: rgba(255,255,255,.14);
+    border-color: rgba(255,255,255,.14);
+  }
+
+  button:active{
+    transform: scale(.98);
+  }
+
+  .below{
+    margin-top: 14px;
+    text-align: center;
+  }
+
+  .back-link{
+    display: inline-block;
+    font-size: .9rem;
+    color: var(--muted);
+    text-decoration: none;
+    max-width: 100%;
+    word-break: break-word;
+    overflow-wrap: anywhere;
+    white-space: normal;
+  }
+
+  .clicks{
+    display: inline-block;
+    font-size: .9rem;
+  }
+  
+  .back-link:hover{
+    color: var(--text);
+  }
+
+  /* mobile: stack */
+  @media (max-width: 560px){
+    form{ flex-direction: column; }
+    button{ width: 100%; }
+  }
+</style>
+</head>
+
+<body>
+  <main class="card">
+    <form onsubmit="return false;">
+      <input
+        id="shortUrl"
+        type="text"
+        value="{{ url_root }}{{ short_code }}"
+        readonly
+        aria-label="Short URL"
+      />
+      <button type="button" id="copyBtn" onclick="copyUrl()">Copy</button>
+    </form>
+    <br>
+    <a href="{{ url }}" class="back-link" target="_blank" rel="noreferrer">{{ url }}</a>
+    <div class="below">
+      <a href="/" class="back-link">&larr; Shorten another</a> &middot; <span class="clicks">{{ clicks }} clicks</span>
+    </div>
+  </main>
+
+<script>
+function copyUrl(){
+  const input = document.getElementById('shortUrl');
+  const btn = document.getElementById('copyBtn');
+
+  input.focus();
+  input.select();
+
+  const text = input.value;
+  if (navigator.clipboard && window.isSecureContext) {
+    navigator.clipboard.writeText(text).then(() => {
+      btn.textContent = 'Copied';
+      setTimeout(() => btn.textContent = 'Copy', 1800);
+    }).catch(() => fallbackCopy(text, btn));
+  } else {
+    fallbackCopy(text, btn);
+  }
+}
+
+function fallbackCopy(text, btn){
+  try{
+    const ta = document.createElement('textarea');
+    ta.value = text;
+    ta.setAttribute('readonly', '');
+    ta.style.position = 'absolute';
+    ta.style.left = '-9999px';
+    document.body.appendChild(ta);
+    ta.select();
+    document.execCommand('copy');
+    document.body.removeChild(ta);
+
+    btn.textContent = 'Copied';
+    setTimeout(() => btn.textContent = 'Copy', 1800);
+  } catch(e){
+    btn.textContent = 'Copy failed';
+    setTimeout(() => btn.textContent = 'Copy', 1800);
+  }
+}
+</script>
+</body>
+</html>
diff --git a/examples/url_shortener/templates/success.html b/examples/url_shortener/templates/success.html
index 393c336..90d3236 100644
--- a/examples/url_shortener/templates/success.html
+++ b/examples/url_shortener/templates/success.html
@@ -1,98 +1,181 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-<meta charset="UTF-8">
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<title>URL Shortened - Shorty</title>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>URL Shortened</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
 <style>
-* {
-  margin: 0;
-  padding: 0;
-  box-sizing: border-box;
-}
+  :root{
+    color-scheme: dark;
+    --bg: #070a0f;
+    --card: rgba(255,255,255,.06);
+    --card2: rgba(255,255,255,.04);
+    --border: rgba(255,255,255,.10);
+    --text: rgba(255,255,255,.92);
+    --muted: rgba(255,255,255,.60);
+    --focus: rgba(255,255,255,.22);
+    --shadow: 0 18px 55px rgba(0,0,0,.55);
+    --radius: 16px;
+  }
 
-body {
-  height: 100vh;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-family: system-ui, -apple-system, sans-serif;
-  background:
-    radial-gradient(1200px 600px at 15% 20%, rgba(99,102,241,.25), transparent 60%),
-    radial-gradient(900px 500px at 85% 75%, rgba(34,197,94,.18), transparent 55%),
-    linear-gradient(180deg, #070b14, #0b1220 35%, #070b14);
-}
+  * { box-sizing: border-box; margin: 0; padding: 0; }
 
-.container {
-  width: 90%;
-  max-width: 600px;
-  text-align: center;
-}
+  body{
+    min-height: 100vh;
+    display: grid;
+    place-items: center;
+    font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+    background:
+      radial-gradient(900px 500px at 20% 15%, rgba(255,255,255,.05), transparent 55%),
+      radial-gradient(800px 500px at 80% 85%, rgba(255,255,255,.04), transparent 55%),
+      var(--bg);
+    color: var(--text);
+    padding: 24px;
+  }
 
-h1 {
-  font-size: 2rem;
-  margin-bottom: 2rem;
-}
+  .card{
+    width: 100%;
+    max-width: 640px;
+    padding: 22px;
+  }
 
-.url-box {
-  padding: 1.5rem;
-  background: #f5f5f5;
-  border-radius: 8px;
-  margin-bottom: 1rem;
-}
+  form{
+    display: flex;
+    gap: 10px;
+    margin-top: 0;
+  }
 
-.short-url {
-  font-size: 1.3rem;
-  font-weight: bold;
-  word-break: break-all;
-}
+  input[type="text"]{
+    width: 100%;
+    padding: 12px 14px;
+    font-size: 1rem;
+    color: var(--text);
+    background: rgba(0,0,0,.30);
+    border: 1px solid var(--border);
+    border-radius: 12px;
+    outline: none;
+    transition: border-color .15s ease, box-shadow .15s ease;
+    font-family: inherit;
+  }
 
-button {
-  padding: 1rem;
-  font-size: 1.1rem;
-  background: linear-gradient(135deg, rgba(99,102,241,.95), rgba(34,197,94,.75));
-  color: #fff;
-  border: none;
-  border-radius: 8px;
-  cursor: pointer;
-}
+  input[type="text"]::placeholder{
+    color: rgba(255,255,255,.38);
+  }
 
-button:hover{
-  filter: brightness(1.06);
-  box-shadow: 0 16px 42px rgba(0,0,0,.42);
-}
+  input[type="text"]:focus{
+    border-color: var(--focus);
+    box-shadow: 0 0 0 4px rgba(255,255,255,.06);
+  }
 
-.back-link {
-  display: inline-block;
-  margin-top: 2rem;
-  color: cyan;
-  text-decoration: none;
-}
+  button{
+    flex: 0 0 auto;
+    padding: 12px 16px;
+    font-size: 0.98rem;
+    font-weight: 600;
+    color: var(--text);
+    background: rgba(255,255,255,.10);
+    border: 1px solid var(--border);
+    border-radius: 12px;
+    cursor: pointer;
+    transition: transform .08s ease, background .15s ease, border-color .15s ease;
+    user-select: none;
+    white-space: nowrap;
+    font-family: inherit;
+  }
 
-.back-link:hover {
-  color: #1E90FF;
-}
+  button:hover{
+    background: rgba(255,255,255,.14);
+    border-color: rgba(255,255,255,.14);
+  }
+
+  button:active{
+    transform: scale(.98);
+  }
+
+  .below{
+    margin-top: 14px;
+    text-align: center;
+  }
+
+  .back-link{
+    display: inline-block;
+    font-size: .9rem;
+    color: var(--muted);
+    text-decoration: none;
+  }
+
+  .back-link:hover{
+    color: var(--text);
+  }
+
+  /* mobile: stack */
+  @media (max-width: 560px){
+    form{ flex-direction: column; }
+    button{ width: 100%; }
+  }
 </style>
 </head>
+
 <body>
-<div class="container">
-  <div class="url-box">
-    <div class="short-url" id="shortUrl">{{ url_root }}{{ short_id }}</div>
-  </div>
-    <button class="copy-btn" onclick="copyUrl()">Copy Link</button>
-  <br>
-  <a href="/" class="back-link">← Shorten another</a>
-</div>
+  <main class="card">
+    <form onsubmit="return false;">
+      <input
+        id="shortUrl"
+        type="text"
+        value="{{ url_root }}{{ short_id }}"
+        readonly
+        aria-label="Short URL"
+      />
+      <button type="button" id="copyBtn" onclick="copyUrl()">Copy</button>
+    </form>
+
+    <div class="below">
+      <a href="/" class="back-link">&larr; Shorten another</a> &middot; <a href="/{{ short_id}}+" class="back-link">Analytics</a>
+    </div>
+  </main>
 
 <script>
-function copyUrl() {
-  const url = document.getElementById('shortUrl').textContent;
-  navigator.clipboard.writeText(url).then(() => {
-    const btn = document.querySelector('.copy-btn');
-    btn.textContent = 'Copied!';
-    setTimeout(() => btn.textContent = 'Copy Link', 2000);
-  });
+function copyUrl(){
+  const input = document.getElementById('shortUrl');
+  const btn = document.getElementById('copyBtn');
+
+  input.focus();
+  input.select();
+
+  const text = input.value;
+  if (navigator.clipboard && window.isSecureContext) {
+    navigator.clipboard.writeText(text).then(() => {
+      btn.textContent = 'Copied';
+      setTimeout(() => btn.textContent = 'Copy', 1800);
+    }).catch(() => fallbackCopy(text, btn));
+  } else {
+    fallbackCopy(text, btn);
+  }
+}
+
+function fallbackCopy(text, btn){
+  try{
+    const ta = document.createElement('textarea');
+    ta.value = text;
+    ta.setAttribute('readonly', '');
+    ta.style.position = 'absolute';
+    ta.style.left = '-9999px';
+    document.body.appendChild(ta);
+    ta.select();
+    document.execCommand('copy');
+    document.body.removeChild(ta);
+
+    btn.textContent = 'Copied';
+    setTimeout(() => btn.textContent = 'Copy', 1800);
+  } catch(e){
+    btn.textContent = 'Copy failed';
+    setTimeout(() => btn.textContent = 'Copy', 1800);
+  }
 }
 </script>
 </body>
 </html>
+