Vue-router redirect on page not found (404)

I'm trying to redirect to 404.html on page not found using the router.beforeEach global hook, without much success (using Vueand Vue-Router 1.0):

router.beforeEach(function (transition) {
    if (transition.to.path === '/*') {
        window.location.href = '/404.html'
    } else {
        transition.next()
    }
});

This is not redirecting to page 404.html when a non-existing route is inserted, just giving me an empty fragment. Only when hard-coding some-site.com/* will it redirect to the expected some-site.com/404.html

I'm sure there's something very obvious here that I'm overlooking, but I cannot figure out what.


Please note that what I am looking for is a redirect to a new page, not a redirect to another route, which could be easily achieved by using router.redirect such as in these snippets:

router.redirect({
    '*': '404'
})

Whereas on my router.map, I could have the following:

 router.map({
    '/404': {
        name: '404'
        component: {
            template: '<p>Page Not Found</p>'
        }
    }
})

Solution 1:

I think you should be able to use a default route handler and redirect from there to a page outside the app, as detailed below:

const ROUTER_INSTANCE = new VueRouter({
    mode: "history",
    routes: [
        { path: "/", component: HomeComponent },
        // ... other routes ...
        // and finally the default route, when none of the above matches:
        { path: "*", component: PageNotFound }
    ]
})

In the above PageNotFound component definition, you can specify the actual redirect, that will take you out of the app entirely:

Vue.component("page-not-found", {
    template: "",
    created: function() {
        // Redirect outside the app using plain old javascript
        window.location.href = "/my-new-404-page.html";
    }
}

You may do it either on created hook as shown above, or mounted hook also.

Please note:

  1. I have not verified the above. You need to build a production version of app, ensure that the above redirect happens. You cannot test this in vue-cli as it requires server side handling.

  2. Usually in single page apps, server sends out the same index.html along with app scripts for all route requests, especially if you have set <base href="/">. This will fail for your /404-page.html unless your server treats it as a special case and serves the static page.

Let me know if it works!

Update for Vue 3 onward:

You'll need to replace the '*' path property with '/:pathMatch(.*)*' if you're using Vue 3 as the old catch-all path of '*' is no longer supported. The route would then look something like this:

{ path: '/:pathMatch(.*)*', component: PathNotFound },

See the docs for more info on this update.

Solution 2:

@mani's response is now slightly outdated as using catch-all '*' routes is no longer supported when using Vue 3 onward. If this is no longer working for you, try replacing the old catch-all path with

{ path: '/:pathMatch(.*)*', component: PathNotFound },

Essentially, you should be able to replace the '*' path with '/:pathMatch(.*)*' and be good to go!

Reason: Vue Router doesn't use path-to-regexp anymore, instead it implements its own parsing system that allows route ranking and enables dynamic routing. Since we usually add one single catch-all route per project, there is no big benefit in supporting a special syntax for *.

(from https://next.router.vuejs.org/guide/migration/#removed-star-or-catch-all-routes)

Solution 3:

This answer may come a bit late but I have found an acceptable solution. My approach is a bit similar to @Mani one but I think mine is a bit more easy to understand.

Putting it into global hook and into the component itself are not ideal, global hook checks every request so you will need to write a lot of conditions to check if it should be 404 and window.location.href in the component creation is too late as the request has gone into the component already and then you take it out.

What I did is to have a dedicated url for 404 pages and have a path * that for everything that not found.

{ path: '/:locale/404', name: 'notFound', component: () => import('pages/Error404.vue') },
{ path: '/:locale/*', 
  beforeEnter (to) {
    window.location = `/${to.params.locale}/404`
  }
}

You can ignore the :locale as my site is using i18n so that I can make sure the 404 page is using the right language.

On the side note, I want to make sure my 404 page is returning httpheader 404 status code instead of 200 when page is not found. The solution above would just send you to a 404 page but you are still getting 200 status code. To do this, I have my nginx rule to return 404 status code on location /:locale/404

server {
    listen                      80;
    server_name                 localhost;

    error_page  404 /index.html;
    location ~ ^/.*/404$ {
      root   /usr/share/nginx/html;
      internal;
    }

    location / {
      root   /usr/share/nginx/html;
      index  index.html index.htm;
      try_files $uri $uri/ @rewrites;
    }

    location @rewrites {
      rewrite ^(.+)$ /index.html last;
    }

    location = /50x.html {
      root   /usr/share/nginx/html;
    }
}