feat(detector): flag Fraunces, Geist, and the new monoculture fonts

Adds Fraunces, Geist (Sans/Mono), Mona Sans, Plus Jakarta Sans,
Space Grotesk, Recoleta, and Instrument Sans to OVERUSED_FONTS.
Brand-domain exceptions for Geist on vercel.com / nextjs.org /
v0.app and Mona Sans on github.com / githubnext.com.

Updates four should-pass fixtures that used the now-flagged faces
as "good examples"; switches to Newsreader + Karla. Adds positive
assertions for Fraunces and Geist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-29 11:31:38 -07:00
co-authored by Claude Opus 4.7
parent 856b90e52b
commit b5c203f38a
7 changed files with 47 additions and 13 deletions
+13 -1
View File
@@ -61,7 +61,13 @@ const BORDER_SAFE_TAGS = new Set(
);
const OVERUSED_FONTS = new Set([
// Older monoculture (still ubiquitous):
'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica',
// Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave):
'fraunces', 'instrument sans',
'geist', 'geist sans', 'geist mono',
'mona sans',
'plus jakarta sans', 'space grotesk', 'recoleta',
]);
// Brand-associated fonts: don't flag these as "overused" on the brand's own domains.
@@ -70,10 +76,16 @@ const GOOGLE_DOMAINS = [
'google.com', 'youtube.com', 'android.com', 'chromium.org',
'chrome.com', 'web.dev', 'gstatic.com', 'firebase.google.com',
];
const VERCEL_DOMAINS = ['vercel.com', 'nextjs.org', 'v0.app'];
const GITHUB_DOMAINS = ['github.com', 'githubnext.com'];
const BRAND_FONT_DOMAINS = {
'roboto': GOOGLE_DOMAINS,
'google sans': GOOGLE_DOMAINS,
'product sans': GOOGLE_DOMAINS,
'geist': VERCEL_DOMAINS,
'geist sans': VERCEL_DOMAINS,
'geist mono': VERCEL_DOMAINS,
'mona sans': GITHUB_DOMAINS,
};
function isBrandFontOnOwnDomain(font) {
@@ -116,7 +128,7 @@ const ANTIPATTERNS = [
category: 'slop',
name: 'Overused font',
description:
'Inter, Roboto, Open Sans, Lato, Montserrat, and Arial are used on millions of sites. Choose a distinctive font that gives your interface personality.',
'Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.',
skillSection: 'Typography',
skillGuideline: 'overused fonts like Inter',
},
+15 -3
View File
@@ -57,7 +57,13 @@ const BORDER_SAFE_TAGS = new Set(
);
const OVERUSED_FONTS = new Set([
// Older monoculture (still ubiquitous):
'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica',
// Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave):
'fraunces', 'instrument sans',
'geist', 'geist sans', 'geist mono',
'mona sans',
'plus jakarta sans', 'space grotesk', 'recoleta',
]);
// Brand-associated fonts: don't flag these as "overused" on the brand's own domains.
@@ -66,10 +72,16 @@ const GOOGLE_DOMAINS = [
'google.com', 'youtube.com', 'android.com', 'chromium.org',
'chrome.com', 'web.dev', 'gstatic.com', 'firebase.google.com',
];
const VERCEL_DOMAINS = ['vercel.com', 'nextjs.org', 'v0.app'];
const GITHUB_DOMAINS = ['github.com', 'githubnext.com'];
const BRAND_FONT_DOMAINS = {
'roboto': GOOGLE_DOMAINS,
'google sans': GOOGLE_DOMAINS,
'product sans': GOOGLE_DOMAINS,
'geist': VERCEL_DOMAINS,
'geist sans': VERCEL_DOMAINS,
'geist mono': VERCEL_DOMAINS,
'mona sans': GITHUB_DOMAINS,
};
function isBrandFontOnOwnDomain(font) {
@@ -112,7 +124,7 @@ const ANTIPATTERNS = [
category: 'slop',
name: 'Overused font',
description:
'Inter, Roboto, Open Sans, Lato, Montserrat, and Arial are used on millions of sites. Choose a distinctive font that gives your interface personality.',
'Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.',
skillSection: 'Typography',
skillGuideline: 'overused fonts like Inter',
},
@@ -2870,10 +2882,10 @@ const REGEX_MATCHERS = [
test: (m, line) => +m[1] >= 3 && hasBorderRadius(line),
fmt: (m) => m[0] },
// --- Overused font ---
{ id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica)\b/gi,
{ id: 'overused-font', regex: /font-family\s*:\s*['"]?(Inter|Roboto|Open Sans|Lato|Montserrat|Arial|Helvetica|Fraunces|Geist Sans|Geist Mono|Geist|Mona Sans|Plus Jakarta Sans|Space Grotesk|Recoleta|Instrument Sans)\b/gi,
test: () => true,
fmt: (m) => m[0] },
{ id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat)\b/gi,
{ id: 'overused-font', regex: /fonts\.googleapis\.com\/css2?\?family=(Inter|Roboto|Open\+Sans|Lato|Montserrat|Fraunces|Plus\+Jakarta\+Sans|Space\+Grotesk|Instrument\+Sans|Mona\+Sans|Geist)\b/gi,
test: () => true,
fmt: (m) => `Google Fonts: ${m[1].replace(/\+/g, ' ')}` },
// --- Pure black background ---
+11 -1
View File
@@ -140,8 +140,18 @@ describe('detectText — overused fonts', () => {
expect(f.some(r => r.antipattern === 'overused-font')).toBe(true);
});
test('detects Fraunces (current AI-default monoculture)', () => {
const f = detectText("h1 { font-family: 'Fraunces', Georgia, serif; }", 'test.css');
expect(f.some(r => r.antipattern === 'overused-font')).toBe(true);
});
test('detects Geist (Vercel-default monoculture)', () => {
const f = detectText("body { font-family: 'Geist', sans-serif; }", 'test.css');
expect(f.some(r => r.antipattern === 'overused-font')).toBe(true);
});
test('does not flag distinctive fonts', () => {
const f = detectText("body { font-family: 'Instrument Sans', sans-serif; }", 'test.css');
const f = detectText("body { font-family: 'Karla', sans-serif; }", 'test.css');
expect(f.filter(r => r.antipattern === 'overused-font')).toHaveLength(0);
});
});
+1 -1
View File
@@ -7,7 +7,7 @@ export const Card = styled.div`
border-radius: 12px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
font-family: 'Geist', system-ui, sans-serif;
font-family: 'Karla', system-ui, sans-serif;
`;
export const Hero = styled.section`
+1 -1
View File
@@ -14,7 +14,7 @@
<style>
.sidebar {
font-family: 'Geist', system-ui, sans-serif;
font-family: 'Karla', system-ui, sans-serif;
background: #fafafa;
}
</style>
+5 -5
View File
@@ -4,23 +4,23 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Typography — Clean Patterns</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600&family=Fraunces:opsz,wght@9..144,400;9..144,700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,400;6..72,700&family=Karla:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Instrument Sans', system-ui, sans-serif;
font-family: 'Karla', system-ui, sans-serif;
background: #f9fafb;
padding: 2rem;
font-size: 16px;
color: #111827;
}
h1 {
font-family: 'Fraunces', Georgia, serif;
font-family: 'Newsreader', Georgia, serif;
font-size: 48px;
font-weight: 700;
margin-bottom: 0.5rem;
}
h2 {
font-family: 'Fraunces', Georgia, serif;
font-family: 'Newsreader', Georgia, serif;
font-size: 32px;
font-weight: 700;
margin: 2rem 0 0.75rem;
@@ -35,7 +35,7 @@
<p>This page uses distinctive fonts, proper pairing, and strong hierarchy.</p>
<h2>Two Font Families</h2>
<p>Fraunces (serif) for headings, Instrument Sans for body. Clear contrast in both structure and personality.</p>
<p>Newsreader (serif) for headings, Karla for body. Clear contrast in both structure and personality, and neither face has been adopted into the current AI-default monoculture.</p>
<h3>Strong Size Hierarchy</h3>
<p>Sizes range from 12px to 48px — a 4:1 ratio with clear visual steps.</p>
+1 -1
View File
@@ -17,7 +17,7 @@ defineProps({
<style scoped>
.card {
font-family: 'Geist', system-ui, sans-serif;
font-family: 'Karla', system-ui, sans-serif;
background: #fff;
}
</style>