Is there a way to reference MetaInfo in layouts for Gridsome? - vue.js

Using Gridsome, I want to reference any given page's MetaInfo like the following in the layout component:
<h1 class="page-title">
{{ metaInfo.title }}
</h1>
In the page I do have the metaInfo defined like this (and that does fill the head correctly)
export default {
metaInfo: {
title: 'About'
}
}

You must change your metaInfo Object to a function and return the title object.
Try this:
<h1 class="page-title">
{{ $metaInfo.title }}
</h1>
And in your export default use this:
metaInfo() {
return {
title: 'About'
};
}

Related

Can't get nuxt-link to work when trying to render _slug.vue page with Nuxt, Apollo and WPGraphQL

I have a nuxt/vue app using apollo to query WPGraphQL on Wordpress. Im having hard time setting up my nuxt-link on my index page to route to my _slug.vue page. If I manually enter the url on the browser using the post's slug, I am able to render the data I want. In my index page, how do I use the post.slug with params to get my _slug.vue page to render?
This is my GraphQL Query:
post(id: $slug, idType: SLUG) {
title
slug
date
id
content(format: RENDERED)
author {
node {
firstName
lastName
}
}
}
}
My /blog/index.vue page has the list of blog posts and I am trying to use nuxt-link to link each post to render _slug.vue:
<template>
<div class="blog">
<h1 class="blog__title">Blog</h1>
<nuxt-link
v-for="post in posts.nodes"
:key="post.slug"
:to="'blog/' + { params: { slug: post.slug } }"
class="blog__post"
>
<h3 class="blog__post-title">
{{ post.title }}
</h3>
<div class="blog__post-content" v-html="post.content" />
</nuxt-link>
</div>
</template>
<script>
import getPostByID from '~/apollo/queries/GetPostByID'
export default {
data() {
return {
posts: [],
query: ''
}
},
apollo: {
posts: {
prefetch: true,
query: getPostByID,
update: (data) => data.post
}
}
</script>
With my _slug.vue file below, It uses the same query as my blog page and is able to render if I type the proper url with slug on the browser:
<template>
<article class="post">
<h1>{{ post.title }}</h1>
<div class="blog__post-content" v-html="post.content" />
</article>
</template>
<script>
import GetPostByID from '~/apollo/queries/GetPostById'
export default {
data() {
return {
post: []
}
},
apollo: {
post: {
prefetch: true,
query: GetPostByID,
variables() {
return { slug: this.$route.params.slug }
}
}
}
}
</script>
And what exactly does ".slug" refer to from "this.$route.params.slug"?
If your index page is correctly displaying the list of posts, then you just need to adjust the url slightly.
<nuxt-link
v-for="post in posts.nodes"
:key="post.slug"
:to="'blog/' + { params: { slug: post.slug } }"
class="blog__post"
>
Should be:
<nuxt-link
v-for="post in posts.nodes"
:key="post.slug"
:to="`blog/${post.slug}`"
class="blog__post"
>
this.$route.params.slug refers to the url parameter you named by creating the dynamic file _slug.vue. So if you have pages/blog/_slug.vue and navigate to your-app.com/blog/my-first-post, my-first-post is the parameter string you get back when accessing this.$route.params.slug.
Slug isn’t a magical keyword, it could be anything instead and depends on the file name you create in your blog directory. Given the same url and pages/blog/_unicorn.vue, you would call this.$route.params.unicorn to return my-first-post.

How to 'load' data into a Vue component?

Trying to wrap my head around components and I'm having a hard time translating the official vuejs.org guide examples (which largely use new Vue({...})) to the module structure offered with a fresh Vue app generated with the CLI.
If I have something like
const products = {
'bat': '9.99',
'glove': '7.50',
'cleets': '12.50'
}
How do I 'load' that into a component?
Products.vue
<template>
<div>
<li v-for="product in products">
{{ product }}
</li>
</div>
</template>
<script>
export default {
name: 'Products',
data: function () {
products: ?? // <-- My understanding is the products data would be added here somehow?
}
}
</script>
And then, in the 'Home.vue' component, something like
<template>
<div>
<Products />
</div>
</template>
<script>
import Products from "#/components/Products.vue";
export default {
name: "Home",
components: {
Products,
},
};
</script>
Any pointers? Thanks for any help! And sorry if it's something that should be obvious, it's frustrating to not be able to figure out a rather basic concept to Vue.js :(
Per #A.khalifa's response I modified Products.vue to the following
<script>
export default {
name: 'Products',
data: function() {
products: [{
'bat': '9.99',
'glove': '7.50',
'cleets': '12.50'
}]
}
}
</script>
That now returns the following errors
10:5 error Elements in iteration expect to have 'v-bind:key' directives vue/require-v-for-key
23:5 error 'products:' is defined but never used no-unused-labels
I'm not sure what to do about the v-bind:key directive... As for products am I not referencing it properly within the directive?
You are supposed to return an object from your data
data: function () {
return {
products: {
'bat': '9.99',
'glove': '7.50',
'cleets': '12.50'
}
}
}
or
data() {
return {
products: {
'bat': '9.99',
'glove': '7.50',
'cleets': '12.50'
}
}
}
and on your template
<li v-for="(price, name) in products" :key="name">
{{ name }} = {{ price }}
</li>
Read more about components basic here: https://v2.vuejs.org/v2/guide/components.html#Base-Example
And using v-for with object here: https://v2.vuejs.org/v2/guide/list.html#v-for-with-an-Object

Get page and page data by slug. Strapi and Nuxt

I have Strapi, and Nuxt with Apollo.
I get data using a dynamic query, but I can’t apply this data for the
title and meta description.
Now I get one page like this:
my query for product.gql
query Product($slug: String) {
products(where: { slug: $slug }) {
id
slug
name
image {
provider_metadata
}
description
}
}
my page page for one product _slug.vue
<template>
<div
class="flex flex-wrap pb-16"
v-for="product in products"
:key="product.slug"
>
<div v-if="product.image" class="w-1/2 flex items-center justify-center">
<cld-image
:publicId="product.image.provider_metadata.public_id"
width="450"
crop="scale"
/>
</div>
<div class="w-1/2">
<p class="font-bold uppercase text-2xl pb-4">
{{ product.category.name }} «{{ product.name }}»
</p>
<div
class="pb-4"
v-if="product.description"
v-html="$md.render(product.description)"
></div>
</div>
</div>
</section>
</template>
<script>
import productQuery from '#/apollo/queries/product/product'
export default {
data() {
return {
products: {},
api_url: process.env.API_URL
}
},
apollo: {
products: {
prefetch: true,
query: productQuery,
variables() {
let params = this.$route.params
return {
slug: params.slug
}
}
}
},
head() {
return {
// title: this.product.meta_title,
}
}
}
</script>
How to apply this data to output in head()?
You havent defined "meta_title" anywhere, so that data simply does not exist.
You need to add a simple text field to your content model called "meta_title" and add it to your query so you can define what it needs to be.
That, or you simply use
head() {
return {
// title: this.product.name
}
}

How to search within nested objects

I have done my research trying to figure out how to achieve what I am describing below, however I had no luck.
In my Algolia index, some records have nested objects.
For example, title and subtitle attributes are of the following format:
title:
{
"en": "English title",
"gr": "Greek title"
}
I would like to execute queries only for a specific subset (in our example "en" or "gr") of these attributes, withoute "exposing" any facet in the UI — language selection would ideally be done “automatically” based on a variable (lang) passed to the Vue component with props. I am using Laravel Scout package with default Vue implementation, as described in documentation here.
My InstantSearch implementation is pretty simple, I am not defining anything specific regarding queries and searchable attributes, I am currently using all the default functionality of Algolia.
<template>
<ais-instant-search
:search-client="searchClient"
index-name="posts_index"
>
<div class="search-box">
<ais-search-box placeholder="Search posts..."></ais-search-box>
</div>
<ais-hits>
<template
slot="item"
slot-scope="{ item }"
>
<div class="list-image">
<img :src="'/images/' + item.image" />
</div>
<div class="list-text">
<h2">
{{ item.title }}
</h2>
<h3>
{{ item.subtitle }}
</h3>
</div>
</template>
</ais-hits>
</ais-instant-search>
</template>
<script>
import algoliasearch from 'algoliasearch/lite';
export default {
data() {
return {
searchClient: algoliasearch(
process.env.ALGOLIA_APP_ID,
process.env.ALGOLIA_SEARCH
),
route: route,
};
},
props: ['lang'],
computed: {
computedItem() {
// computed_item = this.item;
}
}
};
</script>
I would like to somehow pass an option to query “title.en” and “subtitle.en” when variable lang is set to “en”. All this, without the user having to select “title.en” or “subtitle.en” in the UI.
Update
Maybe computed properties is the path to go, however I cannot find how to reference search results/hits attributes (eg item.title) within computed property. It is the code I have commented out.
I think, you can use computed property. Just transform current item according to the current language variable.
new Vue({
template: "<div>{{ computedItem.title }}</div>",
data: {
langFromCookie: "en",
item: {
title: {
en: "Hello",
ru: "Привет"
}
}
},
computed: {
computedItem() {
const item = JSON.parse(JSON.stringify(this.item));
for (value in item) {
if (typeof item[value] === "object" && Object.keys(item[value]).includes(this.langFromCookie))
item[value] = item[value][this.langFromCookie];
}
return item;
}
}
}).$mount("#app")
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
If lang variable is available via props, you can check that inside list-text class and return {{title.en}} or {{title.gr}} accordingly by passing a dynamic lang value title[lang] like below
...
<div class="list-text">
<h2>
{{ item.title[lang] }}
</h2>
<h3>
{{ item.subtitle[lang] }}
</h3>
</div>
If you want to make a request according to lang prop when component mounts ,then you can make a request inside mounted() method then query like below
mounted() {
axios.get(`/getSomethingWithLang/:${this.item.title[this.lang]}`)
...
}

Conditional <router-link> in Vue.js dependant on prop value?

Hopefully this is a rather simple question / answer, but I can't find much info in the docs.
Is there a way to enable or disable the anchor generated by <router-link> dependent on whether a prop is passed in or not?
<router-link class="Card__link" :to="{ name: 'Property', params: { id: id }}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
If there's no id passed to this component, I'd like to disable any link being generated.
Is there a way to do this without doubling up the content into a v-if?
Thanks!
Assuming you want to disable anchor tag as in not clickable and look disabled the option is using CSS. isActive should return true by checking prop id.
<router-link class="Card__link" v-bind:class="{ disabled: isActive }" :to="{ name: 'Property', params: { id: id }}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
<style>
.disabled {
pointer-events:none;
opacity:0.6;
}
<style>
If you want to just disable the navigation , you can use a route guard.
beforeEnter: (to, from, next) => {
next(false);
}
If you need to use it often, consider this:
Create new component
<template>
<router-link
v-if="!disabled"
v-bind="$attrs"
>
<slot/>
</router-link>
<span
v-else
v-bind="$attrs"
>
<slot/>
</span>
</template>
<script>
export default {
name: 'optional-router-link',
props: {
params: Object,
disabled: {
type: Boolean,
default: false,
},
},
};
</script>
Optional, register globally:
Vue.component('optional-router-link', OptionalRouterLink);
Use it as follows:
<optional-router-link
:disabled="isDisabled"
:to="whatever"
>
My link
</optional-router-link>
The problem is that router-link renders as an html anchor tag, and anchor tags do not support the disabled attribute. However you can add tag="button" to router-link:
<router-link :to="myLink" tag="button" :disabled="isDisabled" >
Vue will then render your link as a button, which naturally supports the disabled attribute. Problem solved! The downside is that you have to provide additional styling to make it look like a link. However this is the best way to achieve this functionality and does not rely on any pointer-events hack.
I sometimes do stuff like this:
<component
:is="hasSubLinks ? 'button' : 'router-link'"
:to="hasSubLinks ? undefined : href"
:some-prop="computedValue"
#click="hasSubLinks ? handleClick() : navigate"
>
<!-- arbitrary markup -->
</component>
...
computed: {
computedValue() {
if (this.hasSubLinks) return 'something';
if (this.day === 'Friday') return 'tgif';
return 'its-fine';
},
},
But I basically always wrap router-link, so you can gain control over disabled state, or pre-examine any state or props before rendering the link, with something like this:
<template>
<router-link
v-slot="{ href, route, navigate, isActive, isExactActive }"
:to="to"
>
<a
:class="['nav-link-white', {
'nav-link-white-active': isActive,
'opacity-50': isDisabled,
}]"
:href="isDisabled ? undefined : href"
#click="handler => handleClick(handler, navigate)"
>
<slot></slot>
</a>
</router-link>
</template>
<script>
export default {
name: 'top-nav-link',
props: {
isDisabled: {
type: Boolean,
required: false,
default: () => false,
},
to: {
type: Object,
required: true,
},
},
data() {
return {};
},
computed: {},
methods: {
handleClick(handler, navigate) {
if (this.isDisabled) return undefined;
return navigate(handler);
},
},
};
</script>
In my app right now, I'm noticing that some combinations of #click="handler => handleClick(handler, navigate)" suffer significantly in performance.
For example this changes routes very slow:
#click="isDisabled ? undefined : handler => navigate(handler)"
But the pattern in my full example code above works and has no performance issue.
In general, ternary operator in #click can be very dicey, so if you get issues, don't give up right away, try many different ways to bifurcate on predicates or switch over <component :is="" based on state. navigate itself is an ornery one because it requires the implicit first parameter to work.
I haven't tried, but you should be able to use something like Function.prototype.call(), Function.prototype.apply(), or Function.prototype.bind().
For example, you might be able to do:
#click="handler => setupNavigationTarget(handler, navigate)"
...
setupNavigationTarget(handler, cb) {
if (this.isDisabled) return undefined;
return this.$root.$emit('some-event', cb.bind(this, handler));
},
...
// another component
mounted() {
this.$root.$on('some-event', (navigate) => {
if (['Saturday', 'Sunday'].includes(currentDayOfTheWeek)) {
// halt the navigation event
return undefined;
}
// otherwise continue (and notice how downstream logic
// no longer has to care about the bound handler object)
return navigate();
});
},
You could also use the following:
<router-link class="Card__link" :to="id ? { name: 'Property', params: { id: id }} : {}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
If id is undefined the router won't redirect the page to the link.
I've tried different solutions but only one worked for me, maybe because I'm running if from Nuxt? Although theoretically nuxt-link should work exactly the same as router-link.
Anyway, here is the solution:
<template>
<router-link
v-slot="{ navigate }"
custom
:to="to"
>
<button
role="link"
#click="onNavigation(navigate, $event)"
>
<slot></slot>
</button>
</router-link>
</template>
<script>
export default {
name: 'componentName',
props: {
to: {
type: String,
required: true,
},
},
methods: {
onNavigation(navigate, event) {
if (this.to === '#other-action') {
// do something
} else {
navigate(event);
}
return false;
},
};
</script>