feat: public API, SEO and existing surfaces

This commit is contained in:
Florian Sylvain committed 2026-11-03 03:58:10 +01:00
1 parent 45fbbc33b1
commit c390fcfe8d
103 files changed
+4476 -231

No files matched your search

+110 -17
View File
@@ -9,6 +9,10 @@ import DirectoryPanel from '@/components/DirectoryPanel.vue'
import FallbackChip from '@/components/FallbackChip.vue'
import MediaField from '@/components/MediaField.vue'
import SearchPreview from '@/components/SearchPreview.vue'
import SeoLanguageCards from '@/components/SeoLanguageCards.vue'
import LocaleBadge from '@/components/localization/LocaleBadge.vue'
import LocaleButton from '@/components/localization/LocaleButton.vue'
import LocaleInput from '@/components/localization/LocaleInput.vue'
import SeoAdvancedSection from '@/components/SeoAdvancedSection.vue'
import SeoSocialSection from '@/components/SeoSocialSection.vue'
import SlugField from '@/components/SlugField.vue'
@@ -22,6 +26,14 @@ import { useSeoPanelSlug } from '@/composables/useSeoPanelSlug'
import type { Site } from '@/composables/useSites'
import { SEO_LIMITS, charCount, type ArticleSeo } from '@/lib/articleSeo'
import { chipLabel, descriptionChip } from '@/lib/seoCopy'
import {
altHelp,
descriptionBadge,
descriptionHelp,
pageLanguage,
titleHelp,
type LocalizedSeoContext,
} from '@/lib/localizedSeo'
import type { DirectoryNotice } from '@/lib/directoryTypes'
import { areaUi, fieldUi } from '@/lib/seoFieldUi'
import {
@@ -61,6 +73,7 @@ const props = defineProps<{
saving: boolean
notice: DirectoryNotice | null
focus: SeoFocus | null
localized?: LocalizedSeoContext | null
}>()
const emit = defineEmits<{
@@ -72,6 +85,7 @@ const emit = defineEmits<{
go: [field: SuggestionField]
dismiss: [code: SuggestionCode]
restore: []
'edit-slug': []
}>()
const [DefineSuggestions, Suggestions] = createReusableTemplate()
@@ -85,6 +99,10 @@ const focusTarget = ref<SeoFocus | null>(props.focus)
const device = ref<PreviewDevice>('desktop')
const seo = computed(() => props.modelValue)
const pageName = computed(() => (props.localized ? pageLanguage(props.localized) : ''))
const panelTitle = computed(() =>
props.localized ? t('Search and sharing · {0}', [pageName.value]) : t('Search and sharing'),
)
const siteName = computed(() => siteDisplayName(props.site))
const previewInput = computed<SeoPreviewInput>(() => ({
seo: seo.value,
@@ -250,6 +268,7 @@ function submit() {
:input="previewInput"
:site="site"
:slug="displaySlug"
:localized="localized"
/>
<SocialCardPreview
:input="previewInput"
@@ -262,7 +281,57 @@ function submit() {
<DefineFields>
<div class="flex min-w-0 flex-col gap-4.5">
<div class="flex flex-col gap-2">
<template v-if="localized">
<p class="text-sm text-toned" data-testid="seo-language-scope">
{{
t('Applies only to the {0} version. Page language: {1}.', [
pageName,
localized.lang,
])
}}
</p>
<SeoLanguageCards :context="localized" />
<div
class="flex flex-col gap-3 border border-default bg-default p-4"
data-testid="seo-address-card"
>
<DirectoryField
:label="t('Address (slug)')"
:help="
t(
'Unique within {0} and {1}. Editing the title never changes the address.',
[siteName, pageName],
)
"
>
<LocaleInput
:model-value="savedSlug"
mono
readonly
locked
:aria-label="t('Address (slug)')"
/>
</DirectoryField>
<div class="flex flex-col items-start gap-1.5">
<LocaleButton icon="i-lucide-pencil" @click="emit('edit-slug')">
{{ t('Edit slug') }}
</LocaleButton>
<p class="text-xs/4.5 text-muted">
{{
t('Editing the title never changes the slug. Only Edit slug does.')
}}
</p>
<p class="text-xs/4.5 text-highlighted">
{{
t(
'Links to the old address will stop working. RenewCMS does not create redirects.',
)
}}
</p>
</div>
</div>
</template>
<div v-else class="flex flex-col gap-2">
<SlugField
:label="t('URL slug')"
:mode="slugMode"
@@ -299,9 +368,13 @@ function submit() {
<div class="flex flex-col gap-1.5">
<DirectoryField
:label="t('Meta title')"
optional
:help="t('Recommended 30 to 60 characters. This is a guide, not a limit.')"
:label="localized ? t('Page title') : t('Meta title')"
:optional="!localized"
:help="
localized
? titleHelp(localized)
: t('Recommended 30 to 60 characters. This is a guide, not a limit.')
"
>
<template #hint>
<SoftCounter
@@ -321,14 +394,21 @@ function submit() {
@update:model-value="typeText('seo_title', String($event))"
/>
</DirectoryField>
<FallbackChip v-if="!seo.seo_title" :label="chipLabel('article_title')" />
<FallbackChip
v-if="!seo.seo_title && !localized"
:label="chipLabel('article_title')"
/>
</div>
<div class="flex flex-col gap-1.5">
<DirectoryField
:label="t('Meta description')"
optional
:help="t('Recommended 70 to 160 characters.')"
:label="localized ? t('Description') : t('Meta description')"
:optional="!localized"
:help="
localized
? descriptionHelp(localized)
: t('Recommended 70 to 160 characters.')
"
>
<template #hint>
<SoftCounter
@@ -348,8 +428,19 @@ function submit() {
@update:model-value="typeText('seo_description', String($event))"
/>
</DirectoryField>
<LocaleBadge
v-if="localized && descriptionBadge(localized)"
:tone="localized.resolved?.fallbacks.site_text_skipped ? 'neutral' : 'info'"
:icon="
localized.resolved?.fallbacks.site_text_skipped
? 'i-lucide-minus'
: 'i-lucide-languages'
"
class="self-start"
>{{ descriptionBadge(localized) }}</LocaleBadge
>
<FallbackChip
v-if="descriptionChipLabel && !seo.seo_description"
v-else-if="descriptionChipLabel && !seo.seo_description && !localized"
:label="descriptionChipLabel"
/>
</div>
@@ -396,14 +487,16 @@ function submit() {
@update:model-value="setCover"
/>
<DirectoryField
v-if="seo.cover_image_id !== null"
:label="t('Alt text')"
v-if="seo.cover_image_id !== null || localized"
:label="localized ? t('Image alternative text') : t('Alt text')"
:help="
seo.cover_image_alt
? ''
: t(
'Needed before the cover counts as ready. Sent as image alt in the API.',
)
localized
? altHelp()
: seo.cover_image_alt
? ''
: t(
'Needed before the cover counts as ready. Sent as image alt in the API.',
)
"
>
<UInput
@@ -459,7 +552,7 @@ function submit() {
<DirectoryPanel
wide
:title="t('Search and sharing')"
:title="panelTitle"
:submit-label="t('Save changes')"
:saving="saving"
:submit-disabled="excerptTooLong"