Files
site/src/pages/blog/[slug].vue
T

195 lines
6.5 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>