fix: 修复博客图片加载失败(掘金防盗链)

- 将《Git使用教学》中的掘金外链图片下载至 source/images/ 并替换为站内路径
- _config.yml 注入 no-referrer meta 作为兜底方案
- 新增 tools/download-juejin-images.js 批量下载/替换脚本
This commit is contained in:
2026-07-08 21:52:22 +08:00
parent 1d8ce5a16c
commit b3f44d235a
57 changed files with 1389 additions and 5 deletions
+122
View File
@@ -0,0 +1,122 @@
param(
# 要执行的命令:help / clean / build / server / preview / deploy
[Parameter(Position=0)]
[ValidateSet('help','clean','build','server','preview','deploy')]
[string]$Command = 'help'
)
# 只要出现错误就立即停止,避免后续命令继续执行
$ErrorActionPreference = 'Stop'
# 以脚本所在目录为基准,切回博客根目录
$root = Split-Path -Parent $PSScriptRoot
Set-Location $root
# 环境变量可覆盖默认值
$port = if ($env:PORT) { $env:PORT } else { '4000' }
$hostName = if ($env:HOST) { $env:HOST } else { '127.0.0.1' }
$branch = if ($env:BRANCH) { $env:BRANCH } else { 'main' }
function Show-Usage {
@"
./tools/blog.ps1 clean # public
./tools/blog.ps1 build #
./tools/blog.ps1 server #
./tools/blog.ps1 preview # + +
./tools/blog.ps1 deploy # + + git +
PORT=4000
HOST=127.0.0.1
BRANCH=main
"@
}
function Invoke-Hexo {
# 把后续参数原样转发给 hexo
param([Parameter(ValueFromRemainingArguments = $true)][string[]]$HexoArgs)
& npx hexo @HexoArgs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
function Add-BlogFiles {
# 只暂存博客相关文件,避免误把 node_modules、public 或其他杂项提交进去
$paths = @(
'_config.yml',
'_config.butterfly.yml',
'_config.landscape.yml',
'package.json',
'package-lock.json',
'scaffolds',
'source',
'themes',
'tools'
)
$existingPaths = @()
foreach ($path in $paths) {
if (Test-Path $path) {
$existingPaths += $path
}
}
if ($existingPaths.Count -eq 0) {
Write-Host '[deploy] 没有找到可暂存的博客文件'
exit 1
}
git add -- @existingPaths
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
switch ($Command) {
'clean' {
Write-Host '[clean] 清理生成缓存'
Invoke-Hexo clean
}
'build' {
Write-Host '[build] 生成静态文件'
Invoke-Hexo generate
}
'server' {
Write-Host "[server] 启动预览服务:http://${hostName}:${port}"
Invoke-Hexo server -i $hostName -p $port
}
'preview' {
Write-Host '[preview] 清理并生成静态文件'
Invoke-Hexo clean
Invoke-Hexo generate
Write-Host "[preview] 启动预览服务:http://${hostName}:${port}"
Invoke-Hexo server -i $hostName -p $port
}
'deploy' {
Write-Host '[deploy] 清理并生成静态文件'
Invoke-Hexo clean
Invoke-Hexo generate
Write-Host '[deploy] 只暂存博客相关改动'
Add-BlogFiles
# 没有变更就不提交也不推送
git diff --cached --quiet
if ($LASTEXITCODE -eq 0) {
Write-Host '[deploy] 没有可提交的改动,跳过 commit/push'
exit 0
}
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Write-Host "[deploy] 提交改动:chore: deploy blog $timestamp"
git commit -m "chore: deploy blog $timestamp"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "[deploy] 推送到 origin/$branch"
git push origin $branch
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
default {
Show-Usage
}
}
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
set -euo pipefail
# 切换到博客根目录,确保从任何位置执行都能正常工作
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
# 允许用环境变量覆盖默认值
PORT="${PORT:-4000}"
HOST="${HOST:-127.0.0.1}"
BRANCH="${BRANCH:-main}"
usage() {
cat <<'EOF'
博客脚本用法:
./tools/blog.sh clean # 清理缓存和 public 目录
./tools/blog.sh build # 生成静态文件
./tools/blog.sh server # 启动本地服务
./tools/blog.sh preview # 清理 + 生成 + 启动预览
./tools/blog.sh deploy # 清理 + 生成 + git 提交 + 推送
可选环境变量:
PORT=4000 预览端口
HOST=127.0.0.1 监听地址
BRANCH=main 推送分支
EOF
}
hexo_run() {
# 把参数原样传给 hexo
npx hexo "$@"
}
add_blog_files() {
# 只暂存博客相关文件,避免误把 node_modules、public 或其他杂项提交进去
local paths=(
"_config.yml"
"_config.butterfly.yml"
"_config.landscape.yml"
"package.json"
"package-lock.json"
"scaffolds"
"source"
"themes"
"tools"
)
local existing=()
local path
for path in "${paths[@]}"; do
if [[ -e "$path" ]]; then
existing+=("$path")
fi
done
if [[ ${#existing[@]} -eq 0 ]]; then
echo '[deploy] 没有找到可暂存的博客文件' >&2
exit 1
fi
git add -- "${existing[@]}"
}
case "${1:-help}" in
clean)
echo '[clean] 清理生成缓存'
hexo_run clean
;;
build)
echo '[build] 生成静态文件'
hexo_run generate
;;
server)
echo "[server] 启动预览服务:http://${HOST}:${PORT}"
hexo_run server -i "$HOST" -p "$PORT"
;;
preview)
echo '[preview] 清理并生成静态文件'
hexo_run clean
hexo_run generate
echo "[preview] 启动预览服务:http://${HOST}:${PORT}"
hexo_run server -i "$HOST" -p "$PORT"
;;
deploy)
echo '[deploy] 清理并生成静态文件'
hexo_run clean
hexo_run generate
echo '[deploy] 只暂存博客相关改动'
add_blog_files
# 没有变更就不提交也不推送
if git diff --cached --quiet; then
echo '[deploy] 没有可提交的改动,跳过 commit/push'
exit 0
fi
COMMIT_MSG="chore: deploy blog $(date '+%F %T')"
echo "[deploy] 提交改动:${COMMIT_MSG}"
git commit -m "$COMMIT_MSG"
echo "[deploy] 推送到 origin/${BRANCH}"
git push origin "$BRANCH"
;;
help|-h|--help)
usage
;;
*)
echo "未知命令: $1" >&2
usage
exit 1
;;
esac
+74
View File
@@ -0,0 +1,74 @@
// 下载 Markdown 中的掘金外链图片到 source/images/,并替换链接为站内路径
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const ROOT = 'c:/Users/Administrator/Desktop/新建文件夹/blog';
const MD = path.join(ROOT, 'source/_posts/Git使用教学.md');
const OUT = path.join(ROOT, 'source/images');
function download(url) {
return new Promise((resolve, reject) => {
const lib = url.startsWith('https') ? https : http;
const req = lib.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
// 跟随重定向
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
res.resume();
return download(new URL(res.headers.location, url).href).then(resolve, reject);
}
if (res.statusCode !== 200) {
res.resume();
return reject(new Error('HTTP ' + res.statusCode));
}
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => resolve(Buffer.concat(chunks)));
});
req.on('error', reject);
req.setTimeout(30000, () => req.destroy(new Error('timeout')));
});
}
(async () => {
fs.mkdirSync(OUT, { recursive: true });
let content = fs.readFileSync(MD, 'utf8');
const re = /!\[[^\]]*\]\((https:\/\/p3-juejin\.byteimg\.com\/[^)\s]+)\)/g;
const map = {}; // url -> /images/xxx.webp
let m;
const tasks = [];
while ((m = re.exec(content)) !== null) {
const url = m[1];
if (map[url]) continue;
const seg = url.split('/').pop();
const hash = seg.split('~')[0];
if (!hash) {
console.log('跳过(无法解析文件名): ' + url);
continue;
}
const file = hash + '.webp';
map[url] = '/images/' + file;
tasks.push(
download(url)
.then((buf) => {
fs.writeFileSync(path.join(OUT, file), buf);
console.log('[OK] ' + file + ' <- ' + url);
})
.catch((e) => {
console.log('[FAIL] ' + url + ' -> ' + e.message);
delete map[url];
})
);
}
await Promise.all(tasks);
for (const url in map) {
content = content.split(url).join(map[url]);
}
fs.writeFileSync(MD, content); // UTF-8 无 BOM
console.log('\n完成:处理 ' + Object.keys(map).length + ' 张图片');
})();
+61
View File
@@ -0,0 +1,61 @@
# 下载 Markdown 中的掘金外链图片到 source/images/,并替换链接为站内路径
param(
[string]$MdPath = "c:/Users/Administrator/Desktop/新建文件夹/blog/source/_posts/Git使用教学.md",
[string]$OutDir = "c:/Users/Administrator/Desktop/新建文件夹/blog/source/images"
)
$ErrorActionPreference = "Stop"
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
$content = Get-Content -Path $MdPath -Raw -Encoding UTF8
# 匹配 ![alt](url) 中的掘金图片地址
$pattern = '!\[[^\]]*\]\((https://p3-juejin\.byteimg\.com/[^)\s]+)\)'
$imgMatches = [regex]::Matches($content, $pattern)
$map = @{} # url -> /images/xxx.webp
$count = 0
$failed = @()
foreach ($m in $imgMatches) {
$url = $m.Groups[1].Value
if ($map.ContainsKey($url)) { continue }
# 用 URL 路径最后一段中 "~" 之前的部分作为文件名(即图片 hash)
$seg = ($url -split '/')[-1]
$hash = ($seg -split '~')[0]
if ([string]::IsNullOrEmpty($hash)) {
$failed += "无法解析文件名: $url"
continue
}
$file = $hash + '.webp'
$dest = Join-Path $OutDir $file
try {
curl.exe -s -L --max-time 30 -o $dest $url
if ((Test-Path $dest) -and ((Get-Item $dest).Length -gt 0)) {
$map[$url] = '/images/' + $file
$count++
Write-Host "[OK] $file <- $url"
} else {
$failed += "下载为空: $url"
}
} catch {
$failed += "下载异常: $url -> $_"
}
}
# 替换 Markdown 中的外链
foreach ($kv in $map.GetEnumerator()) {
$content = $content.Replace($kv.Key, $kv.Value)
}
# 写回(UTF-8 无 BOM
[System.IO.File]::WriteAllText($MdPath, $content, [System.Text.UTF8Encoding]::new($false))
Write-Host ""
Write-Host "完成:成功下载 $count 张,失败 $($failed.Count)"
if ($failed.Count -gt 0) {
Write-Host "失败列表:"
$failed | ForEach-Object { Write-Host " - $_" }
}