高级网页编织:依据规范进行审计。网站

在我服役期间omg.lolIRC频道(看来我很多帖子的灵感来源!),斯基比分享了一个有趣的网站,网站规范.

当然,我得承认这个网站的美学有些可疑,但清单依然很有价值。我一直努力确保我的网页开发遵循良好的实践和良好的卫生习惯,但有很多东西我以前从未听说过或接触过。

这是一份很长的清单。基金会、SEO、可访问性、安全性、知名的URI、“代理准备度”(呃)、性能、隐私、韧性、国际化。大约有140项需要勾选,每项都有一句话说明其重要性,并且Required/Recommended/Optional/Avoid一天。

作为一个极繁主义者(换句话说,我喜欢通过在网站上实现功能来拖延写作),我决定逐一检查,看看需要修改或添加什么。

一次刻意的跳跃

首先,我跳过了整个“特工准备”类别。我不会加一个llms.txt用代码库,或者按页计价的 markdown 端点。生成式AI拿到简单易读的标记,而我们其他人却只能用更复杂的HTML,这真是个残酷的笑话,不是吗?

我们没有想办法让网页浏览器直接显示Markdown,而是用胶带粘住了机器人解决方案。

好了,先说到这里。此外,正如我之前写过的,我没有专门针对AI爬虫的软件robots.txt规则。我对猫捉老鼠的复杂封锁游戏不感兴趣。

唯一的赢棋就是不下棋。不过,我确实添加了一些有趣的蜜罐,如果你尝试访问高风险应用路径、默认CMS/根路径、秘密和配置文件位置,或者API和发现端点,可以找到它们。

如果有人感兴趣,我可能会再发帖。

代理人准备度作为一个类别,假设你想要代理人高效地阅读您的网站。与搜索引擎优化(通常只是骗局)不同,LLM的优化只是让你的内容更容易被盗,并不能帮助吸引更多人访问你的网站。

一只傻虫

当你回顾这样的审计时,会让你重新审视很久以前写过的内容。在我的案例中site.webmanifest这让我意识到它已经空了name以及short_name场:

