Implement yoast seo in vue.js - vue.js

How can I implement yoast seo in vue.js?
I try to search an information in google but I could not found anything related. Do anyone have recommend?

You will need to hook the Wordpress REST API into your Vue.js app. Once you have the Yoast plugin installed, you can use a plugin such as this one in order to get the SEO data you require, or create your own endpoints for the queries that you need.

it's a quite complex topic. You either need an SSR setup (e.g. using Nuxt.js) or (what's actually better) you use either this theme:
http://vue-wordpress-demo.bshiluk.com
https://github.com/bucky355/vue-wordpress
or this:
https://wue-theme.tech-nomad.de
https://github.com/Tech-Nomad/wue-theme
The latter one is made by myself due to problems coming with Nuxt.js (window object, keep-alive not working, extra node.js server, not being able to use all the PHP templates). At the time I've started working on it the bucky355 theme didn't exist but it's quite similar to my. Even though I would consider my theme as more simple to use.

You can use vue-yoast components:
<template>
<div id="app">
<label>Title</label>
<input v-model="metaTitle" />
<label>Meta Description</label>
<input v-model="metaDescription" />
<label>Description</label>
<input v-model="description" />
<label>Focus Keyword</label>
<input v-model="focusKeyword" />
<snippet-preview
:title="metaTitle"
:description="metaDescription"
:url="url"
baseUrl="https://my-site.com/"
#update:titleWidth="(value) => titleWidth = value"
#update:titleLengthPercent="(value) => titleLengthPercent = value"
#update:descriptionLengthPercent="(value) => descriptionLengthPercent = value" />
<content-assessor
:title="metaTitle"
:titleWidth="titleWidth"
:description="metaDescription"
:url="url"
:text="description"
:locale="locale"
:resultFilter="assessorResultFilter" />
<seo-assessor
:keyword="focusKeyword"
:title="metaTitle"
:titleWidth="titleWidth"
:description="metaDescription"
:url="url"
:text="description"
:locale="locale"
:resultFilter="assessorResultFilter" />
</div>
</template>
<script>
import { SnippetPreview, ContentAssessor, SeoAssessor } from 'vue-yoast'
import 'vue-yoast/dist/vue-yoast.css'
export default {
name: 'App',
components: {
ContentAssessor,
SeoAssessor,
SnippetPreview
},
data () {
return {
focusKeyword: 'one',
metaTitle: 'Hello!',
metaDescription: 'The short description',
url: 'page/1',
description: '<h2>Here is subtitle!</h2> and some contents in HTML',
titleWidth: 0,
titleLengthPercent: 0,
descriptionLengthPercent: 0,
translations: null,
locale: 'en_US'
}
},
methods: {
assessorResultFilter (value) {
return value
}
}
}
</script>
<style>
#app {
max-width: 800px;
margin: 0 auto;
padding: 16px;
}
label {
display: block;
margin: 0;
padding: 0;
}
.vue-yoast {
margin-bottom: 10px;
}
</style>

Related

List Transitions work only for "enter" not for "leave"

Following the example in the docs, I'm using transition-group for a list of items. Strangely it works when items appear (enter), not when they disappear (leave), meaning they slide down in an animated fashion when appearing, but disappear instantly without animation: the leave animation failed. Why?
<template>
<div v-if="notifications.length">
<transition-group name="notifications">
<span
v-for="notification in notifications"
:key="notification.id"
>
<!-- content -->
</span>
</transition-group>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState({
notifications: state => state.notifications.notifications
})
}
}
</script>
<style lang="scss" scoped>
.notifications-enter-active,
.notifications-leave-active {
transition: all 0.5s;
}
.notifications-enter {
transform: translateY(-100%);
}
.notifications-leave-to {
opacity: 0;
}
</style>
Store
export const mutations = {
DELETE_NOTIFICATION (state, id) {
state.notifications.splice(
state.notifications.findIndex(item => item.id === id),
1
)
}
}
I couldn't reproduce the exact symptom with that code (demo 1), which only transitions on leave instead of enter in your scenario. The reason for that is because the span is display: inline, which prevents the transition.
The Vue docs provide a tip for this:
One important note is that these FLIP transitions do not work with elements set to display: inline. As an alternative, you can use display: inline-block or place elements in a flex context.
So, you can apply display: flex on the transition-group:
<template>
<transition-group class="container">
...
</transition-group>
</template>
<style>
.container {
display: flex;
}
</style>
demo 2
Or display: inline-block on the span to be transitioned:
<template>
<span class="notification-item">
...
</span>
</template>
<style>
.notification-item {
display: inline-block;
}
</style>
demo 3
Turns out by replacing <div v-if="notifications.length"> with <div v-if="notifications"> transitions now work. Even though this doesn't make any sense to me.
If anyone can explain in a comment that'd be nice :)

