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.jsHero 轮播 + 四个 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纯脚本生成,无框架
图片处理sharporiginal → thumbs / large / hero
EXIF 读取exiftool + exifr主读 exiftool,回退 exifr
Markdowngray-matter + markedfrontmatter 解析 + 渲染
地图原生 SVGGeoJSON + Mercator 投影,无在线瓦片
CSSTailwind CDN + archive.css基础层 + 项目定制层(~5000 行)
轮播Swiper 11(本地 vendor)Coverflow Effect
轻交互Alpine.jsTab 切换、专题展开等
灯箱Fancybox 5图片点击放大

技术选型的核心标准是可恢复性:没有后端数据库、没有前端构建框架、不依赖远端构建。几年后依赖变化时,内容仍然可读、可重建、可迁移。


04图片管线

元数据工作流

日常维护从 dist/assets/original/*.jpg 开始——不是从桌面源图库。在 Adobe Bridge 中编辑 EXIF/IPTC 字段,然后运行 npm run update

信息EXIF/IPTC 字段网站用途
拍摄时间DateTimeOriginalCreateDateDateCreatedModifyDate年份筛选、时间排序、caption 年份
城市City城市筛选、地图聚合
子位置Sub-location / Locationcaption 子位置
关键词IPTC:Keywords / XMP-dc:Subject第一个非 # 词 = 题材;首 # 词 = 二级标题
相机/镜头ModelLensModel照片详情
拍摄参数ISOFNumberExposureTimeFocalLength照片详情

Caption 规则:默认格式为 "年份 · 城市 · 子位置"。特殊分类有不同规则——动物生态胶片摄影使用 "年份 · 二级标题",仰望星空 在有二级标题时也使用 "年份 · 二级标题"。这让不同题材用更适合的语言出现:城市风光强调地点,动物生态强调物种,胶片和星空强调作品标题。

少量例外通过 content/photo-meta.json 手动覆盖。覆盖层存在,但不作为日常主数据源。

图片导入

npm run import-photos 从桌面图库导入照片。流程:清空 original/thumbs/large/hero/ → 将桌面图库图片压缩为短边 2160px JPG 写入 original/herophoto/ 子目录写入 hero-list.jsonpremium/ 子目录写入 home-featured.json(保留已有描述)。默认源目录:/Users/shenlinxuan/Desktop/网站照片

衍生图生成

npm run images 使用 sharp 从 original 生成三类 WebP 衍生图:

输出规格用途
thumbs/*.webp720px 宽网格缩略图、卡片预览,速度优先
large/*.webp短边 2160pxFancybox 灯箱,细节优先
hero/*.webp1920px × 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.jsonhome-featured.jsonhome-recent.json),不由自动数据支配。专题 Tab 和年度 Tab 则由自动数据驱动。两种速度并存:照片元数据可快速更新,首页策展慢慢编辑。

共享模块(scripts/shared/)提供页面外壳、导航、路径前缀、游记组件、地图控件等跨脚本复用能力。


07内容格式

各类内容源对应不同的文件格式和 frontmatter 字段:

内容类型位置格式核心字段
文字文章content/journal/*.mdMarkdown + frontmattertitle, order, date/dateStart+dateEnd, kind, hero
探索卡片content/explore/*.mdMarkdown + frontmattertitle, date, link(外链则跳过详情页), status
年度页content/year/*.mdMarkdown + frontmattertitle, blurb, hero
专题清单content/topics/*.jsonJSON 数组照片文件名列表
游记行程content/trips/*.jsonJSONcities, segments, photos
首页精选content/home-featured.jsonJSON 数组image, layout, text, meta
首页近期content/home-recent.jsonJSONphotos + writings 数组
城市坐标content/city-coordinates.jsonJSON城市名 → { 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 updatedata + 全部页面构建
npm run buildimages + 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.htmlnpm 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 中留下可恢复的完整成品。
  • 管线保持幂等。只处理新增和修改的文件,符合个人站点长期小步更新的节奏。
  • 复杂度各归其位。图片处理、元数据、地图、策展、视觉各自独立,可替换但不互相吞没。