diff --git a/infra/gitea/README.md b/infra/gitea/README.md index 0ec2e6d..25f0551 100644 --- a/infra/gitea/README.md +++ b/infra/gitea/README.md @@ -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, 而验收与健康检查都裸 curl 该端点;私有性靠 private 仓 + `DISABLE_REGISTRATION` 保证。 diff --git a/src/lib/exif.ts b/src/lib/exif.ts index a3a107e..76d3a70 100644 --- a/src/lib/exif.ts +++ b/src/lib/exif.ts @@ -87,34 +87,49 @@ export async function resolvePhotoExif( /** * photos 集合加载器 = glob(JSON) + EXIF 构建期补全。 - * entry.data.image 经 z.image() 校验后已是 ImageMetadata(不含源文件路径), - * 因此从 JSON 原文重读 `image` 字符串,按“相对 JSON 文件”定位源图交给 exifr。 * - * 坑:dev 模式下单文件变更由内层 glob loader 的 watcher 增量处理,会绕过本包装层, - * 改图或改 JSON 后需重启 dev server 才能补全 EXIF;SSG 构建每次全量 sync,无此问题。 + * 实现教训(2026-10-25 双日调试定位):Astro 7 内容层在 load() 结束后 + * 会按 loader 的原始写入序列做 schema 解析——包装层在 baseLoader.load() + * 之后用 ctx.store.set 补写的字段会在解析阶段被静默丢弃(exif 全空但 + * 无任何报错的根因)。因此改为**拦截写入**:代理 baseLoader 看到的 + * store,在它 set 每个条目的那一刻异步增强,增强完成的 Promise 收集 + * 后在 load 返回前 await 完毕。 */ export function photosWithExifLoader(base = './content/photos'): Loader { const baseLoader = glob({ pattern: '**/*.json', base }); return { name: 'photos-with-exif', load: async (ctx) => { - await baseLoader.load(ctx); const root = fileURLToPath(ctx.config.root); - await Promise.all( - ctx.store.keys().map(async (id) => { - const entry = ctx.store.get(id); - if (!entry?.filePath) return; - const jsonPath = join(root, entry.filePath); - const raw = JSON.parse(await readFile(jsonPath, 'utf8')) as { - image?: string; - exif?: Partial; - }; - if (typeof raw.image !== 'string') return; - const imagePath = new URL(raw.image, pathToFileURL(dirname(jsonPath) + '/')); - const exif = await resolvePhotoExif(raw.exif, imagePath); - ctx.store.set({ ...entry, data: { ...entry.data, exif } }); - }), - ); + const origSet = ctx.store.set.bind(ctx.store); + const pending: Promise[] = []; + + const enrich = async (entry: Record) => { + const filePath = entry.filePath as string | undefined; + if (!filePath || !filePath.endsWith('.json')) return entry; + const jsonPath = join(root, filePath); + const raw = JSON.parse(await readFile(jsonPath, 'utf8')) as { + image?: string; + exif?: Partial; + }; + if (typeof raw.image !== 'string') return entry; + 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) => { + pending.push( + enrich(entry) + .then((e) => origSet(e)) + .catch(() => origSet(entry)), + ); + }; + + await baseLoader.load({ ...ctx, store: proxiedStore }); + await Promise.all(pending); }, }; }