d407f0b3e9
CI/CD / CI · API (dotnet build + test) (push) Successful in 40s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 29s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m10s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Has been cancelled
- web/website: manifest referenced /icon-192.png and /icon-512.png that didn't exist (broken favicon). Added the real transparent Meezi mark (32/180/192/512) + wired icons into metadata. OG image stays dynamic. - web/admin: had no metadata or icons at all. Added title template, favicon/apple icons (icons/ dir), themeColor, noindex. Dashboard + Koja already carry the real logo; web apps are now consistent. Mobile launcher icons will be handled with the Android packaging task. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
121 lines
3.6 KiB
TypeScript
121 lines
3.6 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { notFound } from "next/navigation";
|
|
import { NextIntlClientProvider } from "next-intl";
|
|
import { getMessages, getTranslations } from "next-intl/server";
|
|
import { routing } from "@/i18n/routing";
|
|
import { LocaleHtml } from "@/components/locale-html";
|
|
|
|
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir";
|
|
|
|
export async function generateMetadata({
|
|
params,
|
|
}: {
|
|
params: Promise<{ locale: string }>;
|
|
}): Promise<Metadata> {
|
|
const { locale } = await params;
|
|
const t = await getTranslations({ locale, namespace: "meta" });
|
|
|
|
const ogLocale = locale === "fa" ? "fa_IR" : "en_US";
|
|
const canonicalBase = `${BASE_URL}/${locale}`;
|
|
|
|
return {
|
|
metadataBase: new URL(BASE_URL),
|
|
title: {
|
|
default: t("homeTitle"),
|
|
template: `%s | ${t("siteName")}`,
|
|
},
|
|
description: t("siteDescription"),
|
|
icons: {
|
|
icon: [
|
|
{ url: "/icon-32.png", sizes: "32x32", type: "image/png" },
|
|
{ url: "/icon-192.png", sizes: "192x192", type: "image/png" },
|
|
{ url: "/icon-512.png", sizes: "512x512", type: "image/png" },
|
|
],
|
|
shortcut: "/icon-32.png",
|
|
apple: "/icon-180.png",
|
|
},
|
|
keywords:
|
|
locale === "fa"
|
|
? ["میزی", "مدیریت کافه", "منوی دیجیتال", "سیستم POS", "نرم افزار رستوران", "QR کد کافه", "مدیریت رستوران ایران"]
|
|
: ["Meezi", "cafe management", "restaurant software", "QR menu", "POS system", "Iran cafe", "digital menu"],
|
|
authors: [{ name: "Meezi", url: BASE_URL }],
|
|
creator: "Meezi",
|
|
publisher: "Meezi",
|
|
openGraph: {
|
|
type: "website",
|
|
locale: ogLocale,
|
|
alternateLocale: locale === "fa" ? "en_US" : "fa_IR",
|
|
url: canonicalBase,
|
|
siteName: t("siteName"),
|
|
title: t("homeTitle"),
|
|
description: t("siteDescription"),
|
|
images: [
|
|
{
|
|
url: `${BASE_URL}/api/og?t=${encodeURIComponent(t("homeTitle"))}&s=${encodeURIComponent(t("siteDescription"))}&rtl=${locale === "fa" ? "1" : "0"}`,
|
|
width: 1200,
|
|
height: 630,
|
|
alt: t("siteName"),
|
|
},
|
|
],
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image",
|
|
site: "@MeeziApp",
|
|
creator: "@MeeziApp",
|
|
title: t("homeTitle"),
|
|
description: t("siteDescription"),
|
|
images: [`${BASE_URL}/api/og?t=${encodeURIComponent(t("homeTitle"))}&s=${encodeURIComponent(t("siteDescription"))}&rtl=${locale === "fa" ? "1" : "0"}`],
|
|
},
|
|
alternates: {
|
|
canonical: canonicalBase,
|
|
languages: {
|
|
"fa": `${BASE_URL}/fa`,
|
|
"en": `${BASE_URL}/en`,
|
|
"x-default": `${BASE_URL}/fa`,
|
|
},
|
|
},
|
|
robots: {
|
|
index: true,
|
|
follow: true,
|
|
googleBot: {
|
|
index: true,
|
|
follow: true,
|
|
"max-video-preview": -1,
|
|
"max-image-preview": "large",
|
|
"max-snippet": -1,
|
|
},
|
|
},
|
|
verification: {
|
|
// Add Google Search Console verification token here when ready
|
|
// google: "your-token",
|
|
},
|
|
category: "technology",
|
|
};
|
|
}
|
|
|
|
export function generateStaticParams() {
|
|
return routing.locales.map((locale) => ({ locale }));
|
|
}
|
|
|
|
export default async function LocaleLayout({
|
|
children,
|
|
params,
|
|
}: {
|
|
children: React.ReactNode;
|
|
params: Promise<{ locale: string }>;
|
|
}) {
|
|
const { locale } = await params;
|
|
if (!routing.locales.includes(locale as "fa" | "en")) notFound();
|
|
const messages = await getMessages();
|
|
const dir = locale === "fa" ? "rtl" : "ltr";
|
|
|
|
return (
|
|
<>
|
|
<LocaleHtml locale={locale} dir={dir} />
|
|
<NextIntlClientProvider messages={messages}>
|
|
{children}
|
|
</NextIntlClientProvider>
|
|
</>
|
|
);
|
|
}
|