I am tryin to create an FAQ component in Nuxt.js that dynamically generates JSON-LD scripts for structured data. The structured data represents an FAQ page with questions and answers.https://developers.google.com/search/docs/appearance/structured-data/faqpage
My current Problem is that it's called too early I think and the content of my component is not attached.
export default defineNuxtPlugin((nuxtApp) => {
const metaLdScripts = []
nuxtApp.$addLdScript = (script) => metaLdScripts.push(script)
const attachLdScripts = () => {
const head = document.getElementsByTagName('head')[0]
const scriptTag = document.createElement('script')
scriptTag.type = 'application/ld+json'
scriptTag.text = `{
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: ${JSON.stringify(metaLdScripts)},
}`
head.appendChild(scriptTag)
}
nuxtApp.hook('page:finish', () => {
attachLdScripts()
})
})
This plugins is called in a different component and it should be filled with it's data.
<template>
<div v-editable="blok" class="c-item c-item-collapse">
<button @click="toggleCollapse" class="c-item-collapse__header">
<div class="c-item-collapse__title">
<ui-headline size="0" :tagName="blok.title_headline">
{{ blok.title }}
</ui-headline>
</div>
<div class="c-item-collapse__actions">
<ui-icon v-if="!isCollapsed" icon="circle-minus" />
<ui-icon v-else icon="circle-plus" />
</div>
</button>
<div></div>
<div v-show="!isCollapsed" class="c-item-collapse__content">
<StoryblokComponent
v-for="blok in blok.items"
:key="blok._uid"
:blok="blok"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const { $addLdScript } = useNuxtApp()
const isCollapsed = ref(true)
function toggleCollapse() {
isCollapsed.value = !isCollapsed.value
}
const { blok } = defineProps({ blok: Object })
onMounted(() => {
$addLdScript({
'@type': 'Question',
name: blok.title,
acceptedAnswer: {
'@type': 'Answer',
text: blok.items.map(
(item) => item.body?.content[0]?.content[0]?.text
),
},
})
})
</script>
<style lang="scss">
.c-item-collapse {
@apply p-8 bg-white rounded-xl md:rounded;
.ui-section--bg-white & {
// box-shadow: 0px 0px 25px 0px #22222214;
@apply bg-gray-light;
}
&__header {
@apply flex justify-between items-center text-graphite w-full;
}
&__actions {
@apply flex items-center;
}
&__title {
@apply font-black font-barlow text-left text-sm md:text-base;
}
&__content {
@apply pt-4 text-xs md:text-base;
}
}
.c-item-collapse + .c-item-collapse {
@apply mt-2 md:mt-4;
}
</style>
Output:
<script type="application/ld+json">{
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: [],
}</script>
I am using a custom plugin because I don't want to have several application/ld+json script tags in the Head for the Item-crawler component since it's all going to be FAQPages and kinda mess up the Google Rich Text Result schema. I tried using the https://github.com/ymmooot/nuxt-jsonld/blob/master/README.md calling the module in different render times, but that didn't work. It still makes separate script tags for each item crawler component in the Page.
I am pretty sure the problem is the timing. After everything is rendered the plugin should be called and executed, I think just setting a timer would be quite some bad practice Ig.I tried nearly all lifecycle hooks presented in the vue docs.