blazing fast link redirects on cloudflare kv hop.dunkirk.sh/u/tacy

feat: add 404 page and more shortcuts

dunkirk.sh d03b25ea 8153c014

verified
+146 -5
+2
README.md
··· 1 1 # 🛎️ Hop 2 2 3 + ![screenshot of the website](https://hc-cdn.hel1.your-objectstorage.com/s/v3/a8ba600bac177769_hop.png) 4 + 3 5 This is a super light weight shortlink service that runs at blazing speeds on cloudflare kv and supplies `dunkirk.sh/q` 4 6 5 7 The canonical repo for this is hosted on tangled over at [`dunkirk.sh/hop`](https://tangled.org/@dunkirk.sh/hop)
+115
src/404.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + 4 + <head> 5 + <meta charset="UTF-8" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 + <title>404 - hop</title> 8 + <link rel="icon" 9 + href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🛎️</text></svg>" /> 10 + <style> 11 + :root { 12 + --bg-primary: oklch(23.17% 0.0113 115.0); 13 + --text-primary: oklch(0.77 0.05 177.41); 14 + --text-muted: oklch(61.43% 0.0603 149.4); 15 + --accent-primary: oklch(82.83% 0.0539 158.7); 16 + --accent-bright: oklch(0.81 0.05 164.12); 17 + } 18 + 19 + * { 20 + margin: 0; 21 + padding: 0; 22 + box-sizing: border-box; 23 + } 24 + 25 + html { 26 + background: var(--bg-primary); 27 + } 28 + 29 + body { 30 + font-family: "Courier New", monospace; 31 + background: var(--bg-primary); 32 + color: var(--text-primary); 33 + min-height: 100vh; 34 + display: flex; 35 + align-items: center; 36 + justify-content: center; 37 + padding: 1.25rem; 38 + flex-direction: column; 39 + text-align: center; 40 + } 41 + 42 + .error-container { 43 + max-width: 31.25rem; 44 + width: 100%; 45 + } 46 + 47 + .error-code { 48 + font-size: 6rem; 49 + font-weight: 700; 50 + background: linear-gradient(135deg, var(--text-muted), var(--accent-primary), var(--accent-bright)); 51 + -webkit-background-clip: text; 52 + -webkit-text-fill-color: transparent; 53 + background-clip: text; 54 + letter-spacing: -0.125rem; 55 + margin-bottom: 1rem; 56 + } 57 + 58 + h1 { 59 + font-size: 1.5rem; 60 + margin-bottom: 0.5rem; 61 + color: var(--text-primary); 62 + } 63 + 64 + p { 65 + color: var(--text-muted); 66 + margin-bottom: 2rem; 67 + font-size: 0.875rem; 68 + line-height: 1.5; 69 + } 70 + 71 + a { 72 + color: var(--accent-primary); 73 + text-decoration: none; 74 + transition: color 0.15s; 75 + font-size: 0.875rem; 76 + } 77 + 78 + a:hover { 79 + color: var(--accent-bright); 80 + text-decoration: underline; 81 + } 82 + 83 + .short-code { 84 + background: oklch(28.59% 0.0107 114.8); 85 + padding: 0.25rem 0.5rem; 86 + border-radius: 0.1875rem; 87 + color: var(--accent-bright); 88 + font-weight: 700; 89 + font-family: "Courier New", monospace; 90 + } 91 + </style> 92 + </head> 93 + 94 + <body style="background-color: var(--bg-primary)"> 95 + <div class="error-container"> 96 + <div class="error-code">404</div> 97 + <h1>short url not found</h1> 98 + <p> 99 + the short code <span class="short-code" id="shortCode"></span> doesn't exist or has been deleted. 100 + </p> 101 + <a href="/">← back to hop</a> 102 + </div> 103 + 104 + <script> 105 + // Extract short code from URL 106 + const shortCode = window.location.pathname.slice(1); 107 + if (shortCode) { 108 + document.getElementById('shortCode').textContent = '/' + shortCode; 109 + } else { 110 + document.getElementById('shortCode').textContent = 'unknown'; 111 + } 112 + </script> 113 + </body> 114 + 115 + </html>
+26 -3
src/index.html
··· 4 4 <head> 5 5 <meta charset="UTF-8" /> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 - <title>hop</title> 7 + <title>hop - fast url shortener</title> 8 + <meta name="description" content="hop - a fast, minimal URL shortener with virtual scrolling and developer-focused UI" /> 9 + 10 + <!-- Open Graph / Facebook --> 11 + <meta property="og:type" content="website" /> 12 + <meta property="og:url" content="https://hop.dunkirk.sh" /> 13 + <meta property="og:title" content="hop - fast url shortener" /> 14 + <meta property="og:description" content="minimal url shortener with virtual scrolling and developer-focused ui" /> 15 + <meta property="og:image" content="https://hc-cdn.hel1.your-objectstorage.com/s/v3/a8ba600bac177769_hop.png" /> 16 + 17 + <!-- Twitter --> 18 + <meta name="twitter:card" content="summary_large_image" /> 19 + <meta name="twitter:url" content="https://hop.dunkirk.sh" /> 20 + <meta name="twitter:title" content="hop - fast url shortener" /> 21 + <meta name="twitter:description" content="minimal url shortener with virtual scrolling and developer-focused ui" /> 22 + <meta name="twitter:image" content="https://hc-cdn.hel1.your-objectstorage.com/s/v3/a8ba600bac177769_hop.png" /> 23 + 8 24 <link rel="icon" 9 25 href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🛎️</text></svg>" /> 10 26 <style> ··· 388 404 <section class="form-section"> 389 405 <div class="shortcut"> 390 406 <kbd>tab</kbd> navigate • <kbd>enter</kbd> submit • 391 - <kbd>esc</kbd> clear • <kbd>cmd+k</kbd> focus • <kbd>cmd+l</kbd> logout 407 + <kbd>esc</kbd> clear • <kbd>cmd+k</kbd> focus • <kbd>cmd+/</kbd> search • <kbd>cmd+l</kbd> logout 392 408 </div> 393 409 <form id="shortenForm"> 394 410 <div class="form-row"> ··· 488 504 urlInput.focus(); 489 505 urlInput.select(); 490 506 } 507 + if ((e.key === "/" && e.target.tagName !== "INPUT") || ((e.metaKey || e.ctrlKey) && e.key === "/")) { 508 + e.preventDefault(); 509 + searchInput.focus(); 510 + searchInput.select(); 511 + } 491 512 if (e.key === "Escape") { 513 + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") { 514 + e.target.blur(); 515 + } 492 516 form.reset(); 493 517 result.classList.remove("show", "error"); 494 518 slugInput.style.borderColor = "var(--border-color)"; 495 - urlInput.focus(); 496 519 } 497 520 if (e.key === "l" && (e.metaKey || e.ctrlKey)) { 498 521 e.preventDefault();
+3 -2
src/index.ts
··· 1 1 import { nanoid } from 'nanoid'; 2 2 import indexHTML from './index.html'; 3 3 import loginHTML from './login.html'; 4 + import notFoundHTML from './404.html'; 4 5 5 6 export default { 6 7 async fetch( ··· 26 27 return Response.redirect(targetUrl, 302); 27 28 } 28 29 29 - return new Response('Short URL not found', { 30 + return new Response(notFoundHTML, { 30 31 status: 404, 31 - headers: { 'Content-Type': 'text/plain' }, 32 + headers: { 'Content-Type': 'text/html' }, 32 33 }); 33 34 } 34 35