严格遵循 nojs.club:修复 P0 到 P3 级别问题

这次对博客做了一轮以 nojs.club 要求为基准的检查。核心目标很简单:页面保持纯静态、无 JavaScript 运行时依赖,同时避免搜索引擎、部署平台、无障碍体验和结构化数据层面的隐性问题。

检查后先修复了两个 P0 级别问题:Netlify 的重定向规则会把错误路径伪装成首页,结构化数据里也可能出现不存在的图片地址。随后又继续处理了三个 P1 级别问题:动效需要尊重用户的系统偏好,文章图片需要更好的原生加载属性,结构化数据也应该尽量不依赖 <script> 标签。接着补上了 P2 级别的 <head> 元信息与 404 收录控制。最后处理 P3 级别的收尾优化:链接样式、中文排版、安全响应头和仓库忽略规则。

修复 Netlify 软 404

原来的 Netlify 配置使用了 SPA 常见写法:

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

这对 React/Vue 一类前端路由应用很常见,但对静态博客并不合适。它会让所有不存在的路径都返回首页内容,并且状态码仍然是 200。这样一来,真正的 404.html 永远不会被使用,搜索引擎也容易把这些错误路径视为“软 404”。

修复方式是删除这条 SPA 回退规则,让 Netlify 使用 _site/404.html 作为真实的 404 页面。现在未命中路径会返回正确的页面与 HTTP 404 状态码。

修复 JSON-LD 中的失效图片

文章页会输出 application/ld+json 类型的结构化数据。它不是可执行 JavaScript,但会被搜索引擎读取。

之前生成器会默认写入:

"image": "https://PureMo-Blog.vercel.app/static/default-cover.png"

以及发布者 logo:

"logo": {
  "url": "https://PureMo-Blog.vercel.app/static/logo.png"
}

问题是当前仓库并没有这些图片文件。如果结构化数据指向不存在的资源,就会产生抓取错误。更进一步,Markdown 示例文章中还有一个演示用的 /logo.png 图片路径,它也不应该被误认为文章封面。

这次修复后,生成器会先检查图片是否真实存在:

  • 文章首图存在时,才写入 image
  • 没有文章首图但 static/default-cover.png 存在时,才使用默认封面;
  • static/logo.png 存在时,才写入 publisher.logo
  • 如果资源不存在,就直接省略对应字段。

这样生成的 JSON-LD 不再包含失效 URL,结构化数据更干净。

构建结果

修复后重新构建站点,_site/posts/01/index.html_site/posts/02/index.html 中的 JSON-LD 都不再包含不存在的 imagelogo 字段。

继续修复 P1:减少动态效果

站点的 CSS 中有不少细节动效,比如链接下划线、卡片 hover、404 页淡入、图片 hover 缩放和平滑滚动。它们本身不需要 JavaScript,但仍然属于浏览器端动画。

为了尊重用户的系统级无障碍偏好,这次加入了 prefers-reduced-motion: reduce

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
  }
}

这样如果读者在系统中开启“减少动态效果”,页面会自动禁用大多数动画、过渡和平滑滚动。它仍然是纯 CSS,也更符合 nojs.club 所强调的“不要替读者设备做过多假设”。

继续修复 P1:图片原生加载优化

Markdown 文章里的图片之前只会自动补 loading="lazy"。这次继续补上了 decoding="async",让浏览器可以异步解码图片,减少图片解码对页面渲染的阻塞。

对于本地 PNG、JPEG、GIF 图片,生成器还会尝试读取图片真实尺寸,并自动补上 widthheight

<img src="/example.png" loading="lazy" decoding="async" width="800" height="450">

这些属性都是浏览器原生能力,不需要任何 JavaScript。它们的作用是让页面在图片加载前就知道图片占位大小,尽量减少布局偏移。

这次检查时还发现一个顺手修复的小问题:表格包裹器之前会被生成成 class_="table-wrapper",导致 CSS 类名不生效。现在已经改为标准的 class="table-wrapper"

继续修复 P1:移除 JSON-LD script,改用 Microdata

虽然 application/ld+json 不是可执行 JavaScript,但它仍然写在 <script> 标签里。对 no-js.club 的 GTmetrix 路线来说通常没有问题;但如果目标是更严格地靠近 nojs.club 精神,最好让最终 HTML 里完全没有 <script>

这次把文章页结构化数据从 JSON-LD 改成 HTML5 Microdata。文章页现在会输出类似这样的语义结构:

