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
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:
@@ -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",
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user