العمارة متعددة المواقع لـ DSOs وسلاسل العيادات البيطرية والصالات الرياضية والامتيازات
مجموعة طب الأسنان التي تضم 50 عيادة لديها نفس مشكلة بنية الموقع الإلكتروني كما لدى سلسلة نوادي رياضية بـ 200 موقع، ومجموعة فنادق بـ 30 فندق، وشبكة كنائس بـ 15 فرع. جميعهم يحتاجون إلى: التحكم المركزي بالعلامة التجارية، المحتوى المخصص لكل موقع، لوحة تحكم إدارية واحدة، صفحات محسّنة للبحث المحلي لكل موقع، ونشر يحدّث كل شيء في نفس الوقت دون كسر أي شيء. الهندسة المعمارية متطابقة. المحتوى مختلف.
لقد بنيت هذا النمط لمجموعات طب الأسنان والامتيازات الرياضية وشبكات العيادات البيطرية وسلاسل المطاعم. في كل مرة، أبدأ بنفس مخطط قاعدة البيانات ونفس بنية مسارات Next.js ونفس التحكم في الوصول بناءً على الأدوار. ما يتغير هو بيانات البذور وتسميات المكونات. تصبح "الخدمات" "الفصول" في نادي رياضي أو "عناصر القائمة" في مطعم. تصبح "الموظفون" "أطباء أسنان" أو "مدربين" أو "أطباء بيطريين." الأنابيب الأساسية؟ متطابقة.
تضع هذه المقالة نمط العمارة متعددة المواقع العام مرة واحدة، ثم توضح كيف يتكيف مع خمس صناعات مختلفة تماماً. إذا كنت تدير أي نوع من أعمال متعددة المواقع — أو كنت مطوراً يبني لأحدها — هذا هو المخطط الأساسي.
جدول المحتويات
- المشكلة الأساسية التي تواجه كل عمل متعدد المواقع
- مخطط قاعدة البيانات العام
- معمارية مسارات Next.js
- الأمان على مستوى الصف ولوحة التحكم الإدارية
- اختلاف الصناعة 1: مجموعات طب الأسنان
- اختلاف الصناعة 2: سلاسل النوادي الرياضية
- اختلاف الصناعة 3: مجموعات الفنادق
- اختلاف الصناعة 4: سلاسل العيادات البيطرية
- اختلاف الصناعة 5: سلاسل المطاعم
- جدول مقارنة الهندسة المعمارية
- النشر والأداء على نطاق واسع
- تفصيل التكاليف: ما يكلفه هذا فعلياً في عام 2025
- الأسئلة الشائعة

