6

I'm using prerender-spa-plugin in order to prerender certain pages so I get better SEO from my Vue app.

My goal is to transform the way I'm currently using Vue-i18n, so I can base it on url param /lang. Examples: /en/home or /nl/home. With this, I would be able to pre-render depending on the language.

I created a prefixer function that adds to every parent route the optional param /:lang?. Here it is:

const withPrefix = (prefix: string, routes: RouteConfig[]): RouteConfig[] => routes.map((route): RouteConfig => {
  // Avoiding mutations
  const clonedRoute = { ...route };
  // Every route except for '/'
  if (clonedRoute.path !== '/') {
    clonedRoute.path = prefix + clonedRoute.path;
  }
  return clonedRoute;
});

In Vue templates, I'm using:

<router-link :to="`/account`">

So I'm trying to manipulate the redirect to the next page according to the lang param.

First approach

The most logical one is (inside Router's beforeEach):

const { lang } = to.params;
const redirectTo = lang ? to.fullPath : `${fullToLang}${to.fullPath}`;
if (from.fullPath !== redirectTo) {
  next({ path: redirectTo });
} else {
  next();
}

But it enters in an endless loop because from is always the same.

Second approach

Using Router's base property.

import Vue from "vue";
import App from "./App.vue";
import VueRouter from "vue-router";
import HelloWorld from "./components/HelloWorld";
import Test from "./components/Test";

Vue.config.productionTip = false;

Vue.use(VueRouter);

const router = new VueRouter({
  mode: "history",
  base: "/en",
  routes: [
    {
      path: ":lang?/",
      component: HelloWorld,
      beforeEnter: (to, from, next) => {
        console.log(1);
        next();
      }
    },
    {
      path: "/:lang?/nope",
      component: Test,
      beforeEnter: (to, from, next) => {
        console.log(2);
        next();
      }
    },
    {
      path: "/:lang?/*",
      beforeEnter: (to, from, next) => {
        console.log(to);
        next("/nope");
      }
    }
  ]
});

new Vue({
  render: h => h(App),
  router
}).$mount("#app");

Or better, live: https://codesandbox.io/embed/vue-template-0bwr9

But, I don't understand why it's redirecting to /en/nope only if the url is not found on the routes (last case). And more, would I have to create a new Router instance each time I want to change base?

Third approach

Wrapper component for router-link injecting :to based on this.$route.params.lang.

This would do it for navigation after the app is loaded but not at the first refresh/initialization.

So, how should I resolve this?

~ Solution ~

So yeah, first approach was the correct way to go but I missunderstood how Router behaves with next and redirects. The condition should be checking the to not the from.

const redirectTo = lang ? to.fullPath : `${fullToLang}${to.fullPath}`;
if (to.fullPath !== redirectTo) {
  // Change language at i18n
  loadLanguageAsync(toLang as Language);

  next({ path: redirectTo });

  return;
}
Jesús Fuentes
  • 919
  • 2
  • 11
  • 33

2 Answers2

2

I am not entirely sure what you are asking. But I assume you want to prefix your navigations with the current language param (../en/..) if they do not already have one?

You could resolve this with a beforeEach() hook and only redirecting if there is no lang param present.

const { lang } = to.params
if(!lang) {
  next({ path: redirectTo })
}
next()

If that's not what you want please clarify and I'll edit my answer

MarcRo
  • 2,434
  • 1
  • 9
  • 24
  • 1
    I was too stubborn thinking that the condition had to be related to `from` not `to`. I didn't understand well the way Router behaves when redirecting and I thought from would be changing after the first `next`. – Jesús Fuentes Aug 21 '19 at 15:05
1

Something like this? The assumption is that the new path starts /[lang]/...

as a note - there are still errors when routing e.g. /:lang/bar -> /foo/bar

Vue.lang = 'en'
function beforeEnter(to, from, next){

  if ((new RegExp(`^/${Vue.lang}$`))
  .test(to.path) 
  || 
  (new RegExp(`^/${Vue.lang}/`))
  .test(to.path))
  {
    next();
  } else {
    
    next({path: `/${Vue.lang}${to.path}`})
  }
};
Vue.mixin({
  beforeRouteEnter: beforeEnter
})
const Foo = { template: '<div>foo - {{$route.path}}</div>' }
const Bar = { template: '<div>bar - {{$route.path}}</div>' }
const Root = { template: '<div>Root - {{$route.path}}</div>' }
const Invalid = { template: '<div>404</div>' }

const routes = [
  
  { path: '/:lang/foo', component: Foo },
  { path: '/:lang/bar', component: Bar },
  { path: '/:lang/*', component: Invalid },
  { path: '/:lang', name: 'Home', component: Root },
  // some weird issue that prevents beforeRouteEnter ? so redirect, but else next is needed
  { path: '/', redirect: to => `/${Vue.lang}`}
]

const router = new VueRouter({
  routes
})

new Vue({
  data(){
    return {
      pLang: Vue.lang,
    }
  },
  computed: {
    lang: {
      get(){
        return this.pLang
      },
      set(val){
        Vue.lang = val
        this.pLang = val
      }
    }
  },
  router,
}).$mount('#app');
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

<div id="app">
  <h1>Hello App!</h1>
  <p>
     {{lang}}
    <select v-model="lang">
      <option value="en">en</option>
      <option value="cn">cn</option>
    </select>
    <!-- use router-link component for navigation. -->
    <!-- specify the link by passing the `to` prop. -->
    <!-- `<router-link>` will be rendered as an `<a>` tag by default -->
    
    <router-link to="/">Root</router-link>
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
    <router-link to="/foo/bar">Go to Foo/Bar - not defined</router-link>
  </p>
  <!-- route outlet -->
  <!-- component matched by the route will render here -->
  <router-view></router-view>
</div>
Community
  • 1
  • 1
Estradiaz
  • 3,483
  • 1
  • 11
  • 22