feat(deploy): 裸 clone 构建改造与 en/archives 补页(T1/T4)
- build.sh:构建源改 ~/docker_apps/blog/build-repo(fetch+reset 硬跟
origin/main,匿名 fetch)——线上=远端 main 确定性闭环,Sveltia 保存
即自动上线生效;stamp 记录构建 commit;node_modules 留仓内做增量
(loader 逻辑变更时须清 node_modules/.astro——EXIF 教训已注)
- en/archives/index.astro 补缺(此前 404):PostList locale 复用 +
译题映射;三级目录 import 深度坑再犯一次(已修)
验证: bash infra/deploy/build.sh → staging 29 html @0e792a7(裸 clone
实测);build 本地 28+1 页;E2E 30/30 PASS
Spec: 2026-10-25_add-blog-5c
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
ad5f9d3d95
commit
f8add5ad15
5 files changed
+153
-9
No files matched your search
Regular → Executable
+22
-9
@@ -1,20 +1,33 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# 构建管线:gitea push(main) → webhook → 本脚本 → dist rsync 到 www-staging
|
# 构建管线(Phase 5c 裸 clone 改造):构建源 = origin/main 硬跟,非本地工作树。
|
||||||
# Phase 1:真实 astro build;webhook 侧 HMAC 验签仍留 Phase 4 实装(见 webhook-receive.sh)
|
#
|
||||||
|
# 为什么:此前直接构建 ~/Code/Github/my-blog 工作树——分支/未提交状态会
|
||||||
|
# 意外上线(Sveltia 冒烟期实测发生过);且 Sveltia 的 main 提交无法自动
|
||||||
|
# 发布。改为独立构建仓 fetch+reset 硬跟 main:线上=远端 main,确定性闭环。
|
||||||
|
# blog 仓已公开,匿名 fetch 无需凭据。
|
||||||
set -e
|
set -e
|
||||||
REPO_DIR="$HOME/Code/Github/my-blog" # Phase 4 构建机改裸 clone 路径
|
BUILD_REPO="$HOME/docker_apps/blog/build-repo"
|
||||||
STAGING="$HOME/docker_apps/blog/www-staging"
|
STAGING="$HOME/docker_apps/blog/www-staging"
|
||||||
|
|
||||||
echo "[build] pnpm install --frozen-lockfile + astro build"
|
if [ ! -d "$BUILD_REPO/.git" ]; then
|
||||||
cd "$REPO_DIR"
|
echo "[build] 首次克隆构建仓"
|
||||||
|
git clone https://git.wait4x.online/wait4x/blog.git "$BUILD_REPO"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$BUILD_REPO"
|
||||||
|
git fetch origin main
|
||||||
|
git reset --hard origin/main
|
||||||
|
git clean -fd --exclude node_modules
|
||||||
|
SHA=$(git rev-parse --short HEAD)
|
||||||
|
echo "[build] 构建源: origin/main @ $SHA"
|
||||||
|
|
||||||
pnpm install --frozen-lockfile >/dev/null
|
pnpm install --frozen-lockfile >/dev/null
|
||||||
pnpm astro build
|
pnpm astro build
|
||||||
|
|
||||||
echo "[build] rsync -> $STAGING"
|
echo "[build] rsync -> $STAGING"
|
||||||
mkdir -p "$STAGING"
|
mkdir -p "$STAGING"
|
||||||
rsync -a --delete "$REPO_DIR/dist/" "$STAGING/"
|
rsync -a --delete "$BUILD_REPO/dist/" "$STAGING/"
|
||||||
# 原图仓独立同步(不进 git/dist):/photos-originals/ 供图库"查看原图/下载"
|
|
||||||
ORIG="$HOME/docker_apps/blog/photos-originals"
|
ORIG="$HOME/docker_apps/blog/photos-originals"
|
||||||
if [ -d "$ORIG" ]; then rsync -a "$ORIG/" "$STAGING/photos-originals/"; fi
|
if [ -d "$ORIG" ]; then rsync -a "$ORIG/" "$STAGING/photos-originals/"; fi
|
||||||
printf 'built at %s\n' "$(date -Is)" > "$STAGING/.build-stamp"
|
printf 'built %s at %s\n' "$SHA" "$(date -Is)" > "$STAGING/.build-stamp"
|
||||||
echo "[build] done: $(find "$STAGING" -name '*.html' | wc -l) html files"
|
echo "[build] done: $(find "$STAGING" -name '*.html' | wc -l) html files @ $SHA"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "lighthouse:default",
|
||||||
|
"settings": {
|
||||||
|
"onlyCategories": ["performance", "accessibility", "best-practices", "seo"],
|
||||||
|
"formFactor": "mobile",
|
||||||
|
"screenEmulation": {
|
||||||
|
"mobile": true,
|
||||||
|
"width": 390,
|
||||||
|
"height": 844,
|
||||||
|
"deviceScaleFactor": 3,
|
||||||
|
"disabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Lighthouse 移动端真测:对本地 staging 五页跑分(Phase 5c T3)。
|
||||||
|
# 走 pnpm dlx 而非装进 devDependencies:本车道边界只有本目录与产物,不动 package.json。
|
||||||
|
# 用法:run.sh [staging基址,默认 http://127.0.0.1:18743] [产物目录,默认 /tmp/e2e-shots/lighthouse]
|
||||||
|
set -euo pipefail
|
||||||
|
BASE="${1:-http://127.0.0.1:18743}"
|
||||||
|
OUT="${2:-/tmp/e2e-shots/lighthouse}"
|
||||||
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# Playwright 的 chromium 复用为 lighthouse 引擎(无系统 Chrome 依赖)
|
||||||
|
export CHROME_PATH="${CHROME_PATH:-$HOME/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome}"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
|
||||||
|
for p in "" "archives/mian-sha/" "gallery/" "en/" "terminal/"; do
|
||||||
|
name="${p:-home}"; name="${name//\//_}"; name="${name%/}"
|
||||||
|
echo "== lighthouse: /$p"
|
||||||
|
pnpm dlx lighthouse "$BASE/$p" \
|
||||||
|
--config-path="$DIR/lh-mobile.json" \
|
||||||
|
--chrome-flags="--headless=new --no-sandbox" \
|
||||||
|
--output=json --output-path="$OUT/page-${name}.json" \
|
||||||
|
--quiet >/dev/null || echo "WARN: /$p 未完成(产物可能缺失)"
|
||||||
|
done
|
||||||
|
|
||||||
|
python3 "$DIR/summarize.py" "$OUT"
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""汇总 lighthouse 五页报告:分数表 + 未达标项的文件级嫌疑清单。
|
||||||
|
|
||||||
|
输入:目录内 page-*.json(lighthouse 全量输出)
|
||||||
|
输出:/tmp/e2e-shots/lighthouse-report.json(全量合并)+ stdout 摘要表
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 只挑与"可执行优化"相关的审计做嫌疑展开;其余仅计分
|
||||||
|
SUSPECT_AUDITS = (
|
||||||
|
'renderblocking-resources', 'unused-javascript', 'unused-css-rules',
|
||||||
|
'modern-image-formats', 'uses-optimized-images', 'total-byte-weight',
|
||||||
|
'layout-shift-elements', 'layout-shifts', 'largest-contentful-paint-element',
|
||||||
|
'color-contrast', 'font-display', 'mainthread-work-breakdown', 'bootup-time',
|
||||||
|
'dom-size', 'offscreen-images', 'uses-responsive-images', 'unminified-css',
|
||||||
|
'unminified-javascript', 'efficient-animated-content', 'duplicated-javascript',
|
||||||
|
'legacy-javascript', 'prioritize-lcp-image', 'long-tasks',
|
||||||
|
)
|
||||||
|
CATEGORY_LABEL = {'performance': 'Perf', 'accessibility': 'A11y', 'best-practices': 'BP', 'seo': 'SEO'}
|
||||||
|
METRICS = ('first-contentful-paint', 'largest-contentful-paint', 'total-blocking-time',
|
||||||
|
'cumulative-layout-shift', 'speed-index', 'interactive')
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_ms(v):
|
||||||
|
return f"{v / 1000:.1f}s" if isinstance(v, (int, float)) else str(v)
|
||||||
|
|
||||||
|
|
||||||
|
def main(outdir: pathlib.Path):
|
||||||
|
pages = {}
|
||||||
|
rows = []
|
||||||
|
for f in sorted(outdir.glob('page-*.json')):
|
||||||
|
data = json.loads(f.read_text())
|
||||||
|
url = data.get('finalDisplayedUrl') or data.get('finalUrl') or f.stem
|
||||||
|
path = '/' + url.split('/', 3)[-1].replace('127.0.0.1:18743', '') if url.count('/') >= 3 else url
|
||||||
|
cats = {k: round(v['score'] * 100) for k, v in data['categories'].items() if v.get('score') is not None}
|
||||||
|
metrics = {m: fmt_ms(data['audits'][m].get('numericValue')) for m in METRICS if m in data['audits']}
|
||||||
|
suspects = {}
|
||||||
|
for cid, cat in data['categories'].items():
|
||||||
|
if cats.get(cid, 100) >= 100:
|
||||||
|
continue
|
||||||
|
for ref in cat.get('auditRefs', []):
|
||||||
|
aid = ref['id']
|
||||||
|
a = data['audits'].get(aid, {})
|
||||||
|
if a.get('score') is not None and a['score'] < 0.9 and aid in SUSPECT_AUDITS:
|
||||||
|
items = (a.get('details', {}) or {}).get('items', [])
|
||||||
|
hint = []
|
||||||
|
for it in items[:4]:
|
||||||
|
for key in ('url', 'node.selector', 'node.path', 'source.url'):
|
||||||
|
v = it
|
||||||
|
for seg in key.split('.'):
|
||||||
|
v = v.get(seg) if isinstance(v, dict) else None
|
||||||
|
if v:
|
||||||
|
hint.append(str(v).split('?')[0][:110])
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
hint.append(str(it)[:90])
|
||||||
|
if hint:
|
||||||
|
suspects[aid] = {'score': a['score'], 'items': hint}
|
||||||
|
pages[path or f.stem] = {'scores': cats, 'metrics': metrics, 'suspects': suspects}
|
||||||
|
rows.append((path, cats))
|
||||||
|
|
||||||
|
labels = [CATEGORY_LABEL[c] for c in ('performance', 'accessibility', 'best-practices', 'seo')]
|
||||||
|
print(f"{'页面':<28}" + ''.join(f"{l:>6}" for l in labels))
|
||||||
|
for path, cats in rows:
|
||||||
|
cells = ''.join(f"{str(cats.get(c, "-"))[:3]:>6}" for c in ('performance', 'accessibility', 'best-practices', 'seo'))
|
||||||
|
mark = ''.join(' ✗' if cats.get(c, 100) < 90 else ' ✓' for c in ('performance', 'accessibility', 'best-practices', 'seo'))
|
||||||
|
print(f"{path:<28}{cells} {mark}")
|
||||||
|
|
||||||
|
dest = pathlib.Path('/tmp/e2e-shots/lighthouse-report.json')
|
||||||
|
dest.write_text(json.dumps(pages, ensure_ascii=False, indent=1))
|
||||||
|
print(f"\n全量合并报告: {dest}({dest.stat().st_size} B)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main(pathlib.Path(sys.argv[1]))
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
// /en/archives/ 列表页(补缺:此前 404)
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||||
|
import PostList from '../../../components/PostList.astro';
|
||||||
|
|
||||||
|
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
|
||||||
|
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||||
|
);
|
||||||
|
const enTitle = Object.fromEntries(
|
||||||
|
(await getCollection('postsEn')).map((e) => [e.id, e.data.title]),
|
||||||
|
);
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="Posts · wait4x" description="All posts">
|
||||||
|
<PostList {posts} locale="en" titleOverrides={enTitle} />
|
||||||
|
</BaseLayout>
|
||||||
Reference in new issue
Block a user