我之前的个人博客是通Hexo来进行搭建的,用了一个landscape-jr0cket主题,用了10多年了,当初搭建的记录还在:通过Hexo在GitHub搭站全记录。不过我现在不喜欢了,我也不想继续用Hexo来创建静态的站点内容了,里面我修改的书签导航栏自适应也不好,对我来说CSS布局这些看着头疼。于是让「Claude-Opus-5」帮我搭建一个工具,自动生成静态站点,Pormpt如下:
我之前的个人博客是通过hexo来进行搭建的,我想你帮我重写一套基于md生成静态站点的工具,工具要简化一点,但是生成的静态站点的UI样式要符合现代的审美,简约而优雅,可以参考:https://investment.walkerdu.com;
排版的布局的要求:还是和以前一样:https://walkerdu.com/,主站左右布局,左侧是文章列表,右侧是分类,标签,年份汇总等;
文章主页默认需要有最近的「上一篇」和「下一篇」文章;且文章右下脚需要有「顶部」和「底部」的加速滚动按钮;
静态站点需要支持disqus的评论模块,像之前hexo那样,最好有浏览数据量得PV数据;
先贴最终重构的效果如下:
第一阶段:重写生成器
架构设计
AI实现设计了一个mdsite Python工具,单包构建,零前端依赖。最终目录结构如下:
textmd_blog/
├── *.md 文章(front-matter 控制是否发布)
├── <文章名>/ 配图目录
└── homepage/ 站点相关的一切
├── site.yml 配置
├── mdsite/ 生成器 Python 包
│ ├── config.py 配置加载
│ ├── loader.py Markdown 解析 + front-matter
│ ├── render.py HTML 渲染
│ ├── builder.py 产物构建
│ └── themes/journal/ 主题(模板 + CSS + JS)
├── cf-worker/ Cloudflare Workers PV 计数器
├── Dockerfile
└── public/ 构建产物
整个生成器只有 5 个模块、约 1800 行 Python,依赖 5 个包:
| 包 | 用途 | 为什么选它 |
|---|---|---|
markdown-it-py |
Markdown → HTML | 严格遵循 CommonMark,插件机制干净 |
mdit-py-plugins |
front-matter / 任务列表 | 官方插件集 |
Jinja2 |
模板渲染 | 支持继承 + include,够用且无运行时 |
Pygments |
代码高亮 | 构建期高亮,前端不用加载 highlight.js |
PyYAML |
配置与 front-matter 解析 | — |
Pillow 只在开启图片转换时才需要,默认关闭。
构建管线:Markdown 是怎么变成网页的
python -m mdsite build 这一条命令内部是一条五段流水线。整体数据流:
几个设计上值得说明的点:
配图走的是「旁路」。图片不经过 Markdown 渲染器,而是由 images.py 直接从源目录镜像到产物目录。但它必须先于 render.py 执行 —— 因为渲染器改写 <img> 标签时需要知道处理后的文件名和真实宽高。这个顺序依赖在 build() 里是显式的:
python# Assets first: the renderer needs the processed filenames and
# dimensions to rewrite <img> tags.
for p in posts:
self._copy_post_assets(p)
self.images.flush()
for p in posts:
render_post(p, self.cfg)
上一篇/下一篇在渲染前就串好。load_posts() 返回的列表已经按日期倒序,所以相邻关系是纯索引操作:
pythonfor i, p in enumerate(posts):
p.newer = posts[i - 1] if i > 0 else None # 更新的一篇
p.older = posts[i + 1] if i + 1 < len(posts) else None
模板上刻意没用含糊的 prev/next,而是「← 上一篇(更新)」「下一篇(更早)→」并各自带日期,避免方向歧义。
一份 Post 列表复用九次。首页、分页、文章页、分类页、标签页、归档总览、归档年份页、RSS、sitemap、搜索索引全部从同一个 posts 列表派生,没有二次解析 Markdown。65 篇文章全量构建 2.5 秒,其中绝大部分时间花在 Pygments 高亮上。
单篇文章的七次变换
把镜头推近到一篇具体的文章,看每一步到底改了什么:
对应的代码就是 render.py 里这几行,顺序不能换:
pythondef render_post(post, cfg) -> None:
html = _MD.render(post.content_md) # ③ Markdown → HTML
html = _inject_heading_ids(html) # ④ 标题注入锚点 id
html = _rewrite_images(html, post, cfg) # ⑤ 图片路径 + 宽高 + lazy
post.content_html = html
post.toc_html = _extract_toc(html) if post.toc else "" # ⑥
post.summary_html = _make_summary(html) # ⑥
为什么 TOC 必须从渲染后的 HTML 里抽、而不是从 Markdown 源码里用正则找 ##:因为代码块里可能有 # 注释,源码正则会把它误判成标题。等 Markdown 渲染完,<h2> 已经是语义明确的结构,再抽取就不会出错。
Markdown 渲染器的配置也很克制:
pythonmd = (MarkdownIt("commonmark", {"highlight": _highlight_code})
.enable("table")
.enable("strikethrough"))
front_matter_plugin(md)
tasklists_plugin(md)
只在 CommonMark 基础上开了表格、删除线、任务列表三项。代码高亮通过 highlight 回调交给 Pygments,产出的是带 <span class="k"> 的静态 HTML —— 前端一行 JS 都不用加载,这也是后面性能优化能把第三方请求压到 1 个的前提之一。
第二阶段:相关优化
性能诊断
站点部署到 Cloudflare Pages 之后,第一感觉是「慢,但说不清慢在哪」。这一节记录的是完整的定位过程,而不只是结论 —— 因为中间有两次我自己下错了判断,都是靠数据纠回来的。
这个顺序不是事后整理出来的「最佳实践」,而是真实的推进路径,每一步都由上一步的异常数据驱动。
步骤 1–2 的经历值得单独记:第一次 curl 测出 DNS=0.0001s / TCP=0.0006s,感觉很快,第二步才发现环境里注入了 HTTP_PROXY,测到的根本不是真实链路。如果不查环境,整个后续诊断都会基于虚假数据。
步骤 3–4 先把「可疑但实际无辜」的项排掉:curl --noproxy 重测确认 br 压缩有 4:1,工作正常;cf-cache-status: DYNAMIC 说明 HTML 每次完整回源,但这是 Pages 静态站的正常行为,不是问题。
步骤 5–6 才到真正有信息量的部分:外部域名清单暴露了 Google Fonts 和 Disqus 带出的广告追踪链,Playwright 瀑布流确认 51 张图并发拖死了 load 事件。
步骤 7–8 回到磁盘核实量级,并做带宽对照组 —— 这一步决定了哪些能用代码修,哪些代码解决不了。
性能基线与瓶颈分布
用 Playwright 绕过代理实测(3 轮中位数):
| 指标 | 首页 | 文章页 truenas-sth |
|---|---|---|
| TTFB | 2373ms | 1885ms |
| FCP | 6064ms | 3368ms |
| DOMContentLoaded | 7205ms | 3822ms |
| load | 8396ms | 60s 未完成 |
| 请求数 | 9 | 33 |
| 传输量 | 100KB / 24.7KB(br) | 125KB / 31.4KB(br) |
三组数据合起来看,时间花在哪、体积堆在哪一目了然:
A 图的结论有点反直觉:首页 FCP 6 秒里,自己的代码只占 227ms,其余 98% 是「回源到洛杉矶」+「等 Google Fonts」。 B 图同样:260MB 产物里 HTML/CSS/JS 加起来只有 11.6MB,图片占 195MB,生成器自身的质量在这道题里根本不是矛盾所在。
根因分析
根因按权重排序:
1. 图片未优化(文章页决定性)
单张 jellyfin_5.png 4.2MB、jellyfin_10.png 3.6MB,Retina 截图像素冗余。全站图片 ~195MB。
更关键:<img> 标签没有 loading="lazy"、没有 width/height,导致 51 张图全部并发加载,把跨洋带宽打满。
2. Google Fonts 阻塞渲染(首页决定性)
fonts.googleapis.com 的 CSS 是 head 里的 render-blocking stylesheet,裸 curl 测 TLS 握手 10.86 秒。
好消息:CSS 字体栈本来就有 Songti SC/PingFang SC 兜底,删掉 Google Fonts 在 Apple 设备上视觉几乎无损。
3. Disqus 立即加载
文章页一打开就加载 Disqus,连带拉起:
c.disquscdn.com(16.7s)cdn.viglink.com广告联盟(20.1s 未返回)d-code.liadm.comLiveRamp 身份追踪(21.7s)
改成 IntersectionObserver,滚动进视口才加载。
4. 跨洋链路带宽(根本瓶颈,代码解决不了)
实测吞吐量:
text第1次 25 KB/s(60s 只传 1.5MB,超时)
第2次 失败(0 bytes)
第3次 9.8 KB/s
第4次 7.4 KB/s
第5次 23 KB/s
平均有效带宽约 15 KB/s。cf-ray 全部 -LAX 后缀,路由到洛杉矶。代码层面的任何优化都解决不了这个问题,唯一出路是换部署节点或接国内 CDN。
5. 全站 canonical 指向首页的 SEO bug
html<link rel="canonical" href="{{ canonical | default('/') }}">
builder.py 从没传过 canonical 变量,所以全站每一页的 canonical 都是首页 URL。Google 把所有文章当首页副本,文章不被收录。
同理 page_desc 也没传,全站 description 相同。
优化实施
- 图片懒加载:
<img>自动加width/height+loading="lazy"。
注意:加了 width/height 之后,CSS 必须同时有:
cssimg { max-width: 100%; height: auto; }
否则 max-width:100% 压缩宽度时高度属性不变,所有超宽图被拉伸变形。
- 图片转换(图片优化引擎,作为可选开关):
实测压缩收益(truenas-sth,48 张静图):
| 方案 | 原始 | 结果 | 代价 |
|---|---|---|---|
| 同格式重压(PNG optimize + 限宽 1600) | 30.7MB | 19.3MB(省 37%) | URL 不变 |
| 转 WebP q82 + 限宽 1600 | 30.7MB | 5.0MB(省 84%) | img 路径变 .webp |
动图的反常识:动画 WebP 在 q90 下比源 GIF 更大(248.5MB → 316.9MB)。GIF 的 256 色调色板对录屏极高效。必须单独设 animate_quality: 70,且加体积保护(输出 ≥ 源文件时直接放弃转换)。
- 去掉 Google Fonts:
css--font-serif: Georgia, 'Songti SC', 'STSong', 'Source Han Serif SC',
'Noto Serif CJK SC', 'Noto Serif SC', SimSun, serif;
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', sans-serif;
拉丁字体排前面让 ASCII 命中,中文回落到 CJK 族。
优化后文章页实测(本地):
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 请求数 | 33 | 11 |
| 传输量 | 31MB(load 60s 超时) | 0.69MB |
| 第三方 | disqus+viglink+liadm | 仅 hm.baidu.com |
| 图片 | 全部并发 | 只加载首屏 7 张 |
最终成果
| 指标 | 重构前(Hexo) | 重构后(本地实测) |
|---|---|---|
| 首屏图片加载 | 51 张全并发 | 7 张(lazy) |
| 第三方请求 | Disqus + viglink + liadm(20s+未返回) | 仅百度统计 |
| Google Fonts | render-blocking,TLS 握手 10.86s | 已移除 |
| Disqus 加载时机 | 页面打开立即 | 滚动进视口时 |
| /static/* 缓存 | max-age=14400(被区域设置覆写) | 待 Dashboard 改为 Respect |
| canonical | 全站指向首页 | 逐页正确 |
| 构建时间 | hexo generate(Node.js 依赖) | < 3 秒 |
| 侧边栏评论 | 无 | hybrid 模式,CLS=0.0000 |
还没解决的:跨洋带宽问题(约 15KB/s,LAX 节点),这需要换部署位置或接国内 CDN,不是代码能解决的。
浏览器缓存陷阱&Cloudflare 缓存头
开发过程中遇到改了 CSS 但页面看起来没变化。
根因:http.server.SimpleHTTPRequestHandler 只发 Last-Modified,不发 Cache-Control,浏览器对这类响应做启发式缓存,直接复用旧 CSS,连请求都不发。
辨认技巧:旧 CSS 有 @media (prefers-color-scheme: dark) 会在暗色系统下变黑底;新 CSS 只有 [data-theme="dark"]。看到黑底就知道浏览器没取新文件。
修法:dev server 的 end_headers() 注入:
pythonself.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
self.send_header("Pragma", "no-cache")
self.send_header("Expires", "0")
生产侧修法:静态资源内容指纹。对 theme 下所有 css/js 算 sha256 取前 10 位,通过模板变量注入:
html<link rel="stylesheet" href="/static/css/journal.css?v={{ asset_v }}">
_headers 是 Cloudflare Pages 提供的一个纯文本配置文件,用来自定义静态资源响应的 HTTP 头——可以覆盖、新增或移除 Cloudflare 默认下发的响应头,而不需要写任何服务端代码。典型用途
- 安全加固:设置
Content-Security-Policy、X-Frame-Options、Referrer-Policy、Permissions-Policy等 - 缓存优化:对带 hash 指纹的静态文件设置
Cache-Control: ... immutable,提升回访加载速度 - CORS 跨域:
Access-Control-Allow-Origin: *(可配合占位符只放行特定子域) - 禁止收录:给
*.pages.dev预览域名加X-Robots-Tag: noindex,避免被搜索引擎索引
最开始以为 _headers 文件没有被部署进去,用户截图证明这个结论是错的。
真因 1:Cloudflare Pages _headers 是所有匹配规则合并
这是最关键的发现。/feed.xml 的响应头把问题暴露得很直白:
textcache-control: public, max-age=3600, public, max-age=0, must-revalidate
↑ /feed.xml 规则 ↑ /* 兜底规则被拼接上来了
我在 _headers 的 /* 兜底里写了 Cache-Control: public, max-age=0, must-revalidate,它被合并进了每一条更具体的规则。/static/css/journal.css 实际拿到的是:
textpublic, max-age=31536000, immutable, must-revalidate
↑ 我的 ↑ /* 污染进来的
immutable 和 must-revalidate 同时存在,等于长缓存被当场废掉。这就是为什么 cf-cache-status 一直是 REVALIDATED 而不是 HIT。
修法:/* 兜底只留安全头,不写 Cache-Control:
text/*
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
HTML 自然回落到 Pages 默认的 max-age=0, must-revalidate,正是想要的效果。
工程教训:Cloudflare Pages
_headers里给/*设 Cache-Control 是反模式。
真因 2:区域级 Browser Cache TTL 覆写源站 max-age
实测对比:
text/static/css/journal.css cf=REVALIDATED max-age=14400 ← 被覆写
/ cf=DYNAMIC max-age=0 ← 原样透传
/feed.xml cf=DYNAMIC max-age=3600 ← 原样透传
规律:只有走边缘缓存的响应被改成 14400(= 4小时,免费版默认值),DYNAMIC 的不受影响。
修法(需要在 Dashboard 手动操作):
textCloudflare Dashboard → walkerdu.com → Caching → Configuration
→ Browser Cache TTL → Respect Existing Headers
不改这项,max-age=31536000, immutable 永远只会以 4 小时生效。
关于Cache-Control的使用场景如下:
| 组合 | 语义 | 用在哪 |
|---|---|---|
public, max-age=31536000, immutable |
缓存一年且永不验证 | 带指纹的静态资源(/assets/*) |
no-cache |
每次回源验证,304 省流量 | index.html 等入口文档 |
| `no-store | 完全不落盘 | 交易、隐私数据接口 |
private, max-age=300 |
仅浏览器缓存 5 分钟 | 用户个性化页面 |
public, max-age=0, s-maxage=86400 |
浏览器每次验证、CDN 缓存一天 | 内容频繁更新但想挡源站压力的页面 |
public, max-age=60, stale-while-revalidate=600 |
1 分钟新鲜,过期 10 分钟内边用边更新 | API 列表、排行榜类接口 |
侧边栏最近评论
用户要求:右侧分类上面加一块最近评论,且不能是构建期拉取(新评论要等下次部署才出现是不能接受的)。
数据源选择,实测两个端点:
| 端点 | CORS | API key | 结论 |
|---|---|---|---|
{shortname}.disqus.com/recent_comments_widget.js |
❌ 无 | 不需要 | 采用 |
disqus.com/api/3.0/posts/list.json |
部分 | 需要 | 支持 JSONP,留作升级 |
widget 端点没有 CORS 头,fetch() 走不通。它用 document.write 输出 HTML,唯一的方法是注入 <script> 并临时借用 document.write 收集输出。
安全劫持 document.write的关键实现:
javascriptconst origWrite = document.write;
const buf = [];
document.write = function(str) {
if (typeof str === 'string' && str.indexOf('dsq-widget') !== -1) {
buf.push(str); // 只吞自己的输出
} else {
origWrite.apply(document, arguments); // 其他原样转发
}
};
script.onload = () => settle(buf.join('')); // 加载完成后无条件恢复
const timer = setTimeout(() => settle(null), cfg.timeout); // 硬超时
document.head.appendChild(script);
注意:动态注入的 script 是 async,浏览器本会忽略 document.write,但我们已劫持,所以能正常收集。
hybrid 模式:比纯客户端更优
三种模式对比:
| mode | 新评论 | Disqus 被墙的读者 | HTML | 构建耗时 |
|---|---|---|---|---|
| hybrid(推荐) | 实时 | 看到构建期快照 | +1.5KB | +1.3s |
| client | 实时 | 整块超时后消失 | +0 | +0 |
| build | 要等下次部署 | 看到快照 | +1.5KB | +1.3s |
hybrid = 构建期烤一份快照进 HTML(兜底) + 浏览器异步覆盖成最新。
实测:
text无评论块基线: FCP 108ms DCL 92ms 资源请求 7
有评论块 : FCP 108ms DCL 90ms 资源请求 7 ← 逐项相同
hybrid 可达 : FCP 164ms CLS 0.0000
hybrid 被墙 : FCP 108ms CLS 0.0000(快照兜底,5 条照常显示)
对主站 FCP 和请求数零影响,通过三条机制保证:
requestIdleCallback延后发起- 骨架占位与真实结构同形,替换时 CLS=0
- 客户端硬超时 8 秒(必须自己计时,Disqus 在被墙网络上
script.onerror可能要等几分钟)
UI布局设计
参考 investment.walkerdu.com 的设计 token 重新对齐:
| Token | 值 | 含义 |
|---|---|---|
--bg |
#F7F5F3 |
米白底色 |
--surface |
#FFFFFF |
卡片底色 |
--text |
#2D2926 |
暖黑正文 |
--accent |
#D97706 |
琥珀橙强调 |
--accent-soft |
#FEF3C7 |
强调色浅版 |
--font-serif |
Georgia, 'Songti SC', ... |
衬线正文 |
--font-sans |
-apple-system, 'PingFang SC', ... |
元信息 |
最终文章页采用三栏布局:
text┌──────────┬────────────────────┬────────────┐
│ TOC │ 正文内容 │ Sidebar │
│ 220px │ flex: 1 │ 300px │
│ sticky │ max-width 受 grid │ │
└──────────┴────────────────────┴────────────┘
断点逻辑:
> 1300px:三栏1100–1300px:TOC + 正文,sidebar 下沉到第 2 列下方< 1100px:单栏,TOC 转顶部可折叠<details>
踩坑:sticky 元素与跨列 grid 冲突
≤1300px 时 sidebar 跨列到第一列,sticky 的 TOC 会一直停留在视口,与跨列的 sidebar 视觉重叠。
修法:改 grid-column: 2 只占正文列;≤1100px TOC 隐藏后再 grid-column: 1。
css.article-aside {
align-self: start; /* 把 sticky 盒子锚定在所在行顶部 */
}
静态站点的 UI 是怎么组合工作的
重构完之后我自己也有个疑问:主站和每一篇文章右侧的分类、标签、归档都长得一模一样,它们是同一个页面被引用了,还是被复制了 N 份? 三栏布局又是怎么"接"在一起的?
这部分对不写前端的人来说最容易误解,单独拆开讲。模板不是运行时引用,是构建期的宏展开
整站的模板只有 11 个文件,关系是两层:
- 继承:
base.html提供<head>/<header>/<main>/<footer>骨架,中间用{% block main %}挖一个洞。其余 9 个页面模板(首页、文章页、分类页、归档页、404……)全部{% extends "base.html" %},各自往洞里填自己的内容。 - 包含:
sidebar.html是一个独立片段,被 6 个模板{% include %}进去。
关键在于 Jinja2 的 extends / include 等价于 C 的 #include,是构建期的文本展开,不是运行时的动态引用:
| 源文件 | 产物 public/ |
|
|---|---|---|
sidebar.html |
只有 1 份 | 被逐字复制进 204 个 HTML 文件 |
| 浏览器请求 | — | 只下载 1 个 HTML,不发起任何"子页面"请求 |
这跟 PHP / 模板引擎那种服务端渲染正好相反:运行时没有任何组装逻辑,全部拼接在 mdsite build 那不到 3 秒里就结束了。服务器只需要会发文件,所以 CDN 能扛全部流量,也不存在后端挂掉这回事。
验证也很直接 —— 抽 4 个不同文章页,把 <aside class="sidebar"> 整段取出来算 MD5:
text7d41b79a3923 19552 bytes /2016/01/11/boost-ipc-cpp-alloc-construct/
7d41b79a3923 19552 bytes /2016/01/14/boost_ipc_pack/
7d41b79a3923 19552 bytes /2016/01/14/hexo-construct-homepage/
7d41b79a3923 19552 bytes /2016/03/21/nginx-conf/
哈希逐字相同,确实是复制。这样冗余吗?算一笔账
textHTML 文件数 204
HTML 总体积 10.53 MB
其中重复的侧栏 3.80 MB(占 36%)
单页原始 128.5 KB
单页 gzip 后 31.0 KB(压掉 76%)
36% 是纯冗余,听起来很浪费。但重复文本恰恰是压缩算法最擅长的东西 —— gzip 之后这部分几乎免费。换来的是整站零动态逻辑、可以纯 CDN 分发。
真正的代价在另一头:改一行侧栏,得重新生成全部 204 个文件。这也解释了为什么我需要那个 build --clean 开关 —— 文章下线、改 slug、改主题名之后,旧文件不会自己消失。
CSS Grid
三栏不是"拼接",是CSS Grid排出来的:
HTML 里这三块是顺序排列的三个块级元素,按默认流会上下堆叠。横向三列完全是 CSS 给的:
css.article-layout {
display: grid;
grid-template-columns: var(--toc-w) minmax(0, 1fr) var(--side-w);
/* 220px 剩余可伸缩 300px */
gap: var(--col-gap);
align-items: start;
}
minmax(0, 1fr) 是中间的正文列:吃掉两侧固定宽度之外的全部剩余空间。minmax(0, ...) 而不是裸 1fr,是为了防止超宽的表格或代码块把这一列顶破 —— 这是 Grid 的一个经典陷阱,1fr 的最小尺寸默认是 auto,会被内容撑开。
窄屏时我只换掉这一行:
css@media (max-width: 1100px) {
.article-layout { grid-template-columns: minmax(0, 1fr); }
}
三栏立刻回落成上下堆叠,HTML 一个字都不用改。这就是「HTML 管结构、CSS 管表现」在实际项目里的样子。
完整的组合链条如下:
textmd 源文件
│
├─ loader.py 解析 front-matter,筛出 status: true 的文章
├─ render.py markdown-it-py 转 HTML 片段,抽 TOC,改写图片路径
├─ images.py 读图片尺寸,写 width/height + loading="lazy"
│
├─ builder.py Jinja2 套模板
│ base.html 骨架
│ └─ post.html 内容
│ └─ include sidebar.html
│
└─ 写出一个完整独立的 index.html
│
▼
浏览器下载:1 个 HTML + 1 个 CSS + 1 个 JS
│
├─ CSS Grid 把三个 div 排成三列
└─ JS 做可选增强(目录高亮 / 代码复制 / 灯箱 / 最近评论)
零前端框架
整套 UI 没有 React、没有 Vue、没有任何构建工具链,只有:
| 层 | 用了什么 | 规模 |
|---|---|---|
| 结构 | Jinja2 模板 | 11 个文件 |
| 样式 | 手写 CSS | 940 行,15 个区块 |
| 行为 | 原生 JS(一个 IIFE) | 约 400 行 |
| 代码高亮 | Pygments,构建期完成 | 前端零成本 |
不用框架不是复古情怀,是场景不匹配:静态博客没有运行时状态管理的需求,引入框架只会带来 bundle 体积和 hydration 开销,而这两样正是我这次要砍掉的东西。
JS 全部是可选增强 —— 禁用 JavaScript 之后文章照样能读、导航照样能点,只是没有目录滚动高亮和代码复制按钮。这是渐进增强(progressive enhancement)的基本要求,也是我在性能优化阶段能把 Disqus 和最近评论都改成"滚动进视口才加载"的前提:它们从一开始就不在关键渲染路径上。
CSS Grid 调试器(可以直接拖)
上一节说三栏是 grid-template-columns: 220px minmax(0, 1fr) 300px 排出来的,但光看代码没什么体感。既然这套生成器的 Markdown 渲染器开了 html: true,那就干脆把一个实时调试器直接写进 md 源文件里:
拖一下控件,下面的代码块会同步输出当前的 CSS。几个值得试的点:
220px minmax(0, 1fr) 300px:就是本站文章页的真实配置。minmax(0, ...)而不是裸1fr的原因在上一节说过 ——1fr的最小尺寸默认是auto,超宽代码块会把正文列顶破。repeat(auto-fit, minmax(90px, 1fr)):把窗口拖窄,列数自己减少,一行媒体查询都不用写。注意auto-fit会折叠空轨道,auto-fill会保留空轨道 —— 要撑满就用前者。- 勾上「1 号跨 2 列」:
grid-column: span 2让单个格子横跨两条轨道,后面的项自动往后挤。这是 Grid 相对 Flex 最实用的能力之一。
写在末尾
这个博客的AI重构其实只花了半天不到的时间,真的有点魔幻,我作为一个服务器开发,真的对HTML/CSS这些前端语言很不熟且抵触,主要还是当初一开始大学学网页设计的时候,那个HTML/CSS怎么摆都不对的时候,心生厌恶了。但是借助AI的加持下,很快完成了个人静态博客的重构,且完全达到我想要的效果,真的觉得AI时代互联网的门槛一下子被抹平了,虽然我现在还在做服务器相关的开发,但是我觉得服务器相关的基础代码也很容易被AI取代,只是各个领域有自己的差异化设计,不过我觉得被AI全面覆盖很快就会实现,一个人维护一个大项目真的不是说说的。
现在AI主要的问题是上下文变长会导致注意力分散的问题,大家也都在通过工程化的方法来进行解决,毕竟人类也有这种问题,但是我觉得这些问题都是小问题,就像我们通过团队协作来处理庞杂的事项,多Agent协作也是为了解决单Agent面对海量知识无法注意力集中的问题,各自擅长自己的模块。
重构完博客后,我打算把自己这些年的知识和经验都整理出来,刻画一个自己的数字分生的出来,哈哈。
现在我觉得要全部精力的拥抱AI,真的,从来没有一个这么容易让自己变强大的时代,如果现在还有人不拥抱AI,真的不可能成功的。
这是一个最好的时代,这是一个最坏的时代,看你自己怎么选择!
评论