@xingwangzhe/stalux 1.18.0 → 1.20.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/package.json +1 -1
- package/src/internal/font-subset.ts +41 -29
- package/src/layouts/Stalux.astro +40 -14
- package/src/scripts/background.ts +17 -16
package/package.json
CHANGED
|
@@ -74,7 +74,8 @@ interface RouteCharSet {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
interface FontManifest {
|
|
77
|
-
common: string; // filename of common subset
|
|
77
|
+
common: string; // filename of common subset TTF
|
|
78
|
+
commonCss: string; // filename of common subset CSS (content-addressed)
|
|
78
79
|
routes: Record<string, string>; // route → subset filename
|
|
79
80
|
/** Per-post mapping: abbrlink → subset filename */
|
|
80
81
|
posts: Record<string, string>;
|
|
@@ -157,22 +158,19 @@ function scanContent(projectRoot: string): ContentFile[] {
|
|
|
157
158
|
for (const f of readdirSync(postsDir)) {
|
|
158
159
|
if (!f.endsWith(".md") && !f.endsWith(".mdx")) continue;
|
|
159
160
|
const content = readFileSync(join(postsDir, f), "utf-8");
|
|
160
|
-
const isTemplate = f.startsWith("_");
|
|
161
161
|
const fm = parsePostFrontmatter(content);
|
|
162
162
|
|
|
163
|
-
// Per-post routes
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
163
|
+
// Per-post routes:与内容集合 glob(*.{md,mdx},含 _ 前缀文件,
|
|
164
|
+
// 见 schemas/collections.ts)保持一致——这些文件都会生成页面,
|
|
165
|
+
// 字体子集必须覆盖,否则页面 subset css 缺失 → 404。
|
|
166
|
+
const abbrlink = fm.abbrlink ?? basename(f, extname(f));
|
|
167
|
+
files.push({
|
|
168
|
+
route: `posts/${abbrlink}`,
|
|
169
|
+
filePath: join(postsDir, f),
|
|
170
|
+
abbrlink,
|
|
171
|
+
});
|
|
173
172
|
// Virtual routes (archives/tags/categories): include ALL posts including templates
|
|
174
173
|
// because content collections load them and they render on those pages
|
|
175
|
-
const abbrlink = fm.abbrlink ?? basename(f, extname(f));
|
|
176
174
|
allPostContents.push({
|
|
177
175
|
abbrlink,
|
|
178
176
|
tags: fm.tags,
|
|
@@ -423,7 +421,7 @@ export async function runFontSubsetting(
|
|
|
423
421
|
const outDir = resolve(projectRoot, FONT_OUT_DIR);
|
|
424
422
|
if (existsSync(outDir)) {
|
|
425
423
|
for (const f of readdirSync(outDir)) {
|
|
426
|
-
if (f.startsWith("common-") || f.startsWith("subset-")) {
|
|
424
|
+
if (f.startsWith("common-") || f.startsWith("subset-") || f === "common.css") {
|
|
427
425
|
try {
|
|
428
426
|
unlinkSync(join(outDir, f));
|
|
429
427
|
} catch {
|
|
@@ -530,7 +528,11 @@ export async function runFontSubsetting(
|
|
|
530
528
|
}
|
|
531
529
|
}
|
|
532
530
|
|
|
533
|
-
// Generate common CSS
|
|
531
|
+
// Generate common CSS (content-addressed filename: 字符集变化 → hash 变化 →
|
|
532
|
+
// 文件名变化,浏览器必然拉新 CSS,避免缓存旧 CSS 引用已删除的旧 TTF 导致 404)
|
|
533
|
+
// dev 模式(skipPerRoute)用固定名 common.css,由 dev 中间件场景复用;
|
|
534
|
+
// 生产构建用 common-<hash>.css,HTML 从 manifest 读取引用。
|
|
535
|
+
const commonCssName = skipPerRoute ? "common.css" : `common-${commonHash}.css`;
|
|
534
536
|
const commonCSS = `/* Auto-generated common font subset */
|
|
535
537
|
@font-face {
|
|
536
538
|
font-family: "LXGW WenKai Subset";
|
|
@@ -538,7 +540,7 @@ export async function runFontSubsetting(
|
|
|
538
540
|
font-display: swap;
|
|
539
541
|
}
|
|
540
542
|
`;
|
|
541
|
-
writeFileSync(join(outDir,
|
|
543
|
+
writeFileSync(join(outDir, commonCssName), commonCSS);
|
|
542
544
|
|
|
543
545
|
const groups = deduplicate(routeSets);
|
|
544
546
|
const routeMap: Record<string, string> = {};
|
|
@@ -570,8 +572,11 @@ export async function runFontSubsetting(
|
|
|
570
572
|
}
|
|
571
573
|
|
|
572
574
|
// Generate per-route CSS (each route gets its own CSS file)
|
|
575
|
+
// CSS 文件名内容寻址:内容只引用一个 TTF,文件名带上该 TTF 的 hash,
|
|
576
|
+
// 内容变化 → 文件名变化 → 浏览器必然拉新,避免缓存旧 CSS 引用已删除 TTF。
|
|
573
577
|
// If no unique chars, reference the common TTF as fallback
|
|
574
578
|
const refTtf = existsSync(outPath) ? filename : commonFilename;
|
|
579
|
+
const refHash = existsSync(outPath) ? hash : commonHash;
|
|
575
580
|
for (const route of routes) {
|
|
576
581
|
const cssContent = `/* Auto-generated font subset for route: ${route} */
|
|
577
582
|
@font-face {
|
|
@@ -581,7 +586,7 @@ export async function runFontSubsetting(
|
|
|
581
586
|
}
|
|
582
587
|
`;
|
|
583
588
|
const cssName = route.replace("/", "-");
|
|
584
|
-
const cssFilename = `subset-${cssName}.css`;
|
|
589
|
+
const cssFilename = `subset-${cssName}-${refHash}.css`;
|
|
585
590
|
writeFileSync(join(outDir, cssFilename), cssContent);
|
|
586
591
|
routeMap[route] = cssFilename;
|
|
587
592
|
|
|
@@ -591,17 +596,24 @@ export async function runFontSubsetting(
|
|
|
591
596
|
}
|
|
592
597
|
}
|
|
593
598
|
}
|
|
594
|
-
|
|
595
|
-
// 6. Generate manifest
|
|
596
|
-
const manifest: FontManifest = {
|
|
597
|
-
common: commonFilename,
|
|
598
|
-
routes: routeMap,
|
|
599
|
-
posts: postMap,
|
|
600
|
-
};
|
|
601
|
-
writeFileSync(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
602
|
-
|
|
603
|
-
logger.info(
|
|
604
|
-
`Font subsetting done: ${subsetCount} route subsets + common (${(fontBuffer.length / 1024 / 1024).toFixed(1)} MB → variable)`,
|
|
605
|
-
);
|
|
606
599
|
}
|
|
600
|
+
|
|
601
|
+
// 6. Generate manifest(dev 也生成:routes/posts 为空,供 Stalux.astro import)
|
|
602
|
+
const manifest: FontManifest = {
|
|
603
|
+
common: commonFilename,
|
|
604
|
+
commonCss: commonCssName,
|
|
605
|
+
routes: routeMap,
|
|
606
|
+
posts: postMap,
|
|
607
|
+
};
|
|
608
|
+
writeFileSync(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
609
|
+
|
|
610
|
+
// 副本写入包内 generated/:Stalux.astro import 它,让 manifest 进入 Astro
|
|
611
|
+
// 依赖图——manifest 变化时页面必然重建,增量构建 restored 的页面不会引用旧资源。
|
|
612
|
+
const genDir = resolve(dirname(fileURLToPath(import.meta.url)), "generated");
|
|
613
|
+
mkdirSync(genDir, { recursive: true });
|
|
614
|
+
writeFileSync(join(genDir, "fonts-manifest.json"), JSON.stringify(manifest));
|
|
615
|
+
|
|
616
|
+
logger.info(
|
|
617
|
+
`Font subsetting done: ${subsetCount} route subsets + common (${(fontBuffer.length / 1024 / 1024).toFixed(1)} MB → variable)`,
|
|
618
|
+
);
|
|
607
619
|
}
|
package/src/layouts/Stalux.astro
CHANGED
|
@@ -5,6 +5,9 @@ import { getCollection } from "astro:content";
|
|
|
5
5
|
import "@styles/base/init.css";
|
|
6
6
|
import "@styles/shared/stagger.css";
|
|
7
7
|
import { getAuthorData, getAiDiscoveryData, getPromoteData, getSiteData } from "@utils/config-utils";
|
|
8
|
+
// font-subset.ts 构建期生成的 manifest 副本(进入 Astro 依赖图,
|
|
9
|
+
// manifest 变化 → 页面重建 → 增量构建 restored 的页面不会引用旧字体资源)
|
|
10
|
+
import fontsManifest from "../internal/generated/fonts-manifest.json";
|
|
8
11
|
const props = Astro.props as Props;
|
|
9
12
|
|
|
10
13
|
interface Props {
|
|
@@ -29,16 +32,44 @@ const promoteConfig = getPromoteData(configEntries);
|
|
|
29
32
|
const aiConfig = getAiDiscoveryData(configEntries);
|
|
30
33
|
const htmlLang = siteConfig.lang || "zh-CN";
|
|
31
34
|
|
|
32
|
-
// Resolve font subset CSS
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
// Resolve font subset CSS URLs(内容寻址,避免浏览器缓存旧 CSS 引用已删除 TTF → 404)
|
|
36
|
+
// 生产构建:font-subset.ts 生成带 hash 的 CSS 文件名并写入 manifest(副本 import 至此),
|
|
37
|
+
// 这里从 manifest 取文件名,内容变化必然换名、浏览器自动拉新。
|
|
38
|
+
// dev 模式:manifest 的 routes 为空(skipPerRoute),回退固定名 /fonts/common.css 与
|
|
39
|
+
// /fonts/subset-<id>.css,由 dev 按需中间件生成。
|
|
40
|
+
const fontManifest = fontsManifest as
|
|
41
|
+
| {
|
|
42
|
+
commonCss?: string;
|
|
43
|
+
routes?: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
| null
|
|
46
|
+
| undefined;
|
|
47
|
+
|
|
48
|
+
let commonFontCssUrl = "/fonts/common.css";
|
|
49
|
+
let fontSubsetUrl: string | undefined;
|
|
50
|
+
if (props.fontSubsetId) {
|
|
51
|
+
fontSubsetUrl = `/fonts/subset-${props.fontSubsetId}.css`;
|
|
52
|
+
}
|
|
53
|
+
if (fontManifest?.commonCss) {
|
|
54
|
+
commonFontCssUrl = `/fonts/${fontManifest.commonCss}`;
|
|
55
|
+
}
|
|
56
|
+
if (props.fontSubsetId && fontManifest?.routes) {
|
|
57
|
+
// route 键可能是 posts/xxx(页面对应 posts-xxx)或含原始 unicode(tag/category),
|
|
58
|
+
// 统一归一化比较:route.replace("/", "-") === decodeURIComponent(fontSubsetId)
|
|
59
|
+
const want = decodeURIComponent(props.fontSubsetId);
|
|
60
|
+
for (const [route, cssFile] of Object.entries(fontManifest.routes)) {
|
|
61
|
+
if (route.replace("/", "-") === want) {
|
|
62
|
+
fontSubsetUrl = `/fonts/${encodeURIComponent(cssFile)}`;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
36
67
|
|
|
37
68
|
// 背景 SVG 以静态 URL 硬编码在 background.ts(纯客户端),
|
|
38
69
|
// 集成在 config:setup 时把 SVG 复制到用户项目 public/background/。
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
70
|
+
// SSR 不渲染背景图,默认露出 .bg-layer 的灰墙底色(--stalux-bg-color),
|
|
71
|
+
// 客户端运行时随机选图;不经过 import.meta.glob 资源管线,
|
|
72
|
+
// 保证增量构建依赖图 hash 稳定。
|
|
42
73
|
const llmPromoteRaw = promoteConfig?.llm_promote ?? "";
|
|
43
74
|
const llmPromote = llmPromoteRaw
|
|
44
75
|
?.replace(/\{author\}/g, authorConfig.name)
|
|
@@ -62,7 +93,7 @@ const llmPromote = llmPromoteRaw
|
|
|
62
93
|
noindex={props.noindex}
|
|
63
94
|
/>
|
|
64
95
|
<!-- Per-route font subset (generated at build time) -->
|
|
65
|
-
<link rel="stylesheet" href=
|
|
96
|
+
<link rel="stylesheet" href={commonFontCssUrl} />
|
|
66
97
|
{fontSubsetUrl && <link rel="stylesheet" href={fontSubsetUrl} />}
|
|
67
98
|
<script>
|
|
68
99
|
import "../scripts/background.ts";
|
|
@@ -97,12 +128,7 @@ const llmPromote = llmPromoteRaw
|
|
|
97
128
|
data-stalux-bg-index="0"
|
|
98
129
|
>
|
|
99
130
|
{llmPromote && <div style="display:none">{llmPromote}</div>}
|
|
100
|
-
<div
|
|
101
|
-
class="bg-layer bg-a"
|
|
102
|
-
style={`background-image: url('${initialBg}'); opacity: 1;`}
|
|
103
|
-
aria-hidden="true"
|
|
104
|
-
>
|
|
105
|
-
</div>
|
|
131
|
+
<div class="bg-layer bg-a" style="opacity: 1;" aria-hidden="true"></div>
|
|
106
132
|
<div class="bg-layer bg-b" aria-hidden="true"></div>
|
|
107
133
|
<main class="stalux-main">
|
|
108
134
|
<slot />
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* 管线(import.meta.glob 会给 SVG 打上每次构建随机的 __ASTRO_ASSET_IMAGE__
|
|
7
7
|
* 占位符,破坏增量构建依赖图 hash),也不做任何构建期扫描。
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 背景行为:
|
|
10
|
+
* - SSR 只渲染灰墙底色(--stalux-bg-color),不渲染任何背景图,构建产物确定。
|
|
11
|
+
* - 首次加载与 VT 软导航每次都会随机选一张背景:astro:page-load 在
|
|
12
|
+
* window load(首访)与 swap 完成后、过渡动画期间(软导航)触发,
|
|
13
|
+
* 此时设置背景,过渡动画中旧图淡出、新图淡入,自然 crossfade 无跳变。
|
|
10
14
|
*/
|
|
11
15
|
declare global {
|
|
12
16
|
interface Window {
|
|
@@ -34,11 +38,11 @@ function setLayerOpacity(layer: "a" | "b", opacity: number): void {
|
|
|
34
38
|
if (el) el.style.opacity = String(opacity);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
function
|
|
41
|
+
function applyBackground(): void {
|
|
38
42
|
// 客户端运行时随机选图,构建产物保持确定性
|
|
39
43
|
const index = Math.floor(Math.random() * backgroundImages.length);
|
|
40
44
|
const layer: "a" | "b" = "a";
|
|
41
|
-
if (backgroundImages.length > 0) {
|
|
45
|
+
if (backgroundImages.length > 0 && backgroundImages[index]) {
|
|
42
46
|
const el = getLayerEl(layer);
|
|
43
47
|
if (el) el.style.backgroundImage = `url('${backgroundImages[index]}')`;
|
|
44
48
|
}
|
|
@@ -48,16 +52,13 @@ function initBackground(): void {
|
|
|
48
52
|
document.body.dataset.staluxBgLayer = layer;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
initBackground();
|
|
62
|
-
}
|
|
63
|
-
});
|
|
55
|
+
// 首次加载与 VT 软导航都触发 astro:page-load:
|
|
56
|
+
// - 首次加载:window load 后触发(router.js addEventListener("load", onPageLoad))。
|
|
57
|
+
// - VT 软导航:swap 完成后、过渡动画期间触发(router.js onPageLoad()),
|
|
58
|
+
// 此时设置随机背景,过渡中旧图淡出、新图淡入,无跳变闪变。
|
|
59
|
+
// 注意:软导航不会重新执行打包的 module 脚本(监听器仍是首访注册的),
|
|
60
|
+
// 所以这里不能做“只执行一次”的去重,每次 page-load 都必须随机;
|
|
61
|
+
// 若软导航后新脚本被重新执行,重复监听导致随机两次也无妨(仍是随机图)。
|
|
62
|
+
// body 的 data-stalux-bg-* 是 SSR 固定占位(a/0,为了构建确定性),
|
|
63
|
+
// 不能再用它判断“服务端已渲染正确背景”,否则随机逻辑永不执行。
|
|
64
|
+
document.addEventListener("astro:page-load", applyBackground);
|