Vue-Router language based route prefix - vue.js

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;
}

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

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>

Related

How to add router with query param to router list?

I want to add a route with query params.
If the url is blog, then navigate to index page.
If the url includes the author query param, replace a component on the page with the BlogAuthorPage component.
router: {
extendsRoutes(routes, resolve) {
routes.push({
name: 'author-page-detail',
path: '/blog?author=*',
component: resolve(__dirname, 'pages/blog/author-page.vue')
})
}
}
This should not be done in nuxt.config.js's router key but rather in your blog.vue page directly with a component router guard.
The code below should be enough to check if the route does have a author query params and redirect to the blog/author-page page.
<script>
export default {
beforeRouteEnter(to, from, next) {
next((vm) => {
if (vm.$route.query?.author) next({ name: 'blog-author-page' })
else next()
})
},
}
</script>
I use "#nuxtjs/router": "^1.6.1",
nuxt.config.js
/*
** #nuxtjs/router module config
*/
routerModule: {
keepDefaultRouter: true,
parsePages: true
}
router.js
import Vue from 'vue'
import Router from 'vue-router'
import BlogIndexPage from '~/pages/blog/index'
import BlogAuthorPage from '~/pages/blog/author-page';
Vue.use(Router);
export function createRouter(ssrContext, createDefaultRouter, routerOptions, config) {
const options = routerOptions ? routerOptions : createDefaultRouter(ssrContext, config).options
return new Router({
...options,
routes: [
...options.routes,
{
path: '/blog',
component: ssrContext.req.url.includes('/blog?author') ? BlogAuthorPage : BlogIndexPage
}
]
})
}

Vue 3 dynamic components at router level

Dynamic imports is needed for me, eg. i have 10 layouts, but user only visited 3 layouts, I should not import all of the layouts, since its consumed unnecessary resources.
Since its dynamic import, each time i switch between Login & Register path <RouterLink :to"{name: 'Login'}" /> & <RouterLink :to"{name: 'Register'}" />, I got rerender or dynamic import the layout again.
My question is what is the better way to handle it, without rerender or dynamic import the layout again? Or can I save the dynamic import component into the current vue 3 context?
App.vue this is my app with watching the router and switch the layout based on route.meta.layout
<template>
<component :is="layout.component" />
</template>
<script>
import DefaultLayout from "./layout/default.vue";
import {
ref,
shallowRef,
reactive,
shallowReactive,
watch,
defineAsyncComponent,
} from "vue";
import { useRoute } from "vue-router";
export default {
name: "App",
setup(props, context) {
const layout = shallowRef(DefaultLayout);
const route = useRoute();
watch(
() => route.meta,
async (meta) => {
if (meta.layout) {
layout = defineAsyncComponent(() =>
import(`./layout/${meta.layout}.vue`)
);
} else {
layout = DefaultLayout;
}
},
{ immediate: true }
);
return { layout };
},
};
</script>
router/index.js this is my router with layout meta
import { createRouter, createWebHistory } from "vue-router";
import Home from "#/views/Home.vue";
import NotFound from "#/views/NotFound.vue";
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/login",
name: "Login",
meta: {
layout: "empty",
},
component: function () {
return import(/* webpackChunkName: "login" */ "../views/Login.vue");
},
},
{
path: "/register",
name: "Register",
meta: {
layout: "empty",
},
component: function () {
return import(/* webpackChunkName: "register" */ "../views/Register.vue");
},
},
{ path: "/:pathMatch(.*)", component: NotFound },
];
const router = createRouter({
history: createWebHistory(import.meta.env.VITE_GITLAB_BASE_PATH),
routes,
scrollBehavior(to, from, savedPosition) {
// always scroll to top
return { top: 0 };
},
});
export default router;
You could use AsyncComponent inside the components option and just use a computed property that returns the current layout, this will load only the current layout without the other ones :
components: {
layout1: defineAsyncComponent(() => import('./components/Layout1.vue')),
layout2: defineAsyncComponent(() => import('./components/Layout2.vue')),
},
Had this issue and Thorsten Lünborg of the Vue core team helped me out.
add the v-if condition and that should resolve it.
<component v-if="layout.name === $route.meta.layout" :is="layout">

Vue router guard triplicating navigation

