بناء دليل عالمي بـ 137 ألف قائمة باستخدام Next.js و Supabase و Vercel ISR
في العام الماضي، أطلقنا دليلاً عالمياً بـ 137,000 قائمة. ليس نموذجاً أولياً. ليس "MVP سنحسنه لاحقاً". نظام إنتاجي يخدم ملايين مشاهدات الصفحة، يحتل مرتبة لآلاف الكلمات الرئيسية طويلة الذيل، وينشئ الصفحات عند الطلب دون أي مشاكل. هذه هي قصة كيفية بنائنا له — والقرارات المعمارية التي جعلتها ممكنة.
المجموعة: Next.js 14 (App Router)، Supabase (PostgreSQL + Edge Functions)، Vercel (الاستضافة + ISR)، وجرعة صحية من الواقعية. ارتكبنا أخطاء. واجهنا عقبات. أعدنا كتابة أشياء اعتقدنا أنها انتهت. لكن المعمارية النهائية تتعامل مع 137,000+ صفحة ديناميكية مع TTFB أقل من 200ms عالمياً، وفاتورة Supabase الخاصة بنا تبقى تحت $100/شهر.
إذا كنت تبني شيئاً مشابهاً — سوق، دليل، منصة قوائم — فهذا هو المقال الذي تمنيت أنه كان موجوداً عندما بدأنا.
جدول المحتويات
- لماذا هذه المجموعة
- طبقة البيانات: Supabase على نطاق واسع
- استراتيجية توليد الصفحات: ISR و SSG ومشكلة 137K
- معمارية URL والـ SEO على نطاق واسع
- البحث والتصفية: الجزء الصعب
- ميزانيات الأداء والتخزين المؤقت للحافة
- المراقبة والقابلية للرصد في الإنتاج
- تفصيل التكاليف: تكلفة هذا فعلاً
- ما كنا سنفعله بشكل مختلف
- الأسئلة الشائعة

