GitHub user mistercrunch deleted a comment on the discussion: Recommended 
approach to add custom fonts with the new theming system and frontend structure

I asked Claude Code to analyze this, here's what it came up with

---

# Deep Analysis: Custom Fonts in Apache Superset

## Current State Analysis

### How Font Loading Currently Works

When you import fonts in `Theme.tsx`:
```typescript
// superset-frontend/src/theme/Theme.tsx
import '@fontsource/inter';
```

Webpack bundles these fonts directly into the build - no CSP configuration 
needed! The fonts become part of your static assets.

**The Problem**: This requires a custom build. Users running the standard 
Docker image cannot add custom fonts without rebuilding the entire frontend.

## The Core Challenge

The real issue isn't about CSP or complexity - it's about **build-time vs 
runtime configuration**:

- **Current approach**: Requires modifying source code and rebuilding
- **Needed**: A solution that works with the stock Docker image

## Why This Matters

Many Superset deployments use the official Docker image without custom builds:
1. **Easier maintenance**: Track official releases without managing custom 
builds
2. **Simpler deployments**: Use docker-compose with standard images
3. **Reduced complexity**: No need for build pipelines
4. **Quick updates**: Pull new versions without rebuilding

These users currently have no way to add custom fonts!

## Proposed Solution: Runtime Font Loading for Stock Docker Images

Building on @mistercrunch's suggestion, here's a solution that enables custom 
fonts without rebuilding:

### 1. Configuration Layer

Add font configuration to `superset_config.py`:

```python
# Font configuration
CUSTOM_FONTS = {
    "primary": {
        "name": "Inter",
        "urls": [
            
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap";
        ],
        "fallback": "Helvetica, Arial, sans-serif"
    },
    "monospace": {
        "name": "JetBrains Mono",
        "urls": [
            
"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap";
        ],
        "fallback": "Monaco, Consolas, monospace"
    }
}

# Automatically update CSP for configured font sources
FONT_CSP_SOURCES = []
for font_config in CUSTOM_FONTS.values():
    for url in font_config.get("urls", []):
        domain = urlparse(url).netloc
        if domain:
            FONT_CSP_SOURCES.append(f"https://{domain}";)

# Merge with existing CSP config
TALISMAN_CONFIG = {
    "content_security_policy": {
        "font-src": ["'self'"] + list(set(FONT_CSP_SOURCES)),
        # ... other CSP directives
    }
}
```

### 2. Backend Integration

Create a Flask endpoint to serve font configuration:

```python
# superset/views/fonts.py
from flask import current_app, jsonify
from flask_appbuilder import expose, BaseView

class FontsView(BaseView):
    route_base = "/api/v1/fonts"
    
    @expose("/config", methods=["GET"])
    def get_font_config(self):
        """Get configured custom fonts"""
        return jsonify({
            "fonts": current_app.config.get("CUSTOM_FONTS", {}),
            "enabled": current_app.config.get("ENABLE_CUSTOM_FONTS", True)
        })
```

### 3. Frontend Integration

Create a dynamic font loader component:

```typescript
// superset-frontend/src/components/FontLoader/index.tsx
import { useEffect, useState } from 'react';
import { SupersetClient } from '@superset-ui/core';

interface FontConfig {
  name: string;
  urls: string[];
  fallback: string;
}

interface FontsConfig {
  primary?: FontConfig;
  monospace?: FontConfig;
  [key: string]: FontConfig | undefined;
}

export const FontLoader: React.FC = () => {
  const [fontsLoaded, setFontsLoaded] = useState(false);

  useEffect(() => {
    const loadFonts = async () => {
      try {
        const response = await SupersetClient.get({
          endpoint: '/api/v1/fonts/config',
        });
        
        const { fonts, enabled } = response.json;
        
        if (!enabled || !fonts) {
          setFontsLoaded(true);
          return;
        }

        // Create link elements for each font URL
        const linkPromises = Object.entries(fonts).flatMap(([key, config]) => {
          const fontConfig = config as FontConfig;
          return fontConfig.urls.map(url => {
            return new Promise((resolve, reject) => {
              const link = document.createElement('link');
              link.rel = 'stylesheet';
              link.href = url;
              link.onload = resolve;
              link.onerror = reject;
              document.head.appendChild(link);
            });
          });
        });

        await Promise.all(linkPromises);
        
        // Update CSS variables with font families
        Object.entries(fonts).forEach(([key, config]) => {
          const fontConfig = config as FontConfig;
          const fontFamily = `"${fontConfig.name}", ${fontConfig.fallback}`;
          document.documentElement.style.setProperty(
            `--font-family-${key}`,
            fontFamily
          );
        });

        setFontsLoaded(true);
      } catch (error) {
        console.error('Failed to load custom fonts:', error);
        setFontsLoaded(true); // Continue with defaults
      }
    };

    loadFonts();
  }, []);

  return null;
};
```

### 4. Theme Integration

Update the theme system to use CSS variables:

