knife

依赖AI完全重写个人主站

目录

我之前的个人博客是通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工具,单包构建,零前端依赖。最终目录结构如下:

text
md_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 这一条命令内部是一条五段流水线。整体数据流:

mdsite 构建管线

几个设计上值得说明的点:

配图走的是「旁路」。图片不经过 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() 返回的列表已经按日期倒序,所以相邻关系是纯索引操作:

python
for 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 高亮上。

单篇文章的七次变换

把镜头推近到一篇具体的文章,看每一步到底改了什么:

一篇 Markdown 的七次变换

对应的代码就是 render.py 里这几行,顺序不能换:

python
def 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 渲染器的配置也很克制:

python
md = (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.com LiveRamp 身份追踪(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 必须同时有:

css
img { 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() 注入:

python
self.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 的响应头把问题暴露得很直白:

text
cache-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 实际拿到的是:

text
public, 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 手动操作):

text
Cloudflare 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的关键实现:

javascript
const 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 和请求数零影响,通过三条机制保证:

  1. requestIdleCallback 延后发起
  2. 骨架占位与真实结构同形,替换时 CLS=0
  3. 客户端硬超时 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:

text
7d41b79a3923   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/

哈希逐字相同,确实是复制。这样冗余吗?算一笔账

text
HTML 文件数        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排出来的:

CSS Grid 把三个 div 排成三列

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 管表现」在实际项目里的样子。

完整的组合链条如下:

text
md 源文件
   │
   ├─ 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 源文件里:

1
2
3
4
5
6

拖一下控件,下面的代码块会同步输出当前的 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,真的不可能成功的。

这是一个最好的时代,这是一个最坏的时代,看你自己怎么选择!

评论