SEO JSON-LD improvement

This commit is contained in:
Sabrina Hennrich 2025-05-28 09:45:02 +02:00
parent d6884ba09c
commit ecd2829c95
7 changed files with 165 additions and 16 deletions

View File

@ -24,7 +24,7 @@ export function usePageMeta () {
// Canonical URL // Canonical URL
const config = useRuntimeConfig() const config = useRuntimeConfig()
const canonical = `${config.public.appUrl}${route.path}` const canonical = `${config.public.appUrl}${page.pageLink}`
// Robots Meta Tag // Robots Meta Tag
const robotsContent = route.path === '/danke' ? 'noindex, nofollow' : 'index, follow' const robotsContent = route.path === '/danke' ? 'noindex, nofollow' : 'index, follow'
@ -83,6 +83,7 @@ export function usePageMeta () {
{ property: 'og:title', content: metaTitle }, { property: 'og:title', content: metaTitle },
{ property: 'og:description', content: metaDescription }, { property: 'og:description', content: metaDescription },
{ property: 'og:image', content: metaImage }, { property: 'og:image', content: metaImage },
{ property: 'og:type', content: 'website' },
{ name: 'twitter:title', content: metaTitle }, { name: 'twitter:title', content: metaTitle },
{ name: 'twitter:description', content: metaDescription }, { name: 'twitter:description', content: metaDescription },
{ name: 'twitter:image', content: metaImage } { name: 'twitter:image', content: metaImage }

View File

@ -17,7 +17,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// Nuxt 3 behandelt den 404-Status automatisch import { createError } from 'nuxt/app'
// Wichtig: Der 404-Status **muss hier geworfen werden**
throw createError({
statusCode: 404,
statusMessage: 'Seite nicht gefunden'
})
</script> </script>
<style lang="sass"> <style lang="sass">

View File

@ -56,6 +56,7 @@
import { useHtmlConverter } from '@/composables/useHTMLConverter' import { useHtmlConverter } from '@/composables/useHTMLConverter'
import SideBarNaviSlider from '@/components/SideBarNaviSlider.vue' import SideBarNaviSlider from '@/components/SideBarNaviSlider.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const runtimeConfig = useRuntimeConfig();
const route = useRoute() const route = useRoute()
const slug = route.params.link const slug = route.params.link
@ -72,8 +73,6 @@
return articles.value.find(item => item.slug === slug) ?? null return articles.value.find(item => item.slug === slug) ?? null
}) })
const { convertToHTML } = useHtmlConverter() const { convertToHTML } = useHtmlConverter()
const htmlContent = (content) => convertToHTML(content) const htmlContent = (content) => convertToHTML(content)
@ -87,6 +86,74 @@
const d = new Date(article.value.dateModified) const d = new Date(article.value.dateModified)
return d.toLocaleDateString('de-DE') return d.toLocaleDateString('de-DE')
}) })
// ARTIKEL META AND JSON_LD INFOS
watchEffect(() => {
if (!article.value) return
const truncate = (text, maxLength) => {
if (!text) return ''
return text.length <= maxLength ? text : text.slice(0, maxLength - 1).trimEnd() + '…'
}
const rawTitle = article.value.header
const rawDescription = article.value.teaser
const metaTitle = truncate(rawTitle, 60)
const metaDescription = truncate(rawDescription, 160)
const metaImage = `${runtimeConfig.public.cmsBaseUrl}${article.value.image?.url}` || 'https://strapi.digimedialoop.de/uploads/DML_Logo_Info_c4011028f9.png'
const metaKeywords = article.value.SEO?.keywords || ''
const metaType = article.value.SEO?.type || 'article'
const canonical = `${runtimeConfig.public.appUrl}${route.fullPath}`
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'NewsArticle',
headline: rawTitle,
image: [metaImage],
datePublished: article.value.dateCreated,
dateModified: article.value.dateModified,
author: {
'@type': 'Person',
name: author
},
publisher: {
'@type': 'Organization',
name: 'digimedialoop',
logo: {
'@type': 'ImageObject',
url: 'https://strapi.digimedialoop.de/uploads/DML_Logo_Info_c4011028f9.png'
}
},
description: metaDescription
}
useHead({
title: metaTitle,
meta: [
{ name: 'description', content: metaDescription },
{ name: 'keywords', content: metaKeywords },
{ name: 'robots', content: 'index, follow' },
{ property: 'og:title', content: metaTitle },
{ property: 'og:description', content: metaDescription },
{ property: 'og:image', content: metaImage },
{ property: 'og:type', content: metaType },
{ name: 'twitter:title', content: metaTitle },
{ name: 'twitter:description', content: metaDescription },
{ name: 'twitter:image', content: metaImage }
],
link: [
{ rel: 'canonical', href: canonical }
],
script: [
{
type: 'application/ld+json',
children: JSON.stringify(jsonLd)
}
]
})
})
</script> </script>
<style scoped lang="sass"> <style scoped lang="sass">

View File

@ -33,7 +33,7 @@
format="webp" format="webp"
sizes="100vw" sizes="100vw"
class="background-image" class="background-image"
alt="" alt="AI and human"
aria-hidden="true" aria-hidden="true"
/> />
<div class="container-15 content"> <div class="container-15 content">

View File

