How to slug an url using Vuejs - vue.js

I'm building a website with several articles. I'm using Vue Router, and for the moment the urls of my articles look like /article/id, for example : http://localhost:8080/article/85.
How can I slug an URL with the article title so it can be http://localhost:8080/article/the-article-title for example ?
I have no idea what kind of code should I provide so here is my article route :
routes: [
{
path: '/article/:id',
component: require('../components/articlePage.vue').default,
name: 'article',
meta: {title: "article"}
},
]

First add a slug in your JSON object if it is not exist in your API or DB
{
id: 1,
title: 'Jungle Book',
slug: 'jungle-book',
showDate: 'Monday',
showTime: '19:10 - 20:55'
}
Change the path in your router index file according to your path and component.
And the path should have ":slug"
{
path: '/movies/:slug',
name: 'moviedetail',
component: MovieDetail
}
In the "router-link" add the slug after v-for
<router-link :to="'/movies/' + movie.slug">
Now get the data from your component
data() {
return {
movie: this.$store.state.data,
movieDetail: []
}
},
methods: {
getMovie(){
this.movie.forEach(e => {
if(e.slug == this.$route.params.slug){
this.movieDetail = e;
}
});
}
},
created() {
this.getMovie();
}

There are different approaches with increasing level of complexity and aspects taken care of.
So to start - in order to slug-ify articles, you have to introduce slugs to the application. Since it is mentioned in comments that all articles are fetched priorly, slugs can be added to each article data before saving them to store with custom function that converts words to kebab-case or one of helper libraries (e.g. dashify).
This will make possible to render list of article links using :slug as route param, instead of id and search in store for by param before rendering article page. Good thing is that it still possible to keep both options (slug and id) available by extending logic to search by 2 fields.
Depending on your target - that might be it. But the problem arises in case article title has been changed and user accesses article via externally saved link (shared in social media, indexed by search engines, etc). This defeats SEO.
In order to keep article accessible, it is a good practice to include slug as a required field on the back-end. Slug still can be generated with same approach, but in CMS - before article is stored in the database. In such case it can be double checked, manually edited (as slugs do not always represent full article title because of characters length, special symbols, etc) and be accessible for querying, thus removing hassle of string manipulation from the front-end application.
Also this creates options to configure 301 redirects to preserve indexation even after slug is changed by saving old slugs. But such problem is out of the scope of the current question, even though is a good practice.

Router:
{
path: '/:slug',
name: 'article',
component: articlePage
}
Component:
use beforeRouteEnter to show the slug
beforeRouteEnter(to, from, next) {
console.log(to.params.slug);
}

Related

How to define route parameters in Router.js depends on Urls in Nuxt Js?

