Restructure the repo into a pnpm + Turborepo monorepo: - apps/web: Nuxt 4 site, now server-rendered and reading all content from the API; pages cached with SWR and purged on content changes - apps/api: NestJS 12 + Prisma 7 + PostgreSQL. Auth with short-lived access tokens and rotating httpOnly refresh cookies, roles (ADMIN, EDITOR), catalog, editable site sections, media upload to S3-compatible storage (converted to WebP), contact messages, audit log - apps/admin: Vue 3 admin panel (Ant Design Vue, Pinia, Tailwind) for products, categories, site sections, messages, media and users - packages/shared: Zod schemas and types shared by API and admin Also: - seed script that loads the original 8 categories / 89 products and section content - Docker images for api (with a one-off migrate service), web, admin, and an nginx gateway for both domains - dev and production compose files, daily database backup service - API e2e tests Removes the committed static out/ build and the old root Dockerfile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47 lines
1.5 KiB
TypeScript
47 lines
1.5 KiB
TypeScript
import { siteConfig } from "#shared/config/site";
|
|
|
|
/** Used when the API is unreachable (the error page must still render) */
|
|
const FALLBACK = {
|
|
name: "ایمن پیشگامان",
|
|
title: "ایمن پیشگامان | تجهیزات ایمنی و حفاظت فردی صنعتی",
|
|
description: "ایمن پیشگامان — تأمینکننده تخصصی تجهیزات حفاظت فردی (PPE) و راهکارهای ایمنی صنعتی.",
|
|
};
|
|
|
|
/** Site-wide <head> defaults shared by the app shell and the error page. */
|
|
export function useSiteSeo() {
|
|
const site = useSiteStore();
|
|
const absUrl = useAbsUrl();
|
|
const s = site.data?.sections.settings;
|
|
|
|
const name = s?.name ?? FALLBACK.name;
|
|
const title = s?.title ?? FALLBACK.title;
|
|
const description = s?.description ?? FALLBACK.description;
|
|
const ogImage = s?.ogImage;
|
|
|
|
useHead({
|
|
titleTemplate: (t) => (t ? `${t} | ${name}` : title),
|
|
});
|
|
|
|
useSeoMeta({
|
|
description,
|
|
themeColor: siteConfig.themeColor,
|
|
formatDetection: "telephone=no",
|
|
ogType: "website",
|
|
ogSiteName: name,
|
|
ogLocale: siteConfig.locale,
|
|
ogTitle: title,
|
|
ogDescription: description,
|
|
twitterCard: "summary_large_image",
|
|
twitterTitle: title,
|
|
twitterDescription: description,
|
|
...(ogImage && {
|
|
ogImage: absUrl(ogImage.url),
|
|
ogImageWidth: ogImage.width ?? undefined,
|
|
ogImageHeight: ogImage.height ?? undefined,
|
|
ogImageAlt: ogImage.alt,
|
|
twitterImage: absUrl(ogImage.url),
|
|
twitterImageAlt: ogImage.alt,
|
|
}),
|
|
});
|
|
}
|