195 lines
6.5 KiB
Vue
195 lines
6.5 KiB
Vue
<script setup lang="ts">
|
||
import { parseBlogContent } from '~/utils/blogContent'
|
||
import { resolveCmsUrl } from '~/utils/cms'
|
||
|
||
const route = useRoute()
|
||
const config = useRuntimeConfig()
|
||
const { getBlogPostBySlug } = useNoroData()
|
||
|
||
const slug = computed(() => route.params.slug as string)
|
||
const post = computed(() => getBlogPostBySlug(slug.value))
|
||
const apiBase = computed(() => String(config.public.cmsApiUrl || ''))
|
||
|
||
watchEffect(() => {
|
||
if (!post.value) {
|
||
throw createError({ statusCode: 404, statusMessage: 'مطلب یافت نشد' })
|
||
}
|
||
})
|
||
|
||
const contentBlocks = computed(() =>
|
||
post.value?.content
|
||
? parseBlogContent(post.value.content, apiBase.value)
|
||
: []
|
||
)
|
||
|
||
const coverUrl = computed(() =>
|
||
post.value?.coverUrl ? resolveCmsUrl(post.value.coverUrl, apiBase.value) : ''
|
||
)
|
||
|
||
const pageTitle = computed(
|
||
() => post.value?.seoTitle || (post.value ? `${post.value.title} — استودیو نورو` : 'وبلاگ')
|
||
)
|
||
|
||
const pageDescription = computed(
|
||
() => post.value?.seoDescription || post.value?.excerpt || 'مقالات استودیو نورو'
|
||
)
|
||
|
||
const canonicalUrl = computed(() => `https://studionoro.ir/blog/${slug.value}`)
|
||
|
||
function formatDate(value?: string) {
|
||
if (!value) return ''
|
||
return new Intl.DateTimeFormat('fa-IR', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
}).format(new Date(value))
|
||
}
|
||
|
||
useHead({
|
||
title: pageTitle,
|
||
meta: computed(() => [
|
||
{ name: 'description', content: pageDescription.value },
|
||
{ property: 'og:type', content: 'article' },
|
||
{ property: 'og:title', content: pageTitle.value },
|
||
{ property: 'og:description', content: pageDescription.value },
|
||
{ property: 'og:url', content: canonicalUrl.value },
|
||
...(coverUrl.value ? [{ property: 'og:image', content: coverUrl.value }] : []),
|
||
{ name: 'twitter:card', content: coverUrl.value ? 'summary_large_image' : 'summary' },
|
||
{ name: 'twitter:title', content: pageTitle.value },
|
||
{ name: 'twitter:description', content: pageDescription.value },
|
||
...(coverUrl.value ? [{ name: 'twitter:image', content: coverUrl.value }] : []),
|
||
]),
|
||
link: [{ rel: 'canonical', href: canonicalUrl.value }],
|
||
script: computed(() => {
|
||
if (!post.value) return []
|
||
return [
|
||
{
|
||
type: 'application/ld+json',
|
||
innerHTML: JSON.stringify({
|
||
'@context': 'https://schema.org',
|
||
'@type': 'Article',
|
||
headline: post.value.title,
|
||
description: pageDescription.value,
|
||
image: coverUrl.value ? [coverUrl.value] : undefined,
|
||
datePublished: post.value.publishedAt,
|
||
author: {
|
||
'@type': 'Organization',
|
||
name: 'استودیو نورو',
|
||
},
|
||
publisher: {
|
||
'@type': 'Organization',
|
||
name: 'استودیو نورو',
|
||
},
|
||
mainEntityOfPage: canonicalUrl.value,
|
||
}),
|
||
},
|
||
]
|
||
}),
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="post">
|
||
<section class="relative overflow-hidden border-b border-noro-border">
|
||
<GlowBackground />
|
||
<div class="noro-container relative py-12 sm:py-16">
|
||
<NuxtLink
|
||
to="/blog"
|
||
class="mb-8 inline-flex items-center gap-2 text-sm text-noro-muted transition hover:text-noro-brand"
|
||
>
|
||
<span aria-hidden="true">→</span>
|
||
بازگشت به وبلاگ
|
||
</NuxtLink>
|
||
|
||
<p v-if="post.publishedAt" class="text-sm text-noro-muted">
|
||
{{ formatDate(post.publishedAt) }}
|
||
</p>
|
||
<h1 class="mt-3 max-w-4xl text-3xl font-bold text-noro-text sm:text-4xl lg:text-5xl">
|
||
{{ post.title }}
|
||
</h1>
|
||
<p v-if="post.excerpt" class="mt-5 max-w-3xl text-lg leading-8 text-noro-muted">
|
||
{{ post.excerpt }}
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="noro-section">
|
||
<div class="noro-container">
|
||
<article class="mx-auto max-w-3xl">
|
||
<div v-if="coverUrl" class="mb-10 overflow-hidden rounded-2xl border border-noro-border">
|
||
<img
|
||
:src="coverUrl"
|
||
:alt="post.title"
|
||
class="aspect-[16/10] w-full object-cover"
|
||
decoding="async"
|
||
>
|
||
</div>
|
||
|
||
<div class="space-y-6 text-base leading-8 text-noro-muted">
|
||
<template v-for="(block, index) in contentBlocks" :key="index">
|
||
<h2
|
||
v-if="block.type === 'h2'"
|
||
class="pt-4 text-2xl font-bold text-noro-text"
|
||
>
|
||
{{ block.text }}
|
||
</h2>
|
||
<h3
|
||
v-else-if="block.type === 'h3'"
|
||
class="pt-2 text-xl font-bold text-noro-text"
|
||
>
|
||
{{ block.text }}
|
||
</h3>
|
||
<h4
|
||
v-else-if="block.type === 'h4'"
|
||
class="pt-1 text-lg font-semibold text-noro-text"
|
||
>
|
||
{{ block.text }}
|
||
</h4>
|
||
<p v-else-if="block.type === 'p'" class="whitespace-pre-line">
|
||
{{ block.text }}
|
||
</p>
|
||
<ul
|
||
v-else-if="block.type === 'ul'"
|
||
class="list-disc space-y-2 pr-6 marker:text-noro-brand"
|
||
>
|
||
<li v-for="(item, itemIndex) in block.items" :key="itemIndex">
|
||
{{ item }}
|
||
</li>
|
||
</ul>
|
||
<figure
|
||
v-else-if="block.type === 'img'"
|
||
class="overflow-hidden rounded-2xl border border-noro-border bg-noro-surface"
|
||
>
|
||
<img
|
||
:src="block.src"
|
||
:alt="block.alt"
|
||
class="w-full object-cover"
|
||
loading="lazy"
|
||
decoding="async"
|
||
>
|
||
<figcaption v-if="block.alt" class="px-4 py-3 text-center text-sm text-noro-muted">
|
||
{{ block.alt }}
|
||
</figcaption>
|
||
</figure>
|
||
</template>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="border-t border-noro-border bg-noro-surface/50 py-16">
|
||
<div class="noro-container text-center">
|
||
<h2 class="text-2xl font-bold text-noro-text">
|
||
برای طراحی لوگوی حرفهای آمادهاید؟
|
||
</h2>
|
||
<p class="mt-3 text-noro-muted">
|
||
با استودیو نورو در تماس باشید تا لوگویی متناسب با هویت برند شما طراحی کنیم.
|
||
</p>
|
||
<UiButton to="/contact" class="mt-6">
|
||
تماس با ما
|
||
</UiButton>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|