旅游照片元数据工作流
这篇文档是这个仓库里旅游相册页面的专用规范。目标只有两个:
- 让旅游照片在灯箱里稳定展示低调的一行元数据
- 让以后“给 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/Jinan/IMG_6319_Jinan.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/Jinan/IMG_6319_Jinan.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 检查或元数据生成
你最常用的实际协作方式:给地点 + 图片链接 + 几句描述
如果你平时不是先自己写完整 Markdown,而是直接把下面这些信息丢给 AI:
- 这次旅行的地点名
- 一批已经上传好的 OSS 图片链接
- 几句很短的主观描述 比如“这次整体偏海边和夜景”“前半段是抵达和街头,后半段是夜景和返程”“文字想写得克制一点,不要像攻略”
那也完全可以直接走当前仓库的标准流程。
推荐输入最少包含:
- 地点名称
例如:
济南 Jinan - 稳定可访问的 JPEG OSS 链接
最好都在
https://oss.nevergpdzy.com/下 - 至少 2 到 5 句你自己的感受、顺序提示或风格要求
例如:
- 想突出什么场景
- 照片大概按什么顺序看
- 想写得偏平静、偏叙述,还是偏日记感
- 哪些照片一定要单独成节,哪些可以放一起
AI 接手时,应该按下面的顺序做:
- 先检查链接是否稳定可访问,且是否为 JPEG。
- 如果只有链接没有分组,先按文件顺序、场景变化和你的描述做初步分组。
- 如果分组仍然不够确定,再读取压缩预览图,只用来判断场景和写正文,不直接读取整批原图。
- 先补出一份“源 Markdown 原稿”,把标题、
##小节、正文段落和图片链接整理好。 - 再把这份源稿交给
scripts/create-travel-article.mjs转成最终docs/Travel/<slug>.mdx。 - 生成后刷新照片元数据、地图数据,并做构建验证。
- 如果发现某些图片没有有效 EXIF、没有 GPS、链接失效,或者地点命名明显不稳,必须明确告诉用户。
也就是说,AI 不应该一上来就直接手写最终 MDX。更稳妥的做法是:
- 先整理出一份人类也能读的源稿
- 再用脚本生成最终 MDX
- 再跑验证
这样后续你想改文案、调分组、增删图片时,都能复用同一条链路,不会退回到一次性手工拼页面。
一个最贴合你当前习惯的输入模板可以是:
地点:济南 Jinan
要求:
- 文字不要写成攻略,偏相册叙述
- 前半段是抵达和泉水,后半段是夜景
- 语气克制一点,不要太抒情
图片:
https://oss.nevergpdzy.com/Travel/Jinan/IMG_6319_Jinan.jpg
https://oss.nevergpdzy.com/Travel/Jinan/IMG_6352_Jinan.jpg
https://oss.nevergpdzy.com/Travel/Jinan/IMG_6376_Jinan.jpg
https://oss.nevergpdzy.com/Travel/Jinan/IMG_6386_Jinan.jpg
AI 收到这类输入后,标准输出应该依次是:
- 先整理出
drafts/travel/<slug>-source.md这类源稿 - 再运行:
npm run create:travel-article -- --source drafts/travel/<slug>-source.md --verify
这条命令会生成最终 docs/Travel/<slug>.mdx,随后刷新 Travel 照片元数据、地图数据并构建站点。它等价于先生成文章,再跑 npm run verify:travel。
- 最后把结果回报给用户,包括:
- 新生成的 Travel 页面路径
- 是否成功刷新元数据和地图
- 是否构建通过
- 哪些图片缺失 EXIF / GPS / 逆地理编码
第三步:AI 把 Markdown 转成仓库里的旅行页面
AI 在这个仓库里接手时,应按下面的规则执行。
1. 页面落点
新文章应放在:
docs/Travel/<slug>.mdx
2. front matter
必须至少有:
---
title: 武汉 Wuhan
description: 这里写这一篇旅行相册的简短摘要。
sidebar_position: -20241218
hide_table_of_contents: true
---
其中:
title会影响灯箱里显示的地点名- 如果你想让地点显示成中文,标题里必须把中文地点放在前面
sidebar_position使用旅行开始日期的负数,格式是-YYYYMMDD,用于让 Travel 卡片、侧栏和上一篇/下一篇按最新旅行在前排列
sidebar_position 不要写成普通顺序号,例如 1, 2, 3...,否则以后新增最新旅行时需要整体改号。也不要为了排序改 Travel 文件名;当前地图数据和首页卡片仍使用文件名 basename 作为文章 id。
同一天多篇文章需要微调顺序时,用小数后缀,例如:
sidebar_position: -20260430.02
sidebar_position: -20260430.01
不要写 -2026043001 这类追加整数位的值,它会比普通日期小很多,导致文章长期排在不该排的位置。
3. 引入组件
页面顶部统一使用:
import TravelGallery, {createTravelPhotos} from '@site/src/components/TravelGallery';
4. 图片数组写法
图片数组应该写域名相对路径,不要直接把整条 URL 写进 createTravelPhotos([...]):
export const arrivalPhotos = createTravelPhotos([
'img_for_Typora/IMG_5867_WuHan.jpg',
'img_for_Typora/IMG_6001_WuHan.jpg',
]);
原因是:
- 这种写法可以稳定支持多目录
- 元数据脚本当前也是围绕“相对路径 -> 下载 -> 解析 EXIF”这条链路工作的
- 如果不同目录里有同名图片,相对路径可以避免冲突
5. 分组规则
AI 处理用户上传的 Markdown 时,优先按这个顺序分组:
- 按源 Markdown 的二级标题或三级标题分组
- 如果没有标题,按明显的场景段落分组
- 如果仍然没有信息,就按原始顺序拆成 2 到 4 组
6. 保留图片顺序
同一组里的图片顺序,默认不要调整。因为:
- 拍摄时间在多数情况下本来就是顺序信息
- 用户通常已经按浏览叙事排过一次
7. 文案与图片的关系
图片下面的元数据会自动显示,所以 AI 不需要把拍摄时间、设备、镜头手工写进正文。
正文更适合承担的是:
- 旅行叙述
- 场景切换
- 各组照片之间的节奏说明
8. 推荐的最小 MDX 模板
---
title: 武汉 Wuhan
description: 以相册的方式记录一次在武汉放慢脚步的短暂停留。
sidebar_position: -20241218
hide_table_of_contents: true
---
import TravelGallery, {createTravelPhotos} from '@site/src/components/TravelGallery';
export const arrivalPhotos = createTravelPhotos([
'img_for_Typora/IMG_5867_WuHan.jpg',
'img_for_Typora/IMG_6001_WuHan.jpg',
]);
export const streetPhotos = createTravelPhotos([
'img_for_Typora/IMG_6069_WuHan.jpg',
'img_for_Typora/IMG_6076_WuHan.jpg',
]);
这里写开场文字。
<TravelGallery images={arrivalPhotos} />
## 把脚步放慢一点
这里写第二段文字。
<TravelGallery images={streetPhotos} />
第四步:生成元数据清单
先在仓库根目录配置一次本地环境变量文件:
3.5 原始 Markdown / OSS 链接到 Travel MDX 的自动生成
如果你手里的是一份普通 Markdown 原稿,里面已经有正文、## 小节和 https://oss.nevergpdzy.com/ 的图片 OSS 链接,可以直接运行:
npm run create:travel-article -- --source drafts/travel/jinan-source.md
如果希望在生成 docs/Travel/*.mdx 后,顺手把照片元数据和地图数据一并刷新,运行:
npm run sync:travel-article -- --source drafts/travel/jinan-source.md
脚本会自动:
- 读取 front matter
- 根据
slug、file_name、fileName、title或源文件名推导输出文件名;也可以用 front matter 的output显式指定输出位置 - 保留
##章节结构 - 提取
oss.nevergpdzy.com下的 JPEG 图片链接,并转成域名相对路径 - 生成
createTravelPhotos([...]) - 注入
TravelStoryMap与TravelGallery - 输出最终的
docs/Travel/<slug>.mdx
scripts/create-travel-article.mjs 会保留源稿 front matter 里的 sidebar_position。因此源稿模板里要提前写好 sidebar_position: -YYYYMMDD,不要等生成 MDX 后再手动补。
几个参数边界需要记住:
--source或-s必填,指向源稿 Markdown。--sync会在写入最终 MDX 后运行npm run generate:travel。--verify会在写入最终 MDX 后依次运行npm run generate:travel和npm run build。- 如果目标文件已经存在,脚本默认拒绝覆盖;确实要覆盖时再加
--force。 - 源稿里的
output只用于指定输出路径,不会写进最终 front matter。
模板见:
scripts/templates/travel-article-source.example.md
Copy-Item .env.example .env.local
然后把 .env.local 里的高德 key 填好:
AMAP_WEB_SERVICE_KEY=你的高德 Web 服务 key
AMAP_JSAPI_KEY=你的高德 JSAPI key
AMAP_SERVICE_HOST=
AMAP_SECURITY_JS_CODE=你的高德安全密钥
说明:
scripts/generate-travel-photo-metadata.mjs会自动读取仓库根目录下的.env.local和.env.development.local- 如果当前 shell 里已经显式设置了
AMAP_WEB_SERVICE_KEY,它会优先于本地文件 AMAP_WEB_SERVICE_KEY只给照片元数据脚本用;AMAP_JSAPI_KEY和AMAP_SERVICE_HOST/AMAP_SECURITY_JS_CODE给前端 Travel 地图组件用- 前端地图通过
npm安装的@amap/amap-jsapi-loader加载,不再使用页面注入loader.js .env.local已被.gitignore忽略,不要提交真实 key- 生产环境优先使用
AMAP_SERVICE_HOST AMAP_SECURITY_JS_CODE只作为本地开发或无代理环境下的兜底方案- 如果把
AMAP_SECURITY_JS_CODE直接注入 Docusaurus 前端配置,它会进入构建产物,部署到服务器后仍然能被客户端看到 - 当前仓库已经做了保护:只要配置了
AMAP_SERVICE_HOST,构建时就不再把AMAP_SECURITY_JS_CODE注入customFields.amap
例如生产环境可以把:
AMAP_SERVICE_HOST=/_AMapService
交给你自己的服务器代理,由服务器再去请求高德服务。
如果你只是把纯静态 build/ 目录直接上传到服务器,而没有额外配置代理,那么想让别人正常加载地图,就只能继续走前端直传 AMAP_SECURITY_JS_CODE 的模式;这样地图能用,但安全密钥也会跟着构建产物一起暴露。
一个最小可维护的做法是:
- 前端构建环境只保留
AMAP_JSAPI_KEY和AMAP_SERVICE_HOST=/_AMapService - 服务器反向代理
/_AMapService - 由服务器在代理转发时补上真正的高德安全密钥
例如 nginx 最少可以先配 Web 服务代理:
location /_AMapService/ {
set $args "$args&jscode=你的安全密钥";
proxy_pass https://restapi.amap.com/;
proxy_set_header Host restapi.amap.com;
}
如果你后面启用了自定义地图样式,再按高德官方安全文档补上 /_AMapService/v4/map/styles 那一段代理规则。
页面内容完成后,如果只是想单独刷新照片元数据,运行:
npm run generate:travel-metadata
如果只想单独刷新 Travel 地图数据,运行:
npm run generate:travel-map-data
如果想一次性把 travel 的照片元数据和地图都刷新,运行:
npm run generate:travel
如果这次只是临时覆盖本地文件里的 key,也可以直接在当前 shell 里设置:
$env:AMAP_WEB_SERVICE_KEY='你的高德 Web 服务 key'
npm run generate:travel-metadata
这个命令会:
- 扫描
docs/Travel目录下的.mdx文件 - 提取所有
createTravelPhotos([...])里的 JPEG 相对路径 - 复用
src/data/travelPhotoMetadata.generated.json里已经存在的有效元数据 - 只下载新增图片,或已有记录里还没有完成 EXIF / GPS 提取的图片
- 读取这些缺失图片的 EXIF / GPS
- 对有 GPS 且还没有逆地理编码缓存的图片调用高德逆地理编码
- 自动把高德请求节流到不超过
3 次/秒 - 生成
src/data/travelPhotoMetadata.generated.json
如果命令成功,终端最后会看到类似:
Wrote N travel photo records to .../src/data/travelPhotoMetadata.generated.json (X metadata cached, Y metadata fetched, A regeo cached, B regeo requested, AMap capped at <= 3/s)
第五步:构建验证
默认推荐直接运行一条完整验证命令:
npm run verify:travel
它会顺序执行:
npm run generate:travelnpm run build
适用场景:
- 新增了 travel 页面
- 新增了照片
- 新增了 GPS / 逆地理编码缓存
- 调整了
TravelGallery或照片元数据展示逻辑
至少检查下面几件事:
- 页面能成功构建
- 旅游页面能正常打开
- 灯箱能正常打开和切换
- 照片下方的一行元数据能正常显示
- 宽屏和窄屏下,日期与“设备 + 镜头”换行逻辑正常
AI 接手“全是图片链接的 Markdown”时的规范动作
以后如果你上传一份 Markdown 给 AI,希望它直接补完这个仓库里的旅游页面,AI 应该按下面的顺序执行。
1. 先提取图片相对路径
如果源内容是:

AI 最终在仓库里应该写成:
'img_for_Typora/IMG_5867_WuHan.jpg'
不是保留整条 URL。
如果源内容是:

AI 最终应该写成:
'Travel/Jinan/IMG_6319_Jinan.jpg'
2. 保留原 Markdown 的章节结构
如果源 Markdown 已经有:
- 标题
- 小标题
- 段落
AI 应尽量保留这些结构,并把每一节对应成一个 createTravelPhotos([...]) 数组和一个 <TravelGallery />。
3. 用标题决定地点文案
如果用户说这篇是“武汉”,那 front matter 标题应该写成类似:
title: 武汉 Wuhan
不要只写英文,否则灯箱里显示的地点文案也会跟着变成英文。
4. 不要手工伪造 EXIF
AI 可以整理页面结构,但不应该在页面里手工伪造如下字段:
- 拍摄时间
- 设备型号
- 镜头型号
- GPS
这些应该来自照片本身。如果照片元数据已经丢了,就应该接受灯箱里少显示,或者直接提醒用户这批图不适合做摄影元数据展示。
5. 读图时只读压缩预览
如果 AI 需要看照片内容,必须先读压缩预览图,不要直接读取原图。
默认规则是:
- 第一档使用长边
1280px - 细节不清时再升到长边
1920px - 接近约
20MB的原图或多图输入都必须先压缩、拆批 - 预览图只用于判断场景、顺序、分组和正文描述
capturedAt、device、lens、GPS 仍然只能来自原始 JPEG、远端稳定 JPEG、exiftool或npm run generate:travel-metadata
6. 生成后默认跑一条验证命令
如果仓库已经在根目录配置好 .env.local,AI 在改完 travel 文件之后,默认应该跑:
npm run verify:travel
只有在明确只想刷新元数据、不做构建验证时,才单独运行:
npm run generate:travel-metadata
如果没有跑 npm run verify:travel,通常就不能算 travel 流程完成。
推荐给未来 AI 的直接指令模板
以后你可以直接把下面这段要求连同 Markdown 一起给 AI:
请把我上传的 Markdown 转成这个仓库里的旅游相册页面,要求:
1. 新文件放到 docs/Travel 下,使用 MDX。
2. front matter 的 title 先写中文地点,再写英文地点。
3. front matter 必须写 `sidebar_position: -YYYYMMDD`,日期取旅行开始日期,用于让 Travel 卡片、侧栏和上一篇/下一篇按最新旅行在前排序。
4. 保留原 Markdown 的章节结构,并按章节生成 createTravelPhotos([...])。
5. createTravelPhotos 里只保留 `https://oss.nevergpdzy.com/` 后面的相对路径,不要保留完整 URL。
6. 使用 `TravelGallery` 渲染每一组图片。
7. 在第一组 `TravelGallery` 之前插入 `<TravelStoryMap />`,让文章页自动复用默认折叠的拍摄点地图。
8. 不要手工编造拍摄时间、设备、镜头、GPS。
9. 如果需要看图,先生成压缩预览图再读;默认长边 1280px,必要时升到 1920px,不要直接读取原图。
10. 默认使用仓库根目录 `.env.local` 里的 `AMAP_WEB_SERVICE_KEY`、`AMAP_JSAPI_KEY`、`AMAP_SERVICE_HOST` / `AMAP_SECURITY_JS_CODE`。
11. 改完后运行 `npm run verify:travel`。
12. 如果发现照片没有有效 EXIF,请明确告诉我哪些图缺失元数据。
13. 如果要覆盖已有 Travel 页面,必须确认后再使用 `--force`。
这段模板的目的,是让 AI 直接走当前仓库已经存在的实现,而不是临时发明另一套图片组件或另一份元数据结构。