I have a router guard beforeEach route to watch if there's user authenticated:
import Vue from "vue";
import VueRouter from "vue-router"
import Login from "../views/Login.vue"
import Home from "../components/Home.vue"
import Register from "../views/Register.vue"
import Dashboard from "../views/Dashboard.vue"
import Pricing from "../components/Pricing.vue"
import Invoices from "../components/Invoices.vue"
import { FirebaseAuth } from "../firebase/firebase"
Vue.use(VueRouter);
const routes = [
{
path: "*",
redirect: "/login",
},
{
path: "/dashboard",
name: "dashboard",
component: Dashboard,
children: [
{
path: "home",
name: "home",
component: Home,
},
{
path: "pricing",
name: "pricing",
component: Pricing,
},
{
path: "invoices",
name: "invoices",
component: Invoices,
}
],
meta: {
auth: true,
},
redirect: "home"
},
{
path: "/login",
name: "login",
component: Login,
},
{
path: "/register",
name: "register",
component: Register,
}
];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
router.beforeEach((to, from, next)=>{
let user = FirebaseAuth.currentUser;
let auth = to.matched.some(record => record.meta.auth);
if (auth && !user) {
next('/login');
} else if (!auth && user) {
next('/dashboard/home');
} else{
next();
}
});
export default router;
When I perform logouts and logins there's an error about redundant navigation, however, I just assumed that it's ok if I just catch this.$router.push('/dashboard/home').catch(err => err); and move on without the console.log err. But creating an alert on component created() I've noticed that the thing is just more serious than what I thought, the component that shows the alert on created() it's showing it three times, and as I have a fetch for restore items on created(), that function is being triggered three times which is obviously not the performance wanted.
async created() {
alert("created")
this.credits = await fetchCredits(this.$firestore, this.$auth.currentUser);
let role = await getCustomClaimRole(this.$auth.currentUser);
this.subscription = role
? role.charAt(0).toUpperCase() + role.slice(1) + " " + "plan"
: "You haven't subscribed yet";
this.isLoading();
},
inside fetchCredits() is the console.log triggering three times
export const fetchCredits = async function (firestore, currentUser) {
// firestore collection of customers
const db = firestore.collection("customers");
/**
* Let's fetch the credits from the user:
*/
const credits = (await db.doc(currentUser.uid).get()).data();
if (credits !== "undefined") {
console.log(credits);
return credits.credits
} else {
return 0;
}
}
I think the problem is with the navigation guard, however, correct me if I'm wrong, but how to solve this?
I think that it has something to do with your router path:
{
path: "*",
redirect: "/login",
},
I have used Vue Router several times, but since I hadn't used wildcards before, I built a simplified Vue 2 CLI test application.
My router:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import Home from '#/components/stackoverflow/router-wildcard-match/Home'
import RouteOne from '#/components/stackoverflow/router-wildcard-match/RouteOne'
import RouteTwo from '#/components/stackoverflow/router-wildcard-match/RouteTwo'
import WildCard from '#/components/stackoverflow/router-wildcard-match/WildCard'
const routes = [
{
path: '/*',
name: 'wildcard',
component: WildCard
},
{
path: '/home',
name: 'home',
component: Home,
},
{
path: '/routeone',
name: 'routeOne',
component: RouteOne,
},
{
path: '/routetwo',
name: 'routeTwo',
component: RouteTwo,
},
]
export default new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
and my navbar component that routes programmatically:
<template>
<div class="navbar-sandbox">
<nav class="navbar navbar-expand-md navbar-light bg-light">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#" #click.prevent="navigate('home')">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" #click.prevent="navigate('routeone')">RouteOne</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" #click.prevent="navigate('routetwo')">RouteTwo</a>
</li>
</ul>
</nav>
</div>
</template>
<script>
export default {
data() {
return {
//currentRoute: 'home',
currentPath: 'home'
}
},
methods: {
// NOTE: Using route names work regardless of having wildcard path
// navigate(route) {
// if (route !== this.currentRoute) {
// this.currentRoute = route;
// this.$router.push({ name: route });
// }
// },
navigate(path) {
if (path !== this.currentPath) {
this.currentPath = path;
this.$router.push({ path: path });
}
}
}
}
</script>
As you can see in my code comments, when I programmatically route via the route names, it works even with having a wildcard path, but when I route via the actual route path, the routes are all intercepted by the wildcard.
My wildcard path is a bit different that yours, /* vs *.

Vue-router and dynamic routes: not working

I am using the next route:
{
path: '/test/:name',
name: 'Test',
component: Test,
// props: true,
secure: false
}
When I'm trying to access the route http://myapp/test/helloworld in the Vue extension for the Chrome I see that route is not matched. But while testing I found that route http://myapp/test/:name (yeah, with the colon and the param name) the component is loaded. The component is anonymous and simple:
let Test = { template: '<div>sd {{ $route.params.name }}</div>' }
Here a working example doing the navigation with Programmatic and Declarative ways, check if you are missing something.
Also if you do the navigation via JS, in order to pass the params to the router.push(), you need to ref the component via name prop, not the path
const Test = {
template: `<div> {{ $route.params.name }}</div>`
}
const router = new VueRouter({
routes: [
{ path: '/test/:name', component: Test, name:'test' }
]
})
const app = new Vue({
router,
methods: {
fromJS () {
this._router.push({ name: 'test', params: { name: 'doe' }})
}
}
}).$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">
<p>
<router-link to="/test/john">John</router-link>
<router-link to="/test/doe">Doe</router-link>
<button #click="fromJS">To doe from JS</button>
</p>
<router-view></router-view>
</div>
Sorry, it was my mistake. I got the next snippet in the bottom of router js file:
router.beforeEach((to, from, next) => {
router.options.routes.forEach((value, key) => {
if (value.path.localeCompare(to.path) === 0) {
if (value.secure === false || store.getters.getLogInStatus === true) {
next()
} else if (value.secure === true && store.getters.getLogInStatus === false) {
router.push({
'name': 'Login'
})
}
}
})
})
Here the third line is where mistake happens because /test/:name is not match /test/anyname.

get all routes in a vue router

Am trying to create a simple menu using vue router , id like to iterate all routes and display in my menu , currently am using below instance method in my component but i just get a function , how would i iterate to get individual routes ?
methods : {
getMenuLinks: function() {
var t = this.$router.map() ;
//t returns a vue object instance
return t._children ;
// did not know how to iterate this
}
}
I want to iterate all maped routes to get something like below of each mapped route :
<a v-link="{ path: 'home' }">Home</a>
In Nuxt, the routes are generated automatically so I couldn't do what #zxzak suggested.
Here's what you can do in that case.
<template v-for="item in items">
<b-nav-item :to="item.path">
{{item.name}}
</b-nav-item>
</template>
export default {
created() {
this.$router.options.routes.forEach(route => {
this.items.push({
name: route.name
, path: route.path
})
})
}
, data() {
return {
items: []
}
}
}
You can simply iterate over $router.options.routes in your template:
<nav>
<router-link v-for="route in $router.options.routes" :key="route.path" :to="route.path">{{ route.name }}</router-link>
</nav>
Maybe add styling for the selected route:
:class="{ active: route.path === $router.currentRoute.path }"
edit: for active class, use https://router.vuejs.org/api/#active-class instead
Since vue-router 3.5, Router instance has now a getRoutes() method.
So an up to date answer could be
<router-link
for="r in routes"
:key="r.path"
:to="r.path"
>
{{ r.name }}
</router-link>
computed: {
routes() { return this.$router.getRoutes() }
}
Instead of relaying on Vue's internals, put routes inside the data of your starting component.
var map = {
'/foo': {
component: Foo
},
'/bar': {
component: Bar
}
}
var routes = Object.keys(map)
var App = Vue.extend({
data: function() {
return {
routes: routes
}
}
})
router.map(map)
router.start(App, '#app')
http://jsfiddle.net/xyu276sa/380/
Another solution is using Webpack's require.context
// search for src/pages/**/index.vue
function routesGen () {
const pages = require.context('./pages/', true, /index\.vue$/)
const filePaths = pages.keys()
const getRoutePath = filePath => filePath.match(/\.(\/\S+)\/index\.vue/)[1]
return filePaths.map(filePath => ({
path: getRoutePath(filePath),
component: pages(filePath).default
}))
}
As VueRouter is simply a JavaScript class as other classes, you can extend it and add any custom functionality including the questionable one:
// TypeScript
import Vue from 'vue';
import VueRouter, { RouteConfig } from 'vue-router';
class VueRouterEx extends VueRouter {
matcher: any;
public routes: RouteConfig[] = [];
constructor(options) {
super(options);
const { addRoutes } = this.matcher;
const { routes } = options;
this.routes = routes;
this.matcher.addRoutes = (newRoutes) => {
this.routes.push(...newRoutes);
addRoutes(newRoutes);
};
}
}
Vue.use(VueRouterEx);
const router = new VueRouterEx({
mode: 'history',
base: process.env.BASE_URL,
routes: [],
});
export default router;
So, from any component, you can get the routes using this.$router.routes