لماذا هذه المجموعة
قيمنا الكثير من الخيارات قبل أن نستقر على Next.js + Supabase + Vercel. المتطلبات الأساسية كانت:
- 137,000+ صفحة فريدة يمكن لمحركات البحث الزحف إليها والفهرسة
- تحميل الصفحة في أقل من ثانية عالمياً (المستخدمون في 40+ دول)
- بيانات ديناميكية — تُحدَّث القوائم يومياً، بعضها كل ساعة
- بحث نصي كامل مع تصفية ذات أوجه متعددة
- واعي للميزانية — هذا لم يكن حلماً ممول من رأس المال الاستثماري
اعتبرنا Astro (رائع للمواقع الثابتة، لكننا احتجنا إلى تفاعل ديناميكي أكثر — على الرغم من أن فريق تطويرنا Astro أطلق مشاريع دليل ممتازة معها). نظرنا إلى WordPress + WPEngine. اعتبرنا بإيجاز SPA نقية مع Algolia.
فاز Next.js لأنه يمتلك ميزة قتالية واحدة: Incremental Static Regeneration. ISR تعني أننا لم نضطر إلى الاختيار بين أداء ثابتة والمحتوى الديناميكي. يمكننا أن يكون لدينا كلاهما.
فازت Supabase على PlanetScale و Neon لأنها توفر المجموعة الكاملة — المصادقة والتخزين ووظائف الحافة وتطبيق Postgres جيد حقاً مع أمان على مستوى الصفوف. بالنسبة لدليل، تحتاج إلى كل هذا.
كان Vercel هو هدف النشر لأن ISR تعمل بشكل أفضل على Vercel (بدون مفاجآت). التكامل أصلي. إعادة التحقق عند الطلب تعمل ببساطة.
ماذا عن الاستضافة الذاتية؟
طورنا إعداد Next.js مستضاف ذاتياً على Railway. كان يعمل، لكن ISR على Next.js المستضاف ذاتياً له غرائب. قصة إلغاء الذاكرة المؤقتة أسوأ. تحتاج إلى إدارة طبقة CDN خاصة بك. بالنسبة لفريق من 3 مهندسين، كانت النفقات التشغيلية لا تستحق توفير $200/شهر.
طبقة البيانات: Supabase على نطاق واسع
تحتفظ قاعدة البيانات Supabase الخاصة بنا بـ 137,000 قائمة، لكل منها 40-60 حقلاً. الفئات والمواقع ومعلومات الاتصال والأوصاف الغنية والصور والتقييمات وساعات التشغيل — كل شيء.
تصميم المخطط
كان أكبر قرار هو ما إذا كنا سنستخدم مخطط علائقي معياري أو نهجاً موجهاً نحو المستندات أكثر مع أعمدة JSONB. ذهبنا هجيناً:
CREATE TABLE listings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
description TEXT,
category_id UUID REFERENCES categories(id),
city_id UUID REFERENCES cities(id),
country_code TEXT NOT NULL,
coordinates GEOGRAPHY(POINT, 4326),
contact JSONB DEFAULT '{}',
attributes JSONB DEFAULT '{}',
media JSONB DEFAULT '[]',
rating_avg NUMERIC(3,2) DEFAULT 0,
rating_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'active',
published_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ DEFAULT NOW(),
search_vector TSVECTOR
);
CREATE INDEX idx_listings_category ON listings(category_id) WHERE status = 'active';
CREATE INDEX idx_listings_city ON listings(city_id) WHERE status = 'active';
CREATE INDEX idx_listings_country ON listings(country_code) WHERE status = 'active';
CREATE INDEX idx_listings_coordinates ON listings USING GIST(coordinates);
CREATE INDEX idx_listings_search ON listings USING GIN(search_vector);
CREATE INDEX idx_listings_slug ON listings(slug);
بيانات علائقية منظمة للأشياء التي نصفيها على (الفئات والمدن والدول). JSONB للأشياء شبه المنظمة التي تختلف لكل قائمة (طرق الاتصال والسمات المخصصة وصفائف الوسائط). أعطى لنا هذا أفضل ما في كلا العالمين — استعلامات مفهرسة سريعة على الأعمدة العلائقية والمرونة على الباقي.
متجه البحث
هذا العمود search_vector حرج. نملأه بمشغل:
CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', COALESCE(NEW.title, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(NEW.description, '')), 'B') ||
setweight(to_tsvector('english', COALESCE(NEW.attributes->>'keywords', '')), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
هذا يعني أن كل قائمة قابلة للبحث بنصها الكامل من خلال Postgres نفسه. لا حاجة لخدمة بحث خارجية للـ 100K قائمة الأولى. سنتحدث عن متى يحدث هذا لاحقاً.
تجميع الاتصالات
يستخدم Supabase PgBouncer لتجميع الاتصالات. مع ISR، تحصل على انفجارات من استدعاءات الوظائف بدون خادم — كل واحد يحتاج إلى اتصال قاعدة بيانات. بدون التجميع، ستستنزف الاتصالات في دقائق.
نستخدم سلسلة الاتصال المجمعة (port 6543) لجميع السياقات بدون خادم وسلسلة الاتصال المباشرة (port 5432) فقط للهجرات والمهام الإدارية. هذا هو أحد تلك الأشياء التي تبدو واضحة لكنها تمسك الناس.
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // Server-side only
{
db: { schema: 'public' },
auth: { persistSession: false }
}
)
استراتيجية توليد الصفحات: ISR و SSG ومشكلة 137K
هنا حيث تصبح الأمور مثيرة للاهتمام. وحيث ارتكبنا أكبر خطأ في البداية.
النهج الساذج (لا تفعل هذا)
محاولتنا الأولى: توليد جميع الصفحات الـ 137,000 في وقت البناء باستخدام generateStaticParams. استغرق البناء 4 ساعات و 22 دقيقة. لدى Vercel الطبقة المجانية حد أقصى 45 دقيقة للبناء. حتى الطبقة Pro تحد عند 6 ساعات. لكن المشكلة الحقيقية لم تكن المهلة الزمنية — كانت حلقة التغذية الراجعة. كل نشر استغرق نصف يوم. هذا غير عملي.
نهج ISR (ما يعمل فعلاً)
إليك الاستراتيجية التي تم شحنها:
- عند البناء: توليد أفضل 5,000 صفحة (حسب حركة المرور) بشكل ثابت
- في أول طلب: توليد الصفحات المتبقية عند الطلب وتخزينها مؤقتاً
- إعادة التحقق: بناءً على الوقت (كل 3600 ثانية) + عند الطلب عبر webhook
// app/listing/[slug]/page.tsx
import { supabase } from '@/lib/supabase'
import { notFound } from 'next/navigation'
export async function generateStaticParams() {
// Only pre-generate top listings by traffic
const { data } = await supabase
.from('listings')
.select('slug')
.eq('status', 'active')
.order('rating_count', { ascending: false })
.limit(5000)
return (data || []).map((listing) => ({
slug: listing.slug,
}))
}
export const revalidate = 3600 // Revalidate every hour
export default async function ListingPage({ params }: { params: { slug: string } }) {
const { data: listing, error } = await supabase
.from('listings')
.select(`
*,
category:categories(*),
city:cities(*, country:countries(*))
`)
.eq('slug', params.slug)
.eq('status', 'active')
.single()
if (!listing || error) notFound()
return <ListingDetail listing={listing} />
}
إعادة التحقق عند الطلب
عندما يحدّث صاحب القائمة بياناته، لا نريد أن ننتظر ساعة على الأكثر لكي تنعش الصفحة. تشغل Supabase webhooks مسار API التالي في Next.js:
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidation-secret')
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { slug, type } = await request.json()
if (type === 'listing') {
revalidatePath(`/listing/${slug}`)
revalidatePath(`/`) // Revalidate homepage too
}
return NextResponse.json({ revalidated: true })
}
هذا يعطينا أفضل ما في كلا العالمين: أداء الموقع الثابت مع الطزاجة الديناميكية. تنتهي الأبنية في أقل من 8 دقائق. الصفحات التي لم يتم توليدها مسبقاً يتم إنشاؤها في أول زيارة وتخزينها مؤقتاً على الحافة.
الأرقام
| المقياس | SSG كامل (ساذج) | ISR (الإنتاج) |
|---|---|---|
| وقت البناء | 4س 22د | 7د 40ث |
| الصفحات عند النشر | 137,000 | 5,000 |
| أول زيارة (غير مخزنة مؤقتاً) | غير متاح | ~800ms |
| الزيارات اللاحقة | ~120ms | ~120ms |
| كمون إعادة التحقق | نشر كامل | < 2 ثانية |
| دقائق البناء الشهري | يتجاوز الحد بكثير | ~230 دقيقة |

