clone

a tiny url shortener with four-character slugs, backed by a kv store

main
HexAsAService / main.ts
main.ts 11.8K
1import { Application, Status } from "jsr:@oak/oak/";2import { Router } from "jsr:@oak/oak/router";34const kv = await Deno.openKv();5const slugPattern = /^[0-9a-f]{4}$/i;6const maxSlugAttempts = 8;78const escapeHtml = (value: string): string =>9 value10 .replaceAll("&", "&amp;")11 .replaceAll("<", "&lt;")12 .replaceAll(">", "&gt;")13 .replaceAll('"', "&quot;")14 .replaceAll("'", "&#39;");1516const renderHomePage = (shortURL?: string): string => {17 const safeShortURL = shortURL ? escapeHtml(shortURL) : "";18 const resultSection = shortURL19 ? `20 <section class="result-card" aria-live="polite">21 <p class="result-label">Rune Ready</p>22 <a href="${safeShortURL}" class="result-link">${safeShortURL}</a>23 <button type="button" class="copy-button" data-copy="${safeShortURL}">Copy Portal</button>24 </section>25 `26 : "";2728 return `<!DOCTYPE html>29<html lang="en">30<head>31 <meta charset="UTF-8">32 <meta name="viewport" content="width=device-width, initial-scale=1.0">33 <title>Hex As a Service</title>34 <style>35 @import url("https://fonts.googleapis.com/css2?family=Cinzel:wght@600;800&family=VT323&display=swap");3637 :root {38 --sky-top: #f3dfb6;39 --sky-mid: #f0c980;40 --sand-base: #d8a25f;41 --sand-deep: #b87a39;42 --card-bg: #f6e1b4;43 --card-border: #7a4b20;44 --ink: #4b290f;45 --muted: #775029;46 --input-bg: #f3d9ab;47 --button-bg: #8d4f23;48 --button-hover: #6f3d1b;49 --rune-glow: #d0802a;50 --focus-ring: rgba(208, 128, 42, 0.35);51 --shadow-color: rgba(92, 50, 19, 0.3);52 --success-bg: rgba(208, 128, 42, 0.18);53 --success-border: #a75e25;54 }5556 * {57 box-sizing: border-box;58 margin: 0;59 padding: 0;60 }6162 html, body {63 height: 100%;64 }6566 body {67 font-family: "VT323", monospace;68 background:69 radial-gradient(circle at 15% 18%, rgba(255, 250, 221, 0.75) 0 10%, transparent 11%),70 linear-gradient(180deg, var(--sky-top) 0%, var(--sky-mid) 48%, var(--sand-base) 49%, var(--sand-deep) 100%);71 color: var(--ink);72 display: flex;73 align-items: center;74 justify-content: center;75 padding: 1rem;76 position: relative;77 }7879 body::after {80 content: "";81 position: fixed;82 inset: 0;83 pointer-events: none;84 background-image: repeating-linear-gradient(85 135deg,86 rgba(122, 75, 32, 0.06) 0 4px,87 transparent 4px 8px88 );89 opacity: 0.6;90 }9192 .form-container {93 width: 100%;94 max-width: 560px;95 background-color: var(--card-bg);96 padding: 2.5rem;97 border-radius: 0;98 border: 4px solid var(--card-border);99 box-shadow:100 0 0 0 4px rgba(241, 191, 112, 0.9),101 10px 10px 0 var(--card-border),102 0 14px 36px var(--shadow-color);103 text-align: center;104 position: relative;105 z-index: 1;106 }107108 .form-container::before {109 content: "☀ FelineStateMachine";110 position: absolute;111 top: -0.9rem;112 left: 50%;113 transform: translateX(-50%);114 background: var(--card-border);115 color: #f7dfac;116 padding: 0.2rem 0.8rem;117 font-size: 1.1rem;118 letter-spacing: 0.04em;119 }120121 h1 {122 font-family: "Cinzel", serif;123 font-size: clamp(1.7rem, 4vw, 2.25rem);124 font-weight: 800;125 margin: 0.75rem 0 0.5rem;126 color: var(--card-border);127 letter-spacing: 0.03em;128 text-transform: uppercase;129 text-shadow: 2px 2px 0 rgba(255, 236, 180, 0.75);130 }131132 p {133 margin-bottom: 1.75rem;134 color: var(--muted);135 font-size: 1.25rem;136 }137138 form {139 display: flex;140 flex-direction: column;141 gap: 1rem;142 }143144 .url-input {145 width: 100%;146 padding: 0.95rem;147 border: 3px solid var(--card-border);148 border-radius: 0;149 background-color: var(--input-bg);150 font-size: 1.35rem;151 color: var(--ink);152 transition: border-color 0.2s ease, box-shadow 0.2s ease;153 font-family: inherit;154 }155156 .url-input::placeholder {157 color: var(--muted);158 }159160 .url-input:focus {161 outline: none;162 border-color: var(--button-bg);163 box-shadow: 0 0 0 4px var(--focus-ring);164 }165 166 .submit-button {167 width: 100%;168 padding: 0.9rem;169 border: 3px solid #4d2a12;170 border-radius: 0;171 background:172 linear-gradient(180deg, #a9632f 0%, var(--button-bg) 70%);173 color: #f8e4bf;174 font-size: 1.25rem;175 font-weight: 700;176 letter-spacing: 0.05em;177 text-transform: uppercase;178 cursor: pointer;179 transition: filter 0.18s ease, transform 0.1s ease;180 font-family: "Cinzel", serif;181 box-shadow: 4px 4px 0 #4d2a12;182 }183184 .submit-button:hover {185 filter: brightness(1.08);186 background:187 linear-gradient(180deg, #935123 0%, var(--button-hover) 70%);188 }189190 .submit-button:active {191 transform: scale(0.98);192 box-shadow: 2px 2px 0 #4d2a12;193 }194195 .submit-button {196 display: none;197 }198199 .url-input:not(:placeholder-shown) + .submit-button {200 display: block;201 }202203 .result-card {204 margin-top: 1.5rem;205 border: 3px solid var(--success-border);206 background: var(--success-bg);207 border-radius: 0;208 padding: 1rem;209 display: flex;210 flex-direction: column;211 gap: 0.75rem;212 animation: rise-in 180ms ease-out;213 box-shadow: inset 0 0 0 1px rgba(255, 231, 168, 0.8);214 }215216 .result-label {217 margin: 0;218 color: var(--card-border);219 font-size: 1rem;220 font-weight: 700;221 text-transform: uppercase;222 letter-spacing: 0.06em;223 }224225 .result-link {226 color: var(--ink);227 word-break: break-all;228 font-weight: 600;229 text-decoration: none;230 font-size: 1.15rem;231 }232233 .result-link:hover {234 text-decoration: underline;235 }236237 .copy-button {238 border: 2px solid var(--success-border);239 border-radius: 0;240 background: #f7dfb2;241 color: var(--ink);242 font-size: 1.15rem;243 font-weight: 600;244 padding: 0.55rem 0.85rem;245 cursor: pointer;246 font-family: inherit;247 }248249 .copy-button:hover {250 border-color: var(--button-bg);251 color: #321b0d;252 background: #f5d69e;253 }254255 @keyframes rise-in {256 from {257 opacity: 0;258 transform: translateY(6px);259 }260 to {261 opacity: 1;262 transform: translateY(0);263 }264 }265266 @media (max-width: 560px) {267 .form-container {268 padding: 2.2rem 1rem 1.4rem;269 box-shadow:270 0 0 0 4px rgba(241, 191, 112, 0.9),271 6px 6px 0 var(--card-border),272 0 10px 24px var(--shadow-color);273 }274275 p {276 font-size: 1.1rem;277 }278 }279 </style>280</head>281<body>282283 <div class="form-container">284 <h1>Hex as a Service</h1>285 <p>Cast a travel rune for any link. Four glyphs, quick portal.</p>286 287 <form action="/new" method="post">288 <input 289 type="url"290 name="url" 291 class="url-input" 292 placeholder="https://your-quest.example" 293 autocomplete="url"294 required295 >296 <button type="submit" class="submit-button">Carve Rune</button>297 </form>298 ${resultSection}299 </div>300301 <script>302 const copyButton = document.querySelector(".copy-button");303 if (copyButton) {304 copyButton.addEventListener("click", async () => {305 const shortURL = copyButton.getAttribute("data-copy");306 if (!shortURL) return;307308 try {309 await navigator.clipboard.writeText(shortURL);310 copyButton.textContent = "Rune Copied";311 setTimeout(() => {312 copyButton.textContent = "Copy Portal";313 }, 1200);314 } catch {315 copyButton.textContent = "Copy Failed";316 setTimeout(() => {317 copyButton.textContent = "Copy Portal";318 }, 1200);319 }320 });321 }322 </script>323324</body>325</html>326`;327};328329const router = new Router();330router.get("/", (ctx) => {331 const hex = ctx.request.url.searchParams.get("hex");332 const shortURL = hex && slugPattern.test(hex)333 ? `${ctx.request.url.origin}/${hex.toLowerCase()}`334 : undefined;335 ctx.response.body = renderHomePage(shortURL);336});337338router.post("/new", async (ctx) => {339 if (!ctx.request.hasBody) {340 ctx.response.status = Status.BadRequest;341 ctx.response.body = { error: "Spell, Hex, requires a target." };342 return;343 }344 const formData = await ctx.request.body.formData();345 const longURL = formData.get("url");346 if (!URL.canParse(longURL)) {347 console.error(`[UnprocessableEntity] ${Date.now()}`);348 ctx.response.status = Status.UnprocessableEntity;349 ctx.response.body = { error: "The hex fizzles on non-urls." };350 return;351 }352353 let slug: string | undefined;354 for (let attempt = 0; attempt < maxSlugAttempts; attempt++) {355 const candidate = crypto.randomUUID().substring(0, 4);356 const existing = await kv.get(["urls", candidate]);357 if (existing.value === null) {358 slug = candidate;359 break;360 }361 }362363 if (!slug) {364 ctx.response.status = Status.ServiceUnavailable;365 ctx.response.body = { error: "The hex grid is saturated. Try again." };366 return;367 }368369 await kv.set(["urls", slug], longURL);370371 console.log(`[Created] ${slug.padStart(6)} ${Date.now()}`);372373 const preferredType = ctx.request.accepts("json", "html");374 if (preferredType === "json") {375 ctx.response.headers.set("Location", `/${slug}`);376 ctx.response.status = Status.Created;377 ctx.response.body = { URL: longURL, hex: slug };378 return;379 }380381 ctx.response.headers.set("Location", `/?hex=${slug}`);382 ctx.response.status = Status.SeeOther;383});384385router.get("/:slug", async (ctx) => {386 const slug = ctx.params.slug;387 if (slug.length !== 4 || !slugPattern.test(slug)) {388 console.error(`[BadRequest] ${Date.now()}`);389 ctx.response.status = Status.BadRequest;390 ctx.response.body = {391 error: "You ain't gotta do all that. Hexes that big don't exist!",392 };393 return;394 }395396 console.log(`[Requested] ${slug.padStart(6)} ${Date.now()}`);397398 const entry = await kv.get(["urls", slug]);399 if (entry.value === null) {400 console.warn(`[NotFound] ${slug.padStart(6)} ${Date.now()}`);401 ctx.response.status = Status.NotFound;402 ctx.response.body = { error: "Your URL is in another castle." };403 return;404 }405406 console.log(`[Found] ${slug.padStart(6)} ${Date.now()}`);407 ctx.response.status = Status.MovedPermanently;408 ctx.response.headers.set("Location", entry.value as string);409});410411const app = new Application();412app.use(router.routes());413app.use(router.allowedMethods());414app.listen({ port: 8080 });