feat: SEO SPA editor integration
This commit is contained in:
1 parent
eb9876fde3
commit
470584400f
62 files changed
+7471
-58
No files matched your search
@@ -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 },
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user