@xingwangzhe/stalux 1.35.1 → 1.35.3

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.
Files changed (29) hide show
  1. package/README.md +4 -0
  2. package/package.json +23 -23
  3. package/src/components/stalux/archives/archivesList.astro +58 -50
  4. package/src/components/stalux/categories/categoriesList.astro +4 -4
  5. package/src/components/stalux/layout/footer.astro +8 -4
  6. package/src/components/stalux/layout/head.astro +3 -3
  7. package/src/components/stalux/layout/navs.astro +15 -15
  8. package/src/components/stalux/links/linkList.astro +4 -4
  9. package/src/components/stalux/links/socialMedia.astro +35 -35
  10. package/src/components/stalux/posts/PostTaxonomy.astro +14 -10
  11. package/src/components/stalux/posts/postCC.astro +7 -7
  12. package/src/components/stalux/posts/postCard.astro +47 -41
  13. package/src/components/stalux/posts/postContent.astro +49 -49
  14. package/src/components/stalux/posts/postList.astro +4 -4
  15. package/src/components/stalux/posts/postToc.astro +16 -16
  16. package/src/components/stalux/tags/tagsCloud.astro +20 -20
  17. package/src/components/stalux/words/wordCard.astro +28 -28
  18. package/src/components/stalux/words/wordList.astro +17 -17
  19. package/src/index.ts +98 -20
  20. package/src/internal/font-slices.ts +56 -6
  21. package/src/internal/html-images.ts +23 -2
  22. package/src/internal/image-dimensions.ts +60 -33
  23. package/src/internal/page-output-cache.ts +78 -0
  24. package/src/layouts/PostLayout.astro +9 -7
  25. package/src/layouts/Stalux.astro +4 -4
  26. package/src/pages/about.astro +6 -6
  27. package/src/pages/categories/[category].astro +4 -4
  28. package/src/pages/tags/[tag].astro +5 -1
  29. package/src/plugins/feature-flags.ts +5 -17
@@ -47,61 +47,61 @@ const mdSourceUrl = exportMd ? `${Astro.url.pathname.replace(/\/$/, "")}.md` : "
47
47
  </h1>
48
48
  <div class="stalux-post-metadata">
49
49
  {props.date && (
50
- <time
51
- class="stalux-post-meta-item"
52
- datetime={timezone ? toMachineDateTime(props.date, timezone) : props.date}
53
- >
54
- {t("post.publishTime")}
55
- {props.date}
56
- </time>
57
- )}
50
+ <time
51
+ class="stalux-post-meta-item"
52
+ datetime={timezone ? toMachineDateTime(props.date, timezone) : props.date}
53
+ >
54
+ {t("post.publishTime")}
55
+ {props.date}
56
+ </time>
57
+ )}
58
58
 
59
59
  {props.updated && (
60
- <time
61
- class="stalux-post-meta-item"
62
- datetime={timezone ? toMachineDateTime(props.updated, timezone) : props.updated}
63
- >
64
- {t("post.updateTime")}
65
- {props.updated}
66
- </time>
67
- )}
60
+ <time
61
+ class="stalux-post-meta-item"
62
+ datetime={timezone ? toMachineDateTime(props.updated, timezone) : props.updated}
63
+ >
64
+ {t("post.updateTime")}
65
+ {props.updated}
66
+ </time>
67
+ )}
68
68
  </div>