I use Nuxt composition api in my project. I have 1 detail page and 2 different url comes from my API. I don't want to create different detail pages for every url. I use extendRoutes in my project and I need to solve this issue.
urls
/sport/asian-football-3581. -> sub category + news title
/video/world/mama-dog-rubble-in-india-3581246. -> category + subcategory + news title
router.js
extendRoutes(routes, resolve) {
routes.push(
{
name: 'detail',
path: '/:subCategory/:title(.*)-:id([0-9]+)',
component: resolve('pages/detail.vue'),
},
path: '/:subCategory/:title(.*)-:id([0-9]+)',
path: '/:category/:subCategory/:title(.*)-:id([0-9]+)',
Paths can come with these two types. I want to route them to same detail page.
And I need to catch subCategory, title, id and if it has category params also. In my solution, if second path comes, I cant catch parameters true
I found answer
path: '/:category/:subCategory?/:title(.*)-:id([0-9]+)',
this solves my problem

best practice to change language dependent content coming from APIs

Background:
I have an SPA (Vuejs) and I implemented the localization both on frontend and backend side. The content is updated successfully without reloading the page when I change the language.
Problem:
But some contents (like product description in the selected language) are coming from an API which is not updated automatically when I change the language. If I refresh the page or call the API again, it works.
Question:
What is the best practice to change language dependent content coming from APIs without page refresh and calling manually all the APIs.
Thanks!
I believe there are many ways of doing what you want to do.
For instance. We have models that have some fields that can be localised, but we only support three languages and the fields are relatively short.
In that scenario, we just have the backend rest api return all localised versions of the string, e.g.:
GET /products could return:
[{
sku: '1',
name: {
en: 'Product name',
nl: 'Productnaam',
fr: 'Nom du produit'
}
}]
which we then just show in VueJs based on the route parameter... {{product.name[$route.params.locale] || product.name[en]}} (we have a composable method for this in stead, but you get the idea.
However, for some endpoints you might not always want to return all the localised versions of the server, just because it's too large (e.g. blogposts that you post in multiple languages while you support 5 languages might just generate a payload that you don't want to...)
In such scenario's, you can just watch for changes on your locale in vuejs, and fetch the localised version.
A third option, which I don't like myself, is have the "language switch" basically reload the entire page...
Either way - there's many ways to do what you need, it all depends on your use case, and your personal preference.
When using the vue-i18n Plugin, instead of sending the localised Text from the backend you could just send variables like exampleText and have them translated in your vue code with {{$t(yourLangVar)}} and this.$t(yourLangVar)
I managed to do it, I don't know if it is a best practice, but it is a working solution and can be useful for others:
App.vue:
I have a LocaleChanger component which emits localChanged on every language change.
the views are loaded into router-view so I added rerenderKey to its key. And when it is changed, everything will be rerendered in the current view(including the APIs of the corresponding components).
And since all of my APIs are called in it, They will be called again.
<template>
<LocaleChanger #localChanged="forceRerender"/>
<div id="nav">
<router-link :to="{ name: 'Home', params: {'lang':$i18n.locale}}">{{$t('pages.home')}}</router-link> |
</div>
<router-view :key="rerenderKey"></router-view>
</template>
<script>
data () {
return {
rerenderKey: 0
};
},
methods: {
async forceRerender(){
await this.$router.push({name: this.$route.name, params: { ...this.$route.params, lang: this.$i18n.locale }})
this.rerenderKey += 1;
},
}
</script>
And since my API-s need the Accept-language header, I added it in main.js to the axios.interceptors.request
axios.interceptors.request.use(
function (config) {
config.headers['Accept-Language'] = store.state.locale;
return config;
},
function (error) {
return Promise.reject(error);
}
);

Extend several new paths to a single page in Nuxt

I am struggeling with the nuxt folder/route structure for a project:
I want to achieve:
All of them should show pages/data/index.vue:
www.example.com/data
www.example.com/data/region
www.example.com/data/region/industry
And then access the parameter region or industry via the $route-class.
If I add _region folder and an _industy.vue it will show those files and I want to show and use the index.vue.
EDIT: Since region and industry are probably dynamic.
You could use this setup in your nuxt.config.js file
export default {
router: {
extendRoutes(routes, resolve) {
routes.push(
{
name: 'data-region-industry',
path: '/data/*/*',
component: resolve(__dirname, 'pages/data/index.vue'),
},
{
name: 'data-region',
path: '/data/*',
component: resolve(__dirname, 'pages/data/index.vue'),
},
)
}
}
}
With this configuration, you can go to either /data, /data/:region or /data/:region/:industry with only your index.vue file. No need to make some strange directories or file, you can keep all in one single place.
PS: the order is important. Put the most specific at top, otherwise /data/* will also catch /data/*/* and you'll never reach data-region-industry. This can be double-checked pretty quickly in the router tab of the Vue devtools.
This was taken from the official documentation: https://nuxtjs.org/docs/2.x/features/file-system-routing#extendroutes
I highly recommend giving it a read, especially if you are using Named views.
As for the URL catch, never heard of $route-class but you could make some kind of split on /, pretty doable!
you could use query in the url instead of params.
www.example.com/data
www.example.com/data/?region=x
www.example.com/data/?region=x&?industry=y
you are still able to access the query data via $route.query. if you don't want to use query you have to manually overwrite vue router of nuxt as far as I know.

VueJS router alias to particular param

I have a lot of articles in my app, and the URL are written like this in Vue Router: /article/:id.
I have particular articles I want to "pin" and have easier URLs. For example: /pinned-article, which should point to /article/3274 and /other-pinned-article, pointing to /article/68173.
I though about adding this to my routes, but it doesn't work:
{ path: '/article/3274', component: Article, alias: '/pinned-article' }
I thought about something else, involving another component:
{ path: '/pinned-article/:id', component: PinnedArticle }
The component PinnedArticle silently aliasing the correct article with a command like router.alias in the <script> section, but it apparently doesn't exist.
Is there a way to solve this problem? I thought I could use some answers I read here in Stackvoverflow (for examples when it comes to redirect /me to /user/:id, but it doesn't apply.
Thanks in advance :)
addRoute
You can achieve this with Dynamic Routing, which is not the same as dynamic route matching, i.e. route params.
(This solution works in both Vue 3 and Vue 2 with Vue Router >= 3.5.0)
By using the addRoute method of Vue router, you can create routes at runtime. You can either use a redirect or not, depending on whether you want the url bar to read /article/3274 or /pinned.
Redirect
If you want the url to change from /pinned to /article/3274, use redirect:
methods: {
pinRoute() {
this.$router.addRoute({
path: '/pinned',
name: 'pinned',
redirect: { name: 'article', params: { id: 3274 }}
})
}
}
Access the route like:
this.$router.push('/pinned')
The above example assumes you give your Article route a name: 'article' property so you can redirect to it
Alias
You can keep the URL as /pinned using alias. Normally the alias would go on the existing Article route definition, but that doesn't work well with route params. You can use a "reverse alias" with a new route:
methods: {
pinRoute() {
this.$router.addRoute({
path: '/params/3274',
name: 'pinned',
alias: '/pinned',
component: () => import('#/views/Article.vue') // Article component path
})
}
}
Access the route like:
this.$router.push('/pinned')
Notes:
You'll probably want to pass an id argument to the pinRoute methods rather than hardcode them like in the examples above.
A nice thing about addRoute with either method above is if the route already exists, say, from the last time you called the method, it gets overwritten. So you can use the method as many times as you like to keep changing the destination of /pinned. (The docs in both Vue 2 and Vue 3 say the route definition will get overwritten, though Vue 2 router throws a duplicate route warning.)
Of course the pinned route won't automatically persist between app refreshes, so you'll need to save/load the pinned id (i.e. using localStorage, etc.) and run one of these methods on app load if you want that

Custom handling forward slashes in vue router ids

I have a use case for needing the id part of a vue route to contain unescaped forward slashes.
My current route looks like this:
{
path: '/browse/:path*',
component: browse,
name: 'browse',
displayName: 'Browse',
meta: { title: 'Browse' },
},
So when a user browses to the above url, the browse component is shown.
However, i want to use the id part of the path (:path*) to contain a nestable fielsystem like path to be consumed by my browse page.
For example the url /browse/project/project1 would take me two levels down in my tree to the project1 item.
Now, the problem i'm running into is that vue router is escaping my ids (path) when navigating programatically, and my url ends up like this: /browse/project%2Fproject1. This is non-ideal and does not look nice to the end user. Also, if the user does browse to /browse/project/project1 manually the app will work correctly and even keep the original encoding in the url bar.
So i could resolve this my making an arbitrary number of child paths and hope that the system never goes over these, but thats not a good way to solve my problem.
I should also clarify that the application will not know anything about the path after /browse as this is generated dynamically by the api that powers the app.
Is there a native way in vue-router to handale this? or should i change up how im doing things.
There is a more elegant solution without workarounds.
Vue router uses path-to-regexp module under the hood and constructions like
const regexp = pathToRegexp('/browse/:path*')
// keys = [{ name: 'browse', delimiter: '/', optional: true, repeat: true }]
https://github.com/pillarjs/path-to-regexp#zero-or-more
const regexp = pathToRegexp('/browse/:path+')
// keys = [{ name: 'browse', delimiter: '/', optional: false, repeat: true }]
https://github.com/pillarjs/path-to-regexp#one-or-more
set repeat flag to true. Any array parameter with repeat flag will be joined with the delimiter (default '/').
So you can pass a splitted array ['project','project1'] instead of 'project/project1' into router.push():
router.push( {name: 'browse', params: {path: ['project','project1']}} );
or
router.push( {name: 'browse', params: {path: 'project/project1'.split('/')}} );
So I managed to 'fix' this with a bit of a hack.
When creating my Vue router instance I am attaching a beforeEach function to replace any outgoing encodings of '/'. This will send the 'correct' URL I am looking for to the client.
const router = new Router({
mode: 'history',
routes,
});
router.beforeEach((to, from, next) => {
// hack to allow for forward slashes in path ids
if (to.fullPath.includes('%2F')) {
next(to.fullPath.replace('%2F', '/'));
}
next();
});
I just stumbled over your question while facing a similiar problem.
Think this is because an id shall identify one single resource and not a nested structure/path to a resource.
Though I haven't solve my problem yet, what you probably want to use is a customQueryString:
https://router.vuejs.org/api/#parsequery-stringifyquery
https://discourse.algolia.com/t/active-url-with-vue-router-for-facet-and-queries/3399
I fixed it by creating helpers for generating hrefs for :to attributes of vue router link.
First i made router accessible for my new helper service like here Access router instance from my service
Then i created router-helpers.js and here i made my helpers, here is an example
import Vue from 'vue'
import router from '../router.js'
// replace %2F in link by /
const hrefFixes = function(to) {
return to.replace(/%2F/g, '/')
}
// my link helper
Vue.prototype.$linkExample = attr => {
// create "to" object for router resolve
const to = { name: `route-name`, params: { param1: attr } }
// this will resolve "to" object, return href param as string
// and then i can replace %2F in that string
return hrefFixes(router.resolve(to).href)
}
Just include this service once in your Vue application an then just use this helper like this
<router-link :to="$linkExample(attr)">text</router-link>