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