69
69
  {(
70
- <div class="stalux-post-page-stats">
71
- {(props.readingMinutes ?? 0) > 0 && (
72
- <span class="stalux-post-reading-time">
73
- {t("post.readingTime")}
74
- {props.readingMinutes && props.readingMinutes > 0
75
- ? props.readingMinutes < 1
76
- ? t("common.lessThanOneMinute")
77
- : t("common.minute", { n: props.readingMinutes })
78
- : ""}
79
- </span>
70
+ <div class="stalux-post-page-stats">
71
+ {(props.readingMinutes ?? 0) > 0 && (
72
+ <span class="stalux-post-reading-time">
73
+ {t("post.readingTime")}
74
+ {props.readingMinutes && props.readingMinutes > 0
75
+ ? props.readingMinutes < 1
76
+ ? t("common.lessThanOneMinute")
77
+ : t("common.minute", { n: props.readingMinutes })
78
+ : ""}
79
+ </span>
80
+ )}
81
+ {(props.wordCount ?? 0) > 0 && (
82
+ <span class="wordCount">
83
+ {t("post.wordCount")}
84
+ {props.wordCount}
85
+ </span>
86
+ )}
87
+ <span id="vercount_container_page_pv">
88
+ {t("post.viewCount")}
89
+ <span id="vercount_value_page_pv">Loading...</span>
90
+ </span>
91
+ </div>
80
92
  )}
81
- {(props.wordCount ?? 0) > 0 && (
82
- <span class="wordCount">
83
- {t("post.wordCount")}
84
- {props.wordCount}
85
- </span>
86
- )}
87
- <span id="vercount_container_page_pv">
88
- {t("post.viewCount")}
89
- <span id="vercount_value_page_pv">Loading...</span>
90
- </span>
91
- </div>
92
- )}
93
93
  {exportMd && (
94
- <div class="stalux-post-source-link-row">
95
- <a
96
- class="stalux-post-source-link"
97
- href={mdSourceUrl}
98
- target="_blank"
99
- rel="noopener noreferrer"
100
- >
101
- {t("post.viewSource")}
102
- </a>
103
- </div>
104
- )}
94
+ <div class="stalux-post-source-link-row">
95
+ <a
96
+ class="stalux-post-source-link"
97
+ href={mdSourceUrl}
98
+ target="_blank"
99
+ rel="noopener noreferrer"
100
+ >
101
+ {t("post.viewSource")}
102
+ </a>
103
+ </div>
104
+ )}
105
105
  </header>
106
106
 
107
107
  <ArticleSummary description={props.description} label={t("post.summary")} />
@@ -14,9 +14,9 @@ const posts = props.posts ?? (await getCollection("posts", ({ data }) => !data.d
14
14
  <div data-stalux="postlist" class="stalux-post-list-container">
15
15
  <ul class="post-list">
16
16
  {posts.map((post, index) => (
17
- <li>
18
- <PostCard post={post} priority={index === 0} />
19
- </li>
20
- ))}
17
+ <li>
18
+ <PostCard post={post} priority={index === 0} />
19
+ </li>
20
+ ))}
21
21
  </ul>
22
22
  </div>
@@ -36,22 +36,22 @@ const tocItems = headings
36
36
  >
37
37
  <h3 class="stalux-toc-title">{title}</h3>
38
38
  {tocItems.length > 0 ? (
39
- <ul class="stalux-toc-list" transition:name="toc-content">
40
- {tocItems.map((heading) => (
41
- <li>
42
- <a
43
- class={`stalux-toc-item ${({ 1: "stalux-toc-depth-1", 2: "stalux-toc-depth-2", 3: "stalux-toc-depth-3", 4: "stalux-toc-depth-4", 5: "stalux-toc-depth-5", 6: "stalux-toc-depth-6" } as Record<number, string>)[heading.depth] || ""} a-none`}
44
- href={`#${heading.slug}`}
45
- data-toc-link
46
- >
47
- {heading.text}
48
- </a>
49
- </li>
50
- ))}
51
- </ul>
52
- ) : (
53
- <div class="stalux-toc-empty">{t("taxonomy.noToc")}</div>
54
- )}
39
+ <ul class="stalux-toc-list" transition:name="toc-content">
40
+ {tocItems.map((heading) => (
41
+ <li>
42
+ <a
43
+ class={`stalux-toc-item ${({ 1: "stalux-toc-depth-1", 2: "stalux-toc-depth-2", 3: "stalux-toc-depth-3", 4: "stalux-toc-depth-4", 5: "stalux-toc-depth-5", 6: "stalux-toc-depth-6" } as Record<number, string>)[heading.depth] || ""} a-none`}
44
+ href={`#${heading.slug}`}
45
+ data-toc-link
46
+ >
47
+ {heading.text}
48
+ </a>
49
+ </li>
50
+ ))}
51
+ </ul>
52
+ ) : (
53
+ <div class="stalux-toc-empty">{t("taxonomy.noToc")}</div>
54
+ )}
55
55
  </nav>
