Cloudflare Workers - SPA with Vuejs

As you know, Vue.js (like many other SPA frameworks) expects that for any path that doesn't map to a specific file, the server falls back to serving the root /index.html file. Vue will then do routing in browser-side JavaScript. You mentioned that you know how to accomplish this fallback with .htaccess, but how can we do it with Workers?

Good news: In Workers, we can write code to do whatever we want!

In fact, the worker code already has a specific block of code to handle "404 not found" errors. One way to solve the problem would be to change this block of code so that instead of returning the 404 error, it returns /index.html.

The code we want to change is this part:

  } catch (e) {
    // if an error is thrown try to serve the asset at 404.html
    if (!DEBUG) {
      try {
        let notFoundResponse = await getAssetFromKV(event, {
          mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/404.html`, req),
        })

        return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 })
      } catch (e) {}
    }

    return new Response(e.message || e.toString(), { status: 500 })
  }

We want to change it to:

  } catch (e) {
    // Fall back to serving `/index.html` on errors.
    return getAssetFromKV(event, {
      mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/index.html`, req),
    })
  }

That should do the trick.


However, the above solution has a slight problem: For any HTML page (other than the root), it will do two lookups, first for the specific path, and only after that will it look for /index.html as a fallback. These lookups are pretty fast, but maybe we can make things faster by being a little bit smarter and detecting HTML pages upfront based on the URL.

To do this, we want to customize the mapRequestToAsset function. You can see a hint about this in a comment in the code:

  /**
   * You can add custom logic to how we fetch your assets
   * by configuring the function `mapRequestToAsset`
   */
  // options.mapRequestToAsset = handlePrefix(/^\/docs/)

Let's go ahead and use it. Replace the above comment with this:

  options.mapRequestToAsset = req => {
    // First let's apply the default handler, which we imported from
    // '@cloudflare/kv-asset-handler' at the top of the file. We do
    // this because the default handler already has logic to detect
    // paths that should map to HTML files, for which it appends
    // `/index.html` to the path.
    req = mapRequestToAsset(req)

    // Now we can detect if the default handler decided to map to
    // index.html in some specific directory.
    if (req.url.endsWith('/index.html')) {
      // Indeed. Let's change it to instead map to the root `/index.html`.
      // This avoids the need to do a redundant lookup that we know will
      // fail.
      return new Request(`${new URL(req.url).origin}/index.html`, req)
    } else {
      // The default handler decided this is not an HTML page. It's probably
      // an image, CSS, or JS file. Leave it as-is.
      return req
    }
  }

Now the code detects specifically HTML requests and replaces them with the root /index.html, so there's no need to waste time looking up a file that doesn't exist only to catch the resulting error. For other kinds of files (images, JS, CSS, etc.) the code will not modify the filename.


There appears to be a built-way to do this now:

import { getAssetFromKV, serveSinglePageApp } from '@cloudflare/kv-asset-handler'
...
let asset = await getAssetFromKV(event, { mapRequestToAsset: serveSinglePageApp })

https://github.com/cloudflare/kv-asset-handler#servesinglepageapp