feat: public API, SEO and existing surfaces
This commit is contained in:
1 parent
45fbbc33b1
commit
c390fcfe8d
103 files changed
+4476
-231
No files matched your search
+110
-17
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user