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();