5分钟搭建MC·JC网页版入口:纯静态三件套实现导航页

发布时间:2026/11/4 7:50:05
5分钟搭建MC·JC网页版入口:纯静态三件套实现导航页

1. 先把“MC·JC 网页版入口”这件事想明白

1.1 这个入口页到底解决什么问题

MC·JC 是一套由若干子功能拼起来的工具组合,它的问题特别典型:功能不是没有,而是入口太散。主控面板在一个地址,作业中心在另一个地址,说明文档放在共享盘,常用的小脚本又在本地文件夹里,谁接手都得先在聊天记录里翻十几分钟链接。所谓“网页版入口”,本质上就是修一个门面,把这堆散落的地址收拢到一个静态页面上,打开浏览器就是一排卡片,点哪去哪,搜索框一搜就能定位。

它不装服务、不连数据库、不涉及任何后台逻辑,就是三个文件:index.html、style.css、app.js,外加一份描述入口的sites.json。正因为这么轻,5 分钟这个承诺才站得住脚——你不需要配环境、装依赖、跑构建,新建文件夹写完就能双击预览(严格说要用本地服务预览,后面会讲原因)。

这套东西适合谁?三类人最合适。第一类是刚接触前端、想找一个“有真实用途又不至于劝退”的练手项目的人,入口页麻雀虽小,HTML 语义化、CSS 响应式、JS 数据渲染和本地存储全都能摸一遍。第二类是需要给团队或自己做一个导航页的人,改几个字段就能上线,维护成本几乎为零。第三类是想给内部工具做统一门面的人,把链接、说明、状态提示集中到一个页面,新人上手时不用再靠口头传授。它不解决业务逻辑,它解决的是“找不到入口”这种每天都在发生、却没人愿意专门立项的小麻烦。

1.2 为什么选纯静态三件套而不是上框架

做入口页最容易犯的错,是把它当成一个正经项目来做。有人一上来就搭脚手架、上组件库、配路由、写状态管理,结果页面本身只有一张卡片列表,构建产物却有几兆,改一个链接还得重新打包部署。我在几个不同规模的项目里都见过这种过度设计,最后维护的人换了两拨,没人愿意碰那个“就为了显示十个链接”的工程。

选纯静态的理由很实在。体积上,三件套压缩后通常在 10KB 到 30KB 之间,首次加载几乎无感;框架方案动辄几百 KB 的运行时,对一张卡片列表来说是纯粹的浪费。部署上,静态文件扔到任何能托管静态资源的地方就能跑,不需要运行时环境,也不会有版本兼容问题。维护上,新人打开文件夹就能看懂,改一行 JSON 就完成一次内容更新,不需要知道什么是依赖注入。

方案首次加载体积上手门槛内容更新方式适用场景
纯静态三件套10–30KB会写标签即可改 JSON 或 HTML入口页、导航页、说明页
前端框架150KB 起需要懂构建与组件改代码后重新构建有复杂交互与状态
后端渲染取决于服务需要服务与模板改模板或数据库需要权限与动态数据

反过来讲,什么时候该换方案?当入口页开始需要登录态、需要按角色显示不同入口、需要记录点击行为做统计时,纯静态就该让位了。判断标准很简单:如果页面上出现“不同人看到不同内容”,纯静态就不够了。在那之前,别提前给自己加戏。

1.3 目录结构与文件职责划分

目录我习惯这样铺,扁平、一眼看完,不搞多层嵌套:

mc-jc-entry/ ├── index.html ├── style.css ├── app.js ├── sites.json └── assets/ └── favicon.svg

index.html只负责骨架,也就是页头、搜索框、卡片容器、页脚这几块固定结构,绝不把十几个入口卡片硬编码进去。style.css负责全部视觉,变量统一放在:root里,改主题色只改一行。app.js负责三件事:拉取数据、渲染卡片、处理搜索与主题切换。sites.json是内容层,所有入口的名称、描述、地址、标签都放这儿。

把数据抽出来这一步,是整篇里最值得强调的设计决定。很多人图省事直接在 HTML 里复制粘贴十段卡片结构,当时确实快,但第一次要调整卡片样式或者新增五个入口时,你就得在几百行重复标签里翻找。抽成 JSON 之后,视图和内容彻底分开:样式改一次全站生效,内容改一处即时更新,而且这份 JSON 后续还能被别的页面复用,比如做一个只显示“文档类”入口的子页面。

