fix(build): 消除构建期远程拉取隐患 + 评论提交反馈 + 友链卡片移动端
构建隐患(本次丢构建的根因)
- footer.html 用 resources.GetRemote 在每个页面渲染时拉 api/links 与 api/feeds,
构建节点连不上 Cloudflare 时重试撞穿 Hugo partial 30s 超时 → 整站构建失败
- 改为「流水线 curl 快照 + 模板读本地文件」:
· 新增 scripts/fetch_snapshots.sh,一次拉齐 conf / friend_links / friend_feeds
(--max-time 30 --retry 2,JSON 校验,失败一律删文件且恒 exit 0)
· .cnb.yml Hugo 构建 stage 改为调用该脚本
· footer.html 改读 site.Data.friend_links / site.Data.friend_feeds,缺失渲染空值
· .gitignore 忽略 /data/friend_links.json 与 /data/friend_feeds.json
- 降级实测:删掉快照后构建仍 EXIT=0,产物 friendLinks 为空数组
评论提交反馈(artalk.js 新增 __atkSubmit 模块)
- 校验:config.beforeSubmit 拦下 内容空 / 昵称空 / 邮箱空 / 邮箱格式错(网址不校验)
- 发送中:editor-submit 常驻 loading,超 7s 补「网络较慢」
- 成功:editor-submitted 按 normal/reply/edit 分文案,is_pending 提示等待审核
- 失败:拦 POST /api/v2/comments,403/429/4xx/5xx/网络异常全部翻译成人话
- 失败时输入内容保留,并清掉 Artalk 自带的重复小字通知
- 无头浏览器实测 19/19 通过(POST 走 mock,未向线上写数据)
友链申请卡片移动端
- max-width:640px 下隐藏 .atk-fl-nick(昵称 + 博主徽章),桌面端不变
改动:6 files, +282 / -61
This commit is contained in:
1 parent
836743b16d
commit
571d552c9d
6 files changed
+355
-61
No files matched your search
@@ -55,23 +55,15 @@
|
||||
|
||||
hugo version
|
||||
|
||||
# ── /conf 快照(评论首屏优化)────────────────────────────────────
|
||||
# 构建期拉一次 Artalk 的 /conf 落到 data/,Hugo 会内联进每个页面。
|
||||
# 前端据此对匿名 /conf 请求本地合成响应 —— Artalk 内部 conf 与评论列表
|
||||
# 是严格串行的,省掉这一跳等于首屏少一整个跨境往返。
|
||||
# 服务端这份 conf 只由 settings 表决定(唯一因人而异的 imgUpload 由前端
|
||||
# 按登录态放行真实请求),所以匿名访客的快照人人相同。
|
||||
# 拉取失败一律降级:删文件 → 模板不内联 → 前端自动走原生远程请求。
|
||||
# 绝不让它拖垮构建(在 if 条件里失败不会触发 set -e)。
|
||||
mkdir -p data
|
||||
if curl -fsS --max-time 30 --retry 1 --retry-delay 2 \
|
||||
"https://api.200181.xyz/api/v2/conf" -o data/artalk_conf.json \
|
||||
&& python3 -c "import json;json.load(open('data/artalk_conf.json',encoding='utf-8'))"; then
|
||||
echo "✅ conf 快照就绪 $(wc -c < data/artalk_conf.json | tr -d ' ') 字节"
|
||||
else
|
||||
rm -f data/artalk_conf.json
|
||||
echo "⚠️ conf 快照拉取失败,已降级为运行时远程拉取"
|
||||
fi
|
||||
# ── 构建期数据快照(评论 conf / 友链 / 友圈)────────────────────
|
||||
# ★ 这里必须用「先 curl 落盘、模板再读本地文件」,**不许**模板里
|
||||
# 用 resources.GetRemote:那是「渲染过程中发网络请求」,footer 每页
|
||||
# 都要走一遍,构建节点到 Cloudflare 一抖动就会重试到撞穿 Hugo 的
|
||||
# partial 30s 超时 → 整站构建失败(2026-10-04 实测白等 14.5 分钟)。
|
||||
# 改成快照后最坏只是数据为空,构建永远不挂。
|
||||
# 详见 scripts/fetch_snapshots.sh 顶部注释。
|
||||
# 脚本内部对每个快照都做了「拉失败就删文件」的降级,且恒 exit 0。
|
||||
bash scripts/fetch_snapshots.sh
|
||||
|
||||
python3 scripts/add_draft_to_hidden.py
|
||||
rm -rf public resources/_gen
|
||||
|
||||
+4
-1
@@ -15,5 +15,8 @@ node_modules/
|
||||
.ci_status
|
||||
# CNB 密钥仓库的本地草稿(含真实令牌,仅供往 Web 界面粘贴,绝不入库)
|
||||
cnb-secrets.yml
|
||||
# 构建期拉取的 Artalk /conf 快照(每次构建现拉,不入库,避免陈旧配置被 git 固化)
|
||||
# 构建期拉取的数据快照(每次构建现拉,不入库,避免陈旧数据被 git 固化)
|
||||
# 由 scripts/fetch_snapshots.sh 生成,见 .cnb.yml「Hugo 构建」stage
|
||||
/data/artalk_conf.json
|
||||
/data/friend_links.json
|
||||
/data/friend_feeds.json
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 构建期数据快照 —— 把「模板渲染时必须用、但只能从远端拿」的数据先 curl 成本地文件
|
||||
# =============================================================================
|
||||
# 为什么必须这么做(2026-10-04 实战教训):
|
||||
# footer.html 原先用 resources.GetRemote 拉 /api/links 与 /api/feeds。Hugo 的
|
||||
# GetRemote 是**在模板渲染过程中同步发请求**的,而 footer 每个页面都要走一遍。
|
||||
# 构建节点(国内)到 api.200181.xyz(Cloudflare)一旦抖动:
|
||||
# WARN Links API fetch failed: dial tcp 104.21.58.236:443: i/o timeout
|
||||
# WARN Links API fetch failed: ... connection reset by peer
|
||||
# Artalk 无关,但这两个拉取会一直重试 → 撞穿 Hugo 的 `partial` 30 秒超时 →
|
||||
# ERROR render of "home" failed: partial "footer.html" timed out after 30s
|
||||
# Error: error building site → exit code: 1
|
||||
# 整站构建失败、站点不更新(实测白等 14.5 分钟)。
|
||||
#
|
||||
# 改成「stage 里 curl 一次 + 模板只读本地文件」后:
|
||||
# 网络问题最多让某个快照变旧/为空(模板 with 判空 → 渲染空值),
|
||||
# **永远不会再让构建挂掉**。命中失败时一律删文件,绝不留半截内容。
|
||||
#
|
||||
# 在流水线里由 .cnb.yml 的「Hugo 构建」stage 调用;本地预览想拿到同款数据
|
||||
# 也可以直接跑:bash scripts/fetch_snapshots.sh
|
||||
#
|
||||
# 产物(均在 .gitignore 里,不入库):
|
||||
# data/artalk_conf.json 评论 /conf(首屏省一跳跨境往返,见 partials/artalk.html)
|
||||
# data/friend_links.json 友链 → window.friendLinks(评论区「友链」徽章)
|
||||
# data/friend_feeds.json 友圈 → window.friendFeeds(评论区「友圈」徽章)
|
||||
# =============================================================================
|
||||
|
||||
set -u
|
||||
|
||||
BASE="${RSSAPI_BASE:-https://api.200181.xyz}"
|
||||
BASE="${BASE%/}"
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DATA_DIR="$ROOT/data"
|
||||
mkdir -p "$DATA_DIR"
|
||||
|
||||
# JSON 合法性校验:python3 不一定有(本机 Windows 可能没有),没有就跳过校验
|
||||
validate_json() {
|
||||
command -v python3 >/dev/null 2>&1 || return 0
|
||||
python3 -c 'import json,sys;json.load(open(sys.argv[1],encoding="utf-8"))' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# snap <输出文件名> <URL>
|
||||
# 成功:原子替换(先写 .tmp 再 mv),并打印字节数
|
||||
# 失败:删除目标文件(含 .tmp),返回非 0 —— 但本脚本最后统一 exit 0,
|
||||
# 因为「快照拿不到」是可接受的降级,不该阻断构建。
|
||||
snap() {
|
||||
local name="$1" url="$2" out tmp ok=1
|
||||
out="$DATA_DIR/$name"
|
||||
tmp="$out.tmp"
|
||||
|
||||
if curl -fsS --max-time 30 --retry 2 --retry-delay 2 --retry-connrefused \
|
||||
-H 'User-Agent: hugo-build-snapshot' "$url" -o "$tmp" 2>/dev/null; then
|
||||
if [ -s "$tmp" ] && validate_json "$tmp"; then
|
||||
mv -f "$tmp" "$out"
|
||||
echo "✅ $name 就绪 $(wc -c < "$out" | tr -d ' ') 字节"
|
||||
return 0
|
||||
fi
|
||||
echo "⚠️ $name 内容校验失败(空文件或非法 JSON)"
|
||||
else
|
||||
echo "⚠️ $name 拉取失败:$url"
|
||||
fi
|
||||
|
||||
rm -f "$tmp" "$out"
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "── 构建期快照(源:$BASE)──"
|
||||
snap artalk_conf.json "$BASE/api/v2/conf" || true
|
||||
snap friend_links.json "$BASE/api/links?all=1" || true
|
||||
snap friend_feeds.json "$BASE/api/feeds" || true
|
||||
|
||||
exit 0
|
||||
@@ -718,6 +718,18 @@ window.initArtalk = function() {
|
||||
saveInitCount(); // 持久化到 sessionStorage
|
||||
|
||||
var config = Object.assign({}, window.artalkConfig); // Clone config
|
||||
|
||||
// ── 提交前校验(昵称 / 邮箱必填;网址随意)──────────────────────────
|
||||
// Artalk 的 editor.submit() 实现是:
|
||||
// conf.beforeSubmit ? conf.beforeSubmit(editor, next) : trigger('editor-submit')
|
||||
// 也就是官方就留了拦截点 —— 不调 next() 就不会走提交流程。
|
||||
// 挂在这份 config 上(而不是内联 <script> 里的 window.artalkConfig)是因为
|
||||
// 两条 init 路径(带 / 不带 localStorage 缓存)都从它派生,挂这里两个分支
|
||||
// 必然都带上;何况缓存那份只存简单类型,函数本来就存不下来。
|
||||
if (window.__atkSubmit && window.__atkSubmit.beforeSubmit) {
|
||||
config.beforeSubmit = window.__atkSubmit.beforeSubmit;
|
||||
}
|
||||
|
||||
var server = config.server.replace(/\/$/, '');
|
||||
var confKey = 'artalk_conf_' + btoa(server);
|
||||
var cachedConf = null;
|
||||
@@ -759,6 +771,14 @@ window.initArtalk = function() {
|
||||
|
||||
var artalk = window.artalk; // Local reference
|
||||
|
||||
// ── 评论提交反馈(发送中 / 成功;失败由文件末尾的 fetch 拦截兜底)──
|
||||
// editor-submit = 提交开始(beforeSubmit 放行后才会触发)
|
||||
// editor-submitted = 提交成功(Artalk 的失败分支直接 return,不触发本事件)
|
||||
if (window.__atkSubmit) {
|
||||
artalk.on('editor-submit', window.__atkSubmit.begin);
|
||||
artalk.on('editor-submitted', window.__atkSubmit.done);
|
||||
}
|
||||
|
||||
// Sync Theme Mode Immediately
|
||||
var currentTheme = document.documentElement.getAttribute('data-theme');
|
||||
if (typeof artalk.setDarkMode === 'function') {
|
||||
@@ -1455,3 +1475,207 @@ artalk.on('list-loaded', function() {
|
||||
}
|
||||
}, true);
|
||||
})();
|
||||
|
||||
// ============================================================================
|
||||
// 评论提交反馈 —— 校验 / 发送中 / 成功 / 失败
|
||||
//
|
||||
// 为什么需要:评论接口(Cloudflare)在国内常年丢包,读者点完「发送」可能好几秒
|
||||
// 没有任何反馈,根本没法判断到底发出去没有;而服务端对「昵称 / 邮箱缺失」是直接
|
||||
// 返 400 的,客户端这边却只在编辑框里弹一行小字,很容易被忽略。
|
||||
//
|
||||
// 四个挂点:
|
||||
// 1) config.beforeSubmit → 提交前校验(内容 / 昵称 / 邮箱必填,邮箱格式;
|
||||
// 网址随意不校验)。不调 next() 就不发请求,
|
||||
// 省掉一次注定失败的跨境往返。
|
||||
// 2) artalk.on('editor-submit') → 提交开始:常驻的「评论发送中…」loading toast;
|
||||
// 超过 7 秒仍未结束再补一条「网络较慢」。
|
||||
// 3) artalk.on('editor-submitted') → 提交成功(Artalk 失败分支直接 return,不会
|
||||
// 触发本事件):关掉 loading,按评论 / 回复 /
|
||||
// 编辑三种模式弹成功 toast。
|
||||
// 4) 拦 fetch 的 POST /api/v2/comments → 失败(HTTP 错误或网络异常)时,把服务端
|
||||
// 的 msg / 标记翻译成人话弹 error toast,并删掉
|
||||
// Artalk 自己那行重复的小字通知。
|
||||
//
|
||||
// 注意:Artalk 提交失败时**不会**清空输入框(只有成功才 reset),所以失败提示里
|
||||
// 可以放心说「内容还在」。另外所有失败都是「服务端已拒」——loading toast 必须
|
||||
// 在同一轮里收干净,不能留一个转圈的 toast 挂在那儿。
|
||||
// ============================================================================
|
||||
(function () {
|
||||
if (window.__atkSubmit) return; // PJAX 会重复执行本文件,只装一次
|
||||
var P = (window.__atkSubmit = {});
|
||||
|
||||
var POST_RE = /\/api\/v2\/comments(\?|$)/;
|
||||
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
var loading = false; // loading toast 是否挂着
|
||||
var slowTimer = null; // 慢网补充提示的定时器
|
||||
var editor = null; // beforeSubmit 拿到的编辑器实例(用来判提交模式)
|
||||
var result = null; // 本轮 POST 的结果,由下面的 fetch 拦截写入
|
||||
|
||||
function toast(type, text) {
|
||||
try {
|
||||
if (window.Toast && typeof window.Toast[type] === 'function') return window.Toast[type](text);
|
||||
if (window.showToast) window.showToast(text, type);
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 一次性收干净:不用维护 Qmsg 实例引用,也顺手清掉「网络较慢」那条。
|
||||
// (Qmsg 的 loading 是 autoClose:false,不主动关就会一直转)
|
||||
function closeLoading() {
|
||||
loading = false;
|
||||
if (slowTimer) { clearTimeout(slowTimer); slowTimer = null; }
|
||||
try { if (window.Toast && window.Toast.closeAll) window.Toast.closeAll(); } catch (e) {}
|
||||
}
|
||||
|
||||
// ── 校验用的取值 ─────────────────────────────────────────────────────
|
||||
function val(el) { return el && typeof el.value === 'string' ? el.value.trim() : ''; }
|
||||
|
||||
function headerEls() {
|
||||
// 首选 Artalk 编辑器自己的访问器:返回的是**这一份编辑器**的输入框。
|
||||
// ⚠️ 不能拿 document.querySelector('[name=nick]') 顶替:Artalk 的 DOM 里
|
||||
// 存在不止一个 nick 输入框(隐藏模板、未激活的回复框都算),拿错就会
|
||||
// 把提示弹在一个看不见的框上,focus 也静默失败。
|
||||
try {
|
||||
if (editor && typeof editor.getHeaderInputEls === 'function') {
|
||||
var h = editor.getHeaderInputEls();
|
||||
if (h && (h.nick || h.email)) return h;
|
||||
}
|
||||
} catch (e) {}
|
||||
var ed = document.querySelector('.atk-main-editor');
|
||||
if (!ed) return {};
|
||||
return {
|
||||
nick: ed.querySelector('.atk-header [name="nick"]'),
|
||||
email: ed.querySelector('.atk-header [name="email"]'),
|
||||
link: ed.querySelector('.atk-header [name="link"]')
|
||||
};
|
||||
}
|
||||
|
||||
function focusEl(el) {
|
||||
if (!el) return;
|
||||
try { el.focus({ preventScroll: true }); } catch (e) { try { el.focus(); } catch (e2) {} }
|
||||
try { el.scrollIntoView({ block: 'center', behavior: 'smooth' }); } catch (e) {}
|
||||
}
|
||||
|
||||
// ── 1) 提交前校验:返回 false 表示已拦下,不发请求 ─────────────────────
|
||||
P.beforeSubmit = function (ed, next) {
|
||||
editor = ed || editor;
|
||||
var content = '';
|
||||
try { content = String(ed.getContentFinal() || '').trim(); } catch (e) {}
|
||||
var els = headerEls();
|
||||
|
||||
if (!content) { toast('warning', '评论内容不能为空'); return; }
|
||||
var nick = val(els.nick), email = val(els.email);
|
||||
if (!nick) { toast('warning', '请先填写昵称'); focusEl(els.nick); return; }
|
||||
if (!email) { toast('warning', '请先填写邮箱(有人回复你时才能通知到)'); focusEl(els.email); return; }
|
||||
if (!EMAIL_RE.test(email)) { toast('warning', '邮箱格式好像不对,检查一下'); focusEl(els.email); return; }
|
||||
// 网址(link)留空或随便填都行 —— 不校验。服务端也只在非空时才查格式
|
||||
next();
|
||||
};
|
||||
|
||||
// ── 2) 提交开始 ──────────────────────────────────────────────────────
|
||||
P.begin = function () {
|
||||
closeLoading(); // 极端情况下上一轮没收干净
|
||||
loading = true;
|
||||
result = null;
|
||||
toast('loading', '评论发送中…');
|
||||
// 慢网补充提示:CF 丢包时首包可能要好几秒,别让读者以为按钮坏了
|
||||
slowTimer = setTimeout(function () {
|
||||
slowTimer = null;
|
||||
if (loading) toast('info', '网络较慢,评论仍在提交中…');
|
||||
}, 7000);
|
||||
};
|
||||
|
||||
// ── 3) 提交成功 ──────────────────────────────────────────────────────
|
||||
P.done = function () {
|
||||
var r = result;
|
||||
closeLoading();
|
||||
if (r && r.pending) { toast('info', '评论已提交,等待审核'); return; }
|
||||
var st = '';
|
||||
try { st = (editor && typeof editor.getState === 'function' && editor.getState()) || ''; } catch (e) {}
|
||||
if (st === 'edit') { toast('success', '修改已保存'); return; }
|
||||
if (st === 'reply') { toast('success', '回复发送成功'); return; }
|
||||
toast('success', '评论发送成功');
|
||||
};
|
||||
|
||||
// ── 4) 失败:把服务端响应翻译成人话 ──────────────────────────────────
|
||||
// 服务端(blog-admin)的文案见 src/routes/comments.ts:
|
||||
// 400 'Invalid parameters: name / email / content / page_key are required'
|
||||
// 400 'Invalid Email' / 400 'Invalid Link'
|
||||
// 429 'Too many requests, please slow down'
|
||||
// 403 'Human verification required'(带 need_captcha,被自研人机门禁拦下)
|
||||
// 403 'Admin access required'(带 need_login,昵称/邮箱撞上管理员账号)
|
||||
// 5xx 'Comment failed' 等
|
||||
function humanize(r) {
|
||||
r = r || {};
|
||||
var d = r.data || {};
|
||||
var msg = String(r.msg || '').trim();
|
||||
if (r.network) return '网络不稳定,评论没发出去 —— 内容还在,稍后重试';
|
||||
if (d.need_login) return '该昵称或邮箱是管理员账号,请先登录后台再评论';
|
||||
if (d.need_captcha) return '请先点一下「点击验证」,通过后再发送评论';
|
||||
if (r.status === 429 || /too many/i.test(msg)) return '评论太频繁了,歇一会儿再发';
|
||||
if (/invalid email/i.test(msg)) return '邮箱格式不对,检查一下再发';
|
||||
if (/invalid link/i.test(msg)) return '网址格式不对(也可以留空)';
|
||||
if (/parent comment not found/i.test(msg)) return '要回复的那条评论已经不在了';
|
||||
if (/required/i.test(msg)) return '昵称、邮箱和内容都要填哦';
|
||||
if (r.status >= 500) return '服务器开小差了,评论没发出去,稍后重试';
|
||||
return msg || ('评论发送失败(HTTP ' + r.status + '),稍后重试');
|
||||
}
|
||||
|
||||
function failToast(r) {
|
||||
closeLoading();
|
||||
toast('error', humanize(r));
|
||||
// Artalk 还会在自己编辑框里塞一行 "评论失败: ..." 的小字通知,与 toast 重复。
|
||||
// 只删文本匹配的那一条,避免误伤「内容已自动恢复」之类其它通知。
|
||||
setTimeout(function () {
|
||||
var ns = document.querySelectorAll('.atk-notify');
|
||||
for (var i = 0; i < ns.length; i++) {
|
||||
if (/评论失败|Failed to comment/.test(ns[i].textContent || '')) {
|
||||
if (ns[i].parentNode) ns[i].parentNode.removeChild(ns[i]);
|
||||
}
|
||||
}
|
||||
}, 80);
|
||||
}
|
||||
|
||||
// ── fetch 拦截:只认「新建评论」这一个请求 ────────────────────────────
|
||||
// 之所以要先 await 读完 body 再放行,是因为 Artalk 在 `await fetch(...)` 之后
|
||||
// 才触发 editor-submitted;如果这里用 res.clone().json() 异步解析而不 await,
|
||||
// 事件的回调会先跑,读到的是上一轮的 result(成功/审核状态就串了)。
|
||||
var origFetch = window.fetch;
|
||||
if (typeof origFetch === 'function') {
|
||||
window.fetch = function (input, init) {
|
||||
var url = String(typeof input === 'string' ? input : (input && input.url) || '');
|
||||
var method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
||||
if (method !== 'POST' || !POST_RE.test(url)) return origFetch.apply(this, arguments);
|
||||
|
||||
var p = origFetch.apply(this, arguments);
|
||||
return p.then(function (res) {
|
||||
if (res && res.ok) {
|
||||
// 成功:读一下 body 看是不是落进了审核队列(服务端返回的就是
|
||||
// 煮熟的评论对象本体,is_pending 是它的字段)
|
||||
return res.clone().json().then(function (d) {
|
||||
result = { ok: true, pending: !!(d && d.is_pending) };
|
||||
return res;
|
||||
}).catch(function () {
|
||||
result = { ok: true, pending: false };
|
||||
return res;
|
||||
});
|
||||
}
|
||||
// 失败:Artalk 拿到 !res.ok 会自己 throw,这里只负责记因 + 弹 toast
|
||||
return res.clone().json().then(function (d) {
|
||||
result = { ok: false, status: res.status, msg: (d && d.msg) || '', data: d || {} };
|
||||
failToast(result);
|
||||
return res;
|
||||
}).catch(function () {
|
||||
result = { ok: false, status: res.status, msg: 'HTTP ' + res.status, data: {} };
|
||||
failToast(result);
|
||||
return res;
|
||||
});
|
||||
}, function (err) {
|
||||
result = { ok: false, network: true, msg: (err && err.message) || 'network error' };
|
||||
failToast(result);
|
||||
throw err; // 原样抛回:Artalk 自己的失败处理(保留输入内容)不受影响
|
||||
});
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -119,7 +119,13 @@
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
/* ---- 移动端:卡片更紧凑(隐藏头像 + 收紧间距 + 微调字号/字距) ---- */
|
||||
/* ---- 移动端:卡片更紧凑(隐藏头像 + 昵称行 + 收紧间距 + 微调字号) ----
|
||||
窄屏把「头像 + 昵称(小赵) + 博主徽章」整行去掉:这三个元素在手机宽度下
|
||||
会把打字机文案挤成一两行、按钮也跟着缩小,观感很差。去掉之后卡片只剩
|
||||
「说明文案 + 申请友链按钮」,信息密度反而更合理。
|
||||
昵称与徽章都在 .atk-fl-nick 这一行里(徽章是 artalk.js 克隆后 append 进去的),
|
||||
所以按 .atk-fl-nick 隐藏即可;徽章再多写一条做兜底(Webkit 老版本对
|
||||
父级 display:none 的继承没问题,这里只是防御式写法)。 */
|
||||
@media (max-width: 640px) {
|
||||
.atk-fl-card {
|
||||
gap: 8px !important;
|
||||
@@ -128,14 +134,15 @@
|
||||
.atk-fl-card .atk-fl-avatar {
|
||||
display: none !important; /* 窄屏省掉头像,把宽度让给文字 */
|
||||
}
|
||||
.atk-fl-card .atk-fl-nick {
|
||||
font-size: 13.5px !important;
|
||||
letter-spacing: 0.2px !important;
|
||||
.atk-fl-card .atk-fl-nick,
|
||||
.atk-fl-card .atk-fl-nick .atk-badge,
|
||||
.atk-fl-card .atk-fl-nick .atk-cmt-badge {
|
||||
display: none !important; /* 昵称(小赵)+ 博主徽章:移动端不显示 */
|
||||
}
|
||||
.atk-fl-card .atk-fl-desc {
|
||||
font-size: 12.5px !important;
|
||||
letter-spacing: 0.2px !important;
|
||||
margin-top: 3px !important;
|
||||
margin-top: 0 !important; /* 昵称行没了,上间距一并去掉 */
|
||||
}
|
||||
.atk-fl-card .atk-fl-btn {
|
||||
font-size: 12.5px !important;
|
||||
|
||||
@@ -79,28 +79,29 @@
|
||||
window.enableInfiniteScroll = {{ .Site.Params.infiniteScroll.enable | default false }};
|
||||
</script>
|
||||
|
||||
{{/* ====== 3. 远程数据获取 - 构建时 ====== */}}
|
||||
{{/* Fetch friend data at build time — no runtime network requests */}}
|
||||
{{/* ====== 3. 友链 / 友圈数据 —— 构建期快照,模板**不发网络请求** ====== */}}
|
||||
{{/* ★ 这里以前是 resources.GetRemote。它是在「每个页面的模板渲染过程中」同步发请求的,
|
||||
而 footer 每页都要走一遍:构建节点(国内)到 api.200181.xyz(Cloudflare)一抖动,
|
||||
两个拉取就会重试到撞穿 Hugo 的 partial 30 秒超时 → 整站构建失败、站点不更新
|
||||
(2026-10-04 实测:WARN Links API fetch failed ... i/o timeout →
|
||||
ERROR render of "home" failed: partial "footer.html" timed out after 30s)。
|
||||
现在改为读 data/ 下的本地快照,由流水线 stage 里的 scripts/fetch_snapshots.sh
|
||||
curl 落盘(见 .cnb.yml「Hugo 构建」)。快照缺失(本地预览 / 拉取失败)时
|
||||
with 判空 → 渲染空数组,最坏只是评论区少了「友链 / 友圈」徽章,构建立马过。 */}}
|
||||
{{ $linkHosts := slice }}
|
||||
{{ $linkNames := slice }}
|
||||
{{ with resources.GetRemote (printf "%s/api/links?all=1" (site.Params.rssapi.base | default "https://api.200181.xyz")) }}
|
||||
{{ with .Err }}
|
||||
{{ warnf "Links API fetch failed: %s" . }}
|
||||
{{ else }}
|
||||
{{ with .Content | transform.Unmarshal }}
|
||||
{{ range .links }}
|
||||
{{ $host := "" }}
|
||||
{{ with .url | urls.Parse }}
|
||||
{{ $host = .Host | strings.TrimPrefix "www." }}
|
||||
{{ end }}
|
||||
{{ $title := .name | default "" }}
|
||||
{{ if $host }}
|
||||
{{ $linkHosts = $linkHosts | append (dict "host" $host "title" $title) }}
|
||||
{{ end }}
|
||||
{{ if $title }}
|
||||
{{ $linkNames = $linkNames | append (dict "lower" (lower $title) "title" $title) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ with site.Data.friend_links }}
|
||||
{{ range .links }}
|
||||
{{ $host := "" }}
|
||||
{{ with .url | urls.Parse }}
|
||||
{{ $host = .Host | strings.TrimPrefix "www." }}
|
||||
{{ end }}
|
||||
{{ $title := .name | default "" }}
|
||||
{{ if $host }}
|
||||
{{ $linkHosts = $linkHosts | append (dict "host" $host "title" $title) }}
|
||||
{{ end }}
|
||||
{{ if $title }}
|
||||
{{ $linkNames = $linkNames | append (dict "lower" (lower $title) "title" $title) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
@@ -110,24 +111,18 @@
|
||||
|
||||
{{ $feedHosts := slice }}
|
||||
{{ $feedNames := slice }}
|
||||
{{ with resources.GetRemote (printf "%s/api/feeds" (site.Params.rssapi.base | default "https://api.200181.xyz")) }}
|
||||
{{ with .Err }}
|
||||
{{ warnf "Feeds API fetch failed: %s" . }}
|
||||
{{ else }}
|
||||
{{ with .Content | transform.Unmarshal }}
|
||||
{{ range .feeds }}
|
||||
{{ $host := "" }}
|
||||
{{ with .url | urls.Parse }}
|
||||
{{ $host = .Host | strings.TrimPrefix "www." }}
|
||||
{{ end }}
|
||||
{{ $title := .feedTitle | default "" }}
|
||||
{{ if $host }}
|
||||
{{ $feedHosts = $feedHosts | append (dict "host" $host "title" $title) }}
|
||||
{{ end }}
|
||||
{{ if $title }}
|
||||
{{ $feedNames = $feedNames | append (dict "lower" (lower $title) "title" $title) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ with site.Data.friend_feeds }}
|
||||
{{ range .feeds }}
|
||||
{{ $host := "" }}
|
||||
{{ with .url | urls.Parse }}
|
||||
{{ $host = .Host | strings.TrimPrefix "www." }}
|
||||
{{ end }}
|
||||
{{ $title := .feedTitle | default "" }}
|
||||
{{ if $host }}
|
||||
{{ $feedHosts = $feedHosts | append (dict "host" $host "title" $title) }}
|
||||
{{ end }}
|
||||
{{ if $title }}
|
||||
{{ $feedNames = $feedNames | append (dict "lower" (lower $title) "title" $title) }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
Reference in new issue
Block a user