跳到主要内容

旅游照片元数据工作流

这篇文档是这个仓库里旅游相册页面的专用规范。目标只有两个:

  • 让旅游照片在灯箱里稳定展示低调的一行元数据
  • 让以后“给 AI 一份全是图片链接的 Markdown,然后让它补完整篇旅行相册”这件事可以重复执行

如果你后续继续往 docs/Travel 增加新的旅行页面,建议以这篇文档为准。

先理解当前实现

当前仓库里旅游照片元数据的链路是:

  1. 你先准备带有效 EXIF 的照片
  2. 照片被转成适合网页的 JPEG 并上传到 https://oss.nevergpdzy.com/ 下的稳定路径
  3. docs/Travel/*.mdx 里用 createTravelPhotos([...]) 按域名相对路径引用这些照片
  4. 默认运行 npm run verify:travel(内部会先执行 npm run generate:travel,再执行 npm run build
  5. 脚本 scripts/generate-travel-photo-metadata.mjs 会按这些相对路径下载 JPEG,读取 EXIF / GPS,并在有 GPS 时调用高德逆地理编码,最后生成 src/data/travelPhotoMetadata.generated.json
  6. 脚本 scripts/generate-travel-map-data.mjs 会按 docs/Travel/*.mdx 里的 TravelGallery 顺序聚合文章地点和逐张照片 GPS 点,生成 src/data/travelMap.generated.json
  7. src/components/TravelGallery/index.js 读取照片元数据;src/components/TravelMap/* 读取地图数据,把 Travel 分类页的地点总览地图和文章页的拍摄点地图渲染出来

如果 Travel 地图出现“点位整体侧漂到道路另一侧”这类问题,先看:

当前灯箱实际会优先使用下面这些字段:

字段来源是否显示
displayLocation优先来自 GPS + 高德逆地理编码简化结果;没有 GPS 或请求失败时回退 locationLabel显示
capturedAtEXIF DateTimeOriginal,没有就回退到 DateTime显示
deviceEXIF Make + Model显示
lensEXIF LensModel,iPhone 会被归一化成短标签显示
locationLabel来自旅行文章标题,作为无 GPS / 请求失败时的回退位置不直接优先显示
gps / reverseGeocodeEXIF 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. 源照片尽量保留“原片链路”

优先级建议是:

  1. 手机或相机原片
  2. AirDrop / 数据线导出 / 相册“导出未修改的原片”
  3. 明确声明保留 EXIF 的批量转换
  4. 最后才是任何社交软件、聊天软件或网页中转

不推荐直接拿下面这些来源当最终发布源:

  • 微信、QQ、微博、小红书等社交平台里转发过的图片
  • 聊天窗口里“另存为”的图片
  • 已经被网站二次压缩过的下载图
  • 在线压缩工具输出图

这些路径最容易把 DateTimeOriginalModelLensModel、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 写成“标题 + 若干小节 + 每节若干图片链接”的结构。例如:

# 武汉

## 抵达

![](https://oss.nevergpdzy.com/img_for_Typora/IMG_5867_WuHan.jpg)
![](https://oss.nevergpdzy.com/img_for_Typora/IMG_6001_WuHan.jpg)

## 街头

![](https://oss.nevergpdzy.com/img_for_Typora/IMG_6069_WuHan.jpg)
![](https://oss.nevergpdzy.com/img_for_Typora/IMG_6076_WuHan.jpg)

这样 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 接手时,应该按下面的顺序做:

  1. 先检查链接是否稳定可访问,且是否为 JPEG。
  2. 如果只有链接没有分组,先按文件顺序、场景变化和你的描述做初步分组。
  3. 如果分组仍然不够确定,再读取压缩预览图,只用来判断场景和写正文,不直接读取整批原图。
  4. 先补出一份“源 Markdown 原稿”,把标题、## 小节、正文段落和图片链接整理好。
  5. 再把这份源稿交给 scripts/create-travel-article.mjs 转成最终 docs/Travel/<slug>.mdx
  6. 生成后刷新照片元数据、地图数据,并做构建验证。
  7. 如果发现某些图片没有有效 EXIF、没有 GPS、链接失效,或者地点命名明显不稳,必须明确告诉用户。

也就是说,AI 不应该一上来就直接手写最终 MDX。更稳妥的做法是:

  1. 先整理出一份人类也能读的源稿
  2. 再用脚本生成最终 MDX
  3. 再跑验证

这样后续你想改文案、调分组、增删图片时,都能复用同一条链路,不会退回到一次性手工拼页面。

一个最贴合你当前习惯的输入模板可以是:

地点:济南 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 收到这类输入后,标准输出应该依次是:

  1. 先整理出 drafts/travel/<slug>-source.md 这类源稿
  2. 再运行:
npm run create:travel-article -- --source drafts/travel/<slug>-source.md --verify

这条命令会生成最终 docs/Travel/<slug>.mdx,随后刷新 Travel 照片元数据、地图数据并构建站点。它等价于先生成文章,再跑 npm run verify:travel

  1. 最后把结果回报给用户,包括:
  • 新生成的 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 时,优先按这个顺序分组:

  1. 按源 Markdown 的二级标题或三级标题分组
  2. 如果没有标题,按明显的场景段落分组
  3. 如果仍然没有信息,就按原始顺序拆成 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
  • 根据 slugfile_namefileNametitle 或源文件名推导输出文件名;也可以用 front matter 的 output 显式指定输出位置
  • 保留 ## 章节结构
  • 提取 oss.nevergpdzy.com 下的 JPEG 图片链接,并转成域名相对路径
  • 生成 createTravelPhotos([...])
  • 注入 TravelStoryMapTravelGallery
  • 输出最终的 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:travelnpm 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_KEYAMAP_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 的模式;这样地图能用,但安全密钥也会跟着构建产物一起暴露。

一个最小可维护的做法是:

  1. 前端构建环境只保留 AMAP_JSAPI_KEYAMAP_SERVICE_HOST=/_AMapService
  2. 服务器反向代理 /_AMapService
  3. 由服务器在代理转发时补上真正的高德安全密钥

例如 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:travel
  • npm run build

适用场景:

  • 新增了 travel 页面
  • 新增了照片
  • 新增了 GPS / 逆地理编码缓存
  • 调整了 TravelGallery 或照片元数据展示逻辑

至少检查下面几件事:

  • 页面能成功构建
  • 旅游页面能正常打开
  • 灯箱能正常打开和切换
  • 照片下方的一行元数据能正常显示
  • 宽屏和窄屏下,日期与“设备 + 镜头”换行逻辑正常

AI 接手“全是图片链接的 Markdown”时的规范动作

以后如果你上传一份 Markdown 给 AI,希望它直接补完这个仓库里的旅游页面,AI 应该按下面的顺序执行。

1. 先提取图片相对路径

如果源内容是:

![](https://oss.nevergpdzy.com/img_for_Typora/IMG_5867_WuHan.jpg)

AI 最终在仓库里应该写成:

'img_for_Typora/IMG_5867_WuHan.jpg'

不是保留整条 URL。

如果源内容是:

![](https://oss.nevergpdzy.com/Travel/JingXi/IMG_5867_WuHan.jpg)

AI 最终应该写成:

'Travel/JingXi/IMG_5867_WuHan.jpg'

2. 保留原 Markdown 的章节结构

如果源 Markdown 已经有:

  • 标题
  • 小标题
  • 段落

AI 应尽量保留这些结构,并把每一节对应成一个 createTravelPhotos([...]) 数组和一个 <TravelGallery />

3. 用标题决定地点文案

如果用户说这篇是“武汉”,那 front matter 标题应该写成类似:

title: 武汉 Wuhan

不要只写英文,否则灯箱里显示的地点文案也会跟着变成英文。

4. 不要手工伪造 EXIF

AI 可以整理页面结构,但不应该在页面里手工伪造如下字段:

  • 拍摄时间
  • 设备型号
  • 镜头型号
  • GPS

这些应该来自照片本身。如果照片元数据已经丢了,就应该接受灯箱里少显示,或者直接提醒用户这批图不适合做摄影元数据展示。

5. 读图时只读压缩预览

如果 AI 需要看照片内容,必须先读压缩预览图,不要直接读取原图。

默认规则是:

  • 第一档使用长边 1280px
  • 细节不清时再升到长边 1920px
  • 接近约 20MB 的原图或多图输入都必须先压缩、拆批
  • 预览图只用于判断场景、顺序、分组和正文描述
  • capturedAtdevicelens、GPS 仍然只能来自原始 JPEG、远端稳定 JPEG、exiftoolnpm 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 直接走当前仓库已经存在的实现,而不是临时发明另一套图片组件或另一份元数据结构。

验收清单

每次新增旅行页面后,建议至少核对下面这些点:

  • front matter 写了 sidebar_position: -YYYYMMDD,日期取旅行开始日期
  • /docs/Travel 卡片顺序、Travel 侧栏顺序和上一篇/下一篇导航都按最新旅行在前排列
  • docs/Travel/<slug>.mdx 中所有图片都只写 https://oss.nevergpdzy.com/ 后面的相对路径
  • 这些相对路径都能在远端域名下访问到
  • 仓库根目录已经配置 .env.local,且其中的 AMAP_WEB_SERVICE_KEYAMAP_JSAPI_KEYAMAP_SERVICE_HOST / AMAP_SECURITY_JS_CODE 可用
  • npm run verify:travel 成功执行
  • src/data/travelPhotoMetadata.generated.json 里能找到新增图片
  • src/data/travelMap.generated.json 已同步刷新
  • 如果用了 --force 覆盖旧页面,确认覆盖的是预期文件,并且没有丢失手工写过的正文
  • 有 GPS 的关键图片已经补齐 reverseGeocode 与简化后的 displayLocation
  • 关键图片至少能看到 capturedAtdevicelens
  • 页面构建成功
  • 灯箱中的地点文案正确
  • 分类页和文章页里的 Travel 地图能正常出现,且文章页地图会按按钮展开
  • 手机端和桌面端的灯箱布局都正常

常见故障与处理

1. 生成脚本成功了,但某些图片元数据全是空

常见原因:

  • 图片本身已经被压缩或清洗过 EXIF
  • 下载到的并不是原始 JPEG
  • 这张图本来就没有镜头或时间字段

优先处理顺序:

  1. 先用 exiftool 或系统命令确认这张 JPEG 本地是否还有元数据
  2. 再确认远端链接是不是同一份文件
  3. 如果远端不是原图,重新上传保留 EXIF 的版本

2. 页面地点显示不对

优先检查这几个东西:

  1. src/data/travelPhotoMetadata.generated.json 里这张图的 gpsreverseGeocodedisplayLocation
  2. 高德逆地理编码返回的地点是否过长,最终是否被正确简化
  3. 如果这张图没有 GPS,或逆地理编码失败,再检查文章 front matter 的 title

因为当前前端显示的地点优先来自照片 GPS 对应的逆地理编码简化结果;只有缺少 GPS 或请求失败时,才回退到文章标题。

3. 同一张图被多个旅行页面复用后报错

当前脚本会检查同一路径是否被多个旅行页面赋予了不同的地点标签。如果同一张图在两篇文章里出现,而且两篇文章标题不一致,脚本可能报:

Conflicting location labels for ...

这种情况要么:

  • 不要跨页面复用同一张图
  • 要么确保它们继承到的是同一个地点标题

4. 用户给的是 HEIC、PNG 或带查询参数的奇怪链接

当前旅游元数据链路最稳的是:

  • 公网 JPEG
  • 路径稳定
  • 能直接下载

如果不是这个形态,先把素材整理成符合要求的 JPEG,再进入旅行页面流程。

5. PowerShell 里看文档或 JSON 出现中文乱码

这个仓库在 Windows PowerShell 里偶尔会出现中文显示异常,但文件本身通常还是正常的 UTF-8。

遇到这种情况:

  • 优先用编辑器直接打开文件确认
  • 或者用 node 读取 UTF-8 内容再看

不要因为终端显示乱码,就误判文档本身已经损坏。

相关文档