Files
sori.studio/pages/post/[slug].vue

103 lines
2.5 KiB
Vue

<script setup>
definePageMeta({
layout: 'post'
})
const route = useRoute()
const slug = computed(() => String(route.params.slug || ''))
const { data: post } = await useFetch(() => `/api/posts/${slug.value}`)
if (!post.value) {
throw createError({
statusCode: 404,
statusMessage: '게시물을 찾을 수 없습니다.'
})
}
const postTag = computed(() => post.value.tags?.[0]?.toUpperCase() || 'POST')
const config = useRuntimeConfig()
const siteUrl = computed(() => String(config.public.siteUrl || '').replace(/\/$/g, ''))
const pageUrl = computed(() => `${siteUrl.value}/post/${post.value.slug}`)
const seoTitle = computed(() => post.value.seoTitle || post.value.title)
const seoDescription = computed(() => post.value.seoDescription || post.value.excerpt || 'sori.studio 개인 블로그')
const canonicalUrl = computed(() => post.value.canonicalUrl || pageUrl.value)
const ogImage = computed(() => post.value.ogImage || post.value.featuredImage || '')
/**
* 절대 URL 생성
* @param {string} value - 원본 URL
* @returns {string} 절대 URL
*/
const toAbsoluteUrl = (value) => {
if (!value) {
return ''
}
if (/^https?:\/\//i.test(value)) {
return value
}
return `${siteUrl.value}${value.startsWith('/') ? value : `/${value}`}`
}
useHead(() => ({
title: seoTitle.value,
link: [
{
rel: 'canonical',
href: canonicalUrl.value
}
],
meta: [
{
name: 'description',
content: seoDescription.value
},
{
name: 'robots',
content: post.value.noindex ? 'noindex, nofollow' : 'index, follow'
},
{
property: 'og:title',
content: seoTitle.value
},
{
property: 'og:description',
content: seoDescription.value
},
{
property: 'og:url',
content: pageUrl.value
},
...(ogImage.value
? [
{
property: 'og:image',
content: toAbsoluteUrl(ogImage.value)
},
{
name: 'twitter:card',
content: 'summary_large_image'
}
]
: [])
]
}))
</script>
<template>
<ContentRenderer>
<ProseHeaderCard>
<p class="post-detail__eyebrow text-sm uppercase text-white/70">
{{ postTag }}
</p>
<h1 class="post-detail__title mt-3 text-4xl font-semibold leading-tight">
{{ post.title }}
</h1>
</ProseHeaderCard>
<ContentMarkdownRenderer class="post-detail__content" :content="post.content" />
</ContentRenderer>
</template>