multi-site-theming
SKILL.md
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; }
// Inheritance
public Guid? ParentThemeId { get; set; }
public Theme? ParentTheme { get; set; }
// Scope
public Guid? TenantId { get; set; } // Null = global/base
public Guid? SiteId { get; set; } // Null = tenant-wide
// Tokens
public ThemeTokens Tokens { get; set; } = new();
// Assets
public ThemeAssets Assets { get; set; } = new();
// Metadata
public bool IsDefault { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedUtc { get; set; }
public DateTime? ModifiedUtc { get; set; }
}
public enum ThemeType
{
Base, // Foundation theme
Brand, // Tenant/brand level
Site, // Individual site
Variant // Light/dark variants
}
public class ThemeTokens
{
// Colors
public ColorTokens Colors { get; set; } = new();
// Typography
public TypographyTokens Typography { get; set; } = new();
// Spacing
public SpacingTokens Spacing { get; set; } = new();
// Borders & Shadows
public BorderTokens Borders { get; set; } = new();
public ShadowTokens Shadows { get; set; } = new();
// Component-specific overrides
public Dictionary<string, Dictionary<string, string>> Components { get; set; } = new();
}
public class ColorTokens
{
// Brand colors
public string Primary { get; set; } = "#3B82F6";
public string Secondary { get; set; } = "#6366F1";
public string Accent { get; set; } = "#F59E0B";
// Semantic colors
public string Success { get; set; } = "#10B981";
public string Warning { get; set; } = "#F59E0B";
public string Error { get; set; } = "#EF4444";
public string Info { get; set; } = "#3B82F6";
// Surface colors
public string Background { get; set; } = "#FFFFFF";
public string Surface { get; set; } = "#F9FAFB";
public string Border { get; set; } = "#E5E7EB";
// Text colors
public string TextPrimary { get; set; } = "#111827";
public string TextSecondary { get; set; } = "#6B7280";
public string TextMuted { get; set; } = "#9CA3AF";
}
public class ThemeAssets
{
public string? LogoUrl { get; set; }
public string? LogoDarkUrl { get; set; }
public string? FaviconUrl { get; set; }
public string? BackgroundImageUrl { get; set; }
public List<string> FontUrls { get; set; } = new();
public string? CustomCss { get; set; }
}
Theme Resolution
Cascading Theme Service
public class ThemeResolver
{
public async Task<ResolvedTheme> ResolveAsync(ThemeContext context)
{
var themes = new List<Theme>();
// 1. Load base theme
var baseTheme = await _repository.GetBaseThemeAsync();
if (baseTheme != null) themes.Add(baseTheme);
// 2. Load tenant/brand theme
if (context.TenantId.HasValue)
{
var tenantTheme = await _repository.GetTenantThemeAsync(context.TenantId.Value);
if (tenantTheme != null) themes.Add(tenantTheme);
}
// 3. Load site theme
if (context.SiteId.HasValue)
{
var siteTheme = await _repository.GetSiteThemeAsync(context.SiteId.Value);
if (siteTheme != null) themes.Add(siteTheme);
}
// 4. Apply user preferences (dark mode, contrast)
if (context.UserPreferences != null)
{
var variantTheme = await ResolveVariantAsync(themes.Last(), context.UserPreferences);
if (variantTheme != null) themes.Add(variantTheme);
}
// Merge themes in order
return MergeThemes(themes);
}
private ResolvedTheme MergeThemes(IEnumerable<Theme> themes)
{
var resolved = new ResolvedTheme();
foreach (var theme in themes)
{
// Later themes override earlier ones
MergeTokens(resolved.Tokens, theme.Tokens);
MergeAssets(resolved.Assets, theme.Assets);
}
return resolved;
}
}
public class ThemeContext
{
public Guid? TenantId { get; set; }
public Guid? SiteId { get; set; }
public UserPreferences? UserPreferences { get; set; }
}
public class UserPreferences
{
public ColorScheme ColorScheme { get; set; } = ColorScheme.System;
public bool HighContrast { get; set; }
public bool ReducedMotion { get; set; }
}
public enum ColorScheme
{
Light,
Dark,
System
}
CSS Variable Generation
Variable Generator
public class CssVariableGenerator
{
public string GenerateCssVariables(ResolvedTheme theme)
{
var sb = new StringBuilder();
sb.AppendLine(":root {");
// Colors
GenerateColorVariables(sb, theme.Tokens.Colors);
// Typography
GenerateTypographyVariables(sb, theme.Tokens.Typography);
// Spacing
GenerateSpacingVariables(sb, theme.Tokens.Spacing);
// Borders & Shadows
GenerateBorderVariables(sb, theme.Tokens.Borders);
GenerateShadowVariables(sb, theme.Tokens.Shadows);
sb.AppendLine("}");
// Dark mode variant
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("}");
// Manual dark mode class
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($"{indent}--color-primary: {colors.Primary};");
sb.AppendLine($"{indent}--color-secondary: {colors.Secondary};");
sb.AppendLine($"{indent}--color-accent: {colors.Accent};");
sb.AppendLine();
sb.AppendLine($"{indent}/* Semantic Colors */");
sb.AppendLine($"{indent}--color-success: {colors.Success};");
sb.AppendLine($"{indent}--color-warning: {colors.Warning};");
sb.AppendLine($"{indent}--color-error: {colors.Error};");
sb.AppendLine($"{indent}--color-info: {colors.Info};");
sb.AppendLine();
sb.AppendLine($"{indent}/* Surface Colors */");
sb.AppendLine($"{indent}--color-background: {colors.Background};");
sb.AppendLine($"{indent}--color-surface: {colors.Surface};");
sb.AppendLine($"{indent}--color-border: {colors.Border};");
sb.AppendLine();
sb.AppendLine($"{indent}/* Text Colors */");
sb.AppendLine($"{indent}--color-text-primary: {colors.TextPrimary};");
sb.AppendLine($"{indent}--color-text-secondary: {colors.TextSecondary};");
sb.AppendLine($"{indent}--color-text-muted: {colors.TextMuted};");
}
}
Generated CSS Output
:root {
/* Brand Colors */
--color-primary: #3B82F6;
--color-secondary: #6366F1;
--color-accent: #F59E0B;
/* Semantic Colors */
--color-success: #10B981;
--color-warning: #F59E0B;
--color-error: #EF4444;
--color-info: #3B82F6;
/* Surface Colors */
--color-background: #FFFFFF;
--color-surface: #F9FAFB;
--color-border: #E5E7EB;
/* Text Colors */
--color-text-primary: #111827;
--color-text-secondary: #6B7280;
--color-text-muted: #9CA3AF;
/* Typography */
--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 */
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-3: 0.75rem;
--spacing-4: 1rem;
--spacing-6: 1.5rem;
--spacing-8: 2rem;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: #111827;
--color-surface: #1F2937;
--color-border: #374151;
--color-text-primary: #F9FAFB;
--color-text-secondary: #D1D5DB;
--color-text-muted: #9CA3AF;
}
}
[data-theme="dark"] {
--color-background: #111827;
--color-surface: #1F2937;
--color-border: #374151;
--color-text-primary: #F9FAFB;
--color-text-secondary: #D1D5DB;
--color-text-muted: #9CA3AF;
}
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
{
// Identity
public string CompanyName { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
// Visual Identity
public Guid? ThemeId { get; set; }
public string? CustomDomain { get; set; }
// Logos
public string? LogoUrl { get; set; }
public string? LogoDarkUrl { get; set; }
public string? FaviconUrl { get; set; }
public string? AppIconUrl { get; set; }
// Contact
public string? SupportEmail { get; set; }
public string? SupportUrl { get; set; }
// Legal
public string? TermsUrl { get; set; }
public string? PrivacyUrl { get; set; }
// Feature Flags
public bool ShowPoweredBy { get; set; } = true;
public bool CustomEmailTemplates { get; set; }
}
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
// Theme switcher
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());
// Listen for system changes
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 Dictionaryheadless-api-design- Theme API deliverycontent-type-modeling- Theme as content type
Weekly Installs
2
Repository
melodic-software/claude-code-pluginsInstalled on
windsurf1
trae1
opencode1
codex1
claude-code1
antigravity1