命名上我建议全部小写加连字符,别用中文文件名,也别用空格。看着是小事,等到部署到某些对大小写敏感的静态托管环境上,Style.css和style.css就是两个文件,白屏排查半小时起步。

2. 页面四层骨架拆解:结构、样式、交互、数据

2.1 结构层:语义化标签怎么选才不留坑

入口页的结构其实就四块:顶部栏、标题区、卡片区、页脚。听起来简单,但我见过大量用一堆div堆出来的版本,读屏软件念出来是一串没有意义的“分组”,键盘 Tab 键跳转顺序也乱。语义化标签不是为了好看,它直接决定了页面在无障碍场景下能不能用。

顶部栏用header,里面放品牌标识、搜索输入框、主题切换按钮。标题区用section包一个h1和一句副标题,h1全页只能有一个,这是硬规矩。卡片区用section加aria-live="polite",这样搜索过滤后结果数量变化时,读屏软件会播报,不用用户自己去数。每张卡片本身用a标签直接当容器,而不是div套a——前者天然可聚焦、天然支持回车打开、天然有链接语义,后者你得手动补tabindex和键盘事件,纯属自找麻烦。

页脚用footer,放版本号和一句说明就够。整个页面不需要nav,因为卡片区不是导航菜单,用nav反而语义错位。搜索框必须带aria-label,因为它的提示文字在placeholder里,占位符在输入后消失,读屏用户拿不到标签信息。

<header class="topbar"> <div class="brand"> <span class="brand-mark">MC</span> <span class="brand-text">JC 入口</span> </div> <div class="search"> <input id="search" type="search" placeholder="搜索入口,按 / 聚焦" aria-label="搜索入口"> </div> <button id="theme" class="theme-btn" aria-label="切换深浅主题">切换主题</button> </header> <main> <section class="hero"> <h1>MC·JC 网页版入口</h1> <p>把散落的入口收进一个页面,打开就能点。</p> </section> <section id="grid" class="grid" aria-live="polite"></section> <p id="empty" class="empty" hidden>没有匹配的入口,换个关键词试试。</p> </main> <footer class="foot"> <span>MC·JC 入口页 · 静态版</span> </footer>

注意#empty那个提示段落,它默认带hidden。搜索无结果时才由脚本去掉这个属性。这个细节很多人漏掉,结果用户搜了个不存在的词,页面一片空白,以为页面挂了。

2.2 样式层:CSS 变量加 Grid 一行搞定响应式

样式这块,最省事的做法是把所有颜色、圆角、间距抽成 CSS 变量放在:root,深色模式只需要覆盖同名变量,不用写两套选择器。变量命名的原则是“按用途不按颜色”,写--brand而不是--blue,哪天品牌色从蓝换成绿,变量名不用动。

响应式布局别写一堆媒体查询。卡片区用grid-template-columns: repeat(auto-fit, minmax(260px, 1fr))一行解决:浏览器自己算当前宽度能塞几列,每列最窄 260px,剩余空间均分。这个 260px 不是拍脑袋定的,它来自卡片内最小内容的宽度——标题两到四个字加上一行描述,再留出左右各 20px 内边距,260px 是保证描述不被压成两三个字一行的下限。如果你的入口名称普遍较长,把它调到 280px 或 300px 更合适。

