Fix font rendering issue (#1448)

Co-authored-by: Brandon Pereira <brandon-pereira@users.noreply.github.com>
This commit is contained in:
Elizabet Oliveira 2025-12-05 16:50:38 +00:00 committed by GitHub
parent 7c391dfb02
commit 630592dbff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 163 additions and 37 deletions

View file

@ -0,0 +1,29 @@
---
'@hyperdx/app': minor
---
# Font Rendering Fix
Migrate from Google Fonts CDN to Next.js self-hosted fonts for improved reliability and production deployment.
## Changes
- Replaced Google Fonts imports with `next/font/google` for IBM Plex Mono, Roboto Mono, Inter, and Roboto
- Font variables are applied server-side in `_document.tsx` and available globally via CSS class inheritance
- Implemented dynamic font switching with CSS variables (`--app-font-family`) and Mantine theme integration
- Font configuration centralized in `src/config/fonts.ts` with derived maps for CSS variables and Mantine compatibility
- Added Roboto font option alongside existing fonts (IBM Plex Mono, Roboto Mono, Inter)
- CSS variable always has a value (defaults to Inter) even when user preference is undefined
- Removed old Google Fonts CDN links from `_document.tsx`
- `!important` flag used only in CSS for external components (nextra sidebar), not in inline styles
- Fonts are now available globally without external CDN dependency, fixing production deployment issues
## Benefits
- ✅ Self-hosted fonts that work in production even when CDNs are blocked
- ✅ Improved performance with automatic optimization
- ✅ Works with Content Security Policy (CSP) headers
- ✅ Mantine components and sidebar now properly inherit selected fonts
- ✅ Font selection persists through user preferences
- ✅ DRY font configuration with derived maps prevents duplication
- ✅ Server-side font setup eliminates runtime performance overhead

View file

@ -17,6 +17,13 @@ import {
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { IS_LOCAL_MODE } from '@/config';
import {
DEFAULT_FONT_VAR,
DEFAULT_MANTINE_FONT,
FONT_VAR_MAP,
MANTINE_FONT_MAP,
} from '@/config/fonts';
import { ibmPlexMono, inter, roboto, robotoMono } from '@/fonts';
import { ThemeWrapper } from '@/ThemeWrapper';
import { useConfirmModal } from '@/useConfirm';
import { QueryParamProvider as HDXQueryParamProvider } from '@/useQueryParam';
@ -61,6 +68,9 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const confirmModal = useConfirmModal();
const background = useBackground(userPreferences);
const selectedMantineFont =
MANTINE_FONT_MAP[userPreferences.font] || DEFAULT_MANTINE_FONT;
// port to react query ? (needs to wrap with QueryClientProvider)
useEffect(() => {
if (IS_LOCAL_MODE) {
@ -96,10 +106,27 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
}, []);
useEffect(() => {
// TODO: Remove after migration to Mantine
document.body.style.fontFamily = userPreferences.font
? `"${userPreferences.font}", sans-serif`
: '"IBM Plex Mono"';
// Apply font classes to html element for CSS variable resolution.
// Although _document.tsx sets these server-side, they must be re-applied client-side
// during hydration to ensure CSS variables are available for dynamic font switching.
// This is critical for the --app-font-family CSS variable to work across all components.
if (typeof document !== 'undefined') {
const fontClasses = [
ibmPlexMono.variable,
robotoMono.variable,
inter.variable,
roboto.variable,
];
document.documentElement.classList.add(...fontClasses);
}
}, []);
useEffect(() => {
// Update CSS variable for global font cascading
if (typeof document !== 'undefined') {
const fontVar = FONT_VAR_MAP[userPreferences.font] || DEFAULT_FONT_VAR;
document.documentElement.style.setProperty('--app-font-family', fontVar);
}
}, [userPreferences.font]);
const getLayout = Component.getLayout ?? (page => page);
@ -124,7 +151,7 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<QueryParamProvider adapter={NextAdapter}>
<QueryClientProvider client={queryClient}>
<ThemeWrapper
fontFamily={userPreferences.font}
fontFamily={selectedMantineFont}
colorScheme={userPreferences.theme === 'dark' ? 'dark' : 'light'}
>
{getLayout(<Component {...pageProps} />)}

View file

@ -1,8 +1,17 @@
import { Head, Html, Main, NextScript } from 'next/document';
import { ibmPlexMono, inter, roboto, robotoMono } from '@/fonts';
export default function Document() {
const fontClasses = [
ibmPlexMono.variable,
robotoMono.variable,
inter.variable,
roboto.variable,
].join(' ');
return (
<Html lang="en">
<Html lang="en" className={fontClasses}>
<Head>
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
<script src="/__ENV.js" />
@ -12,16 +21,6 @@ export default function Document() {
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
</Head>
<body>
<Main />

View file

@ -604,7 +604,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
onHide={closeInstallInstructions}
/>
<div
className={`${styles.wrapper} inter`}
className={`${styles.wrapper}`}
style={{
position: fixed ? 'fixed' : 'initial',
letterSpacing: '0.05em',

View file

@ -14,15 +14,9 @@ import {
Text,
} from '@mantine/core';
import { OPTIONS_FONTS } from './config/fonts';
import { UserPreferences, useUserPreferences } from './useUserPreferences';
const OPTIONS_FONTS = [
'IBM Plex Mono',
'Roboto Mono',
'Inter',
{ value: 'or use your own font', disabled: true },
];
const OPTIONS_THEMES = [
{ label: 'Dark', value: 'dark' },
{ label: 'Light', value: 'light' },

View file

@ -0,0 +1,53 @@
export type FontConfig = {
variable: string;
fallback: string;
};
export const FONT_CONFIG: Record<string, FontConfig> = {
'IBM Plex Mono': {
variable: 'var(--font-ibm-plex-mono)',
fallback: 'monospace',
},
'Roboto Mono': {
variable: 'var(--font-roboto-mono)',
fallback: 'monospace',
},
Inter: {
variable: 'var(--font-inter)',
fallback: 'sans-serif',
},
Roboto: {
variable: 'var(--font-roboto)',
fallback: 'sans-serif',
},
};
export const DEFAULT_FONT_CONFIG = FONT_CONFIG.Inter;
// Derived maps for convenience
export const FONT_VAR_MAP = Object.entries(FONT_CONFIG).reduce(
(acc, [name, config]) => {
acc[name] = config.variable;
return acc;
},
{} as Record<string, string>,
);
export const MANTINE_FONT_MAP = Object.entries(FONT_CONFIG).reduce(
(acc, [name, config]) => {
acc[name] = `${config.variable}, ${config.fallback}`;
return acc;
},
{} as Record<string, string>,
);
export const DEFAULT_FONT_VAR = DEFAULT_FONT_CONFIG.variable;
export const DEFAULT_MANTINE_FONT = `${DEFAULT_FONT_CONFIG.variable}, ${DEFAULT_FONT_CONFIG.fallback}`;
// UI options for font selection
export const OPTIONS_FONTS = [
'IBM Plex Mono',
'Roboto Mono',
'Inter',
'Roboto',
];

29
packages/app/src/fonts.ts Normal file
View file

@ -0,0 +1,29 @@
import { IBM_Plex_Mono, Inter, Roboto, Roboto_Mono } from 'next/font/google';
export const ibmPlexMono = IBM_Plex_Mono({
weight: ['300', '400', '500', '600', '700'],
subsets: ['latin'],
variable: '--font-ibm-plex-mono',
display: 'swap',
});
export const robotoMono = Roboto_Mono({
weight: ['100', '300', '400', '500', '700'],
subsets: ['latin'],
variable: '--font-roboto-mono',
display: 'swap',
});
export const inter = Inter({
weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
});
export const roboto = Roboto({
weight: ['100', '300', '400', '500', '700'],
subsets: ['latin'],
variable: '--font-roboto',
display: 'swap',
});

View file

@ -7,7 +7,7 @@ export type UserPreferences = {
isUTC: boolean;
timeFormat: '12h' | '24h';
theme: 'light' | 'dark';
font: 'IBM Plex Mono' | 'Inter';
font: 'IBM Plex Mono' | 'Roboto Mono' | 'Inter' | 'Roboto';
backgroundEnabled?: boolean;
backgroundUrl?: string;
backgroundBlur?: number;

View file

@ -8,22 +8,18 @@
@import './_utilities';
@import url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css');
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100..700&display=swap');
.inter {
font-family: Inter, Roboto, 'Helvetica Neue', sans-serif;
font-family: var(--font-inter), 'Helvetica Neue', sans-serif;
}
.roboto {
font-family: 'Roboto', sans-serif;
font-family: var(--font-roboto), sans-serif;
}
.roboto-mono {
font-family: 'Roboto Mono', monospace;
font-family: var(--font-roboto-mono), monospace;
}
.mono {
font-family: 'IBM Plex Mono', monospace;
font-family: var(--font-ibm-plex-mono), monospace;
}
@ -31,10 +27,11 @@
--mantine-webkit-font-smoothing: inherit;
--mantine-moz-font-smoothing: inherit;
color-scheme: dark;
--app-font-family: var(--font-inter);
}
body {
font-family: 'IBM Plex Mono', monospace;
font-family: var(--app-font-family);
font-size: var(--mantine-font-size-sm);
}
@ -200,7 +197,7 @@ body {
// Nextra Overrides
.nextra-toc,
.nextra-sidebar-container {
font-family: 'Roboto', sans-serif;
font-family: var(--font-inter) !important;
}
// custom overrides for jsontree

View file

@ -2,8 +2,6 @@ html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
}
a {