01项目概述
SLX 个人档案 是一座长期个人数字档案站,包含首页、摄影、地图、文字、探索、关于六个页面。核心设计理念:照片文件本身就是长期数据库,网站只是把这些信息重新组织成不同的浏览维度。
系统分三层:
- 内容层:照片文件(EXIF/IPTC/XMP 元数据)、Markdown 文章、JSON 策展数据、城市坐标表。
- 生成层:Node.js 脚本读取内容,生成图片衍生图、数据 JSON 和静态 HTML。
- 观看层:各页面用自己的交互方式呈现同一批内容。
全流程三个阶段:
[桌面图库] → npm run import-photos → [original]
↓
npm run images → [thumbs / large / hero]
↓
npm run data → [photo-data.json]
↓
npm run update → [dist/*.html]
02系统架构
六个房间
| 房间 | 来源 | 说明 |
|---|---|---|
| 首页 | build-home.js | Hero 轮播 + 四个 Tab + Portal Cards |
| 摄影 | build-gallery-page.js | 三行筛选 + 密铺画廊 + 灯箱 |
| 地图 | build-map.js / build-map-wall.js / build-map-gallery.js | 点位版、照片墙版、城市筛选页 |
| 文字 | build-journal.js | 文章列表 + 游记 + 自定义 Markdown 指令 |
| 探索 | build-explore.js | 系统说明与实验片段 |
| 关于 | 手动维护 | 不走统一模板 |
数据流
全站共享同一份照片元数据:dist/assets/original/*.jpg 的 EXIF/IPTC/XMP → build-photo-data.js → 三份数据 JSON,再由此生成各页面。
EXIF/IPTC/XMP (original/*.jpg)
│
▼
build-photo-data.js
│
├──→ photo-data.json ──→ 摄影页、灯箱、精选、年度
├──→ map-data.json ──→ 地图页、照片墙
└──→ cities.json ──→ 地图城市列表
分类、城市、年份的筛选按钮均从当前照片数据统计生成,不维护旧的硬编码列表。新增一个城市或改动关键词后,筛选入口自动跟随数据变化。
03技术栈
| 层级 | 技术 | 说明 |
|---|---|---|
| 静态生成 | Node.js | 纯脚本生成,无框架 |
| 图片处理 | sharp | original → thumbs / large / hero |
| EXIF 读取 | exiftool + exifr | 主读 exiftool,回退 exifr |
| Markdown | gray-matter + marked | frontmatter 解析 + 渲染 |
| 地图 | 原生 SVG | GeoJSON + Mercator 投影,无在线瓦片 |
| CSS | Tailwind CDN + archive.css | 基础层 + 项目定制层(~5000 行) |
| 轮播 | Swiper 11(本地 vendor) | Coverflow Effect |
| 轻交互 | Alpine.js | Tab 切换、专题展开等 |
| 灯箱 | Fancybox 5 | 图片点击放大 |
技术选型的核心标准是可恢复性:没有后端数据库、没有前端构建框架、不依赖远端构建。几年后依赖变化时,内容仍然可读、可重建、可迁移。
04图片管线
元数据工作流
日常维护从 dist/assets/original/*.jpg 开始——不是从桌面源图库。在 Adobe Bridge 中编辑 EXIF/IPTC 字段,然后运行 npm run update。
| 信息 | EXIF/IPTC 字段 | 网站用途 |
|---|---|---|
| 拍摄时间 | DateTimeOriginal → CreateDate → DateCreated → ModifyDate | 年份筛选、时间排序、caption 年份 |
| 城市 | City | 城市筛选、地图聚合 |
| 子位置 | Sub-location / Location | caption 子位置 |
| 关键词 | IPTC:Keywords / XMP-dc:Subject | 第一个非 # 词 = 题材;首 # 词 = 二级标题 |
| 相机/镜头 | Model、LensModel | 照片详情 |
| 拍摄参数 | ISO、FNumber、ExposureTime、FocalLength | 照片详情 |
Caption 规则:默认格式为 "年份 · 城市 · 子位置"。特殊分类有不同规则——动物生态和胶片摄影使用 "年份 · 二级标题",仰望星空 在有二级标题时也使用 "年份 · 二级标题"。这让不同题材用更适合的语言出现:城市风光强调地点,动物生态强调物种,胶片和星空强调作品标题。
少量例外通过 content/photo-meta.json 手动覆盖。覆盖层存在,但不作为日常主数据源。
图片导入
npm run import-photos 从桌面图库导入照片。流程:清空 original/、thumbs/、large/、hero/ → 将桌面图库图片压缩为短边 2160px JPG 写入 original/ → herophoto/ 子目录写入 hero-list.json → premium/ 子目录写入 home-featured.json(保留已有描述)。默认源目录:/Users/shenlinxuan/Desktop/网站照片。
衍生图生成
npm run images 使用 sharp 从 original 生成三类 WebP 衍生图:
| 输出 | 规格 | 用途 |
|---|---|---|
thumbs/*.webp | 720px 宽 | 网格缩略图、卡片预览,速度优先 |
large/*.webp | 短边 2160px | Fancybox 灯箱,细节优先 |
hero/*.webp | 1920px × 16:9 居中裁剪 | 首页轮播,目标 200–420KB,质量自适应 |
管线保持幂等:输出文件比源文件新时跳过。新增 10 张图只处理这 10 张,符合长期小步更新的节奏。
数据构建
npm run data 使用 exiftool 批量读取 original/*.jpg 的 EXIF/IPTC/XMP(回退 exifr),用 sharp 读取尺寸,应用 photo-meta.json 覆盖,生成四份 JSON:
| 文件 | 内容 |
|---|---|
photo-data.json | 全站照片单一真相源——名称、标题、分类、年份、尺寸、路径、caption、城市、坐标、相机参数 |
map-data.json | 按城市聚合的照片列表 |
cities.json | 城市名称、照片数量、省份、坐标、首图 |
on-this-day.json | 按 MM-DD 聚合的"历史上的今天" |
每张照片在系统里不只是"图片路径",而是一条完整记录——它知道自己的年份、城市、地点、题材、宽高比、各尺寸图片路径、caption、相机参数和经纬度。同一张照片可以出现在摄影页、地图页、城市页、首页精选和年度统计中,不需要到处维护说明。
05技术亮点
以下三项是本站的核心技术设计,每篇有独立的详细说明:
EXIF 驱动架构
核心判断:把方向反过来——先写事实进照片,再让网站读取事实。拍摄时间、城市、子位置、题材分类、二级标题、相机参数都来自 EXIF/IPTC/XMP 元数据,不依赖 CMS、数据库或文件名解析。构建脚本将每张照片展开为结构化记录写入 photo-data.json,这是全站所有页面的共同数据源。
密铺画廊布局引擎
摄影页不使用普通瀑布流,而是自研 Justified Gallery 引擎。核心算法:贪心分行(每行累加宽高比找到最接近目标行高的切片点)、相邻竖图惩罚(避免竖图视觉扎堆)、末行保护(1–2 张时不强制填满)。259 行原生 JavaScript,根据设备宽度自适应行高。
照片墙地图放置算法
地图照片墙版在全幅 SVG 中国地图上为每个城市分配拍立得照片组的位置——一个带多因子评分和约束检测的二维放置问题。需避开地图 polygon 安全区、界面控件保留区和已放置照片组,同时优先靠近城市图钉位置。
06页面构建
npm run update 依次执行 11 个构建脚本,每个脚本只处理对应页面:
| 脚本 | 职责 |
|---|---|
build-photo-data.js | 数据构建核心,输出 photo-data / map-data / cities / on-this-day |
build-home-topics.js | 首页专题数据——历史上的今天 + 自动生成专题 + 保留手动专题 |
build-home.js | 首页策展层——Hero 轮播 + 四个 Tab + Portal Cards |
build-gallery-page.js | 摄影页——筛选按钮 + 密铺画廊 |
build-map.js / build-map-wall.js / build-map-gallery.js | 地图页——点位版 / 照片墙版 / 城市页 |
build-journal.js | 文字页——文章列表 + 游记 + Markdown 指令解析 |
build-explore.js | 探索页——卡片 + 详情页 + 外部链接 |
build-year.js | 年度页——统计 + blurb + hero 图 |
其中首页是策展层——Hero 图片、精选内容和近期内容来自人工编辑的 JSON(hero-list.json、home-featured.json、home-recent.json),不由自动数据支配。专题 Tab 和年度 Tab 则由自动数据驱动。两种速度并存:照片元数据可快速更新,首页策展慢慢编辑。
共享模块(scripts/shared/)提供页面外壳、导航、路径前缀、游记组件、地图控件等跨脚本复用能力。
07内容格式
各类内容源对应不同的文件格式和 frontmatter 字段:
| 内容类型 | 位置 | 格式 | 核心字段 |
|---|---|---|---|
| 文字文章 | content/journal/*.md | Markdown + frontmatter | title, order, date/dateStart+dateEnd, kind, hero |
| 探索卡片 | content/explore/*.md | Markdown + frontmatter | title, date, link(外链则跳过详情页), status |
| 年度页 | content/year/*.md | Markdown + frontmatter | title, blurb, hero |
| 专题清单 | content/topics/*.json | JSON 数组 | 照片文件名列表 |
| 游记行程 | content/trips/*.json | JSON | cities, segments, photos |
| 首页精选 | content/home-featured.json | JSON 数组 | image, layout, text, meta |
| 首页近期 | content/home-recent.json | JSON | photos + writings 数组 |
| 城市坐标 | content/city-coordinates.json | JSON | 城市名 → { lat, lng, province } |
Markdown 文章支持自定义指令扩展:::trip-map[trip-id] 渲染行程地图 SVG、::trip-gallery[trip-id] 渲染行程照片网格、::photo[filename] 引用单张照片。
08运行时
大部分内容在构建时已生成,浏览器只负责必须现场完成的交互:
| 脚本 | 职责 |
|---|---|
gallery-layout.js | 密铺画廊布局引擎——按容器宽度取目标行高、贪心分行、相邻竖图惩罚、末行保护,窗口 resize 时 300ms 防抖重绘 |
map.js | 地图点位版——Mercator 投影、滚轮缩放(1–6x)、拖拽平移、hover 预览卡、点击跳转城市页,移动端双指缩放 |
| 首页内联脚本 | 硬顶吸滚动、Tab 切换动画、专题展开/收起、回到顶部、加载进入动效 |
筛选逻辑也在前端完成——读者即时切换题材、地点、年份时不用回服务端,URL 同步筛选状态方便分享。轻交互由 Alpine.js 承担(Tab 切换、专题展开),重型交互(画廊布局、地图缩放)由原生 JS 处理。
09部署与维护
部署
Vercel 静态部署:Framework Preset = Other,Build/Install Command 留空,Output Directory = dist。部署前在本地运行构建,将 dist/ 成品提交到 Git。
构建命令
| 命令 | 用途 |
|---|---|
npm run import-photos | 全量重新导入桌面图库 |
npm run images | 生成 thumbs / large / hero |
npm run data | 扫描 EXIF,生成数据 JSON |
npm run update | data + 全部页面构建 |
npm run build | images + update(全量换图) |
维护入口
| 操作 | 命令 |
|---|---|
| 改 EXIF 元数据 | 仅 npm run update |
| 新增/替换图片 | npm run images && npm run update |
| 调整 Hero / 精选 / 近期 | 编辑 content/ 对应 JSON → npm run update |
| 新增文字 / 探索 / 年度 | 新增 content/ 对应文件 → npm run update |
| 调整导航 | gallery-config.js + about_me.html → npm run update |
常见问题
| 现象 | 排查 |
|---|---|
| Bridge 改了信息但网站没变 | 确认改的是 dist/assets/original/ 的文件,不是桌面源图库 |
| 新图缩略图是旧的 | 运行 npm run images && npm run update |
| 地图缺城市 | 运行 npm run data 看缺失项,补 city-coordinates.json |
| 分类/caption 不对 | 优先改 EXIF;少量例外用 photo-meta.json 覆盖 |
| 画廊 / 地图 / Hero 空白 | 检查对应数据 JSON 和运行时脚本是否正确加载 |
| Vercel 部署后没变 | 确认已提交 dist/ 到 Git |
10设计决策
- 照片优先于页面。照片文件保存长期事实,页面只是事实的某种呈现。照片不被页面结构锁死。
- 数据优先于硬编码。分类、城市、年份从当前照片统计生成,不维护过时列表。
- 策展优先于全自动。首页 Hero、精选和近期保留人工判断,不让算法决定第一印象。
- 本地地图优先于在线瓦片。站点的核心需求是城市与照片的空间索引,不需要绑定第三方服务。
- 静态生成优先于后台系统。减少运行时依赖,让每次发布都在 Git 中留下可恢复的完整成品。
- 管线保持幂等。只处理新增和修改的文件,符合个人站点长期小步更新的节奏。
- 复杂度各归其位。图片处理、元数据、地图、策展、视觉各自独立,可替换但不互相吞没。