:root { --bg: #f5f6f8; --panel: #ffffff; --text: #1f2329; --muted: #6b7280; --line: #e5e7eb; --brand: #2f6fed; --radius: 14px; --gap: 16px; --shadow: 0 1px 2px rgba(16,24,40,.06), 0 8px 24px rgba(16,24,40,.06); } [data-theme="dark"] { --bg: #0f1216; --panel: #171c22; --text: #e6e9ee; --muted: #9aa4b2; --line: #242c36; --brand: #5b8cff; --shadow: 0 1px 2px rgba(0,0,0,.45); } * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.65 system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; transition: background .2s ease, color .2s ease; } .topbar { display: flex; align-items: center; gap: 12px; padding: 12px 20px; background: var(--panel); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 9; } .search { flex: 1; } .search input { width: 100%; padding: 9px 14px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg); color: var(--text); font-size: 14px; outline: none; } .search input:focus { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(47,111,237,.15); } main { max-width: 1120px; margin: 0 auto; padding: 28px 20px 48px; } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--gap); } .card { display: block; padding: 18px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); text-decoration: none; color: inherit; transition: transform .18s ease, border-color .18s ease; } .card:hover { transform: translateY(-2px); border-color: var(--brand); } .card:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; } .card h2 { margin: 0 0 6px; font-size: 16px; } .card p { margin: 0; color: var(--muted); font-size: 13px; }

prefers-color-scheme这个媒体查询值得加一段:用户系统是深色模式时,首次进来直接给深色,别让人一睁眼被白板闪一下。实现方式是脚本读取matchMedia('(prefers-color-scheme: dark)')的匹配结果作为初始值,再叠加本地存储里用户手动选择过的偏好,后者优先级更高。

2.3 交互层:搜索过滤与主题记忆

交互只有两件事,但每件都有讲究。搜索这块,直接在input事件里逐字过滤就够了,入口数量在几十条的量级,性能完全不是问题。真要防抖,用 120ms 到 200ms 之间比较合适,太短起不到作用,太长用户会觉得输入卡顿。

过滤逻辑是拿关键词去匹配名称、描述、标签三个字段,全部转小写再比对,避免大小写带来的漏匹配。这里有个坑:中文没有大小写概念,但标签里常混着英文缩写,不转小写的话搜 “doc” 匹配不到 “Doc”。转小写是成本最低的兼容处理。

主题切换按钮点一下,把document.documentElement.dataset.theme在dark和light之间来回切,同时写进localStorage。之所以写在documentElement上而不是body上,是为了让:root级别的变量覆盖生效——变量定义在:root,覆盖也必须发生在同一层级或更高层级。这个细节我第一次写的时候踩过,写在body上结果变量根本没被覆盖,排查了半天。

键盘快捷键/聚焦搜索框,是入口页这类页面里体验提升最明显的一个小功能。做法是在document上监听keydown,判断event.key === '/'且当前焦点不在输入框里(用document.activeElement.tagName判断,排除INPUT和TEXTAREA),然后preventDefault并聚焦。不排除输入框的话,用户想在搜索框里输入斜杠打不出来,反而添乱。

注意:preventDefault一定要加,否则浏览器会把这个按键当成“快速查找”的触发键,部分环境下会弹出浏览器自带搜索条,体验很割裂。

2.4 数据层:入口配置抽成 JSON 的结构设计

sites.json的结构直接决定后续好不好维护。我的字段设计是这样的:name是卡片标题,desc是一句话说明,url是目标地址,tag是分类标签,icon是可选图标或首字母。tag字段除了显示分类,还能参与搜索匹配,一举两得。

[ { "name": "主控面板", "desc": "查看整体运行状态与任务概览", "url": "./console.html", "tag": "核心" }, { "name": "作业中心", "desc": "提交任务、查看执行记录", "url": "./jobs.html", "tag": "核心" }, { "name": "使用说明", "desc": "从零开始的完整操作文档", "url": "./docs.html", "tag": "文档" }, { "name": "常见问题", "desc": "高频问题与排查思路汇总", "url": "./faq.html", "tag": "文档" } ]

url用相对路径是个有意的选择,比如./console.html,这样整个目录搬到任何子路径下都不会失效。如果写成/console.html,一旦部署在/entry/这样的子路径里,请求就会跑到根目录去,404 是必然的。

数据加载失败必须有降级方案。fetch出错时不要让页面空着,给一个明确提示,同时把所有可能的原因大致列一下。这个提示看起来是小事,但它是用户和你之间唯一的沟通渠道,没有它,用户只能看到白屏然后来问你“页面坏了”。

3. 五分钟实操:从零到能打开

3.1 第一步:建目录并把四个空文件铺好

打开终端,两行命令解决问题。这里不用任何包管理器,所以也没什么初始化流程要走。

mkdir mc-jc-entry && cd mc-jc-entry mkdir assets touch index.html style.css app.js sites.json

顺带把favicon.svg也放进去。图标这事别嫌麻烦,浏览器标签页上默认那个空白文档图标,在一堆标签里根本分不清,做一个纯色底加两个字母的 SVG,十几行就够。SVG 图标的好处是不用管分辨率,也不会因为尺寸不对发虚。

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> <rect width="64" height="64" rx="14" fill="#2f6fed"/> <text x="32" y="42" font-size="26" font-family="sans-serif" fill="#fff" text-anchor="middle">M</text> </svg>

index.html的<head>里记得加上favicon引用、viewport元信息、description描述。viewport漏掉的话,手机上整个页面会按 980px 宽度渲染再缩小,字小到看不清,这是移动端适配最常见的第一个坑。

3.2 第二步:把卡片渲染逻辑写进 app.js

渲染函数的核心思路是:拿数据、清空容器、循环建节点、塞进去。用document.createElement而不是直接拼innerHTML,是为了避免数据里的特殊字符破坏结构,同时也省去手动转义的麻烦。虽然入口数据是自己维护的、可信度很高,但养成这个习惯没坏处——哪天数据源变成别人提供的,你就不用回头返工。

const grid = document.getElementById('grid'); const emptyTip = document.getElementById('empty'); const searchInput = document.getElementById('search'); const themeBtn = document.getElementById('theme'); let allSites = []; function buildCard(item) { const card = document.createElement('a'); card.className = 'card'; card.href = item.url; card.target = '_blank'; card.rel = 'noopener noreferrer'; const title = document.createElement('h2'); title.textContent = item.name; const desc = document.createElement('p'); desc.textContent = item.desc; card.append(title, desc); return card; } function render(list) { grid.innerHTML = ''; list.forEach(item => grid.appendChild(buildCard(item))); emptyTip.hidden = list.length > 0; } function filter(keyword) { const key = keyword.trim().toLowerCase(); if (!key) return render(allSites); const result = allSites.filter(item => [item.name, item.desc, item.tag].join(' ').toLowerCase().includes(key) ); render(result); } searchInput.addEventListener('input', e => filter(e.target.value)); document.addEventListener('keydown', e => { const inField = ['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName); if (e.key === '/' && !inField) { e.preventDefault(); searchInput.focus(); } }); fetch('./sites.json') .then(res => { if (!res.ok) throw new Error('HTTP ' + res.status); return res.json(); }) .then(data => { allSites = data; render(allSites); }) .catch(err => { grid.innerHTML = ''; emptyTip.hidden = false; emptyTip.textContent = '入口数据加载失败:' + err.message + ',请检查文件路径与访问方式。'; });

target="_blank"配rel="noopener noreferrer"是必须成对出现的。少了noopener,新打开的页面能通过window.opener反过来操作当前页,是个已知的安全隐患;noreferrer则顺带把来源信息也拦住。

3.3 第三步:主题切换与偏好持久化

主题这块单拎出来说,因为它有一个“闪烁”问题需要处理。如果等脚本执行完再切换到深色,用户会先看到一帧白屏再变黑,观感很差。解决办法是在head里内联一小段脚本,在页面渲染前就把>(function () { const saved = localStorage.getItem('mcjc-theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const theme = saved || (prefersDark ? 'dark' : 'light'); document.documentElement.dataset.theme = theme; })();

切换逻辑放在app.js里,读取当前值取反,写回dataset和localStorage。用try...catch包一下localStorage的读写,因为在部分隐私模式下访问它会直接抛异常,不包的话整个脚本就停在那儿,后面的渲染逻辑全部不执行,页面照样白屏。

themeBtn.addEventListener('click', () => { const current = document.documentElement.dataset.theme; const next = current === 'dark' ? 'light' : 'dark'; document.documentElement.dataset.theme = next; try { localStorage.setItem('mcjc-theme', next); } catch (e) { /* 存储不可用时静默降级,仅本次会话生效 */ } });

3.4 第四步:本地起服务预览,别直接双击文件

这一步是新手最容易翻车的地方。双击index.html用file://协议打开,样式和脚本能跑,但fetch('./sites.json')会失败,控制台报一串跨域相关的错误。原因不复杂:file://下浏览器把本地文件视为不同源,fetch请求被安全策略拦截。这不是你代码写错了,是协议限制。

绕过方式很简单,起一个本地静态服务即可。装过 Python 的话,一行命令:

python3 -m http.server 8080

然后在浏览器访问http://localhost:8080。没装 Python 也行,Node 环境用npx serve一样的效果,或者干脆用编辑器自带的预览插件。核心就是让页面走http://而不是file://。

注意:本地服务起来的页面,改动文件后刷新即可看到效果。但如果浏览器缓存比较激进,可能刷不出新样式,这时候用强制刷新(多数浏览器是 Ctrl+F5 或 Cmd+Shift+R),比清缓存省事。

3.5 第五步:托管上线与缓存参数怎么设

静态文件上线,本质就是把这四个文件传到一个能对外提供静态资源的地方。这里不讲具体平台,讲原理和参数,因为无论放哪儿,规矩是一样的。

第一,路径全部用相对路径。index.html引用./style.css,app.js请求./sites.json,卡片地址也写成相对路径。这样整个目录可以挂在根路径,也可以挂在/tools/mc-jc/这样的子路径,不用改任何一行代码。

第二,HTML 不缓存,静态资源长缓存。index.html设Cache-Control: no-cache或者很短的max-age,保证用户每次拿到最新的页面结构;style.css、app.js、图标这类带文件指纹的可以设很长的max-age。如果没有构建流程加指纹,那就折中:把max-age设成几小时,别设成年。

第三,sites.json的缓存要短。它是内容层,改得最频繁,设成no-cache或者max-age=60比较合适。要是设成长缓存,你改完链接自己能看到,同事打开还是旧的,然后跑来问你为什么不生效,这种沟通成本比省下的那点带宽贵得多。

第四,确认 MIME 类型。.json必须是application/json,.css是text/css,.js是text/javascript。多数托管环境会自动处理,但自己搭服务的时候容易漏。类型不对时浏览器会拒绝执行,表现为“文件明明存在但功能不生效”。

4. 常见问题排查实录

4.1 打开页面一片白,什么都没有

白屏是最高频的问题,排查顺序按“先看控制台,再看网络,最后看代码”来,别一上来就怀疑自己逻辑写错。

第一步,打开浏览器开发者工具的控制台。如果有一条红色的Uncaught SyntaxError,那就是 JS 语法写错了,通常是少了个括号或逗号,错误信息里会直接给出行号。第二步看网络面板,找sites.json那一条,状态码是 404 就是路径写错,是 200 但内容为空就是文件真没内容。第三步才轮到看渲染逻辑,比如getElementById拿到的元素是不是null——脚本写在body里元素之前,就是这个问题,把script标签挪到body末尾或者加defer属性都能解决。

还有一种白屏是“有内容但看不见”,多半是颜色问题。深色模式下文字和背景撞色,或者变量没被正确覆盖,看起来就是空白。用开发者工具的元素检查面板手动改一下背景色,能立刻判断是不是这个原因。

4.2 样式完全没生效,页面变成纯文本

样式全丢,第一反应应该是去看style.css的请求。如果网络面板里这条请求是 404,那问题在路径;如果是 200 但样式没应用,问题在 CSS 本身。

路径问题里最常见的是大小写。本地环境某些文件系统不区分大小写,Style.css和style.css访问都一样,部署到区分大小写的环境上立刻 404。所以从第一天起就统一小写,别给自己埋雷。

CSS 本身的问题,按这几种查:选择器写错(#grid写成.grid)、属性值缺单位(padding: 18而不是18px)、变量拼错(定义了--brand却引用--brand-color,引用不存在的变量不会报错,只会静默失效)。最后一种最难查,因为控制台干干净净,什么都不提示。

4.3 fetch 报错或数据加载不出来

报错信息大致分两类,判断方法看协议。地址栏是file://开头的,那就是前面说的本地文件跨域限制,起个本地服务即可。地址栏是http://还报错的,看具体信息:404是路径问题,检查sites.json到底在不在那个位置;Unexpected token开头的是 JSON 格式错误,多半是最后一个对象多了个逗号,或者引号用了中文全角。

JSON 格式错误有个很实用的排查技巧:把文件内容整个粘到浏览器的控制台里,用JSON.parse()跑一遍,报错信息会指出具体位置。比肉眼逐行扫快得多。

还有一种是“数据加载了但没渲染”。这时候在.then里加一句console.log(data),看看拿到的是数组还是对象。如果sites.json最外层写成了{"list": [...]}这样的对象,而代码按数组处理,forEach就会报not a function。

4.4 手机上卡片挤成一列或者横向溢出

移动端的问题基本集中在三处。宽溢出最常见的原因是某个元素设了固定宽度,比如卡片写死width: 420px,窄屏下直接撑破视口,页面出现横向滚动条。用max-width: 100%或者干脆改成 Grid 的弹性布局能解决。

卡片在手机上只剩一列是正常的,minmax(260px, 1fr)在 375px 宽的屏幕上确实只够放一列。想在小屏上更紧凑,可以在媒体查询里把最小值降到 220px 左右,同时把内边距从 18px 收到 14px。但别低于 200px,否则描述文字会被压成每行两三个字,阅读体验反而更差。

还有一种是点击区域太小。手机上卡片之间如果挨得太近,容易点错。把gap保持在 12px 到 16px 之间,卡片高度不低于 72px,基本就不会误触了。

4.5 常见问题速查表

现象最可能原因排查动作解决方式
整页白屏JS 语法错误看控制台第一条红色报错按行号修语法
有 HTML 无样式CSS 路径 404 或大小写不符网络面板查 style.css统一小写、改相对路径
卡片不出现JSON 加载失败看有没有 Uncaught 报错起本地服务、修 JSON 格式
搜索无反应事件未绑定或元素 id 不符控制台打印元素引用核对 id 与脚本位置
主题切换无效变量覆盖层级不对检查 dataset 写在哪个元素写到 documentElement
手机横向滚动元素固定宽度超出视口逐个隐藏元素定位改弹性宽度
上线后 404用了根路径引用看请求地址是否缺前缀全改相对路径
改了内容不生效缓存策略过长强刷后是否正常缩短 json 缓存时间

5. 几个踩过坑之后才明白的经验

5.1 数据与视图分离这一步,早做早省事

我一开始做入口页也是硬编码,十个卡片复制十遍结构,当时觉得快。三个月后要加一个“分类分组”的功能,我得把十段重复标签逐个改,改到第六个就开始怀疑人生。抽成 JSON 之后,同样的改动只是给渲染函数加一层分组逻辑,数据一行不动。

这件事的普适价值在于:只要一份内容会出现两次以上,它就该被抽出来。入口页里每条链接的名称、描述、地址,在搜索、渲染、排序里都要用到,散在 HTML 里就意味着三处维护。抽出来之后只有一处,剩下两处都是读。这个判断标准放到任何项目里都成立。

配套的一个小习惯是给 JSON 加字段时保持向后兼容。比如后面想加一个hot: true标记热门入口,渲染时用item.hot判断,没这个字段的条目照样正常显示,不用回头把旧数据全补一遍。

5.2 入口页的安全边界要提前划清楚

入口页本身没有后端,攻击面很小,但有两点必须守住。第一,只放可信的链接。这个页面很容易变成“什么链接都往里塞”的杂物间,一旦混进来源不明的地址,点进去的风险就落在使用者身上。我的做法是每条入口在提交前都过一次人工确认,宁可少放,不放不确定的。

第二,新窗口打开必须带rel="noopener noreferrer"。这个属性组合前面提过,这里再说一次是因为它真的容易被忘。少了它,被打开的页面可以通过window.opener.location把当前页重定向到别处,用户看到的是“我点了个链接,回到入口页却变成了另一个网站”,很难解释清楚。

还有一点是别在页面里放任何敏感信息。入口页是公开可访问的静态文件,写在里面的东西等于公开。地址、名称、说明这些是设计上就要公开的,除此之外的东西一概不放,包括任何看起来“只有内部人看得懂”的缩写。

5.3 这个页面后续还能怎么长

入口页做出来之后,最容易想到的扩展是按标签分组显示。sites.json里已经有tag字段,渲染时先按tag归并,每组前面加个小标题,页面结构立刻清晰不少。再进一步,把标签做成可点击的筛选条,点“文档”就只剩文档类入口,和搜索框互补。

第二个方向是加一个“最近访问”区。用localStorage记录点击过的入口,取最近五条显示在顶部。这个功能对高频使用者很实用,实现也就是在卡片的click事件里写一条记录,成本很低。

第三个方向是状态提示。如果某个入口暂时不可用,卡片上可以显示一个灰态标记。但这需要数据层支持,得在sites.json里加status字段并手动维护,或者接一个轻量的可用性检查。前者简单但容易过期,后者要写逻辑但更准,按实际需要选。

我个人在实际操作中的体会是,入口页这类小工具的价值不在于技术多复杂,而在于它是否真的被用起来。判断标准很朴素:如果两周后还有人主动打开它找东西,说明入口收拢得对;如果没人用,多半是入口不全或者分类不清,回去补数据比加功能更管用。