56
56
 
57
57
  <script src="../../../scripts/toc-scrollspy.ts"></script>
@@ -24,26 +24,26 @@ const tagLinks = tagList.map((t) => ({
24
24
  <!-- 移动端:SSR 卡片 -->
25
25
  <div class="stalux-cloud-cards" id="tags-cards">
26
26
  {tagList.length > 0 ? (
27
- tagGroups.map((group) => (
28
- <div class="stalux-cloud-tag-group">
29
- {group.map(({ name, count }) => (
30
- <a
31
- href={`/tags/${name}/`}
32
- class="stalux-cloud-tag-card"
33
- data-stalux-card
34
- data-stalux-tag-card
35
- data-count={count}
36
- data-astro-prefetch
37
- >
38
- <span class="stalux-cloud-tag-name">{name}</span>
39
- <span class="stalux-cloud-tag-count">{count}</span>
40
- </a>
41
- ))}
42
- </div>
43
- ))
44
- ) : (
45
- <div class="stalux-cloud-no-tags">{t("tags.noTags")}</div>
46
- )}
27
+ tagGroups.map((group) => (
28
+ <div class="stalux-cloud-tag-group">
29
+ {group.map(({ name, count }) => (
30
+ <a
31
+ href={`/tags/${name}/`}
32
+ class="stalux-cloud-tag-card"
33
+ data-stalux-card
34
+ data-stalux-tag-card
35
+ data-count={count}
36
+ data-astro-prefetch
37
+ >
38
+ <span class="stalux-cloud-tag-name">{name}</span>
39
+ <span class="stalux-cloud-tag-count">{count}</span>
40
+ </a>
41
+ ))}
42
+ </div>
43
+ ))
44
+ ) : (
45
+ <div class="stalux-cloud-no-tags">{t("tags.noTags")}</div>
46
+ )}
47
47
  </div>
48
48
 
49
49
  <!-- 桌面端:Canvas 标签云 -->
@@ -23,32 +23,32 @@ const machineDate = date && timezone ? toMachineDateTime(date, timezone) : date;
23
23
  <Content />
24
24
  </div>
25
25
  {(date || source || sourceDate) && (
26
- <footer class="stalux-word-footer">
27
- {date ? (
28
- <time class="stalux-word-date" datetime={machineDate}>
29
- {date}
30
- </time>
31
- ) : (
32
- <span />
33
- )}
34
- {(source || sourceDate) && (
35
- <div class="stalux-word-source-meta">
36
- {sourceDate && <span class="stalux-word-source-date">{sourceDate}</span>}
37
- {source &&
38
- (link ? (
39
- <a
40
- href={link}
41
- target="_blank"
42
- rel="noopener noreferrer"
43
- class="stalux-word-source-link"
44
- >
45
- — {source}
46
- </a>
47
- ) : (
48
- <span class="stalux-word-source-text">— {source}</span>
49
- ))}
50
- </div>
51
- )}
52
- </footer>
53
- )}
26
+ <footer class="stalux-word-footer">
27
+ {date ? (
28
+ <time class="stalux-word-date" datetime={machineDate}>
29
+ {date}
30
+ </time>
31
+ ) : (
32
+ <span />
33
+ )}
34
+ {(source || sourceDate) && (
35
+ <div class="stalux-word-source-meta">
36
+ {sourceDate && <span class="stalux-word-source-date">{sourceDate}</span>}
37
+ {source &&
38
+ (link ? (
39
+ <a
40
+ href={link}
41
+ target="_blank"
42
+ rel="noopener noreferrer"
43
+ class="stalux-word-source-link"
44
+ >
45
+ — {source}
46
+ </a>
47
+ ) : (
48
+ <span class="stalux-word-source-text">— {source}</span>
49
+ ))}
50
+ </div>
51
+ )}
52
+ </footer>
53
+ )}
54
54
  </article>
