diff --git a/Ai-data/prompt-seo.txt b/Ai-data/prompt-seo.txt
new file mode 100644
index 0000000..54a55b8
--- /dev/null
+++ b/Ai-data/prompt-seo.txt
@@ -0,0 +1,18 @@
+### Sitemap + robots.txt
+- Do NOT ship public/sitemap.xml or public/robots.txt (nginx proxies both to Meshkee API).
+- Live sitemap is an index: /sitemap.xml → /sitemap-main.xml + /sitemap-products.xml.
+- Publish GET /meshkee/sitemap-config.json generated at build time from app routes.
+- Include public static routes automatically; exclude auth/checkout/cart/admin paths.
+- Canonical detail URLs (Meshkee defaults — omit templates unless customized):
+ - Product: /products/{id}/{nameFaSlug} → GET /tenants/{domain}/products/by-id/{id}
+ - Category: /products/category/{id}/{nameFaSlug} → GET /tenants/{domain}/categories/by-id/{id} (then list products with categoryId)
+ - Blog: /blog/{id}/{titleSlug} → GET /tenants/{domain}/blogs/by-id/{id}
+ - Portfolio: /portfolios/{id}/{titleFaSlug} → GET /tenants/{domain}/portfolios/by-id/{id}
+- Slugify: Farsi letters kept, spaces → -, strip other punctuation. Do not use legacy DB slug in public URLs.
+- After deploy: business dashboard → Website → Settings → Sync sitemap config (optional for CMS URLs; needed for static pages).
+
+### On-page SEO (every public page)
+- Unique
and meta description on every crawlable page
+- Exactly one H1; H2/H3 for real sections
+- All content images must have meaningful alt (CMS title when available)
+- Prefer CMS fields for titles/descriptions; never leave Next defaults
diff --git a/package.json b/package.json
index 9ad2872..ebd95b3 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,8 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "prebuild": "node scripts/generate-sitemap-config.mjs",
+ "generate:sitemap-config": "node scripts/generate-sitemap-config.mjs",
"build": "next build",
"start": "next start",
"lint": "eslint"
diff --git a/scripts/generate-sitemap-config.mjs b/scripts/generate-sitemap-config.mjs
new file mode 100644
index 0000000..eb6e036
--- /dev/null
+++ b/scripts/generate-sitemap-config.mjs
@@ -0,0 +1,110 @@
+/**
+ * Scans app page routes and writes src/lib/sitemap-config.generated.json
+ * for GET /meshkee/sitemap-config.json (Meshkee sitemap sync).
+ */
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const appDir = path.join(root, "src", "app");
+const outFile = path.join(root, "src", "lib", "sitemap-config.generated.json");
+
+const EXCLUDE_SEGMENTS = new Set([
+ "api",
+ "meshkee",
+ "admin",
+ "auth",
+ "login",
+ "checkout",
+ "cart",
+ "account",
+]);
+
+const PRIORITY = {
+ "/": { changefreq: "weekly", priority: 1.0 },
+ "/cms-website": { changefreq: "monthly", priority: 0.9 },
+ "/website": { changefreq: "monthly", priority: 0.9 },
+ "/website-qom": { changefreq: "monthly", priority: 0.9 },
+ "/seo": { changefreq: "monthly", priority: 0.8 },
+ "/dedicated-application": { changefreq: "monthly", priority: 0.8 },
+ "/meshkee-cms": { changefreq: "monthly", priority: 0.8 },
+ "/graphic-designer": { changefreq: "monthly", priority: 0.7 },
+ "/portfolio": { changefreq: "weekly", priority: 0.8 },
+ "/blog": { changefreq: "weekly", priority: 0.8 },
+};
+
+function websiteDomain() {
+ const fromEnv =
+ process.env.NEXT_PUBLIC_WEBSITE_DOMAIN ||
+ process.env.WEBSITE_DOMAIN ||
+ process.env.NEXT_PUBLIC_MESHKEE_DOMAIN ||
+ process.env.NEXT_PUBLIC_SITE_URL;
+
+ if (fromEnv) {
+ return fromEnv.replace(/^https?:\/\//, "").replace(/\/$/, "");
+ }
+
+ return "www.meshkee.com";
+}
+
+function walkPages(dir, segments = []) {
+ /** @type {string[]} */
+ const pages = [];
+ if (!fs.existsSync(dir)) return pages;
+
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
+ const full = path.join(dir, entry.name);
+
+ if (entry.isDirectory()) {
+ if (EXCLUDE_SEGMENTS.has(entry.name)) continue;
+ if (/^\[.+\]$/.test(entry.name) || /^\(.+\)$/.test(entry.name)) {
+ if (/^\(.+\)$/.test(entry.name)) {
+ pages.push(...walkPages(full, segments));
+ }
+ continue;
+ }
+ pages.push(...walkPages(full, [...segments, entry.name]));
+ continue;
+ }
+
+ if (
+ entry.name === "page.tsx" ||
+ entry.name === "page.ts" ||
+ entry.name === "page.jsx" ||
+ entry.name === "page.js"
+ ) {
+ const routePath = segments.length ? `/${segments.join("/")}` : "/";
+ pages.push(routePath);
+ }
+ }
+
+ return pages;
+}
+
+function main() {
+ const paths = [...new Set(walkPages(appDir))].sort((a, b) => {
+ if (a === "/") return -1;
+ if (b === "/") return 1;
+ return a.localeCompare(b);
+ });
+
+ const staticPages = paths.map((routePath) => {
+ const meta = PRIORITY[routePath] ?? { priority: 0.5 };
+ return { path: routePath, ...meta };
+ });
+
+ const config = {
+ baseUrl: `https://${websiteDomain()}`,
+ staticPages,
+ };
+
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
+ fs.writeFileSync(outFile, `${JSON.stringify(config, null, 2)}\n`, "utf8");
+ console.log(
+ `Wrote ${path.relative(root, outFile)} (${staticPages.length} static pages)`,
+ );
+}
+
+main();
diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx
index c860cb1..8334a61 100644
--- a/src/app/blog/[slug]/page.tsx
+++ b/src/app/blog/[slug]/page.tsx
@@ -48,6 +48,12 @@ export async function generateMetadata({
return {
title: `${blog.title} | مشکی`,
description: blog.abstract ?? undefined,
+ alternates: { canonical: `/blog/${blog.slug}` },
+ openGraph: {
+ title: blog.title,
+ description: blog.abstract ?? undefined,
+ type: "article",
+ },
};
} catch {
return { title: "مقاله | مشکی" };
@@ -126,7 +132,11 @@ export default async function BlogDetailPage({ params }: BlogDetailPageProps) {
{cover ? (
-
+
) : null}
diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx
index 721592c..265e8d4 100644
--- a/src/app/blog/page.tsx
+++ b/src/app/blog/page.tsx
@@ -69,7 +69,7 @@ export default async function BlogListPage({ searchParams }: BlogPageProps) {
>
-
+
{post.title}
diff --git a/src/app/meshkee/sitemap-config.json/route.ts b/src/app/meshkee/sitemap-config.json/route.ts
new file mode 100644
index 0000000..998558b
--- /dev/null
+++ b/src/app/meshkee/sitemap-config.json/route.ts
@@ -0,0 +1,17 @@
+import sitemapConfig from "@/lib/sitemap-config.generated.json";
+
+const headers = {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type",
+ "Cache-Control": "public, max-age=60",
+ "Content-Type": "application/json; charset=utf-8",
+};
+
+export async function OPTIONS() {
+ return new Response(null, { status: 204, headers });
+}
+
+export async function GET() {
+ return Response.json(sitemapConfig, { headers });
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 8333cff..99fa88c 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,3 +1,4 @@
+import type { Metadata } from "next";
import { AppSection } from "@/components/AppSection";
import { BlogSection } from "@/components/BlogSection";
import { CmsSection } from "@/components/CmsSection";
@@ -10,6 +11,15 @@ import { Portfolio } from "@/components/Portfolio";
import { RevealObserver } from "@/components/RevealObserver";
import { Services } from "@/components/Services";
+export const metadata: Metadata = {
+ title: {
+ absolute: "تیم برنامهنویسی و تجارت الکترونیک مشکی | Meshkee",
+ },
+ description:
+ "راهکارهای مدرن برای تجارت شما — طراحی وبسایت، اپلیکیشن، سیستمهای مدیریتی، سئو و گرافیک توسط تیم مشکی.",
+ alternates: { canonical: "/" },
+};
+
export default function HomePage() {
return (
<>
diff --git a/src/app/portfolio/[slug]/page.tsx b/src/app/portfolio/[slug]/page.tsx
index 8a36d65..74e40d8 100644
--- a/src/app/portfolio/[slug]/page.tsx
+++ b/src/app/portfolio/[slug]/page.tsx
@@ -66,6 +66,12 @@ export async function generateMetadata({
return {
title: `${title} | مشکی`,
description: portfolio.abstract ?? undefined,
+ alternates: { canonical: `/portfolio/${portfolio.slug}` },
+ openGraph: {
+ title,
+ description: portfolio.abstract ?? undefined,
+ type: "article",
+ },
};
} catch {
return { title: "نمونهکار | مشکی" };
@@ -180,6 +186,7 @@ export default async function PortfolioDetailPage({
category={categoryFa}
date={date}
dateTime={portfolio.publishedAt}
+ title={title}
/>
{portfolio.mainTextHtml ? (
diff --git a/src/app/portfolio/page.tsx b/src/app/portfolio/page.tsx
index ca2ae65..d7ac638 100644
--- a/src/app/portfolio/page.tsx
+++ b/src/app/portfolio/page.tsx
@@ -181,7 +181,7 @@ export default async function PortfolioListPage({
{image ? (
) : null}
diff --git a/src/app/robots.ts b/src/app/robots.ts
deleted file mode 100644
index b228014..0000000
--- a/src/app/robots.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { MetadataRoute } from "next";
-
-export default function robots(): MetadataRoute.Robots {
- const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "https://www.meshkee.com";
-
- return {
- rules: {
- userAgent: "*",
- allow: "/",
- },
- sitemap: `${siteUrl}/sitemap.xml`,
- host: siteUrl,
- };
-}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
deleted file mode 100644
index d7db2c0..0000000
--- a/src/app/sitemap.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { MetadataRoute } from "next";
-
-const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "https://www.meshkee.com";
-
-const routes = [
- "/",
- "/cms-website",
- "/website-qom",
- "/website",
- "/seo",
- "/dedicated-application",
- "/meshkee-cms",
- "/graphic-designer",
- "/portfolio",
- "/blog",
-];
-
-export default function sitemap(): MetadataRoute.Sitemap {
- const lastModified = new Date();
-
- return routes.map((path) => ({
- url: `${siteUrl}${path === "/" ? "" : path}`,
- lastModified,
- changeFrequency: path === "/" ? "weekly" : "monthly",
- priority: path === "/" ? 1 : path === "/website-qom" || path === "/cms-website" ? 0.9 : 0.7,
- }));
-}
diff --git a/src/components/BlogCoverImage.tsx b/src/components/BlogCoverImage.tsx
index 3e466ed..db3b6fe 100644
--- a/src/components/BlogCoverImage.tsx
+++ b/src/components/BlogCoverImage.tsx
@@ -4,10 +4,15 @@ import { useState } from "react";
type BlogCoverImageProps = {
src: string | null;
+ alt?: string;
className?: string;
};
-export function BlogCoverImage({ src, className = "" }: BlogCoverImageProps) {
+export function BlogCoverImage({
+ src,
+ alt = "",
+ className = "",
+}: BlogCoverImageProps) {
const [failed, setFailed] = useState(false);
if (!src || failed) {
@@ -25,7 +30,7 @@ export function BlogCoverImage({ src, className = "" }: BlogCoverImageProps) {
return (
setFailed(true)}
className={`h-auto w-full object-contain transition duration-700 group-hover:scale-105 ${className}`.trim()}
/>
diff --git a/src/components/BlogSection.tsx b/src/components/BlogSection.tsx
index 74dab8a..7841b66 100644
--- a/src/components/BlogSection.tsx
+++ b/src/components/BlogSection.tsx
@@ -53,7 +53,7 @@ export async function BlogSection() {
>
-
+
{post.title}
diff --git a/src/components/PortfolioGallery.tsx b/src/components/PortfolioGallery.tsx
index 602edf7..3fd62d4 100644
--- a/src/components/PortfolioGallery.tsx
+++ b/src/components/PortfolioGallery.tsx
@@ -13,6 +13,7 @@ type PortfolioGalleryProps = {
category?: string | null;
date?: string | null;
dateTime?: string | null;
+ title?: string | null;
};
export function PortfolioGallery({
@@ -21,6 +22,7 @@ export function PortfolioGallery({
category,
date,
dateTime,
+ title,
}: PortfolioGalleryProps) {
const slides: GalleryImage[] = [
...(cover ? [{ id: "cover", url: cover }] : []),
@@ -77,7 +79,7 @@ export function PortfolioGallery({
>
@@ -118,7 +120,11 @@ export function PortfolioGallery({
>
@@ -175,7 +181,11 @@ export function PortfolioGallery({
event.stopPropagation()}
/>
diff --git a/src/components/ServicePageLayout.tsx b/src/components/ServicePageLayout.tsx
index 1687745..d367603 100644
--- a/src/components/ServicePageLayout.tsx
+++ b/src/components/ServicePageLayout.tsx
@@ -48,7 +48,7 @@ function SolutionFeatureCard({ feature }: { feature: Feature }) {
{feature.icon ? (