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,101 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import FallbackChip from '@/components/FallbackChip.vue'
|
||||
import type { MediaItem } from '@/composables/useMedia'
|
||||
import type { Site } from '@/composables/useSites'
|
||||
import { hostOf } from '@/lib/directory'
|
||||
import { chipLabel } from '@/lib/seoCopy'
|
||||
import {
|
||||
socialDescription,
|
||||
socialTitle,
|
||||
type SeoPreviewInput,
|
||||
type SocialImageSource,
|
||||
} from '@/lib/seoPreview'
|
||||
|
||||
const props = defineProps<{
|
||||
input: SeoPreviewInput
|
||||
site: Site | undefined
|
||||
imageSource: SocialImageSource
|
||||
imageName: string
|
||||
imageItem: MediaItem | null
|
||||
}>()
|
||||
|
||||
const imageFailed = ref(false)
|
||||
|
||||
watch(
|
||||
() => props.imageItem?.url,
|
||||
() => {
|
||||
imageFailed.value = false
|
||||
},
|
||||
)
|
||||
|
||||
const host = computed(() => hostOf(props.site?.base_url ?? ''))
|
||||
const title = computed(() => socialTitle(props.input))
|
||||
const description = computed(() => socialDescription(props.input))
|
||||
const chips = computed(() => {
|
||||
const imageChips: Record<SocialImageSource, string | null> = {
|
||||
social: null,
|
||||
cover: chipLabel('cover_image'),
|
||||
site: chipLabel('site_image'),
|
||||
none: chipLabel('no_image'),
|
||||
}
|
||||
return [
|
||||
props.input.seo.social_title ? null : chipLabel('meta_title'),
|
||||
props.input.seo.social_description ? null : chipLabel('meta_description'),
|
||||
imageChips[props.imageSource],
|
||||
].filter((chip): chip is string => chip !== null)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2" data-testid="social-card">
|
||||
<span class="font-semibold text-highlighted">{{ t('Social card') }}</span>
|
||||
<UCard
|
||||
variant="outline"
|
||||
:ui="{
|
||||
root: 'rounded-none bg-default ring-0 border border-default divide-y-0 shadow-none',
|
||||
body: 'p-0 sm:p-0',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="social-card-image relative flex aspect-191/100 items-center justify-center overflow-hidden font-mono text-2xs text-muted"
|
||||
data-testid="social-card-image"
|
||||
>
|
||||
<span v-if="imageSource !== 'none'">{{ imageName }}</span>
|
||||
<img
|
||||
v-if="imageItem && !imageFailed"
|
||||
:src="imageItem.url"
|
||||
alt=""
|
||||
class="absolute inset-0 size-full object-cover"
|
||||
@error="imageFailed = true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5 border-t border-default px-3 py-2.5">
|
||||
<span class="text-2xs/4 text-muted uppercase">{{ host }}</span>
|
||||
<span
|
||||
class="truncate font-semibold text-highlighted"
|
||||
data-testid="social-card-title"
|
||||
>{{ title }}</span
|
||||
>
|
||||
<span
|
||||
class="line-clamp-2 text-xs/4.5 text-toned"
|
||||
data-testid="social-card-description"
|
||||
>{{ description }}</span
|
||||
>
|
||||
</div>
|
||||
</UCard>
|
||||
<div v-if="chips.length > 0" class="flex flex-wrap gap-1.5">
|
||||
<FallbackChip v-for="chip in chips" :key="chip" :label="chip" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.social-card-image {
|
||||
background:
|
||||
repeating-linear-gradient(135deg, var(--ui-border) 0 1px, transparent 1px 12px),
|
||||
var(--rc-sand);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user