Compare commits

..
10 Commits
Author SHA1 Message Date
amirhosein.ashourloo 1d320b8f10 Redesign core pages and connect dynamic content 2026-10-08 18:42:19 +03:30
amirhosein.ashourloo 55d0fad2c8 feat: redesign site pages and connect contact API 2026-09-22 02:00:26 +03:30
amirhosein.ashourloo 6b3e87a09d Redesign about page and stabilize site data 2026-09-21 18:10:21 +03:30
amirhosein.ashourloo fe4c110eaf Merge Next security update with homepage dependencies 2026-09-16 22:11:17 +03:30
amirhosein.ashourloo 8855e09c23 Edit 2026-09-16 21:53:35 +03:30
websites-deploy 4b87869acb Upgrade Next.js to 15.5.25 for RSC RCE patch.
Keep standalone output for lean Meshkee production runtime.
2026-09-08 11:11:27 +00:00
amirhosein.ashourloo 6f7e24d21b Countactus 2026-08-15 17:15:53 +03:30
amirhosein.ashourloo 2f6c1a547e Add en title 2026-08-15 14:04:09 +03:30
Alireza HassaniandCursor 02c7dd51d2 Tune caching: shorter revalidate, lighter fetches, on-demand purge.
Use paginated list fetches with stripped list payloads, recent-item helpers for carousels, fetch tags + POST /api/revalidate, and 5-minute page ISR instead of 1 hour.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 11:32:18 +03:30
Alireza HassaniandCursor f341b010c0 Reduce memory use by caching API calls and using slug endpoints.
Use React cache() for list/slug fetches, hit single-item portfolio/blog routes instead of loading full lists, and dedupe metadata + page requests on news/blog detail pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 11:15:51 +03:30
160 changed files with 7546 additions and 1184 deletions
+4
View File
@@ -0,0 +1,4 @@
# Runtime env (set in PM2 ecosystem on the websites VM — not committed)
# Secret for POST /api/revalidate (on-demand cache purge after CMS publish)
REVALIDATE_SECRET=
+4 -4
View File
@@ -110,12 +110,12 @@ src/
## صفحه «درباره ما» ## صفحه «درباره ما»
ظاهر این صفحه الهام‌گرفته از [omniyat.com/omniyat-atelier](https://www.omniyat.com/omniyat-atelier) ظاهر این صفحه بر پایه زبان بصری جدید وب‌سایت رئوفی طراحی شده است.
است، اما **تمام محتوا از صفحه درباره ما سایت رئوفی** برداشته شده است. است، اما **تمام محتوا از صفحه درباره ما سایت رئوفی** برداشته شده است.
سیستم چیدمانِ برگرفته از مرجع: سیستم چیدمانِ برگرفته از مرجع:
| ویژگی | مرجع (Omniyat) | پیاده‌سازی | | ویژگی | طراحی هدف | پیاده‌سازی |
| --- | --- | --- | | --- | --- | --- |
| ریتم عمودی بلوک متن | `192px 48px` | `192px 48px` (موبایل: `96px 24px`) | | ریتم عمودی بلوک متن | `192px 48px` | `192px 48px` (موبایل: `96px 24px`) |
| کپشن بالای تیتر | ۱۴px، `letter-spacing: 1.4px` | همان — فقط روی بخش لاتین | | کپشن بالای تیتر | ۱۴px، `letter-spacing: 1.4px` | همان — فقط روی بخش لاتین |
@@ -137,7 +137,7 @@ src/
## صفحه «خدمات ما» ## صفحه «خدمات ما»
همان سیستم چیدمان صفحه درباره ما (الهام‌گرفته از Omniyat Atelier). همان سیستم چیدمان صفحه درباره ما و زبان بصری جدید وب‌سایت.
**نکته‌ی مهم درباره‌ی محتوا:** صفحه‌ی خدمات سایت فعلی فقط شامل شش عنوان خدمت و تیتر **نکته‌ی مهم درباره‌ی محتوا:** صفحه‌ی خدمات سایت فعلی فقط شامل شش عنوان خدمت و تیتر
«با خدمات ما بیشتر آشنا شوید...» است — هیچ توضیحی برای خدمات ندارد و لینک همه‌ی «با خدمات ما بیشتر آشنا شوید...» است — هیچ توضیحی برای خدمات ندارد و لینک همه‌ی
@@ -159,7 +159,7 @@ src/
## نکات ## نکات
- لینک‌های ناوبری به مسیرهای واقعی سایت (`/portfolios`، `/news`، …) اشاره می‌کنند - لینک‌های ناوبری به مسیرهای واقعی سایت (`/projects`، `/news`، …) اشاره می‌کنند
ولی تاکنون فقط صفحه اصلی و درباره ما طراحی شده‌اند. ولی تاکنون فقط صفحه اصلی و درباره ما طراحی شده‌اند.
- انیمیشن‌ها به `prefers-reduced-motion` احترام می‌گذارند. - انیمیشن‌ها به `prefers-reduced-motion` احترام می‌گذارند.
- کروسل پروژه‌ها برای RTL نوشته شده (`scrollLeft` منفی) و با کیبورد و لمس کار می‌کند. - کروسل پروژه‌ها برای RTL نوشته شده (`scrollLeft` منفی) و با کیبورد و لمس کار می‌کند.
+15 -1
View File
@@ -7,7 +7,7 @@ const nextConfig: NextConfig = {
formats: ["image/avif", "image/webp"], formats: ["image/avif", "image/webp"],
// Every quality value used across the page. Required from Next.js 16 on. // Every quality value used across the page. Required from Next.js 16 on.
qualities: [70, 72, 75, 80, 82, 88, 90], qualities: [70, 72, 75, 80, 82, 88, 90],
// Portfolio media served live from the meshkee API. The storage host has // Project media served live from the meshkee API. The storage host has
// already changed once mid-project (parminstorage.ir -> parspack.net) — // already changed once mid-project (parminstorage.ir -> parspack.net) —
// keep both around since either could be what a given item still points to. // keep both around since either could be what a given item still points to.
remotePatterns: [ remotePatterns: [
@@ -16,6 +16,20 @@ const nextConfig: NextConfig = {
{ protocol: "https", hostname: "c804387.parspack.net" }, { protocol: "https", hostname: "c804387.parspack.net" },
], ],
}, },
async redirects() {
return [
{
source: "/our-services",
destination: "/atelier",
permanent: true,
},
{
source: "/portfolios/:path*",
destination: "/projects/:path*",
permanent: true,
},
];
},
}; };
export default nextConfig; export default nextConfig;
+4783 -56
View File
File diff suppressed because it is too large Load Diff
+6 -4
View File
@@ -3,20 +3,22 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "node scripts/dev.cjs",
"build": "next build", "build": "next build",
"start": "next start" "start": "next start"
}, },
"dependencies": { "dependencies": {
"next": "15.5.4", "next": "15.5.25",
"react": "19.1.1", "react": "^19.2.8",
"react-dom": "19.1.1" "react-dom": "^19.2.8",
"swiper": "^14.2.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.1.13", "@tailwindcss/postcss": "^4.1.13",
"@types/node": "^22", "@types/node": "^22",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint-config-next": "^15.5.25",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",
"typescript": "^5" "typescript": "^5"
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 876 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

+48
View File
@@ -0,0 +1,48 @@
const { execFileSync, spawn } = require("node:child_process");
const path = require("node:path");
function supportsNext(nodePath) {
try {
const version = execFileSync(nodePath, ["--version"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
const [major, minor] = version.replace(/^v/, "").split(".").map(Number);
return major >= 20 || (major === 18 && minor >= 18) || (major === 19 && minor >= 8);
} catch {
return false;
}
}
function installedNodes() {
const command = process.platform === "win32" ? "where.exe" : "which";
const args = process.platform === "win32" ? ["node"] : ["-a", "node"];
try {
return execFileSync(command, args, { encoding: "utf8" })
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean);
} catch {
return [];
}
}
const nodePath = [process.execPath, ...installedNodes()].find(supportsNext);
if (!nodePath) {
console.error("Next.js به Node.js 18.18 یا جدیدتر نیاز دارد. لطفاً Node.js LTS را نصب کنید.");
process.exit(1);
}
const nextCli = require.resolve("next/dist/bin/next");
const child = spawn(nodePath, [nextCli, "dev", ...process.argv.slice(2)], {
cwd: path.resolve(__dirname, ".."),
stdio: "inherit",
env: process.env,
});
child.on("exit", (code, signal) => {
if (signal) process.kill(process.pid, signal);
process.exit(code ?? 1);
});
+8 -97
View File
@@ -1,114 +1,25 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import atelier from "@/assets/images/about/atelier.jpg"; import { RaoofiAbout } from "@/components/about/RaoofiAbout";
import about from "@/assets/images/about/about.jpg";
import heroImage from "@/assets/images/hero/hero-2.jpg";
import heroImageMobile from "@/assets/images/hero/hero-2-mobile.jpg";
import { Pillars } from "@/components/about/Pillars";
import { Editorial } from "@/components/shared/Editorial";
import { FullBleed } from "@/components/shared/FullBleed";
import { PageCta } from "@/components/shared/PageCta";
import { PageHero } from "@/components/shared/PageHero";
import { ProjectGallery } from "@/components/shared/ProjectGallery";
import { Reveal } from "@/components/Reveal";
import { SiteFooter } from "@/components/SiteFooter"; import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader"; import { SiteHeader } from "@/components/SiteHeader";
import { aboutPage } from "@/data/site"; import { getRecentProjects, toDisplayProject } from "@/lib/api";
import { getPortfolios, toDisplayProject } from "@/lib/api";
export const revalidate = 300;
export const metadata: Metadata = { export const metadata: Metadata = {
title: "درباره ما", title: "درباره ما",
description: description: "روایت شکل‌گیری، ارزش‌ها و مسیر گروه ساختمانی رئوفی از سال ۱۳۷۹ تا امروز.",
"از سال ۱۳۷۹، دیدگاه مهندس قاسم رئوفی‌منش مسیر گروه رئوفی را تعریف کرده است؛ دیدگاهی که هر پروژه را به تجربه‌ای اصیل و ماندگار از سبک زندگی ارتقاء می‌دهد.",
}; };
export default async function AboutPage() { export default async function AboutPage() {
const portfolios = await getPortfolios(); const projects = await getRecentProjects(8);
const displayProjects = portfolios.map(toDisplayProject); const displayProjects = projects.map(toDisplayProject);
return ( return (
<> <>
<SiteHeader /> <SiteHeader />
<RaoofiAbout projects={displayProjects} />
<main>
<PageHero
image={heroImage}
imageMobile={heroImageMobile}
caption={aboutPage.hero.caption}
title={aboutPage.hero.title}
breadcrumb={aboutPage.hero.breadcrumb}
/>
{/* Overview */}
<section className="bg-paper">
<Editorial
caption={aboutPage.overview.caption}
captionLatin={aboutPage.overview.captionLatin}
title={aboutPage.overview.title}
body={aboutPage.overview.body}
/>
<FullBleed image={atelier} alt="معماری پروژه‌های گروه ساختمانی رئوفی" />
</section>
{/* Philosophy */}
<section className="bg-sand">
<Editorial
caption={aboutPage.philosophy.caption}
captionLatin={aboutPage.philosophy.captionLatin}
title={aboutPage.philosophy.title}
body={aboutPage.philosophy.body}
/>
</section>
{/* Foundations */}
<section className="bg-paper">
<Editorial
caption={aboutPage.pillars.caption}
captionLatin={aboutPage.pillars.captionLatin}
title={aboutPage.pillars.title}
body={aboutPage.pillars.body}
/>
<Pillars />
</section>
{/* Portfolio */}
<section className="bg-sand">
<Editorial
caption={aboutPage.portfolio.caption}
captionLatin={aboutPage.portfolio.captionLatin}
title={aboutPage.portfolio.title}
body={aboutPage.portfolio.body}
/>
<ProjectGallery projects={displayProjects} />
</section>
{/* Closing statement */}
<section className="bg-paper">
<div className="mx-auto max-w-[1400px] px-6 py-24 text-center lg:px-12 lg:py-48">
<Reveal>
<p className="mx-auto max-w-[860px] text-[1.35rem] leading-[2.1] font-light text-ink lg:text-[1.75rem] lg:leading-[2]">
{aboutPage.closing.body}
</p>
</Reveal>
<Reveal delay={140}>
<span
aria-hidden="true"
className="mx-auto mt-14 block h-px w-24 bg-gold"
/>
</Reveal>
</div>
<FullBleed image={about} alt="نمای پروژه‌های گروه ساختمانی رئوفی" />
</section>
<PageCta
caption={aboutPage.cta.caption}
title={aboutPage.cta.title}
action={aboutPage.cta.action}
/>
</main>
<SiteFooter /> <SiteFooter />
</> </>
); );
+72
View File
@@ -0,0 +1,72 @@
import { NextResponse } from "next/server";
const CONTACT_ENDPOINT =
"https://api.meshkee.com/api/v1/tenants/raoofigroup.com/contact-submissions";
const REQUEST_TIMEOUT_MS = 10_000;
type ContactRequest = {
name?: unknown;
phone?: unknown;
email?: unknown;
message?: unknown;
};
function clean(value: unknown, maxLength: number) {
return typeof value === "string" ? value.trim().slice(0, maxLength) : "";
}
export async function POST(request: Request) {
let body: ContactRequest;
try {
body = (await request.json()) as ContactRequest;
} catch {
return NextResponse.json({ message: "اطلاعات فرم معتبر نیست." }, { status: 400 });
}
const name = clean(body.name, 120);
const cellNumber = clean(body.phone, 16);
const email = clean(body.email, 254);
const text = clean(body.message, 4_000);
if (!name || !text || !/^\+989\d{9}$/.test(cellNumber)) {
return NextResponse.json({ message: "لطفاً اطلاعات فرم را به‌درستی وارد کنید." }, { status: 400 });
}
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return NextResponse.json({ message: "نشانی ایمیل معتبر نیست." }, { status: 400 });
}
try {
const response = await fetch(CONTACT_ENDPOINT, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "درخواست تماس از وب‌سایت رئوفی",
name,
cellNumber,
text,
...(email ? { email } : {}),
}),
cache: "no-store",
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
if (!response.ok) {
return NextResponse.json(
{ message: "ارسال پیام با خطا روبه‌رو شد. لطفاً دوباره تلاش کنید." },
{ status: response.status >= 500 ? 502 : 400 },
);
}
return NextResponse.json({ message: "پیام شما با موفقیت ارسال شد." }, { status: 201 });
} catch {
return NextResponse.json(
{ message: "در حال حاضر ارتباط با سرویس پیام برقرار نیست. لطفاً کمی بعد دوباره تلاش کنید." },
{ status: 503 },
);
}
}

Some files were not shown because too many files have changed in this diff Show More