feat: SEO SPA editor integration

This commit is contained in:
Florian Sylvain committed 2026-10-16 03:06:57 +02:00
1 parent eb9876fde3
commit 470584400f
62 files changed
+7471 -58

No files matched your search

+105
View File
@@ -0,0 +1,105 @@
import { t, locale } from '@/i18n'
import { chipLabel, descriptionChip, type ChipKey } from '@/lib/seoCopy'
import { formatAbsoluteDateFromIso } from '@/lib/relativeDate'
import type { ResolvedSeo } from '@/lib/seoTypes'
const MONTH_NAMES = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
]
const EMPTY_VALUE = '–'
export interface ApiPreviewRow {
key: string
value: string
chip: string | null
}
function pad(value: number) {
return value.toString().padStart(2, '0')
}
export function formatApiDate(iso: string | null) {
if (!iso) {
return EMPTY_VALUE
}
const date = new Date(iso)
const clock = `${pad(date.getHours())}:${pad(date.getMinutes())}`
return locale.value === 'fr'
? `${formatAbsoluteDateFromIso(iso)}, ${clock}`
: `${date.getDate()} ${MONTH_NAMES[date.getMonth()]} ${date.getFullYear()}, ${clock}`
}
function titleChip(source: string) {
return source === 'title' ? chipLabel('article_title') : null
}
function imageChip(source: string) {
const keys: Record<string, ChipKey> = {
cover: 'cover_image',
site: 'site_image',
none: 'no_image',
}
const key = keys[source]
return key ? chipLabel(key) : null
}
function canonicalChip(source: string) {
return source === 'site' ? chipLabel('canonical') : null
}
function authorLine(author: ResolvedSeo['authors'][number]) {
const links = author.same_as.length
return [
author.name,
author.url ? author.url.replace(/^https?:\/\//, '') : '',
links === 0 ? '' : links === 1 ? t('1 social link') : t('{0} social links', [links]),
]
.filter(Boolean)
.join(' · ')
}
function jsonLdSummary(jsonLd: Record<string, unknown>) {
const fields = Object.keys(jsonLd).filter((key) => !key.startsWith('@'))
return [String(jsonLd['@type'] ?? ''), fields.join(', ')].filter(Boolean).join(' · ')
}
export function apiPreviewRows(resolved: ResolvedSeo): ApiPreviewRow[] {
const { fallbacks } = resolved
return [
{ key: 'title', value: resolved.title, chip: titleChip(fallbacks.title) },
{
key: 'description',
value: resolved.description,
chip: descriptionChip(fallbacks.description),
},
{
key: 'canonical_url',
value: resolved.canonical_url,
chip: canonicalChip(fallbacks.canonical),
},
{ key: 'robots', value: resolved.robots, chip: null },
{ key: 'og.type', value: resolved.og.type, chip: null },
{ key: 'og.image', value: resolved.og.image, chip: imageChip(fallbacks.image) },
{ key: 'og.image_alt', value: resolved.og.image_alt, chip: null },
{ key: 'twitter.card', value: resolved.twitter.card, chip: null },
{
key: 'authors',
value: resolved.authors.map(authorLine).join('; ') || EMPTY_VALUE,
chip: null,
},
{ key: 'published_at', value: formatApiDate(resolved.published_at), chip: null },
{ key: 'modified_at', value: formatApiDate(resolved.modified_at), chip: null },
{ key: 'json_ld', value: jsonLdSummary(resolved.json_ld), chip: null },
]
}