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

The discussion reveals several key insights about the current font loading 
mechanism in Superset:

1. **Theme.tsx as the Central Point**: Currently, the recommended approach is 
to import fonts directly in `superset-frontend/src/theme/Theme.tsx`, which 
serves as the canonical location for theme-related imports.

2. **Separation of Concerns**: Font files must be loaded separately from theme 
configuration. The theme JSON only references fonts by name through the 
`fontFamily` token, but doesn't handle the actual loading.

3. **CSP Considerations**: When using external fonts (CDNs), Content Security 
Policy updates are required in `config.py`, adding operational complexity.

## Problem Statement

The current approach has several limitations:

1. **Lack of Standardization**: No standardized mechanism for font loading 
exists, leading to ad-hoc implementations
2. **Build-time Coupling**: Fonts must be imported at build time in Theme.tsx, 
requiring code changes and rebuilds
3. **Limited Flexibility**: No runtime configuration option for fonts without 
modifying source code
4. **CSP Management**: External font usage requires manual CSP configuration

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

## Conclusion

This solution provides a robust, future-proof approach to custom font 
management in Superset that:
- Eliminates the need for build-time modifications
- Provides runtime configuration flexibility
- Maintains performance and security best practices
- Offers a smooth migration path from the current approach

The implementation leverages existing Superset patterns (configuration, API 
endpoints, theme system) while introducing minimal complexity. This approach 
would make Superset more flexible for enterprise deployments where brand 
consistency is important, while maintaining the simplicity that individual 
users expect.

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

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