Teknikal's_Domain

#<NTA:NnT:SSrgS:H6.6-198:W200-90.72:CBWg>

Shortening My URLs With tekdmn.me

2021-02-08 6 min read Behind the scenes Blog improvements Programming Tech Web stuff Teknikal_Domain Unable to load comment count

Has anyone noticed that I’ve been giving out URLs of the form tekdmn.me and not teknikaldomain.me? Well, not only did I buy another domain that’s just a shorter version of this one, but it’s also, at the moment, the singular domain I have that’s 100% serverless. How? Cloudflare, obviously.

So the core of that domain is a Cloudflare worker and some KV store, but first, let’s go over the reasoning. My reasoning was simple: teknikaldomain.me is long. tekdmn.me is short. Shorter names are easier to type. Therefore, let’s make it shorter. Realistically, I could have just bought it and used a single DNS CNAME to redirect everything to teknikaldomain.me, but, well, I had a slightly better plan: as long as I’m making shorter URLs, why not… a URL shortener? This way I can make shorter links overall, like turning https://teknikaldomain.me/post/ipv6-is-a-total-nightmare to https://tekdmn.me/ip6. But I wanted a system with a few specific features:

  • Allows passthrough: https://tekdmn.me/post/ipv6-is-a-total-nightmare is completely valid
  • Allows arbitrary short URLs: I don’t want to see a lot of https://tekdmn.me/ae0069d
  • Has no reliance on other services, meaning no data sharing, and no things out of my control that can break
  • Allows arbitrary long URLs: I can make a short URL for anything, including things not already on my blog.

What was my solution? Obviously, just under 40 lines of (actual) JavaScript1. Here is literally everything that is tekdmn.me:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Event handler boilerplate
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event))
})

// Put simply: Don't repeat yourself
function prepareRedirectResponse(path) {
  return new Response(
    '<html><head><title>Redirecting to ' +
      path +
      '...</title></head><body>Please wait for the redirect, or, <a href="' +
      path +
      '">click here.</a></body></html>',
    {
      status: 302,
      headers: { 'content-type': 'text/html', location: path },
    },
  )
}

async function handleRequest(event) {
  const cache = caches.default
  const request = event.request

  // Only allow HTTP GET and HTTP HEAD
  if (request.method === 'GET' || request.method === 'HEAD') {
    // If requesting the homepage, redirect straight to blog.
    if (request.url === 'https://tekdmn.me/') {
      return prepareRedirectResponse('https://teknikaldomain.me/')
    }

    // Otherwise, attempt to fetch from cache
    let response = await cache.match(request)

    if (!response) {
      // No response.

      // Key = tekdmn path, value = full redirect URL
      // And that split is probably the dumbest (but easiest) way to isolate the path
      // and strip off the trailing slash
      const key = request.url.split('.me/')[1].replace(/\/$/, "")
      const mapped_url = await URL_MAP.get(key) // Get URL from Cloudflare KV
      response = prepareRedirectResponse(
        mapped_url ? mapped_url : 'https://teknikaldomain.me/' + key,
      ) // Use URL from KV if found, otherwise, just do a straight pass-through
      // Prepare and cache the response
      const respHeaders = { 'cache-control': 'public, max-age=86400' }
      response = new Response(response.body, { ...response, respHeaders })
      event.waitUntil(cache.put(request, response.clone()))
    }

    // Send it
    return response
  } else {
    // Remember, only GET and HEAD
    return new Response(null, { status: 405 })
  }
}

As with the rest of my Cloudflare Workers code, the first part is just some boilerplate event handler, the real magic is handleRequest. We start by declaring that we’re using Cloudflare’s default, shared HTTP cache, and then filter the request on HTTP method, I only allow GET and HEAD, the rest will return 405 Method Not Allowed as seen at the very bottom. A second very quick check is performed, seeing if the domain root was requested (A.K.A., no path, A.K.A., GET / HTTP/1.1), which is an immediate redirect over to the homepage of teknikaldomain.me, do not pass Go, do not collect $200.

With these two initial checks out of the way, we check if Cloudflare has cached an HTTP response to a similar HTTP request, which we will return immediately if one is found, since that means we don’t need to recalculate it all (that’s literally the point of a cache). Assuming it’s not in the cache, I will get the path component of the URL in a slightly hacky but still perfectly valid manner, using split(), and then I will query Cloudflare’s KV using that URL path as the key. The regex replace() is there to strip a trailing / character if any, so that https://tekdmn.me/nas and https://tekdmn.me/nas/ are interpreted identically. All values in KV are complete URLs, meaning all I need to do now is construct a Response that will 302 you to that URL, which is the job of prepareRedirectResponse, which literally just constructs and returns a Response. If the path given was not a valid key, then I’ll just redirect over using the path specified, allowing for a perfect pass-through to the proper blog domain without any extra work. That is the job of line 44, which uses a ternary operator. You can think of that as a compact if, where instead of this format:

if (condition) {
    return something
} else {
    return something-else
}

It looks more like this:

condition ? something : something-else

Because a variable being null evaluates, in this case, somehow, to false, then we can use that line, which roughly translates to, in English, “if mapped_url is not null, then use the value of mapped_url, otherwise, use the value of https://teknikaldomain.me/ plus the given path.”

Finally after all that, we have a valid Response object. One final step before sending is to attach the correct headers to it, and then tell Cloudflare to store that in the cache, so that we can later use this exact response again if we have a similar request coming in. Both flow paths, cached or uncached, converge just in time to return the Response object back to the client.

This short little script is all that I need to do what I want, meaning I can now give much more manageable URLs out, and they should all map back to the correct place, with almost no effort on my part after this, nice! Even better, what you’re seeing is only version 1.1.0 of the script, which in this case, is the third revision. The code itself is pretty much set-and-forget.


  1. As reported by both sloccount and cloc, there’s only 39 lines of code in that file, the rest are blanks or just comments. ↩︎

comments powered by Disqus