<article itemscope itemtype="https://schema.org/Article">
  <link itemprop="mainEntityOfPage" href="https://PureMo-Blog.vercel.app/posts/03/">
  <meta itemprop="description" content="...">
  <h1 itemprop="headline">严格遵循 nojs.club:修复 P0 到 P3 级别问题</h1>
  <time datetime="2026-05-16" itemprop="datePublished dateModified">2026-05-16</time>
  <div itemprop="articleBody">
    ...
  </div>
</article>

这样搜索引擎仍然能读到文章结构化信息,但页面不再需要任何 <script> 标签。重新构建后,全站 _site/ 中已经没有真实脚本标签。

继续修复 P2:404 不再输出 canonical

404 页面之前会输出:

<link rel="canonical" href="https://PureMo-Blog.vercel.app/404.html">

这对错误页面来说并不理想。404 页面本身不应该被搜索引擎收录,也不需要声明规范 URL。现在 404 页改为输出:

<meta name="robots" content="noindex">

同时模板会在 page_id == '404' 时跳过 canonical。重新构建后,_site/404.html 中已经没有 rel="canonical",只保留 robots noindex

继续修复 P2:补齐色彩与社交元信息

这次还补齐了几个不依赖 JavaScript 的 <head> 元信息。

首先是浏览器色彩模式提示:

<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#00796b" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)">

color-scheme 可以告诉浏览器页面同时支持亮色和暗色,theme-color 则让移动端浏览器工具栏等界面更贴近站点主题。

然后是 Open Graph 与 Twitter Card 元信息:

<meta property="og:type" content="article">
<meta property="og:title" content="页面标题">
<meta property="og:description" content="页面摘要">
<meta property="og:url" content="规范链接">
<meta property="og:site_name" content="站点名称">
<meta name="twitter:card" content="summary">

文章页还会额外输出:

<meta property="article:published_time" content="2026-05-16">
<meta property="article:modified_time" content="2026-05-16">

这些都是静态 HTML 元信息,不会引入任何脚本,但能改善链接分享、搜索摘要和页面机器可读性。

继续修复 P3:链接样式改回原生下划线

早期样式里,所有链接都会用 a::after 生成一个伪元素来模拟下划线动画。这种写法视觉上没问题,但会给每个链接额外创建伪元素,也会让普通行内链接变成 inline-block,对中文段落里的自然换行不够友好。

这次把全站链接改成浏览器原生的 text-decoration

a {
  text-decoration-line: underline;
  text-decoration-color: transparent;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color 0.2s, text-decoration-color 0.2s;
}

a:hover {
  text-decoration-color: currentColor;
}

导航、按钮、文章卡片等不需要下划线的元素,则通过选择器继续保持 text-decoration: none。这样既保留了 hover 反馈,也减少了额外伪元素和排版副作用。

继续修复 P3:中文正文排版

正文段落之前使用了 text-align: justifyhyphens: auto。这对英文长段落可能有帮助,但中文段落里容易造成字间距被拉开,阅读时出现不自然的空隙。

现在正文段落改为:

.post-content p {
  text-align: start;
}

同时移除了 text-justifyhyphens 相关规则。中文正文会按自然行宽排版,整体阅读体验更稳定。

继续修复 P3:安全响应头

既然目标是严格无脚本,部署层也可以明确告诉浏览器:本站不执行脚本。

Netlify 与 Vercel 都新增了响应头,其中最关键的是 CSP:

Content-Security-Policy: default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests

其中 script-src 'none' 明确禁止脚本执行。由于前面已经把 JSON-LD <script> 改成 Microdata,当前页面不再依赖任何脚本标签,因此这个策略不会破坏站点功能。

同时还补了两个常见安全头:

Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff

这些配置不会改变页面内容,但能减少错误 MIME 解析、来源泄露和嵌入风险。

继续修复 P3:仓库忽略规则

构建产物 _site/.build_manifest.json 本来已经在 .gitignore 中,这次额外补上了 .vscode/

.vscode/

这样本地编辑器配置不会被误提交,仓库更干净。

最终结果

这次改动没有引入任何前端脚本,也没有改变博客的无 JavaScript 运行方式。站点仍然保持纯 HTML/CSS 输出,同时部署行为、结构化数据、图片加载、无障碍体验、404 收录控制、社交分享信息、安全响应头和中文阅读体验都更符合静态博客应有的语义。