Skip to content

Commit c3c7710

Browse files
committed
add computer modern fonts to the list of possible typography options
1 parent 554903b commit c3c7710

File tree

7 files changed

+117
-18
lines changed

7 files changed

+117
-18
lines changed

docs/changelog/index.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ description: "List of all notable changes and updates to Reactive Resume"
44
rss: true
55
---
66

7+
<Update label="v5.0.9" description="9th February 2026">
8+
- Add Computer Modern web fonts to the font selector, allowing the user to choose from a variety of "Computer Modern" (LaTeX) fonts.
9+
- This lets you create a resume that looks just like a LaTeX document, with the same fonts and styles.
10+
- Update dependencies to the latest versions.
11+
</Update>
12+
713
<Update label="v5.0.8" description="9th February 2026">
814
- Remove Passkey support from the authentication system, as it was causing issues with the authentication provider.
915
- Update dependencies to the latest versions.

docs/spec.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "reactive-resume",
33
"description": "Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.",
4-
"version": "5.0.8",
4+
"version": "5.0.9",
55
"license": "MIT",
66
"type": "module",
77
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8",

scripts/fonts/generate.ts

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
/**
2-
* This script is responsible for generating a JSON file containing the fonts served by Google Fonts.
3-
* The JSON file will be used to populate the typography options in the resume builder.
2+
* This script generates a JSON file containing the fonts served by Google Fonts,
3+
* and also injects the Computer Modern font families as served by https://github.com/bitmaks/cm-web-fonts
4+
* to ensure they appear in the application's typography options.
45
*
5-
* Information about the Google Fonts Developer API can be found here: https://developers.google.com/fonts/docs/developer_api
6+
* See:
7+
* - Google Fonts API: https://developers.google.com/fonts/docs/developer_api
8+
* - Computer Modern Web Fonts: https://github.com/bitmaks/cm-web-fonts
69
*/
710

811
import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -19,6 +22,7 @@ const FONTS_DIR = "./scripts/fonts";
1922
const RESPONSE_FILE = `${FONTS_DIR}/response.json`;
2023
const WEBFONTLIST_FILE = `${FONTS_DIR}/webfontlist.json`;
2124

