Replace the landing with the artifact design and rebuild the admin in Ant Design.
Adds consultation leads, section editors, and the one-time content reset for this release. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,52 +1,93 @@
|
||||
<template>
|
||||
<article v-if="post" class="min-h-screen px-6 pb-24 pt-28 md:px-10">
|
||||
<SiteNavbar
|
||||
:brand="chrome.brand"
|
||||
:logo-url="chrome.logoUrl"
|
||||
:timezone="chrome.timezone"
|
||||
/>
|
||||
<div class="mx-auto max-w-[760px]">
|
||||
<NuxtLink to="/blog" class="text-sm text-ash hover:text-volt">→ بازگشت به بلاگ</NuxtLink>
|
||||
<h1 class="mt-8 font-display text-3xl font-extrabold leading-tight md:text-5xl">
|
||||
{{ post.title }}
|
||||
</h1>
|
||||
<p class="mt-4 text-sm text-ash">
|
||||
{{ post.publishedAt ? new Date(post.publishedAt).toLocaleDateString("fa-IR") : "" }}
|
||||
</p>
|
||||
<div class="mt-10 whitespace-pre-line text-base leading-relaxed text-bone/90">{{ plainContent }}</div>
|
||||
<article v-if="post" class="py-24 relative overflow-hidden">
|
||||
<div class="absolute top-0 right-0 w-[500px] h-[500px] bg-brand-100/50 dark:bg-brand-600/15 rounded-full blur-[140px] pointer-events-none -z-10" />
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 text-right">
|
||||
<nav aria-label="مسیر صفحه" class="text-xs font-bold text-slate-400 dark:text-slate-500 mb-6">
|
||||
<ol class="flex flex-wrap items-center gap-2">
|
||||
<li><NuxtLink to="/" class="hover:text-brand-600 dark:hover:text-brand-400 transition-colors">خانه</NuxtLink></li>
|
||||
<li aria-hidden="true">/</li>
|
||||
<li><NuxtLink to="/blog" class="hover:text-brand-600 dark:hover:text-brand-400 transition-colors">بلاگ</NuxtLink></li>
|
||||
<li aria-hidden="true">/</li>
|
||||
<li aria-current="page" class="text-slate-500 dark:text-slate-400">{{ post.title }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<header class="space-y-4 mb-10">
|
||||
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-black text-slate-900 dark:text-white leading-[1.35] tracking-tight">{{ post.title }}</h1>
|
||||
<p v-if="post.excerpt" class="text-base sm:text-lg text-slate-600 dark:text-slate-300 leading-relaxed">{{ post.excerpt }}</p>
|
||||
<time v-if="post.publishedAt" :datetime="post.publishedAt" class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200/70 dark:border-slate-700/70 text-xs font-semibold text-slate-500 dark:text-slate-300">
|
||||
<span class="w-2 h-2 rounded-full bg-brand-500" />
|
||||
{{ formatFaDate(post.publishedAt) }}
|
||||
</time>
|
||||
</header>
|
||||
|
||||
<div v-if="post.coverUrl" class="mb-10 rounded-3xl overflow-hidden border border-slate-200/90 dark:border-slate-800 shadow-soft-card aspect-[16/9] bg-slate-100 dark:bg-slate-900">
|
||||
<img :src="post.coverUrl" :alt="post.title" width="1200" height="675" fetchpriority="high" class="w-full h-full object-cover">
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-900/90 rounded-3xl p-6 sm:p-10 border border-slate-200/90 dark:border-slate-800 shadow-soft-card">
|
||||
<div class="whitespace-pre-line text-base leading-loose text-slate-700 dark:text-slate-300">{{ plainContent }}</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-10">
|
||||
<NuxtLink to="/blog" class="inline-flex items-center gap-2.5 px-6 py-3 rounded-full border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-200 font-bold text-sm hover:border-brand-600 dark:hover:border-brand-400 hover:text-brand-600 dark:hover:text-brand-400 hover:bg-brand-50/50 dark:hover:bg-slate-800/50 transition-all duration-300">
|
||||
<svg class="w-4 h-4 text-brand-600 dark:text-brand-400 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
|
||||
<span>بازگشت به بلاگ</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const chrome = await useSiteChrome();
|
||||
const route = useRoute();
|
||||
const slug = String(route.params.slug);
|
||||
const { data: post } = await useAsyncData(`blog-${slug}`, () => fetchPublicPost(slug), {
|
||||
getCachedData: () => undefined,
|
||||
});
|
||||
const { data: post } = await useAsyncData(`blog-${slug}`, () => apiFetch<BlogPost>(`/api/public/blog/${encodeURIComponent(slug)}`).catch(() => null));
|
||||
|
||||
if (!post.value) {
|
||||
throw createError({ statusCode: 404, statusMessage: "پست پیدا نشد" });
|
||||
throw createError({ statusCode: 404, statusMessage: "پست پیدا نشد", fatal: true });
|
||||
}
|
||||
|
||||
const plainContent = computed(() => {
|
||||
const raw = post.value?.content || "";
|
||||
return raw
|
||||
const plainContent = computed(() =>
|
||||
(post.value?.content || "")
|
||||
.replace(/<\/p>/gi, "\n\n")
|
||||
.replace(/<br\s*\/?>/gi, "\n")
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.trim();
|
||||
});
|
||||
.trim(),
|
||||
);
|
||||
|
||||
const { canonical, siteUrl } = usePageSeo(
|
||||
() => ({
|
||||
title: post.value?.seoTitle || post.value?.title,
|
||||
description: post.value?.seoDesc || post.value?.excerpt,
|
||||
ogImage: post.value?.ogImage || post.value?.coverUrl,
|
||||
canonical: post.value?.canonical,
|
||||
}),
|
||||
{ type: "article" },
|
||||
);
|
||||
|
||||
useSeoMeta({
|
||||
title: post.value.seoTitle || post.value.title,
|
||||
description: post.value.seoDesc || post.value.excerpt,
|
||||
ogTitle: post.value.seoTitle || post.value.title,
|
||||
ogDescription: post.value.seoDesc || post.value.excerpt,
|
||||
ogImage: post.value.ogImage || post.value.coverUrl || undefined,
|
||||
articlePublishedTime: () => post.value?.publishedAt || undefined,
|
||||
articleModifiedTime: () => post.value?.updatedAt || undefined,
|
||||
});
|
||||
if (post.value.canonical) {
|
||||
useHead({ link: [{ rel: "canonical", href: post.value.canonical }] });
|
||||
}
|
||||
|
||||
useBreadcrumbJsonLd([
|
||||
{ name: "خانه", path: "/" },
|
||||
{ name: "بلاگ", path: "/blog" },
|
||||
{ name: post.value.title, path: `/blog/${slug}` },
|
||||
]);
|
||||
|
||||
useJsonLd(() => ({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
headline: post.value?.title,
|
||||
description: post.value?.seoDesc || post.value?.excerpt,
|
||||
datePublished: post.value?.publishedAt || undefined,
|
||||
dateModified: post.value?.updatedAt || post.value?.publishedAt || undefined,
|
||||
inLanguage: "fa-IR",
|
||||
mainEntityOfPage: canonical.value,
|
||||
...(post.value?.coverUrl ? { image: new URL(post.value.coverUrl, siteUrl).toString() } : {}),
|
||||
author: { "@id": `${siteUrl}/#organization` },
|
||||
publisher: { "@id": `${siteUrl}/#organization` },
|
||||
}));
|
||||
</script>
|
||||
|
||||
@@ -1,38 +1,55 @@
|
||||
<template>
|
||||
<div class="min-h-screen px-6 pb-24 pt-28 md:px-10">
|
||||
<SiteNavbar
|
||||
:brand="chrome.brand"
|
||||
:logo-url="chrome.logoUrl"
|
||||
:timezone="chrome.timezone"
|
||||
/>
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<h1 class="font-display text-4xl font-extrabold md:text-6xl">بلاگ</h1>
|
||||
<p class="mt-4 max-w-xl text-ash">یادداشتهایی درباره محصول، مهندسی و ساخت نرمافزار.</p>
|
||||
<ul class="mt-16 divide-y divide-white/[0.07] border-y border-white/[0.07]">
|
||||
<li v-for="post in posts" :key="post.id" class="py-8">
|
||||
<NuxtLink :to="`/blog/${post.slug}`" class="group block">
|
||||
<p class="text-sm text-ash">
|
||||
{{ post.publishedAt ? new Date(post.publishedAt).toLocaleDateString("fa-IR") : "" }}
|
||||
</p>
|
||||
<h2 class="mt-2 font-display text-2xl font-bold group-hover:text-volt md:text-3xl">
|
||||
{{ post.title }}
|
||||
</h2>
|
||||
<p class="mt-3 max-w-2xl text-sm text-ash">{{ post.excerpt }}</p>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="!posts?.length" class="mt-10 text-ash">هنوز پستی منتشر نشده است.</p>
|
||||
<section class="py-24 relative overflow-hidden">
|
||||
<div class="absolute top-0 right-0 w-[500px] h-[500px] bg-brand-100/50 dark:bg-brand-600/15 rounded-full blur-[140px] pointer-events-none -z-10" />
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<PageHeading
|
||||
:title="page?.title || 'بلاگ'"
|
||||
:subtitle="page?.seo?.description || 'یادداشتهایی درباره محصول، مهندسی و ساخت نرمافزار.'"
|
||||
:crumbs="[{ label: 'خانه', to: '/' }, { label: page?.title || 'بلاگ', to: '/blog' }]"
|
||||
/>
|
||||
|
||||
<div v-if="posts?.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<NuxtLink
|
||||
v-for="post in posts"
|
||||
:key="post.id"
|
||||
:to="`/blog/${post.slug}`"
|
||||
class="fade-item group bg-white dark:bg-slate-900/90 rounded-3xl border border-slate-200/90 dark:border-slate-800 shadow-soft-card hover:shadow-hover-card hover:border-brand-300 dark:hover:border-brand-500/50 transition-all duration-300 overflow-hidden flex flex-col text-right"
|
||||
>
|
||||
<div class="aspect-[16/9] bg-gradient-to-br from-brand-50 via-sky-50 to-blue-100/70 dark:from-slate-900 dark:via-[#0c1e3f] dark:to-slate-900 overflow-hidden">
|
||||
<SafeImage
|
||||
v-if="post.coverUrl"
|
||||
:src="post.coverUrl"
|
||||
:alt="post.title"
|
||||
width="640"
|
||||
height="360"
|
||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-6 space-y-2 flex-1 flex flex-col">
|
||||
<time v-if="post.publishedAt" :datetime="post.publishedAt" class="text-xs font-bold text-slate-400 dark:text-slate-500">{{ formatFaDate(post.publishedAt) }}</time>
|
||||
<h2 class="text-lg font-bold text-slate-900 dark:text-white group-hover:text-brand-600 dark:group-hover:text-brand-400 transition-colors">{{ post.title }}</h2>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 leading-relaxed font-normal flex-1">{{ post.excerpt }}</p>
|
||||
<span class="pt-3 inline-flex items-center gap-2 text-sm font-bold text-brand-600 dark:text-brand-400">
|
||||
<svg class="w-4 h-4 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
|
||||
ادامه مطلب
|
||||
</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<p v-else class="text-center text-slate-500 dark:text-slate-400">هنوز پستی منتشر نشده است.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const chrome = await useSiteChrome();
|
||||
const { data: posts } = await useAsyncData("blog-list", () => fetchPublicBlog(), {
|
||||
getCachedData: () => undefined,
|
||||
});
|
||||
const { data: page } = await useAsyncData("page-blog", () => fetchPage("blog"), {
|
||||
getCachedData: () => undefined,
|
||||
});
|
||||
applyPageSeo(page.value?.seo);
|
||||
const [{ data: posts }, { data: page }] = await Promise.all([
|
||||
useAsyncData("blog-list", () => apiFetch<BlogPostSummary[]>("/api/public/blog").catch(() => [])),
|
||||
usePageMeta("blog"),
|
||||
]);
|
||||
|
||||
usePageSeo(() => page.value?.seo ?? { title: "بلاگ — منتک" });
|
||||
useBreadcrumbJsonLd([
|
||||
{ name: "خانه", path: "/" },
|
||||
{ name: page.value?.title || "بلاگ", path: "/blog" },
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
<template>
|
||||
<div class="min-h-screen px-6 pb-24 pt-28 md:px-10">
|
||||
<SiteNavbar
|
||||
:brand="chrome.brand"
|
||||
:logo-url="chrome.logoUrl"
|
||||
:timezone="chrome.timezone"
|
||||
/>
|
||||
<div class="mx-auto max-w-[1500px]">
|
||||
<h1 class="font-display text-4xl font-extrabold md:text-6xl">گالری</h1>
|
||||
<p class="mt-4 max-w-xl text-ash">تصاویر منتخب از پروژهها و فضای استودیو.</p>
|
||||
<div class="mt-16 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<figure v-for="item in items" :key="item.id" class="overflow-hidden border border-white/[0.07] bg-coal">
|
||||
<div
|
||||
class="aspect-[4/3] bg-cover bg-center"
|
||||
:style="item.imageUrl ? { backgroundImage: `url(${item.imageUrl})` } : {}"
|
||||
/>
|
||||
<figcaption class="p-4">
|
||||
<p class="font-display text-lg font-bold">{{ item.title }}</p>
|
||||
<p v-if="item.category" class="mt-1 text-xs text-ash">{{ item.category }}</p>
|
||||
<section class="py-24 relative overflow-hidden">
|
||||
<div class="absolute top-0 right-0 w-[500px] h-[500px] bg-brand-100/50 dark:bg-brand-600/15 rounded-full blur-[140px] pointer-events-none -z-10" />
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<PageHeading
|
||||
:title="page?.title || 'گالری'"
|
||||
:subtitle="page?.seo?.description || 'تصاویر منتخب از پروژهها و فضای استودیو منتک.'"
|
||||
:crumbs="[{ label: 'خانه', to: '/' }, { label: page?.title || 'گالری', to: '/gallery' }]"
|
||||
/>
|
||||
|
||||
<div v-if="items?.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<figure
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
class="fade-item group bg-white dark:bg-slate-900/90 rounded-3xl border border-slate-200/90 dark:border-slate-800 shadow-soft-card hover:shadow-hover-card hover:border-brand-300 dark:hover:border-brand-500/50 transition-all duration-300 overflow-hidden text-right"
|
||||
>
|
||||
<div class="aspect-[4/3] bg-slate-100 dark:bg-slate-800 overflow-hidden">
|
||||
<SafeImage :src="item.imageUrl" :alt="item.title" width="640" height="480" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
|
||||
</div>
|
||||
<figcaption class="p-5">
|
||||
<p class="text-base font-bold text-slate-900 dark:text-white">{{ item.title }}</p>
|
||||
<p v-if="item.category" class="mt-1 text-xs text-slate-500 dark:text-slate-400">{{ item.category }}</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<p v-if="!items?.length" class="mt-10 text-ash">گالری خالی است.</p>
|
||||
<p v-else class="text-center text-slate-500 dark:text-slate-400">گالری خالی است.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const chrome = await useSiteChrome();
|
||||
const { data: items } = await useAsyncData("gallery", () => fetchPublicGallery(), {
|
||||
getCachedData: () => undefined,
|
||||
});
|
||||
const { data: page } = await useAsyncData("page-gallery", () => fetchPage("gallery"), {
|
||||
getCachedData: () => undefined,
|
||||
});
|
||||
applyPageSeo(page.value?.seo);
|
||||
const [{ data: items }, { data: page }] = await Promise.all([
|
||||
useAsyncData("gallery", () => apiFetch<GalleryItem[]>("/api/public/gallery").catch(() => [])),
|
||||
usePageMeta("gallery"),
|
||||
]);
|
||||
|
||||
usePageSeo(() => page.value?.seo ?? { title: "گالری — منتک" });
|
||||
useBreadcrumbJsonLd([
|
||||
{ name: "خانه", path: "/" },
|
||||
{ name: page.value?.title || "گالری", path: "/gallery" },
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -1,58 +1,56 @@
|
||||
<template>
|
||||
<div v-if="site">
|
||||
<SiteNavbar
|
||||
:brand="site.settings.brandName"
|
||||
:logo-url="site.settings.logoUrl"
|
||||
:timezone="site.settings.timezone"
|
||||
/>
|
||||
<main>
|
||||
<HeroSection :data="site.sections.hero" />
|
||||
<section class="relative overflow-hidden py-16 md:py-24">
|
||||
<div class="w-[110%] -translate-x-[5%] -rotate-2 bg-volt py-4 text-ink md:py-5 rtl:translate-x-[5%]">
|
||||
<SiteMarquee
|
||||
:items="site.sections.marquee.items"
|
||||
:duration="26"
|
||||
class-name="font-display text-xl font-bold tracking-[-0.01em] md:text-3xl"
|
||||
item-class-name="mx-4 md:mx-6"
|
||||
separator-class-name="text-ink/50"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<ManifestoSection
|
||||
:data="site.sections.manifesto"
|
||||
:intro="{ body: site.sections.hero.body, cta: site.sections.hero.cta }"
|
||||
/>
|
||||
<ServicesSection :data="site.sections.services" />
|
||||
<WorkSection :data="site.sections.work" />
|
||||
<ProcessSection :data="site.sections.process" />
|
||||
<StatsSection :data="site.sections.stats" />
|
||||
<TeamSection :data="site.sections.team" />
|
||||
</main>
|
||||
<SiteFooter
|
||||
:brand="site.settings.brandName"
|
||||
:email="site.settings.email"
|
||||
:phone="site.settings.phone"
|
||||
:address="site.settings.address"
|
||||
:socials="site.settings.socials"
|
||||
:data="site.sections.footer"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="grid min-h-screen place-items-center text-ash">
|
||||
Unable to load site content. Is the API running?
|
||||
<div>
|
||||
<HeroSection />
|
||||
<MetricsBar />
|
||||
<FeaturesSection />
|
||||
<SolutionsSection />
|
||||
<TestimonialsSection />
|
||||
<CtaBanner />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { data: site } = await useAsyncData("public-site", () => fetchPublicSite(), {
|
||||
// always refetch so admin edits show up after refresh
|
||||
getCachedData: () => undefined,
|
||||
});
|
||||
const homePage = computed(() => site.value?.pages?.find((p: any) => p.key === "home"));
|
||||
applyPageSeo(homePage.value?.seo);
|
||||
const { settings, pageSeo, section } = useSiteContent();
|
||||
const { siteUrl } = usePageSeo(pageSeo("home"));
|
||||
const hero = section("hero");
|
||||
|
||||
const loaderTitle = useState("intro-loader-title", () => "هوشافزار منتک");
|
||||
watchEffect(() => {
|
||||
const hero = site.value?.sections?.hero as { loaderTitle?: string } | undefined;
|
||||
if (hero?.loaderTitle) loaderTitle.value = hero.loaderTitle;
|
||||
useJsonLd(() => {
|
||||
const s = settings.value;
|
||||
const sameAs = s.socials.map((x) => x.url).filter((u) => /^https?:\/\//.test(u));
|
||||
return [
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"@id": `${siteUrl}/#organization`,
|
||||
name: s.brandName,
|
||||
alternateName: "Montech",
|
||||
url: `${siteUrl}/`,
|
||||
logo: s.logoUrl ? new URL(s.logoUrl, siteUrl).toString() : `${siteUrl}/brand/logo.svg`,
|
||||
email: s.email,
|
||||
telephone: s.phone,
|
||||
address: { "@type": "PostalAddress", addressLocality: s.address, addressCountry: "IR" },
|
||||
...(sameAs.length ? { sameAs } : {}),
|
||||
},
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"@id": `${siteUrl}/#website`,
|
||||
url: `${siteUrl}/`,
|
||||
name: s.brandName,
|
||||
inLanguage: "fa-IR",
|
||||
publisher: { "@id": `${siteUrl}/#organization` },
|
||||
},
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"@id": `${siteUrl}/#webpage`,
|
||||
url: `${siteUrl}/`,
|
||||
name: `${hero.value.titleLine1} ${hero.value.titleHighlight}`,
|
||||
description: hero.value.subtitle,
|
||||
inLanguage: "fa-IR",
|
||||
isPartOf: { "@id": `${siteUrl}/#website` },
|
||||
about: { "@id": `${siteUrl}/#organization` },
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user