fix: 修复升级后整站无样式与点击特效失效

- 旧哈希静态资源随版本升级被删除,被缓存的 HTML 仍请求旧文件名导致
  FileNotFoundException 500,补 compat/assets 历史资源并在构建时合并
- postbuild 增加静态资源引用校验,缺失即构建失败
- ClickEffect 改为常驻配置节点 + data 属性驱动,避免配置未落库时静默失效;
  强制回流修正 transition 不触发;特效层挂到 body 下避免被导航栏遮挡
- 新增 scripts: postbuild/snapshot-assets/package/smoke-clickfx
- 版本 1.2.1
This commit is contained in:
2026-09-27 16:27:51 +08:00
parent a127d5a314
commit 125d0592c9
42 changed files with 951 additions and 123 deletions
+80
View File
@@ -0,0 +1,80 @@
/**
* 构建后置处理:
* 1. 复制 logo.png 到 assets(保留历史行为)
* 2. 合并 compat/assets 下的历史版本静态资源,避免站点/代理缓存中的旧 HTML
* 仍然引用上一版本的哈希文件名时出现 500(FileNotFoundException)
* 3. 校验所有 templates/*.html 引用的静态资源真实存在,缺失即构建失败
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const assetsDir = path.join(root, "templates", "assets");
const compatDir = path.join(root, "compat", "assets");
const templatesDir = path.join(root, "templates");
fs.mkdirSync(assetsDir, { recursive: true });
// 1. logo
const logoSrc = path.join(templatesDir, "logo.png");
if (fs.existsSync(logoSrc)) {
fs.copyFileSync(logoSrc, path.join(assetsDir, "logo.png"));
}
// 2. 历史资源合并(已存在的同名文件以当前构建为准,不覆盖)
if (fs.existsSync(compatDir)) {
let copied = 0;
let skipped = 0;
for (const entry of fs.readdirSync(compatDir, { withFileTypes: true })) {
if (!entry.isFile()) continue;
const src = path.join(compatDir, entry.name);
const dst = path.join(assetsDir, entry.name);
if (fs.existsSync(dst)) {
skipped++;
continue;
}
fs.copyFileSync(src, dst);
copied++;
}
console.log(`[compat] 历史静态资源:复制 ${copied} 个,当前构建已覆盖 ${skipped} 个`);
}
// 3. 资源引用校验
const missing = [];
const checked = new Set();
/** 主题静态资源前缀,必须与 astro.config.mjs 的 base 保持一致 */
const prefixes = ["/themes/warm-island/assets/"];
/** Thymeleaf 的 @{...} 链接表达式同样需要校验 */
const REFERENCE_RE =
/(?:href|src)=["']([^"']*?\/(?:themes\/[^/]+)\/assets\/[^"']+?)["']/g;
for (const file of fs.readdirSync(templatesDir)) {
if (!file.endsWith(".html")) continue;
const html = fs.readFileSync(path.join(templatesDir, file), "utf8");
REFERENCE_RE.lastIndex = 0;
let match;
while ((match = REFERENCE_RE.exec(html)) !== null) {
let ref = match[1];
if (ref.includes("@{")) continue; // 由 Halo 运行时解析,跳过
for (const prefix of prefixes) {
if (!ref.startsWith(prefix)) continue;
const rel = ref.slice(prefix.length).split("#")[0].split("?")[0];
const key = `${file}:${rel}`;
if (checked.has(key)) continue;
checked.add(key);
if (!fs.existsSync(path.join(assetsDir, rel))) {
missing.push(`${file} -> ${ref}`);
}
}
}
}
if (missing.length > 0) {
console.error(`[verify] 发现 ${missing.length} 处静态资源缺失:`);
for (const item of missing) console.error(` - ${item}`);
process.exit(1);
}
console.log(`[verify] 静态资源引用校验通过(${checked.size} 条引用)`);