| name | multi-site-theming |
| description | Use when implementing per-site themes, white-labeling, or brand override systems. Covers tenant-specific branding, theme inheritance, CSS variable hierarchies, and dynamic theme switching for multi-site CMS architectures. |
| allowed-tools | Read, Glob, Grep, Task, Skill |
Multi-Site Theming
Guidance for implementing per-site themes, white-labeling, and brand customization in multi-site CMS architectures.
When to Use This Skill
- Implementing per-tenant or per-site branding
- Designing theme inheritance hierarchies
- Building white-label customization systems
- Configuring dynamic theme switching
- Managing brand overrides at runtime
Theme Architecture
Theme Hierarchy
┌─────────────────────────────────────────────────────────────────┐
│ THEME HIERARCHY │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ BASE THEME │ │
│ │ (Default colors, typography, spacing, components) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ BRAND THEME │ │
│ │ (Corporate colors, fonts, logo, brand identity) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SITE THEME │ │
│ │ (Site-specific overrides, micro-branding) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ USER PREFERENCES │ │
│ │ (Dark/light mode, accessibility, contrast) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Theme Model
Core Theme Entity
public class Theme
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;
public ThemeType Type { get; set; }
public Guid? ParentThemeId { get; set; }
public Theme? ParentTheme { get; set; }
public Guid? TenantId { get; set; }
public Guid? SiteId { get; set; }
public ThemeTokens Tokens { get; set; } = new();
public ThemeAssets Assets { get; set; } = new();
public bool IsDefault { get; set; }
public bool IsActive { ; ; }
DateTime CreatedUtc { ; ; }
DateTime? ModifiedUtc { ; ; }
}
ThemeType
{
Base,
Brand,
Site,
Variant
}
{
ColorTokens Colors { ; ; } = ();
TypographyTokens Typography { ; ; } = ();
SpacingTokens Spacing { ; ; } = ();
BorderTokens Borders { ; ; } = ();
ShadowTokens Shadows { ; ; } = ();
Dictionary<, Dictionary<, >> Components { ; ; } = ();
}
{
Primary { ; ; } = ;
Secondary { ; ; } = ;
Accent { ; ; } = ;
Success { ; ; } = ;
Warning { ; ; } = ;
Error { ; ; } = ;
Info { ; ; } = ;
Background { ; ; } = ;
Surface { ; ; } = ;
Border { ; ; } = ;
TextPrimary { ; ; } = ;
TextSecondary { ; ; } = ;
TextMuted { ; ; } = ;
}
{
? LogoUrl { ; ; }
? LogoDarkUrl { ; ; }
? FaviconUrl { ; ; }
? BackgroundImageUrl { ; ; }
List<> FontUrls { ; ; } = ();
? CustomCss { ; ; }
}
Theme Resolution
Cascading Theme Service
public class ThemeResolver
{
public async Task<ResolvedTheme> ResolveAsync(ThemeContext context)
{
var themes = new List<Theme>();
var baseTheme = await _repository.GetBaseThemeAsync();
if (baseTheme != null) themes.Add(baseTheme);
if (context.TenantId.HasValue)
{
var tenantTheme = await _repository.GetTenantThemeAsync(context.TenantId.Value);
if (tenantTheme != null) themes.Add(tenantTheme);
}
if (context.SiteId.HasValue)
{
var siteTheme = await _repository.GetSiteThemeAsync(context.SiteId.Value);
if (siteTheme != null) themes.Add(siteTheme);
}
if (context.UserPreferences != null)
{
var variantTheme = await ResolveVariantAsync(themes.Last(), context.UserPreferences);
if (variantTheme != null) themes.Add(variantTheme);
}
return MergeThemes(themes);
}
private ResolvedTheme MergeThemes(IEnumerable<Theme> themes)
{
var resolved = ResolvedTheme();
( theme themes)
{
MergeTokens(resolved.Tokens, theme.Tokens);
MergeAssets(resolved.Assets, theme.Assets);
}
resolved;
}
}
{
Guid? TenantId { ; ; }
Guid? SiteId { ; ; }
UserPreferences? UserPreferences { ; ; }
}
{
ColorScheme ColorScheme { ; ; } = ColorScheme.System;
HighContrast { ; ; }
ReducedMotion { ; ; }
}
ColorScheme
{
Light,
Dark,
System
}
CSS Variable Generation
Variable Generator
public class CssVariableGenerator
{
public string GenerateCssVariables(ResolvedTheme theme)
{
var sb = new StringBuilder();
sb.AppendLine(":root {");
GenerateColorVariables(sb, theme.Tokens.Colors);
GenerateTypographyVariables(sb, theme.Tokens.Typography);
GenerateSpacingVariables(sb, theme.Tokens.Spacing);
GenerateBorderVariables(sb, theme.Tokens.Borders);
GenerateShadowVariables(sb, theme.Tokens.Shadows);
sb.AppendLine("}");
if (theme.DarkVariant != null)
{
sb.AppendLine();
sb.AppendLine("@media (prefers-color-scheme: dark) {");
sb.AppendLine(" :root {");
GenerateColorVariables(sb, theme.DarkVariant.Colors, " ");
sb.AppendLine(" }");
sb.AppendLine("}");
sb.AppendLine();
sb.AppendLine("[data-theme=\"dark\"] {");
GenerateColorVariables(sb, theme.DarkVariant.Colors, " ");
sb.AppendLine("}");
}
return sb.ToString();
}
private void GenerateColorVariables(
StringBuilder sb,
ColorTokens colors,
string indent = " ")
{
sb.AppendLine($"{indent}/* Brand Colors */");
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine();
}
}
Generated CSS Output
:root {
--color-primary: #3B82F6;
--color-secondary: #6366F1;
--color-accent: #F59E0B;
--color-success: #10B981;
--color-warning: #F59E0B;
--color-error: #EF4444;
--color-info: #3B82F6;
--color-background: #FFFFFF;
--color-surface: #F9FAFB;
--color-border: #E5E7EB;
--color-text-primary: #111827;
--color-text-secondary: #6B7280;
--color-text-muted: #9CA3AF;
--font-family-base: 'Inter', system-ui, sans-serif;
--font-family-heading: 'Inter', system-ui, sans-serif;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
: ;
: ;
: ;
: ;
}
(: dark) {
{
: ;
: ;
: ;
: ;
: ;
: ;
}
}
{
: ;
: ;
: ;
: ;
: ;
: ;
}
Theme API
REST Endpoints
GET /api/themes # List all themes
GET /api/themes/{id} # Get theme by ID
POST /api/themes # Create theme
PUT /api/themes/{id} # Update theme
DELETE /api/themes/{id} # Delete theme
GET /api/themes/resolve # Resolve current theme (by context)
GET /api/themes/{id}/css # Get generated CSS
GET /api/themes/{id}/variables # Get CSS variables JSON
POST /api/themes/{id}/preview # Preview theme changes
Theme Delivery Endpoint
[Route("api/themes")]
public class ThemeController : ControllerBase
{
[HttpGet("resolve/css")]
[ResponseCache(Duration = 3600, VaryByHeader = "X-Tenant-Id,X-Site-Id")]
public async Task<IActionResult> GetResolvedCss(
[FromHeader(Name = "X-Tenant-Id")] Guid? tenantId,
[FromHeader(Name = "X-Site-Id")] Guid? siteId)
{
var context = new ThemeContext
{
TenantId = tenantId,
SiteId = siteId
};
var theme = await _resolver.ResolveAsync(context);
var css = _generator.GenerateCssVariables(theme);
return Content(css, "text/css");
}
[HttpGet("{id}/variables")]
public async Task<ActionResult<ThemeTokens>> GetVariables(Guid id)
{
var theme = await _repository.GetByIdAsync(id);
if (theme == null) return NotFound();
return Ok(theme.Tokens);
}
}
White-Label Configuration
Tenant Branding Settings
public class TenantBrandingSettings
{
public string CompanyName { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
public Guid? ThemeId { get; set; }
public string? CustomDomain { get; set; }
public string? LogoUrl { get; set; }
public string? LogoDarkUrl { get; set; }
public string? FaviconUrl { get; set; }
public string? AppIconUrl { get; set; }
public string? SupportEmail { get; set; }
public string? SupportUrl { get; set; }
public string? TermsUrl { get; set; }
? PrivacyUrl { ; ; }
ShowPoweredBy { ; ; } = ;
CustomEmailTemplates { ; ; }
}
Frontend Integration
Blazor Theme Provider
@inject IThemeService ThemeService
<CascadingValue Value="@CurrentTheme">
@ChildContent
</CascadingValue>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
private ResolvedTheme? CurrentTheme { get; set; }
protected override async Task OnInitializedAsync()
{
CurrentTheme = await ThemeService.GetCurrentThemeAsync();
}
}
JavaScript Theme Switching
const ThemeSwitcher = {
setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
},
getTheme() {
return localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
},
init() {
this.setTheme(this.getTheme());
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', e => {
if (!localStorage.getItem('theme')) {
this.setTheme(e.matches ? 'dark' : 'light');
}
});
}
};
Related Skills
design-token-management - Token schemas and Style Dictionary
headless-api-design - Theme API delivery
content-type-modeling - Theme as content type