feat(07-02): enrich blog article page with full SEO meta + Article/Breadcrumb JSON-LD
- D-15: useSeoMeta extended with ogImage (absolute via resolveOgImage),
ogUrl (canonical), ogLocale + ogLocaleAlternate (emitted only when bilingual
pair exists), twitterCard + twitterImage, article:published_time,
article:modified_time (fallback to date when updated absent — D-13),
articleAuthor
- SEO-11/SEO-15: useSchemaOrg([defineArticle, defineBreadcrumb])
— Article author/publisher reference global Person via @id=#killian
(from app/utils/seo-person.ts KILLIAN_PERSON_ID), image mirrors ogImage,
mainEntityOfPage = canonical; BreadcrumbList emits Accueil → Blog → title
- Pitfall 7: altExists query via queryCollection('blog_en'|'blog_fr') with
literal collection names (Vite extractor constraint)
- inLanguageTag computed cast to satisfy overly narrow defineArticle typings
without changing runtime emission
- Validated SSR: curl /fr/blog/test-kotlin-syntax returns og:image absolute,
article:published_time, Article JSON-LD (author @id=#killian), BreadcrumbList 3 items
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { KILLIAN_PERSON_ID } from '~/utils/seo-person'
|
||||
import { resolveOgImage } from '~/utils/resolve-og-image'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const localePath = useLocalePath()
|
||||
const route = useRoute()
|
||||
@@ -38,6 +41,16 @@ const { data: surround } = await useAsyncData(
|
||||
{ watch: [locale] },
|
||||
)
|
||||
|
||||
// Détecter la version dans l'autre langue (pour og:locale:alternate, D-15, Pitfall 7)
|
||||
const { data: altExists } = await useAsyncData(
|
||||
`blog-alt-${locale.value}-${slug}`,
|
||||
() =>
|
||||
isFr.value
|
||||
? queryCollection('blog_en').path(`/en/blog/${slug}`).first()
|
||||
: queryCollection('blog_fr').path(`/fr/blog/${slug}`).first(),
|
||||
{ watch: [locale] },
|
||||
)
|
||||
|
||||
interface SurroundArticle {
|
||||
path: string
|
||||
title: string
|
||||
@@ -78,6 +91,13 @@ const readingMinutes = computed(() => {
|
||||
return useReadingTime(page.value?.description ?? '')
|
||||
})
|
||||
|
||||
const SITE_URL = 'https://killiandalcin.fr'
|
||||
const ogImage = computed(() => resolveOgImage(page.value as { image?: string } | null))
|
||||
const canonicalUrl = computed(() => `${SITE_URL}${localePath('/blog/' + slug)}`)
|
||||
const publishedIso = computed(() => page.value?.date)
|
||||
const modifiedIso = computed(() => page.value?.updated ?? page.value?.date) // D-13
|
||||
const inLanguageTag = computed(() => (isFr.value ? 'fr-FR' : 'en-US')) as unknown as ComputedRef<'fr-FR'>
|
||||
|
||||
interface TocLink {
|
||||
id: string
|
||||
depth: number
|
||||
@@ -96,7 +116,37 @@ useSeoMeta({
|
||||
ogTitle: () => page.value?.title,
|
||||
ogDescription: () => page.value?.description,
|
||||
ogType: 'article',
|
||||
ogImage,
|
||||
ogUrl: canonicalUrl,
|
||||
ogLocale: () => (isFr.value ? 'fr_FR' : 'en_US'),
|
||||
ogLocaleAlternate: () => (altExists.value ? [isFr.value ? 'en_US' : 'fr_FR'] : []),
|
||||
twitterCard: 'summary_large_image',
|
||||
twitterImage: ogImage,
|
||||
articlePublishedTime: publishedIso,
|
||||
articleModifiedTime: modifiedIso,
|
||||
articleAuthor: () => ["Killian' Dal-Cin"],
|
||||
})
|
||||
|
||||
useSchemaOrg([
|
||||
defineArticle({
|
||||
headline: () => page.value?.title,
|
||||
description: () => page.value?.description,
|
||||
image: ogImage,
|
||||
datePublished: publishedIso,
|
||||
dateModified: modifiedIso,
|
||||
inLanguage: inLanguageTag,
|
||||
author: { '@id': KILLIAN_PERSON_ID },
|
||||
publisher: { '@id': KILLIAN_PERSON_ID },
|
||||
mainEntityOfPage: canonicalUrl,
|
||||
}),
|
||||
defineBreadcrumb({
|
||||
itemListElement: [
|
||||
{ name: () => t('blog.breadcrumb.home'), item: () => localePath('/') },
|
||||
{ name: () => t('blog.breadcrumb.blog'), item: () => localePath('/blog') },
|
||||
{ name: () => page.value?.title ?? '' },
|
||||
],
|
||||
}),
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
Reference in New Issue
Block a user