Catch-all route for production build of Vue Router/Vite-based SPA

Viewed 28

I have a Vue3-based SPA using Vue Router and Vite as dev server. The app has three valid URL paths:

  1. /
  2. /first
  3. /second

For production, it will be deployed on Apache under a prefix, i.e. the production URL paths will be:

  1. /prefix
  2. /prefix/first
  3. /prefix/second

What I want to achieve is that clients should be redirected to a valid URL path (e.g. /prefix) of the application even when they initially request an invalid URL path, such as /prefix/invalid.

For this purpose, I've defined the following router.js:

import { createWebHistory, createRouter } from "vue-router";
const routes = [
  {
    path: "/first",
    component: () => import("./components/First.vue")
  },
  {
    path: "/second",
    component: () => import("./components/Second.vue")
  },
  {
    path: "/:anything+",
    redirect: "/"
  },
];
const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL), routes});
export default router;

The third route entry defines a catch-all route that will match whenever clients request an invalid URL path. This works fine in development on Vite, i.e. when serving the app via npm run dev -- --base='/prefix/'. Even when the browser initially requests /prefix/invalid, the app's /prefix route is loaded and displayed.

When deploying with npm run build -- --base='/prefix/' for production, however, the catch-all route starts working only after clients have initially requested /prefix or /prefix/index.html and the SPA has been loaded. When they initially request /prefix/invalid or even /prefix/first or /prefix/second, Apache responds with 404 Not Found. This is of course because the whole routing is implemented client-side in JavaScript, so my catch-all route and everything else routing-related will only work once the SPA itself has been loaded and its JavaScript is executing in the browser.

My question is: is there a way to make initial requests for invalid URL paths work in production on Apache like in dev on Vite? I tried adding a file public/.htaccess like this:

ErrorDocument 404 import.meta.env.BASE_URL

But the expression import.meta.env.BASE_URL, which Vite statically replaces with the base config option value in router.js, is not replaced in this file - which is consistent with the documentation of the public directory. Hence this approach doesn't work.

Not sure whether I'm following the right path with .htaccess or whether my business problem has any better -possibly simpler- solution?

1 Answers

The problem can be solved by generating .htaccess dynamically using a simple Vite plugin, which I define inline in vite.config.js for simplicity.

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// A private Vite plugin for generating .htaccess dynamically
function generateHtaccess () {

  // Resolved Vite configuration, including "base" option
  let viteConfig;

  return {
    name: 'generate-htaccess',

    // Rollup Plugin API output generation hook
    // See https://rollupjs.org/guide/en/#generatebundle
    generateBundle() {
      this.emitFile({
        type: 'asset',
        fileName: '.htaccess',
        source: `ErrorDocument 404 ${viteConfig.base}\n`
      });
    },

    // Vite Plugin API specific hook
    // See https://vitejs.dev/guide/api-plugin.html#configresolved
    configResolved(resolvedConfig) {
      viteConfig = resolvedConfig;
    },
  };
}

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), generateHtaccess()],
});

A non-prefixed production build, such as npx vite build, will hence generate dist/.htaccess with the following content:

ErrorDocument 404 /

while specifying a prefix, e.g. npx vite build --base='/~user/' will generate:

ErrorDocument 404 /~user/

With this .htaccess, clients initially requesting any invalid URL path in production on Apache will be properly redirected to the same catch-all route -the app's root directory- as defined in router.js. This behavior will work consistently for any --base value supplied at build time.

Related