GitHub user mistercrunch added a comment to 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

The discussion reveals that adding custom fonts to Superset is actually quite 
straightforward:

### The Simple Solution That Works Today

Users can already add custom fonts by:

1. **Adding a CDN import** to `Theme.tsx`:
```typescript
// superset-frontend/src/theme/Theme.tsx
import '@fontsource/inter';
// or
import 
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap';
```

2. **Updating CSP in config.py**:
```python
TALISMAN_CONFIG = {
    "content_security_policy": {
        "font-src": ["'self'", "https://fonts.googleapis.com";, 
"https://fonts.gstatic.com";],
        # ... other directives
    }
}
```

3. **Referencing the font in theme configuration**:
```json
{
  "typography": {
    "fontFamily": "Inter, Helvetica, Arial, sans-serif"
  }
}
```

This approach works well and is relatively simple! So why consider anything 
more complex?

## The Case for Enhancement

While the current approach is functional, there are scenarios where a more 
automated solution could add value:

1. **Multi-tenant Deployments**: Organizations running Superset for multiple 
clients who each want their own fonts
2. **No-code Configuration**: Allowing administrators to change fonts without 
developer involvement
3. **Dynamic Branding**: Switching fonts based on user preferences or 
organizational units
4. **Simplified CSP Management**: Automatically updating CSP based on font 
configuration to prevent security misconfigurations

## Proposed Solution: Dynamic Font Loading via Flask/Jinja2

Building on @mistercrunch's suggestion, here's a comprehensive solution that 
would provide a clean, configurable approach to font loading:

### 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"
    }
}
```

## Recommendation

For most users, the current approach of importing fonts in Theme.tsx and 
updating CSP is perfectly adequate:

### Quick Start Guide for Custom Fonts (Current Approach)
1. Edit `superset-frontend/src/theme/Theme.tsx` and add your font import
2. Update `TALISMAN_CONFIG` in `superset_config.py` to allow the font CDN
3. Reference the font in your theme configuration
4. Rebuild the frontend

This works well for:
- Single deployments with consistent branding
- Organizations comfortable with code changes
- Static font requirements

### When to Consider the Dynamic Approach

The proposed dynamic loading solution makes sense for:
- **SaaS providers** running multi-tenant Superset instances
- **Large enterprises** with multiple brands/divisions
- **Managed service providers** who need to configure fonts without code access
- **Organizations** wanting to change fonts without engineering involvement

## Conclusion

While Superset already supports custom fonts through a straightforward CDN + 
CSP approach, there's value in considering a more dynamic solution for specific 
use cases. The proposed Flask/Jinja2 approach would:

1. **Keep the simple path simple** - The current approach remains available
2. **Enable advanced use cases** - Runtime configuration for multi-tenant 
scenarios
3. **Reduce configuration errors** - Automatic CSP management
4. **Improve developer experience** - No rebuilds needed for font changes

The key is recognizing that both approaches have their place: the current 
solution for simplicity, and the proposed enhancement for flexibility in 
enterprise deployments.

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

----
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