@@ -21,24 +21,24 @@ const sorted = [...entries].sort((a, b) => {
21
21
 
22
22
  <ul class="stalux-word-list-list">
23
23
  {sorted.map((entry) => {
24
- const hash = entry.data.abbrlink ? `#word-${entry.data.abbrlink}` : undefined;
25
- return (
26
- <li class="stalux-word-list-item">
27
- {hash ? (
28
- <a
29
- href={hash}
30
- class="a-none"
31
- data-word-hash={hash}
32
- aria-label={`Jump to ${entry.data.abbrlink}`}
33
- >
24
+ const hash = entry.data.abbrlink ? `#word-${entry.data.abbrlink}` : undefined;
25
+ return (
26
+ <li class="stalux-word-list-item">
27
+ {hash ? (
28
+ <a
29
+ href={hash}
30
+ class="a-none"
31
+ data-word-hash={hash}
32
+ aria-label={`Jump to ${entry.data.abbrlink}`}
33
+ >
34
+ <WordCard entry={entry} timezone={timezone} />
35
+ </a>
36
+ ) : (
34
37
  <WordCard entry={entry} timezone={timezone} />
35
- </a>
36
- ) : (
37
- <WordCard entry={entry} timezone={timezone} />
38
- )}
39
- </li>
40
- );
41
- })}
38
+ )}
39
+ </li>
40
+ );
41
+ })}
42
42
  </ul>
43
43
 
44
44
  <script src="../../../scripts/word-list.ts"></script>
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  readdirSync,
20
20
  readFileSync,
21
21
  } from "node:fs";
22
+ import { createRequire } from "node:module";
22
23
  import path from "node:path";
23
24
  import { fileURLToPath } from "node:url";
24
25
  import partytown from "@astrojs/partytown";
@@ -30,6 +31,7 @@ import { fontProviders } from "astro/config";
30
31
  // pagefind 是 ESM-only 包,需要在模块顶层导入
31
32
  // 因为 astro:build:done 钩子中 Vite module runner 已关闭,无法动态 import
32
33
  import { createIndex as pagefindCreateIndex } from "pagefind";
34
+ import { parse } from "parse5";
33
35
  import { parse as parseYaml } from "yaml";
34
36
  import type { StaluxOptions } from "./config";
35
37
  import { expressiveCode } from "./expressive-code";
@@ -38,10 +40,12 @@ import {
38
40
  clearPageFontSubsets,
39
41
  readLinkedStylesheetText,
40
42
  resolveFontInputs,
41
- writePageFontSubset,
43
+ writePageFontSubsetAsync,
42
44
  } from "./internal/font-slices";
43
- import { addLocalImageDimensions } from "./internal/image-dimensions";
45
+ import { findHtmlImages } from "./internal/html-images";
46
+ import { addLocalImageDimensions, type ImageDimensions } from "./internal/image-dimensions";
44
47
  import { createInjectedRoutes } from "./internal/injected-routes";
48
+ import { createPageOutputCache } from "./internal/page-output-cache";
45
49
  import { createRuntimeCacheKey } from "./internal/runtime-cache-key";
