每个人都有过这样的经历:收藏夹里躺着一百多个网址,平时根本不会打开,真到要找某个工具的时候,又得在浏览器历史记录里翻半天。我做了个项目,名字叫“lingbot-map”,简单说就是一个个人网络书签地图站,把散落在各处的链接、灵感、工具、资料按自己的方式重新组织成一张网。这个项目做完之后,我在产品设计、信息架构和前端性能几个方面积累了不少想法,这篇文章完整记录下来,希望对做同类个人站、导航站或者知识库的朋友有参考价值。项目本身并不复杂,没有引入高大上的框架,也没有做后端服务,数据全放在前端,打开页面就能用。但正因为简单,反而腾出了很多精力去打磨信息组织和交互细节。适合想要搭建个人网址导航、书签管理、资源聚合页面的开发者,也适合正在犹豫“静态站到底该怎么做”的初级前端。1. lingbot-map的项目由来:从一百多个书签到一张网先交代一下背景。我做这个项目的原因,是因为浏览器原生书签实在撑不住我的使用习惯了。书签栏只有一排,收藏夹越来越大,网址存进去之后基本就是“已经收藏了,等于已经用过了”,之后再也没有点开过。最痛苦的是跨设备同步,工作电脑、家里电脑、手机三套环境,书签经常对不上,有的在收藏夹里躺了两年,我甚至不知道它还在。一开始我想过用现成的书签工具,比如一些基于标签的导航站服务。但我很快意识到一个问题,我需要的其实不是“收藏工具”,而是“展示工具”。很多书签服务做得再复杂,使用的场景停留在“搜索一个地址然后跳转”,但我更在意的是:打开一个页面,一眼就能看到我领域内需要用到的全部资源分布,它们之间是什么关系,哪些是核心,哪些是配套工具。这听起来像地图,而不是列表。于是“lingbot-map”这个想法就出现了,把它做成一张以自己为圆心、按主题分层展开的信息地图。这个项目的核心理念很简单:要么不做,要做就做成自己每天打开三遍的都首页。两次迭代测试之后,我把整个项目的定位固定下来,它的核心能力可以概括成三条:快速收录、直观展示、低维护成本。快速收录指的是我能用最少的操作把一个新链接加入系统;直观展示指的是页面结构对信息的组织度足够高;低维护成本指的是我不需要定时花精力去维护这个系统,它自己用静态托管躺着就行。当然,项目取名“lingbot-map”也有一点私心。“lingbot”原本是我给另一个小工具起的代号,后来觉得这个代号很适合延续下来。“map”则直接点明了本质,网页上的卡片不是简单的网格排列,而是有位置、有关系的信息地图。1.1 痛点:收藏夹变坟场收藏夹变成坟场,这个过程几乎是静默发生的。刚开始用浏览器书签时,我还会把每个链接好好分类,工具一栏、文档一栏、灵感一栏。但几个月之后,新建的书签数量超过了整理次数,新收藏的网址找不到合适的定位,就直接丢进“其他书签”。再往后,书签多到我连分类都懒得看,只凭记忆搜索。这个问题如果只靠“多分类”是解决不了的。因为书签列表本质上是线性结构,我看到的永远是“一行文字加一个小图标”,它对大脑的刺激非常有限。真正的效率在于把信息放到合适的位置上,页面一打开,视觉重心自然落在最重要的板块,而不是每次从头扫到尾。所以在正式开发前,我统计了自己收藏夹里那一百多个网址,按使用频率和主题做了聚类。结果很有说服力:大概 70% 的资源日常根本用不到,但它们占据了书签栏大部分空间,反而把真正高频的工具挤到了视觉盲区。这个发现直接决定了 lingbot-map 的第一版设计原则——不要展示所有,只展示值得被看到的。1.2 产品定位:不是又一个网址导航市面上的导航站,比如各种“好用网站合集”,通常做法是把一大堆网址按大分类平铺开,页面长得像信息瀑布。这种站的问题不在内容质量,而在结构上。它是以“网站”为中心,而不是以“使用场景”为中心。访客进入页面之后要自己判断“我是来干什么的”,然后再去找对应的分类。我给 lingbot-map 设定的目标完全相反,以“使用场景”为中心。页面上的每个区块,对应的是我日常要做的一件事,比如“速记与灵感收集”“技术检索”“图像处理工具”“聊天与搜索”。当我需要记录一条临时想法时,我不会去记“速记工具在哪个分类下”,我只需要知道“去左上角找便签”。这种转变看着细小,实际体验差别巨大,它把页面从一个存放链接的仓库,变成了一张决策地图。有人可能会问:这不就是一个带分组的书签页吗?区别在于分组方式。普通书签分组往往是“按内容类型”,比如“工具”“资源”“文章”,这种分法的毛病在于类型本身不代表用途。而 lingbot-map 的分组是“按任务”,把“做图时需要的一系列参考和工具”放一起,把“写文章时要用到的检索和素材”放一起,这样进入页面之后不需要二次思考。2. 数据模型与分层设计:先想清楚怎么组织信息很多个人项目一上来就写界面,把数据直接硬编码在模板里,做到后边改动一次想哭一次。lingbot-map 的第一版确实也踩了这个坑,但很快我做了重构。核心原则只有一条:界面和内容必须完全分离。界面上展示什么、卡片怎么排、显示文案是什么,都不是硬编码的问题,而是数据层应该决定的。我最终采用的是纯前端架构:一个 JSON 文件负责全部数据,页面运行时加载这个数据,再根据数据结构渲染出地图区块。选择这种方案的理由很直白:不需要搭服务器,不需要数据库,GitHub 或者任何静态托管平台都能直接跑起来,内容更新只需要改数据文件后提交推送。JSON 的数据结构我设计成三层:第一层是“板块”(section),对应页面上的每个大区域,比如“设计资源”。第二层是“分组”(group),板块下面再拆成的组,比如“配色工具”“字体工具”“图标素材”。第三层是“条目”(item),每个具体网址包含标题、链接、描述、图标。层级并不是我拍脑袋定的,它直接映射了我的使用习惯。板块对应的是“当我打开页面时,我打算做哪类事情”,分组对应的是“做这件事时的具体子步骤”,条目就是那个具体工具本身。如果层级再少一层,信息会显得扁平,分组不够;如果层级再多一层,维护成本就会上升到不太需要的地步。2.1 节点数据格式:JSON 还是 CMS在数据格式选择上,我也认真比较过几类方案。第一种是直接用 JSON 文件,像上面说的这样;第二种是接一个无头 CMS;第三种是自己在本地维护一个 SQLite 数据库,再通过构建脚本生成静态数据。无头 CMS 的优势在于可以通过网页端可视化编辑,甚至支持多人协作,但对单人项目来说太重了。我需要的只是快速加一个标题和链接,开个 CMS 管理面板反而多了一层心智负担。SQLite 方案适合数据量极大、查询条件复杂的场景,对一个几十上百条链接的导航站来说,用上了大炮打蚊子。最终选 JSON 还有一个隐藏好处:Git 变成本能的版本管理器。我对每条链接的任何修改,在 git 历史里都有迹可循。某天某个链接失效了,想回退到原来的配置,一条 git checkout 就完事了。这种透明度和可回溯性,是数据库方案很难给的。实际 JSON 文件的格式并不复杂,大致结构如下:{ "sections": [ { "id": "design", "title": "设计资源", "subtitle": "做图之前先来这里逛一圈", "groups": [ { "name": "配色", "items": [ { "title": "Coolors", "url": "https://coolors.co", "desc": "快速生成配色方案", "icon": "https://coolors.co/favicon.ico", "tags": ["color", "quick"] }, { "title": "Color Hunt", "url": "https://colorhunt.co", "desc": "设计师分享的调色板集合", "icon": "https://colorhunt.co/favicon.ico", "tags": ["color", "inspire"] } ] } ] } ] }每个条目我都存了一个 icon 字段,用于在卡片上显示站点的小图标。其实如果省事的话,也可以让前端根据域名自动拼接 favicon,比如https://域名/favicon.ico,但这种方式经常遇到部分站点不提供默认入口图标的情况。所以我更倾向显式存储图标字段,虽然维护时多写一行,但展示效果稳定得多。2.2 分类粒度与标签体系分类粒度是我反复调整最久的地方。最初分成 4 个板块,每个板块下面只有两三个分组,结果打开页面后发现,视觉上很空,而且部分板块之间有明显的重叠。比如“灵感参考”和“设计资源”里都放了 Behance,这让我意识到一个问题:一个链接可以归属多个场景,但卡片只能放在一个物理位置。为了解决这种场景重叠,我在数据模型里增加了“tags”字段。每个条目除了归属在主分类下,还能打上多个标签。页面上的主体布局仍然按板块和分组排列,但顶部增加了一个全局标签筛选条。点击某个标签后,页面上所有非对称卡片会做模糊过滤,只展示包含此标签的卡片。这样一来,同一个链接可以同时出现在“设计资源 - 灵感参考”和“图片素材 - 案例收集”两条检索路径下,不会丢失信息。标签体系不宜过于复杂。我定义了一批有限的标签,比如quick(高频工具)、free(免费)、inspire(灵感型)、tutorial(教程型),这样筛选时心智负担小,也不会出现一个链接打十个标签的情况。实践下来,一条链接平均打 1-2 个标签最合适,多于 2 个就说明分组本身可能需要调整。3. 前端实现的关键决策:地图化呈现的几种路径数据模型定好之后,真正的挑战在前端。让它看起来不只是一个普通网格导航站,而是有“地图感”,这个目标说起来容易,做起来并不简单。我尝试过几种路径,最终确定的方案是几轮取舍的结果。第一种思路是使用真正的可视化地图布局,比如 Canvas 或 SVG,把每个节点变成平面上的坐标点,支持拖拽和缩放。这种方案视觉效果最炫,但问题也最多:节点之间的间距需要手动调,数据一多就容易交错,移动端适配困难。而且这种方案对于“扫码找到一个链接”这种任务来说,效率可能反而比传统网格低。第二种思路是使用网格流式布局,配合锚点和分组标题。这个方案稳定可靠,也是很多导航站采用的。问题在于立体感不足,滚动操作太多,一眼看不完整个板块之间的关系。第三种思路,也是我最终选定的方案:网格布局 + 分组卡片 + 可视层级。页面保持基于网格的自然流动布局,但每个分组本身是一张卡片,卡片有明确的标题和底色,板块之间用大号标题和分隔线拉开距离。视觉上,进入页面后能明显感觉到“色彩模糊的小地图块”,每个板块就像地图上的片区。3.1 前端框架与渲染逻辑的选择考虑到项目规模,我没有引入 React 或 Vue,而是用原生 JavaScript 加一个小型状态管理对象搞定。这不是为了刻意追求极简,而是我发现:对于“一次加载,渲染完毕,之后不频繁交互”的页面,框架带来的虚拟 DOM 更新优势完全用不上。渲染逻辑分成三步:首先用fetch加载 JSON 数据,然后按板块顺序生成对应的 DOM 容器,最后遍历分组和条目,填充卡片。整个渲染过程一次性完成,后续交互主要依赖事件委托。事件委托是这里的一个小技巧。每个卡片按钮不单独绑定点击事件,而是在整个容器上绑定一个 click 监听,通过>const state = { rawData: null, activeTag: null, searchQuery: "" }; document.querySelector(".map-container").addEventListener("click", (e) => { const card = e.target.closest(".map-card"); if (!card) return; window.open(card.dataset.url, "_blank"); });这种简洁逻辑还有一个好处:对于后来想看我代码的人几乎没有学习成本。3.2 卡片交互与地图感的营造要让网格布局产生“地图感”,核心在间距、层次和动效三个要素。间距方面,分组之间的留白必须明显大于卡片之间的留白。我实验下来,卡片之间 8px、分组卡片内边距 16px、板块间隔 32px 是比较舒服的比例。视觉上,同组的卡片有明显的“聚合感”,而不同板块之间不会混作一团。层次方面,每组卡片背景使用近似于页面底色但略深一点的色块,板块标题用更大的字号和图标作为起始引导。卡片本身不添加边框,只用极浅的投影来区分层级。整个页面颜色控制在一个低饱和度的体系内,既不会信息过载,也避免把导航页做得像广告牌。动效方面,我没有做花哨的入场动画,只保留了一个 150ms 的悬浮反馈:卡片在鼠标悬停时上浮 2px,颜色轻微变深。这个反馈时间不长不短,恰好让用户感知到“这里可以点”。对于移动端的触摸,用hover不够自然,所以我还加了一个a:active状态,按下时卡片会轻微下沉。3.3 搜索与筛选的实现思路页面再好看,如果信息多了找不到链接,也是白搭。所以我在顶部预留了一个搜索框。搜索逻辑不复杂,但有一个点值得说:搜索之前先做数据归一化。也就是将所有标题、描述、标签统一转成小写,再做子串匹配。这样搜索“figma”时,即使数据里存的是“Figma”,也能正确命中。筛选逻辑分两种模式叠加:标签筛选与关键词搜索。两者是“与”的关系,也就是说同时满足标签和搜索条件才显示。实现时,我维护一个visibleSections对象,在每次状态变化后重新计算当前哪些分组、哪些卡片应该保留。所有隐藏元素使用hidden属性,而不是动态删除 DOM 节点,这样再次切换筛选条件时,恢复显示不需要重新渲染,效率更高。这里分享一个我实测中很有用的做法:搜索框的输入事件做个 200ms 的防抖。不是所有输入都需要立即触发筛选,比如用户打字速度很快,中间停顿的那几个中间状态一点儿意义都没有。防抖之后,筛选函数只在用户停顿超过 200ms 才执行,体感上反而是“更跟手”的。4. 部署、数据备份与更新流程lingbot-map 的部署非常简单,因为它是标准静态网站。构建产物只有 HTML、CSS、JS、JSON 和少量图片资源,没有任何服务端依赖。我可以非常确定地说,这套方案在部署层面是“零运维”的,只要托管平台不挂,服务就不会挂。我选择的是将其托管在支持静态页面的平台,利用 Git 仓库作为触发源。每次往主分支推送变更,平台自动构建并发布。部署流程被压缩到极致:改完数据文件,执行一次git push,十几秒后线上就是最新版本。但这不意味着不需要考虑数据安全。因为整个站点的内容全部来自 JSON 数据文件,它是一个人的知识地图,丢了会很可惜。所以我把数据备份提到了第一优先级。4.1 静态部署步骤与环境配置如果你也想快速搭一个类似站点,环境准备其实非常少:安装 Node.js(用于本地启动开发预览)。下载或克隆 lingbot-map 项目骨架,目录结构大致为:lingbot-map/ ├── docs/ │ ├── index.html │ ├── assets/ │ │ ├── style.css │ │ ├── app.js │ │ └── data/ │ │ └── map.json ├── tools/ │ └── check-links.js └── README.md本地使用npx serve docs启动端口预览。修改map.json,加入自己的网址与板块。推送到远程仓库,由静态托管平台自动执行部署。这些步骤看起来多,其实实际操作的熟练以后,从“拿到一个新域名”到“上线一个自己的导航站”,半小时内就能完成。关键在做项目骨架时把目录和数据层分离清楚,后续添加链接就只是“编辑 JSON + 推送”。4.2 图片资源与 favicon 的离线缓存导航站虽然内容以链接为主,但图标和缩略图还是一笔不小的外部资源。我最早直接使用站点自己的 favicon 链接,后来发现部分网站的图标服务不稳定,有的图片加载很慢,有的直接断开。这个问题在网速较慢时尤其明显,可能整页卡片有一排小叉号,观感很差。我的解决方案是:将常用站点的 favicon 统一下载,缩放到 32x32 或 64x64,放到本地assets/icons/目录下。数据字段中的 icon 值改为相对路径。只有极少数不常用或不方便下载图标的站点,才保留外链。这样首屏加载速度快很多,也彻底摆脱了外部图片服务的波动。需要注意,favicon 尺寸不必贪大。导航卡片上的图标展示区域通常只有 16 或者 24 像素,下载 512 像素的图标完全是在浪费资源。官方 favicon 一般都有 32 或 64 的版本,抓下来之后反而比大图标更适合展示。如果有站点没有提供公开的 favicon,可以用一种取巧的办法,直接从页面 HTML 的link标签里解析。写一段简单的脚本遍历已知 URL,抓取页头中有rel="icon"的地址,能省去不少手动找图标的功夫。4.3 链接有效性检查纯静态站的另一个隐患是链接失效。链接存了三年之后,原来的站点关闭或改版了,用户点过去看到的是 404。这种问题不能靠人工定期检查,所以我写了一个小的 Node 脚本,放在tools/check-links.js中,用来自动化检查数据文件中的所有外链。脚本原理非常简单:读取map.json。遍历所有条目的url。用 Node 的http/https模块发起HEAD请求,捕捉状态码。如果请求失败或状态码大于 400,把对应的条目输出到一个待检列表。考虑到部分站点会屏蔽非浏览器客户端,脚本里设置了一个常规浏览器的 User-Agent,超时时间设为 10 秒,防止某个卡住的请求拖慢整个检查流程。这个脚本我设置成每两周手动跑一次,偶尔也会配合错误日志做一次全量修复。5. 踩过的坑与实测调优手记说句实在话,这个项目功能不复杂,但越简单的东西越容易栽进各种意想不到的坑里。下面这几个坑,有的是我自己慢慢排查出来的,有的则是走了很大弯路之后才发现的。记录在这里,希望能帮后来者少花点时间。5.1 绝对路径与相对路径的坑第一个坑与路径有关。最开始我把 JSON 数据中的图标路径写成相对路径assets/icons/xxx.png,本地测试一切正常。但部署之后,部分页面刷新就出现图标丢失的问题。排查发现:当前页面 URL 如果是https://site.com/assets/icons/xxx.png,相对路径还能解析;但如果通过子路径访问,比如个人站挂在https://username.github.io/lingbot-map/下,相对路径的解析基准就完全不同了,导致图标全部 404。解决办法是在 HTML 里写一个<base>标签,或是在 JavaScript 中通过一个定位函数拼接完整路径。我采用的是第二种方式:在app.js顶部声明const ICON_BASE = window.location.origin + window.location.pathname,渲染时用ICON_BASE + iconPath拼出完整路径。这样不管站点部署在哪个层级,资源都能正确加载。5.2 搜索输入法导致的丢失问题第二个坑出在搜索框。中文输入法场景下,如果直接监听input事件,用户在拼音组合阶段就已经触发了多次筛选,导致每次键入字母时,界面卡片瞬间消失又恢复,非常飘。这个问题的本质是:拼音输入法产生的文本是“中间态”,不应该作为过滤依据。解决思路很巧妙:监听compositionstart和compositionend事件。当compositionstart触发时,设置一个isComposing标志;只有在该标志为false时才执行筛选逻辑。更新后的代码如下:let isComposing = false; searchInput.addEventListener("compositionstart", () => { isComposing = true; }); searchInput.addEventListener("compositionend", (e) => { isComposing = false; handleSearch(e.target.value); }); searchInput.addEventListener("input", (e) => { if (!isComposing) handleSearch(e.target.value); });这个小改进对日常使用感受提升非常明显,尤其是大量的时间是在过滤中文关键词的场景,几乎完全消除了“筛选抖动”。5.3 移动端适配与触摸延迟第三个坑在移动端。导航站在手机上的使用比例不低,但手机屏幕窄、手指粗,对小卡片的点击精准度要求很高。第一版设计没有做特别的移动端处理,卡片的高度和间距沿用桌面端数值,结果实测在手机上经常点到旁边卡片。经过几轮调试,最终确定移动端阈值:屏幕宽度小于 640px 时,卡片最小高度增大到 48px,卡片内边距也整体上调,保证每个链接都有明确的“热区”。同时关闭了卡片上的部分 hover 效果,因为移动端不存在鼠标悬停,hover 效果反而会影响触摸响应的视觉反馈。相对复杂的问题在于触摸延迟,移动端浏览器在触摸之后通常会等待 300ms 再触发点击事件,用来判断用户是否要双击缩放。现代浏览器在设置了正确viewport并且使用width=device-width之后会移除这个延迟,但仍有些旧版本浏览器会保留。保险起见,我在 CSS 中加入了:html { touch-action: manipulation; }这行代码告诉浏览器:这个元素上不需要双击缩放,触摸时可以立即触发点击。对导航类页面来说,每次点击节省 300ms,累计起来的体验差异非常明显。5.4 数据更新与缓存策略静态站虽然部署简单,但更新之后有个常见的隐患:浏览器缓存。JSON 数据文件和 JS 文件一样,会被浏览器缓存,导致页面部署成功后,用户端看到的仍是旧数据。这个问题在 Safari 里尤其顽固。我的处理方法是为静态资源增加带 hash 的版本号:每次构建时生成一个随机版本号,附加到 JS 和 JSON 资源的请求地址上。由于整个页面是静态托管,没有构建步骤,实现方式就简单粗暴一些——在 HTML 中写一个固定的版本号,每次手动更新数据文件时顺手改一下版本号,资源请求 URL 会变化,自然绕过缓存。<script src="app.js?v=3"></script>这个办法虽然原始,但极其实用。版本号不需要自动化,因为内容更新频率本身就低,每次改动记住 +1 就行。6. lingbot-map的边界探索与扩展方向项目做到这个阶段,基本功能已经稳定,剩下的问题不再是“还能加什么”,而是“什么不应该加”。个人导航站最容易犯的毛病就是功能无限膨胀:今天想加备忘录,明天想加 RSS 阅读器,后天想加日程管理。每加一个新模块,维护成本和界面复杂度都会上升,最终背离“快速找到链接”这个初衷。所以我在功能边界上做了很明确的取舍。凡是“需要频繁交互、涉及多步操作”的功能,通通不做,交给更专业的在线工具;凡是“静态展示、信息组织”层面的能力,才会考虑继续优化。按照这个原则,下面几个方向是我认为比较有价值且不破坏架构的扩展。6.1 分组折叠与个性化排序现在所有板块默认全部展开,板块多了之后页面会变得很长。一个比较自然的升级是增加分组的折叠能力:每个板块标题可以点击折叠,折叠状态持久化到 localStorage。这样用户进入页面后,首先看到的是一级板块列表,需要哪个板块就展开哪个板块,信息架构更接近真正的“地图”。折叠状态只需要用一个对象记录在 localStorage 里,形如:{ "design": "collapsed", "code": "expanded", "reading": "collapsed" }逻辑上并不复杂,但对页面观感的控制力会提升很多。而且折叠状态还能为用户提供“分区关注”的能力,比如今天聚焦写文案,就把写作相关板块展开,其他全部收纳起来。6.2 链接评分与历史访问记录另一个我一直在犹豫要不要做的方向,是给每个链接记录访问次数和评分。理论上,如果我能知道哪些卡片被频繁点击,就可以把最高频的链接自动提升到每个板块的开头位置,实现“使用时自动优化排序”。这个功能对纯前端项目也能实现,只需要在每次点击时向 localStorage 写入时间戳和计数。但说实话,目前我的使用场景里数据量还不够大,几十个链接手动排序也没问题,自动排序反而让我对页面结构失去控制感。这个功能我暂时只做了一个最简单的“最近常用”标记,放在每个板块的开头,展示点击次数最多的三个条目。等将来收录更多链接之后,再决定要不要升级成完整的个性化排序算法。6.3 多主题与自定义外观外观层面,我准备做成“主题包”的机制。界面颜色不是硬编码,而是通过 CSS 变量定义,换主题只需要切换一套变量值就行。这算是对未来可能的“发一个小产品给别人用”的提前准备。毕竟每个人对导航站的审美需求差别很大,有人喜欢极简白,有人喜欢深色模式,还有人可能想要一点毛玻璃质感。通过 CSS 变量来切换主题,不影响逻辑层,维护成本也低。实现方式其实非常简单::root { --bg-color: #fdfbf7; --card-bg: #ffffff; --text-main: #1f2937; --accent: #4f46e5; } .theme-dark { --bg-color: #111827; --card-bg: #1f2937; --text-main: #f9fafb; --accent: #818cf8; }页面所有具体颜色只引用这些变量,主题切换脚本只需切换document.body的 class。我还打算把主题选择同样存到 localStorage,这样访问者自己也可以切,而不用动代码。6.4 输入体验的优化:快速添加模式现在往 lingbot-map 里加链接,流程是手动编辑 JSON。虽然技术上是常规操作,但对非开发人员,或者偶尔在手机上想添加链接的时候,这个过程还是太别扭。我设想过一个“快速添加”入口:在页面右上角放一个加号按钮,点击弹出小面板,只需要填标题和 URL,数据会自动暂存到一个pending.json列表里。由于没有后端,“自动写入文件”这件事纯前端做不到。所以我做了一点变通:快速添加面板生成一段 JSON 代码,复制下来然后粘贴进map.json中即可。虽然还是手动操作,但至少省去了打开代码编辑器、寻找正确分类的麻烦。如果未来这个功能用得频繁,我可能会再加一个脚本:读取一个专门存放临时条目的 JSON 文件,执行时自动去重并合并进主数据。这样就真的做到了“添加链接只需要推一次代码”的水平。写在最后做 lingbot-map 这个项目,最大的收获不是学会某个具体技术,而是理顺了“个人工具类产品”的设计思路。信息组织这件事,很多时候比视觉精美更重要。它需要你认真思考自己的使用场景,提炼出真正的核心需求,再围绕这个需求做大量减法。我现在每天打开这个页面,已经不再把它当作一个“需要维护的项目”,而是当作一个真正在使用的效率工具。随着我工作方向的变化,板块内容还在持续调整:新增了 AI 工具板块,弱化了一些低频设计参考站,顺便把高频的搜索工具往页面中心挪了挪。这就是自己的信息地图,它有生命,会随着我的使用习惯一起进化。如果你也想做类似的个人导航页,我的建议只有一条:先从整理自己的收藏夹开始,扔掉那些两年都没打开过的链接,只留下真正高频和必要的资源,然后再开始写代码。页面能不能感动自己,往往在技术之外就已经决定了。
每个人都有过这样的经历:收藏夹里躺着一百多个网址,平时根本不会打开,真到要找某个工具的时候,又得在浏览器历史记录里翻半天。我做了个项目,名字叫“lingbot-map”,简单说就是一个个人网络书签地图站,把散落在各处的链接、灵感、工具、资料按自己的方式重新组织成一张网。这个项目做完之后,我在产品设计、信息架构和前端性能几个方面积累了不少想法,这篇文章完整记录下来,希望对做同类个人站、导航站或者知识库的朋友有参考价值。项目本身并不复杂,没有引入高大上的框架,也没有做后端服务,数据全放在前端,打开页面就能用。但正因为简单,反而腾出了很多精力去打磨信息组织和交互细节。适合想要搭建个人网址导航、书签管理、资源聚合页面的开发者,也适合正在犹豫“静态站到底该怎么做”的初级前端。1. lingbot-map的项目由来:从一百多个书签到一张网先交代一下背景。我做这个项目的原因,是因为浏览器原生书签实在撑不住我的使用习惯了。书签栏只有一排,收藏夹越来越大,网址存进去之后基本就是“已经收藏了,等于已经用过了”,之后再也没有点开过。最痛苦的是跨设备同步,工作电脑、家里电脑、手机三套环境,书签经常对不上,有的在收藏夹里躺了两年,我甚至不知道它还在。一开始我想过用现成的书签工具,比如一些基于标签的导航站服务。但我很快意识到一个问题,我需要的其实不是“收藏工具”,而是“展示工具”。很多书签服务做得再复杂,使用的场景停留在“搜索一个地址然后跳转”,但我更在意的是:打开一个页面,一眼就能看到我领域内需要用到的全部资源分布,它们之间是什么关系,哪些是核心,哪些是配套工具。这听起来像地图,而不是列表。于是“lingbot-map”这个想法就出现了,把它做成一张以自己为圆心、按主题分层展开的信息地图。这个项目的核心理念很简单:要么不做,要做就做成自己每天打开三遍的都首页。两次迭代测试之后,我把整个项目的定位固定下来,它的核心能力可以概括成三条:快速收录、直观展示、低维护成本。快速收录指的是我能用最少的操作把一个新链接加入系统;直观展示指的是页面结构对信息的组织度足够高;低维护成本指的是我不需要定时花精力去维护这个系统,它自己用静态托管躺着就行。当然,项目取名“lingbot-map”也有一点私心。“lingbot”原本是我给另一个小工具起的代号,后来觉得这个代号很适合延续下来。“map”则直接点明了本质,网页上的卡片不是简单的网格排列,而是有位置、有关系的信息地图。1.1 痛点:收藏夹变坟场收藏夹变成坟场,这个过程几乎是静默发生的。刚开始用浏览器书签时,我还会把每个链接好好分类,工具一栏、文档一栏、灵感一栏。但几个月之后,新建的书签数量超过了整理次数,新收藏的网址找不到合适的定位,就直接丢进“其他书签”。再往后,书签多到我连分类都懒得看,只凭记忆搜索。这个问题如果只靠“多分类”是解决不了的。因为书签列表本质上是线性结构,我看到的永远是“一行文字加一个小图标”,它对大脑的刺激非常有限。真正的效率在于把信息放到合适的位置上,页面一打开,视觉重心自然落在最重要的板块,而不是每次从头扫到尾。所以在正式开发前,我统计了自己收藏夹里那一百多个网址,按使用频率和主题做了聚类。结果很有说服力:大概 70% 的资源日常根本用不到,但它们占据了书签栏大部分空间,反而把真正高频的工具挤到了视觉盲区。这个发现直接决定了 lingbot-map 的第一版设计原则——不要展示所有,只展示值得被看到的。1.2 产品定位:不是又一个网址导航市面上的导航站,比如各种“好用网站合集”,通常做法是把一大堆网址按大分类平铺开,页面长得像信息瀑布。这种站的问题不在内容质量,而在结构上。它是以“网站”为中心,而不是以“使用场景”为中心。访客进入页面之后要自己判断“我是来干什么的”,然后再去找对应的分类。我给 lingbot-map 设定的目标完全相反,以“使用场景”为中心。页面上的每个区块,对应的是我日常要做的一件事,比如“速记与灵感收集”“技术检索”“图像处理工具”“聊天与搜索”。当我需要记录一条临时想法时,我不会去记“速记工具在哪个分类下”,我只需要知道“去左上角找便签”。这种转变看着细小,实际体验差别巨大,它把页面从一个存放链接的仓库,变成了一张决策地图。有人可能会问:这不就是一个带分组的书签页吗?区别在于分组方式。普通书签分组往往是“按内容类型”,比如“工具”“资源”“文章”,这种分法的毛病在于类型本身不代表用途。而 lingbot-map 的分组是“按任务”,把“做图时需要的一系列参考和工具”放一起,把“写文章时要用到的检索和素材”放一起,这样进入页面之后不需要二次思考。2. 数据模型与分层设计:先想清楚怎么组织信息很多个人项目一上来就写界面,把数据直接硬编码在模板里,做到后边改动一次想哭一次。lingbot-map 的第一版确实也踩了这个坑,但很快我做了重构。核心原则只有一条:界面和内容必须完全分离。界面上展示什么、卡片怎么排、显示文案是什么,都不是硬编码的问题,而是数据层应该决定的。我最终采用的是纯前端架构:一个 JSON 文件负责全部数据,页面运行时加载这个数据,再根据数据结构渲染出地图区块。选择这种方案的理由很直白:不需要搭服务器,不需要数据库,GitHub 或者任何静态托管平台都能直接跑起来,内容更新只需要改数据文件后提交推送。JSON 的数据结构我设计成三层:第一层是“板块”(section),对应页面上的每个大区域,比如“设计资源”。第二层是“分组”(group),板块下面再拆成的组,比如“配色工具”“字体工具”“图标素材”。第三层是“条目”(item),每个具体网址包含标题、链接、描述、图标。层级并不是我拍脑袋定的,它直接映射了我的使用习惯。板块对应的是“当我打开页面时,我打算做哪类事情”,分组对应的是“做这件事时的具体子步骤”,条目就是那个具体工具本身。如果层级再少一层,信息会显得扁平,分组不够;如果层级再多一层,维护成本就会上升到不太需要的地步。2.1 节点数据格式:JSON 还是 CMS在数据格式选择上,我也认真比较过几类方案。第一种是直接用 JSON 文件,像上面说的这样;第二种是接一个无头 CMS;第三种是自己在本地维护一个 SQLite 数据库,再通过构建脚本生成静态数据。无头 CMS 的优势在于可以通过网页端可视化编辑,甚至支持多人协作,但对单人项目来说太重了。我需要的只是快速加一个标题和链接,开个 CMS 管理面板反而多了一层心智负担。SQLite 方案适合数据量极大、查询条件复杂的场景,对一个几十上百条链接的导航站来说,用上了大炮打蚊子。最终选 JSON 还有一个隐藏好处:Git 变成本能的版本管理器。我对每条链接的任何修改,在 git 历史里都有迹可循。某天某个链接失效了,想回退到原来的配置,一条 git checkout 就完事了。这种透明度和可回溯性,是数据库方案很难给的。实际 JSON 文件的格式并不复杂,大致结构如下:{ "sections": [ { "id": "design", "title": "设计资源", "subtitle": "做图之前先来这里逛一圈", "groups": [ { "name": "配色", "items": [ { "title": "Coolors", "url": "https://coolors.co", "desc": "快速生成配色方案", "icon": "https://coolors.co/favicon.ico", "tags": ["color", "quick"] }, { "title": "Color Hunt", "url": "https://colorhunt.co", "desc": "设计师分享的调色板集合", "icon": "https://colorhunt.co/favicon.ico", "tags": ["color", "inspire"] } ] } ] } ] }每个条目我都存了一个 icon 字段,用于在卡片上显示站点的小图标。其实如果省事的话,也可以让前端根据域名自动拼接 favicon,比如https://域名/favicon.ico,但这种方式经常遇到部分站点不提供默认入口图标的情况。所以我更倾向显式存储图标字段,虽然维护时多写一行,但展示效果稳定得多。2.2 分类粒度与标签体系分类粒度是我反复调整最久的地方。最初分成 4 个板块,每个板块下面只有两三个分组,结果打开页面后发现,视觉上很空,而且部分板块之间有明显的重叠。比如“灵感参考”和“设计资源”里都放了 Behance,这让我意识到一个问题:一个链接可以归属多个场景,但卡片只能放在一个物理位置。为了解决这种场景重叠,我在数据模型里增加了“tags”字段。每个条目除了归属在主分类下,还能打上多个标签。页面上的主体布局仍然按板块和分组排列,但顶部增加了一个全局标签筛选条。点击某个标签后,页面上所有非对称卡片会做模糊过滤,只展示包含此标签的卡片。这样一来,同一个链接可以同时出现在“设计资源 - 灵感参考”和“图片素材 - 案例收集”两条检索路径下,不会丢失信息。标签体系不宜过于复杂。我定义了一批有限的标签,比如quick(高频工具)、free(免费)、inspire(灵感型)、tutorial(教程型),这样筛选时心智负担小,也不会出现一个链接打十个标签的情况。实践下来,一条链接平均打 1-2 个标签最合适,多于 2 个就说明分组本身可能需要调整。3. 前端实现的关键决策:地图化呈现的几种路径数据模型定好之后,真正的挑战在前端。让它看起来不只是一个普通网格导航站,而是有“地图感”,这个目标说起来容易,做起来并不简单。我尝试过几种路径,最终确定的方案是几轮取舍的结果。第一种思路是使用真正的可视化地图布局,比如 Canvas 或 SVG,把每个节点变成平面上的坐标点,支持拖拽和缩放。这种方案视觉效果最炫,但问题也最多:节点之间的间距需要手动调,数据一多就容易交错,移动端适配困难。而且这种方案对于“扫码找到一个链接”这种任务来说,效率可能反而比传统网格低。第二种思路是使用网格流式布局,配合锚点和分组标题。这个方案稳定可靠,也是很多导航站采用的。问题在于立体感不足,滚动操作太多,一眼看不完整个板块之间的关系。第三种思路,也是我最终选定的方案:网格布局 + 分组卡片 + 可视层级。页面保持基于网格的自然流动布局,但每个分组本身是一张卡片,卡片有明确的标题和底色,板块之间用大号标题和分隔线拉开距离。视觉上,进入页面后能明显感觉到“色彩模糊的小地图块”,每个板块就像地图上的片区。3.1 前端框架与渲染逻辑的选择考虑到项目规模,我没有引入 React 或 Vue,而是用原生 JavaScript 加一个小型状态管理对象搞定。这不是为了刻意追求极简,而是我发现:对于“一次加载,渲染完毕,之后不频繁交互”的页面,框架带来的虚拟 DOM 更新优势完全用不上。渲染逻辑分成三步:首先用fetch加载 JSON 数据,然后按板块顺序生成对应的 DOM 容器,最后遍历分组和条目,填充卡片。整个渲染过程一次性完成,后续交互主要依赖事件委托。事件委托是这里的一个小技巧。每个卡片按钮不单独绑定点击事件,而是在整个容器上绑定一个 click 监听,通过>const state = { rawData: null, activeTag: null, searchQuery: "" }; document.querySelector(".map-container").addEventListener("click", (e) => { const card = e.target.closest(".map-card"); if (!card) return; window.open(card.dataset.url, "_blank"); });这种简洁逻辑还有一个好处:对于后来想看我代码的人几乎没有学习成本。3.2 卡片交互与地图感的营造要让网格布局产生“地图感”,核心在间距、层次和动效三个要素。间距方面,分组之间的留白必须明显大于卡片之间的留白。我实验下来,卡片之间 8px、分组卡片内边距 16px、板块间隔 32px 是比较舒服的比例。视觉上,同组的卡片有明显的“聚合感”,而不同板块之间不会混作一团。层次方面,每组卡片背景使用近似于页面底色但略深一点的色块,板块标题用更大的字号和图标作为起始引导。卡片本身不添加边框,只用极浅的投影来区分层级。整个页面颜色控制在一个低饱和度的体系内,既不会信息过载,也避免把导航页做得像广告牌。动效方面,我没有做花哨的入场动画,只保留了一个 150ms 的悬浮反馈:卡片在鼠标悬停时上浮 2px,颜色轻微变深。这个反馈时间不长不短,恰好让用户感知到“这里可以点”。对于移动端的触摸,用hover不够自然,所以我还加了一个a:active状态,按下时卡片会轻微下沉。3.3 搜索与筛选的实现思路页面再好看,如果信息多了找不到链接,也是白搭。所以我在顶部预留了一个搜索框。搜索逻辑不复杂,但有一个点值得说:搜索之前先做数据归一化。也就是将所有标题、描述、标签统一转成小写,再做子串匹配。这样搜索“figma”时,即使数据里存的是“Figma”,也能正确命中。筛选逻辑分两种模式叠加:标签筛选与关键词搜索。两者是“与”的关系,也就是说同时满足标签和搜索条件才显示。实现时,我维护一个visibleSections对象,在每次状态变化后重新计算当前哪些分组、哪些卡片应该保留。所有隐藏元素使用hidden属性,而不是动态删除 DOM 节点,这样再次切换筛选条件时,恢复显示不需要重新渲染,效率更高。这里分享一个我实测中很有用的做法:搜索框的输入事件做个 200ms 的防抖。不是所有输入都需要立即触发筛选,比如用户打字速度很快,中间停顿的那几个中间状态一点儿意义都没有。防抖之后,筛选函数只在用户停顿超过 200ms 才执行,体感上反而是“更跟手”的。4. 部署、数据备份与更新流程lingbot-map 的部署非常简单,因为它是标准静态网站。构建产物只有 HTML、CSS、JS、JSON 和少量图片资源,没有任何服务端依赖。我可以非常确定地说,这套方案在部署层面是“零运维”的,只要托管平台不挂,服务就不会挂。我选择的是将其托管在支持静态页面的平台,利用 Git 仓库作为触发源。每次往主分支推送变更,平台自动构建并发布。部署流程被压缩到极致:改完数据文件,执行一次git push,十几秒后线上就是最新版本。但这不意味着不需要考虑数据安全。因为整个站点的内容全部来自 JSON 数据文件,它是一个人的知识地图,丢了会很可惜。所以我把数据备份提到了第一优先级。4.1 静态部署步骤与环境配置如果你也想快速搭一个类似站点,环境准备其实非常少:安装 Node.js(用于本地启动开发预览)。下载或克隆 lingbot-map 项目骨架,目录结构大致为:lingbot-map/ ├── docs/ │ ├── index.html │ ├── assets/ │ │ ├── style.css │ │ ├── app.js │ │ └── data/ │ │ └── map.json ├── tools/ │ └── check-links.js └── README.md本地使用npx serve docs启动端口预览。修改map.json,加入自己的网址与板块。推送到远程仓库,由静态托管平台自动执行部署。这些步骤看起来多,其实实际操作的熟练以后,从“拿到一个新域名”到“上线一个自己的导航站”,半小时内就能完成。关键在做项目骨架时把目录和数据层分离清楚,后续添加链接就只是“编辑 JSON + 推送”。4.2 图片资源与 favicon 的离线缓存导航站虽然内容以链接为主,但图标和缩略图还是一笔不小的外部资源。我最早直接使用站点自己的 favicon 链接,后来发现部分网站的图标服务不稳定,有的图片加载很慢,有的直接断开。这个问题在网速较慢时尤其明显,可能整页卡片有一排小叉号,观感很差。我的解决方案是:将常用站点的 favicon 统一下载,缩放到 32x32 或 64x64,放到本地assets/icons/目录下。数据字段中的 icon 值改为相对路径。只有极少数不常用或不方便下载图标的站点,才保留外链。这样首屏加载速度快很多,也彻底摆脱了外部图片服务的波动。需要注意,favicon 尺寸不必贪大。导航卡片上的图标展示区域通常只有 16 或者 24 像素,下载 512 像素的图标完全是在浪费资源。官方 favicon 一般都有 32 或 64 的版本,抓下来之后反而比大图标更适合展示。如果有站点没有提供公开的 favicon,可以用一种取巧的办法,直接从页面 HTML 的link标签里解析。写一段简单的脚本遍历已知 URL,抓取页头中有rel="icon"的地址,能省去不少手动找图标的功夫。4.3 链接有效性检查纯静态站的另一个隐患是链接失效。链接存了三年之后,原来的站点关闭或改版了,用户点过去看到的是 404。这种问题不能靠人工定期检查,所以我写了一个小的 Node 脚本,放在tools/check-links.js中,用来自动化检查数据文件中的所有外链。脚本原理非常简单:读取map.json。遍历所有条目的url。用 Node 的http/https模块发起HEAD请求,捕捉状态码。如果请求失败或状态码大于 400,把对应的条目输出到一个待检列表。考虑到部分站点会屏蔽非浏览器客户端,脚本里设置了一个常规浏览器的 User-Agent,超时时间设为 10 秒,防止某个卡住的请求拖慢整个检查流程。这个脚本我设置成每两周手动跑一次,偶尔也会配合错误日志做一次全量修复。5. 踩过的坑与实测调优手记说句实在话,这个项目功能不复杂,但越简单的东西越容易栽进各种意想不到的坑里。下面这几个坑,有的是我自己慢慢排查出来的,有的则是走了很大弯路之后才发现的。记录在这里,希望能帮后来者少花点时间。5.1 绝对路径与相对路径的坑第一个坑与路径有关。最开始我把 JSON 数据中的图标路径写成相对路径assets/icons/xxx.png,本地测试一切正常。但部署之后,部分页面刷新就出现图标丢失的问题。排查发现:当前页面 URL 如果是https://site.com/assets/icons/xxx.png,相对路径还能解析;但如果通过子路径访问,比如个人站挂在https://username.github.io/lingbot-map/下,相对路径的解析基准就完全不同了,导致图标全部 404。解决办法是在 HTML 里写一个<base>标签,或是在 JavaScript 中通过一个定位函数拼接完整路径。我采用的是第二种方式:在app.js顶部声明const ICON_BASE = window.location.origin + window.location.pathname,渲染时用ICON_BASE + iconPath拼出完整路径。这样不管站点部署在哪个层级,资源都能正确加载。5.2 搜索输入法导致的丢失问题第二个坑出在搜索框。中文输入法场景下,如果直接监听input事件,用户在拼音组合阶段就已经触发了多次筛选,导致每次键入字母时,界面卡片瞬间消失又恢复,非常飘。这个问题的本质是:拼音输入法产生的文本是“中间态”,不应该作为过滤依据。解决思路很巧妙:监听compositionstart和compositionend事件。当compositionstart触发时,设置一个isComposing标志;只有在该标志为false时才执行筛选逻辑。更新后的代码如下:let isComposing = false; searchInput.addEventListener("compositionstart", () => { isComposing = true; }); searchInput.addEventListener("compositionend", (e) => { isComposing = false; handleSearch(e.target.value); }); searchInput.addEventListener("input", (e) => { if (!isComposing) handleSearch(e.target.value); });这个小改进对日常使用感受提升非常明显,尤其是大量的时间是在过滤中文关键词的场景,几乎完全消除了“筛选抖动”。5.3 移动端适配与触摸延迟第三个坑在移动端。导航站在手机上的使用比例不低,但手机屏幕窄、手指粗,对小卡片的点击精准度要求很高。第一版设计没有做特别的移动端处理,卡片的高度和间距沿用桌面端数值,结果实测在手机上经常点到旁边卡片。经过几轮调试,最终确定移动端阈值:屏幕宽度小于 640px 时,卡片最小高度增大到 48px,卡片内边距也整体上调,保证每个链接都有明确的“热区”。同时关闭了卡片上的部分 hover 效果,因为移动端不存在鼠标悬停,hover 效果反而会影响触摸响应的视觉反馈。相对复杂的问题在于触摸延迟,移动端浏览器在触摸之后通常会等待 300ms 再触发点击事件,用来判断用户是否要双击缩放。现代浏览器在设置了正确viewport并且使用width=device-width之后会移除这个延迟,但仍有些旧版本浏览器会保留。保险起见,我在 CSS 中加入了:html { touch-action: manipulation; }这行代码告诉浏览器:这个元素上不需要双击缩放,触摸时可以立即触发点击。对导航类页面来说,每次点击节省 300ms,累计起来的体验差异非常明显。5.4 数据更新与缓存策略静态站虽然部署简单,但更新之后有个常见的隐患:浏览器缓存。JSON 数据文件和 JS 文件一样,会被浏览器缓存,导致页面部署成功后,用户端看到的仍是旧数据。这个问题在 Safari 里尤其顽固。我的处理方法是为静态资源增加带 hash 的版本号:每次构建时生成一个随机版本号,附加到 JS 和 JSON 资源的请求地址上。由于整个页面是静态托管,没有构建步骤,实现方式就简单粗暴一些——在 HTML 中写一个固定的版本号,每次手动更新数据文件时顺手改一下版本号,资源请求 URL 会变化,自然绕过缓存。<script src="app.js?v=3"></script>这个办法虽然原始,但极其实用。版本号不需要自动化,因为内容更新频率本身就低,每次改动记住 +1 就行。6. lingbot-map的边界探索与扩展方向项目做到这个阶段,基本功能已经稳定,剩下的问题不再是“还能加什么”,而是“什么不应该加”。个人导航站最容易犯的毛病就是功能无限膨胀:今天想加备忘录,明天想加 RSS 阅读器,后天想加日程管理。每加一个新模块,维护成本和界面复杂度都会上升,最终背离“快速找到链接”这个初衷。所以我在功能边界上做了很明确的取舍。凡是“需要频繁交互、涉及多步操作”的功能,通通不做,交给更专业的在线工具;凡是“静态展示、信息组织”层面的能力,才会考虑继续优化。按照这个原则,下面几个方向是我认为比较有价值且不破坏架构的扩展。6.1 分组折叠与个性化排序现在所有板块默认全部展开,板块多了之后页面会变得很长。一个比较自然的升级是增加分组的折叠能力:每个板块标题可以点击折叠,折叠状态持久化到 localStorage。这样用户进入页面后,首先看到的是一级板块列表,需要哪个板块就展开哪个板块,信息架构更接近真正的“地图”。折叠状态只需要用一个对象记录在 localStorage 里,形如:{ "design": "collapsed", "code": "expanded", "reading": "collapsed" }逻辑上并不复杂,但对页面观感的控制力会提升很多。而且折叠状态还能为用户提供“分区关注”的能力,比如今天聚焦写文案,就把写作相关板块展开,其他全部收纳起来。6.2 链接评分与历史访问记录另一个我一直在犹豫要不要做的方向,是给每个链接记录访问次数和评分。理论上,如果我能知道哪些卡片被频繁点击,就可以把最高频的链接自动提升到每个板块的开头位置,实现“使用时自动优化排序”。这个功能对纯前端项目也能实现,只需要在每次点击时向 localStorage 写入时间戳和计数。但说实话,目前我的使用场景里数据量还不够大,几十个链接手动排序也没问题,自动排序反而让我对页面结构失去控制感。这个功能我暂时只做了一个最简单的“最近常用”标记,放在每个板块的开头,展示点击次数最多的三个条目。等将来收录更多链接之后,再决定要不要升级成完整的个性化排序算法。6.3 多主题与自定义外观外观层面,我准备做成“主题包”的机制。界面颜色不是硬编码,而是通过 CSS 变量定义,换主题只需要切换一套变量值就行。这算是对未来可能的“发一个小产品给别人用”的提前准备。毕竟每个人对导航站的审美需求差别很大,有人喜欢极简白,有人喜欢深色模式,还有人可能想要一点毛玻璃质感。通过 CSS 变量来切换主题,不影响逻辑层,维护成本也低。实现方式其实非常简单::root { --bg-color: #fdfbf7; --card-bg: #ffffff; --text-main: #1f2937; --accent: #4f46e5; } .theme-dark { --bg-color: #111827; --card-bg: #1f2937; --text-main: #f9fafb; --accent: #818cf8; }页面所有具体颜色只引用这些变量,主题切换脚本只需切换document.body的 class。我还打算把主题选择同样存到 localStorage,这样访问者自己也可以切,而不用动代码。6.4 输入体验的优化:快速添加模式现在往 lingbot-map 里加链接,流程是手动编辑 JSON。虽然技术上是常规操作,但对非开发人员,或者偶尔在手机上想添加链接的时候,这个过程还是太别扭。我设想过一个“快速添加”入口:在页面右上角放一个加号按钮,点击弹出小面板,只需要填标题和 URL,数据会自动暂存到一个pending.json列表里。由于没有后端,“自动写入文件”这件事纯前端做不到。所以我做了一点变通:快速添加面板生成一段 JSON 代码,复制下来然后粘贴进map.json中即可。虽然还是手动操作,但至少省去了打开代码编辑器、寻找正确分类的麻烦。如果未来这个功能用得频繁,我可能会再加一个脚本:读取一个专门存放临时条目的 JSON 文件,执行时自动去重并合并进主数据。这样就真的做到了“添加链接只需要推一次代码”的水平。写在最后做 lingbot-map 这个项目,最大的收获不是学会某个具体技术,而是理顺了“个人工具类产品”的设计思路。信息组织这件事,很多时候比视觉精美更重要。它需要你认真思考自己的使用场景,提炼出真正的核心需求,再围绕这个需求做大量减法。我现在每天打开这个页面,已经不再把它当作一个“需要维护的项目”,而是当作一个真正在使用的效率工具。随着我工作方向的变化,板块内容还在持续调整:新增了 AI 工具板块,弱化了一些低频设计参考站,顺便把高频的搜索工具往页面中心挪了挪。这就是自己的信息地图,它有生命,会随着我的使用习惯一起进化。如果你也想做类似的个人导航页,我的建议只有一条:先从整理自己的收藏夹开始,扔掉那些两年都没打开过的链接,只留下真正高频和必要的资源,然后再开始写代码。页面能不能感动自己,往往在技术之外就已经决定了。