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">
|
<script setup lang="ts">
|
||||||
|
import { KILLIAN_PERSON_ID } from '~/utils/seo-person'
|
||||||
|
import { resolveOgImage } from '~/utils/resolve-og-image'
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const localePath = useLocalePath()
|
const localePath = useLocalePath()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -38,6 +41,16 @@ const { data: surround } = await useAsyncData(
|
|||||||
{ watch: [locale] },
|
{ 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 {
|
interface SurroundArticle {
|
||||||
path: string
|
path: string
|
||||||
title: string
|
title: string
|
||||||
@@ -78,6 +91,13 @@ const readingMinutes = computed(() => {
|
|||||||
return useReadingTime(page.value?.description ?? '')
|
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 {
|
interface TocLink {
|
||||||
id: string
|
id: string
|
||||||
depth: number
|
depth: number
|
||||||
@@ -96,7 +116,37 @@ useSeoMeta({
|
|||||||
ogTitle: () => page.value?.title,
|
ogTitle: () => page.value?.title,
|
||||||
ogDescription: () => page.value?.description,
|
ogDescription: () => page.value?.description,
|
||||||
ogType: 'article',
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
Reference in New Issue
Block a user