Link Shortener#

Does what it says on the tin...

Github

cloudflare icon javascript icon 🌐

Link Shortener

Description

A small web app that shortens links. Built as a way to explore Cloudflare Workers, wrangler and KV.

Features

Screenshots

Home page
Home Page
Basic authentication dialog
Basic authentication dialog
Add Link page
Add Link
Success page
Success page
Error page
Error page
Manage page
Manage existing links

Code Highlights

The manage page is populated by a fetch to a /list endpoint that returns a JSON array. Since this list of links could become large I wanted an easy,reusable way populate the DOM on the /manage path. For this I created an autonomous custom element called ‘ShortenedLink’. It takes ‘href’ and ‘shortid’ attributes in order to create a details tag that contains a short link, long link, and a button for deleting the link. When the delete button is clicked, a form is submitted to the /manage path with the id.

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
59
60
61
            class ShortenedLink extends HTMLElement {
                constructor() {
                    super()
                }
                #setDOM() {
                    this.innerHTML = '' //clear any existing content
                    let details = document.createElement('details')
                    let summary = document.createElement('summary')
                    let shortAnchor = document.createElement("a");
                    let longAnchor = document.createElement('a')
                    let deleteForm = document.createElement('form')
                    let delInput = document.createElement('input')
                    let submit = document.createElement('button')
                    let fullDisplayURL = this.href //default on a long string
                    try {
                        fullDisplayURL = new URL(this.href).hostname //attempt to shorten that string to the hostname
                    } catch (e) {} //do noting since we already have a string we can use
                    //ensure the string we're displaying is not too long
                    let displayURL =
                        fullDisplayURL.length > 20
                            ? fullDisplayURL.substring(0, 17) + '...'
                            : fullDisplayURL;

                    summary.innerText = `${this.shortid} (${displayURL})`
                    shortAnchor.href = `/${this.shortid}`
                    shortAnchor.innerText = this.shortid
                    longAnchor.href = this.href
                    longAnchor.innerText = this.href
                    longAnchor.target = '_blank'
                    deleteForm.method = 'delete'
                    delInput.type = 'hidden'
                    delInput.name = 'id'
                    delInput.value = this.shortid
                    delInput.style.display = 'none'
                    submit.type = 'submit'
                    submit.innerText = 'delete'
                    deleteForm.appendChild(delInput)
                    deleteForm.appendChild(submit)
                    details.appendChild(summary)
                    details.appendChild(shortAnchor)
                    details.appendChild(longAnchor)
                    details.appendChild(deleteForm)
                    this.appendChild(details)
                }
                get href() {
                    return this.getAttribute('href')
                }
                set href(val) {
                    this.setAttribute('href', val)
                    this.#setDOM()
                }
                get shortid() {
                    return this.getAttribute('shortid')
                }
                set shortid(val) {
                    this.setAttribute('shortid', val)
                    this.#setDOM()
                }
            }
            window.customElements.define('shortened-link', ShortenedLink);

The itty-router package allows routes to be set up in a super simple way that is similar to ExpressJS. It can even accept middleware functions for added functionality such as checking authentication or conditional responses.

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
const router = Router();
//taken from the Cloudflare Workers site template
const serveAsset = async () => await AssetFromKV(event);

//if there is an issue with authentication then a badAuthResponse will be returned (effectively an error page)
//if there are no issues then this function will return nothing, acting as middleware.
const verifyBasicAuth = async request => {
        if (!request.headers.has('Authorization')) {
            return await badAuthResponse(request, 'No authorization')
        } else {
            const { user, pass } = basicAuthentication(request)
            if (user != USERNAME || pass != PASSWORD) {
                return await badAuthResponse(request, 'Bad authorization')
            }
        }
    }

//if a path is found for a short ID then redirect to it
//otherwise this request will be handled by the normal router
const findShortURL = async req => {
        let key = new URL(req.url).pathname.substring(1)
        let result = null
        await SHORTENED_LINKS.get(key)
            .then(url => (result = url)) //assign the URL to the result
            .catch() //let this fall through
        if (result) {
            return Response.redirect(result, 302)
        }
    }

Setting up the API is as simple as passing the middleware functions before the final routing function. The ‘addRoute’ method creates link IDs from a substring of the base64 encoding of the current date and time, so while short URLs could end up very similar to each other if they’re created in succession, they will always be unique.

1
2
3
4
5
6
7
router.post('/add.html', verifyBasicAuth, addRoute)
router.delete('/manage.html', verifyBasicAuth, removeRoute)
//serve a JSON array of links
router.get('/list', verifyBasicAuth, listShortURLs)
//for all other routes check for a short URL with the matching path/id, then try serving the asset from KV storage
//will default on 404 if the short link or asset is not found
router.get('*', findShortURL, serveAsset)

GitHub Action

Most testing can be done using wrangler dev in the command line, however using Cloudflare’s template for this action means I can maintain the latest code on their edge network.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
name: Deploy CF Worker with Wrangler

on:
  push:
    branches:
      - main
    paths:
      - 'public/**'
      - 'workers-site/**'
      - 'wrangler.toml'

jobs:
  deploy:
    runs-on: ubuntu-latest
    name: Deploy
    steps:
      - uses: actions/[email protected]
      - name: Publish
        uses: cloudflare/[email protected]
        with:
          apiToken: $


Itty-router makes managing routes a breeze and with continuous deployment this is a great set-up for deploying prototypes or small apps on a global scale. I’m keen to explore larger, more complex APIs or even OAuth2 in the future.

You can check out the full code on GitHub.