Files
blog/themes/Ying/layouts/partials/head.html
T
zqlit 50a906e07c perf(comments): 评论加载提速 + 骨架屏 + 友链弹窗 pjax 修复 + toast 统一 Message.js
优化:
- initArtalk 加节点级幂等守卫(mount.__atkInited,失败回滚),
  消除 artalk.html/mypjax 多入口导致的 4× comments + 4× pv 重复请求
- fetch 包装层对无 body 的 GET/HEAD 去掉多余 content-type,
  消掉 Artalk 对 GET /comments 的 CORS 预检(按完整 URL 缓存,
  每篇文章都重付一次 RTT)。实测评论列表 +1150~1565ms → +626ms

修复:
- 评论列表骨架屏改注入 .atk-list-body(原骨架在 #Comments 内,
  被 Artalk.init 清空),list-loaded/故障/12s 兜底移除
- 友链申请弹窗抽成 modules/friendlink.js 进 page-only bundle
  (原内联脚本在 #pjax-container 外,pjax 后 flApplyOpenModal 未定义)
- toast 统一走本地化 Message.js/Qmsg 适配层(保留 window.Toast/showToast 旧 API)

文档:
- README / 架构总览 更新;新增 评论加载优化方案.md
2026-10-04 15:00:52 +08:00

192 lines
8.1 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{{ .Title }}</title>
<!-- SEO & Meta Tags -->
<meta name="description" content="{{ if .IsHome }}{{ .Site.Params.description }}{{ else }}{{ .Description | default .Summary | plainify | truncate 160 }}{{ end }}" />
<link rel="canonical" href="{{ .RelPermalink | absURL }}" />
{{- $keywords := slice "大学" "教程" "blog" "优世界" "优设计" "博客" "大学生博客" "生活" "文案" -}}
{{- if .Params.tags -}}
{{- $keywords = .Params.tags -}}
{{- end -}}
{{- if .Params.keywords -}}
{{- $keywords = .Params.keywords -}}
{{- end -}}
<meta name="keywords" content="{{ delimit $keywords "," }}" />
<!-- Open Graph -->
<meta property="og:title" content="{{ .Title }}" />
<meta property="og:description" content="{{ if .IsHome }}{{ .Site.Params.description }}{{ else }}{{ .Description | default .Summary | plainify | truncate 160 }}{{ end }}" />
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
<meta property="og:url" content="{{ .Permalink }}" />
<meta property="og:site_name" content="{{ .Site.Title }}" />
<meta name="nilaila-verify" content="97dd49b8e6e941828e647b11bea73891">
{{- $image := .Site.Params.authorImage -}}
{{- if .Params.cover -}}
{{- $image = .Params.cover -}}
{{- end -}}
{{- if $image -}}
<meta property="og:image" content="{{ $image | absURL }}" />
<meta name="twitter:image" content="{{ $image | absURL }}" />
{{- end -}}
<!-- Twitter Card -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="{{ .Title }}" />
<meta name="twitter:description" content="{{ if .IsHome }}{{ .Site.Params.description }}{{ else }}{{ .Description | default .Summary | plainify | truncate 160 }}{{ end }}" />
<!-- RSS Feed -->
<link rel="alternate" type="application/rss+xml" title="{{ site.Title }}" href="/rss.xml">
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<!-- Resource Hints -->
<link rel="preconnect" href="https://cravatar.cn">
{{- /* 评论 / RSS API:Artalk 与友链数据都发 CORS 跨域请求。
preconnect 必须带 crossorigin —— 浏览器对「带凭据」和「匿名」两条连接分池,
不带 crossorigin 预热的连接 CORS 请求无法复用,等于白预连(net::ERR 之外
还要重付一次 DNS + TCP + TLS)。dns-prefetch 兜底不支持 preconnect 的浏览器。 */ -}}
{{- $apiHosts := slice -}}
{{- with site.Params.artalk.server }}{{ $apiHosts = $apiHosts | append (strings.TrimSuffix "/" .) }}{{ end -}}
{{- with site.Params.rssapi.base }}{{ $apiHosts = $apiHosts | append (strings.TrimSuffix "/" .) }}{{ end -}}
{{- range ($apiHosts | uniq) }}
<link rel="preconnect" href="{{ . }}" crossorigin>
<link rel="dns-prefetch" href="{{ . }}">
{{- end }}
{{ with site.Params.rssapi.base }}<script>window.rssApiBase = "{{ . }}";</script>{{ end }}
<link rel="dns-prefetch" href="https://bsz.dusays.com:9001">
<!-- Analytics -->
<script async src="https://019e3dec-3312-7873-862a-3f56ac99ea83.spst2.com/ustat.js"></script>
<script>
(function () {
try {
var theme = localStorage.getItem('theme');
if (!theme) {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme = 'dark';
} else {
theme = 'light';
}
}
document.documentElement.setAttribute('data-theme', theme);
// Listen for system changes
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
// Only switch if user hasn't manually set a preference
// Or maybe we should respect system change if user hasn't explicitly locked it?
// Common behavior: manual override sticks.
if (!localStorage.getItem('theme')) {
var newTheme = event.matches ? "dark" : "light";
document.documentElement.setAttribute('data-theme', newTheme);
}
});
}
} catch (e) {}
})();
</script>
<!-- Preload Fonts -->
<link rel="preload" href="{{ "font/zql-v2.woff2" | relURL }}" as="font" type="font/woff2" crossorigin>
<!-- remixicon 不预加载,避免与 CSS 中带 query 参数的 URL 不一致导致警告 -->
{{ $echartsHead := resources.Get "js/libs/echarts.min.js" | resources.Fingerprint }}
{{ $artalkHead := resources.Get "js/libs/Artalk.js" | resources.Fingerprint }}
<!-- Site Configuration -->
<script>
window.optionalAssetUrls = {
echarts: {{ $echartsHead.RelPermalink | jsonify | safeJS }},
artalk: {{ $artalkHead.RelPermalink | jsonify | safeJS }}
};
window.loadScriptOnce = window.loadScriptOnce || function(src, globalName, callback) {
if (globalName && window[globalName]) {
callback();
return;
}
var existing = document.querySelector('script[data-dynamic-src="' + src + '"]');
if (existing) {
existing.addEventListener('load', callback, { once: true });
return;
}
var script = document.createElement('script');
script.src = src;
script.defer = true;
script.dataset.dynamicSrc = src;
script.onload = callback;
document.head.appendChild(script);
};
window.siteConfig = {
artalk: {
server: "{{ .Site.Params.artalk.server }}",
site: "{{ .Site.Params.artalk.site }}",
emoticons: "{{ .Site.Params.artalk.emoticons | absURL }}",
gravatar: "{{ .Site.Params.artalk.gravatar }}"
},
author: {
avatar: "{{ .Site.Params.author.avatar }}",
description: "{{ .Site.Params.author.description }}"
},
barrage: {
enableHitokoto: {{ .Site.Params.barrage.enableHitokoto | default true }},
hitokotoUrl: "{{ .Site.Params.barrage.hitokotoUrl }}"
}
};
</script>
<!-- Stylesheets -->
{{ $normalize := resources.Get "css/libs/normalize.css" }}
{{ $remixicon := resources.Get "css/libs/remixicon.css" }}
{{ $uikit := resources.Get "css/libs/uikit.min.css" }}
{{ $nprogress := resources.Get "css/libs/nprogress.min.css" }}
{{ $message := resources.Get "css/libs/message.min.css" }}
{{ $fontSlices := resources.Get "css/font-slices.css" }}
{{ $main := resources.Get "css/main.css" }}
{{ $shortcodes := resources.Get "css/shortcodes.css" }}
{{ $style := slice $normalize $remixicon $uikit $nprogress $message $fontSlices $main $shortcodes | resources.Concat "css/style.css" | resources.Minify | resources.Fingerprint }}
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
{{- if and .Site.Params.style.enableBackgroundImage .Site.Params.style.backgroundImageUrl -}}
<style>
body {
background-image: url('{{ .Site.Params.style.backgroundImageUrl }}') !important;
background-size: {{ .Site.Params.style.backgroundSize | default "cover" }} !important;
background-repeat: {{ .Site.Params.style.backgroundRepeat | default "no-repeat" }} !important;
background-position: {{ .Site.Params.style.backgroundPosition | default "center" }} !important;
background-attachment: {{ .Site.Params.style.backgroundAttachment | default "fixed" }} !important;
}
[data-theme="dark"] body {
background-image: url(/image/background/deno.webp) !important;
background-size: contain !important;
background-repeat: round !important;
background-position: center !important;
background-attachment: fixed !important;
}
[data-theme="dark"] body::before {
content: '' !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
background-color: rgba(0, 0, 0, 0.4) !important;
pointer-events: none !important;
z-index: -1 !important;
}
@media (max-width: 768px) {
body {
background-attachment: scroll !important;
}
}
</style>
{{- end -}}
<!-- <script src="/js/jquery.min.js"></script> -->
<script src="/js/instantpage.min.js" type="module" defer></script>
<meta name="template" content="Amigo" />
</head>