106 lines
3.0 KiB
TypeScript
106 lines
3.0 KiB
TypeScript
import { t, locale } from '@/i18n'
|
||
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}`
|
||
}
|
||
|
||
const FALLBACK_CHIPS: Record<string, Record<string, () => string>> = {
|
||
title: { title: () => t('article title + template') },
|
||
description: {
|
||
body: () => t('first sentence of the body'),
|
||
excerpt: () => t('excerpt'),
|
||
site: () => t('site default description'),
|
||
},
|
||
image: {
|
||
cover: () => t('cover image'),
|
||
site: () => t('site social image'),
|
||
none: () => t('No image'),
|
||
},
|
||
canonical: { site: () => t('site address + slug') },
|
||
}
|
||
|
||
function fallbackChip(field: string, source: string) {
|
||
return FALLBACK_CHIPS[field]?.[source]?.() ?? 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: fallbackChip('title', fallbacks.title) },
|
||
{
|
||
key: 'description',
|
||
value: resolved.description,
|
||
chip: fallbackChip('description', fallbacks.description),
|
||
},
|
||
{
|
||
key: 'canonical_url',
|
||
value: resolved.canonical_url,
|
||
chip: fallbackChip('canonical', 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: fallbackChip('image', 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 },
|
||
]
|
||
}
|