Compare commits

...
12 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
amirhosein.ashourloo 9455ebe7cd Merge branch 'main' of https://gitea.meshkee.com/meshkee-websites/raoufi-group 2026-08-14 10:25:04 +03:30
amirhosein.ashourloo 9342d15767 Update 2026-08-14 10:22:37 +03:30
160 changed files with 7547 additions and 1185 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`) |
| کپشن بالای تیتر | ۱۴px، `letter-spacing: 1.4px` | همان — فقط روی بخش لاتین |
@@ -137,7 +137,7 @@ src/
## صفحه «خدمات ما»
همان سیستم چیدمان صفحه درباره ما (الهام‌گرفته از Omniyat Atelier).
همان سیستم چیدمان صفحه درباره ما و زبان بصری جدید وب‌سایت.
**نکته‌ی مهم درباره‌ی محتوا:** صفحه‌ی خدمات سایت فعلی فقط شامل شش عنوان خدمت و تیتر
«با خدمات ما بیشتر آشنا شوید...» است — هیچ توضیحی برای خدمات ندارد و لینک همه‌ی
@@ -159,7 +159,7 @@ src/
## نکات
- لینک‌های ناوبری به مسیرهای واقعی سایت (`/portfolios`، `/news`، …) اشاره می‌کنند
- لینک‌های ناوبری به مسیرهای واقعی سایت (`/projects`، `/news`، …) اشاره می‌کنند
ولی تاکنون فقط صفحه اصلی و درباره ما طراحی شده‌اند.
- انیمیشن‌ها به `prefers-reduced-motion` احترام می‌گذارند.
- کروسل پروژه‌ها برای RTL نوشته شده (`scrollLeft` منفی) و با کیبورد و لمس کار می‌کند.
+15 -1
View File
@@ -7,7 +7,7 @@ const nextConfig: NextConfig = {
formats: ["image/avif", "image/webp"],
// Every quality value used across the page. Required from Next.js 16 on.
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) —
// keep both around since either could be what a given item still points to.
remotePatterns: [
@@ -16,6 +16,20 @@ const nextConfig: NextConfig = {
{ 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;
+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",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "node scripts/dev.cjs",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "15.5.4",
"react": "19.1.1",
"react-dom": "19.1.1"
"next": "15.5.25",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"swiper": "^14.2.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.13",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint-config-next": "^15.5.25",
"tailwindcss": "^4.1.13",
"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 atelier from "@/assets/images/about/atelier.jpg";
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 { RaoofiAbout } from "@/components/about/RaoofiAbout";
import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader";
import { aboutPage } from "@/data/site";
import { getPortfolios, toDisplayProject } from "@/lib/api";
import { getRecentProjects, toDisplayProject } from "@/lib/api";
export const revalidate = 300;
export const metadata: Metadata = {
title: "درباره ما",
description:
"از سال ۱۳۷۹، دیدگاه مهندس قاسم رئوفی‌منش مسیر گروه رئوفی را تعریف کرده است؛ دیدگاهی که هر پروژه را به تجربه‌ای اصیل و ماندگار از سبک زندگی ارتقاء می‌دهد.",
description: "روایت شکل‌گیری، ارزش‌ها و مسیر گروه ساختمانی رئوفی از سال ۱۳۷۹ تا امروز.",
};
export default async function AboutPage() {
const portfolios = await getPortfolios();
const displayProjects = portfolios.map(toDisplayProject);
const projects = await getRecentProjects(8);
const displayProjects = projects.map(toDisplayProject);
return (
<>
<SiteHeader />
<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>
<RaoofiAbout projects={displayProjects} />
<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