fix(seo): self-canonical + unique description on 6 pages that deduped to home
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m4s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m9s
CI/CD / CI · Admin Web (tsc) (push) Successful in 38s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 52s
CI/CD / Deploy · all services (push) Successful in 2m1s

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 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-24 21:32:08 +03:30
parent 8ea98bdc09
commit 166f2b2586
9 changed files with 74 additions and 8 deletions
+7 -1
View File
@@ -25,7 +25,13 @@
"termsTitle": "Terms of Service — Meezi", "termsTitle": "Terms of Service — Meezi",
"docsTitle": "Documentation & Help — Meezi", "docsTitle": "Documentation & Help — Meezi",
"statusTitle": "Service Status — 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": { "nav": {
"features": "Features", "features": "Features",
+7 -1
View File
@@ -25,7 +25,13 @@
"termsTitle": "شرایط استفاده — میزی", "termsTitle": "شرایط استفاده — میزی",
"docsTitle": "مستندات و راهنما — میزی", "docsTitle": "مستندات و راهنما — میزی",
"statusTitle": "وضعیت سرویس — میزی", "statusTitle": "وضعیت سرویس — میزی",
"contactTitle": "تماس با ما — میزی" "contactTitle": "تماس با ما — میزی",
"contactDesc": "با تیم میزی در ارتباط باشید — پشتیبانی تلفنی، ایمیل و چت آنلاین برای کافه‌ها و رستوران‌ها.",
"careersDesc": "به میزی بپیوندید — ما در حال ساخت پلتفرم هوشمند مدیریت کافه و رستوران برای ایران هستیم. موقعیت‌های شغلی را ببینید.",
"statusDesc": "وضعیت لحظه‌ای سرویس‌های میزی — صندوق، داشبورد و APIها. آپ‌تایم و رویدادهای جاری را ببینید.",
"privacyDesc": "میزی چگونه داده‌های شما را جمع‌آوری، استفاده و محافظت می‌کند. سیاست حریم خصوصی ما را بخوانید.",
"termsDesc": "شرایط استفاده از پلتفرم مدیریت کافه و رستوران میزی.",
"docsDesc": "مرکز راهنمای میزی — آموزش گام‌به‌گام صندوق، منو، میزها، انبار، کارکنان، گزارش‌ها و بیشتر."
}, },
"nav": { "nav": {
"features": "امکانات", "features": "امکانات",
@@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { SITE_URL, pageAlternates } from "@/lib/seo";
import { Navbar } from "@/components/layout/navbar"; import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer"; import { Footer } from "@/components/layout/footer";
import { import {
@@ -25,7 +26,12 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: "meta" }); 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 = { const fa = {
@@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { SITE_URL, pageAlternates } from "@/lib/seo";
import { Navbar } from "@/components/layout/navbar"; import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer"; import { Footer } from "@/components/layout/footer";
import { Phone, Mail, MapPin, Clock, MessageSquare, ArrowLeft, ArrowRight } from "lucide-react"; import { Phone, Mail, MapPin, Clock, MessageSquare, ArrowLeft, ArrowRight } from "lucide-react";
@@ -11,7 +12,12 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: "meta" }); 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 = { const fa = {
+7 -1
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { SITE_URL, pageAlternates } from "@/lib/seo";
import { Navbar } from "@/components/layout/navbar"; import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer"; import { Footer } from "@/components/layout/footer";
import { ArrowLeft, ArrowRight, Search, LifeBuoy } from "lucide-react"; import { ArrowLeft, ArrowRight, Search, LifeBuoy } from "lucide-react";
@@ -18,7 +19,12 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: "meta" }); 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 = { const fa = {
@@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { SITE_URL, pageAlternates } from "@/lib/seo";
import { Navbar } from "@/components/layout/navbar"; import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer"; import { Footer } from "@/components/layout/footer";
import { Shield } from "lucide-react"; import { Shield } from "lucide-react";
@@ -11,7 +12,12 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: "meta" }); 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 = { const fa = {
+7 -1
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { SITE_URL, pageAlternates } from "@/lib/seo";
import { Navbar } from "@/components/layout/navbar"; import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer"; import { Footer } from "@/components/layout/footer";
import { CheckCircle2, Server, Globe, Database, Zap, RefreshCw } from "lucide-react"; import { CheckCircle2, Server, Globe, Database, Zap, RefreshCw } from "lucide-react";
@@ -12,7 +13,12 @@ export async function generateMetadata({
}) : Promise<Metadata> { }) : Promise<Metadata> {
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: "meta" }); 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 = { const fa = {
+7 -1
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { SITE_URL, pageAlternates } from "@/lib/seo";
import { Navbar } from "@/components/layout/navbar"; import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer"; import { Footer } from "@/components/layout/footer";
import { FileText } from "lucide-react"; import { FileText } from "lucide-react";
@@ -11,7 +12,12 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: "meta" }); 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 = { const fa = {
+18
View File
@@ -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}`,
},
};
}