旅游照片元数据工作流
这篇文档是这个仓库里旅游相册页面的专用规范。目标只有两个:
- 让旅游照片在灯箱里稳定展示低调的一行元数据
- 让以后“给 AI 一份全是图片链接的 Markdown,然后让它补完整篇旅行相册”这件事可以重复执行
如果你后续继续往 docs/Travel 增加新的旅行页面,建议以这篇文档为准。
先理解当前实现
当前仓库里旅游照片元数据的链路是:
- 你先准备带有效 EXIF 的照片
- 照片被转成适合网页的 JPEG 并上传到
https://oss.nevergpdzy.com/下的稳定路径 - 在
docs/Travel/*.mdx里用createTravelPhotos([...])按域名相对路径引用这些照片 - 默认运行
npm run verify:travel(内部会先执行npm run generate:travel,再执行npm run build) - 脚本
scripts/generate-travel-photo-metadata.mjs会按这些相对路径下载 JPEG,读取 EXIF / GPS,并在有 GPS 时调用高德逆地理编码,最后生成src/data/travelPhotoMetadata.generated.json - 脚本
scripts/generate-travel-map-data.mjs会按docs/Travel/*.mdx里的TravelGallery顺序聚合文章地点和逐张照片 GPS 点,生成src/data/travelMap.generated.json src/components/TravelGallery/index.js读取照片元数据;src/components/TravelMap/*读取地图数据,把 Travel 分类页的地点总览地图和文章页的 拍摄点地图渲染出来
如果 Travel 地图出现“点位整体侧漂到道路另一侧”这类问题,先看:
当前灯箱实际会优先使用下面这些字段:
| 字段 | 来源 | 是否显示 |
|---|---|---|
displayLocation | 优先来自 GPS + 高德逆地理编码简化结果;没有 GPS 或请求失败时回退 locationLabel | 显示 |
capturedAt | EXIF DateTimeOriginal,没有就回退到 DateTime | 显示 |
device | EXIF Make + Model | 显示 |
lens | EXIF LensModel,iPhone 会被归一化成短标签 | 显示 |
locationLabel | 来自旅行文章标题,作为无 GPS / 请求失败时的回退位置 | 不直接优先显示 |
gps / reverseGeocode | EXIF GPS 与高德逆地理编码的规范化缓存 | 不直接显示 |
另外还会生成 hasMetadata,用于标记这一张图是否至少提取到了部分可用元数据。
需要记住的几个关键事实
1. 页面显示的地点优先来自逆地理编码,缺 GPS 时才回退标题
当前页面展示的地点文字优先来自照片 GPS 经过高德逆地理编码后的简化结果,不再一律使用文章标题。
只有在下面这些情况下,才会回退到文章 front matter 里的 title:
- 照片本身没有可用 GPS
- 逆地理编码请求失败
- 逆地理编码虽然成功,但没有得到可读的景点名或行政区组合
例如:
---
title: 武汉 Wuhan
---
最终灯箱里会显示 武汉。
再例如:
---
title: 黄龙与九寨沟 Jiuzhai Valley
---
最终灯箱里会显示 黄龙与九寨沟。
所以如果你想让地点显示正确,最重要的是把旅行文章标题写对,而不是指望照片 GPS 决定前端文案。
2. 当前元数据脚本只处理 JPEG,标准输入是域名相对路径
当前脚本识别的是 .jpg / .jpeg 图片引用。标准写法是域名相对路径,例如:
img_for_Typora/IMG_5867_WuHan.jpg
JingXi/IMG_5867_WuHan.jpg
Travel/JingXi/IMG_5867_WuHan.jpg
也就是说:
- 旅游页面里应该引用 JPEG 文件
- 新文章应该优先写域名相对路径,而不是只写文件名
- 如果你的原图是
HEIC/HEIF,要先转成 JPEG
为了兼容旧文章,组件仍然能接受只写文件名的写法,但它会自动把:
IMG_5867_WuHan.jpg
解释成:
img_for_Typora/IMG_5867_WuHan.jpg
这只是兼容规则,不是新的 推荐规范。
HEIF 转 JPEG 的具体命令可以继续参考:
3. iPhone 镜头名称会被压缩成短标签
当前前端和生成脚本都对 iPhone 镜头做了短标签归一化,最终只保留下面这几种:
超广角主摄长焦前置
如果不是 iPhone,或者镜头型号无法命中这些规则,就会保留原始镜头字段。
用户上传前的准备规范
如果你希望 AI 后续能把照片元数据正确带出来,上传前请先满足下面这些前提。
1. 源照片尽量保留“原片链路”
优先级建议是:
- 手机或相机原片
- AirDrop / 数据线导出 / 相册“导出未修改的原片”
- 明确声明保留 EXIF 的批量转换
- 最后才是任何社交软件、聊 天软件或网页中转
不推荐直接拿下面这些来源当最终发布源:
- 微信、QQ、微博、小红书等社交平台里转发过的图片
- 聊天窗口里“另存为”的图片
- 已经被网站二次压缩过的下载图
- 在线压缩工具输出图
这些路径最容易把 DateTimeOriginal、Model、LensModel、GPS 等信息直接抹掉。
2. 尽量一篇旅行文章只对应一个地点标题
因为 locationLabel 仍然由文章标题继承,并且它现在承担“无 GPS 时的回退位置”职责,所以一篇 docs/Travel/*.mdx 最好只描述一个主地点。否则:
- 页面标题会变得模糊
- 灯箱里每一张图都会显示同一个地点名
- 没有 GPS 的照片会统一回退成这个标题地点
3. 文件名尽量稳定、可读、一次定好
当前仓库里旅游照片的末级文件名大多类似:
IMG_5867_WuHan.jpg
IMG_7107_HongKong.jpg
IMG_0248_JiuzhaiValley.jpg
推荐继续保持这种风格:
- 保留相机原始序号
- 在后缀里带上地点英文标识
- 上传之后不要再反复重命名
一旦文件名变了,你需要同时更新:
- 远端图片地址
docs/Travel/*.mdx中的引用- 重新生成元数据清单
如果你用了多级目录,那路径本身也要稳定。例如:
img_for_Typora/IMG_5867_WuHan.jpg
Travel/JingXi/IMG_5867_WuHan.jpg
4. 图片链接最好已经是稳定的公网地址
如果你下次给 AI 的是“全是图片链接的 Markdown”,最好这些链接已经满足:
- 能直接在浏览器打开
- 是稳定地址,不带临时鉴权参数
- 最终落在
https://oss.nevergpdzy.com/这个域名下 - 文件扩展名是
.jpg或.jpeg
如果你给的是临时链接、短链接、重定向链、网盘鉴权链接,AI 可以改文档,但元数据脚本不一定能稳定拉取到最终文件。
上传前如何检查照片有没有被压缩掉有效元数据
这里的“有效元数据”,对当前仓库来说,至少指下面这几项里有一部分仍然存在:
- 拍摄时间
DateTimeOriginal - 设备型号
Model - 镜头型号
LensModel - 可选的 GPS 信息
如果这些字段全没了,灯箱里通常就只会剩地点,或者完全没有可显示的摄影信息。
先看最直观的异常信号
如果一张图出现下面这些现象,通常就要怀疑它已经不是原始带 EXIF 的版本了:
- 文件体积明显异常小,只有几百 KB,但本来应该是手机原图
- 分辨率被压到固定长边,比如 1280、1600、2048 一类
- 拍摄时间、设备型号、镜头型号全部缺失
- 同一组照片里只有极少数照片还带设备信息
- 明明当时开了定位,但所有照片都完全没有 GPS
如果你拍的是 iPhone 原片,而你手上的 JPEG 普遍已经变成“体积明显变小 + 尺寸被统一压缩 + EXIF 基本空了”,基本可以认为这是中间平台处理过的版本。
最推荐的检查方式:exiftool
如果你机器里已经装了 exiftool,优先用它。它最适合检查“元数据还剩多少”。
检查单张图:
exiftool -DateTimeOriginal -Model -LensModel -GPSLatitude -GPSLongitude -ImageWidth -ImageHeight IMG_0001.jpg
如果想顺带看是否有后期软件痕迹,也可以再加一个 Software:
exiftool -DateTimeOriginal -Model -LensModel -GPSLatitude -GPSLongitude -Software IMG_0001.jpg
理想状态一般是:
DateTimeOriginal有值Model有值LensModel有值- 如果当时开启了定位,
GPSLatitude/GPSLongitude也有值
如果只剩像素尺寸,而拍摄时间、设备、镜头都没了,就不要拿这份图去做旅游元数据展示。
Windows 无额外工具时的检查方式
如果你没有 exiftool,可以直接用 PowerShell 读 EXIF 的几个关键字段:
Add-Type -AssemblyName System.Drawing
function Read-ExifAscii($image, $id) {
$prop = $image.PropertyItems | Where-Object Id -eq $id | Select-Object -First 1
if (-not $prop) {
return $null
}
return ([System.Text.Encoding]::ASCII.GetString($prop.Value)).Trim([char]0)
}
$path = (Resolve-Path .\IMG_0001.jpg).Path
$image = [System.Drawing.Image]::FromFile($path)
[pscustomobject]@{
DateTimeOriginal = Read-ExifAscii $image 0x9003
Model = Read-ExifAscii $image 0x0110
LensModel = Read-ExifAscii $image 0xA434
Width = $image.Width
Height = $image.Height
}
$image.Dispose()
如何判断结果是否合格:
DateTimeOriginal有值,说明拍摄时间大概率还在Model有值,说明设备信息还在LensModel有值,说明镜头信息大概率还在- 三项都空,基本可以判定这份 JPEG 的关键 EXIF 已经被剥离了
macOS 无额外工具时的检查方式
macOS 可以先用内置的 mdls 做一个快速筛查:
mdls -name kMDItemContentCreationDate \
-name kMDItemAcquisitionMake \
-name kMDItemAcquisitionModel \
-name kMDItemPixelWidth \
-name kMDItemPixelHeight \
IMG_0001.jpg
如果这里已经完全看不到拍摄时间和设备型号,那这张图大概率也不适合作为旅游元数据源。
需要注意的是:
mdls更适合快速筛查- 镜头和 GPS 之类更完整的 EXIF,还是
exiftool更稳
图形界面也可以做第一轮检查
如果你不想先跑命令,也可以先用系统界面看一眼:
- Windows:右键图片,打开“属性 -> 详细信息”
- macOS:选中文件,按空格预览或“显示简介”
如果这里已经完全看不到拍摄日期、设备、尺寸等信息,就没必要再指望仓库脚本能从这张图里提取到完整数据。
这个仓库里推荐的完整操作流程
下面这套流程,适合以后继续增加新的旅行页面。
第一步:准备照片
要求:
- 尽量保留 EXIF
- 最终使用 JPEG
- 文件名和路径稳定
- 图片已经上传到
https://oss.nevergpdzy.com/下的稳定地址 - 不再使用已退役 picture 域名;旧素材清单进入 Travel 流程前必须先规范到
https://oss.nevergpdzy.com/
如果原始格式是 HEIC,请先参考 苹果 HEIF 照片网站展示指南 转成 JPEG,再做后续步骤。
第二步:准备给 AI 的 Markdown
如果你希望以后 AI 能稳定接手,最好把源 Markdown 写成“标题 + 若干小节 + 每节若干图片链接”的结构。例如:
# 武汉
## 抵达


## 街头


这样 AI 可以直接按照小节来拆分 createTravelPhotos([...]) 数组。
如果你给的是一整坨没有标题、只有图片 URL 的 Markdown,AI 也能处理,但它就只能靠图片顺序、路径和文件名猜测分组,结果会不如显式分段稳定。
如果 AI 需要先看图再决定分组、标题或正文,默认不要直接读取原图:
- 先生成压缩预览图,再让 AI 阅读
- 直接读取原图容易触发对话上下文大小限制,经验风险线约在
20MB左右 - 默认第一档预览图使用长边
1280px - 如果
1280px看不清小字、路牌、菜单、票据或关键细节,再升到长边1920px - 多图任务要分批压缩、分批阅读,不要一次读取整组原图
- 压缩预览图只用于 AI 理解画面、分组和写文案,不用于 EXIF 检查或元数据生成