From 166f2b258648b45a8305dcd6856ce8eebc6eda93 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Wed, 24 Jun 2026 21:32:08 +0330 Subject: [PATCH] fix(seo): self-canonical + unique description on 6 pages that deduped to home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit contact / careers / status / privacy / terms / docs set no alternates, so they inherited the layout's canonical (= the locale homepage) — Google treats them as duplicates of the home page and drops them. Each now sets a self-referencing canonical + full fa/en/x-default hreflang (new shared lib/seo.ts pageAlternates) and a unique meta description (added *Desc keys, fa/en) + per-page OpenGraph. Co-Authored-By: Claude Opus 4.8 --- web/website/messages/en.json | 8 +++++++- web/website/messages/fa.json | 8 +++++++- web/website/src/app/[locale]/careers/page.tsx | 8 +++++++- web/website/src/app/[locale]/contact/page.tsx | 8 +++++++- web/website/src/app/[locale]/docs/page.tsx | 8 +++++++- web/website/src/app/[locale]/privacy/page.tsx | 8 +++++++- web/website/src/app/[locale]/status/page.tsx | 8 +++++++- web/website/src/app/[locale]/terms/page.tsx | 8 +++++++- web/website/src/lib/seo.ts | 18 ++++++++++++++++++ 9 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 web/website/src/lib/seo.ts diff --git a/web/website/messages/en.json b/web/website/messages/en.json index 79e09c6..aef270c 100644 --- a/web/website/messages/en.json +++ b/web/website/messages/en.json @@ -25,7 +25,13 @@ "termsTitle": "Terms of Service — Meezi", "docsTitle": "Documentation & Help — Meezi", "statusTitle": "Service Status — Meezi", - "contactTitle": "Contact Us — Meezi" + "contactTitle": "Contact Us — Meezi", + "contactDesc": "Get in touch with the Meezi team — phone, email, and live chat support for cafés and restaurants.", + "careersDesc": "Join Meezi — we're building the smart café & restaurant management platform for Iran. See open roles.", + "statusDesc": "Live status of Meezi services — POS, dashboard, and APIs. Check current uptime and incidents.", + "privacyDesc": "How Meezi collects, uses, and protects your data. Read our privacy policy.", + "termsDesc": "The terms of service that govern your use of Meezi's café & restaurant management platform.", + "docsDesc": "Meezi help center — step-by-step guides for POS, menu, tables, inventory, staff, reports, and more." }, "nav": { "features": "Features", diff --git a/web/website/messages/fa.json b/web/website/messages/fa.json index 58cc5ea..2e862c6 100644 --- a/web/website/messages/fa.json +++ b/web/website/messages/fa.json @@ -25,7 +25,13 @@ "termsTitle": "شرایط استفاده — میزی", "docsTitle": "مستندات و راهنما — میزی", "statusTitle": "وضعیت سرویس — میزی", - "contactTitle": "تماس با ما — میزی" + "contactTitle": "تماس با ما — میزی", + "contactDesc": "با تیم میزی در ارتباط باشید — پشتیبانی تلفنی، ایمیل و چت آنلاین برای کافه‌ها و رستوران‌ها.", + "careersDesc": "به میزی بپیوندید — ما در حال ساخت پلتفرم هوشمند مدیریت کافه و رستوران برای ایران هستیم. موقعیت‌های شغلی را ببینید.", + "statusDesc": "وضعیت لحظه‌ای سرویس‌های میزی — صندوق، داشبورد و APIها. آپ‌تایم و رویدادهای جاری را ببینید.", + "privacyDesc": "میزی چگونه داده‌های شما را جمع‌آوری، استفاده و محافظت می‌کند. سیاست حریم خصوصی ما را بخوانید.", + "termsDesc": "شرایط استفاده از پلتفرم مدیریت کافه و رستوران میزی.", + "docsDesc": "مرکز راهنمای میزی — آموزش گام‌به‌گام صندوق، منو، میزها، انبار، کارکنان، گزارش‌ها و بیشتر." }, "nav": { "features": "امکانات", diff --git a/web/website/src/app/[locale]/careers/page.tsx b/web/website/src/app/[locale]/careers/page.tsx index 9def4c7..8af7542 100644 --- a/web/website/src/app/[locale]/careers/page.tsx +++ b/web/website/src/app/[locale]/careers/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { getTranslations } from "next-intl/server"; +import { SITE_URL, pageAlternates } from "@/lib/seo"; import { Navbar } from "@/components/layout/navbar"; import { Footer } from "@/components/layout/footer"; import { @@ -25,7 +26,12 @@ export async function generateMetadata({ }): Promise { const { locale } = await params; const t = await getTranslations({ locale, namespace: "meta" }); - return { title: t("careersTitle") }; + return { + title: t("careersTitle"), + description: t("careersDesc"), + alternates: pageAlternates(locale, "/careers"), + openGraph: { title: t("careersTitle"), description: t("careersDesc"), url: `${SITE_URL}/${locale}/careers` }, + }; } const fa = { diff --git a/web/website/src/app/[locale]/contact/page.tsx b/web/website/src/app/[locale]/contact/page.tsx index f445d97..d078669 100644 --- a/web/website/src/app/[locale]/contact/page.tsx +++ b/web/website/src/app/[locale]/contact/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { getTranslations } from "next-intl/server"; +import { SITE_URL, pageAlternates } from "@/lib/seo"; import { Navbar } from "@/components/layout/navbar"; import { Footer } from "@/components/layout/footer"; import { Phone, Mail, MapPin, Clock, MessageSquare, ArrowLeft, ArrowRight } from "lucide-react"; @@ -11,7 +12,12 @@ export async function generateMetadata({ }): Promise { const { locale } = await params; const t = await getTranslations({ locale, namespace: "meta" }); - return { title: t("contactTitle") }; + return { + title: t("contactTitle"), + description: t("contactDesc"), + alternates: pageAlternates(locale, "/contact"), + openGraph: { title: t("contactTitle"), description: t("contactDesc"), url: `${SITE_URL}/${locale}/contact` }, + }; } const fa = { diff --git a/web/website/src/app/[locale]/docs/page.tsx b/web/website/src/app/[locale]/docs/page.tsx index 221e956..86b83fb 100644 --- a/web/website/src/app/[locale]/docs/page.tsx +++ b/web/website/src/app/[locale]/docs/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { getTranslations } from "next-intl/server"; +import { SITE_URL, pageAlternates } from "@/lib/seo"; import { Navbar } from "@/components/layout/navbar"; import { Footer } from "@/components/layout/footer"; import { ArrowLeft, ArrowRight, Search, LifeBuoy } from "lucide-react"; @@ -18,7 +19,12 @@ export async function generateMetadata({ }): Promise { const { locale } = await params; const t = await getTranslations({ locale, namespace: "meta" }); - return { title: t("docsTitle") }; + return { + title: t("docsTitle"), + description: t("docsDesc"), + alternates: pageAlternates(locale, "/docs"), + openGraph: { title: t("docsTitle"), description: t("docsDesc"), url: `${SITE_URL}/${locale}/docs` }, + }; } const fa = { diff --git a/web/website/src/app/[locale]/privacy/page.tsx b/web/website/src/app/[locale]/privacy/page.tsx index 0fe9bd5..4495e00 100644 --- a/web/website/src/app/[locale]/privacy/page.tsx +++ b/web/website/src/app/[locale]/privacy/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { getTranslations } from "next-intl/server"; +import { SITE_URL, pageAlternates } from "@/lib/seo"; import { Navbar } from "@/components/layout/navbar"; import { Footer } from "@/components/layout/footer"; import { Shield } from "lucide-react"; @@ -11,7 +12,12 @@ export async function generateMetadata({ }): Promise { const { locale } = await params; const t = await getTranslations({ locale, namespace: "meta" }); - return { title: t("privacyTitle") }; + return { + title: t("privacyTitle"), + description: t("privacyDesc"), + alternates: pageAlternates(locale, "/privacy"), + openGraph: { title: t("privacyTitle"), description: t("privacyDesc"), url: `${SITE_URL}/${locale}/privacy` }, + }; } const fa = { diff --git a/web/website/src/app/[locale]/status/page.tsx b/web/website/src/app/[locale]/status/page.tsx index 2a71304..25ed63f 100644 --- a/web/website/src/app/[locale]/status/page.tsx +++ b/web/website/src/app/[locale]/status/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { getTranslations } from "next-intl/server"; +import { SITE_URL, pageAlternates } from "@/lib/seo"; import { Navbar } from "@/components/layout/navbar"; import { Footer } from "@/components/layout/footer"; import { CheckCircle2, Server, Globe, Database, Zap, RefreshCw } from "lucide-react"; @@ -12,7 +13,12 @@ export async function generateMetadata({ }) : Promise { const { locale } = await params; const t = await getTranslations({ locale, namespace: "meta" }); - return { title: t("statusTitle") }; + return { + title: t("statusTitle"), + description: t("statusDesc"), + alternates: pageAlternates(locale, "/status"), + openGraph: { title: t("statusTitle"), description: t("statusDesc"), url: `${SITE_URL}/${locale}/status` }, + }; } const fa = { diff --git a/web/website/src/app/[locale]/terms/page.tsx b/web/website/src/app/[locale]/terms/page.tsx index f351255..9a7c730 100644 --- a/web/website/src/app/[locale]/terms/page.tsx +++ b/web/website/src/app/[locale]/terms/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { getTranslations } from "next-intl/server"; +import { SITE_URL, pageAlternates } from "@/lib/seo"; import { Navbar } from "@/components/layout/navbar"; import { Footer } from "@/components/layout/footer"; import { FileText } from "lucide-react"; @@ -11,7 +12,12 @@ export async function generateMetadata({ }): Promise { const { locale } = await params; const t = await getTranslations({ locale, namespace: "meta" }); - return { title: t("termsTitle") }; + return { + title: t("termsTitle"), + description: t("termsDesc"), + alternates: pageAlternates(locale, "/terms"), + openGraph: { title: t("termsTitle"), description: t("termsDesc"), url: `${SITE_URL}/${locale}/terms` }, + }; } const fa = { diff --git a/web/website/src/lib/seo.ts b/web/website/src/lib/seo.ts new file mode 100644 index 0000000..b6eb20c --- /dev/null +++ b/web/website/src/lib/seo.ts @@ -0,0 +1,18 @@ +export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://meezi.ir"; + +/** + * Self-referencing canonical + complete fa/en/x-default hreflang cluster for a + * page. Pass the path WITHOUT the locale prefix, e.g. "/contact" (or "" for the + * locale home). Use on every page's `alternates` so inner pages don't inherit + * the homepage canonical (which silently de-dupes them in Search Console). + */ +export function pageAlternates(locale: string, path = "") { + return { + canonical: `${SITE_URL}/${locale}${path}`, + languages: { + fa: `${SITE_URL}/fa${path}`, + en: `${SITE_URL}/en${path}`, + "x-default": `${SITE_URL}/fa${path}`, + }, + }; +}