Download index.html from getreadystack/webfont-license-audit: direct link, hf CLI and curl.
- Browser
- Download file 44.1 kB
-
https://huggingface.co/spaces/getreadystack/webfont-license-audit/resolve/main/index.html
- Command line
-
hf download hf://spaces/getreadystack/webfont-license-audit/index.html
-
curl -L -o index.html https://huggingface.co/spaces/getreadystack/webfont-license-audit/resolve/main/index.html
44.1 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Webfont License Audit — check webfont licensing in your css</title> | |
| <meta name="description" content="This free browser tool audits @font-face CSS for the listed proprietary families, binary formats, renamed OFL families, and remote Google Fonts links."> | |
| <style> | |
| :root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512} | |
| .wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px} | |
| textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box} | |
| .bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer} | |
| input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px} | |
| table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65} | |
| .sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600} | |
| .note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa} | |
| </style> | |
| <script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script> | |
| <!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/webfont-license-audit"></head> | |
| <body> | |
| <div class="wrap"> | |
| <h1>Webfont License Audit — check webfont licensing in your css</h1> | |
| <p class="lede">Audits @font-face CSS for proprietary foundry families, desktop-format binaries, renamed OFL families and remote Google Fonts hotlinks - the questions a webfont licence audit letter asks. Runs entirely in your browser — nothing is uploaded.</p> | |
| <textarea id="src" spellcheck="false">@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"); | |
| @import url("https://use.typekit.net/abc1234.css"); | |
| @import url("https://fonts.bunny.net/css?family=lato:400,700"); | |
| @import url("https://pro.fontawesome.com/releases/v5.15.4/css/all.css"); | |
| /* MyFonts Webfont Build - counter.myfonts.net/cjs/webfont-build-8841.js */ | |
| /* brand terms sheet: http://www.foundry-terms.example/webfont-tiers */ | |
| @font-face { | |
| font-family: "Helvetica Neue"; | |
| font-style: normal; | |
| font-weight: 400; | |
| src: url("../assets/fonts/HelveticaNeue-Roman.otf") format("opentype"); | |
| } | |
| @font-face { | |
| font-family: "Gotham"; | |
| font-style: normal; | |
| font-weight: 500; | |
| src: url("../assets/fonts/Gotham-Medium.woff2") format("woff2"); | |
| } | |
| @font-face { | |
| font-family: "Proxima Nova"; | |
| font-style: normal; | |
| font-weight: 600; | |
| src: url("../assets/fonts/ProximaNova-Semibold.woff2") format("woff2"); | |
| } | |
| @font-face { | |
| font-family: "Open Sans Custom"; | |
| font-style: normal; | |
| font-weight: 400; | |
| src: url(data:font/woff2;base64,d09GMgABAAAAAAZgABAAAAAADEwAAAYNAAEAAAAA) format("woff2"); | |
| } | |
| @font-face { | |
| font-family: "Inter"; | |
| font-style: normal; | |
| font-weight: 400; | |
| src: url("../assets/fonts/Inter-Regular.woff2") format("woff2"); | |
| } | |
| body { | |
| font-family: "Helvetica Neue", "Proxima Nova", Arial, sans-serif; | |
| } | |
| .display { | |
| font-family: "Gotham", "Open Sans Custom", sans-serif; | |
| }</textarea> | |
| <div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div> | |
| <div id="out" aria-live="polite"></div> | |
| <div id="paid"><strong>Free here:</strong> Scans every CSS, SCSS and LESS file in the workspace and lists each webfont licence risk with its rule id, severity and line number.. <strong>In the editor:</strong> Exports the whole finding list as a dated FONT-LICENSE-AUDIT.md plus CSV evidence pack you keep and hand to a foundry auditor or your own legal team. — that part asks for a licence key.</div> | |
| <p class="note">Same engine as the VS Code extension, byte for byte.</p> | |
| </div> | |
| <script>var WFL_RULES = [ | |
| {"id": "proprietary-selfhost", "sev": "error", "kind": "family-list", "msg": "@font-face serves a foundry family that is almost never covered by a desktop purchase - a separate webfont licence is normally required before this file ships"}, | |
| {"id": "desktop-format-served", "sev": "error", "kind": "src-format", "msg": "A .ttf/.otf/.eot binary is served to browsers from @font-face - desktop EULAs commonly licence those formats for installed use only"}, | |
| {"id": "font-data-uri", "sev": "error", "kind": "src-datauri", "msg": "Font binary is base64-embedded in the stylesheet - several foundry EULAs name embedding and re-distribution of the binary as a separate grant"}, | |
| {"id": "ofl-reserved-font-name", "sev": "error", "kind": "rfn", "msg": "An SIL OFL family is shipped under a modified name - the Reserved Font Name clause forbids keeping the original name on a modified or re-hinted build"}, | |
| {"id": "google-fonts-remote", "sev": "error", "kind": "host", "hosts": ["fonts.googleapis.com", "fonts.gstatic.com"], "msg": "Remote Google Fonts request sends the visitor IP address to a third country - the ground that LG Muenchen I decided in case 3 O 17493/20 on 2022-01-20"}, | |
| {"id": "adobe-typekit-remote", "sev": "warn", "kind": "host", "hosts": ["use.typekit.net", "p.typekit.net"], "msg": "Adobe Fonts kit is loaded remotely - that licence is bound to the domains listed in the kit, so a new domain or a self-hosted copy is out of scope"}, | |
| {"id": "myfonts-counter", "sev": "warn", "kind": "host", "hosts": ["counter.myfonts.net", "mfcount"], "msg": "MyFonts pageview counter is present - the webfont licence is metered by pageview, so traffic growth silently moves the tier"}, | |
| {"id": "fontawesome-pro", "sev": "warn", "kind": "host", "hosts": ["pro.fontawesome.com", "kit.fontawesome.com", "fa-pro"], "msg": "Font Awesome Pro asset is referenced - Pro is a per-seat paid plan and its kit token must not sit in a public repository"}, | |
| {"id": "third-party-font-cdn", "sev": "warn", "kind": "host", "hosts": ["fonts.bunny.net", "cdn.jsdelivr.net/npm/@fontsource", "unpkg.com/@fontsource", "cdnfonts.com"], "msg": "Font is pulled from a third-party CDN - the served binary and its licence terms are outside your build and can change without notice"}, | |
| {"id": "no-license-note", "sev": "warn", "kind": "file-note", "msg": "The stylesheet declares @font-face but carries no licence line - an audit letter asks for the licence covering each served family"}, | |
| {"id": "ofl-attribution-missing", "sev": "warn", "kind": "ofl-note", "msg": "An SIL OFL family is served but the OFL notice is not in this file - the licence requires the copyright and permission notice to travel with the font"}, | |
| {"id": "insecure-license-url", "sev": "info", "kind": "http-url", "msg": "Licence or foundry reference uses plain http - the linked terms cannot be shown to have been unmodified in transit"} | |
| ] | |
| ;</script> | |
| <script>/* Webfont License Audit - engine. Same file runs in Node and in the browser. */ | |
| var RULES = (typeof module !== 'undefined' && module.exports) ? require('./rules.json') : window.WFL_RULES; | |
| var PROPRIETARY = [ | |
| 'helvetica neue', 'helvetica now', 'futura', 'gotham', 'avenir', 'avenir next', | |
| 'proxima nova', 'din next', 'brandon grotesque', 'frutiger', 'univers', | |
| 'trade gothic', 'neue haas grotesk', 'akzidenz-grotesk', 'sofia pro', | |
| 'circular std', 'gill sans', 'museo sans', 'founders grotesk', 'tiempos', | |
| 'canela', 'graphik', 'national 2', 'whitney', 'apercu' | |
| ]; | |
| var OFL = [ | |
| 'open sans', 'roboto', 'roboto mono', 'lato', 'montserrat', 'inter', 'noto sans', | |
| 'noto serif', 'source sans 3', 'source sans pro', 'ibm plex sans', 'ibm plex mono', | |
| 'fira sans', 'fira code', 'work sans', 'nunito', 'poppins', 'raleway', | |
| 'merriweather', 'jetbrains mono', 'public sans', 'dm sans', 'space grotesk' | |
| ]; | |
| var STYLE_WORDS = [ | |
| 'italic', 'oblique', 'bold', 'light', 'regular', 'medium', 'semibold', 'semi', | |
| 'black', 'heavy', 'thin', 'extralight', 'extrabold', 'condensed', 'display', | |
| 'mono', 'text', 'sans', 'serif', 'var', 'variable', 'jp', 'kr', 'sc', 'tc' | |
| ]; | |
| var LICENSE_NOTE = /(sil open font license|open font license|ofl-1\.1|@license|licen[cs]e\s*[:=]|licen[cs]ed\s+(to|under|for)|eula|webfont licen[cs]e|copyright\s*\(c\)|foundry agreement)/i; | |
| var OFL_NOTE = /(sil open font license|open font license|ofl-1\.1|ofl\.txt)/i; | |
| function norm(s) { | |
| return String(s || '').replace(/["']/g, '').replace(/\s+/g, ' ').trim().toLowerCase(); | |
| } | |
| function ruleById(id) { | |
| for (var i = 0; i < RULES.length; i++) { if (RULES[i].id === id) return RULES[i]; } | |
| return { id: id, sev: 'warn', msg: id }; | |
| } | |
| function push(out, seen, id, line) { | |
| var key = id + '@' + line; | |
| if (seen[key]) return; | |
| seen[key] = 1; | |
| var r = ruleById(id); | |
| out.push({ check: r.id, sev: r.sev, msg: r.msg, line: line }); | |
| } | |
| /* Collect @font-face blocks with their family line and src lines. */ | |
| function blocks(lines) { | |
| var out = [], cur = null, depth = 0; | |
| for (var i = 0; i < lines.length; i++) { | |
| var raw = lines[i]; | |
| var bare = raw.replace(/\/\*[\s\S]*?\*\//g, ''); | |
| if (!cur && /@font-face/i.test(bare)) { | |
| cur = { start: i + 1, family: '', familyLine: i + 1, src: [] }; | |
| depth = 0; | |
| } | |
| if (!cur) continue; | |
| var fam = bare.match(/font-family\s*:\s*([^;}]+)/i); | |
| if (fam && !cur.family) { cur.family = norm(fam[1]); cur.familyLine = i + 1; } | |
| var srcRe = /url\(\s*([^)]*)\)/gi, m; | |
| while ((m = srcRe.exec(bare)) !== null) { | |
| cur.src.push({ line: i + 1, url: norm(m[1]) }); | |
| } | |
| if (/src\s*:/i.test(bare) && cur.src.length === 0) { cur.srcLine = i + 1; } | |
| for (var c = 0; c < bare.length; c++) { | |
| if (bare[c] === '{') depth++; | |
| else if (bare[c] === '}') { | |
| depth--; | |
| if (depth <= 0) { out.push(cur); cur = null; break; } | |
| } | |
| } | |
| } | |
| if (cur) out.push(cur); | |
| return out; | |
| } | |
| function isOflFamily(family) { | |
| for (var i = 0; i < OFL.length; i++) { | |
| if (family === OFL[i]) return true; | |
| } | |
| return false; | |
| } | |
| /* "Open Sans Custom" -> reserved-font-name problem. "Open Sans Condensed" -> a real style. */ | |
| function renamedOfl(family) { | |
| for (var i = 0; i < OFL.length; i++) { | |
| var base = OFL[i]; | |
| if (family.length > base.length && family.indexOf(base + ' ') === 0) { | |
| var tail = family.slice(base.length + 1).split(' '); | |
| for (var t = 0; t < tail.length; t++) { | |
| if (STYLE_WORDS.indexOf(tail[t]) === -1) return base; | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| function check(text, opts) { | |
| opts = opts || {}; | |
| var src = String(text == null ? '' : text); | |
| var lines = src.split(/\r?\n/); | |
| var out = [], seen = {}; | |
| var hasNote = LICENSE_NOTE.test(src); | |
| var hasOflNote = OFL_NOTE.test(src); | |
| var bs = blocks(lines); | |
| for (var b = 0; b < bs.length; b++) { | |
| var blk = bs[b]; | |
| var fam = blk.family; | |
| var localOnly = blk.src.length === 0; | |
| if (!localOnly && PROPRIETARY.indexOf(fam) !== -1) { | |
| push(out, seen, 'proprietary-selfhost', blk.familyLine); | |
| } | |
| var renamed = renamedOfl(fam); | |
| if (renamed) push(out, seen, 'ofl-reserved-font-name', blk.familyLine); | |
| if (isOflFamily(fam) && !hasOflNote) { | |
| push(out, seen, 'ofl-attribution-missing', blk.familyLine); | |
| } | |
| for (var s = 0; s < blk.src.length; s++) { | |
| var u = blk.src[s].url; | |
| if (/^data:/.test(u)) { push(out, seen, 'font-data-uri', blk.src[s].line); continue; } | |
| var path = u.split('?')[0].split('#')[0]; | |
| if (/\.(ttf|otf|eot)$/.test(path) && !isOflFamily(fam) && !renamed) { | |
| push(out, seen, 'desktop-format-served', blk.src[s].line); | |
| } | |
| } | |
| } | |
| /* One licence line covers the whole stylesheet, so this fires once per file. */ | |
| if (bs.length && !hasNote) push(out, seen, 'no-license-note', bs[0].start); | |
| /* Host rules read the whole file: @import and url() both count. */ | |
| for (var i = 0; i < lines.length; i++) { | |
| var low = lines[i].toLowerCase(); | |
| var n = i + 1; | |
| for (var r = 0; r < RULES.length; r++) { | |
| var rule = RULES[r]; | |
| if (rule.kind !== 'host') continue; | |
| for (var h = 0; h < rule.hosts.length; h++) { | |
| if (low.indexOf(rule.hosts[h]) !== -1) { push(out, seen, rule.id, n); break; } | |
| } | |
| } | |
| if (low.indexOf('http://') !== -1 && /(licen[cs]e|eula|foundry|font|typekit|myfonts|fontshop)/.test(low)) { | |
| push(out, seen, 'insecure-license-url', n); | |
| } | |
| } | |
| out.sort(function (a, b2) { return a.line - b2.line; }); | |
| return { findings: out, fileCount: 1, ruleCount: RULES.length, today: opts.today || '' }; | |
| } | |
| var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length }; | |
| if (typeof module !== 'undefined' && module.exports) { module.exports = API; } | |
| if (typeof window !== 'undefined') { window.WFLENGINE = API; } | |
| </script> | |
| <script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다. | |
| ; | |
| function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); } | |
| function summaryLine(res) { | |
| const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length; | |
| return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.'; | |
| } | |
| function toText(res, label) { | |
| const f = (res && res.findings) || []; | |
| const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)]; | |
| f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); }); | |
| return lines.join('\n'); | |
| } | |
| function toHtml(res) { | |
| const f = (res && res.findings) || []; | |
| if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>'; | |
| const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join(''); | |
| return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>'; | |
| } | |
| var WFLREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml }; | |
| if (typeof module !== 'undefined' && module.exports) module.exports = WFLREPORT; | |
| else window.WFLREPORT = WFLREPORT; | |
| </script> | |
| <script> | |
| (function(){ | |
| var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today'); | |
| today.value=new Date().toISOString().slice(0,10); | |
| function run(){ try { var res=window.WFLENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.WFLREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } } | |
| go.onclick=run; if (src.value.trim()) run(); | |
| })(); | |
| </script> | |
| <section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://www.npmjs.com/package/@readystack/webfont-license-audit" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'webfont-license-audit',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/webfont-license-audit" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'webfont-license-audit',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/webfont-license-audit" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'webfont-license-audit',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">npx @readystack/webfont-license-audit <file> | |
| npx @readystack/webfont-license-audit --mcp | |
| docker run --rm -v "$PWD:/w" getreadystack/webfont-license-audit /w</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/team" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section> | |
| <section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 12 rules Webfont License Audit checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"webfont-license-audit",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section> | |
| <section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">The 12 licence questions a foundry audit letter asks, run against your CSS</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">LG Muenchen I, 3 O 17493/20, 2022-01-20: 100 euro damages to one visitor for a remote Google Fonts call; a solicitor's reply bills by the hour.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_swHA4hMj6MekPR1NJiBDnHzv3WHxuyGOWy04M1oYI2K" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section> | |
| <!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script> | |
| <script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script> | |
| <script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&","<":"<",">":">",'"':"""}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-20">Updated 2026-09-20</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-20","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p>Fourteen findings from twelve rules on a single stylesheet: that is what a front-end developer shipping a site into the EU gets back the first time their CSS is read as a licence document instead of as code. Seven of those fourteen are errors.</p> | |
| <p>Type is the one asset on a web page that is almost always licensed and almost never tracked. Nobody writes a ticket called "confirm the webfont licence". The font arrives in a brand handover, in a theme, or in a pull request from an agency that has since been replaced, and from then on it is simply what the site looks like. The licence, if there ever was one, is a PDF in somebody's mailbox.</p> | |
| <p>Then a letter arrives. Foundries run audits; in Germany a separate wave of warning letters targets one line of CSS. On 2022-01-20 the Landgericht Muenchen I decided case 3 O 17493/20: a website had loaded a font from fonts.googleapis.com, the visitor's IP address went with the request, and the court awarded that visitor 100 euro. One visitor. One ground. One @import line.</p> | |
| <p>Webfont License Audit reads the file the way the letter does. It walks every .css, .scss and .less file in the workspace, parses each @font-face block, and answers four questions with line numbers attached.</p> | |
| <p>Which families does this site serve to a browser? The rule proprietary-selfhost holds 25 foundry names - Helvetica Neue, Gotham, Avenir, Proxima Nova, DIN Next, Frutiger, Graphik, Canela, Apercu among them - and fires when one of them is served from your own server, because a desktop purchase almost never carries a webfont grant.</p> | |
| <p>In which format? desktop-format-served fires on a .ttf, .otf or .eot handed to browsers, since a desktop EULA typically licenses the installed binary, not the one visitors download. font-data-uri fires when the binary is base64-embedded into the stylesheet, which several EULAs treat as a separate grant.</p> | |
| <p>From whose server? google-fonts-remote, adobe-typekit-remote, third-party-font-cdn, myfonts-counter and fontawesome-pro each name a remote host and say what its licence is actually tied to - a domain list, a pageview tier, a per-seat plan.</p> | |
| <p>Under what licence, in writing? no-license-note fires when a stylesheet declares @font-face and carries no licence line anywhere. ofl-attribution-missing fires when one of 23 SIL OFL families is served without the OFL notice travelling beside it. ofl-reserved-font-name fires when an OFL family ships under a modified name, which the Reserved Font Name clause forbids.</p> | |
| <p>The numbers in this article come from the fixtures inside the extension, not from an estimate. _fixtures/clean.css - an Inter and JetBrains Mono stack with its OFL notice present - returns 0 findings. _fixtures/dirty.css returns 14: seven errors, six warnings, one info, and every one of the twelve rules fires on it, so you can read each message before pointing the linter at your own repository.</p> | |
| <p>A language model will recite the SIL Open Font License accurately. It will not tell you that the stylesheet you are shipping tonight has four remote @import lines and five @font-face blocks, and it will not give you the line numbers. Generic CSS linters check syntax and performance; none carry a foundry name list.</p> | |
| <p>The free scan is the whole scan: every file in the workspace, every finding, every line number, all twelve rules, no key. That job finishes on its own. What the full version adds is a different job - turning the finding list into a dated FONT-LICENSE-AUDIT.md and a CSV evidence pack you own and can hand to an auditor, because when the letter arrives what you need is not a linter run, it is a document.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku141962_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Webfont License Audit actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads every .css, .scss and .less file in your workspace, parses each @font-face block, and reports webfont licence risks with a rule id, a severity and a line number. Twelve rules cover the declared font-family, every src url including base64 data URIs, remote font hosts in @import lines, and whether the stylesheet carries a licence line at all.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Front-end developers and agency teams shipping websites into the EU and the UK, especially anyone who inherited a stylesheet whose font stack nobody can name a licence for. It is written for the moment a foundry audit letter or a German Google-Fonts warning letter lands and somebody has to answer it with file names and line numbers.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why will a chatbot or a free checker not do this?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A language model can recite the SIL Open Font License, but it cannot see the four @import lines and the five @font-face blocks in the stylesheet you are shipping tonight, and it will not hand you line numbers. Generic CSS linters check syntax and performance; none of them hold a list of foundry families or the OFL Reserved Font Name clause.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what does the full version add?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free, with no key: the entire scan across the whole workspace, every finding, every line number, all twelve rules. Nothing is hidden or counted down. The full version adds a different job - exporting that finding list as a dated FONT-LICENSE-AUDIT.md plus a CSV evidence pack you own, keep, and hand to an auditor or your legal team.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost if a person did it?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A solicitor reading one stylesheet's font stack and writing the reply to a warning letter bills by the hour. For scale, on 2022-01-20 the Landgericht München I decided case 3 O 17493/20 and awarded a single visitor 100 euro because a site fetched a font from fonts.googleapis.com. That is one visitor, on one ground, from one line of CSS.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Webfont License Audit checks the file you open against 12 rules from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Webfont License Audit actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads every .css, .scss and .less file in your workspace, parses each @font-face block, and reports webfont licence risks with a rule id, a severity and a line number. Twelve rules cover the declared font-family, every src url including base64 data URIs, remote font hosts in @import lines, and whether the stylesheet carries a licence line at all."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "Front-end developers and agency teams shipping websites into the EU and the UK, especially anyone who inherited a stylesheet whose font stack nobody can name a licence for. It is written for the moment a foundry audit letter or a German Google-Fonts warning letter lands and somebody has to answer it with file names and line numbers."}}, {"@type": "Question", "name": "Why will a chatbot or a free checker not do this?", "acceptedAnswer": {"@type": "Answer", "text": "A language model can recite the SIL Open Font License, but it cannot see the four @import lines and the five @font-face blocks in the stylesheet you are shipping tonight, and it will not hand you line numbers. Generic CSS linters check syntax and performance; none of them hold a list of foundry families or the OFL Reserved Font Name clause."}}, {"@type": "Question", "name": "What is free and what does the full version add?", "acceptedAnswer": {"@type": "Answer", "text": "Free, with no key: the entire scan across the whole workspace, every finding, every line number, all twelve rules. Nothing is hidden or counted down. The full version adds a different job - exporting that finding list as a dated FONT-LICENSE-AUDIT.md plus a CSV evidence pack you own, keep, and hand to an auditor or your legal team."}}, {"@type": "Question", "name": "What would this cost if a person did it?", "acceptedAnswer": {"@type": "Answer", "text": "A solicitor reading one stylesheet's font stack and writing the reply to a warning letter bills by the hour. For scale, on 2022-01-20 the Landgericht München I decided case 3 O 17493/20 and awarded a single visitor 100 euro because a site fetched a font from fonts.googleapis.com. That is one visitor, on one ground, from one line of CSS."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. Webfont License Audit checks the file you open against 12 rules from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:141962,slug:"webfont-license-audit",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/huggingface-model-license-matrix"><span class="yj-rel-t">Hugging Face Model Licence Matrix — all 70 model-card `license:` identifiers ×</span><span class="yj-rel-p">$4.99</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/ai-model-license-lint"><span class="yj-rel-t">AI Model License Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/base-image-eol-lint"><span class="yj-rel-t">Base Image EOL Lint - Dockerfile and CI end-of-life check</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/email-footer-law-lint"><span class="yj-rel-t">Email Footer Law Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/license-flip-audit"><span class="yj-rel-t">License Flip Audit - BUSL/SSPL/AGPL in your deps</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/oracle-jdk-license-gate"><span class="yj-rel-t">Oracle JDK License Gate</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body> | |
| </html> | |