معمارية URL والـ SEO على نطاق واسع
مع 137,000 صفحة، معمارية URL ليست بعد الفكرة — إنها معمارية. كل URL هي فرصة تصنيف.
التسلسل الهرمي للـ URL
/ → الصفحة الرئيسية
/categories/[category-slug] → صفحات الفئات (48 فئة)
/locations/[country]/[city] → صفحات الموقع
/listing/[listing-slug] → القائمة الفردية
/search?q=...&category=...&city=... → نتائج البحث (noindex)
صفحات تقاطع الفئة + الموقع هي كنز SEO الحقيقي:
/categories/restaurants/us/new-york → "المطاعم في نيويورك"
/categories/hotels/uk/london → "الفنادق في لندن"
يتم توليد صفحات التقاطع هذه بشكل ديناميكي مع ISR. هناك تقريباً 12,000 تركيبة صحيحة. كل واحدة تستهدف كلمة رئيسية محددة بذيل طويل.
توليد خريطة الموقع
مع 137K عنوان URL، تحتاج إلى ملفات فهرس خريطة الموقع. حد Google هو 50,000 عنوان URL لكل خريطة موقع.
// app/sitemap/[id]/route.ts
export async function GET(request: Request, { params }: { params: { id: string } }) {
const page = parseInt(params.id)
const perPage = 45000 // Stay under the 50K limit
const offset = page * perPage
const { data: listings } = await supabase
.from('listings')
.select('slug, updated_at')
.eq('status', 'active')
.order('id')
.range(offset, offset + perPage - 1)
const xml = generateSitemapXml(listings)
return new Response(xml, {
headers: { 'Content-Type': 'application/xml' },
})
}
نقسم إلى 4 خرائط موقع: sitemap-0.xml حتى sitemap-3.xml، يشار إليها من قبل فهرس خريطة الموقع. فهرس Google Search 98% من عناوين URL المرسلة خلال 6 أسابيع.
البيانات المنظمة
كل صفحة قائمة تتضمن بيانات منظمة JSON-LD. للدليل، مخطط LocalBusiness حرج:
const structuredData = {
'@context': 'https://schema.org',
'@type': 'LocalBusiness',
name: listing.title,
description: listing.description,
address: {
'@type': 'PostalAddress',
addressLocality: listing.city.name,
addressCountry: listing.city.country.code,
},
geo: {
'@type': 'GeoCoordinates',
latitude: listing.coordinates?.lat,
longitude: listing.coordinates?.lng,
},
aggregateRating: listing.rating_count > 0 ? {
'@type': 'AggregateRating',
ratingValue: listing.rating_avg,
reviewCount: listing.rating_count,
} : undefined,
}
البحث والتصفية: الجزء الصعب
البحث دائماً الجزء الصعب. دائماً.
المرحلة الأولى: بحث النصوص الكاملة في Postgres
بالنسبة لإطلاقنا الأولي، تعامل بحث Postgres tsvector مع كل شيء. إنه سريع بما يكفي لـ 137K صفوف مع فهرس GIN. استغرقت أوقات الاستعلام 40-80ms في المتوسط.
const { data } = await supabase
.from('listings')
.select('id, slug, title, description, category:categories(name)')
.textSearch('search_vector', query, { type: 'websearch' })
.eq('status', 'active')
.eq('country_code', countryFilter)
.order('rating_avg', { ascending: false })
.range(0, 19)
المرحلة الثانية: عندما لم تكن Postgres كافية
حول 80,000 قائمة، بدأت عمليات البحث المعقدة ذات الأوجه المتعددة (الفئة + الموقع + النص + الترتيب) بالوصول إلى 300-500ms. مقبول لمعظم التطبيقات، لكن المستخدمين متوقعة النتائج الفورية.
أضفنا Typesense كطبقة بحث. ليس Algolia (مكلف جداً بحجمنا — سندفع $500+/شهر). ليس Meilisearch (رائع، لكن بحث الجغرافيا في Typesense كان أفضل لحالة الاستخدام الخاصة بنا).
يعمل Typesense على عينة Hetzner بقيمة $48/شهر. المزامنة من Supabase عبر إعادة فهرسة كاملة كل ليلة + تحديثات webhook في الوقت الفعلي. متوسط استعلامات البحث الآن 8-15ms.
| حل البحث | وقت الاستعلام (p50) | وقت الاستعلام (p99) | التكلفة الشهرية | البحث ذو الأوجه المتعددة |
|---|---|---|---|---|
| Postgres FTS | 45ms | 320ms | $0 (مضمن) | محدود |
| Typesense | 9ms | 28ms | $48 | ممتاز |
| Algolia | ~5ms | ~15ms | $500+ | ممتاز |
| Meilisearch | ~8ms | ~22ms | $48 (مستضاف ذاتياً) | جيد |
ميزانيات الأداء والتخزين المؤقت للحافة
وضعنا أهدافاً أداء عدوانية من اليوم الأول:
- TTFB: < 200ms (العالمي p75)
- LCP: < 1.5s
- CLS: < 0.05
- إجمالي وزن الصفحة: < 300KB (التحميل الأولي)
شبكة Vercel Edge
يتم تخزين صفحات ISR مؤقتاً في شبكة Vercel edge — 100+ نقاط حضور عالمياً. بمجرد توليد الصفحة وتخزينها مؤقتاً، تخدم من أقرب موقع حافة. هذا هو السبب في بقاء TTFB تحت 200ms حتى للمستخدمين في جنوب شرق آسيا أو أمريكا الجنوبية.
تحسين الصور
لكل قائمة 1-8 صور. هذا يعني احتمالياً أكثر من مليون صورة. نستخدم تحسين الصور المدمج في Vercel مع next/image:
<Image
src={listing.media[0]?.url}
alt={listing.title}
width={800}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
loading={index === 0 ? 'eager' : 'lazy'}
quality={75}
/>
يتم تخزين الصور في Supabase Storage وتقديمها من خلال CDN الصور Vercel. الصور الأصلية غالباً 2-5MB؛ بعد التحسين، تصبح 40-120KB. وفر هذا وحده ما يقرب من 80% على النطاق الترددي.
المراقبة والقابلية للرصد في الإنتاج
تشغيل 137K صفحة في الإنتاج بدون مراقبة يشبه القيادة معصوب العينين. إليك مجموعتنا:
- Vercel Analytics: Core Web Vitals، مراقبة المستخدم الحقيقي
- Sentry: تتبع الأخطاء (نتقاط ~50 خطأ/يوم، معظمها من روبوتات تُرسل بيانات غير صحيحة)
- لوحة Supabase: أداء قاعدة البيانات، تحليل الاستعلام
- Checkly: مراقبة اصطناعية، فترات 5 دقائق في المسارات الحرجة
- Google Search Console: تغطية الفهرس، إحصائيات الزحف
المراقبة الأكثر قيمة التي أعددناها كانت استعلام Supabase يومي يحسب الصفحات المفهرسة مقابل إجمالي القوائم النشطة. إذا انخفضت النسبة أقل من 95%، نحصل على تنبيه. اكتشف هذا انحدار sitemap خلال 24 ساعة من نشر تغيير سيء.
تفصيل التكاليف: تكلفة هذا فعلاً
يسأل الناس دائماً عن التكاليف. إليك الإنفاق الفعلي الشهري اعتباراً من Q1 2025:
| الخدمة | الخطة | التكلفة الشهرية |
|---|---|---|
| Vercel | Pro | $20 |
| Vercel Bandwidth (زائدة) | الدفع حسب الاستخدام | ~$35 |
| Supabase | Pro | $25 |
| Supabase Database (الحوسبة) | عينة صغيرة | $48 |
| Typesense (Hetzner) | CX31 | $48 |
| Checkly | Starter | $7 |
| Sentry | Team | $26 |
| المجال + DNS (Cloudflare) | الطبقة المجانية | $0 |
| الإجمالي | ~$209/شهر |
تقديم 137,000 صفحة مع ملايين مشاهدات الصفحة الشهرية بحوالي $200/شهر. حاول فعل هذا مع إعداد خادم تقليدي يشغل WordPress.
إذا كنت تفكر في مشروع مشابه وتريد فهم كيفية تعيين معمارية مثل هذه إلى ميزانيتك، صفحة التسعير الخاصة بنا تفصل كيف نقيم عادة مشاريع الدلائل والأسواق.
ما كنا سنفعله بشكل مختلف
ابدأ مع ISR من اليوم الأول. أهدرنا أسبوعين محاولين جعل SSG الكامل يعمل قبل قبول أن الرياضيات لم تنجح.
استخدم Typesense من البداية. كان Postgres FTS جيداً في البداية، لكن نقل البحث في منتصف المشروع كان مزعجاً. كانت $48/شهر تستحق الاستثمار من الإطلاق.
استثمر في التحقق من البيانات في وقت مبكر. مع 137K قائمة مستوردة من مصادر مختلفة، كانت جودة البيانات كابوساً. كان يجب أن نبني مخططات Zod أصارم وخطوط أنابيب التحقق قبل الاستيراد الأول، وليس بعد أن وجدنا آلاف السجلات المكسورة في الإنتاج.
اختبر مع أحجام البيانات الواقعية في التدريج. بيئة التدريج الخاصة بنا كانت لديها 500 قائمة. الاستعلامات التي عملت بشكل رائع على 500 صفوف انهارت عند 137K. نقيم الآن التدريج مع عينة عشوائية 20% من بيانات الإنتاج.
إذا كنت تخطط لمشروع دليل أو سوق وتريد تجنب نفس الألغام، تواصل مع فريقنا. لقد مررنا بهذا كثيراً بما يكفي لنعرف أين تكمن الألغام.
الأسئلة الشائعة
كم من الوقت يستغرق بناء دليل بـ 100K+ قائمة مع Next.js؟ بالنسبة لفريقنا، استغرقت المعمارية الأولية والميزات الأساسية حوالي 10 أسابيع. استيراد البيانات والتنظيف والتحقق أضاف 3-4 أسابيع أخرى. الإجمالي من البداية إلى إطلاق الإنتاج كان تقريباً 14 أسبوع. إذا كنت تعمل مع فريق تطوير Next.js فعل هذا من قبل، يمكنك توفير 2-3 أسابيع من هذا.
هل يمكن لـ Supabase التعامل مع 100,000+ صفوف لدليل؟ بالتأكيد. تعمل Supabase على Postgres، الذي يتعامل مع ملايين الصفوف دون كسر العرق. المفتاح هو الفهرسة الصحيحة — بدون فهارس على أكثر الأعمدة استعلاماً، تتدهور الأداء بسرعة. مع الفهارس التي وصفناها أعلاه، استعلاماتنا على 137K صفوف تعود باستمرار في أقل من 50ms للبحث عن سجل واحد.
ما الفرق بين ISR و SSG للمواقع الكبيرة؟ SSG (Static Site Generation) ينشئ كل صفحة عند نشر البرنامج. ISR (Incremental Static Regeneration) ينشئ مجموعة فرعية عند النشر ويولد الباقي عند الطلب. لمواقع بأكثر من ~10,000 صفحة، ISR عملي ضروري — أصبح SSG الكامل ببطء شديد بحيث لا تكون دورات النشر معقولة.
كيف تتعامل مع SEO لـ 137,000 صفحة ديناميكية؟ ثلاثة أشياء مهمة في الغالب: إنشاء sitemap صحيح مقسم عبر ملفات متعددة، بيانات منظمة فريدة (JSON-LD) على كل صفحة قائمة، والتأكد من أن صفحات ISR المولدة تعيد رموز حالة HTTP 200 صحيحة (ليس 404s ناعمة). نولد أيضاً عناوين وأوصاف meta فريدة لكل صفحة باستخدام بيانات القائمة — لا محتوى meta مكرر.
هل ISR من Vercel موثوق للإنتاج على نطاق واسع؟ في تجربتنا، نعم. شغلنا هذا الإعداد لأكثر من 8 أشهر بوقت تشغيل 99.98%. الحوادث الوحيدة كانت خطأ متعمد — نشر سيء كسر webhook إعادة التحقق الخاص بنا، وكان هناك نافذة صيانة واحدة في Supabase تسببت في 15 دقيقة من البحث المتدهور. ذاكرة التخزين المؤقت للحافة من Vercel صلبة جداً.
هل يجب أن أستخدم Algolia أو Typesense لدليل كبير؟ يعتمد على ميزانيتك. Algolia هي معيار الصناعة مع أفضل تجربة مطور، لكنها تصبح مكلفة ماضي 100K سجل — توقع $500-1000+/شهر. توفر Typesense 90% من الوظائف بجزء من التكلفة عند استضافتها ذاتياً. اخترنا Typesense ولم نندم عليها.
كيف تحتفظون بـ 137,000 قائمة محدثة؟ نستخدم مزيجاً من الأساليب: إعادة تحقق عند الطلب تُشغل بـ webhooks من Supabase عند تغيير القوائم الفردية، إعادة تحقق ISR بناءً على الوقت (كل ساعة) كشبكة أمان، ووظيفة دفعية ليلية تتحقق من البيانات المتقادمة وتشغل إعادة تحقق بكميات كبيرة. يمكن لأصحاب القوائم أيضاً طلب تحديث صفحة يدوياً من خلال لوحة التحكم الخاصة بهم.
هل يمكن لهذه المعمارية أن تعمل مع CMS بدون رأس بدلاً من Supabase؟ نعم، لكن مع المقايضات. إعداد CMS بدون رأس مثل Sanity أو Contentful يعمل جيداً للجانب إدارة المحتوى، لكن ستحتاج على الأرجح إلى قاعدة بيانات لـ البحث والاستعلامات المعقدة. بنينا مشاريع دليل حيث يعيش المحتوى التحريري في CMS بدون رأس وتعيش بيانات القائمة في Postgres — إنها نهج هجينة صحيح.