From f1865dca6c8aaf576c720ac02557ec24bde5f072 Mon Sep 17 00:00:00 2001 From: Vaica <94612053+zqlit@users.noreply.github.com> Date: Fri, 30 Jan 2026 23:07:58 +0800 Subject: [PATCH] =?UTF-8?q?added=20=E6=96=87=E7=AB=A0=E4=B8=80=E7=AF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/upy.yml | 3 + .../index.md | 141 +++++++++++++ package-lock.json | 39 +++- package.json | 1 + scripts/check_links.js | 198 ++++++------------ static/link_status.json | 42 ++++ themes/Ying/assets/css/main.css | 99 ++++++++- themes/Ying/assets/js/main.js | 102 +++++++++ themes/Ying/assets/js/mypjax.js | 4 + 9 files changed, 478 insertions(+), 151 deletions(-) create mode 100644 content/post/2026-02-01-博客友链实时健康监测方案/index.md create mode 100644 static/link_status.json diff --git a/.github/workflows/upy.yml b/.github/workflows/upy.yml index d8bda95f..4852a5d7 100644 --- a/.github/workflows/upy.yml +++ b/.github/workflows/upy.yml @@ -61,6 +61,9 @@ jobs: - name: Generate Friend Circle Data run: node scripts/generate_circle_data.js + - name: Check Link Status + run: node scripts/check_links.js + - name: Build run: rm -rf public && hugo --minify diff --git a/content/post/2026-02-01-博客友链实时健康监测方案/index.md b/content/post/2026-02-01-博客友链实时健康监测方案/index.md new file mode 100644 index 00000000..52615c90 --- /dev/null +++ b/content/post/2026-02-01-博客友链实时健康监测方案/index.md @@ -0,0 +1,141 @@ +--- +title: "博客友链实时健康监测方案" +date: 2026-02-01T10:00:00+08:00 +draft: false +categories: ["技术"] +tags: ["GitHub Actions", "Node.js", "博客优化", "自动化"] +--- + +最近给博客加上了一个实用的功能:**友链实时健康监测**。 + +起因是备案审查越来越严格。对于个人博客来说,如果友链中的某个域名过期被抢注,摇身一变成为博彩或色情网站,而我的博客还挂着它的链接,那么我的域名很有可能因为“导流违规内容”而被注销备案,甚至面临关站风险。 + +为了避免这种“人在家中坐,锅从天上来”的情况,手动每天一个个点开检查显然是不现实的。于是,利用 GitHub Actions 和简单的 Node.js 脚本实现自动化监测,就成了最佳解决方案。 + +## 实现思路 + +整体流程非常简单,分为三个环节: + +1. **数据源**:读取博客现有的友链配置文件(YAML 格式)。 +2. **监测脚本**:编写 Node.js 脚本,批量请求友链地址,检测 HTTP 状态码。 +3. **自动化运行**:通过 GitHub Actions 定时执行脚本,生成状态报告(JSON),并在前端展示。 + +## 1. 编写监测脚本 + +首先,在博客根目录的 `scripts` 文件夹下创建一个 `check_links.js`。我们需要用到 `axios` 来发送请求,`js-yaml` 来读取友链数据。 + +这个脚本的核心逻辑是: +- 读取 `themes/Ying/data/links.yaml`。 +- 对每个链接发起 `HEAD` 请求(如果失败则重试 `GET`)。 +- 如果状态码在 200-400 之间,判定为存活(True),否则为挂了(False)。 +- 将结果保存为 `static/link_status.json`。 + +```javascript +const fs = require('fs'); +const yaml = require('js-yaml'); +const axios = require('axios'); +const path = require('path'); + +// 配置文件路径 +const linksPath = path.join(__dirname, '../themes/Ying/data/links.yaml'); +// 输出结果路径(直接输出到 static 目录,Hugo 构建时会直接复制到根目录) +const outputPath = path.join(__dirname, '../static/link_status.json'); + +async function checkLinks() { + try { + const fileContents = fs.readFileSync(linksPath, 'utf8'); + const data = yaml.load(fileContents); + const results = {}; + + // 遍历所有友链 + const blogs = data.blogs; + for (const blog of blogs) { + try { + // 优先使用 HEAD 请求节省流量 + await axios.head(blog.url, { timeout: 10000 }); + results[blog.url] = true; + console.log(`[OK] ${blog.name}`); + } catch (error) { + // HEAD 失败尝试 GET + try { + await axios.get(blog.url, { timeout: 10000 }); + results[blog.url] = true; + console.log(`[OK] ${blog.name} (GET)`); + } catch (err2) { + results[blog.url] = false; + console.log(`[FAIL] ${blog.name}`); + } + } + } + + fs.writeFileSync(outputPath, JSON.stringify(results, null, 2)); + } catch (e) { + console.error(e); + process.exit(1); + } +} + +checkLinks(); +``` + +## 2. 配置 GitHub Actions + +脚本写好了,需要让它动起来。修改 `.github/workflows/upy.yml`(或者你的部署流程文件),在 Hugo 构建之前加入监测步骤。 + +这样每次博客更新,或者定时触发时,都会生成最新的 `link_status.json`。 + +```yaml +jobs: + deploy: + steps: + # ... 前置环境配置 ... + + - name: Check Link Status + run: node scripts/check_links.js + + - name: Build + run: hugo --minify +``` + +为了保证数据的实时性,建议配置 `schedule` 定时触发: + +```yaml +on: + schedule: + - cron: '0 */12 * * *' # 每12小时自动检查一次 +``` + +## 3. 前端展示状态 + +最后一步,在前端 JS 中读取这个 JSON 文件,并根据状态给友链添加样式。 + +我在 `main.js` 中加入了如下逻辑: +1. `fetch('/link_status.json')` 获取状态。 +2. 遍历页面上的友链元素。 +3. 如果状态为 `true`,添加绿色圆点;如果为 `false`,添加红色圆点。 + +```javascript +// 伪代码示例 +fetch('/link_status.json') + .then(response => response.json()) + .then(statusMap => { + document.querySelectorAll('.friend-link-item').forEach(item => { + const url = item.getAttribute('href'); + if (statusMap[url] === false) { + item.classList.add('dead-link'); // 样式控制显示红点 + } else { + item.classList.add('active-link'); // 样式控制显示绿点 + } + }); + }); +``` + +配合 CSS,就能实现如下效果: +- 🟢 **绿色**:网站可访问,安心。 +- 🔴 **红色**:网站无法访问,需要重点关注。 + +## 总结 + +这套方案最大的好处是**零成本、自动化**。不需要额外部署监控服务,完全利用 GitHub Actions 的计算资源。 + +对于注重 SEO 和网站安全的博主来说,这是一个性价比极高的“基建”工作。既对自己负责,也对访客负责。毕竟,谁也不想点击一个链接后发现是 404,或者跳转到奇怪的页面吧。 diff --git a/package-lock.json b/package-lock.json index e1430c0c..fd53a2b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "axios": "^1.13.4", "dotenv": "^17.2.3", "gray-matter": "^4.0.3", + "js-yaml": "^4.1.1", "rss-parser": "^3.13.0", "sqlite3": "^5.1.7" } @@ -124,12 +125,9 @@ } }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/asynckit": { "version": "0.4.0", @@ -696,6 +694,26 @@ "node": ">=6.0" } }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", @@ -895,12 +913,11 @@ "optional": true }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" diff --git a/package.json b/package.json index 6b726ae9..39b4ac6e 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "axios": "^1.13.4", "dotenv": "^17.2.3", "gray-matter": "^4.0.3", + "js-yaml": "^4.1.1", "rss-parser": "^3.13.0", "sqlite3": "^5.1.7" } diff --git a/scripts/check_links.js b/scripts/check_links.js index de95435b..3d7629ea 100644 --- a/scripts/check_links.js +++ b/scripts/check_links.js @@ -1,156 +1,76 @@ const fs = require('fs'); +const yaml = require('js-yaml'); +const axios = require('axios'); const path = require('path'); -// Configuration -const LINKS_FILE = path.join(__dirname, '../themes/Ying/static/json/link_lite.json'); -const CONCURRENCY = 10; // Check 10 links at a time -const TIMEOUT = 10000; // 10 seconds timeout +const linksPath = path.join(__dirname, '../themes/Ying/data/links.yaml'); +const outputPath = path.join(__dirname, '../static/link_status.json'); -async function checkLink(link) { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), TIMEOUT); - +// Ensure static dir exists +if (!fs.existsSync(path.dirname(outputPath))) { + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); +} + +async function checkLinks() { + console.log('Starting link check...'); try { - // Some blogs block headless requests, so we fake a User-Agent - const response = await fetch(link.url, { - method: 'HEAD', // Try HEAD first to save bandwidth - signal: controller.signal, - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' - } - }); + const fileContents = fs.readFileSync(linksPath, 'utf8'); + const data = yaml.load(fileContents); - clearTimeout(timeoutId); + if (!data || !data.blogs) { + console.log('No blogs found in links.yaml'); + return; + } + + const results = {}; + // Process in chunks to avoid too many concurrent connections + const batchSize = 10; + const blogs = data.blogs; - if (response.ok) { - return { link, status: 'OK', code: response.status }; - } else if (response.status === 405 || response.status === 403 || response.status === 503) { - // Some servers reject HEAD or block bots, try GET - const controllerGet = new AbortController(); - const timeoutIdGet = setTimeout(() => controllerGet.abort(), TIMEOUT); - try { - const responseGet = await fetch(link.url, { - method: 'GET', - signal: controllerGet.signal, - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + for (let i = 0; i < blogs.length; i += batchSize) { + const batch = blogs.slice(i, i + batchSize); + await Promise.all(batch.map(async (blog) => { + if (!blog.url) return; + try { + const response = await axios.head(blog.url, { + timeout: 10000, + maxRedirects: 5, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + }, + validateStatus: (status) => status >= 200 && status < 400 + }); + results[blog.url] = true; + console.log(`[OK] ${blog.name}`); + } catch (error) { + // Retry with GET if HEAD fails (some servers block HEAD) + try { + console.log(`[RETRY] ${blog.name} (HEAD failed, trying GET)...`); + await axios.get(blog.url, { + timeout: 10000, + maxRedirects: 5, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + }, + validateStatus: (status) => status >= 200 && status < 400 + }); + results[blog.url] = true; + console.log(`[OK] ${blog.name} (GET success)`); + } catch (err2) { + results[blog.url] = false; + console.log(`[FAIL] ${blog.name} - ${error.message}`); } - }); - clearTimeout(timeoutIdGet); - if (responseGet.ok) { - return { link, status: 'OK', code: responseGet.status }; - } else { - return { link, status: 'FAIL', code: responseGet.status }; } - } catch (err) { - clearTimeout(timeoutIdGet); - return { link, status: 'ERROR', error: err.message }; - } - } else { - return { link, status: 'FAIL', code: response.status }; + })); } - } catch (error) { - clearTimeout(timeoutId); - // Retry with GET if HEAD failed with network error (sometimes happens) - if (error.name !== 'AbortError') { - const controllerGet = new AbortController(); - const timeoutIdGet = setTimeout(() => controllerGet.abort(), TIMEOUT); - try { - const responseGet = await fetch(link.url, { - method: 'GET', - signal: controllerGet.signal, - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' - } - }); - clearTimeout(timeoutIdGet); - if (responseGet.ok) { - return { link, status: 'OK', code: responseGet.status }; - } else { - return { link, status: 'ERROR', error: error.message }; // Return original error if GET also fails or is not tried properly - } - } catch (err) { - clearTimeout(timeoutIdGet); - return { link, status: 'ERROR', error: error.message }; - } - } - return { link, status: 'ERROR', error: error.message }; - } -} -async function main() { - console.log('Starting friend link check...'); - console.log(`Reading links from: ${LINKS_FILE}`); + fs.writeFileSync(outputPath, JSON.stringify(results, null, 2)); + console.log(`Link status saved to ${outputPath}`); - if (!fs.existsSync(LINKS_FILE)) { - console.error('Error: Link file not found!'); - process.exit(1); - } - - let linksData; - let links; - try { - const fileContent = fs.readFileSync(LINKS_FILE, 'utf8'); - linksData = JSON.parse(fileContent); - // Adapt to both array (old) and object with friends key (new) - if (Array.isArray(linksData)) { - links = linksData; - } else if (linksData.friends && Array.isArray(linksData.friends)) { - // The new format has [name, url, avatar] arrays inside friends array - // We need to convert them to objects {name, url} for the checker - links = linksData.friends.map(item => { - if (Array.isArray(item)) { - return { name: item[0], url: item[1] }; - } - return item; - }); - } else { - console.error('Error: Unknown JSON format'); - process.exit(1); - } } catch (e) { - console.error('Error parsing JSON:', e.message); + console.error(e); process.exit(1); } - - console.log(`Found ${links.length} links. Checking...`); - - const results = []; - const failedLinks = []; - - // Process in chunks - for (let i = 0; i < links.length; i += CONCURRENCY) { - const chunk = links.slice(i, i + CONCURRENCY); - const chunkResults = await Promise.all(chunk.map(checkLink)); - - chunkResults.forEach(res => { - if (res.status === 'OK') { - process.stdout.write('.'); // Progress indicator - } else { - process.stdout.write('F'); - failedLinks.push(res); - } - results.push(res); - }); - } - - console.log('\n\nCheck completed!'); - console.log('---------------------------------------------------'); - - if (failedLinks.length === 0) { - console.log('✅ All links are accessible!'); - } else { - console.log(`⚠️ Found ${failedLinks.length} inaccessible links:\n`); - failedLinks.forEach((item, index) => { - console.log(`${index + 1}. ${item.link.name} (${item.link.url})`); - if (item.code) { - console.log(` Status Code: ${item.code}`); - } else { - console.log(` Error: ${item.error}`); - } - console.log(''); - }); - } } -main(); +checkLinks(); diff --git a/static/link_status.json b/static/link_status.json new file mode 100644 index 00000000..bd920ee9 --- /dev/null +++ b/static/link_status.json @@ -0,0 +1,42 @@ +{ + "https://loseu.cc": true, + "https://blog.keepke.com": true, + "https://heitaosan.com": true, + "https://blog.qydzz.cn": true, + "https://vian.top": true, + "https://blog.allnull.cn": true, + "https://blog.fivk.cn": true, + "https://blog.200011.net": true, + "https://www.rz.sb": true, + "https://duanxiansen.com": true, + "https://sweetjing.cc/": true, + "https://mojinxi.cn": true, + "https://www.sirit.com.cn": true, + "https://vimin.cc": true, + "https://xyzbz.cn": true, + "https://sumiler.com": true, + "https://www.hedelei.com": true, + "https://beifeng.me": true, + "https://t-t.live": true, + "https://baipin.pw": false, + "https://whooc.com": true, + "https://felixway.cn": true, + "https://www.liuchengrui.cn": true, + "https://vjo.cc": true, + "https://www.ershiliu.com/": true, + "https://www.liveout.cn/": true, + "https://blog.krisyan.dev": false, + "https://dai.ge": true, + "https://ihello.cc": true, + "https://likaijian.com": true, + "https://skylan.cc": true, + "https://www.zoujiang.com": true, + "https://www.jdeal.cn/": true, + "https://aloner.ink": true, + "https://www.dusays.com": true, + "https://digu.plus": true, + "https://jb18.cm": true, + "https://lms.im": true, + "https://immmmm.com": true, + "https://www.zishu.me": true +} \ No newline at end of file diff --git a/themes/Ying/assets/css/main.css b/themes/Ying/assets/css/main.css index 48c87c5e..89b28895 100644 --- a/themes/Ying/assets/css/main.css +++ b/themes/Ying/assets/css/main.css @@ -289,11 +289,108 @@ code::-webkit-scrollbar { color: #8be9fd; } -/* Artalk AI Comment Integration */ +/* Link Status Badge - Optimized */ +.links-item { + position: relative; +} + +/* Card View - Active */ +.link-active-badge { + position: absolute; + top: 8px; + right: 8px; + width: 8px; + height: 8px; + background-color: #4ade80; /* Soft Green */ + border-radius: 50%; + border: 2px solid #fff; /* White border for contrast */ + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + z-index: 10; +} + +[data-theme="dark"] .link-active-badge { + background-color: #22c55e; + border-color: #1f2933; /* Dark theme bg color */ + box-shadow: none; +} + +/* Card View - Dead */ +.link-dead-badge { + position: absolute; + top: 8px; + right: 8px; + width: 8px; + height: 8px; + background-color: #f87171; /* Soft Red */ + border-radius: 50%; + border: 2px solid #fff; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + z-index: 10; +} + +[data-theme="dark"] .link-dead-badge { + background-color: #ef4444; + border-color: #1f2933; + box-shadow: none; +} + +/* List View - Common */ +.links ul>li.link-active-li, +.links ul>li.link-dead-li { + position: relative; +} + +/* List View - Active */ +.links ul>li.link-active-li::before { + content: ""; + display: block; + position: absolute; + top: 10px; /* Aligned with text usually */ + width: 6px; + height: 6px; + border-radius: 50%; + background-color: #4ade80 !important; + box-shadow: none; +} + +/* List View - Dead */ +.links ul>li.link-dead-li::before { + content: ""; + display: block; + position: absolute; + top: 10px; + width: 6px; + height: 6px; + border-radius: 50%; + background-color: #f87171 !important; + box-shadow: none; +} + +[data-theme="dark"] .links ul>li.link-active-li::before { + background-color: #22c55e !important; +} + +[data-theme="dark"] .links ul>li.link-dead-li::before { + background-color: #ef4444 !important; +} + .atk-list.has-ai-comment .atk-list-no-comment { display: none !important; } +/* AI Comment Badge - Black & White Style */ +.atk-comment .atk-badge { + background: #000 !important; + color: #fff !important; + border: 1px solid #000 !important; +} + +[data-theme="dark"] .atk-comment .atk-badge { + background: #fff !important; + color: #000 !important; + border: 1px solid #fff !important; +} + [data-theme="dark"] pre, diff --git a/themes/Ying/assets/js/main.js b/themes/Ying/assets/js/main.js index cb39be35..f0d2b875 100644 --- a/themes/Ying/assets/js/main.js +++ b/themes/Ying/assets/js/main.js @@ -905,3 +905,105 @@ function initSearchFunctionality() { document.addEventListener('DOMContentLoaded', initSearchFunctionality); // Re-init on Pjax complete document.addEventListener('pjax:complete', initSearchFunctionality); + +// Call on ready +$(document).ready(function() { + window.initLinkStatus(); +}); + +// Link Status Check +window.initLinkStatus = function() { + console.log('initLinkStatus called'); + + // Selectors for both Card View (.links-item) and List View (.links li) + var $cardLinks = $('.links-item a'); + var $listLinks = $('.links ul > li > a'); + + console.log('Found card links:', $cardLinks.length); + console.log('Found list links:', $listLinks.length); + + // Only run if there are any links to check + if ($cardLinks.length === 0 && $listLinks.length === 0) return; + + console.log('Fetching /link_status.json...'); + fetch('/link_status.json?t=' + new Date().getTime()) + .then(response => { + console.log('Fetch response status:', response.status); + if (!response.ok) throw new Error('Status file not found'); + return response.json(); + }) + .then(data => { + console.log('Link status data loaded, keys:', Object.keys(data).length); + let activeCount = 0; + let deadCount = 0; + + // Helper function to check URL status + const getUrlStatus = (url) => { + if (!url) return 'unknown'; + var cleanUrl = url.replace(/\/$/, ''); + var httpsUrl = cleanUrl.replace(/^http:/, 'https:'); + var httpUrl = cleanUrl.replace(/^https:/, 'http:'); + + const keys = [ + url, + cleanUrl, + cleanUrl + '/', + httpsUrl, + httpUrl, + httpsUrl + '/', + httpUrl + '/' + ]; + + let isDead = false; + for (const key of keys) { + if (data[key] === true) return 'alive'; + if (data[key] === false) isDead = true; + } + + return isDead ? 'dead' : 'unknown'; + }; + + // Process Card View Links + $cardLinks.each(function() { + var $link = $(this); + var url = $link.attr('href'); + var status = getUrlStatus(url); + + if (status === 'alive') { + var $item = $link.closest('.links-item'); + if ($item.find('.link-active-badge').length === 0) { + $item.append(''); + activeCount++; + } + } else if (status === 'dead') { + var $item = $link.closest('.links-item'); + if ($item.find('.link-dead-badge').length === 0) { + $item.append(''); + deadCount++; + } + } + }); + + // Process List View Links + $listLinks.each(function() { + var $link = $(this); + var url = $link.attr('href'); + var status = getUrlStatus(url); + + if (status === 'alive') { + var $li = $link.closest('li'); + $li.addClass('link-active-li'); + $li.attr('title', '可访问'); + activeCount++; + } else if (status === 'dead') { + var $li = $link.closest('li'); + $li.addClass('link-dead-li'); + $li.attr('title', '无法访问'); + deadCount++; + } + }); + + console.log('Link status updated. Active:', activeCount, 'Dead:', deadCount); + }) + .catch(err => console.error('Link status check failed:', err)); +}; diff --git a/themes/Ying/assets/js/mypjax.js b/themes/Ying/assets/js/mypjax.js index 1af4f842..ad8e12ea 100644 --- a/themes/Ying/assets/js/mypjax.js +++ b/themes/Ying/assets/js/mypjax.js @@ -177,6 +177,10 @@ document.addEventListener("pjax:complete", function () { initCodeCopy() // 初始化代码折叠 initCodeFold() + // 初始化友链状态 + if (typeof window.initLinkStatus === 'function') { + window.initLinkStatus(); + } // 重载评论 if (typeof window.initArtalk === 'function') { window.initArtalk();