spark-html-font 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/package.json +40 -0
- package/src/index.d.ts +67 -0
- package/src/index.js +169 -0
- package/src/vite.js +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# ⚡ spark-html-font
|
|
2
|
+
|
|
3
|
+
Font loading optimizer for [spark-html](https://www.npmjs.com/package/spark-html)
|
|
4
|
+
sites — configure every font **once**, get the whole loading story: correct
|
|
5
|
+
`@font-face` + `font-display`, preload links, and a **size-adjusted fallback
|
|
6
|
+
face** so the swap doesn't shift the layout. Zero dependencies.
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
// vite.config.js — bake into every built page
|
|
10
|
+
import font from 'spark-html-font/vite';
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
plugins: [spark(), prerender(), font({
|
|
14
|
+
fonts: [
|
|
15
|
+
{ family: 'Inter', src: '/fonts/inter-var.woff2', weight: '100 900' },
|
|
16
|
+
{ family: 'Fira Code', google: true, weights: [400, 700] },
|
|
17
|
+
],
|
|
18
|
+
})],
|
|
19
|
+
};
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```css
|
|
23
|
+
body { font-family: var(--font-inter); }
|
|
24
|
+
code { font-family: var(--font-fira-code); }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
What lands in `<head>` (before `</head>`, on every built page):
|
|
28
|
+
|
|
29
|
+
- `<link rel="preload" as="font">` per self-hosted file — the fetch starts
|
|
30
|
+
with the HTML;
|
|
31
|
+
- an inline `<style>` with the `@font-face` rules (`font-display: swap` by
|
|
32
|
+
default) **plus** an `"Inter Fallback"` face — `local("Arial")` with
|
|
33
|
+
`size-adjust` / `ascent-override` / `descent-override` — so text set in the
|
|
34
|
+
fallback occupies the same space as the real font: **no layout shift on
|
|
35
|
+
swap**;
|
|
36
|
+
- for Google fonts: `preconnect` to both Google hosts + the `css2`
|
|
37
|
+
stylesheet URL (no build-time network);
|
|
38
|
+
- a `--font-<slug>` CSS var per family with the full stack
|
|
39
|
+
(`"Inter", "Inter Fallback", system-ui, sans-serif`).
|
|
40
|
+
|
|
41
|
+
Built-in approximate fallback metrics ship for popular families (Inter,
|
|
42
|
+
Roboto, Open Sans, Lato, Montserrat, Poppins, Nunito, Source Sans Pro);
|
|
43
|
+
pass `metrics: { sizeAdjust, ascent, descent, lineGap }` for anything else,
|
|
44
|
+
or `adjust: false` to skip the fallback face.
|
|
45
|
+
|
|
46
|
+
## Runtime form
|
|
47
|
+
|
|
48
|
+
No build step? Inject the same tags from main.js:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import { fonts } from 'spark-html-font';
|
|
52
|
+
fonts({ fonts: [{ family: 'Inter', src: '/fonts/inter-var.woff2' }] });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Idempotent; returns a `stop()` that removes the tags.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install -D spark-html-font
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Options
|
|
64
|
+
|
|
65
|
+
| Option (per font) | Meaning |
|
|
66
|
+
|--------|---------|
|
|
67
|
+
| `family` | The font-family name. |
|
|
68
|
+
| `src` | Self-hosted file(s); format sniffed from the extension. |
|
|
69
|
+
| `google: true` | Google-hosted — emits preconnect + css2 stylesheet instead. |
|
|
70
|
+
| `weight` / `weights` | `400`, `"100 900"` (variable), or `[400, 700]` for Google. |
|
|
71
|
+
| `display` | `font-display` strategy, default `swap`. |
|
|
72
|
+
| `metrics` / `adjust` / `adjustFrom` | Fallback-face tuning (see above). |
|
|
73
|
+
| `preload` | Per-font preload toggle; also a top-level `preload` for all. |
|
|
74
|
+
|
|
75
|
+
Top-level: `fallback` — generic families appended to every var stack
|
|
76
|
+
(default `['system-ui', 'sans-serif']`).
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spark-html-font",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Font loading optimizer for spark-html sites — @font-face + preload + size-adjusted fallbacks in one config, no FOUT, no layout shift. Zero dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"homepage": "https://wilkinnovo.github.io/spark",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./vite": {
|
|
15
|
+
"types": "./src/index.d.ts",
|
|
16
|
+
"default": "./src/vite.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node test/font.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/wilkinnovo/spark.git",
|
|
28
|
+
"directory": "packages/spark-html-font"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"spark-html",
|
|
32
|
+
"fonts",
|
|
33
|
+
"font-display",
|
|
34
|
+
"preload",
|
|
35
|
+
"cls",
|
|
36
|
+
"layout-shift",
|
|
37
|
+
"vite-plugin"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT"
|
|
40
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spark-html-font — font loading optimizer. Zero deps.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface FontMetrics {
|
|
6
|
+
/** size-adjust, percent (e.g. 107.4). */
|
|
7
|
+
sizeAdjust: number;
|
|
8
|
+
/** ascent-override, percent. */
|
|
9
|
+
ascent: number;
|
|
10
|
+
/** descent-override, percent. */
|
|
11
|
+
descent: number;
|
|
12
|
+
/** line-gap-override, percent. Default 0. */
|
|
13
|
+
lineGap?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FontFace {
|
|
17
|
+
family: string;
|
|
18
|
+
/** Self-hosted file(s) (woff2/woff/ttf/otf). Omit for Google fonts. */
|
|
19
|
+
src?: string | string[];
|
|
20
|
+
/** Google-hosted: emit preconnect + the css2 stylesheet URL instead of @font-face. */
|
|
21
|
+
google?: boolean;
|
|
22
|
+
/** e.g. 400, "700", or a variable range "100 900". */
|
|
23
|
+
weight?: string | number;
|
|
24
|
+
/** For Google fonts: the weights to request, e.g. [400, 700]. */
|
|
25
|
+
weights?: Array<string | number>;
|
|
26
|
+
style?: string;
|
|
27
|
+
/** font-display strategy. Default "swap". */
|
|
28
|
+
display?: string;
|
|
29
|
+
/** Override the source format sniffed from the file extension. */
|
|
30
|
+
format?: string;
|
|
31
|
+
/** Fallback-face metrics; built-in approximations exist for popular families. */
|
|
32
|
+
metrics?: FontMetrics;
|
|
33
|
+
/** Disable the size-adjusted fallback face for this family. */
|
|
34
|
+
adjust?: boolean;
|
|
35
|
+
/** The local() font the fallback face adjusts. Default "Arial". */
|
|
36
|
+
adjustFrom?: string;
|
|
37
|
+
/** Disable the preload link for this font. */
|
|
38
|
+
preload?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface FontConfig {
|
|
42
|
+
fonts?: FontFace[];
|
|
43
|
+
/** Generic families appended to every --font-<slug> stack. Default ['system-ui', 'sans-serif']. */
|
|
44
|
+
fallback?: string[];
|
|
45
|
+
/** Disable all preload links. Default true (preload on). */
|
|
46
|
+
preload?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The full CSS block: @font-face rules, fallback faces, :root vars. */
|
|
50
|
+
export function fontCss(config?: FontConfig): string;
|
|
51
|
+
|
|
52
|
+
/** The <link> descriptors: preloads, Google preconnects + stylesheet. */
|
|
53
|
+
export function fontLinks(config?: FontConfig): Array<Record<string, string>>;
|
|
54
|
+
|
|
55
|
+
/** Links + style serialized as an HTML block (marked data-spark-font). */
|
|
56
|
+
export function fontHtml(config?: FontConfig): string;
|
|
57
|
+
|
|
58
|
+
/** Runtime: inject the tags into document.head now. Idempotent; returns stop(). */
|
|
59
|
+
export function fonts(config?: FontConfig): () => void;
|
|
60
|
+
|
|
61
|
+
declare const _default: {
|
|
62
|
+
fonts: typeof fonts;
|
|
63
|
+
fontCss: typeof fontCss;
|
|
64
|
+
fontLinks: typeof fontLinks;
|
|
65
|
+
fontHtml: typeof fontHtml;
|
|
66
|
+
};
|
|
67
|
+
export default _default;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spark-html-font — font loading in one place, layout shift in none.
|
|
3
|
+
*
|
|
4
|
+
* Configure every font once; get back the whole loading story:
|
|
5
|
+
*
|
|
6
|
+
* • @font-face declarations with the right `font-display` (swap default)
|
|
7
|
+
* • <link rel="preload"> for self-hosted woff2 (fetch starts with the HTML)
|
|
8
|
+
* • a size-adjusted local FALLBACK face per family (ascent/descent/
|
|
9
|
+
* size-adjust overrides on Arial) so the swap doesn't move the page —
|
|
10
|
+
* built-in approximate metrics for popular families, overridable
|
|
11
|
+
* • Google Fonts: preconnect + the css2 stylesheet URL, no build-time network
|
|
12
|
+
* • a :root CSS var per family — `font-family: var(--font-inter)` — whose
|
|
13
|
+
* stack is real font → fallback face → your generic fallbacks
|
|
14
|
+
*
|
|
15
|
+
* Two ways to apply it:
|
|
16
|
+
*
|
|
17
|
+
* // at runtime (main.js) — injects <style>/<link> into <head>
|
|
18
|
+
* import { fonts } from 'spark-html-font';
|
|
19
|
+
* fonts({ fonts: [{ family: 'Inter', src: '/fonts/inter-var.woff2', weight: '100 900' }] });
|
|
20
|
+
*
|
|
21
|
+
* // at build (vite.config.js) — bakes the same tags into every built page
|
|
22
|
+
* import font from 'spark-html-font/vite';
|
|
23
|
+
* plugins: [spark(), prerender(), font({ fonts: [...] })]
|
|
24
|
+
*
|
|
25
|
+
* Zero dependencies; pure string generation plus a little DOM.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// Approximate Arial-adjusted fallback metrics (fontaine-style) for popular
|
|
29
|
+
// families. Percentages; good enough to keep the swap from moving the page.
|
|
30
|
+
// Override per font with `metrics: { sizeAdjust, ascent, descent, lineGap }`.
|
|
31
|
+
const METRICS = {
|
|
32
|
+
'inter': { sizeAdjust: 107.4, ascent: 90.2, descent: 22.5, lineGap: 0 },
|
|
33
|
+
'roboto': { sizeAdjust: 100.3, ascent: 92.8, descent: 24.4, lineGap: 0 },
|
|
34
|
+
'open sans': { sizeAdjust: 105.4, ascent: 101.3, descent: 27.8, lineGap: 0 },
|
|
35
|
+
'lato': { sizeAdjust: 97.4, ascent: 101.3, descent: 21.9, lineGap: 0 },
|
|
36
|
+
'montserrat': { sizeAdjust: 112.5, ascent: 86.1, descent: 22.3, lineGap: 0 },
|
|
37
|
+
'poppins': { sizeAdjust: 112.2, ascent: 93.8, descent: 31.3, lineGap: 0 },
|
|
38
|
+
'nunito': { sizeAdjust: 101.9, ascent: 99.4, descent: 34.7, lineGap: 0 },
|
|
39
|
+
'source sans pro': { sizeAdjust: 94.1, ascent: 104.6, descent: 29.0, lineGap: 0 },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const slug = (family) => family.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
43
|
+
|
|
44
|
+
function formatOf(src) {
|
|
45
|
+
const ext = String(src).split(/[?#]/)[0].split('.').pop().toLowerCase();
|
|
46
|
+
return { woff2: 'woff2', woff: 'woff', ttf: 'truetype', otf: 'opentype' }[ext] || 'woff2';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// css2 URL for a Google-hosted family: Inter + [400,700] →
|
|
50
|
+
// https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap
|
|
51
|
+
function googleUrl(font) {
|
|
52
|
+
const fam = font.family.replace(/ /g, '+');
|
|
53
|
+
const weights = [].concat(font.weights || font.weight || []).filter((w) => w !== undefined);
|
|
54
|
+
const axis = weights.length ? `:wght@${weights.join(';')}` : '';
|
|
55
|
+
return `https://fonts.googleapis.com/css2?family=${fam}${axis}&display=${font.display || 'swap'}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The full CSS block for a config: @font-face per self-hosted font, a
|
|
60
|
+
* size-adjusted "<Family> Fallback" face per family with known/provided
|
|
61
|
+
* metrics, and one `--font-<slug>` var per family on :root.
|
|
62
|
+
*/
|
|
63
|
+
export function fontCss(config = {}) {
|
|
64
|
+
const list = config.fonts || [];
|
|
65
|
+
const generic = config.fallback || ['system-ui', 'sans-serif'];
|
|
66
|
+
const rules = [];
|
|
67
|
+
const vars = [];
|
|
68
|
+
|
|
69
|
+
for (const font of list) {
|
|
70
|
+
const fam = font.family;
|
|
71
|
+
if (!fam) continue;
|
|
72
|
+
|
|
73
|
+
if (!font.google && font.src) {
|
|
74
|
+
const srcs = [].concat(font.src)
|
|
75
|
+
.map((s) => `url("${s}") format("${font.format || formatOf(s)}")`)
|
|
76
|
+
.join(', ');
|
|
77
|
+
rules.push(
|
|
78
|
+
`@font-face { font-family: "${fam}"; src: ${srcs};` +
|
|
79
|
+
` font-weight: ${font.weight || font.weights?.join(' ') || 400};` +
|
|
80
|
+
` font-style: ${font.style || 'normal'};` +
|
|
81
|
+
` font-display: ${font.display || 'swap'}; }`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The CLS killer: a local()-based stand-in sized like the real font, so
|
|
86
|
+
// text set in the fallback occupies the same space before the swap.
|
|
87
|
+
const m = font.metrics || METRICS[fam.toLowerCase()];
|
|
88
|
+
const stack = [`"${fam}"`];
|
|
89
|
+
if (m && font.adjust !== false) {
|
|
90
|
+
const local = font.adjustFrom || 'Arial';
|
|
91
|
+
rules.push(
|
|
92
|
+
`@font-face { font-family: "${fam} Fallback"; src: local("${local}");` +
|
|
93
|
+
` size-adjust: ${m.sizeAdjust}%; ascent-override: ${m.ascent}%;` +
|
|
94
|
+
` descent-override: ${m.descent}%; line-gap-override: ${m.lineGap ?? 0}%; }`,
|
|
95
|
+
);
|
|
96
|
+
stack.push(`"${fam} Fallback"`);
|
|
97
|
+
}
|
|
98
|
+
vars.push(`--font-${slug(fam)}: ${stack.concat(generic).join(', ')};`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (vars.length) rules.push(`:root { ${vars.join(' ')} }`);
|
|
102
|
+
return rules.join('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The <link> tags for a config, as { rel, href, ...attrs } descriptors:
|
|
107
|
+
* preload for self-hosted files (unless preload:false), preconnect + the
|
|
108
|
+
* css2 stylesheet for Google-hosted families.
|
|
109
|
+
*/
|
|
110
|
+
export function fontLinks(config = {}) {
|
|
111
|
+
const links = [];
|
|
112
|
+
let google = false;
|
|
113
|
+
for (const font of config.fonts || []) {
|
|
114
|
+
if (font.google) {
|
|
115
|
+
if (!google) {
|
|
116
|
+
google = true;
|
|
117
|
+
links.push({ rel: 'preconnect', href: 'https://fonts.googleapis.com' });
|
|
118
|
+
links.push({ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' });
|
|
119
|
+
}
|
|
120
|
+
links.push({ rel: 'stylesheet', href: googleUrl(font) });
|
|
121
|
+
} else if (font.src && config.preload !== false && font.preload !== false) {
|
|
122
|
+
for (const s of [].concat(font.src)) {
|
|
123
|
+
links.push({ rel: 'preload', href: s, as: 'font', type: `font/${formatOf(s)}`, crossorigin: '' });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return links;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Serialize the links + style as an HTML block (used by the vite plugin; the
|
|
131
|
+
// data-spark-font marker makes injection idempotent).
|
|
132
|
+
export function fontHtml(config = {}) {
|
|
133
|
+
const attrs = (l) => Object.entries(l)
|
|
134
|
+
.map(([k, v]) => (v === '' ? k : `${k}="${v}"`))
|
|
135
|
+
.join(' ');
|
|
136
|
+
const links = fontLinks(config)
|
|
137
|
+
.map((l) => `<link data-spark-font ${attrs(l)}>`);
|
|
138
|
+
const css = fontCss(config);
|
|
139
|
+
if (css) links.push(`<style data-spark-font>\n${css}\n</style>`);
|
|
140
|
+
return links.join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Runtime form: inject the <link>/<style> tags into document.head now.
|
|
145
|
+
* Idempotent (a second call is a no-op); returns a stop() that removes them.
|
|
146
|
+
*/
|
|
147
|
+
export function fonts(config = {}) {
|
|
148
|
+
if (typeof document === 'undefined' || !document.head) return () => {};
|
|
149
|
+
if (document.head.querySelector('[data-spark-font]')) return () => {};
|
|
150
|
+
const nodes = [];
|
|
151
|
+
for (const l of fontLinks(config)) {
|
|
152
|
+
const el = document.createElement('link');
|
|
153
|
+
el.setAttribute('data-spark-font', '');
|
|
154
|
+
for (const [k, v] of Object.entries(l)) el.setAttribute(k, v);
|
|
155
|
+
document.head.appendChild(el);
|
|
156
|
+
nodes.push(el);
|
|
157
|
+
}
|
|
158
|
+
const css = fontCss(config);
|
|
159
|
+
if (css) {
|
|
160
|
+
const style = document.createElement('style');
|
|
161
|
+
style.setAttribute('data-spark-font', '');
|
|
162
|
+
style.textContent = css;
|
|
163
|
+
document.head.appendChild(style);
|
|
164
|
+
nodes.push(style);
|
|
165
|
+
}
|
|
166
|
+
return () => nodes.forEach((n) => n.remove());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default { fonts, fontCss, fontLinks, fontHtml };
|
package/src/vite.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spark-html-font/vite — bake the font tags into every built page.
|
|
3
|
+
*
|
|
4
|
+
* Runs in closeBundle (order post, after spark-prerender has written its
|
|
5
|
+
* per-route HTML) and inserts the <link rel="preload">s + inline
|
|
6
|
+
* <style data-spark-font> right before </head> in each page — so the font
|
|
7
|
+
* fetch starts with the HTML and first paint uses the size-adjusted
|
|
8
|
+
* fallback. Component fragments have no <head> and are skipped naturally;
|
|
9
|
+
* pages already carrying data-spark-font are left alone (idempotent).
|
|
10
|
+
*
|
|
11
|
+
* import font from 'spark-html-font/vite';
|
|
12
|
+
* plugins: [spark(), prerender(), font({ fonts: [...] })]
|
|
13
|
+
*/
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
import { readFile, writeFile, readdir, stat } from 'node:fs/promises';
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
|
+
import { fontHtml } from './index.js';
|
|
18
|
+
|
|
19
|
+
async function htmlFiles(dir) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const name of await readdir(dir)) {
|
|
22
|
+
const full = join(dir, name);
|
|
23
|
+
const s = await stat(full);
|
|
24
|
+
if (s.isDirectory()) out.push(...await htmlFiles(full));
|
|
25
|
+
else if (name.endsWith('.html')) out.push(full);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function sparkFont(config = {}) {
|
|
31
|
+
let outDir = 'dist';
|
|
32
|
+
return {
|
|
33
|
+
name: 'spark-html-font',
|
|
34
|
+
apply: 'build',
|
|
35
|
+
configResolved(viteConfig) {
|
|
36
|
+
if (viteConfig && viteConfig.build && viteConfig.build.outDir) outDir = viteConfig.build.outDir;
|
|
37
|
+
},
|
|
38
|
+
closeBundle: {
|
|
39
|
+
order: 'post',
|
|
40
|
+
async handler() {
|
|
41
|
+
const root = resolve(outDir);
|
|
42
|
+
if (!existsSync(root)) return;
|
|
43
|
+
const block = fontHtml(config);
|
|
44
|
+
if (!block) return;
|
|
45
|
+
let pages = 0;
|
|
46
|
+
for (const file of await htmlFiles(root)) {
|
|
47
|
+
const html = await readFile(file, 'utf8');
|
|
48
|
+
if (!/<\/head>/i.test(html)) continue; // fragment (component) — skip
|
|
49
|
+
if (html.includes('data-spark-font')) continue; // already injected
|
|
50
|
+
await writeFile(file, html.replace(/<\/head>/i, `${block}\n</head>`), 'utf8');
|
|
51
|
+
pages++;
|
|
52
|
+
}
|
|
53
|
+
if (pages) console.log(`[spark-html-font] injected font loading into ${pages} page(s)`);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|