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 Dictionary
  • headless-api-design - Theme API delivery
  • content-type-modeling - Theme as content type
Weekly Installs
2
Installed on
windsurf1
trae1
opencode1
codex1
claude-code1
antigravity1