feat: add Noto Sans as default admin UI font with multi-script support#600
feat: add Noto Sans as default admin UI font with multi-script support#600
Conversation
🦋 Changeset detectedLatest commit: ca778ba The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | ca778ba | Apr 16 2026, 10:54 AM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
Pull request overview
Adds self-hosted Noto Sans as the default admin UI font via Astro’s Font API, with an EmDash config option to include additional writing systems.
Changes:
- Injects a default
fontsentry (--font-emdash) from the EmDash Astro integration, with optional extra script families viafonts.scriptsand opt-out viafonts: false. - Adds a custom
notoSansfont provider that merges multiple Noto Sans script families under one logical font (viaunicode-rangestacking). - Updates admin UI CSS and docs to reference the new font configuration and CSS variable.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/astro/routes/admin.astro | Adds <Font cssVariable="--font-emdash" /> and uses --font-emdash in the boot loader font stack. |
| packages/core/src/astro/integration/runtime.ts | Introduces EmDashConfig.fonts config API with script support / opt-out. |
| packages/core/src/astro/integration/index.ts | Injects default Noto Sans fonts into Astro config via updateConfig. |
| packages/core/src/astro/integration/font-provider.ts | New custom provider that wraps Google Fonts provider and merges script faces. |
| packages/admin/src/styles.css | Overrides Tailwind --font-sans to use --font-emdash with system/emoji fallbacks. |
| docs/src/content/docs/reference/configuration.mdx | Documents the new fonts config option and script list. |
| .changeset/wet-kings-bake.md | Adds changeset for the new default admin font feature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async init(context) { | ||
| await googleProvider.init?.(context); | ||
| }, | ||
| async resolveFont(resolveFontOptions) { | ||
| const { weights, styles, formats } = resolveFontOptions; | ||
|
|
||
| // Resolve the base Noto Sans (Latin, Cyrillic, Greek, etc.) | ||
| const base = await googleProvider.resolveFont(resolveFontOptions); | ||
| const baseFonts = base?.fonts ?? []; | ||
|
|
||
| if (!options?.scripts?.length) { | ||
| return base; | ||
| } | ||
|
|
||
| // Collect subset names already covered by the base font so we | ||
| // can filter out duplicate faces from extra script families. | ||
| // e.g. Noto Sans Arabic includes latin/latin-ext faces that | ||
| // would otherwise override the base Noto Sans latin faces. | ||
| const baseSubsets = new Set(baseFonts.map((f) => f.meta?.subset).filter(Boolean)); | ||
|
|
||
| // Resolve additional script families | ||
| const extraFonts = await Promise.all( | ||
| options.scripts.map(async (script) => { | ||
| const family = NOTO_SCRIPT_FAMILIES[script]; | ||
| if (!family) { | ||
| // Silently skip subset names that are already covered | ||
| // by the base Noto Sans font (latin, cyrillic, etc.) | ||
| if (ALL_GOOGLE_SUBSETS.includes(script)) { | ||
| return undefined; | ||
| } | ||
| console.warn( | ||
| `[emdash] Unknown Noto Sans script "${script}". ` + | ||
| `Available: ${Object.keys(NOTO_SCRIPT_FAMILIES).join(", ")}`, | ||
| ); |
There was a problem hiding this comment.
This provider logs unknown scripts with console.warn, which bypasses Astro’s integration logger formatting/filtering. Since init(context) is available, consider capturing context.logger (if present) and using logger.warn(...) instead (fall back to console.warn only if no logger is provided).
| familyName: family, | ||
| weights, | ||
| styles, | ||
| // Pass all known subset names so the unifont provider | ||
| // doesn't filter out any faces. Each script family | ||
| // only returns faces for its own subsets anyway. | ||
| subsets: ALL_GOOGLE_SUBSETS, | ||
| formats, | ||
| options: undefined, |
There was a problem hiding this comment.
When resolving extra script families, the call to googleProvider.resolveFont explicitly sets options: undefined, while the base call forwards the full resolveFontOptions (including any provider options Astro passes through). This can lead to base and extra faces being generated with different Google provider options (e.g. display strategy) for the same logical font. Forward resolveFontOptions.options (or spread resolveFontOptions and override only familyName/subsets) so all resolved families use consistent options.
| familyName: family, | |
| weights, | |
| styles, | |
| // Pass all known subset names so the unifont provider | |
| // doesn't filter out any faces. Each script family | |
| // only returns faces for its own subsets anyway. | |
| subsets: ALL_GOOGLE_SUBSETS, | |
| formats, | |
| options: undefined, | |
| ...resolveFontOptions, | |
| familyName: family, | |
| // Pass all known subset names so the unifont provider | |
| // doesn't filter out any faces. Each script family | |
| // only returns faces for its own subsets anyway. | |
| subsets: ALL_GOOGLE_SUBSETS, |
| const emdashFonts = | ||
| fontsConfig === false | ||
| ? [] | ||
| : [ | ||
| { | ||
| provider: notoSans({ | ||
| scripts: fontsConfig?.scripts, | ||
| }), | ||
| name: "Noto Sans", | ||
| cssVariable: "--font-emdash", | ||
| weights: ["100 900" as const], | ||
| styles: ["normal" as const, "italic" as const], | ||
| subsets: [ | ||
| "latin" as const, | ||
| "latin-ext" as const, | ||
| "cyrillic" as const, | ||
| "cyrillic-ext" as const, | ||
| "devanagari" as const, | ||
| "greek" as const, | ||
| "greek-ext" as const, | ||
| "vietnamese" as const, | ||
| ], | ||
| fallbacks: ["ui-sans-serif", "system-ui", "sans-serif"], | ||
| }, | ||
| ]; | ||
|
|
||
| updateConfig({ | ||
| security: securityConfig, | ||
| // fonts is a valid AstroConfig key but may not be in the | ||
| // type definition for the minimum supported Astro version | ||
| ...({ fonts: emdashFonts } as Record<string, unknown>), |
There was a problem hiding this comment.
updateConfig sets the top-level fonts config to emdashFonts (or [] when fonts: false) without incorporating any existing astroConfig.fonts. This risks overriding user/site font configuration (e.g. templates in this repo define fonts: [...] in astro.config.mjs) and fonts: false would unexpectedly wipe those entries. Consider merging instead (append EmDash’s --font-emdash entry to any existing astroConfig.fonts, and when fonts === false avoid touching fonts at all).
| const emdashFonts = | |
| fontsConfig === false | |
| ? [] | |
| : [ | |
| { | |
| provider: notoSans({ | |
| scripts: fontsConfig?.scripts, | |
| }), | |
| name: "Noto Sans", | |
| cssVariable: "--font-emdash", | |
| weights: ["100 900" as const], | |
| styles: ["normal" as const, "italic" as const], | |
| subsets: [ | |
| "latin" as const, | |
| "latin-ext" as const, | |
| "cyrillic" as const, | |
| "cyrillic-ext" as const, | |
| "devanagari" as const, | |
| "greek" as const, | |
| "greek-ext" as const, | |
| "vietnamese" as const, | |
| ], | |
| fallbacks: ["ui-sans-serif", "system-ui", "sans-serif"], | |
| }, | |
| ]; | |
| updateConfig({ | |
| security: securityConfig, | |
| // fonts is a valid AstroConfig key but may not be in the | |
| // type definition for the minimum supported Astro version | |
| ...({ fonts: emdashFonts } as Record<string, unknown>), | |
| const emdashFont = | |
| fontsConfig === false | |
| ? undefined | |
| : { | |
| provider: notoSans({ | |
| scripts: fontsConfig?.scripts, | |
| }), | |
| name: "Noto Sans", | |
| cssVariable: "--font-emdash", | |
| weights: ["100 900" as const], | |
| styles: ["normal" as const, "italic" as const], | |
| subsets: [ | |
| "latin" as const, | |
| "latin-ext" as const, | |
| "cyrillic" as const, | |
| "cyrillic-ext" as const, | |
| "devanagari" as const, | |
| "greek" as const, | |
| "greek-ext" as const, | |
| "vietnamese" as const, | |
| ], | |
| fallbacks: ["ui-sans-serif", "system-ui", "sans-serif"], | |
| }; | |
| const existingFonts = Array.isArray((astroConfig as Record<string, unknown>).fonts) | |
| ? ((astroConfig as Record<string, unknown>).fonts as unknown[]) | |
| : []; | |
| const fontsUpdate = | |
| emdashFont === undefined | |
| ? {} | |
| : ({ | |
| fonts: [...existingFonts, emdashFont], | |
| } as Record<string, unknown>); | |
| updateConfig({ | |
| security: securityConfig, | |
| // fonts is a valid AstroConfig key but may not be in the | |
| // type definition for the minimum supported Astro version | |
| ...fontsUpdate, |
| * Users can add extra script coverage (Arabic, CJK, etc.) by adding | ||
| * their own fonts targeting --font-emdash in astro.config. |
There was a problem hiding this comment.
The comment suggests users can "add extra script coverage ... by adding their own fonts targeting --font-emdash in astro.config", but the integration’s approach (and font-provider.ts header) indicates Astro warns/overwrites when multiple font entries share the same cssVariable. This guidance is likely to mislead users into a non-working setup. Update the comment to point to emdash({ fonts: { scripts: [...] } }), or to disabling EmDash fonts (fonts:false) before configuring their own font entry/CSS variable.
| * Users can add extra script coverage (Arabic, CJK, etc.) by adding | |
| * their own fonts targeting --font-emdash in astro.config. | |
| * To add extra script coverage (Arabic, CJK, etc.), configure EmDash with | |
| * emdash({ fonts: { scripts: [...] } }) rather than adding another font entry | |
| * that targets --font-emdash. If you need to supply your own font entry and | |
| * CSS variable instead, disable EmDash-managed fonts first with fonts: false. |
What does this PR do?
Adds Noto Sans as the default font for the admin UI, loaded via the Astro Font API. Fonts are downloaded from Google at build time and self-hosted, so there are no runtime CDN requests. Users can add additional writing systems (Arabic, CJK, Hebrew, etc.) via a simple config option.
Type of change
How it works
font-provider.ts) wraps the Google provider to resolve multiple Noto Sans script families under a singlefont-familyname. This is needed because Astro's Font API warns and overwrites when different font names share a cssVariable. The custom provider merges all faces so they stack viaunicode-range.updateConfiginastro:config:setup.--font-sansto usevar(--font-emdash, <system fallback>).<Font cssVariable="--font-emdash" />component is added to the admin route head (withoutpreloadto avoid downloading all subsets eagerly).User-facing API
25 scripts available: arabic, armenian, bengali, chinese-simplified, chinese-traditional, chinese-hongkong, devanagari, ethiopic, georgian, gujarati, gurmukhi, hebrew, japanese, kannada, khmer, korean, lao, malayalam, myanmar, oriya, sinhala, tamil, telugu, thai, tibetan.
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runpnpm locale:extracthas been run (if applicable)AI-generated code disclosure
Screenshots / test output
All 2375 core tests, 549 admin tests, and all other package tests pass.