مقدمة
معظم قوالب المواقع تحل مشكلة واحدة لنوع واحد من الأعمال.
قالب مطعم فاخر يبدو جميلًا لكنه غير مناسب لبيتزا. قالب dining casual يبدو ودودًا لكنه خاطئ للمطاعم الراقية. لذلك يبني المطورون ثلاثة قوالب منفصلة، ويحافظون على ثلاث قواعد كود منفصلة، ويضاعفون عبء العمل ثلاث مرات.
هناك طريقة أفضل.
هذا الدليل يشرح كيف تصمّم قاعدة كود واحدة بـ React وTailwind CSS تنتج عدة أنماط بصرية مختلفة فعليًا — لكل نمط هويته وشخصيته ولغته التصميمية — دون تكرار أي مكوّن.
هذه هي نفس المعمارية وراء Savoura Dywan، قالب موقع مطاعم يقدّم ثلاثة Variants كاملة — luxury وcasual وpizzeria — من قاعدة كود موحّدة. بنهاية هذا الدليل ستفهم ليس فقط كيف تبنيه، بل لماذا اتُّخذ كل قرار معماري.
ما هو قالب Multi-Variant
قالب Multi-Variant هو قاعدة كود واحدة تعرض تجارب بصرية مختلفة بشكل جوهري بناءً على قيمة إعداد واحدة.
ليس مجرد تغيير لون. وليس مجرد تبديل خط. بل تحول حقيقي في الهوية البصرية — ألوان مختلفة، مقاييس Typography مختلفة، مسافات مختلفة، وشخصيات مختلفة للمكوّنات — مع مشاركة نفس البنية والمنطق والمكوّنات الأساسية.
تخيّله كممثل واحد يؤدي ثلاث شخصيات مختلفة تمامًا. الهيكل واحد. الأداء مختلف بالكامل.
هذه المعمارية تخدم جمهورين:
مشترو القوالب — شركات تحتاج موقعًا احترافيًا بسرعة، مهيأً لهويتها، دون تكلفة تطوير مخصص.
المطورون — مهندسون يحتاجون تسليم عدة مشاريع عملاء بسرعة من قاعدة كود واحدة قابلة للصيانة، مع تقليل زمن البناء وزيادة الاتساق.
1. الأساس — الإعداد المركزي
كل شيء في قالب Multi-Variant يبدأ من مصدر حقيقة واحد. قبل كتابة أي مكوّن، عرّف معمارية الإعدادات.
أنشئ src/config/siteConfig.js:
export const VARIANTS = {
LUXURY: 'luxury',
CASUAL: 'casual',
PIZZERIA: 'pizzeria'
};
export const siteConfig = {
// The single value that controls everything
variant: VARIANTS.LUXURY,
branding: {
name: 'Le Jardin',
tagline: 'Fine dining redefined',
logo: '/assets/logo.svg',
favicon: '/favicon.ico'
},
seo: {
title: 'Le Jardin — Fine Dining Restaurant',
description: 'An intimate fine dining experience in the heart of the city',
keywords: 'fine dining, restaurant, gourmet, reservation',
ogImage: '/assets/og-image.jpg',
canonicalUrl: 'https://lejardin.com'
},
contact: {
phone: '+212 6XX XXX XXX',
email: 'contact@lejardin.ma',
address: '12 Rue Mohammed V, Casablanca',
mapUrl: 'https://maps.google.com/?q=...',
whatsapp: '+212 6XX XXX XXX'
},
hours: {
weekdays: '12:00 — 23:00',
weekends: '11:00 — 00:00',
closed: 'Monday'
},
social: {
instagram: 'https://instagram.com/lejardin',
facebook: 'https://facebook.com/lejardin',
tiktok: ''
},
features: {
enableReservations: true,
enableWhatsApp: true,
enablePWA: true,
enableNewsletter: true,
enableDarkMode: true,
enableParticles: false,
enableRTL: false
},
content: {
heroTitle: 'Where Every Meal Becomes a Memory',
heroSubtitle: 'Award-winning cuisine crafted with passion',
aboutText: 'Founded in 2010, Le Jardin has been...',
reservationText: 'Reserve your table for an unforgettable evening'
}
};
هذا الملف هو ما يلمسه المشتري. لا شيء غيره. كل المحتوى، وكل مفاتيح الميزات، وكل بيانات التواصل — في مكان واحد. وباقي القاعدة تقرأ هذه الإعدادات وتعرض بناءً عليها.
2. نظام الثيم حسب الـ Variant
الهوية البصرية لكل Variant تعيش في ملف Theme مخصص. هنا يظهر الفرق الحقيقي.
أنشئ src/config/themes.js:
export const themes = {
luxury: {
// Colors
primary: '#c9a84c',
primaryDark: '#a8873a',
secondary: '#1a1a2e',
background: '#0d0d0d',
surface: '#1a1a1a',
text: '#f5f0e8',
textMuted: '#9a8878',
border: '#2a2a2a',
accent: '#e8d5a3',
// Typography
fontHeading: '"Playfair Display", serif',
fontBody: '"Cormorant Garamond", serif',
fontMono: '"Courier Prime", monospace',
headingWeight: '300',
bodyWeight: '400',
letterSpacing: '0.15em',
// Spacing and sizing
borderRadius: '0px',
buttonRadius: '0px',
cardRadius: '0px',
// Motion personality
transitionSpeed: '0.6s',
transitionEasing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
// Visual personality
shadowStyle: '0 20px 60px rgba(0,0,0,0.5)',
overlayOpacity: '0.7',
heroHeight: '100vh'
},
casual: {
primary: '#e07b39',
primaryDark: '#c4622a',
secondary: '#2d5016',
background: '#faf7f2',
surface: '#ffffff',
text: '#2c2c2c',
textMuted: '#6b6b6b',
border: '#e8e0d5',
accent: '#f4a261',
fontHeading: '"Nunito", sans-serif',
fontBody: '"Open Sans", sans-serif',
fontMono: '"Fira Code", monospace',
headingWeight: '700',
bodyWeight: '400',
letterSpacing: '0.02em',
borderRadius: '12px',
buttonRadius: '50px',
cardRadius: '16px',
transitionSpeed: '0.3s',
transitionEasing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
shadowStyle: '0 4px 20px rgba(0,0,0,0.08)',
overlayOpacity: '0.5',
heroHeight: '90vh'
},
pizzeria: {
primary: '#d62828',
primaryDark: '#a61e1e',
secondary: '#f7b731',
background: '#1a1a1a',
surface: '#242424',
text: '#ffffff',
textMuted: '#aaaaaa',
border: '#333333',
accent: '#f7b731',
fontHeading: '"Oswald", sans-serif',
fontBody: '"Roboto", sans-serif',
fontMono: '"Source Code Pro", monospace',
headingWeight: '700',
bodyWeight: '400',
letterSpacing: '0.05em',
borderRadius: '4px',
buttonRadius: '4px',
cardRadius: '4px',
transitionSpeed: '0.2s',
transitionEasing: 'ease-out',
shadowStyle: '0 2px 10px rgba(0,0,0,0.3)',
overlayOpacity: '0.6',
heroHeight: '85vh'
}
};
لاحظ الفكرة: كل Variant لها شخصية كاملة — ليس فقط الألوان، بل Typography، وقيم border radius، وسرعات الحركة، وeasing. Variant luxury تتحرك ببطء وأناقة، casual بإحساس spring، وpizzeria بسرعة مباشرة.
هذه الفروقات في الشخصية هي ما يجعل كل Variant مميزة فعلًا وليست مجرد إعادة تلوين.
3. حقن الثيم — طبقة الـ Context
الآن اربط الإعداد بالثيم عبر React Context يمكن لكل المكونات الوصول إليه.
أنشئ src/context/ThemeContext.jsx:
import { createContext, useContext, useEffect, useMemo } from 'react';
import { siteConfig } from '../config/siteConfig';
import { themes } from '../config/themes';
const ThemeContext = createContext(null);
export const ThemeProvider = ({ children }) => {
const variant = siteConfig.variant;
const theme = themes[variant];
const config = siteConfig;
useEffect(() => {
const root = document.documentElement;
// Inject all theme values as CSS custom properties
Object.entries(theme).forEach(([key, value]) => {
const cssVar = \`--\${key.replace(/([A-Z])/g, '-$1').toLowerCase()}\`;
root.style.setProperty(cssVar, value);
});
// Set variant class on body for Tailwind variant targeting
document.body.className = document.body.className
.replace(/variant-\\w+/g, '')
.trim();
document.body.classList.add(\`variant-\${variant}\`);
// Apply fonts dynamically
root.style.setProperty('--font-heading', theme.fontHeading);
root.style.setProperty('--font-body', theme.fontBody);
}, [variant, theme]);
const value = useMemo(() => ({
theme,
variant,
config,
isLuxury: variant === 'luxury',
isCasual: variant === 'casual',
isPizzeria: variant === 'pizzeria'
}), [theme, variant, config]);
return (
{children}
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
};
لف التطبيق بهذا الـ Provider في main.jsx:
import { ThemeProvider } from './context/ThemeContext';
ReactDOM.createRoot(document.getElementById('root')).render(
);
الآن كل مكوّن في التطبيق يملك وصولًا مباشرًا للثيم والإعداد عبر Hook واحد نظيف.
4. بناء مكوّنات واعية بالـ Variant
هنا تظهر قوة المعمارية. المكوّنات تتكيّف بصريًا عبر Theme Context دون تمرير variant props يدويًا.
هذا مثال مكوّن Button يتصرف بشكل مختلف عبر الأنماط الثلاثة:
import { useTheme } from '../context/ThemeContext';
import { motion } from 'framer-motion';
export const Button = ({
children,
variant: btnVariant = 'primary',
size = 'md',
onClick,
className = ''
}) => {
const { theme, isLuxury, isCasual, isPizzeria } = useTheme();
const baseStyles = \`
inline-flex items-center justify-center
font-medium tracking-widest
transition-all duration-300
cursor-pointer border-0 outline-none
\`;
const sizeStyles = {
sm: 'px-4 py-2 text-xs',
md: 'px-8 py-3 text-sm',
lg: 'px-12 py-4 text-base'
};
const variantStyles = {
primary: isLuxury
? 'bg-transparent border border-[--primary] text-[--primary] hover:bg-[--primary] hover:text-[--background] uppercase'
: isCasual
? 'bg-[--primary] text-white hover:bg-[--primary-dark] shadow-lg hover:shadow-xl hover:-translate-y-0.5'
: 'bg-[--primary] text-white hover:bg-[--primary-dark] font-bold uppercase',
secondary: isLuxury
? 'bg-[--primary] text-[--background] uppercase'
: isCasual
? 'bg-transparent border-2 border-[--primary] text-[--primary] hover:bg-[--primary] hover:text-white'
: 'bg-[--secondary] text-[--background] font-bold'
};
const motionProps = isLuxury
? { whileHover: { letterSpacing: '0.2em' }, transition: { duration: 0.4 } }
: isCasual
? { whileHover: { scale: 1.05 }, whileTap: { scale: 0.95 } }
: { whileHover: { scale: 1.02 }, whileTap: { scale: 0.98 } };
return (
{children}
);
};
مكوّن واحد. ثلاث شخصيات بصرية وحركية مختلفة بالكامل. بلا prop drilling وبلا شروط متناثرة في كل الملفات.
5. أقسام خاصة بكل Variant
بعض الأقسام تحتاج أكثر من اختلاف styles — تحتاج layouts مختلفة أصلًا. Hero فاخرة تكون سينمائية وملء الشاشة، casual أكثر ودًا، وpizzeria جريئة وسريعة.
تعامل مع هذا عبر section resolver بسيط:
// src/sections/Hero/index.jsx
import { useTheme } from '../../context/ThemeContext';
import { LuxuryHero } from './LuxuryHero';
import { CasualHero } from './CasualHero';
import { PizzeriaHero } from './PizzeriaHero';
export const Hero = () => {
const { variant } = useTheme();
const heroes = {
luxury: LuxuryHero,
casual: CasualHero,
pizzeria: PizzeriaHero
};
const HeroComponent = heroes[variant];
return ;
};
كل Hero Component يأخذ بياناته من الإعداد المشترك لكنه يرسم بمنطقه الخاص للـ layout والحركة والهوية البصرية، دون تلويثه بشروط لباقي الأنماط.
6. إعداد Tailwind لدعم Multi-Variant
وسّع tailwind.config.js ليدعم utilities معتمدة على الثيم:
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
primary: 'var(--primary)',
'primary-dark': 'var(--primary-dark)',
secondary: 'var(--secondary)',
background: 'var(--background)',
surface: 'var(--surface)',
'text-base': 'var(--text)',
'text-muted': 'var(--text-muted)',
border: 'var(--border)',
accent: 'var(--accent)'
},
fontFamily: {
heading: 'var(--font-heading)',
body: 'var(--font-body)'
},
borderRadius: {
variant: 'var(--border-radius)',
button: 'var(--button-radius)',
card: 'var(--card-radius)'
},
transitionDuration: {
variant: 'var(--transition-speed)'
}
}
},
plugins: []
};
بهذا يمكنك استخدام bg-primary وfont-heading وrounded-card في كل المكونات، وستنعكس تلقائيًا قيم الثيم الحالي.
7. SEO ديناميكي لكل Variant
كل Variant تخدم نشاطًا مختلفًا، وكل نشاط يحتاج بيانات SEO خاصة. عالج ذلك في مكوّن SEO مخصص:
// src/components/SEO.jsx
import { useEffect } from 'react';
import { siteConfig } from '../config/siteConfig';
export const SEO = () => {
const { seo, branding } = siteConfig;
useEffect(() => {
document.title = seo.title;
const setMeta = (name, content, isProperty = false) => {
const attr = isProperty ? 'property' : 'name';
let tag = document.querySelector(\`meta[\${attr}="\${name}"]\`);
if (!tag) {
tag = document.createElement('meta');
tag.setAttribute(attr, name);
document.head.appendChild(tag);
}
tag.setAttribute('content', content);
};
setMeta('description', seo.description);
setMeta('keywords', seo.keywords);
setMeta('og:title', seo.title, true);
setMeta('og:description', seo.description, true);
setMeta('og:image', seo.ogImage, true);
setMeta('og:url', seo.canonicalUrl, true);
setMeta('twitter:title', seo.title);
setMeta('twitter:description', seo.description);
setMeta('twitter:image', seo.ogImage);
}, []);
return null;
};
ضعه مرة واحدة في App.jsx. عندما يحدّث المشتري بيانات siteConfig.js، تتحدّث بيانات SEO كاملة تلقائيًا.
8. Demo Mode Switcher
لبيع القالب، تحتاج وسيلة لعرض الأنماط الثلاثة مباشرة دون نشر ثلاثة مواقع منفصلة. ابنِ demo switcher يبدّل الـ variant وقت التشغيل:
// src/components/DemoSwitcher.jsx
import { useState, useEffect } from 'react';
import { VARIANTS } from '../config/siteConfig';
const DEMO_KEY = 'dywan_demo_variant';
export const DemoSwitcher = () => {
const isDemoMode = globalThis._importMeta_.env.VITE_DEMO_MODE === 'true';
const [current, setCurrent] = useState(
() => localStorage.getItem(DEMO_KEY) || VARIANTS.LUXURY
);
useEffect(() => {
if (!isDemoMode) return;
localStorage.setItem(DEMO_KEY, current);
window.location.reload();
}, [current]);
if (!isDemoMode) return null;
return (
{Object.values(VARIANTS).map((v) => (
))}
);
};
ضع VITE_DEMO_MODE=true في ملف .env.demo. رابط الديمو يظهر السويتشر، والإنتاج لا يظهره. المشتري يرى الثلاثة Variants قبل الشراء.
9. بنية المجلدات
بنية واضحة تجعل القالب أسهل للصيانة وللتوسعة من طرف المشتري:
src/
├── config/
│ ├── siteConfig.js ← Buyer touches this only
│ └── themes.js ← Visual personalities
├── context/
│ └── ThemeContext.jsx ← Theme injection
├── components/
│ ├── ui/ ← Shared atomic components
│ │ ├── Button.jsx
│ │ ├── Card.jsx
│ │ └── Badge.jsx
│ ├── layout/ ← Navbar, Footer, Layout
│ └── shared/ ← SEO, PWA, Loaders
├── sections/
│ ├── Hero/
│ │ ├── index.jsx ← Variant resolver
│ │ ├── LuxuryHero.jsx
│ │ ├── CasualHero.jsx
│ │ └── PizzeriaHero.jsx
│ ├── Menu/
│ ├── About/
│ ├── Testimonials/
│ └── Contact/
├── hooks/
│ ├── useTheme.js
│ ├── useDirection.js
│ └── usePWAInstall.js
├── locales/
│ ├── en/translation.json
│ ├── fr/translation.json
│ └── ar/translation.json
└── pages/
├── Home.jsx
├── Menu.jsx
└── Contact.jsx
10. ما الذي يجعل هذا قابلًا للبيع
المعمارية أعلاه قوية هندسيًا. لكن الهندسة وحدها لا تبيع القوالب. هذه القرارات هي التي تبيع:
- ملف إعداد واحد. المشترون ليسوا دائمًا مطورين، وحتى المطورون يريدون البساطة. ملف واحد لتغيير كل شيء هو ميزة تسويقية قبل أن يكون ميزة تقنية.
- ثلاث Variants حقيقية. ليست ثلاثة ألوان. بل ثلاث هويات بصرية مميزة. المشتري يرى نشاطه في واحدة منها مباشرة.
- دعم RTL جاهز. سوق MENA فيه ملايين الشركات التي تحتاج مواقع عربية. قلة من القوالب المنافسة توفر هذا. وهذا فارق يبرر سعرًا أعلى.
- PWA مدمجة. العميل يراها كميزة شبيهة بالتطبيقات الأصلية. قيمة محسوسة بتكلفة تنفيذ منخفضة عند بنائها مبكرًا.
- وضع Demo. المشتري يحتاج أن يرى قبل أن يشتري. ديمو مباشر مع variant switcher يزيل أكبر اعتراض في بيع القوالب: عدم اليقين.
الخاتمة
قالب Multi-Variant ليس مشروعًا أكثر تعقيدًا، بل أكثر وعيًا. ميزانية التعقيد التي تصرفها في المعمارية بالبداية تُستعاد في كل مرة تضيف Variant جديدة، أو تنضم مشتريًا جديدًا، أو توسّع القالب بقسم جديد.
الفكرة الأساسية هي: قاعدة الكود لا تعرف أي نشاط تخدمه. الإعداد هو الذي يعرف. حافظ على هذا الفصل نظيفًا، وسيكبر القالب بلا حدود — Variants جديدة، أسواق جديدة، قطاعات جديدة — دون إعادة البناء من الصفر.
وهذا هو الفرق بين قالب ومحرك قوالب.
Savoura Dywan مبني على هذه المعمارية نفسها — متاح الآن بثلاث Variants كاملة، ودعم RTL متعدد اللغات، وإمكانية PWA، وإعداد كامل من ملف واحد. استكشف Savoura Dywan على Dywan Dev.
