Introduction
La plupart des templates web résolvent un seul problème pour un seul type d’activité.
Un template de restaurant de luxe peut être superbe mais inutilisable pour une pizzeria. Un template casual peut être chaleureux mais inadapté à la haute gastronomie. Résultat : les développeurs créent trois templates séparés, maintiennent trois bases de code séparées, et triplent leur charge de travail.
Il existe une meilleure approche.
Ce guide vous montre comment architecturer une seule base de code React + Tailwind CSS capable de produire plusieurs variantes visuelles distinctes — chacune avec sa propre identité, personnalité et langage design — sans dupliquer un seul composant.
C’est exactement l’architecture derrière Savoura Dywan, un template de site restaurant qui livre trois variantes complètes — luxury, casual et pizzeria — depuis une base de code unifiée. À la fin de ce guide, vous comprendrez non seulement comment le construire, mais aussi pourquoi chaque décision d’architecture a été prise.
Qu’est-ce qu’un template multi-variantes
Un template multi-variantes est une base de code unique qui rend des expériences visuelles réellement différentes selon une valeur de configuration.
Pas seulement un changement de couleur. Pas seulement un changement de police. Un vrai changement d’identité visuelle — palettes, échelles typo, espacements, personnalité des composants — tout en conservant la même structure, logique et les mêmes composants sous-jacents.
Pensez-y comme un seul acteur jouant trois personnages complètement différents. Le squelette reste identique. L’interprétation est totalement différente.
Cette architecture sert deux publics :
Les acheteurs de templates — des entreprises qui ont besoin d’un site professionnel rapidement, aligné à leur identité de marque, sans coût de développement sur mesure.
Les développeurs — des ingénieurs qui doivent livrer plusieurs projets clients rapidement depuis une base de code unique, en réduisant le temps de build et en augmentant la cohérence.
1. La base — configuration centrale
Dans un template multi-variantes, tout part d’une seule source de vérité. Avant d’écrire un seul composant, définissez votre architecture de configuration.
Créez 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'
}
};
Ce fichier est celui que votre acheteur modifie. Rien d’autre. Chaque contenu, chaque toggle de fonctionnalité, chaque contact — tout est centralisé. Le reste du code lit cette config et rend en conséquence.
2. Le système de thème par variante
L’identité visuelle de chaque variante vit dans un fichier thème dédié. C’est ici que la vraie différenciation se produit.
Créez 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'
}
};
Ce qui est important : chaque variante a une vraie personnalité — pas seulement les couleurs, mais aussi la typo, les rayons de bord, les vitesses d’animation et les courbes easing. La variante luxury se déplace lentement et élégamment. La casual rebondit avec une courbe spring. La pizzeria est rapide et directe.
Ces différences de personnalité rendent chaque variante réellement distincte, pas simplement recolorée.
3. Injection du thème — couche Context
Connectez maintenant votre config au système de thème via un contexte React accessible partout.
Créez 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;
};
Entourez votre app avec ce provider dans main.jsx :
import { ThemeProvider } from './context/ThemeContext';
ReactDOM.createRoot(document.getElementById('root')).render(
);
Désormais, chaque composant accède au thème et à la config via un hook unique et propre.
4. Construire des composants conscients des variantes
C’est ici que l’architecture paie. Les composants adaptent leur apparence via le contexte de thème, sans prop drilling de la variante.
Voici un composant Button qui se comporte différemment dans les trois variantes :
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}
);
};
Un seul composant. Trois comportements visuels et animations totalement différents. Pas de prop drilling, pas de switchs de variante dispersés.
5. Sections spécifiques par variante
Certaines sections ont besoin de plus qu’une différence de style : elles ont besoin d’un layout différent. Un hero luxury est cinématique plein écran. Un hero casual est chaleureux. Un hero pizzeria est direct et impactant.
Gérez cela avec un resolver simple :
// 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 ;
};
Chaque composant hero lit ses données depuis la config partagée, mais rend avec sa propre logique de layout, motion et visuel. Cela garde chaque variante lisible et évite d’y mélanger des conditions pour les autres.
6. Configuration Tailwind pour le support multi-variantes
Étendez votre tailwind.config.js pour supporter des utilitaires spécifiques à la variante :
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: []
};
Vous pouvez ensuite utiliser bg-primary, font-heading, rounded-card partout, et ils reflètent automatiquement la variante active.
7. SEO dynamique par variante
Chaque variante sert un business différent. Chaque business a besoin de métadonnées SEO spécifiques. Gérez cela dans un composant dédié :
// 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;
};
Placez-le une seule fois dans App.jsx. Quand l’acheteur met à jour siteConfig.js, tout le SEO suit automatiquement.
8. Le switcher de mode démo
Pour vendre le template, vous devez pouvoir montrer les trois variantes en live sans déployer trois sites séparés. Construisez un switcher démo qui override la variante à l’exécution :
// 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) => (
))}
);
};
Définissez VITE_DEMO_MODE=true dans votre fichier .env.demo. Votre URL démo affiche le switcher, votre déploiement production non. Les acheteurs peuvent tester les trois variantes avant achat.
9. Structure de dossiers
Une structure propre rend le template maintenable pour les acheteurs qui veulent l’étendre :
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. Ce qui rend cela vendable
L’architecture ci-dessus est solide techniquement. Mais l’ingénierie seule ne vend pas des templates. Ces décisions précises, si :
- Un seul fichier de config. Les acheteurs ne sont pas tous développeurs. Même les développeurs-acheteurs veulent de la simplicité. Un fichier pour tout changer est une fonctionnalité marketing.
- Trois variantes réellement différentes. Pas trois jeux de couleurs. Trois identités visuelles distinctes. Les acheteurs se projettent immédiatement dans l’une d’elles.
- RTL prêt à l’emploi. Le marché MENA compte des millions de business ayant besoin de sites en arabe. Presque aucun template concurrent ne l’offre. C’est un différenciateur premium.
- PWA incluse. Les clients y voient une fonctionnalité type application native. Forte valeur perçue pour un coût d’implémentation réduit si prévu tôt.
- Mode démo. Les acheteurs doivent voir avant d’acheter. Une démo live avec switcher de variantes supprime la plus grande objection en vente de templates : l’incertitude.
Conclusion
Un template multi-variantes n’est pas un projet plus complexe. C’est un projet plus réfléchi. Le budget de complexité investi dans l’architecture au départ est amorti à chaque nouvelle variante, à chaque nouvel acheteur, et à chaque extension.
L’idée clé est la suivante : la base de code ne sait pas quel business elle sert. La config, elle, le sait. Gardez cette séparation propre, et le template se scale indéfiniment — nouvelles variantes, nouveaux marchés, nouvelles industries — sans repartir de zéro.
C’est la différence entre un template et un moteur de templates.
Savoura Dywan est construit sur cette architecture exacte — disponible dès maintenant avec trois variantes complètes, support multilingue RTL, capacité PWA, et configuration totale depuis un seul fichier. Explorer Savoura Dywan sur Dywan Dev.
