added 文章一篇
This commit is contained in:
1 parent
558020036f
commit
f1865dca6c
9 files changed
+477
-150
No files matched your search
@@ -61,6 +61,9 @@ jobs:
|
|||||||
- name: Generate Friend Circle Data
|
- name: Generate Friend Circle Data
|
||||||
run: node scripts/generate_circle_data.js
|
run: node scripts/generate_circle_data.js
|
||||||
|
|
||||||
|
- name: Check Link Status
|
||||||
|
run: node scripts/check_links.js
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: rm -rf public && hugo --minify
|
run: rm -rf public && hugo --minify
|
||||||
|
|
||||||
|
|||||||
@@ -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,或者跳转到奇怪的页面吧。
|
||||||
Generated
+28
-11
@@ -8,6 +8,7 @@
|
|||||||
"axios": "^1.13.4",
|
"axios": "^1.13.4",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
"rss-parser": "^3.13.0",
|
"rss-parser": "^3.13.0",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7"
|
||||||
}
|
}
|
||||||
@@ -124,12 +125,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "1.0.10",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
|
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||||
"dependencies": {
|
|
||||||
"sprintf-js": "~1.0.2"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
@@ -696,6 +694,26 @@
|
|||||||
"node": ">=6.0"
|
"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": {
|
"node_modules/has-symbols": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
@@ -895,12 +913,11 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "3.14.2",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.2.tgz",
|
"resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^1.0.7",
|
"argparse": "^2.0.1"
|
||||||
"esprima": "^4.0.0"
|
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"axios": "^1.13.4",
|
"axios": "^1.13.4",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
"rss-parser": "^3.13.0",
|
"rss-parser": "^3.13.0",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7"
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-138
@@ -1,156 +1,76 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
const axios = require('axios');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
// Configuration
|
const linksPath = path.join(__dirname, '../themes/Ying/data/links.yaml');
|
||||||
const LINKS_FILE = path.join(__dirname, '../themes/Ying/static/json/link_lite.json');
|
const outputPath = path.join(__dirname, '../static/link_status.json');
|
||||||
const CONCURRENCY = 10; // Check 10 links at a time
|
|
||||||
const TIMEOUT = 10000; // 10 seconds timeout
|
|
||||||
|
|
||||||
async function checkLink(link) {
|
// Ensure static dir exists
|
||||||
const controller = new AbortController();
|
if (!fs.existsSync(path.dirname(outputPath))) {
|
||||||
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkLinks() {
|
||||||
|
console.log('Starting link check...');
|
||||||
try {
|
try {
|
||||||
// Some blogs block headless requests, so we fake a User-Agent
|
const fileContents = fs.readFileSync(linksPath, 'utf8');
|
||||||
const response = await fetch(link.url, {
|
const data = yaml.load(fileContents);
|
||||||
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'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
if (!data || !data.blogs) {
|
||||||
|
console.log('No blogs found in links.yaml');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.ok) {
|
const results = {};
|
||||||
return { link, status: 'OK', code: response.status };
|
// Process in chunks to avoid too many concurrent connections
|
||||||
} else if (response.status === 405 || response.status === 403 || response.status === 503) {
|
const batchSize = 10;
|
||||||
// Some servers reject HEAD or block bots, try GET
|
const blogs = data.blogs;
|
||||||
const controllerGet = new AbortController();
|
|
||||||
const timeoutIdGet = setTimeout(() => controllerGet.abort(), TIMEOUT);
|
for (let i = 0; i < blogs.length; i += batchSize) {
|
||||||
try {
|
const batch = blogs.slice(i, i + batchSize);
|
||||||
const responseGet = await fetch(link.url, {
|
await Promise.all(batch.map(async (blog) => {
|
||||||
method: 'GET',
|
if (!blog.url) return;
|
||||||
signal: controllerGet.signal,
|
try {
|
||||||
headers: {
|
const response = await axios.head(blog.url, {
|
||||||
'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'
|
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() {
|
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
|
||||||
console.log('Starting friend link check...');
|
console.log(`Link status saved to ${outputPath}`);
|
||||||
console.log(`Reading links from: ${LINKS_FILE}`);
|
|
||||||
|
|
||||||
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) {
|
} catch (e) {
|
||||||
console.error('Error parsing JSON:', e.message);
|
console.error(e);
|
||||||
process.exit(1);
|
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();
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -289,11 +289,108 @@ code::-webkit-scrollbar {
|
|||||||
color: #8be9fd;
|
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 {
|
.atk-list.has-ai-comment .atk-list-no-comment {
|
||||||
display: none !important;
|
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,
|
[data-theme="dark"] pre,
|
||||||
|
|||||||
@@ -905,3 +905,105 @@ function initSearchFunctionality() {
|
|||||||
document.addEventListener('DOMContentLoaded', initSearchFunctionality);
|
document.addEventListener('DOMContentLoaded', initSearchFunctionality);
|
||||||
// Re-init on Pjax complete
|
// Re-init on Pjax complete
|
||||||
document.addEventListener('pjax:complete', initSearchFunctionality);
|
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('<span class="link-active-badge" title="可访问"></span>');
|
||||||
|
activeCount++;
|
||||||
|
}
|
||||||
|
} else if (status === 'dead') {
|
||||||
|
var $item = $link.closest('.links-item');
|
||||||
|
if ($item.find('.link-dead-badge').length === 0) {
|
||||||
|
$item.append('<span class="link-dead-badge" title="无法访问"></span>');
|
||||||
|
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));
|
||||||
|
};
|
||||||
@@ -177,6 +177,10 @@ document.addEventListener("pjax:complete", function () {
|
|||||||
initCodeCopy()
|
initCodeCopy()
|
||||||
// 初始化代码折叠
|
// 初始化代码折叠
|
||||||
initCodeFold()
|
initCodeFold()
|
||||||
|
// 初始化友链状态
|
||||||
|
if (typeof window.initLinkStatus === 'function') {
|
||||||
|
window.initLinkStatus();
|
||||||
|
}
|
||||||
// 重载评论
|
// 重载评论
|
||||||
if (typeof window.initArtalk === 'function') {
|
if (typeof window.initArtalk === 'function') {
|
||||||
window.initArtalk();
|
window.initArtalk();
|
||||||
|
|||||||
Reference in new issue
Block a user