25+
/** Returns JSON from Google Fonts API or (unless --force) from local cache if it exists */
2226
async function getGoogleFontsJSON() {
2327
let contents: string | null = null;
2428

@@ -44,21 +48,99 @@ async function getGoogleFontsJSON() {
4448
return data;
4549
}
4650

51+
/** Map Google Fonts API variant strings to simple weights */
4752
function variantToWeight(variant: Variant): Weight | null {
4853
if (["100", "200", "300", "500", "600", "700", "800", "900"].includes(variant)) return variant as Weight;
4954
if (variant === "regular") return "400";
5055
return null;
5156
}
5257

58+
/**
59+
* Helper: Get additional Computer Modern webfonts (manually curated from https://github.com/bitmaks/cm-web-fonts)
60+
* These do NOT come from Google Fonts but should appear in webfontlist.json output.
61+
* Files are delivered via jsDelivr CDN.
62+
*/
63+
function getComputerModernWebFonts(): WebFont[] {
64+
const CDN = "https://cdn.jsdelivr.net/gh/bitmaks/cm-web-fonts@latest/font";
65+
66+
return [
67+
{
68+
type: "web",
69+
category: "display",
70+
family: "Computer Modern Bright",
71+
weights: ["400", "700"],
72+
preview: `${CDN}/Bright/cmunbmr.woff`,
73+
files: {
74+
"400": `${CDN}/Bright/cmunbmr.woff`,
75+
"400italic": `${CDN}/Bright/cmunbmo.woff`,
76+
"700": `${CDN}/Bright/cmunbbx.woff`,
77+
"700italic": `${CDN}/Bright/cmunbxo.woff`,
78+
},
79+
},
80+
{
81+
type: "web",
82+
category: "serif",
83+
family: "Computer Modern Concrete",
84+
weights: ["400", "700"],
85+
preview: `${CDN}/Concrete/cmunorm.woff`,
86+
files: {
87+
"400": `${CDN}/Concrete/cmunorm.woff`,
88+
"400italic": `${CDN}/Concrete/cmunobi.woff`,
89+
"700": `${CDN}/Concrete/cmunobx.woff`,
90+
"700italic": `${CDN}/Concrete/cmunoti.woff`,
91+
},
92+
},
93+
{
94+
type: "web",
95+
category: "sans-serif",
96+
family: "Computer Modern Sans",
97+
weights: ["400", "700"],
98+
preview: `${CDN}/Sans/cmunss.woff`,
99+
files: {
100+
"400": `${CDN}/Sans/cmunss.woff`,
101+
"400italic": `${CDN}/Sans/cmunsl.woff`,
102+
"700": `${CDN}/Sans/cmunsx.woff`,
103+
"700italic": `${CDN}/Sans/cmunsi.woff`,
104+
},
105+
},
106+
{
107+
type: "web",
108+
category: "serif",
109+
family: "Computer Modern Serif",
110+
weights: ["400", "700"],
111+
preview: `${CDN}/Serif/cmunrm.woff`,
112+
files: {
113+
"400": `${CDN}/Serif/cmunrm.woff`,
114+
"400italic": `${CDN}/Serif/cmunti.woff`,
115+
"700": `${CDN}/Serif/cmunbx.woff`,
116+
"700italic": `${CDN}/Serif/cmunbi.woff`,
117+
},
118+
},
119+
{
120+
type: "web",
121+
category: "monospace",
122+
family: "Computer Modern Typewriter",
123+
weights: ["400", "700"],
124+
preview: `${CDN}/Typewriter/cmuntt.woff`,
125+
files: {
126+
"400": `${CDN}/Typewriter/cmuntt.woff`,
127+
"400italic": `${CDN}/Typewriter/cmunit.woff`,
128+
"700": `${CDN}/Typewriter/cmuntx.woff`,
129+
"700italic": `${CDN}/Typewriter/cmuntb.woff`,
130+
},
131+
},
132+
];
133+
}
134+
53135
export async function generateFonts() {
54136
const response = await getGoogleFontsJSON();
55-
console.log(`Found ${response.items.length} fonts in total.`);
137+
console.log(`Found ${response.items.length} fonts in total (Google Fonts).`);
56138

57139
const filteredItems = response.items.filter(
58140
(item) => !skippedFamilies.some((family) => item.family.includes(family)),
59141
);
60142

61-
const result: WebFont[] = filteredItems.slice(0, argLimit).map((item) => {
143+
const googleFontResults: WebFont[] = filteredItems.slice(0, argLimit).map((item) => {
62144
// 1. weights: Only non-italic, convert "regular" to "400"
63145
const weights: Weight[] = item.variants.map((v) => variantToWeight(v)).filter((w): w is Weight => !!w);
64146

@@ -82,11 +164,19 @@ export async function generateFonts() {
82164
} satisfies WebFont;
83165
});
84166

85-
const jsonString = argCompress ? JSON.stringify(result) : JSON.stringify(result, null, 2);
167+
// Manually append Computer Modern web fonts
168+
const computerModernFonts = getComputerModernWebFonts();
169+
const allWebFonts: WebFont[] = [...computerModernFonts, ...googleFontResults];
170+
171+
console.log(
172+
`Added ${computerModernFonts.length} Computer Modern Web Fonts. Total output: ${allWebFonts.length} web fonts.`,
173+
);
174+
175+
const jsonString = argCompress ? JSON.stringify(allWebFonts) : JSON.stringify(allWebFonts, null, 2);
86176
await mkdir(FONTS_DIR, { recursive: true });
87177
await writeFile(WEBFONTLIST_FILE, jsonString, "utf-8");
88178

89-
console.log(`Generated ${result.length} fonts in the list.`);
179+
console.log(`Generated ${allWebFonts.length} fonts in the list (including Computer Modern web fonts).`);
90180
}
91181

92182
if (import.meta.main) {

scripts/fonts/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type Item = {
3030
subsets: string[];
3131
variants: Variant[];
3232
colorCapabilities?: string[];
33-
files: Record<Variant, string>;
33+
files: Partial<Record<Variant, string>>;
3434
};
3535

3636
export type APIResponse = {
@@ -47,5 +47,5 @@ export type WebFont = {
4747
family: string;
4848
weights: Weight[];
4949
preview: string;
50-
files: Record<FileWeight, string>;
50+
files: Partial<Record<FileWeight, string>>;
5151
};

src/components/typography/combobox.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ type FontFamilyComboboxProps = Omit<ComboboxProps, "options">;
6565

6666
export function FontFamilyCombobox({ className, ...props }: FontFamilyComboboxProps) {
6767
const options = useMemo(() => {
68-
return [...localFontList, ...webFontList].map((font: LocalFont | WebFont) => ({
68+
return [...webFontList, ...localFontList].map((font: LocalFont | WebFont) => ({
6969
value: font.family,
7070
keywords: [font.family],
7171
label: <FontDisplay name={font.family} type={font.type} url={"preview" in font ? font.preview : undefined} />,
@@ -79,15 +79,18 @@ type FontWeightComboboxProps = Omit<MultipleComboboxProps, "options"> & { fontFa
7979

8080
export function FontWeightCombobox({ fontFamily, ...props }: FontWeightComboboxProps) {
8181
const options = useMemo(() => {
82-
const fontData = webFontMap.get(fontFamily);
82+
const webFontData = webFontMap.get(fontFamily);
83+
const localFontData = localFontList.find((font) => font.family === fontFamily);
8384

8485
let weights: string[] = [];
8586

86-
if (!fontData || !Array.isArray(fontData.weights)) {
87-
// Provide all possible options for local fonts or unknown fontFamily
88-
weights = ["100", "200", "300", "400", "500", "600", "700", "800", "900"];
87+
if (webFontData && Array.isArray(webFontData.weights) && webFontData.weights.length > 0) {
88+
weights = webFontData.weights as string[];
89+
} else if (localFontData && Array.isArray(localFontData.weights) && localFontData.weights.length > 0) {
90+
weights = localFontData.weights as string[];
8991
} else {
90-
weights = fontData.weights as string[];
92+
// Fallback to all possible weights
93+
weights = ["100", "200", "300", "400", "500", "600", "700", "800", "900"];
9194
}
9295

9396
return weights.map((variant: string) => ({

src/components/typography/webfontlist.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)