البداية السريعة
اقرأ ملف شركتك ونفّذ بحثًا من تطبيق Next.js — متغيّرات البيئة والترويسات وغلاف الاستجابة والتقسيم إلى صفحات.
تبني هذه الصفحة أصغر شيء يعمل: مشروع Next.js يقرأ ملف شركتك على الخادم ويشغّل بحثًا من المتصفّح. انسخ أولًا القيم الأربع من الإعدادات ← المواقع ← اربط موقعك.
١. البيئة
# مضيف النظام مضافًا إليه /api. كل مسار أدناه نسبةً إليه.
NEXT_PUBLIC_API_BASE_URL=https://crm.example.com/api
# أيّ موقع يمثّله هذا النشر. يُرسَل في X-Site.
NEXT_PUBLIC_SITE_KEY=my-site
# المفتاح القابل للنشر (pk_…). عامّ بالتصميم: يُدمج في حزمة المتصفّح ولا يعمل
# إلا من نطاقات هذا الموقع نفسه.
NEXT_PUBLIC_SITE_PUBLISHABLE_KEY=pk_my-site_0123456789abcdef01234567
# المفتاح السرّي (sk_…) للنداءات من الخادم. بلا بادئة NEXT_PUBLIC_ عمدًا، حتى لا
# تضعه Next في حزمة المتصفّح أبدًا. اختياري — نداءات الخادم ترجع إلى المفتاح
# القابل للنشر عند غيابه.
SITE_SECRET_KEY=المتغيّرات المسبوقة بـ NEXT_PUBLIC_ وحدها هي التي تصل إلى المتصفّح. إن رأيت
يومًا sk_ داخل ملف JavaScript مبنيّ، فذلك المفتاح صار عامًّا — ألغِه من تبويب
«اربط موقعك» وأنشئ غيره.
٢. مكان واحد يبني الترويسات
كلا القناتين — المتصفّح وخادمك — يجب أن ترسلا الهوية نفسها، فعرّفها مرّة واحدة.
export function siteHeaders({ server = false } = {}) {
const headers: Record<string, string> = { Accept: 'application/json' };
const siteKey = process.env.NEXT_PUBLIC_SITE_KEY;
if (siteKey) headers['X-Site'] = siteKey;
// نداء الخادم لا يحمل Origin كي تفحصه الواجهة، وهذا بالضبط ما يحلّ محلّه
// المفتاح السرّي.
const apiKey =
(server ? process.env.SITE_SECRET_KEY : undefined) ??
process.env.NEXT_PUBLIC_SITE_PUBLISHABLE_KEY;
if (apiKey) headers['X-Api-Key'] = apiKey;
return headers;
}
export const API = process.env.NEXT_PUBLIC_API_BASE_URL!;القيمة غير المضبوطة يجب ألّا تُرسل ترويسة أصلًا، لا ترويسة فارغة: الطلب بلا
مفتاح هو طلب لا يدّعي هوية، أما X-Api-Key: '' فهو مفتاح لا يمكن التعرّف عليه —
وهذا شيء آخر قابل للرفض.
٣. اقرأ ملف الشركة (من الخادم)
import { API, siteHeaders } from '@/lib/wzgate';
export default async function Page() {
const res = await fetch(`${API}/public/company-profile`, {
headers: siteHeaders({ server: true }),
// الملف نادر التغيّر؛ أعد التحقّق بدل الجلب مع كل طلب.
next: { revalidate: 300 },
});
if (!res.ok) throw new Error(`Company profile: ${res.status}`);
const { data } = await res.json();
return <h1>{data.name}</h1>;
}٤. البحث (من المتصفّح)
import { API, siteHeaders } from '@/lib/wzgate';
export async function searchProperties(q: string, page = 1) {
const params = new URLSearchParams({ q, page: String(page), perPage: '24' });
const res = await fetch(`${API}/public/search?${params}`, {
headers: siteHeaders(),
});
const body = await res.json();
if (!res.ok) throw new Error(body.code ?? 'SEARCH_FAILED');
return { rows: body.data, pagination: body.pagination };
}غلاف الاستجابة
كل إجابة مغلّفة:
{ "success": true, "data": { }, "timestamp": "2026-09-14T09:30:27.019Z" }والإجابة المقسّمة إلى صفحات تضع pagination بجوار data لا داخلها:
{
"success": true,
"data": [],
"pagination": {
"total": 100, "lastPage": 9, "currentPage": 1, "perPage": 12,
"prev": null, "next": 2
},
"timestamp": "2026-09-14T09:30:27.019Z"
}فكّ الغلاف عن data مرّة واحدة في طبقة الاتصال، ودَعْ بقيّة الكود يرى صفوفًا
عادية.
ليست كل القوائم مقسّمة. GET /public/me/favorites و/public/me/saved-searches
و/public/me/viewing-requests و/public/owner-listings/mine تعيد مصفوفة
مجرّدة داخل data. عامِل «data ليست مصفوفة» على أنها «لا صفوف» بدل استدعاء
.map عليها.
التقسيم إلى صفحات
القوائم تقبل page (يبدأ من ١) و**perPage**، وسقف perPage هو ١٠٠ — وطلب
أكثر من ذلك يعطيك ١٠٠ بصمت، وليس خطأ. لا يوجد معامل اسمه limit.
GET /public/properties?page=2&perPage=24معاملات الاستعلام غير المعروفة خطأ 400
تتحقّق الواجهة من سلاسل الاستعلام بصرامة: المعامل الذي لم تعلنه هو 400، وليس
شيئًا يُتجاهل.
GET /public/properties?type=apartment
→ 400 { "message": ["property type should not exist"] }احذف معاملات واجهتك الخاصة قبل النداء. لا تمرّر سلسلة استعلام المتصفّح كما هي.
بعد ذلك
- تعريف موقعك — ماذا يحدث حين تغيب
X-Site، ولماذا يحتاجها نشر المعاينة. - الأخطاء — على ماذا تتفرّع حين يفشل النداء.
- مرجع الواجهة — كل نقطة نهاية ومعاملاتها.