المشكلة الأساسية التي تواجه كل عمل متعدد المواقع
لنكن صريحين حول ما يحدث عادة. يبدأ امتياز أو عمل متعدد المواقع بموقع واحد. ثم يفتحون موقعاً ثانياً. يقوم شخص ما بتشغيل نسخة WordPress ثانية. بحلول الوقت الذي يصل فيه العدد إلى 15 موقعاً، لديك 15 موقع WordPress منفصلاً، و15 مظهراً مختلفاً (بعضها متأخر عن ثلاث إصدارات)، و15 مجموعة مختلفة من المكونات الإضافية، وصفر تحكم مركزي.
يريد مدير التسويق تحديث الدعوة إلى الإجراء الرئيسية للعلامة التجارية عبر جميع المواقع. هذا يعني 15 تسجيل دخول و15 تعديل وصلاة لأن لا أحد قد كسر قالبهم. يريد فريق تحسين محركات البحث أن يرى أي المواقع تنشر محتوى المدونة وأيها ذهب إلى الظلام لمدة ستة أشهر. لا توجد لوحة تحكم لذلك — فقط جدول بيانات نسي شخص ما تحديثه في مارس.
هذه هي نفس المشكلة سواء كنت منظمة دعم طب الأسنان (DSO) تدير 50 ممارسة أو مجموعة مطاعم بـ 200 موقع. الأعراض متطابقة:
- انجراف العلامة التجارية. تنحرف المواقع عن العلامة التجارية لأن لا أحد ينفذ الاتساق.
- تجزئة محرك البحث. لا توجد صفحات محسّنة للبحث المحلي منظمة، لا اتساق في علامات المخطط، لا خريطة موقع مركزية.
- فوضى إدارية. تدير كل موقع موقعه الخاص (بشكل سيء)، أو يتعامل المقر الرئيسي مع كل شيء (ببطء).
- مخاطر النشر. لا يجب أن يؤدي تحديث موقع موقع واحد إلى تعطيل آخر.
الحل ليس أفضل مظهر CMS. إنها معمارية مختلفة تماماً.
مخطط قاعدة البيانات العام
كل شيء يبدأ بجدول locations. هذا هو المرسى للنظام بأكمله. أستخدم Supabase كطبقة قاعدة البيانات والمصادقة لأنها توفر Postgres والأمان على مستوى الصف والاشتراكات في الوقت الفعلي وطبقة مجانية سخية — لكن المخطط يعمل مع أي قاعدة بيانات علائقية.
إليك المخطط الأساسي:
-- جدول المرسى. كل جزء من المحتوى الخاص بالموقع
-- يشير إلى هذا عبر location_id.
CREATE TABLE locations (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
address TEXT NOT NULL,
city TEXT NOT NULL,
state TEXT NOT NULL,
zip TEXT NOT NULL,
lat DECIMAL(10, 8),
lng DECIMAL(11, 8),
phone TEXT,
email TEXT,
hours JSONB DEFAULT '{}',
photos TEXT[] DEFAULT '{}',
description TEXT,
metadata JSONB DEFAULT '{}',
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- جداول المحتوى تتبع نفس النمط:
-- location_id قابل للقيمة الفارغة.
-- NULL = مشترك عبر جميع المواقع
-- قيمة = خاص بهذا الموقع
CREATE TABLE services (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
slug TEXT NOT NULL,
description TEXT,
price_range TEXT,
duration TEXT,
category TEXT,
sort_order INT DEFAULT 0,
is_active BOOLEAN DEFAULT true,
metadata JSONB DEFAULT '{}'
);
CREATE TABLE staff (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
slug TEXT NOT NULL,
title TEXT,
photo TEXT,
bio TEXT,
credentials TEXT[],
specialties TEXT[],
sort_order INT DEFAULT 0,
is_active BOOLEAN DEFAULT true
);
CREATE TABLE blog_posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
title TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
content TEXT,
excerpt TEXT,
author_id UUID REFERENCES staff(id),
published_at TIMESTAMPTZ,
is_published BOOLEAN DEFAULT false,
tags TEXT[] DEFAULT '{}',
metadata JSONB DEFAULT '{}'
);
CREATE TABLE testimonials (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
author_name TEXT NOT NULL,
rating INT CHECK (rating >= 1 AND rating <= 5),
content TEXT,
is_approved BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE events (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
location_id UUID REFERENCES locations(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
event_date TIMESTAMPTZ,
end_date TIMESTAMPTZ,
is_active BOOLEAN DEFAULT true
);
نمط location_id القابل للقيمة الفارغة هو الرؤية الأساسية. عندما تحتوي منشور مدونة على location_id = NULL، تكون مقالة على مستوى الشبكة ("5 نصائح لأسنان صحية" مشتركة عبر جميع 50 عيادة أسنان). عندما تحتوي location_id على قيمة، تكون خاصة بذلك الموقع ("الدكتور سميث ينضم إلى ممارستنا في أوستن"). نفس الجدول، نفس أنماط الاستعلام، لكن المحتوى يمكن أن يكون مشتركاً أو محلياً مع عمود واحد.
عمود metadata JSONB هو حيث تعيش الحقول الخاصة بالصناعة. قد يخزن الموقع البيطري {"insurance_accepted": ["Delta Dental", "Cigna"], "parking_info": "Free lot behind building"}. يخزن صالة الألعاب الرياضية {"equipment": ["squat racks", "rowing machines"], "peak_hours": "5-7 PM weekdays"}. لا يلزم هجرة مخطط — فقط أشكال JSON مختلفة.
معمارية مسارات Next.js
يتم تعيين Next.js App Router بشكل نظيف إلى هذا النموذج الأساسي. إليك بنية المسار التي تعمل لكل صناعة:
app/
├── page.tsx # الصفحة الرئيسية
├── locations/
│ ├── page.tsx # محدد الموقع (خريطة + البحث الجغرافي)
│ └── [slug]/
│ ├── page.tsx # صفحة تفاصيل الموقع
│ ├── staff/page.tsx # قائمة الموظفين للموقع
│ └── services/page.tsx # الخدمات للموقع
├── services/
│ └── [service]/page.tsx # وصف الخدمة المشتركة
├── blog/
│ ├── page.tsx # جميع منشورات المدونة
│ └── [post]/page.tsx # منشور مدونة فردي
├── about/page.tsx
└── contact/page.tsx
صفحة تفاصيل الموقع (/locations/[slug]) هي حيث يحدث السحر. استدعاء generateStaticParams واحد يستعلم عن كل موقع نشط ويقوم بعرض جميعها مسبقاً في وقت البناء:
// app/locations/[slug]/page.tsx
import { createClient } from '@/lib/supabase/server'
export async function generateStaticParams() {
const supabase = createClient()
const { data: locations } = await supabase
.from('locations')
.select('slug')
.eq('is_active', true)
return locations?.map((loc) => ({ slug: loc.slug })) ?? []
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const supabase = createClient()
const { data: location } = await supabase
.from('locations')
.select('*')
.eq('slug', params.slug)
.single()
if (!location) return {}
return {
title: `${location.name} | ${location.city}, ${location.state}`,
description: location.description,
openGraph: {
title: `${location.name} - ${location.city}`,
images: location.photos?.[0] ? [location.photos[0]] : [],
},
}
}
export default async function LocationPage({ params }: { params: { slug: string } }) {
const supabase = createClient()
const [{ data: location }, { data: staff }, { data: services }, { data: testimonials }] =
await Promise.all([
supabase.from('locations').select('*').eq('slug', params.slug).single(),
supabase.from('staff').select('*').eq('location_id', params.slug), // simplified
supabase.from('services').select('*').or(`location_id.is.null,location_id.eq.${locationId}`),
supabase.from('testimonials').select('*').eq('is_approved', true),
])
// عرض صفحة الموقع مع جميع البيانات
// هذا هو نفس بنية المكون بغض النظر عن الصناعة
}
استعلام الخدمات يستخدم هذا المرشح or — احصل على الخدمات حيث location_id فارغة (خدمات مشتركة) أو تطابق الموقع الحالي. هذا يعني أنه يمكن لمجموعة طب الأسنان تحديد "تنظيف الأسنان" مرة واحدة لجميع المواقع، ثم إضافة "Invisalign" فقط للمواقع التي تقدمها. لا يوجد تكرار.
لصفحة محدد الموقع، أقوم بتخزين إحداثيات lat/lng واستخدام امتداد PostGIS الخاص بـ Supabase للاستعلامات الجغرافية:
-- العثور على المواقع في نطاق 25 ميلاً من إحداثيات المستخدم
SELECT *,
(point(lng, lat) <@> point($1, $2)) * 1.60934 AS distance_miles
FROM locations
WHERE is_active = true
ORDER BY point(lng, lat) <@> point($1, $2)
LIMIT 20;

الأمان على مستوى الصف ولوحة التحكم الإدارية
هذا هو حيث العمارة حقاً تؤتي ثمارها. سياسات Supabase RLS تسمح لك بتحديد الوصول إلى البيانات على مستوى قاعدة البيانات — ليس في كود التطبيق الخاص بك.
-- مديرو الموقع يمكنهم فقط رؤية بيانات موقعهم الخاص
CREATE POLICY "Location managers see own data" ON services
FOR ALL
USING (
location_id IN (
SELECT location_id FROM user_locations
WHERE user_id = auth.uid()
)
OR
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid() AND role = 'network_admin'
)
);
يرى مديرو الشبكة كل شيء. يرى مديرو الموقع فقط موقعهم. ينطبق هذا على كل جدول — الخدمات ومنشورات المدونة والموظفين والتوصيات والأحداث. نمط سياسة واحد، يُطبق باستمرار.
تعرض لوحة التحكم الإدارية مقاييس على مستوى الشبكة:
- حداثة المحتوى: أي المواقع لم تحدّث مدونتها منذ 30+ يوماً؟
- حركة المرور لكل موقع: بيانات Google Search Console مجمعة حسب slug الموقع
- العملاء المتوقعون لكل موقع: طلبات النماذج وطلبات الحجز حسب الموقع
- الامتثال للعلامة التجارية: هل تستخدم جميع المواقع الشعار الموافق عليه والألوان ونص الدعوة إلى الإجراء؟
اختلاف الصناعة 1: مجموعات طب الأسنان
يجب أن يشعر موقع مجموعة طب الأسنان بأنه علامة تجارية موحدة لطب الأسنان بينما يسمح لكل ممارسة بتسليط الضوء على مقدمي الخدمات والتخصصات الفريدة.
الخدمات تتماشى مع إجراءات طب الأسنان: التنظيف والحشوات والتيجان والزراعات و Invisalign والعناية الطارئة. بعضها عام (تقوم كل موقع بالتنظيف)، والبعض الآخر خاص بالموقع (فقط ثلاث مواقع تقدم طب الأسنان بالتخدير).
الموظفون هم أطباء أسنان وأخصائيو صحة فم ومديرو مكاتب. يحصل كل واحد على ملف شخصي يحتوي على بيانات اعتماد (DDS و DMD) والتخصصات والتعليم والصورة الاحترافية. يريد الوالدان اختيار طبيب أسنان للأطفال أن يرى من سيعالج طفلهم.
الدعوة إلى الإجراء هي "حجز موعد". هذا يتصل بـ Calendly أو NexHealth أو نظام حجز مخصص. تحدد أداة الحجز الموقع مسبقاً بناءً على صفحة الموقع التي جاء منها المستخدم.
أهداف البحث المحلي: "طبيب أسنان في [مدينة]"، "[إجراء] في [مدينة]"، "طبيب أسنان للطوارئ [مدينة] [ولاية]". تحصل كل صفحة موقع على علامات بيانات منظمة لمخططات Dentist و LocalBusiness.
JSONB Metadata تخزن: خطط التأمين المقبولة، معلومات الركن، ميزات إمكانية الوصول، اللغات المتحدثة، ما إذا كانت تقبل مرضى جدد.
اختلاف الصناعة 2: سلاسل النوادي الرياضية
تستبدل سلاسل النوادي الرياضية "الخدمات" بـ "الفصول" — لكن النموذج الأساسي هو نفسه. فئة اليوغا في الموقع A وفئة HIIT في الموقع B هي مجرد صفوف في جدول الخدمات بقيم location_id مختلفة.
الخدمات هي أنواع الفصول مع بيانات الجدول الزمني. تخزن البيانات الوصفية الجدول الزمني الأسبوعي كـ JSON وتعيين المدرب والحد الأقصى للسعة وما إذا كانت الزيارات المرتجلة مسموحة.
الموظفون هم مدربون ومدرسون يحملون شهادات (NASM و ACE و CrossFit L2) والتخصصات والتوفر للحجوزات الشخصية للتدريب.
الدعوة إلى الإجراء هي "انضم الآن" — دفع اشتراك Stripe يتعامل مع طبقات العضوية والوصول عبر الموقع. يجب أن يكون العضو الذي اشترك في الموقع وسط المدينة قادراً على تسجيل الوصول في الموقع الضاحية أيضاً.
أهداف البحث المحلي: "صالة ألعاب بالقرب مني"، "فصول لياقة بدنية [مدينة]"، "فصول [نوع الفصل] [مدينة]"، "مدرب شخصي [مدينة]".
JSONB Metadata تخزن: قائمة المعدات والجدول الزمني للفصل وساعات الذروة والمرافق (حمام بخار وحمام سباحة ورعاية أطفال) وتوفر الركن المجاني.
اختلاف الصناعة 3: مجموعات الفنادق
تستفيد مجموعات الفنادق البوتيك وسلاسل الفنادق المستقلة بشكل كبير من هذا النمط — خاصة لأنه يتيح الحجوزات المباشرة التي تتجاوز رسوم وكالات السفر عبر الإنترنت (عادة 15-25% لكل حجز على Booking.com أو Expedia).
الخدمات تصبح أنواع الغرف: الغرفة الموحدة وجناح الملك والشقة الفاخرة. يحصل كل واحد على صور وقوائم الميزات والقدم المربعة والتسعير الأساسي. يعيش التسعير الخاص بالموقع في البيانات الوصفية أو جدول أسعار منفصل بنطاقات التاريخ.
الموظفون أخف هنا — ربما مدير عام مميز أو كونسيرج مشهور لروايات العلامة التجارية.
الدعوة إلى الإجراء هي "احجز مباشرة" — نمط FME (البحث والمطابقة والتفاعل) الذي يعطي الضيوف سبباً لحجز موقع الفندق الخاص به بدلاً من وكالة سفر عبر الإنترنت. عادة ما تكون "ضمان أفضل سعر" أو ترقية مجانية.
أهداف البحث المحلي: "فنادق في [مدينة]"، "تقييمات [اسم الفندق]"، "فندق بوتيك [حي] [مدينة]"، "فنادق بالقرب من [معلم]".
JSONB Metadata تخزن: المرافق (حمام سباحة وسبا ومطعم وصالة ألعاب رياضية وشحن السيارات الكهربائية) والمعالم القريبة وتقويم الأحداث المحلية وأوقات الوصول والمغادرة وسياسة الحيوانات الأليفة.
اختلاف الصناعة 4: سلاسل العيادات البيطرية
تنمو سلاسل العيادات البيطرية بسرعة في عام 2025 — التوحيد في الطب البيطري يعكس ما حدث مع مجموعات طب الأسنان قبل عقد من الزمان. تنطبق نفس العمارة متعددة المواقع بشكل مثالي.
الخدمات هي خدمات الرعاية البيطرية: فحوصات الصحة والتطعيمات وتنظيف الأسنان والجراحة والعناية الطارئة والإقامة والعناية بالصحة. تقدم بعض المواقع رعاية الحيوانات الغريبة؛ معظمها لا.
الموظفون هم الأطباء البيطريون بخبرة الأنواع (الحيوانات الصغيرة والخيول والحيوانات الغريبة) والتصديقات المجلسية والتعليم.
الدعوة إلى الإجراء هي "احجز موعداً" مع منعطف — يجب أن تجمع نموذج الاستقبال معلومات الحيوان الأليف (النوع والسلالة والعمر وسبب الزيارة) لتوجيه الموعد بشكل صحيح.
أهداف البحث المحلي: "طبيب بيطري في [مدينة]"، "طبيب بيطري طارئ [مدينة]"، "[نوع حيوان] طبيب بيطري [مدينة]"، "تنظيف أسنان الحيوانات الأليفة [مدينة]".
JSONB Metadata تخزن: الأنواع المقبولة وساعات الطوارئ (إن اختلفت عن ساعات العمل العادية) وسعة الإقامة وما إذا كان لديهم مختبر وتصوير في الموقع.
اختلاف الصناعة 5: سلاسل المطاعم
الخدمات تصبح أقسام القائمة: المقبلات والأطباق الرئيسية والحلويات والمشروبات. الشيء الحاسم هنا هو أن التسعير يمكن أن يختلف حسب الموقع. يكلف البرغر 14 دولاراً في أوستن و 19 دولاراً في مانهاتن. يتعامل عمود metadata مع هذا باستخدام تجاوزات تسعير خاصة بالموقع.
الموظفون هم الشيفات أو مديرو الشواء المميزون — هذا يعمل بشكل أفضل للعلامات التجارية حيث يكون الأشخاص وراء الطعام جزءاً من القصة.
الدعوة إلى الإجراء هي "طلب عبر الإنترنت" — رابط يدرك الموقع يوجهك إلى نظام الطلبات عبر الإنترنت الصحيح (Toast أو Square أو ChowNow أو مخصص) لأقرب موقع للمستخدم.
أهداف البحث المحلي: "قائمة [اسم المطعم] [مدينة]"، "مطاعم بالقرب مني"، "مطعم [نوع الطعام] [مدينة]"، "ساعات [اسم المطعم]".
JSONB Metadata تخزن: نطاق التسليم وتوفر الحجوزات (مع رابط OpenTable أو Resy) وتفاصيل الركن وسعة الطعام الخاص وأوقات ساعة الكوكتيل.
جدول مقارنة الهندسة المعمارية
| المكون | مجموعة طب الأسنان | سلسلة النادي الرياضي | مجموعة الفنادق | سلسلة العيادة البيطرية | المطعم |
|---|---|---|---|---|---|
| تسمية "الخدمات" | الإجراءات | الفصول | أنواع الغرف | خدمات الحيوانات الأليفة | عناصر القائمة |
| تسمية "الموظفون" | أطباء الأسنان | المدربون | الإدارة | الأطباء البيطريون | الشيفات |
| الدعوة الرئيسية إلى الإجراء | احجز موعداً | انضم إلى العضوية | احجز غرفة | احجز موعداً | اطلب عبر الإنترنت |
| تكامل الحجز | NexHealth و Calendly | اشتراكات Stripe | مخصص / Cloudbeds | مخصص + استقبال الحيوانات الأليفة | Toast و Square |
| البيانات المحلية الأساسية | التأمين والركن | الجدول الزمني والمعدات | المرافق والمعالم | الأنواع وساعات الطوارئ | تسعير القائمة والتسليم |
| كلمة البحث الرئيسية | "طبيب أسنان في [مدينة]" | "صالة ألعاب بالقرب مني" | "فنادق في [مدينة]" | "طبيب بيطري في [مدينة]" | "قائمة [الماركة] [مدينة]" |
| علامات المخطط | Dentist و LocalBusiness | SportsActivityLocation | Hotel و LodgingBusiness | VeterinaryCare | Restaurant و Menu |
| جداول قاعدة البيانات المتغيرة | 0 | 0 | 0 | 0 | 0 |
الصف الأخير هو النقطة. لا توجد جداول قاعدة بيانات تتغير بين الصناعات. تستخدم نفس جداول locations و services و staff و blog_posts و testimonials و events. التسميات في واجهة المستخدم تتغير. أشكال البيانات الوصفية تتغير. الهندسة المعمارية لا تتغير.
النشر والأداء على نطاق واسع
نقوم بالنشر على Vercel مع ISR (الإنشاء الثابت الإضافي). تُنشأ كل صفحة موقع بشكل ثابت وقت البناء وتُعاد التحقق من صحتها كل 60 ثانية. بالنسبة لسلسلة 200 موقع، هذا يعني 200 صفحة HTML ثابتة تحمل في أقل من ثانية على أي جهاز.
الأرقام مهمة. إليك ما نراه عادة:
- وقت البناء لـ 200 موقع: ~45 ثانية على Vercel Pro
- TTFB لكل صفحة موقع: < 50ms (يُخدم من CDN الحافة)
- درجات Lighthouse: 95+ عبر اللوحة
- إعادة التحقق من صحة ISR: 60 ثانية من الملل والتحقق من الصحة تعني أن تحديثات المحتوى تظهر في غضون دقيقة بدون إعادة بناء كاملة
إضافة موقع جديد هي إدراج في قاعدة البيانات بالإضافة إلى استدعاء إعادة التحقق من الصحة عند الطلب الاختياري. لا يلزم نشر جديد. تختار دالة generateStaticParams المواقع الجديدة في دورة البناء أو ISR التالية.
// مسار API لتفعيل إعادة التحقق من الصحة عند إضافة/تحديث موقع
import { revalidatePath } from 'next/cache'
export async function POST(request: Request) {
const { slug } = await request.json()
revalidatePath('/locations')
revalidatePath(`/locations/${slug}`)
return Response.json({ revalidated: true })
}
تفصيل التكاليف: ما يكلفه هذا فعلياً في عام 2025
دعنا نتحدث عن أرقام حقيقية. هذا سؤال شائع نتلقاه أثناء محادثات التسعير.
| المكون | التكلفة الشهرية (50 موقع) | التكلفة الشهرية (200 موقع) |
|---|---|---|
| Supabase Pro | $25 | $25 (نفس الطبقة تتعامل مع كليهما) |
| Vercel Pro | $20 | $20 |
| Vercel Bandwidth (تجاوز) | ~$0 | ~$40 |
| Domain + DNS (Cloudflare) | $0 | $0 |
| Image CDN (Cloudflare R2) | ~$5 | ~$15 |
| Monitoring (Sentry) | $26 | $26 |
| إجمالي البنية التحتية | ~$76/شهر | ~$126/شهر |
قارن ذلك بـ 50 موقع WordPress منفصلة بـ ~$30/شهر لكل واحد للاستضافة المدارة — هذا $1500/شهر قبل أن تفكر حتى في الصيانة أو تراخيص المكونات الإضافية أو الشخص الذي يجب أن يبقيهم جميعاً محدثة.
الاستثمار الإنمائي أعلى في البداية — نقتبس عادة بنية متعددة المواقع في نطاق $30K-$80K اعتماداً على التعقيد — لكن تكلفة التشغيل الجاري هي جزء صغير من بديل WordPress multisite. وأنت لا تدفع $500/شهر لكل موقع لبعض بائعي موقع الامتياز الذي يقفلك في منصته.
للفريق المهتم باستكشاف تكاملات CMS بدون رأس أو التفكير في Astro بدلاً من Next.js لبناء ثابت أسرع حتى، تنطبق نفس هندسة قاعدة البيانات. إطار عمل الواجهة الأمامية قابل للتبديل؛ نموذج البيانات ليس كذلك.
الأسئلة الشائعة
هل يمكن لهذه الهندسة المعمارية التعامل مع المواقع في مناطق زمنية مختلفة؟
بالتأكيد. يخزن عمود hours JSONB ساعات التشغيل الخاصة بكل موقع في المنطقة الزمنية المحلية. نتضمن حقل timezone (مثل "America/Chicago") في البيانات الوصفية للموقع واستخدمه لأي عروض حساسة للوقت مثل شارات "Open Now". يتم تخزين كل الطوابع الزمنية في قاعدة البيانات بصيغة UTC وتحويلها على الواجهة الأمامية.
كيف تتعامل مع المواقع التي تقدم خدمات مختلفة؟
هذا هو نمط location_id القابل للقيمة الفارغة أثناء العمل. الخدمات ذات location_id = NULL مشتركة عبر جميع المواقع — تظهر في صفحة كل موقع. الخدمات ذات location_id محددة تظهر فقط لذلك الموقع. يمكنك أيضاً استخدام جدول الوصل (location_services) للعلاقات كثير-إلى-كثير إذا كانت الخدمات المشتركة بحاجة إلى تجاوزات خاصة بالموقع مثل التسعير أو التوفر المخصص.
ماذا يحدث عندما يفتح موقع جديد؟
يضيف مدير الشبكة الموقع عبر لوحة التحكم. هذا ينشئ صفاً في جدول locations ويطلق webhook يفعّل إعادة التحقق من صحة ISR وتكون صفحة الموقع الجديد مباشرة في غضون 60 ثانية. لا حاجة للمطور أو الإعادة أو تغييرات DNS. يرث الموقع جميع الخدمات والمحتوى المشترك فوراً.
هل هذا أفضل من WordPress Multisite للامتيازات؟ لمعظم الأعمال متعددة المواقع، نعم. كان WordPress Multisite الإجابة المعيارية لعقد، لكن لها مشاكل حقيقية: يمكن لثغرة المكون الإضافي الواحدة أن تأخذ الشبكة بأكملها، تتدهور الأداء مع إضافة مواقع، وتحتاج إلى مسؤول نظام مخصص لإبقاء الأمور صحية. هذه الهندسة المعمارية بدون رأس توفر أداء الموقع الثابت والأمان على مستوى قاعدة البيانات وصفر مخاطر وقت التشغيل المشترك بين المواقع.
كيف يمكن لمديري الموقع تحرير محتواهم الخاص بدون كسر المواقع الأخرى؟ الأمان على مستوى الصف في قاعدة البيانات يضمن أن مدير الموقع في أوستن حرفياً لا يمكنه رؤية أو تعديل البيانات التي تنتمي إلى موقع دنفر. لم يتم تطبيقه بواسطة كود التطبيق الذي قد يكون به أخطاء — تم تطبيقه بواسطة Postgres نفسها. حتى لو كانت واجهة المستخدم الإدارية بها خطأ حاولت الاستعلام عن بيانات موقع آخر، ستُرجع قاعدة البيانات نتائج فارغة.
هل تحصل كل موقع على خريطة موقع خاصة به؟
تحصل كل صفحة موقع على إدخالتها في خريطة موقع ديناميكية واحدة يتم إنشاؤها في وقت البناء. نقوم أيضاً بإنشاء بيانات منظمة خاصة بالموقع (JSON-LD) بمخطط LocalBusiness وإحداثيات جغرافية وساعات التشغيل والأنواع الخاصة بالصناعة. تعامل Google كل صفحة /locations/[slug] كقائمة عمل محلية متميزة، وهو بالضبط ما تريده لترتيبات الحزمة المحلية.
هل يمكن للمواقع أن تحصل على منشورات مدونة خاصة بها بينما تشارك محتوى الشبكة؟
نعم — هذا هو نمط location_id القابل للقيمة الفارغة مرة أخرى. منشورات المدونة ذات location_id = NULL تظهر في كل موقع مدونة. الوظائف ذات location_id المحددة تظهر فقط على مدونة هذا الموقع. يمكن لموقع في ميامي نشر منشور حول حدث محلي في المجتمع بينما يقوم الفريق البشري بنشر قيادة فكرية على مستوى الشبكة. كلاهما يظهر في مدونة ميامي؛ فقط المنشور البشري يظهر في كل مكان آخر.
كم تكلفة الصيانة الجارية مقابل إدارة 50 موقع منفصل؟ مع هذه الهندسة المعمارية، يوجد قاعدة أكواد واحدة ونشر واحد ومجموعة واحدة من الاعتماديات للحفاظ عليها. تعمل البنية التحتية الشهرية بسعر $75-$125 حسب الحجم. قارن ذلك بـ 50 تثبيت WordPress: $1500/شهر في الاستضافة وحدها، بالإضافة إلى 10-20 ساعة شهرياً في تحديثات المكون الإضافي وتصحيحات الأمان واستكشاف أخطاء الموقع الوحيد الذي كسره تحديث تلقائي. رأينا شركات متعددة المواقع تقطع ميزانية عمليات الويب السنوية بنسبة 60-70% بعد الترحيل إلى هذا النمط.