seo
This commit is contained in:
18
src/app/(auth)/layout.tsx
Normal file
18
src/app/(auth)/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "ورود و ثبتنام | مدستاگرام",
|
||||
template: "%s | مدستاگرام",
|
||||
},
|
||||
description: "ورود یا ثبتنام در مدستاگرام",
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { cookies } from "next/headers";
|
||||
import InfinitePosts from "@/components/academy/InfinitePakage";
|
||||
import { Metadata } from "next";
|
||||
import AcademyFilter from "@/components/academy/AcademyFilter";
|
||||
import { pageSeo } from "@/config/pageSeo";
|
||||
import { generatePageMetadata } from "@/utils/generatePageMetadata";
|
||||
|
||||
interface IProjectsProps {
|
||||
params: Promise<{ slug?: string[] }>;
|
||||
@@ -22,40 +24,41 @@ export async function generateMetadata({
|
||||
searchParams,
|
||||
}: IProjectsProps): Promise<Metadata> {
|
||||
const filters = await searchParams;
|
||||
|
||||
// خواندن مقادیر فیلترها (اگر خالی بود مقدار پیشفرض در نظر گرفته میشه)
|
||||
|
||||
const isDefaultAcademy =
|
||||
!filters.category &&
|
||||
!filters.type &&
|
||||
!filters.search &&
|
||||
!filters.minPrice &&
|
||||
!filters.maxPrice &&
|
||||
!filters.hasOffer &&
|
||||
!filters.sortBy;
|
||||
|
||||
if (isDefaultAcademy) {
|
||||
return generatePageMetadata({
|
||||
title: pageSeo.academy.title,
|
||||
description: pageSeo.academy.description,
|
||||
path: pageSeo.academy.path,
|
||||
keywords: [...pageSeo.academy.keywords],
|
||||
});
|
||||
}
|
||||
|
||||
const categoryLabel = filters.category || "حوزه زیبایی";
|
||||
const typeLabel = filters.type ? ` به صورت ${filters.type}` : "";
|
||||
const descTypeLabel = filters.type ? ` بصورت ${filters.type}` : "";
|
||||
|
||||
// مقادیر پیشفرض زمانی که هیچ فیلتری انتخاب نشده
|
||||
let title = "آموزش آنلاین انواع خدمات حوزه زیبایی و مدلینگ - مدستاگرام";
|
||||
let description = "آموزش آنلاین تخصصی و حرفهای از انواع اساتید حوزه زیبایی در همه زمینهها اعم از آرایشگری، مدلینگ و عکاسی - مدستاگرام";
|
||||
const title = `آموزش ${categoryLabel}${typeLabel} | آموزشگاه مدستاگرام`;
|
||||
const description = `آموزش تخصصی و حرفهای ${categoryLabel}${descTypeLabel} در آموزشگاه مدستاگرام. پکیجهای آموزشی زیبایی، میکاپ، مدلینگ و عکاسی.`;
|
||||
|
||||
// اگر کاربر فیلتر "دسته بندی" یا "نوع" رو انتخاب کرده بود، متنها داینامیک میشن
|
||||
if (filters.category || filters.type) {
|
||||
title = `آموزش آنلاین انواع خدمات ${categoryLabel}${typeLabel} - مدستاگرام`;
|
||||
description = `آموزش آنلاین تخصصی و حرفه ای خدمات ${categoryLabel}${descTypeLabel} در مدستاگرام`;
|
||||
}
|
||||
|
||||
// ساخت آدرس URL داینامیک بر اساس فیلترها
|
||||
let canonicalUrl = "https://modstagram.com/academy";
|
||||
const queryParams = new URLSearchParams();
|
||||
if (filters.category) queryParams.append("category", filters.category);
|
||||
if (filters.type) queryParams.append("type", filters.type);
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
if (queryString) {
|
||||
canonicalUrl += `?${queryString}`;
|
||||
}
|
||||
const path = queryString
|
||||
? `${pageSeo.academy.path}?${queryString}`
|
||||
: pageSeo.academy.path;
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: canonicalUrl,
|
||||
},
|
||||
};
|
||||
return generatePageMetadata({ title, description, path });
|
||||
}
|
||||
|
||||
export default async function Projects({
|
||||
@@ -89,6 +92,9 @@ export default async function Projects({
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<h1 className="sr-only">
|
||||
{pageSeo.academy.title.replace(" | مدستاگرام", "")}
|
||||
</h1>
|
||||
<AcademyFilter />
|
||||
<InfinitePosts filters={filters} token={token} />
|
||||
</Container>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Metadata } from "next";
|
||||
import CourseDetail from "./CourseClient";
|
||||
import { generatePageMetadata } from "@/utils/generatePageMetadata";
|
||||
|
||||
// نوع Props را بهروز کردیم که params یک Promise باشد
|
||||
type Props = {
|
||||
params: Promise<{ id: string; title: string }>;
|
||||
};
|
||||
|
||||
// تابع metadata با await روی params
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
try {
|
||||
const { id } = await params; // اول await میکنیم، بعد id میگیریم
|
||||
const { id, title: slugTitle } = await params;
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://app.modstagram.ir/api/v1/academy/academy/get/curses?page=1&limit=20&status=accept&_id=${id}`,
|
||||
{ cache: "no-store" }
|
||||
@@ -32,23 +31,25 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
.replace(/<[^>]*>?/gm, "")
|
||||
.substring(0, 150);
|
||||
|
||||
return {
|
||||
title: `${courseName} - ${category} - مدستاگرام`,
|
||||
description: `${category} - ${cleanCaption} - مدرس: ${teacher} - مدستاگرام`,
|
||||
};
|
||||
return generatePageMetadata({
|
||||
title: `${courseName} | ${category} | آموزشگاه مدستاگرام`,
|
||||
description: `${cleanCaption || courseName}. دسته: ${category}. مدرس: ${teacher}. پکیج آموزشی تخصصی در آموزشگاه مدستاگرام.`,
|
||||
path: `/academy/${id}/${encodeURIComponent(slugTitle || courseName)}`,
|
||||
type: "article",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("خطا در دریافت اطلاعات سئو:", error);
|
||||
}
|
||||
|
||||
return {
|
||||
title: "جزئیات دوره - مدستاگرام",
|
||||
description: "توضیحات دوره آکادمی - مدستاگرام",
|
||||
};
|
||||
return generatePageMetadata({
|
||||
title: "جزئیات دوره | آموزشگاه مدستاگرام",
|
||||
description: "مشاهده جزئیات پکیج آموزشی در آموزشگاه مدستاگرام.",
|
||||
path: `/academy/${id}/${encodeURIComponent(slugTitle || "course")}`,
|
||||
});
|
||||
}
|
||||
|
||||
// صفحه اصلی را async کردیم تا بتوانیم params را await کنیم
|
||||
export default async function CoursePage({ params }: Props) {
|
||||
const { id } = await params; // استخراج id از params
|
||||
const { id } = await params;
|
||||
return <CourseDetail courseId={id} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import Header from "@/components/main/Header";
|
||||
import TabNavigation from "@/components/TabNavigation";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "ثبت پروژه جدید | مدستاگرام",
|
||||
description: "ثبت درخواست همکاری و پروژه جدید در مدستاگرام",
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Metadata } from "next";
|
||||
import { Suspense } from "react";
|
||||
import ProjectDetailClient from "./ProjectDetailClient";
|
||||
import PageLoader from "@/components/ui/PageLoader";
|
||||
import { generatePageMetadata } from "@/utils/generatePageMetadata";
|
||||
|
||||
type ProjectDetailPageProps = {
|
||||
params: Promise<{ id: string; title: string }>;
|
||||
@@ -10,13 +11,14 @@ type ProjectDetailPageProps = {
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: ProjectDetailPageProps): Promise<Metadata> {
|
||||
const { title } = await params;
|
||||
const { id, title } = await params;
|
||||
const decodedTitle = decodeURIComponent(title || "پروژه");
|
||||
|
||||
return {
|
||||
title: `${decodedTitle} | پروژهها | مدستاگرام`,
|
||||
description: `جزئیات پروژه ${decodedTitle} در مدستاگرام`,
|
||||
};
|
||||
return generatePageMetadata({
|
||||
title: `${decodedTitle} | پروژههای مدستاگرام`,
|
||||
description: `جزئیات پروژه «${decodedTitle}» در مدستاگرام. مشاهده شرایط همکاری، بودجه و ثبت پیشنهاد.`,
|
||||
path: `/projects/${id}/${encodeURIComponent(decodedTitle)}`,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function ProjectDetailPage({
|
||||
|
||||
@@ -4,12 +4,15 @@ import ProjectsFilter from "@/components/projects/ProjectsFilter";
|
||||
import { fetchProjects } from "@/api/fetchProjects";
|
||||
import { cookies } from "next/headers";
|
||||
import { Metadata } from "next";
|
||||
import { pageSeo } from "@/config/pageSeo";
|
||||
import { generatePageMetadata } from "@/utils/generatePageMetadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "پروژهها | مدستاگرام",
|
||||
description:
|
||||
"لیست پروژههای مد، زیبایی و عکاسی در مدستاگرام. پروژه مناسب خود را پیدا کنید.",
|
||||
};
|
||||
export const metadata: Metadata = generatePageMetadata({
|
||||
title: pageSeo.projects.title,
|
||||
description: pageSeo.projects.description,
|
||||
path: pageSeo.projects.path,
|
||||
keywords: [...pageSeo.projects.keywords],
|
||||
});
|
||||
|
||||
type ProjectsPageProps = {
|
||||
searchParams: Promise<{
|
||||
@@ -38,7 +41,9 @@ export default async function ProjectsPage({ searchParams }: ProjectsPageProps)
|
||||
|
||||
return (
|
||||
<Container className="pb-28">
|
||||
<h1 className="sr-only">پروژههای مد و زیبایی در مدستاگرام</h1>
|
||||
<h1 className="sr-only">
|
||||
{pageSeo.projects.title.replace(" | مدستاگرام", "")}
|
||||
</h1>
|
||||
<ProjectsFilter expertise={filters.expertise || ""} />
|
||||
<InfiniteProjects
|
||||
initialData={initialData}
|
||||
|
||||
@@ -6,6 +6,8 @@ import StoriesBar from "@/components/stories/StoriesBar";
|
||||
import Header from "@/components/main/Header";
|
||||
import TabNavigation from "@/components/TabNavigation";
|
||||
import { Metadata } from "next";
|
||||
import { pageSeo } from "@/config/pageSeo";
|
||||
import { generatePageMetadata } from "@/utils/generatePageMetadata";
|
||||
|
||||
// درونریزی فایلهای دیتا برای استخراج نام شهر و استان
|
||||
import provinces from "@/data/provinces.json";
|
||||
@@ -27,6 +29,25 @@ interface IModelsProps {
|
||||
// تابع هوشمند تولید متادیتا
|
||||
export async function generateMetadata({ params, searchParams }: IModelsProps): Promise<Metadata> {
|
||||
const [urlParams, filters] = await Promise.all([params, searchParams]);
|
||||
|
||||
const isDefaultHome =
|
||||
(!urlParams.slug || urlParams.slug.length === 0) &&
|
||||
!filters.expertise &&
|
||||
!filters.province &&
|
||||
!filters.city &&
|
||||
!filters.userLevel &&
|
||||
!filters.rateFilter &&
|
||||
!filters.hashtag;
|
||||
|
||||
if (isDefaultHome) {
|
||||
return generatePageMetadata({
|
||||
title: pageSeo.home.title,
|
||||
description: pageSeo.home.description,
|
||||
path: pageSeo.home.path,
|
||||
keywords: [...pageSeo.home.keywords],
|
||||
});
|
||||
}
|
||||
|
||||
let locationLabel = "";
|
||||
let levelLabel = "";
|
||||
|
||||
@@ -36,11 +57,13 @@ export async function generateMetadata({ params, searchParams }: IModelsProps):
|
||||
|
||||
// اولویت با متن فارسی ساخته شده در URL
|
||||
if (decodedText.startsWith("استخدام")) {
|
||||
return {
|
||||
title: `${decodedText} | مدستاگرام`,
|
||||
description: `لیست برترین متخصصین مد و زیبایی: ${decodedText}`,
|
||||
alternates: { canonical: "https://modstagram.com" }
|
||||
};
|
||||
const title = `${decodedText} | مدستاگرام`;
|
||||
const description = `لیست برترین متخصصین مد و زیبایی: ${decodedText}. پیدا کردن مدل، عکاس و آرایشگر حرفهای در مدستاگرام.`;
|
||||
return generatePageMetadata({
|
||||
title,
|
||||
description,
|
||||
path: `/${urlParams.slug.join("/")}`,
|
||||
});
|
||||
}
|
||||
|
||||
const pSlug = decodeURIComponent(urlParams.slug[0]);
|
||||
@@ -71,12 +94,14 @@ export async function generateMetadata({ params, searchParams }: IModelsProps):
|
||||
|
||||
const expertiseLabel = filters.expertise || "مدل، عکاس و آرایشگر";
|
||||
const title = `استخدام ${expertiseLabel}${levelLabel}${locationLabel} | مدستاگرام`;
|
||||
|
||||
return {
|
||||
title,
|
||||
description: `پلتفرم تخصصی استخدام ${expertiseLabel}. بهترین متخصصین را در مدستاگرام پیدا کنید.`,
|
||||
alternates: { canonical: "https://modstagram.com" },
|
||||
};
|
||||
const description = `پلتفرم تخصصی استخدام ${expertiseLabel}${locationLabel}. بهترین متخصصین حوزه زیبایی و مد را در مدستاگرام پیدا کنید.`;
|
||||
|
||||
const path =
|
||||
urlParams.slug && urlParams.slug.length > 0
|
||||
? `/${urlParams.slug.join("/")}`
|
||||
: pageSeo.home.path;
|
||||
|
||||
return generatePageMetadata({ title, description, path });
|
||||
}
|
||||
|
||||
export default async function Models({ params, searchParams }: IModelsProps) {
|
||||
@@ -140,7 +165,15 @@ export default async function Models({ params, searchParams }: IModelsProps) {
|
||||
<Header />
|
||||
<Container>
|
||||
{/* H1 مخفی برای بهبود سئو بر اساس آدرس صفحه */}
|
||||
<h1 className="sr-only">استخدام {displayExpertise}{levelText} در مدستاگرام</h1>
|
||||
<h1 className="sr-only">
|
||||
{(!urlParams.slug || urlParams.slug.length === 0) &&
|
||||
!filters.expertise &&
|
||||
!filters.province &&
|
||||
!filters.city &&
|
||||
!filters.userLevel
|
||||
? pageSeo.home.title.replace(" | مدستاگرام", "")
|
||||
: `استخدام ${displayExpertise}${levelText} در مدستاگرام`}
|
||||
</h1>
|
||||
|
||||
{/* کامپوننت فیلتر با مقدار تخصص فعلی */}
|
||||
<ModelsFilter expertise={expertise || filters.expertise || ""} />
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import Footer from "@/components/main/Footer";
|
||||
import Header from "@/components/main/Header";
|
||||
import type { Metadata } from "next";
|
||||
import { pageSeo } from "@/config/pageSeo";
|
||||
import { generatePageMetadata } from "@/utils/generatePageMetadata";
|
||||
|
||||
export const metadata: Metadata = generatePageMetadata({
|
||||
title: pageSeo.about.title,
|
||||
description: pageSeo.about.description,
|
||||
path: pageSeo.about.path,
|
||||
keywords: [...pageSeo.about.keywords],
|
||||
});
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,8 @@ import InfiniteBillboard from "@/components/billboards/InfiniteBillboard";
|
||||
import BillboardsFilter from "@/components/billboards/BillboardPage/BillboardsFilter";
|
||||
import { fetchBillboards } from "@/api/fetchBillboards";
|
||||
import { Metadata } from "next";
|
||||
import { pageSeo } from "@/config/pageSeo";
|
||||
import { generatePageMetadata } from "@/utils/generatePageMetadata";
|
||||
|
||||
// درونریزی دیتاها برای تبدیل متن به ID
|
||||
import provinces from "@/data/provinces.json";
|
||||
@@ -72,20 +74,38 @@ const getSEOData = (slugSegments: string[] = [], filters: any) => {
|
||||
return { provinceId, cityId, category, categoryLabel, locationLabel };
|
||||
};
|
||||
|
||||
// --- اصلاح تایتل و دیسکریپشن ---
|
||||
// --- متادیتای سئو ---
|
||||
export async function generateMetadata({ params, searchParams }: IBillboardsProps): Promise<Metadata> {
|
||||
const [urlParams, filters] = await Promise.all([params, searchParams]);
|
||||
|
||||
const isDefaultBillboards =
|
||||
(!urlParams.slug || urlParams.slug.length === 0) &&
|
||||
!filters.province &&
|
||||
!filters.city &&
|
||||
!filters.category &&
|
||||
!filters.search &&
|
||||
!filters.sort;
|
||||
|
||||
if (isDefaultBillboards) {
|
||||
return generatePageMetadata({
|
||||
title: pageSeo.billboards.title,
|
||||
description: pageSeo.billboards.description,
|
||||
path: pageSeo.billboards.path,
|
||||
keywords: [...pageSeo.billboards.keywords],
|
||||
});
|
||||
}
|
||||
|
||||
const { locationLabel, categoryLabel } = getSEOData(urlParams.slug, filters);
|
||||
|
||||
const title = `تبلیغات و بیلبورد ${categoryLabel}${locationLabel} | مدستاگرام`;
|
||||
const description = `بهترین ${categoryLabel}${locationLabel}. لیست مراکز معتبر و بیلبوردهای تبلیغاتی در پلتفرم تخصصی مدستاگرام.`;
|
||||
const title = `تبلیغات ${categoryLabel}${locationLabel} | بیلبورد مدستاگرام`;
|
||||
const description = `بیلبورد و تبلیغات ${categoryLabel}${locationLabel} در مدستاگرام. فضای ویژه تبلیغات کسبوکارهای حوزه زیبایی برای دیدهشدن بیشتر و جذب مشتری.`;
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: { canonical: "https://modstagram.com/billboards" },
|
||||
openGraph: { title, description, url: "https://modstagram.com/billboards", siteName: "مدستاگرام", locale: "fa_IR", type: "website" }
|
||||
};
|
||||
const path =
|
||||
urlParams.slug && urlParams.slug.length > 0
|
||||
? `/billboards/${urlParams.slug.join("/")}`
|
||||
: pageSeo.billboards.path;
|
||||
|
||||
return generatePageMetadata({ title, description, path });
|
||||
}
|
||||
|
||||
// --- اصلاح فیلتر و محتوا ---
|
||||
@@ -115,7 +135,13 @@ export default async function Billboards({ params, searchParams }: IBillboardsPr
|
||||
return (
|
||||
<Container>
|
||||
<h1 className="sr-only">
|
||||
تبلیغات {categoryLabel}{locationLabel} در مدستاگرام
|
||||
{(!urlParams.slug || urlParams.slug.length === 0) &&
|
||||
!filters.province &&
|
||||
!filters.city &&
|
||||
!filters.category &&
|
||||
!filters.search
|
||||
? pageSeo.billboards.title.replace(" | مدستاگرام", "")
|
||||
: `تبلیغات ${categoryLabel}${locationLabel} در مدستاگرام`}
|
||||
</h1>
|
||||
|
||||
<BillboardsFilter />
|
||||
|
||||
@@ -73,11 +73,11 @@ export async function generateMetadata({
|
||||
const address = billboard.address || `${province} ${city} ${neighborhood}`;
|
||||
const dynamicDescription = `${address} - ${billboard.description || ""}`.substring(0, 160);
|
||||
|
||||
// کلمه "مدستاگرام" توسط تابع generatePageMetadata به انتهای تایتل اضافه میشود
|
||||
return generatePageMetadata({
|
||||
title: dynamicTitle,
|
||||
description: dynamicDescription,
|
||||
path: `/billboards/${id}/${encodeURIComponent(urlSlug)}`,
|
||||
appendSiteName: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
18
src/app/explore/layout.tsx
Normal file
18
src/app/explore/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Metadata } from "next";
|
||||
import { pageSeo } from "@/config/pageSeo";
|
||||
import { generatePageMetadata } from "@/utils/generatePageMetadata";
|
||||
|
||||
export const metadata: Metadata = generatePageMetadata({
|
||||
title: pageSeo.explore.title,
|
||||
description: pageSeo.explore.description,
|
||||
path: pageSeo.explore.path,
|
||||
keywords: [...pageSeo.explore.keywords],
|
||||
});
|
||||
|
||||
export default function ExploreLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import ExploreFilterBar from "@/components/explore/ExploreFilterBar";
|
||||
import ExploreGrid from "@/components/explore/ExploreGrid";
|
||||
import Header from "@/components/main/Header";
|
||||
import { ExploreFilterId } from "@/constants/exploreFilters";
|
||||
import { pageSeo } from "@/config/pageSeo";
|
||||
import { useCallback, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
@@ -54,6 +55,9 @@ export default function ExplorePage() {
|
||||
<>
|
||||
<Header />
|
||||
<Container className="pb-28">
|
||||
<h1 className="sr-only">
|
||||
{pageSeo.explore.title.replace(" | مدستاگرام", "")}
|
||||
</h1>
|
||||
<ExploreFilterBar
|
||||
activeFilter={activeFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Metadata } from "next";
|
||||
import localFont from "next/font/local";
|
||||
import { defaultSEOConfig } from "@/config/seoConfig";
|
||||
import { pageSeo, siteKeywords } from "@/config/pageSeo";
|
||||
import SiteJsonLd from "@/components/seo/SiteJsonLd";
|
||||
|
||||
import "./globals.css";
|
||||
import Layout from "@/components/Layout";
|
||||
@@ -10,24 +12,15 @@ const iranSansFont = localFont({
|
||||
src: "./../../public/fonts/IRANSansX-Regular.woff",
|
||||
});
|
||||
|
||||
const defaultTitle =
|
||||
typeof defaultSEOConfig.title === "string"
|
||||
? defaultSEOConfig.title
|
||||
: "مدستاگرام";
|
||||
const defaultTitle = pageSeo.home.title;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: defaultTitle,
|
||||
},
|
||||
description: defaultSEOConfig.description,
|
||||
description: pageSeo.home.description,
|
||||
metadataBase: new URL("https://modstagram.com"),
|
||||
keywords: [
|
||||
"مادستاگرام",
|
||||
"مدلینگ",
|
||||
"بیلبورد تبلیغاتی",
|
||||
"پست مدل",
|
||||
"آموزشگاه مدلینگ",
|
||||
],
|
||||
keywords: siteKeywords,
|
||||
authors: [{ name: "Modstagram", url: "https://modstagram.com" }],
|
||||
creator: "Modstagram",
|
||||
alternates: {
|
||||
@@ -80,6 +73,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="fa" dir="rtl" suppressHydrationWarning>
|
||||
<body className={`${iranSansFont.className} antialiased`}>
|
||||
<SiteJsonLd />
|
||||
<RegisterSW />
|
||||
<Layout>{children}</Layout>
|
||||
</body>
|
||||
|
||||
@@ -2,6 +2,13 @@ import Header from "@/components/main/Header";
|
||||
import TabNavigation from "@/components/TabNavigation";
|
||||
import { Suspense } from "react";
|
||||
import PageLoader from "@/components/ui/PageLoader";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "انتشار پست | مدستاگرام",
|
||||
description: "انتشار پست، ویدئو یا استوری در مدستاگرام",
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Metadata } from "next";
|
||||
import { fetchPostById } from "@/api/fetchPostById";
|
||||
import PostFeedView from "@/components/posts/PostFeedView";
|
||||
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
|
||||
import { buildStorageUrl } from "@/components/main/BaseUrl";
|
||||
import { Suspense } from "react";
|
||||
import PageLoader from "@/components/ui/PageLoader";
|
||||
import { generatePageMetadata } from "@/utils/generatePageMetadata";
|
||||
|
||||
type Props = { params: Promise<{ id: string }> };
|
||||
|
||||
@@ -11,34 +12,23 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { id } = await params;
|
||||
const post = await fetchPostById(id);
|
||||
const title = post?.caption
|
||||
? `${post.caption.slice(0, 60)} | مادستاگرام`
|
||||
: `پست | مادستاگرام`;
|
||||
? `${post.caption.slice(0, 60)} | مدستاگرام`
|
||||
: "پست | مدستاگرام";
|
||||
const description =
|
||||
post?.caption?.slice(0, 160) ||
|
||||
"مشاهده پست در مادستاگرام — پلتفرم مدلینگ و تبلیغات";
|
||||
"مشاهده پست در مدستاگرام — پلتفرم تخصصی حوزه زیبایی، مدلینگ و عکاسی";
|
||||
const ogImage = post?.files?.[0]?.path
|
||||
? buildStorageUrl(post.files[0].path)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
return generatePageMetadata({
|
||||
title,
|
||||
description,
|
||||
alternates: { canonical: `https://modstagram.com/posts/${id}` },
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: `https://modstagram.com/posts/${id}`,
|
||||
type: "article",
|
||||
images: ogImage ? [{ url: ogImage, width: 1080, height: 1350 }] : [],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title,
|
||||
description,
|
||||
images: ogImage ? [ogImage] : [],
|
||||
},
|
||||
robots: { index: true, follow: true },
|
||||
};
|
||||
path: `/posts/${id}`,
|
||||
type: "article",
|
||||
imageUrl: ogImage,
|
||||
imageAlt: post?.caption?.slice(0, 80) || "پست مدستاگرام",
|
||||
});
|
||||
}
|
||||
|
||||
export default async function PostPage({ params }: Props) {
|
||||
@@ -55,7 +45,7 @@ export default async function PostPage({ params }: Props) {
|
||||
__html: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SocialMediaPosting",
|
||||
headline: post.caption?.slice(0, 110) || "پست مادستاگرام",
|
||||
headline: post.caption?.slice(0, 110) || "پست مدستاگرام",
|
||||
url: `https://modstagram.com/posts/${id}`,
|
||||
datePublished: post.createdAt,
|
||||
author: {
|
||||
|
||||
@@ -4,6 +4,8 @@ import SearchFilter from "@/components/search/SearchFilter";
|
||||
import { fetchSearch } from "@/api/fetchSearch";
|
||||
import InfiniteSearch from "@/components/search/InfiniteSearch";
|
||||
import { Metadata } from "next";
|
||||
import { pageSeo } from "@/config/pageSeo";
|
||||
import { generatePageMetadata } from "@/utils/generatePageMetadata";
|
||||
|
||||
interface ISearchProps {
|
||||
searchParams: Promise<{
|
||||
@@ -15,30 +17,24 @@ interface ISearchProps {
|
||||
// تبدیل متادیتای ثابت به داینامیک
|
||||
export async function generateMetadata({ searchParams }: ISearchProps): Promise<Metadata> {
|
||||
const { search } = await searchParams;
|
||||
|
||||
const title = search
|
||||
? `نتایج جستجو برای "${search}" | مدستاگرام`
|
||||
: "جستجوی بیلبوردها و پروفایلها | مدستاگرام";
|
||||
|
||||
const description = search
|
||||
? `نتایج جستجوی تخصصی برای ${search} در حوزه مد و زیبایی مدستاگرام.`
|
||||
: "جستجو و فیلتر بیلبورد، فروشگاه، شهر و دستهبندی در پلتفرم مدستاگرام.";
|
||||
if (!search) {
|
||||
return generatePageMetadata({
|
||||
title: pageSeo.search.title,
|
||||
description: pageSeo.search.description,
|
||||
path: pageSeo.search.path,
|
||||
keywords: [...pageSeo.search.keywords],
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
const title = `نتایج جستجو برای «${search}» | مدستاگرام`;
|
||||
const description = `نتایج جستجوی تخصصی برای «${search}» در حوزه مد، زیبایی و مدلینگ در مدستاگرام.`;
|
||||
|
||||
return generatePageMetadata({
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: "https://modstagram.com/search",
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: "https://modstagram.com/search",
|
||||
siteName: "مدستاگرام",
|
||||
locale: "fa_IR",
|
||||
type: "website",
|
||||
}
|
||||
};
|
||||
path: `${pageSeo.search.path}?search=${encodeURIComponent(search)}`,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function Search({ searchParams }: ISearchProps) {
|
||||
|
||||
@@ -6,6 +6,8 @@ export const metadata: Metadata = {
|
||||
default: "تنظیمات",
|
||||
template: "%s | مدستاگرام",
|
||||
},
|
||||
description: "تنظیمات حساب کاربری در مدستاگرام",
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
43
src/components/seo/SiteJsonLd.tsx
Normal file
43
src/components/seo/SiteJsonLd.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { pageSeo, SITE_NAME, SITE_URL } from "@/config/pageSeo";
|
||||
|
||||
/** JSON-LD برای موتورهای جستجو و سیستمهای هوش مصنوعی */
|
||||
export default function SiteJsonLd() {
|
||||
const schema = {
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "Organization",
|
||||
"@id": `${SITE_URL}/#organization`,
|
||||
name: SITE_NAME,
|
||||
url: SITE_URL,
|
||||
logo: `${SITE_URL}/images/icons/256x256.png`,
|
||||
description: pageSeo.home.description,
|
||||
inLanguage: "fa-IR",
|
||||
},
|
||||
{
|
||||
"@type": "WebSite",
|
||||
"@id": `${SITE_URL}/#website`,
|
||||
name: SITE_NAME,
|
||||
url: SITE_URL,
|
||||
description: pageSeo.home.description,
|
||||
inLanguage: "fa-IR",
|
||||
publisher: { "@id": `${SITE_URL}/#organization` },
|
||||
potentialAction: {
|
||||
"@type": "SearchAction",
|
||||
target: {
|
||||
"@type": "EntryPoint",
|
||||
urlTemplate: `${SITE_URL}/search?search={search_term_string}`,
|
||||
},
|
||||
"query-input": "required name=search_term_string",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
104
src/config/pageSeo.ts
Normal file
104
src/config/pageSeo.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
export const SITE_URL = "https://modstagram.com";
|
||||
export const SITE_NAME = "مدستاگرام";
|
||||
|
||||
export type PageSeoEntry = {
|
||||
title: string;
|
||||
description: string;
|
||||
path: string;
|
||||
keywords?: string[];
|
||||
};
|
||||
|
||||
/** متادیتای صفحات اصلی — منبع واحد برای title / description */
|
||||
export const pageSeo = {
|
||||
home: {
|
||||
title: "مدستاگرام | پلتفرم تخصصی پیدا کردن مدل، عکاس و آرایشگر",
|
||||
description:
|
||||
"مدستاگرام، شبکه اجتماعی تخصصی حوزه زیبایی برای پیدا کردن مدل، عکاس و آرایشگر حرفهای، مشاهده پستها، ارتباط مستقیم و همکاری در پروژههای زیبایی و مد.",
|
||||
path: "/",
|
||||
keywords: [
|
||||
"مدستاگرام",
|
||||
"مدل",
|
||||
"عکاس",
|
||||
"آرایشگر",
|
||||
"مدلینگ",
|
||||
"شبکه اجتماعی زیبایی",
|
||||
"استخدام مدل",
|
||||
],
|
||||
},
|
||||
academy: {
|
||||
title: "آموزشگاه مدستاگرام | پکیجهای آموزشی زیبایی، میکاپ و عکاسی",
|
||||
description:
|
||||
"در آموزشگاه مدستاگرام، پکیجهای آموزشی تخصصی حوزه زیبایی، میکاپ، مدلینگ و عکاسی را ببینید، آموزش بگذارید و مهارتهای خود را حرفهایتر کنید.",
|
||||
path: "/academy",
|
||||
keywords: [
|
||||
"آموزشگاه مدستاگرام",
|
||||
"آموزش میکاپ",
|
||||
"آموزش مدلینگ",
|
||||
"آموزش عکاسی",
|
||||
"پکیج آموزشی زیبایی",
|
||||
],
|
||||
},
|
||||
projects: {
|
||||
title: "پروژههای مدستاگرام | ثبت و انتخاب همکاریهای زیبایی و مدلینگ",
|
||||
description:
|
||||
"در بخش پروژههای مدستاگرام، درخواست همکاری ثبت کنید یا از بین پروژههای زیبایی، مدلینگ، عکاسی و آرایشگری بهترین فرصت را برای همکاری انتخاب کنید.",
|
||||
path: "/projects",
|
||||
keywords: [
|
||||
"پروژه مدلینگ",
|
||||
"پروژه زیبایی",
|
||||
"همکاری مدل",
|
||||
"درخواست همکاری",
|
||||
"پروژه عکاسی",
|
||||
],
|
||||
},
|
||||
billboards: {
|
||||
title: "بیلبورد مدستاگرام | تبلیغات هدفمند کسبوکارهای حوزه زیبایی",
|
||||
description:
|
||||
"بیلبورد مدستاگرام، فضای ویژه تبلیغات کسبوکارهای حوزه زیبایی، سالنهای آرایش، برندهای آرایشی و خدمات تخصصی برای دیدهشدن بیشتر و جذب مشتری.",
|
||||
path: "/billboards",
|
||||
keywords: [
|
||||
"بیلبورد مدستاگرام",
|
||||
"تبلیغات زیبایی",
|
||||
"تبلیغ سالن آرایش",
|
||||
"تبلیغات مد و زیبایی",
|
||||
],
|
||||
},
|
||||
explore: {
|
||||
title: "اکسپلور مدستاگرام | کشف پستها و محتوای حوزه زیبایی",
|
||||
description:
|
||||
"در اکسپلور مدستاگرام، جدیدترین پستها، ریلها و محتوای تخصصی حوزه مد، زیبایی، میکاپ و عکاسی را کشف کنید.",
|
||||
path: "/explore",
|
||||
keywords: ["اکسپلور", "پست زیبایی", "ریل مد", "محتوای مدلینگ"],
|
||||
},
|
||||
search: {
|
||||
title: "جستجو در مدستاگرام | پیدا کردن مدل، عکاس، آرایشگر و بیلبورد",
|
||||
description:
|
||||
"جستجوی تخصصی در مدستاگرام برای یافتن مدل، عکاس، آرایشگر، بیلبورد و کسبوکارهای حوزه زیبایی.",
|
||||
path: "/search",
|
||||
keywords: ["جستجوی مدل", "جستجوی آرایشگر", "جستجوی بیلبورد"],
|
||||
},
|
||||
about: {
|
||||
title: "درباره مدستاگرام | پلتفرم تخصصی حوزه زیبایی و مد",
|
||||
description:
|
||||
"درباره مدستاگرام، پلتفرم تخصصی شبکه اجتماعی و همکاری در حوزه زیبایی، مدلینگ، عکاسی و آرایشگری.",
|
||||
path: "/about-us",
|
||||
keywords: ["درباره مدستاگرام", "تماس مدستاگرام"],
|
||||
},
|
||||
} as const satisfies Record<string, PageSeoEntry>;
|
||||
|
||||
export const siteKeywords = [
|
||||
"مدستاگرام",
|
||||
"مدل",
|
||||
"عکاس",
|
||||
"آرایشگر",
|
||||
"مدلینگ",
|
||||
"زیبایی",
|
||||
"میکاپ",
|
||||
"عکاسی",
|
||||
"پروژه مد",
|
||||
"بیلبورد زیبایی",
|
||||
"آموزش میکاپ",
|
||||
"شبکه اجتماعی زیبایی",
|
||||
"استخدام مدل",
|
||||
"همکاری مدلینگ",
|
||||
];
|
||||
@@ -1,23 +1,20 @@
|
||||
import { NextSeoProps } from "next-seo";
|
||||
|
||||
const BASE_URL = "https://modstagram.com";
|
||||
import { pageSeo, SITE_URL } from "@/config/pageSeo";
|
||||
|
||||
export const defaultSEOConfig: NextSeoProps = {
|
||||
title: "مدستاگرام - استخدام مدل و عکاس و آرایشگر در محل",
|
||||
description:
|
||||
"پلتفرم استخدام مدلینگ، آرایشگر در محل و عکاس حرفه ای مدل مزون، مدل عروس، مدل عکاسی، مدل آرایشی اپلیکیشنی اختصاصی استخدام و تبلیغات حوزه زیبایی",
|
||||
canonical: BASE_URL,
|
||||
title: pageSeo.home.title,
|
||||
description: pageSeo.home.description,
|
||||
canonical: SITE_URL,
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "fa_IR",
|
||||
url: BASE_URL,
|
||||
url: SITE_URL,
|
||||
site_name: "مدستاگرام",
|
||||
title: "مدستاگرام - استخدام مدل و عکاس و آرایشگر در محل",
|
||||
description:
|
||||
"پلتفرم استخدام مدلینگ، آرایشگر در محل و عکاس حرفه ای مدل مزون، مدل عروس، مدل عکاسی، مدل آرایشی اپلیکیشنی اختصاصی استخدام و تبلیغات حوزه زیبایی",
|
||||
title: pageSeo.home.title,
|
||||
description: pageSeo.home.description,
|
||||
images: [
|
||||
{
|
||||
url: `${BASE_URL}/images/icons/256x256.png`,
|
||||
url: `${SITE_URL}/images/icons/256x256.png`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "مدستاگرام",
|
||||
|
||||
@@ -1,69 +1,74 @@
|
||||
import { defaultSEOConfig } from "@/config/seoConfig";
|
||||
import { SITE_URL } from "@/config/pageSeo";
|
||||
import { Metadata } from "next";
|
||||
|
||||
interface PageMetadataOptions {
|
||||
title: string;
|
||||
description: string;
|
||||
path: string; // این باید مسیر نسبی صفحه باشد، مثلا '/users/123'
|
||||
type?: "website" | "article" | "profile"; // انواع Open Graph را اضافه کنید
|
||||
path: string;
|
||||
type?: "website" | "article" | "profile";
|
||||
publishedTime?: string;
|
||||
modifiedTime?: string;
|
||||
imageUrl?: string; // آدرس تصویر خاص برای این صفحه
|
||||
imageAlt?: string; // متن جایگزین تصویر
|
||||
imageUrl?: string;
|
||||
imageAlt?: string;
|
||||
keywords?: string[];
|
||||
/** اگر true باشد « - مدستاگرام» به انتهای عنوان اضافه میشود */
|
||||
appendSiteName?: boolean;
|
||||
}
|
||||
|
||||
export function generatePageMetadata(options: PageMetadataOptions): Metadata {
|
||||
const baseUrl = "https://modstagram.com"; // مطمئن شوید BASE_URL یکسان باشد
|
||||
const fullUrl = `${baseUrl}${options.path}`;
|
||||
const defaultOgImage = defaultSEOConfig.openGraph?.images?.[0];
|
||||
|
||||
const finalTitle = options.title
|
||||
? `${options.title} - مدستاگرام`
|
||||
: defaultSEOConfig.title; // عنوان صفحه + نام سایت
|
||||
const finalDescription = options.description || defaultSEOConfig.description;
|
||||
export function generatePageMetadata(options: PageMetadataOptions): Metadata {
|
||||
const fullUrl = `${SITE_URL}${options.path.startsWith("/") ? options.path : `/${options.path}`}`;
|
||||
|
||||
const finalTitle = options.appendSiteName
|
||||
? `${options.title} | مدستاگرام`
|
||||
: options.title;
|
||||
const finalDescription = options.description || defaultSEOConfig.description || "";
|
||||
|
||||
const images = options.imageUrl
|
||||
? [
|
||||
{
|
||||
url: options.imageUrl,
|
||||
width: 1200, // توصیه میشود این ابعاد را رعایت کنید
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: options.imageAlt || options.title,
|
||||
},
|
||||
]
|
||||
: defaultSEOConfig.openGraph?.images || []; // اگر تصویر خاصی نیست، از تصاویر پیشفرض استفاده کند
|
||||
: defaultOgImage
|
||||
? [defaultOgImage]
|
||||
: [];
|
||||
|
||||
return {
|
||||
title: {
|
||||
absolute: finalTitle,
|
||||
},
|
||||
description: finalDescription,
|
||||
keywords: options.keywords,
|
||||
alternates: {
|
||||
canonical: fullUrl,
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
openGraph: {
|
||||
title: finalTitle,
|
||||
description: finalDescription,
|
||||
url: fullUrl,
|
||||
siteName: defaultSEOConfig.openGraph?.site_name,
|
||||
siteName: defaultSEOConfig.openGraph?.site_name || "مدستاگرام",
|
||||
locale: "fa_IR",
|
||||
type: options.type || "website",
|
||||
// images: images,
|
||||
images,
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
site: defaultSEOConfig.twitter?.site,
|
||||
creator: defaultSEOConfig.twitter?.handle,
|
||||
title: finalTitle,
|
||||
description: finalDescription,
|
||||
images: images.map((img) => img.url), // تصاویر توییتر
|
||||
images: images.map((img) => img.url),
|
||||
},
|
||||
// اگر تگ های meta اضافی از defaultSEOConfig نیاز دارید
|
||||
// ...defaultSEOConfig.additionalMetaTags.reduce((acc: any, tag: any) => {
|
||||
// if (tag.name === 'google-site-verification') acc.google = tag.content;
|
||||
// if (tag.name === 'samandehi') acc.samandehi = tag.content;
|
||||
// return acc;
|
||||
// }, {}),
|
||||
|
||||
// برای meta tags های خاص مقاله/پروفایل
|
||||
...(options.type === "article" &&
|
||||
options.publishedTime &&
|
||||
options.modifiedTime
|
||||
@@ -74,13 +79,5 @@ export function generatePageMetadata(options: PageMetadataOptions): Metadata {
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(options.type === "profile"
|
||||
? {
|
||||
// اینجا می توانید متاتگ های schema.org برای پروفایل را اضافه کنید اگر از ساختار json-ld استفاده می کنید
|
||||
// @context: 'https://schema.org',
|
||||
// @type: 'ProfilePage',
|
||||
// ...
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user