@ -100,12 +100,9 @@ const localePath = useLocalePath()
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
const route = useRoute() const route = useRoute()
const router = useRouter()
import { storeToRefs } from 'pinia'
import { useMainStore } from '@/stores/main' import { useMainStore } from '@/stores/main'
const mainStore = useMainStore() const mainStore = useMainStore()
const { cmsUrl, projects, dataFetched } = storeToRefs(mainStore)
const project = computed(() => mainStore.getProjectByLink(route.params.link)) const project = computed(() => mainStore.getProjectByLink(route.params.link))
const customer = computed(() => { const customer = computed(() => {
if (!project.value || !project.value.customer) return null if (!project.value || !project.value.customer) return null
@ -125,8 +122,6 @@ watch(project, (newProject) => {
} }
}) })
import { useHtmlConverter } from '@/composables/useHTMLConverter' import { useHtmlConverter } from '@/composables/useHTMLConverter'
const { convertToHTML } = useHtmlConverter() const { convertToHTML } = useHtmlConverter()
const htmlContent = (data: string) => { const htmlContent = (data: string) => {
@ -138,7 +133,83 @@ const setCurrentImage = (image: any) => {
currentImage.value = image currentImage.value = image
} }
// META INFO UND JSON_LD für Projekte
const runtimeConfig = useRuntimeConfig()
watchEffect(() => {
if (!project.value) return
const metaTitle = project.value.projectTitle.length > 60
? project.value.projectTitle.slice(0, 57).trim() + '…'
: project.value.projectTitle
const firstParagraph = project.value.projectDescription?.[0]?.children?.[0]?.text || ''
const metaDescription = firstParagraph.length > 160
? firstParagraph.slice(0, 157).trim() + '…'
: firstParagraph
const customerCompany = project.value.customer?.company || ''
const customerCity = project.value.customer?.city || ''
const technologyKeywords = project.value.technologies?.map(t => t.titel).join(', ') || ''
const metaKeywords = [customerCompany, customerCity, technologyKeywords]
.filter(Boolean)
.join(', ')
const metaImage = project.value.projectImages?.[0]?.url
? `${runtimeConfig.public.cmsBaseUrl}${project.value.projectImages[0].url}`
: 'https://strapi.digimedialoop.de/uploads/DML_Logo_Info_c4011028f9.png'
const canonical = `${runtimeConfig.public.appUrl}${route.fullPath}`
const jsonLd = {
"@context": "https://schema.org",
"@type": "CreativeWork",
name: project.value.projectTitle,
description: metaDescription,
url: canonical,
image: metaImage,
datePublished: project.value.launchDate,
author: {
"@type": "Organization",
name: "digimedialoop"
},
publisher: {
"@type": "Organization",
name: "digimedialoop",
logo: {
"@type": "ImageObject",
url: "https://strapi.digimedialoop.de/uploads/DML_Logo_Info_c4011028f9.png"
}
}
}
useHead({
title: metaTitle,
meta: [
{ name: 'description', content: metaDescription },
{ name: 'keywords', content: metaKeywords },
{ name: 'robots', content: 'index, follow' },
{ property: 'og:title', content: metaTitle },
{ property: 'og:description', content: metaDescription },
{ property: 'og:image', content: metaImage },
{ property: 'og:type', content: 'website' },
{ name: 'twitter:title', content: metaTitle },
{ name: 'twitter:description', content: metaDescription },
{ name: 'twitter:image', content: metaImage }
],
link: [
{ rel: 'canonical', href: canonical }
],
script: [
{
type: 'application/ld+json',
children: JSON.stringify(jsonLd)
}
]
})
})
</script> </script>
<style lang="sass"> <style lang="sass">

View File

@ -93,14 +93,18 @@ const jsonLdProjects = computed(() => {
"itemListElement": projects.value.map((project, index) => ({ "itemListElement": projects.value.map((project, index) => ({
"@type": "ListItem", "@type": "ListItem",
"position": index + 1, "position": index + 1,
"url": origin + (project.link ? getProjectLink(project.link) : ''), "item": {
"@type": "CreativeWork", // alternativ "WebPage", wenn es sich um Projektseiten handelt
"@id": origin + (project.link ? getProjectLink(project.link) : ''),
"name": project.projectTitle || 'Projekt', "name": project.projectTitle || 'Projekt',
"image": cmsUrl + (project.projectImages?.[0]?.url) || '', "image": cmsUrl + (project.projectImages?.[0]?.url || ''),
"description": project.projectDescription?.[0]?.children?.[0]?.text || '' "description": project.projectDescription?.[0]?.children?.[0]?.text || ''
}
})) }))
} }
}) })
// useHead einbinden, wenn Projekte da sind // useHead einbinden, wenn Projekte da sind
watchEffect(() => { watchEffect(() => {
if (jsonLdProjects.value) { if (jsonLdProjects.value) {

View File

@ -6,7 +6,7 @@
src="/uploads/large_DML_Home_Hero_4f27bc7f8e.webp" src="/uploads/large_DML_Home_Hero_4f27bc7f8e.webp"
class="hero-bg" class="hero-bg"
sizes="sm:100vw md:100vw lg:100vw" sizes="sm:100vw md:100vw lg:100vw"
alt="" alt="website example"
aria-hidden="true" aria-hidden="true"
priority priority
loading="eager" loading="eager"