diff --git a/cli/engine/detect-antipatterns-browser.js b/cli/engine/detect-antipatterns-browser.js index cb830c3dc..400c9294b 100644 --- a/cli/engine/detect-antipatterns-browser.js +++ b/cli/engine/detect-antipatterns-browser.js @@ -139,16 +139,6 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'overused fonts like Inter', }, - { - id: 'single-font', - category: 'slop', - scopes: ['type'], - name: 'Single font without hierarchy', - description: - 'Only one font family is used for the entire page. A single family can work when weight and size contrast carry the hierarchy; otherwise pair a distinctive display font with a refined body font.', - skillSection: 'Typography', - skillGuideline: 'only one font family for the entire page', - }, { id: 'flat-type-hierarchy', category: 'slop', @@ -4712,12 +4702,6 @@ function checkTypography() { if (isBrandFontOnOwnDomain(font)) continue; findings.push({ type: 'overused-font', detail: `Primary font: ${font} (${Math.round(share * 100)}% of text)` }); } - - // Single-font check: only one distinct primary font across all text - if (fontUsage.size === 1) { - const only = [...fontUsage.keys()][0]; - findings.push({ type: 'single-font', detail: `only font used is ${only}` }); - } } const sizes = new Set(); @@ -4979,14 +4963,6 @@ function checkPageTypography(doc, win) { findings.push({ id: 'overused-font', snippet: `Primary font: ${font}` }); } - // Single font - if (fonts.size === 1) { - const els = doc.querySelectorAll('*'); - if (els.length >= 20) { - findings.push({ id: 'single-font', snippet: `only font used is ${[...fonts][0]}` }); - } - } - // Flat type hierarchy const sizes = new Set(); const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, a, li, td, th, label, button, div'); diff --git a/cli/engine/engines/regex/detect-text.mjs b/cli/engine/engines/regex/detect-text.mjs index 03cb0d978..b88ebf0b8 100644 --- a/cli/engine/engines/regex/detect-text.mjs +++ b/cli/engine/engines/regex/detect-text.mjs @@ -241,24 +241,6 @@ const REGEX_MATCHERS = [ ]; const REGEX_ANALYZERS = [ - // Single font - (content, filePath) => { - const fontFamilyRe = /font-family\s*:\s*([^;}]+)/gi; - const fonts = new Set(); - let m; - while ((m = fontFamilyRe.exec(content)) !== null) { - for (const f of m[1].split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase())) { - if (f && !GENERIC_FONTS.has(f)) fonts.add(f); - } - } - for (const f of extractGoogleFontFamilies(content)) fonts.add(f); - if (fonts.size !== 1 || content.split('\n').length < 20) return []; - const name = [...fonts][0]; - const lines = content.split('\n'); - let line = 1; - for (let i = 0; i < lines.length; i++) { if (lines[i].toLowerCase().includes(name)) { line = i + 1; break; } } - return [finding('single-font', filePath, `only font used is ${name}`, line)]; - }, // Flat type hierarchy (content, filePath) => { const sizes = new Set(); @@ -626,10 +608,11 @@ const TEXT_CONTENT_ANALYZER_IDS = [ function runTextContentAnalyzers(content, filePath, options = {}) { const profile = options?.profile; if (!shouldRunPageAnalyzers(content, filePath)) return []; - // The 3 text-content analyzers are at indices 3-5 in REGEX_ANALYZERS. + // The 3 text-content analyzers are at indices 2-4 in REGEX_ANALYZERS + // (single-font's removal on 2026-07-29 shifted every index down one). const findings = []; for (let i = 0; i < TEXT_CONTENT_ANALYZER_IDS.length; i++) { - const analyzer = REGEX_ANALYZERS[3 + i]; + const analyzer = REGEX_ANALYZERS[2 + i]; const ruleId = TEXT_CONTENT_ANALYZER_IDS[i]; findings.push(...profileFindings(profile, { engine: 'regex', @@ -750,7 +733,6 @@ function detectText(content, filePath, options = {}) { // Page-level analyzers only run on full pages if (shouldRunPageAnalyzers(content, filePath)) { const analyzerIds = [ - 'single-font', 'flat-type-hierarchy', 'monotonous-spacing', 'em-dash-overuse', diff --git a/cli/engine/engines/static-html/detect-html.mjs b/cli/engine/engines/static-html/detect-html.mjs index 53aadd058..482ba0cc3 100644 --- a/cli/engine/engines/static-html/detect-html.mjs +++ b/cli/engine/engines/static-html/detect-html.mjs @@ -60,9 +60,6 @@ function checkStaticPageTypography(document, window) { for (const font of overusedFound) { findings.push({ id: 'overused-font', snippet: `Primary font: ${font}` }); } - if (fonts.size === 1 && document.querySelectorAll('*').length >= 20) { - findings.push({ id: 'single-font', snippet: `only font used is ${[...fonts][0]}` }); - } const sizes = new Set(); for (const el of document.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, a, li, td, th, label, button, div')) { const fontSize = parseFloat(window.getComputedStyle(el).fontSize); diff --git a/cli/engine/registry/antipatterns.mjs b/cli/engine/registry/antipatterns.mjs index 38ce07390..003614786 100644 --- a/cli/engine/registry/antipatterns.mjs +++ b/cli/engine/registry/antipatterns.mjs @@ -28,16 +28,6 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'overused fonts like Inter', }, - { - id: 'single-font', - category: 'slop', - scopes: ['type'], - name: 'Single font without hierarchy', - description: - 'Only one font family is used for the entire page. A single family can work when weight and size contrast carry the hierarchy; otherwise pair a distinctive display font with a refined body font.', - skillSection: 'Typography', - skillGuideline: 'only one font family for the entire page', - }, { id: 'flat-type-hierarchy', category: 'slop', diff --git a/cli/engine/rules/checks.mjs b/cli/engine/rules/checks.mjs index 12a89675a..aee24a947 100644 --- a/cli/engine/rules/checks.mjs +++ b/cli/engine/rules/checks.mjs @@ -3911,12 +3911,6 @@ function checkTypography() { if (isBrandFontOnOwnDomain(font)) continue; findings.push({ type: 'overused-font', detail: `Primary font: ${font} (${Math.round(share * 100)}% of text)` }); } - - // Single-font check: only one distinct primary font across all text - if (fontUsage.size === 1) { - const only = [...fontUsage.keys()][0]; - findings.push({ type: 'single-font', detail: `only font used is ${only}` }); - } } const sizes = new Set(); @@ -4178,14 +4172,6 @@ function checkPageTypography(doc, win) { findings.push({ id: 'overused-font', snippet: `Primary font: ${font}` }); } - // Single font - if (fonts.size === 1) { - const els = doc.querySelectorAll('*'); - if (els.length >= 20) { - findings.push({ id: 'single-font', snippet: `only font used is ${[...fonts][0]}` }); - } - } - // Flat type hierarchy const sizes = new Set(); const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, a, li, td, th, label, button, div'); diff --git a/extension/devtools/panel.js b/extension/devtools/panel.js index 57013a646..65eafd1a0 100644 --- a/extension/devtools/panel.js +++ b/extension/devtools/panel.js @@ -228,7 +228,6 @@ const FIX_SKILLS = { 'side-tab': 'distill, polish', 'border-accent-on-rounded':'distill, polish', 'overused-font': 'typeset', - 'single-font': 'typeset', 'flat-type-hierarchy': 'typeset', 'gradient-text': 'typeset, distill', 'ai-color-palette': 'colorize, distill', diff --git a/tests/detect-antipatterns-fixtures.test.mjs b/tests/detect-antipatterns-fixtures.test.mjs index eaf65b7fb..6492b1976 100644 --- a/tests/detect-antipatterns-fixtures.test.mjs +++ b/tests/detect-antipatterns-fixtures.test.mjs @@ -497,10 +497,12 @@ describe('detectHtml — static HTML/CSS fixtures', () => { assert.match(sideTabs[0].snippet, /border-left: 2px solid oklch/); }); - it('typography-should-flag: detects all three issues', async () => { + it('typography-should-flag: detects both issues', async () => { const f = await detectHtml(path.join(FIXTURES, 'typography-should-flag.html')); assert.ok(f.some(r => r.antipattern === 'overused-font')); - assert.ok(f.some(r => r.antipattern === 'single-font')); + // single-font retired 2026-07-29: one family with weight/size contrast is + // a legitimate system, and the rule mostly punished minimal test pages. + assert.ok(!f.some(r => r.antipattern === 'single-font'), 'retired rule single-font should not resurface'); assert.ok(f.some(r => r.antipattern === 'flat-type-hierarchy')); assert.equal( f.some(r => r.antipattern === 'low-contrast'),