{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png", ...

任何尝试“添加到主屏幕”的安卓用户都会得到一个没有标签的快捷方式。更糟的是,图标路径是根相关关系的 (/android-chrome-192x192.png当实际文件存在于/assets/images/favicon/android-chrome-192x192.png.自从我第一次用某个在线 favicon 工具生成清单后,清单就一直默默地出错并指向 404,之后我再也没看过输出。我两个都解决了:

{
  "name": "brennan.day",
  "short_name": "brennan.day",
  "start_url": "/",
  "scope": "/",
  "icons": [
    {"src": "/assets/images/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png"},
    {"src": "/assets/images/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png"}
  ],
  "theme_color": "#fbf1c7",
  "background_color": "#fbf1c7",
  "display": "standalone"
}

此外,令人尴尬的是,它被设置为#65312fff.我不知道我是什么时候写错字的——8位十六进制值确实有实用性,只是多了两位数字添加透明度的阿尔法通道但这里发生的并不是这个。

总之,我把六边形分成了浅暗对,和我网站的调色板相匹配:

安全头部,或者说:我一直拖延的事情

安全头部投入了大量工作。我的netlify.toml最初X-Frame-Options,即一个过时的 X-XSS-Protection我删除了该标题,因为它在任何当前浏览器中都没有作用,并且规格说要避免使用它。

我没有内容安全政策不高速交通系统,也没有关于交叉源地隔离.

为一个嵌入webring小部件、使用KaTeX、Mermaid并有IndieAuth评论流的网站写CSP花了不少功夫:

Content-Security-Policy = default-src 'self';
  script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'
    https://cloud.umami.is https://neatstats.net https://cdn.jsdelivr.net
    https://webring.fun https://silly.possiblyaxolotl.com
    https://bubbles.town https://status.lol;
  style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net
    https://silly.possiblyaxolotl.com;
  img-src 'self' data: https:;
  font-src 'self' data: https://cdn.jsdelivr.net;
  connect-src 'self' https://cloud.umami.is https://*.supabase.co
    https://webring.fun https://neatstats.net https://bubbles.town;
  frame-src https://www.youtube.com;
  object-src 'none'; base-uri 'self';
  form-action 'self' https://indieauth.com https://buttondown.com
    https://*.supabase.co;
  frame-ancestors 'none'; upgrade-insecure-requests;
  report-to default;

我不会假装那是严格CSP。它依然存在'unsafe-inline'无论是脚本还是样式,因为我的网站有针对关键CSS的内联块(它是有争议的做法)以及分布在模板中的多个一次性内联标签(主题切换、滚动进度、IndieAuth PKCE流程)。

要做到最好,就意味着每个脚本都要有随机数或哈希值,坦白说这不是我感兴趣的重构。我现在的设备仍然能通过锁定 XSS 漏洞来提升安全性领域可以是脚本/样式/连接/帧源。

对于Strict-Transport-Security以及Permissions-Policy没什么复杂的补充:

Strict-Transport-Security = "max-age=63072000; includeSubDomains"
Permissions-Policy = "camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()"

Cross-Origin-Embedder-Policy让我停下来思考,这个规格的默认推荐是require-corp该系统阻止任何未明确选择加入自身资源的跨来源资源Cross-Origin-Resource-Policy标题。

例如,我的网站热链接会加载来自aaronparecki.com,indieweb.org, 和mirrors.creativecommons.org这些都不是我能控制的。require-corp否则这些影像就会坏掉。

我用过credentialless相反,以同样方式隔离页面,但只是剥夺跨源请求中的凭证,而非直接屏蔽。

我还写了Reporting-Endpoints头部指向一个小巧的新Netlify函数,记录浏览器发送的任何内容:

// netlify/functions/csp-report.js
exports.handler = async (event) => {
  if (event.httpMethod !== "POST") return { statusCode: 405, body: "" };
  console.warn("[reporting-api] " + (event.body || "").slice(0, 4000));
  return { statusCode: 204, body: "" };
};

这并不是一个真正的可观测性流水线,因为它只是console.warn通过Netlify的原木排水管进入原木。但如果我的CSP为访客弄坏了什么,我就能查明。

CSP学习曲线

当然,实施CSP并不像复制规范建议那么简单。我最初的CSP在我网站上搞砸了几个东西:

  • webring 的小工具来自webring.fun以及silly.possiblyaxolotl.com停止加载
  • 该bubbles.town点赞按钮加载失败
  • 该status.lol小部件被屏蔽了
  • NeatStats 分析功能停止工作

问题是这些第三方服务会调用他们自己的域名进行API调用,而我没有把这些域名包含在我的connect-src指令。此外,有些服务会加载我没有加入白名单的CSS文件style-src.

我不得不反复添加缺失的域名:

script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'
  https://cloud.umami.is https://neatstats.net https://cdn.jsdelivr.net
  https://webring.fun https://silly.possiblyaxolotl.com
  https://bubbles.town https://status.lol;
style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net
  https://silly.possiblyaxolotl.com;
connect-src 'self' https://cloud.umami.is https://*.supabase.co
  https://webring.fun https://neatstats.net https://bubbles.town;
form-action 'self' https://indieauth.com https://buttondown.com
  https://*.supabase.co;

这里的教训是,CSP需要准确知道你的第三方服务使用哪些域名——不仅用于加载脚本,还包括他们在后台调用的任何API调用。浏览器控制台错误对调试非常宝贵。

在实施了最初的CSP后,我发现我的鲜味指标突然降到接近零。事实证明,Umami 已经更改了他们的 API 网关域名,从仅cloud.umami.is也使用gateway.umami.is,eu.umami.is,以及各种api-gateway.*.umami.dev端点。

这是一个已知的问题,会导致拥有严格CSP的站点的追踪失效。我不得不把所有这些新的网关域都添加到我的connect-src指令恢复分析功能。

我还不得不更新我的服务人员,完全跳过跨原点请求。服务工作者试图处理对外部域名的请求(webring 脚本、徽章图片等),但由于 CORS 限制,无法正确缓存,导致了“未定义”的响应错误。

通过只检查同源请求,服务工作者现在专注于我自己的内容,同时允许外部资源正常加载。

糟糕透顶的饼干

该清单的隐私类别标记了cookie同意为Required.但你猜怎么着?我的网站一点Cookie都没有,太好了!鲜味,我的分析数据是文档为无 Cookie我已经在我隐私政策,而且我网站上的其他地方也没有生成Cookie。

并非每一项都值得因为清单上说的就给出“是”必修.阅读理由说明,确认是否适用于你的网站。

除此之外,还有更好的Cookie替代品可以使用。如需保存用户偏好和设置,请使用localStorage:

// Set it
localStorage.setItem('theme', 'dark');

// Get it
const theme = localStorage.getItem('theme'); // 'dark'

// Remove it
localStorage.removeItem('theme');

本地存储的存储限制为10MB,而Cookie的存储限制为4KB。关键是数据会永久保存,除非被清除。

sessionStorage工作原理与localStorage完全相同,但用户关闭浏览器标签页时会自动清除数据。

sessionStorage.setItem('formStep', '3');

对于大型复杂数据(例如离线图片、大型缓存JSON),你可以使用localForage(一个押韵的图书馆)。原生 IndexedDB 功能强大但复杂(使用回调、光标和数据库)。

localForage是一个很小的库,提供类似localStorage的API,但数据在后台存储在IndexedDB。

// Include the library, then:
localforage.setItem('shoppingCart', { items: 200, total: '$500' });
localforage.getItem('shoppingCart').then((cart) => console.log(cart));

它处理二进制数据(blobs/文件),容量超过 50MB,并使用 Promises(异步)来避免冻结网页。

有用的服务员

我以前的服务人员缓存了/assets/css/style.css这个文件自从我把CSS拆分成多个文件后就不存在了,所以实际上它什么缓存都没有。我重写了,使得导航失败时会跳回真正的离线页面,而不是浏览器自己的“无联网”界面,图片加载时优先缓存,而不是网络优先加载:

if (event.request.mode === 'navigate') {
  event.respondWith(
    fetch(event.request).catch(() =>
      caches.match(event.request).then(response => response || caches.match(OFFLINE_URL))
    )
  );
  return;
}

我还加了一个检查,完全跳过跨来源请求,这导致了与CORS相关的错误。服务工作者现在只处理同源请求,允许外部资源(webring 脚本、徽章图片等)正常加载:

// Skip cross-origin requests to avoid CORS issues
if (!event.request.url.startsWith(self.location.origin)) {
  return;
}

终于修复了烦人的闪光灯

在测试这些更改时,我终于解决了页面加载时的闪烁问题——主内容和侧边栏的元素在页面加载时会明显变大小,无论是否首次访问。

这是因为我采用了延期技术media="print" onload="this.media='all'".这是一艘已知模式用于异步加载CSS以提升性能,但这有个令人恼火的缺点,就是导致未样式内容的闪现以及布局变更当延迟的CSS最终加载并应用时。

我只是取消了延迟加载,让CSS正常加载。代价是初始渲染稍微慢一些,但不会再闪烁了。有时候性能优化反而导致了它本该解决的问题。

其他小修正(快速射击回合!)

有几件事一旦我知道要找,就算快了:

text-wrap: balance/text-wrap: pretty在标题和正文中,浏览器能很好地处理孤儿的最后一句。

h1, h2, h3, h4, h5, h6 {
  text-wrap: balance;
}

p, li, dd, figcaption {
  text-wrap: pretty;
}

scrollbar-gutter: stable所以滚动页面(几乎每篇文章)和不滚动的页面之间宽度不会相互变化。

html {
  scrollbar-gutter: stable;
}

scroll-margin-top/scroll-padding-top关于航向和锚定目标,所以跳到#comments或者目录链接没有直接出现在置顶页下方(这个问题困扰我很久了)。

html {
  scroll-padding-top: 5rem;
}

h1, h2, h3, h4, h5, h6,
:target,
[id] {
  scroll-margin-top: 5rem;
}

A@view-transition封锁其中navigation: auto所以 Chrome(目前)会在页面加载之间进行动画。不支持的浏览器则正常导航,但只需一行选择。

@view-transition {
  navigation: auto;
}

强制色彩模式支持,适用于运行辅助功能的用户,比如 Windows 高对比度。我的彩虹导航下划线和滚动进度条纯粹是装饰,没有边框可依靠,所以这段内容允许它们保留:

@media (forced-colors: active) {
  .site-header::after,
  nav.site-nav a::after {
    background: CanvasText;
    forced-color-adjust: none;
  }

  button, .hero-btn, input[type="submit"] {
    border: 1px solid ButtonText;
  }
}

:user-valid/:user-invalid在表单字段中,CSS采用实时验证样式,而不是JS类切换,而且只有在你操作完字段后才触发,所以在你输入任何东西之前,不会弹出需要但空的输入。

input:user-invalid, textarea:user-invalid {
  border-color: var(--link-hover);
}

input:user-valid, textarea:user-valid {
  border-color: var(--nav-green);
}

容器查询在侧边栏模块中,这样徽章网格响应的是模块渲染后的宽度,而不仅仅是视口。

.sidebar .module {
  container-type: inline-size;
  container-name: sidebar-module;
}

@container sidebar-module (max-width: 220px) {
  .badges-grid {
    grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
  }
}

contain: layout paint在明信片和侧边栏模块中,告诉浏览器这些子树不能影响外部任何东西。

a.post-card {
  contain: layout paint style;
}

投机规则用于同源预取、跳过蜜罐路径以及任何标记的操作rel="nofollow":

可见的面包屑在帖子和页面上,与BreadcrumbList我已经写过但从未在网站上渲染过的 JSON-LD:


  
  1. Home
  2. Advanced Webweaving: Auditing Against Specification.Website

inputmode,enterkeyhint, 和autocomplete在评论登录、通讯、联系方式和留言簿表单中,这样正确的移动键盘就能显示:

A/500.html页面和/maintenance.html后者是有文档记录的一行重定向翻转netlify.toml以防万一需要让网站进入维护模式。(未来可能发生的不可预见灾难。):

# Commented out until I actually need it
# [[redirects]]
#   from = "/*"
#   to = "/maintenance.html"
#   status = 503
#   force = true

IndexNow,这是我之前从未听说过的规程。Bing、Yandex 和其他几个软件允许你推送“已更改”的延迟,而不是等待爬行。生成一个密钥,放在站点根节点,然后我在我已有的 WebSub ping 旁边添加了一个部署后钩子:

curl -s -X POST "https://api.indexnow.org/indexnow" \
  -H "Content-Type: application/json; charset=utf-8" \
  -d "{\"host\": \"brennan.day\", \"key\": \"...\", \"urlList\": }"

Awebfinger端点/.well-known/webfinger,这就是Fevarial的解析方式acct:brennan@brennan.day-风格标识符用于剖析链接。我的是静态文件,而不是查询响应端点,因为我只有一个身份要解析(我的):

{
  "subject": "acct:brennan@brennan.day",
  "links": [
    { "rel": "self", "type": "application/activity+json", "href": "https://social.lol/@brennan" }
  ]
}

A/.well-known/traffic-adviceFile,它是Chrome专用的,告诉它的私有预取代理是否被允许向我的Origin发送预取流量:

[{ "user_agent": "prefetch-proxy", "google_prefetch_proxy_eap": { "fraction": 1.0 } }]

添加一个联合发行模块(sy:updatePeriod,sy:updateFrequency) 在我的RSS订阅源中,因此有能力的订阅阅读器现在可以显示我的博客更新频率:


  
    daily
    1
  

Redirect-By: Netlify,一个头,说明是哪个系统发出了重定向,以备我调试重定向链的话:

[[headers]]
  for = "/*.html"
  [headers.values]
    No-Vary-Search = '''params=("utm_source" "utm_medium" "utm_campaign" "fbclid" "gclid")'''

[[headers]]
  for = "/*"
  [headers.values]
    Redirect-By = "Netlify"

[[headers]]
  for = "/assets/*"
  [headers.values]
    Cross-Origin-Resource-Policy = "cross-origin"

还有一些补充:

  • No-Vary-Search在HTML响应中列出通常的跟踪参数(utm_source,fbclid,gclid,等等)所以缓存处理/post/以及/post/?utm_source=mastodon用同一个页面而不是两个页面(我尽量避免任何追踪参数,但其他网站不会这么做)。
  • Cross-Origin-Resource-Policy: cross-origin在我的资产头部,所以其他原点隔离的更改并没有阻止别人嵌入我自己的图片。
  • 核心网页指标作为一个类别,就是回顾真实用户的体验,我就是这么做的PageSpeed 洞察.目前的评估为通过了:最大含糊涂料(LCP)1.8秒,与下一次绘画的交互(INP)68毫秒,累计布局偏移(CLS)0.其他指标:First Contentful Paint(FCP)1.6秒,到第一字节的时间(TTFB)1.6秒.这些数据取自最新的28天数据Chrome 用户体验报告手机版。

问题仍未解决

还有一些我没能从冗长详细的140项清单中提到:

  • DNS CAA记录和DNSSEC这些都由CDN控制,不在这个仓库里,所以我需要审核一下。
  • 受信任类型需要对每个人进行审计innerHTML-风格的水槽覆盖我的模板以及第三方小部件我不控制。
  • 在webring/反应按钮脚本上的SRI直到他们被固定到某个版本之前都不会发生@main.移动目标的子资源完整性只是意味着下次我推送提交时哈希会被破坏。我确实把它加到了我置顶的美人鱼剧本里,那就合理了。
  • 实际运行时间监控这意味着要选择并付费购买第三方服务,我觉得这对我的网站来说没必要。
  • 严格CSP带随机数/哈希这需要重构我所有的内联脚本和样式,才能使用合适的CSP机制,这比我现在愿意投入的工作量还多。

真正的重点

这部分我要说,这一切其实都不是关于清单的。别误会,清单是是它适合发现那些你因为几个月来每天都看同一个代码而不再看到的东西。

修复了破损的网页清单、主题色元标签无效,以及我实际上没用的面包屑模式。

但价值不在于满足所有要求,而是了解我因无知而缺失的网站,并做出有意义的改变以改善读者和访客的体验。

如果你自己运营网站,通过清单我很好奇你会发现什么!(如果这些改动真的导致了什么问题,请告诉我。)

关于布伦南·肯尼斯·布朗酷儿梅蒂斯作家、文化评论家和网站开发者,常驻莫金斯西斯(卡尔加里),属于第7条约领地。著有九本书且持续不断,是Fireweed写作学校和Berry House Studio的创始人,以及皇家山大学写作俱乐部的创始人。他的研究成果被《世界报》及其他出版物引用。喜欢这些内容吗?支持我的工作,帮助我创造更多:Patreon|共菲|GitHub 赞助商
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论