fix(exif): 构建 EXIF 提取全线复活——拦截式 loader + 数据仓清障
- 根因双层:①包装层 post-load 的 store.set 被 Astro 7 解析层静默丢弃
→ 改拦截 baseLoader 写入内联增强(enrich 在 set 时机完成)
②内容层增量同步的持久化数据仓复用旧空条目 → rm node_modules/.astro
全量重提取(loader 注释已记此坑:改 loader 逻辑须清数据仓或换 loader 名)
- 图库 19/21 张参数条实出(镜头/光圈/快门/ISO/焦距/时间)
- 同时并入 op-gitea T3 交付:public/admin/ 三件(Sveltia 0.252 pinned
自托管)+ gitea [cors] 台账;其发现的 title omit 坑已在 config 修正
验证: build 28 页;dist/gallery data-exif 19 条含 ISO;E2E 30/30 PASS
(staging 29 html)
Spec: 2026-10-25_add-terminal-books-admin
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
40346c3f21
commit
cef648db9a
2 files changed
+36
-21
No files matched your search
@@ -50,7 +50,7 @@ sqlite 直接热拷贝会撕裂,备份前 `docker compose down` 或用 `sqlite
|
|||||||
|
|
||||||
## 已知坑
|
## 已知坑
|
||||||
|
|
||||||
- **[cors] 段已启用(2026-10-25,Sveltia CMS 试水)**:`ALLOW_DOMAIN = https://wait4x.online,http://127.0.0.1:18742`;本地源为 T3 试水临时项,试水收尾应移除;`HEADERS` 必须含 `Authorization`(token 认证跨源必需,默认仅 Content-Type,User-Agent)。依据:docs.gitea.com config-cheat-sheet#cors
|
- **[cors] 段已启用(2026-10-25,Sveltia CMS 试水)**:`ALLOW_DOMAIN = https://wait4x.online`(试水期的本地源已于收尾移除);`HEADERS` 必须含 `Authorization`(token 认证跨源必需,默认仅 Content-Type,User-Agent)。依据:docs.gitea.com config-cheat-sheet#cors
|
||||||
|
|
||||||
- **不要开 `REQUIRE_SIGNIN_VIEW`**:它会把匿名 `/api/v1/version` 一并 403,
|
- **不要开 `REQUIRE_SIGNIN_VIEW`**:它会把匿名 `/api/v1/version` 一并 403,
|
||||||
而验收与健康检查都裸 curl 该端点;私有性靠 private 仓 + `DISABLE_REGISTRATION` 保证。
|
而验收与健康检查都裸 curl 该端点;私有性靠 private 仓 + `DISABLE_REGISTRATION` 保证。
|
||||||
|
|||||||
+35
-20
@@ -87,34 +87,49 @@ export async function resolvePhotoExif(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* photos 集合加载器 = glob(JSON) + EXIF 构建期补全。
|
* photos 集合加载器 = glob(JSON) + EXIF 构建期补全。
|
||||||
* entry.data.image 经 z.image() 校验后已是 ImageMetadata(不含源文件路径),
|
|
||||||
* 因此从 JSON 原文重读 `image` 字符串,按“相对 JSON 文件”定位源图交给 exifr。
|
|
||||||
*
|
*
|
||||||
* 坑:dev 模式下单文件变更由内层 glob loader 的 watcher 增量处理,会绕过本包装层,
|
* 实现教训(2026-10-25 双日调试定位):Astro 7 内容层在 load() 结束后
|
||||||
* 改图或改 JSON 后需重启 dev server 才能补全 EXIF;SSG 构建每次全量 sync,无此问题。
|
* 会按 loader 的原始写入序列做 schema 解析——包装层在 baseLoader.load()
|
||||||
|
* 之后用 ctx.store.set 补写的字段会在解析阶段被静默丢弃(exif 全空但
|
||||||
|
* 无任何报错的根因)。因此改为**拦截写入**:代理 baseLoader 看到的
|
||||||
|
* store,在它 set 每个条目的那一刻异步增强,增强完成的 Promise 收集
|
||||||
|
* 后在 load 返回前 await 完毕。
|
||||||
*/
|
*/
|
||||||
export function photosWithExifLoader(base = './content/photos'): Loader {
|
export function photosWithExifLoader(base = './content/photos'): Loader {
|
||||||
const baseLoader = glob({ pattern: '**/*.json', base });
|
const baseLoader = glob({ pattern: '**/*.json', base });
|
||||||
return {
|
return {
|
||||||
name: 'photos-with-exif',
|
name: 'photos-with-exif',
|
||||||
load: async (ctx) => {
|
load: async (ctx) => {
|
||||||
await baseLoader.load(ctx);
|
|
||||||
const root = fileURLToPath(ctx.config.root);
|
const root = fileURLToPath(ctx.config.root);
|
||||||
await Promise.all(
|
const origSet = ctx.store.set.bind(ctx.store);
|
||||||
ctx.store.keys().map(async (id) => {
|
const pending: Promise<void>[] = [];
|
||||||
const entry = ctx.store.get(id);
|
|
||||||
if (!entry?.filePath) return;
|
const enrich = async (entry: Record<string, unknown>) => {
|
||||||
const jsonPath = join(root, entry.filePath);
|
const filePath = entry.filePath as string | undefined;
|
||||||
const raw = JSON.parse(await readFile(jsonPath, 'utf8')) as {
|
if (!filePath || !filePath.endsWith('.json')) return entry;
|
||||||
image?: string;
|
const jsonPath = join(root, filePath);
|
||||||
exif?: Partial<PhotoExif>;
|
const raw = JSON.parse(await readFile(jsonPath, 'utf8')) as {
|
||||||
};
|
image?: string;
|
||||||
if (typeof raw.image !== 'string') return;
|
exif?: Partial<PhotoExif>;
|
||||||
const imagePath = new URL(raw.image, pathToFileURL(dirname(jsonPath) + '/'));
|
};
|
||||||
const exif = await resolvePhotoExif(raw.exif, imagePath);
|
if (typeof raw.image !== 'string') return entry;
|
||||||
ctx.store.set({ ...entry, data: { ...entry.data, exif } });
|
const imagePath = new URL(raw.image, pathToFileURL(dirname(jsonPath) + '/'));
|
||||||
}),
|
const exif = await resolvePhotoExif(raw.exif, imagePath);
|
||||||
);
|
return { ...entry, data: { ...(entry.data as object), exif } };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Object.create 保留 store 其余方法/绑定;仅拦截 set
|
||||||
|
const proxiedStore = Object.create(ctx.store);
|
||||||
|
proxiedStore.set = (entry: Record<string, unknown>) => {
|
||||||
|
pending.push(
|
||||||
|
enrich(entry)
|
||||||
|
.then((e) => origSet(e))
|
||||||
|
.catch(() => origSet(entry)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
await baseLoader.load({ ...ctx, store: proxiedStore });
|
||||||
|
await Promise.all(pending);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user