46
50
  import {
47
51
  appendUniquePlugin,
@@ -49,11 +53,7 @@ import {
49
53
  prepareSatteriProcessor,
50
54
  } from "./internal/satteri-config";
51
55
  import { createViteAliases } from "./internal/vite-aliases";
52
- import {
53
- applyHtmlImageLoadingPolicy,
54
- featureFlagsHast,
55
- featureFlagsMdast,
56
- } from "./plugins/feature-flags";
56
+ import { featureFlagsHast, featureFlagsMdast } from "./plugins/feature-flags";
57
57
  import { temml } from "./plugins/satteri-temml";
58
58
  import { describeError } from "./utils/diagnostics";
59
59
 
@@ -386,53 +386,127 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
386
386
  // Astro's Markdown image transform drops arbitrary lazy-loading hints
387
387
  // added during HAST processing. Reapply the policy to emitted HTML.
388
388
  const htmlFiles = await fs.readdir(outDir, { recursive: true });
389
+ const sourceDir = fileURLToPath(new URL(".", import.meta.url));
390
+ const resolveModule = createRequire(import.meta.url).resolve;
391
+ const parserEntry = resolveModule("parse5");
392
+ const nativeEntry = resolveModule("@xingwangzhe/cjk-font-split-native");
393
+ const outputCache = await createPageOutputCache(
394
+ path.resolve("node_modules/.astro/stalux-page-output"),
395
+ [
396
+ ...[
397
+ "index.ts",
398
+ "internal/font-slices.ts",
399
+ "internal/image-dimensions.ts",
400
+ "internal/html-images.ts",
401
+ "internal/page-output-cache.ts",
402
+ "internal/style-names.generated.ts",
403
+ "plugins/feature-flags.ts",
404
+ "../package.json",
405
+ ].map((entry) => path.join(sourceDir, entry)),
406
+ path.join(path.dirname(nativeEntry), "package.json"),
407
+ path.resolve(path.dirname(parserEntry), "../package.json"),
408
+ ],
409
+ pageFontData?.fontBuffer,
410
+ htmlFiles
411
+ .filter((entry) => /\.(?:css|png|jpe?g|gif|webp)$/iu.test(entry))
412
+ .map((entry) => path.join(outDir, entry)),
413
+ );
414
+ let cacheHits = 0;
389
415
  let optimizedImagePages = 0;
390
416
  let pageFontPages = 0;
391
417
  if (pageFontData) await fs.mkdir(pageFontData.outputDir, { recursive: true });
392
418
  if (pageFontData) await fs.mkdir(pageFontData.cacheDir, { recursive: true });
393
419
  const referencedPageFonts = new Set<string>();
394
420
  const linkedCssTextCache = new Map<string, string>();
395
- for (const entry of htmlFiles) {
396
- if (typeof entry !== "string" || !entry.endsWith(".html")) continue;
421
+ const dimensionsCache = new Map<string, Promise<ImageDimensions | undefined>>();
422
+ const pageEntries = htmlFiles.filter((entry) => entry.endsWith(".html"));
423
+ const processPage = async (entry: string) => {
397
424
  const output = path.join(outDir, entry);
398
425
  const before = await fs.readFile(output, "utf8");
399
426
  const withoutPageFont = before.replace(
400
427
  /<style data-stalux-page-font>[\s\S]*?<\/style>/gi,
401
428
  "",
402
429
  );
403
- const withImageDimensions = await addLocalImageDimensions(
404
- withoutPageFont,
405
- outDir,
406
- );
407
- let after = applyHtmlImageLoadingPolicy(withImageDimensions);
430
+ const cached = await outputCache.get(entry, withoutPageFont);
431
+ if (cached) {
432
+ if (cached.font && pageFontData) {
433
+ if (!referencedPageFonts.has(cached.font.filename)) {
434
+ copyFileSync(
435
+ cached.font.sourcePath,
436
+ path.join(pageFontData.outputDir, cached.font.filename),
437
+ );
438
+ }
439
+ referencedPageFonts.add(cached.font.filename);
440
+ pageFontPages++;
441
+ }
442
+ if (cached.html !== before) {
443
+ await fs.writeFile(output, cached.html);
444
+ optimizedImagePages++;
445
+ }
446
+ cacheHits++;
447
+ return;
448
+ }
449
+ let cachedFont: { filename: string; sourcePath: string } | undefined;
450
+ const dependencies = new Set<string>();
451
+ const document = parse(withoutPageFont, { sourceCodeLocationInfo: true });
452
+ const images = findHtmlImages(withoutPageFont, document);
453
+ let after = await addLocalImageDimensions(withoutPageFont, outDir, {
454
+ images,
455
+ cache: dimensionsCache,
456
+ loadingPolicy: true,
457
+ dependencies,
458
+ });
408
459
  if (pageFontData) {
409
460
  const linkedStylesheetText = readLinkedStylesheetText(
410
461
  after,
411
462
  output,
412
463
  outDir,
413
464
  linkedCssTextCache,
465
+ document,
466
+ dependencies,
414
467
  );
415
- const pageFont = writePageFontSubset(
468
+ const pageFont = await writePageFontSubsetAsync(
416
469
  after,
417
470
  pageFontData.fontBuffer,
418
471
  pageFontData.cacheDir,
419
472
  "/_astro/fonts/",
420
473
  linkedStylesheetText,
474
+ "",
475
+ document,
421
476
  );
422
477
  if (pageFont) {
423
478
  after = pageFont.html;
424
479
  pageFontPages++;
480
+ cachedFont = {
481
+ filename: pageFont.filename,
482
+ sourcePath: pageFont.sourcePath,
483
+ };
484
+ if (!referencedPageFonts.has(pageFont.filename)) {
485
+ copyFileSync(
486
+ pageFont.sourcePath,
487
+ path.join(pageFontData.outputDir, pageFont.filename),
488
+ );
489
+ }
425
490
  referencedPageFonts.add(pageFont.filename);
426
- copyFileSync(
427
- pageFont.sourcePath,
428
- path.join(pageFontData.outputDir, pageFont.filename),
429
- );
430
491
  }
431
492
  }
432
493
  if (after !== before) {
433
494
  await fs.writeFile(output, after);
434
495
  optimizedImagePages++;
435
496
  }
497
+ await outputCache.set(
498
+ entry,
499
+ withoutPageFont,
500
+ {
501
+ html: after,
502
+ font: cachedFont,
503
+ },
504
+ [...dependencies],
505
+ );
506
+ };
507
+ // Bound I/O concurrency without retaining the entire site's HTML in memory.
508
+ for (let offset = 0; offset < pageEntries.length; offset += 4) {
509
+ await Promise.all(pageEntries.slice(offset, offset + 4).map(processPage));
436
510
  }
437
511
  if (pageFontData) {
438
512
  clearPageFontSubsets(pageFontData.outputDir, referencedPageFonts);
@@ -441,11 +515,15 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
441
515
  logger.info(
442
516
  `generated page-specific body font subsets for ${pageFontPages} HTML pages`,
443
517
  );
518
+ logger.info(
519
+ `HTML postprocessing ${(performance.now() - started).toFixed(1)}ms; cache ${cacheHits}/${htmlFiles.filter((entry) => entry.endsWith(".html")).length}`,
520
+ );
444
521
 
445
522
  // 后处理:Pagefind 搜索索引。启用时任何失败都必须让构建失败,
446
523
  // 避免发布一个页面正常但搜索已损坏的产物。
447
524
  if (opt.pagefind) {
448
525
  logger.info("Running Pagefind indexer...");
526
+ const pagefindStarted = performance.now();
449
527
 
450
528
  try {
451
529
  const { index, errors } = await pagefindCreateIndex();
@@ -466,7 +544,7 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
466
544
  });
467
545
 
468
546
  logger.info(
469
- `indexed ${page_count} pages → ${outputPath} (${(performance.now() - started).toFixed(1)}ms)`,
547
+ `indexed ${page_count} pages → ${outputPath} (${(performance.now() - pagefindStarted).toFixed(1)}ms)`,
470
548
  );
471
549
  } catch (error) {
472
550
  logger.error(`Pagefind indexing failed: ${describeError(error)}`);
@@ -18,7 +18,7 @@ import { styleNames } from "./style-names.generated.ts";
18
18
  import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
19
19
  import { dirname, join, resolve, sep } from "node:path";
20
20
  import { fileURLToPath, pathToFileURL } from "node:url";
21
- import { subsetFont } from "@xingwangzhe/cjk-font-split-native";
21
+ import { FontSubsetter } from "@xingwangzhe/cjk-font-split-native";
22
22
  import type { AstroIntegrationLogger } from "astro";
23
23
  import { type DefaultTreeAdapterMap, parse } from "parse5";
24
24
 
@@ -46,6 +46,8 @@ const PAGE_SLICE_OUT_DIR = "node_modules/.astro/stalux-page-fonts";
46
46
  */
47
47
  export const RESERVED_GLYPHS = "0123456789";
48
48
 
49
+ const preparedFonts = new WeakMap<Buffer, FontSubsetter>();
50
+
49
51
  // ---------------------------------------------------------------------------
50
52
  // Types
51
53
  // ---------------------------------------------------------------------------
@@ -164,10 +166,12 @@ export function readLinkedStylesheetText(
164
166
  pagePath: string,
165
167
  outputDir: string,
166
168
  cache = new Map<string, string>(),
169
+ document = parse(html),
170
+ dependencies?: Set<string>,
167
171
  ): string {
168
172
  const root = resolve(outputDir);
169
173
  const links: string[] = [];
170
- collectStylesheetLinks(parse(html), links);
174
+ collectStylesheetLinks(document, links);
171
175
  const content: string[] = [];
172
176
  for (const href of links) {
173
177
  if (/^(?:[a-z][a-z\d+.-]*:|\/\/|data:)/iu.test(href)) continue;
@@ -180,14 +184,15 @@ export function readLinkedStylesheetText(
180
184
  continue;
181
185
  }
182
186
  if (cssPath !== root && !cssPath.startsWith(`${root}${sep}`)) continue;
187
+ dependencies?.add(cssPath);
183
188
  if (!cache.has(cssPath)) {
184
189
  try {
185
- cache.set(cssPath, readFileSync(cssPath, "utf8"));
190
+ cache.set(cssPath, cssGeneratedText(readFileSync(cssPath, "utf8")));
186
191
  } catch {
187
192
  cache.set(cssPath, "");
188
193
  }
189
194
  }
190
- content.push(cssGeneratedText(cache.get(cssPath) ?? ""));
195
+ content.push(cache.get(cssPath) ?? "");
191
196
  }
192
197
  return content.join(" ");
193
198
  }
@@ -256,8 +261,40 @@ export function writePageFontSubset(
256
261
  publicUrlPrefix = "/_astro/fonts/",
257
262
  linkedStylesheetText = "",
258
263
  extraChars = "",
264
+ document = parse(html),
259
265
  ): PageFontSubset | undefined {
260
- const document = parse(html);
266
+ const input = preparePageFont(html, fontBuffer, linkedStylesheetText, extraChars, document);
267
+ if (!input) return undefined;
268
+ return renderPageFont(
269
+ html,
270
+ publicUrlPrefix,
271
+ input.chars,
272
+ input.prepared.subset(input.chars.join(""), cacheDir),
273
+ );
274
+ }
275
+
276
+ export async function writePageFontSubsetAsync(
277
+ html: string,
278
+ fontBuffer: Buffer,
279
+ cacheDir: string,
280
+ publicUrlPrefix = "/_astro/fonts/",
281
+ linkedStylesheetText = "",
282
+ extraChars = "",
283
+ document = parse(html),
284
+ ): Promise<PageFontSubset | undefined> {
285
+ const input = preparePageFont(html, fontBuffer, linkedStylesheetText, extraChars, document);
286
+ if (!input) return undefined;
287
+ const result = await input.prepared.subsetAsync(input.chars.join(""), cacheDir);
288
+ return renderPageFont(html, publicUrlPrefix, input.chars, result);
289
+ }
290
+
291
+ function preparePageFont(
292
+ html: string,
293
+ fontBuffer: Buffer,
294
+ linkedStylesheetText: string,
295
+ extraChars: string,
296
+ document: DefaultTreeAdapterMap["document"],
297
+ ) {
261
298
  const body = findBody(document);
262
299
  if (!body) return undefined;
263
300
 
@@ -274,7 +311,20 @@ export function writePageFontSubset(
274
311
  );
275
312
  if (chars.length === 0) return undefined;
276
313
 
277
- const result = subsetFont(fontBuffer, chars.join(""), cacheDir);
314
+ let prepared = preparedFonts.get(fontBuffer);
315
+ if (!prepared) {
316
+ prepared = new FontSubsetter(fontBuffer);
317
+ preparedFonts.set(fontBuffer, prepared);
318
+ }
319
+ return { chars, prepared };
320
+ }
321
+
322
+ function renderPageFont(
323
+ html: string,
324
+ publicUrlPrefix: string,
325
+ chars: string[],
326
+ result: { hash: string; path: string; cacheHit: boolean },
327
+ ): PageFontSubset {
278
328
  const filename = `page-${result.hash}.woff2`;
279
329
 
280
330
  const rel = `${publicUrlPrefix}${filename}`;