```typescript
// superset-frontend/src/theme/index.ts
export const theme = {
  typography: {
    families: {
      sansSerif: 'var(--font-family-primary, Inter, Helvetica, Arial, 
sans-serif)',
      monospace: 'var(--font-family-monospace, "JetBrains Mono", Monaco, 
Consolas, monospace)',
    },
  },
  // ... rest of theme
};
```

### 5. Jinja2 Template Alternative

For even faster loading, implement a Jinja2 template approach:

```html
<!-- superset/templates/appbuilder/baselayout.html -->
{% if config.CUSTOM_FONTS %}
  <style id="superset-custom-fonts">
    {% for key, font_config in config.CUSTOM_FONTS.items() %}
      {% for url in font_config.urls %}
        @import url('{{ url }}');
      {% endfor %}
      :root {
        --font-family-{{ key }}: "{{ font_config.name }}", {{ 
font_config.fallback }};
      }
    {% endfor %}
  </style>
{% endif %}
```

## Benefits of This Approach

1. **Zero Build-time Changes**: Fonts can be configured without modifying 
source code or rebuilding
2. **Automatic CSP Management**: CSP policies are automatically updated based 
on font configuration
3. **Progressive Enhancement**: Falls back gracefully if font loading fails
4. **Performance Optimized**: Fonts load in parallel with the application
5. **Developer Friendly**: Simple configuration in superset_config.py
6. **Extensible**: Easy to add new font categories (heading, display, etc.)

## Migration Path

1. **Phase 1**: Implement the configuration layer and API endpoint
2. **Phase 2**: Add the FontLoader component and integrate with existing 
Theme.tsx
3. **Phase 3**: Update documentation and provide migration examples
4. **Phase 4**: Deprecate direct imports in Theme.tsx (with backward 
compatibility)

## Configuration Examples

### Example 1: Using Google Fonts
```python
CUSTOM_FONTS = {
    "primary": {
        "name": "Roboto",
        "urls": 
["https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap";],
        "fallback": "Arial, sans-serif"
    }
}
```

### Example 2: Using Self-hosted Fonts
```python
CUSTOM_FONTS = {
    "primary": {
        "name": "CustomBrand",
        "urls": ["/static/assets/fonts/custom-brand.css"],
        "fallback": "Arial, sans-serif"
    }
}
```

### Example 3: Multiple Font Families
```python
CUSTOM_FONTS = {
    "primary": {
        "name": "Inter",
        "urls": 
["https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap";],
        "fallback": "Arial, sans-serif"
    },
    "heading": {
        "name": "Playfair Display",
        "urls": 
["https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&display=swap";],
        "fallback": "Georgia, serif"
    },
    "monospace": {
        "name": "Fira Code",
        "urls": 
["https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&display=swap";],
        "fallback": "Consolas, monospace"
    }
}
```

## Two Approaches: Build-time vs Runtime

### Approach 1: Build-time (Current Method)
**How it works**: Import fonts in `Theme.tsx`, webpack bundles them into the 
build
```typescript
import '@fontsource/inter';  // Font gets bundled, no CSP needed
```

**Pros**:
- Fonts are bundled and served from same origin
- No CSP configuration required
- Best performance (fonts are part of the build)

**Cons**:
- Requires custom build
- Cannot use with stock Docker image
- Need rebuild for any font change

### Approach 2: Runtime Loading (Proposed)
**How it works**: Load fonts from CDN at runtime via configuration

**Pros**:
- Works with stock Docker image
- Change fonts without rebuilding
- Configure via `superset_config.py`

**Cons**:
- Requires CSP configuration for external fonts
- Slight performance impact (external request)
- Depends on CDN availability

## Who Needs What?

### Use Build-time Approach If:
- You maintain custom builds anyway
- You need maximum performance
- Fonts rarely change
- You want to avoid external dependencies

### Use Runtime Approach If:
- You use the stock Docker image
- You need to change fonts without rebuilding
- You manage multiple deployments with different brands
- You want configuration-based customization

## Conclusion

The current build-time approach works well but excludes a significant user base 
- those using stock Docker images. The proposed runtime loading solution would:

1. **Enable custom fonts for Docker users** - No custom builds required
2. **Simplify font management** - Change fonts via configuration
3. **Support multi-tenant scenarios** - Different fonts per deployment
4. **Maintain backward compatibility** - Existing build-time approach still 
works

The implementation would finally allow all Superset users to customize fonts, 
regardless of whether they maintain custom builds or use the official Docker 
image. This democratizes branding capabilities and makes Superset more 
accessible for organizations that want customization without the overhead of 
maintaining build pipelines.

GitHub link: 
https://github.com/apache/superset/discussions/34397#discussioncomment-13939636

----
This is an automatically sent email for notifications@superset.apache.org.
To unsubscribe, please send an email to: 
notifications-unsubscr...@superset.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscr...@superset.apache.org
For additional commands, e-mail: notifications-h...@superset.apache.org

Reply via email to