这座档案站最核心的设计决定,不是选了哪个框架或哪种渲染方式,而是一个数据问题:照片的元数据应该住在哪里?
我的回答是:住在照片文件里。
核心想法
传统做法是在 CMS 后台、电子表格或配置文件里维护图片的标题、地点、年份和分类。照片是一张图,信息是另一份表。两份数据需要手动同步,迟早会不一致。
这座站的做法反过来。照片的 EXIF/IPTC/XMP 元数据——拍摄时间、城市、子位置、关键词、相机参数——就是网站的数据源。我不在 JSON 里重复写一遍"这张图拍于 2025 年的南京",而是让生成脚本直接去读照片文件里的这些信息。
网站只是元数据的多种投影。
数据流:辐辏模型
EXIF/IPTC/XMP (original/*.jpg)
│
▼
build-photo-data.js
│
├──→ photo-data.json ──→ 摄影页、灯箱、精选、年度
├──→ map-data.json ──→ 地图页、照片墙
└──→ cities.json ──→ 地图城市列表
photo-data.json 是辐辏的中心节点。每张照片被展开为一条扁平记录:名称、标题、分类、关键词、年份、时间戳、宽高、宽高比、四套图片路径、地点、caption、城市、子位置、省份、GPS、相机、镜头、ISO、光圈、快门、焦距。
所有页面从同一份 JSON 读取。分类筛选按钮从当前照片数据统计生成,城市筛选从当前照片数据统计生成,年份筛选从当前照片数据统计生成。没有硬编码的列表需要维护。
这意味着:新增一张照片,只要 EXIF 写对了,所有页面自动更新。删掉一张照片,所有引用自动消失。不存在"照片删了但筛选按钮还在"的不一致。
三层 EXIF 读取策略
系统在不同场景使用三种不同的 EXIF 读取方式:
| 层级 | 工具 | 场景 | 特点 |
|---|---|---|---|
| 轻量层 | 手写二进制解析器 | 目录扫描时快速排序 | 只读日期,只读文件头 192KB |
| 主力层 | exiftool | 数据构建 | 最准确的元数据工具,批量 JSON 输出 |
| 回退层 | exifr | exiftool 不可用时 | 纯 JavaScript,无外部依赖 |
手写解析器在 gallery-utils.js 中实现。它直接读取 JPEG 的 APP1 段,解析 TIFF 字节序(MM 大端 / II 小端),遍历 IFD0 条目,跳转到 EXIF Sub-IFD,提取 DateTimeOriginal(0x9003)、DateTimeDigitized(0x9004)和 ModifyDate(0x0132)三个日期标签。
这个解析器不依赖任何第三方库,只用于"快速拿到拍摄日期来排序"的轻量场景。完整的数据构建仍然交给 exiftool。
双策略回退保证了构建的鲁棒性:exiftool 是元数据领域的金标准,但它是一个外部二进制;exifr 是纯 JavaScript,在 CI 环境或未安装 exiftool 的系统上仍然能正常工作。
分类即关键词
分类不维护单独的分类表。IPTC 关键词中的第一个非 # 词就是分类。# 开头的词是二级标签。
这个决定带来两个好处:
第一,摄影页的筛选按钮从当前照片数据统计生成。新增一个分类不需要改代码,只需要在 Bridge 里给照片加上对应关键词。
第二,部分分类有特殊的展示规则。动物生态 的 caption 用物种名替换城市(2023 · 蓑羽鹤 而非 2023 · 烟台);胶片摄影 用二级标题替换城市;仰望星空 在有二级标签时同样替换。
默认 caption: 2025 · 南京 · 保利大剧院
动物生态: 2023 · 蓑羽鹤
胶片摄影: 2024 · Portra 400
仰望星空+二级: 2025 · 银河拱桥
这些规则编码在 build-photo-data.js 的 secondaryCaptionFor() 函数中。
两种速度
系统刻意区分快数据和慢数据。
快数据(自动从 EXIF 生成):摄影页全量照片、地图页城市聚合、城市筛选页、年份统计、分类列表。每次 npm run update 都会重建。
慢数据(人工策展):首页 Hero 轮播、精选描述、近期内容。由 content/hero-list.json、content/home-featured.json、content/home-recent.json 手动维护。
图库可以频繁更新,首页精选可以慢慢编辑。两种速度互不干扰。策展层引用照片的方式是文件名匹配——图片的元数据仍然来自 EXIF,但"哪些图上首页、以什么顺序出现、描述写什么"由我决定。
可迁移性
这套架构最让我安心的地方是可迁移性。
如果有一天不再用这个网站,照片里的时间、城市、位置和关键词仍然存在。EXIF 是国际标准,任何图片管理工具都能读取。数据不会因为网站框架过时而丢失。
如果有一天不用 Bridge,生成脚本仍然能把元数据变成地图、筛选器、caption 和精选页面。数据和展示分离。
迁移时只需要带走照片文件。所有信息都跟着走。