VueJS: Why parent components method unable to delete/destroy child's child (`vue2-dropzone`) component entirely?

I am creating a slider in vuejs and am using vue2-dropzone plugin for file uploads where each slide (slide-template.vue) has a vue2-dropzone component.
When app loads, image files are manually added in each vue2-dropzone (manuallyAddFile plugins API) queried from image API (hosted on heroku)
The issue is when I delete the first slide, calling the parent's (slider.vue) method removeSlideFn (passed down to child as prop) from child (slide-template.vue) component first slide is deleted but not entirely the dropzone images of the first slides are not destroyed and remains in the DOM, instead images of slide2, (the next slide) are deleted from the DOM (Pls try it once on codesandbox demo to actually know what I am mean). This does not happen when I delete slide2 or slide3 but only on slide1.
CodeSandBox Demo
App.vue
<template>
<div id="app">
<img width="15%" src="./assets/logo.png">
<slider />
</div>
</template>
<script>
import slider from "./components/slider";
export default {
name: "App",
components: {
slider
}
};
</script>
components\slider.vue (parent)
<template>
<div>
<hooper ref="carousel" :style="hooperStyle" :settings="hooperSettings">
<slide :key="idx" :index="idx" v-for="(slideItem, idx) in slideList">
<slide-template
:slideItem="slideItem"
:slideIDX="idx"
:removeSlideFn="removeCurrSlide" />
</slide>
<hooper-navigation slot="hooper-addons"></hooper-navigation>
<hooper-pagination slot="hooper-addons"></hooper-pagination>
</hooper>
<div class="buttons has-addons is-centered is-inline-block">
<button class="button is-info" #click="slidePrev">PREV</button>
<button class="button is-info" #click="slideNext">NEXT</button>
</div>
</div>
</template>
<script>
import {
Hooper,
Slide,
Pagination as HooperPagination,
Navigation as HooperNavigation
} from "hooper";
import "hooper/dist/hooper.css";
import slideTemplate from "./slide-template.vue";
import { slideShowsRef } from "./utils.js";
export default {
data() {
return {
sliderRef: "SlideShow 1",
slideList: [],
hooperSettings: {
autoPlay: false,
centerMode: true,
progress: true
},
hooperStyle: {
height: "265px"
}
};
},
methods: {
slidePrev() {
this.$refs.carousel.slidePrev();
},
slideNext() {
this.$refs.carousel.slideNext();
},
//Removes slider identified by IDX
removeCurrSlide(idx) {
this.slideList.splice(idx, 1);
},
// Fetch data from firebase
getSliderData() {
let that = this;
let mySliderRef = slideShowsRef.child(this.sliderRef);
mySliderRef.once("value", snap => {
if (snap.val()) {
this.slideList = [];
snap.forEach(childSnapshot => {
that.slideList.push(childSnapshot.val());
});
}
});
}
},
watch: {
getSlider: {
handler: "getSliderData",
immediate: true
}
},
components: {
slideTemplate,
Hooper,
Slide,
HooperPagination,
HooperNavigation
}
};
</script>
components/slide-template.vue (child, with vue2-dropzone)
<template>
<div class="slide-wrapper">
<slideTitle :heading="slideItem.heading" />
<a class="button delete remove-curr-slide" #click="deleteCurrSlide(slideIDX)" ></a>
<vue2Dropzone
#vdropzone-file-added="fileWasAdded"
#vdropzone-thumbnail="thumbnail"
#vdropzone-mounted="manuallyAddFiles(slideItem.zones)"
:destroyDropzone="false"
:include-styling="false"
:ref="`dropZone${ slideIDX }`"
:id="`customDropZone${ slideIDX }`"
:options="dropzoneOptions">
</vue2Dropzone>
</div>
</template>
<script>
import slideTitle from "./slide-title.vue";
import vue2Dropzone from "#dkjain/vue2-dropzone";
import { generate_ObjURLfromImageStream, asyncForEach } from "./utils.js";
export default {
props: ["slideIDX", "slideItem", "removeSlideFn"],
data() {
return {
dropzoneOptions: {
url: "https://vuejs-slider-node-lokijs-api.herokuapp.com/imageUpload",
thumbnailWidth: 150,
autoProcessQueue: false,
maxFiles: 1,
maxFilesize: 2,
addRemoveLinks: true,
previewTemplate: this.template()
}
};
},
components: {
slideTitle,
vue2Dropzone
},
methods: {
template: function() {
return `<div class="dz-preview dz-file-preview">
<div class="dz-image">
<img data-dz-thumbnail/>
</div>
<div class="dz-details">
<!-- <div class="dz-size"><span data-dz-size></span></div> -->
<!-- <div class="dz-filename"><span data-dz-name></span></div> -->
</div>
<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
<div class="dz-error-message"><span data-dz-errormessage></span></div>
<div class="dz-success-mark"><i class="fa fa-check"></i></div>
<div class="dz-error-mark"><i class="fa fa-close"></i></div>
</div>`;
},
thumbnail: function(file, dataUrl) {
var j, len, ref, thumbnailElement;
if (file.previewElement) {
file.previewElement.classList.remove("dz-file-preview");
ref = file.previewElement.querySelectorAll("[data-dz-thumbnail]");
for (j = 0, len = ref.length; j < len; j++) {
thumbnailElement = ref[j];
thumbnailElement.alt = file.name;
}
thumbnailElement.src = dataUrl;
return setTimeout(
(function(_this) {
return function() {
return file.previewElement.classList.add("dz-image-preview");
};
})(this),
1
);
}
},
// Drag & Drop Events
async manuallyAddFiles(zoneData) {
if (zoneData) {
let dropZone = `dropZone${this.slideIDX}`;
asyncForEach(zoneData, async fileInfo => {
var mockFile = {
size: fileInfo.size,
name: fileInfo.originalName || fileInfo.name,
type: fileInfo.type,
id: fileInfo.id,
childZoneId: fileInfo.childZoneId
};
let url = `https://vuejs-slider-node-lokijs-api.herokuapp.com/images/${
fileInfo.id
}`;
let objURL = await generate_ObjURLfromImageStream(url);
this.$refs[dropZone].manuallyAddFile(mockFile, objURL);
});
}
},
fileWasAdded(file) {
console.log("Successfully Loaded Files from Server");
},
deleteCurrSlide(idx) {
this.removeSlideFn(idx);
}
}
};
</script>
<style lang="scss">
.slide-wrapper {
position: relative;
}
[id^="customDropZone"] {
background-color: orange;
font-family: "Arial", sans-serif;
letter-spacing: 0.2px;
/* color: #777; */
transition: background-color 0.2s linear;
// height: 200px;
padding: 40px;
}
[id^="customDropZone"] .dz-preview {
width: 160px;
display: inline-block;
}
[id^="customDropZone"] .dz-preview .dz-image {
width: 80px;
height: 80px;
margin-left: 40px;
margin-bottom: 10px;
}
[id^="customDropZone"] .dz-preview .dz-image > div {
width: inherit;
height: inherit;
// border-radius: 50%;
background-size: contain;
}
[id^="customDropZone"] .dz-preview .dz-image > img {
width: 100%;
}
[id^="customDropZone"] .dz-preview .dz-details {
color: white;
transition: opacity 0.2s linear;
text-align: center;
}
[id^="customDropZone"] .dz-success-mark,
.dz-error-mark {
display: none;
}
.dz-size {
border: 2px solid blue;
}
#previews {
border: 2px solid red;
min-height: 50px;
z-index: 9999;
}
.button.delete.remove-curr-slide {
padding: 12px;
margin-top: 5px;
margin-left: 5px;
position: absolute;
right: 150px;
background-color: red;
}
</style>
slide-title.vue (not that important)
<template>
<h2 contenteditable #blur="save"> {{ heading }} </h2>
</template>
<script>
export default {
props: ["heading"],
methods: {
save() {
this.$emit("onTitleUpdate", event.target.innerText.trim());
}
}
};
</script>
utils.js (utility)
export async function generate_ObjURLfromImageStream(url) {
return await fetch(url)
.then(response => {
return response.body;
})
.then(rs => {
const reader = rs.getReader();
return new ReadableStream({
async start(controller) {
while (true) {
const { done, value } = await reader.read();
// When no more data needs to be consumed, break the reading
if (done) {
break;
}
// Enqueue the next data chunk into our target stream
controller.enqueue(value);
}
// Close the stream
controller.close();
reader.releaseLock();
}
});
})
// Create a new response out of the stream
.then(rs => new Response(rs))
// Create an object URL for the response
.then(response => {
return response.blob();
})
.then(blob => {
// generate a objectURL (blob:url/<uuid> list)
return URL.createObjectURL(blob);
})
.catch(console.error);
}
Technically this is how the app works, slider.vue loads & fetches data from database (firebase) and stores in a data array slideList, loops over the slideList & passes each slideData (prop slideItem) to vue-dropzone component (in slide-template.vue), when dropzone mounts it fires the manuallyAddFiles(slideItem.zones) on the #vdropzone-mounted custom event.
The async manuallyAddFiles() fetches image from an API (hosted on heroku), creates (generate_ObjURLfromImageStream(url)) a unique blob URL for the image (blob:/) and then calls plugins API dropZone.manuallyAddFile() to load the image into the corresponding dropzone.
To delete the current slide, child's deleteCurrSlide() calls parent's (slider.vue) removeSlideFn (passed as prop) method with the idx of current slide. The removeSlideFn use splice to remove the item at the corresponding array idx this.slideList.splice(idx, 1).
The problem is when I delete the first slide, first slide is deleted but not entirely, the dropzone images of the first slides are not destroyed and still remains in the DOM, instead the images of slide2, (the next slide) are deleted from the DOM.
CodeSandBox Demo
I am not sure what is causing the issue, may it's due to something in the vue's reactivity system OR Vue's Array reactivity caveat that is causing this.
Can anybody pls help me understand & resolve this and if possible point out the reason to the root of the problem.
Your help is much appreciated.
Thanks,
I think you probably missunderstand what is going on:
In VueJS there is a caching method which allow the reusing of existing component generated: - Each of your object are considered equals when rendered (at a DOM level).
So VueJS remove the last line because it is probably ask the least calculation and then recalcul the expected state. There are many side case to this (sometime, the local state is not recalculated). To avoir this: As recommended in the documentation, use :key to trace the id of your object. From the documentation:
When Vue is updating a list of elements rendered with v-for, by default it uses an “in-place patch” strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, Vue will patch each element in-place and make sure it reflects what should be rendered at that particular index. This is similar to the behavior of track-by="$index" in Vue 1.x.
This default mode is efficient, but only suitable when your list render output does not rely on child component state or temporary DOM state (e.g. form input values).
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item. An ideal value for key would be the unique id of each item. This special attribute is a rough equivalent to track-by in 1.x, but it works like an attribute, so you need to use v-bind to bind it to dynamic values...
new Vue({
el: "#app",
data: {
counterrow: 1,
rows: [],
},
methods: {
addrow: function() {
this.counterrow += 1;
this.rows.push({
id: this.counterrow,
model: ""
});
},
removerows: function(index) {
this.rows.splice(index, 1);
},
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<table>
<tr>
<td><input type="text" name="test1" /></td>
<td><button class="btn" #click="addrow">add row</button></td>
</tr>
<tr v-for="(row,index) in rows" :key="row.id">
<td><input type="text" name="test2" v-model="row.model" /></td>
<td><button class="btn" #click="removerows(index)">remove </button></td>
</tr>
</table>
</div>
In this code:
I corrected the fact counterrow was never incremented
I added a :key
The documentation of :key
What did you mean by
The problem is when I delete the first slide, first slide is deleted but not entirely, the dropzone images of the first slides are not destroyed and still remains in the DOM, instead the images of slide2, (the next slide) are deleted from the DOM.
From what I see, the elements are no longer in the DOM

VueJS + Nuxtjs Unexpected Token 'export'

So i have this code as my index page and It was working, but a couple minutes later it just stopped.
the error is:
SyntaxError
Unexpected token export
Within the script section, If i remove my import then the error will go away, but I need to import it and use it. It was working with the package being imported, but I have looked this code up and down I have no idea what the heck is going on.
Anyone have any suggestions? Am I dumb and have missed something so simple?
<template>
<section class='container'>
<img class='my-4' src="~/assets/images/carousel/1.png" alt="card" />
<div class='text-center mx-auto my-4'>
<button> Send a card </button>
<p class='subtle my-4'> Or </p>
<button class='btn-blue'> Open a card </button>
</div>
<div id="qrcode"></div>
</section>
</template>
<script>
import qrcode from 'qrcode-generator-es6'; <<<<<<<<< SYNTAX ERROR AROUND HERE
export default{
data : function(){
return {};
},
methods : {
},
mounted : function(){
const qr = new qrcode(0, 'M');
qr.addData('https://app.voxicard.com/?v=vx-9FEFCA66-F592-4FF5-97B8-93B2FD78666D');
qr.make();
document.getElementById('qrcode').innerHTML = qr.createSvgTag({
margin : 0,
cellColor : function(){
return "#48658B";
},
});
},
};
</script>
<style>
#qrcode {
width: 200px;
height: 200px;
background-color: red;
}
img {
display: block;
max-height: 500px;
text-align: center;
margin: auto;
}
button {
font-size: 125%;
}
</style>
In your build property in nuxt.config.js you'll need to add a transpile block that targets this library:
build: {
transpile: [
'qrcode-generator-es6'
]
}
This is due to the fact that nuxt expects libraries to export as CJS modules and not ES6 modules.
In nuxt.config.js replace export default { on module.exports = {

Vue: Using material-design-icons offline / size

I am using materialdesignicons in my vue project.
require ('../node_modules/#mdi/font/css/materialdesignicons.min.css);
Vue.use(Vuetify, {iconfont:'mdi'});
I have a handful of icons which I dynamically create:
<v-icon>{{ some-mdi-file }}</v-icon>
When I build for production via (npm run build) I get the following error:
asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
img/materialdesignicons-webfont.someHash.svg (3.77 MiB)
That file size is huge because it includes every icon, regardless of whether it's being used. Is there a way to trim that file size down by only packaging the specific icons used. Is there a different package I should be using? Caveat: The project is hosted offline, so I need to include the fonts directly in my project.
I looked at vue-material-design-icons but it looks like it may not work for dynamic icon names and it says nothing about the overall file size/performance.
I have also looked here but clicking on the 'size warning' link brings me to a page where the Vue portion is not filled out
https://dev.materialdesignicons.com/getting-started/webfont
I would recommend using the #mdi/js package for this which provides SVG paths for each icon and supports tree shaking. Currently Vuetify doesn't support SVG icons but it should in the future.
For now, it's easy enough to create a custom icon component:
<template>
<svg :class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path :d="path" />
</svg>
</template>
<script>
export default {
name: 'my-icon',
data: () => ({
path: '',
}),
methods: {
updatePath() {
if (!this.$scopedSlots) return
if (typeof this.$scopedSlots.default !== 'function') return
this.path = this.$scopedSlots
.default()
.map((n) => n.text)
.join('')
},
},
mounted() {
this.updatePath()
},
updated() {
this.updatePath()
},
}
</script>
<style scoped>
.icon {
display: block;
color: inherit;
fill: currentColor;
width: 24px;
height: 24px;
}
<style>
Then to use it you just need to import your component and the icon you want to use:
<template>
<div class="app">
<my-icon>{{mdiCheck}}</my-icon>
</div>
</template>
<script>
import MyIcon from 'path/to/my/icon.vue'
import { mdiCheck } from '#mdi/js'
export default {
name: 'my-app',
components: {
MyIcon,
}
data: () => ({
mdiCheck,
}),
}
</script>

VueJS single file component briefly showing SVG unstyled

I have a really simple Vue single-file component (using Vue 2.4.2) that includes an SVG image using a set of predefined SVG symbols and work perfectly.
I notice that the icon is briefly shown unstyled before the component (non-scoped) style is applied. Important to note that:
When including the scss in our main .scss file, the problem does not occur
Using v-cloak with has no effect
Occurs on latest Chrome, FF and Safari (MacOS)
Q: I can obviously use the workaround of including it in the main scss file, but I was wondering if this is SVG-styling specific or if a delay is normal when using component-style?
My component (additional scss omitted):
<template>
<i class="icon" v-if="symbol" :class="{'icon-spin': spinning}">
<svg>
<use v-bind:xlink:href="symbol"></use>
</svg>
</i>
</template>
<script>
export default {
name: 'Icon',
props: {
icon: {type: String},
spinning: {type: Boolean, default: false}
},
computed: {
symbol () {
return this.icon ? '#' + this.icon : ''
}
}
}
</script>
<style lang="scss">
#import '../../style/variables';
.icon {
display: inline-block;
width: $icon-size;
height: $icon-size;
line-height: 1;
svg {
width: 100%;
height: 100%;
fill: currentColor;
}
...