├── workers-site ├── .cargo-ok ├── .gitignore ├── package.json ├── package-lock.json └── index.js ├── web ├── public │ └── favicon.ico ├── src │ ├── assets │ │ ├── logo.png │ │ ├── github.png │ │ └── base.css │ ├── main.js │ ├── components │ │ └── BlogInfoCard.vue │ └── App.vue ├── vite.config.js └── index.html ├── .vscode └── extensions.json ├── wrangler.toml ├── package.json ├── .gitignore ├── LICENSE ├── .github └── workflows │ └── deploy.yml ├── index.js └── README.md /workers-site/.cargo-ok: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /workers-site/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | worker 3 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idealclover/Friend-Link-House/main/web/public/favicon.ico -------------------------------------------------------------------------------- /web/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idealclover/Friend-Link-House/main/web/src/assets/logo.png -------------------------------------------------------------------------------- /web/src/assets/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idealclover/Friend-Link-House/main/web/src/assets/github.png -------------------------------------------------------------------------------- /web/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | # TODO: 需修改的配置,用于上传到 CloudFlare 2 | name = "blogroll" 3 | type = "webpack" 4 | account_id = "ed02115b0333e518a62d649ee291264b" 5 | workers_dev = true 6 | # route = "blogroll.icl.moe/*" 7 | zone_id = "374cfa163417e4f4a64edb66a0ddc218" 8 | 9 | [site] 10 | bucket = "./web/dist" 11 | entry-point = "workers-site" 12 | -------------------------------------------------------------------------------- /web/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | resolve: { 10 | alias: { 11 | '@': fileURLToPath(new URL('./src', import.meta.url)) 12 | } 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /workers-site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "worker", 4 | "version": "1.0.0", 5 | "description": "A template for kick starting a Cloudflare Workers project", 6 | "main": "index.js", 7 | "author": "Ashley Lewis ", 8 | "license": "MIT", 9 | "dependencies": { 10 | "@cloudflare/kv-asset-handler": "~0.1.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blogroll", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "cd web && vite", 6 | "gen": "node index.js", 7 | "build": "cd web && vite build", 8 | "preview": "cd web && vite preview --port 5050" 9 | }, 10 | "dependencies": { 11 | "https-proxy-agent": "^5.0.1", 12 | "node-fetch": "^2.6.7", 13 | "rss": "^1.2.2", 14 | "rss-parser": "^3.12.0", 15 | "vue": "^3.2.29" 16 | }, 17 | "devDependencies": { 18 | "@vitejs/plugin-vue": "^2.1.0", 19 | "vite": "^2.7.13" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | web/public/opml.xml 31 | web/public/rss.xml 32 | web/public/linkList.json 33 | web/src/assets/opml.json 34 | web/src/assets/data.json 35 | -------------------------------------------------------------------------------- /workers-site/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worker", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@cloudflare/kv-asset-handler": { 8 | "version": "0.1.2", 9 | "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.1.2.tgz", 10 | "integrity": "sha512-otES1gV5mEhNh82p/sJERPMMrC7UOLV2JyfKf4e3EX1TmMkZ3N8IDGAqRNsoRU8UYTO7wc83I7pH1p4ozAdgMQ==", 11 | "requires": { 12 | "mime": "^2.5.2" 13 | } 14 | }, 15 | "mime": { 16 | "version": "2.5.2", 17 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", 18 | "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nanjing University Linux Users Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 友链屋 | idealclover 16 | 17 | 18 | 22 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | schedule: 7 | - cron: "0 4,16 * * *" 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: 14 | - 18.x 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | with: 19 | # 令 GitHub 在 git clone 和 git checkout 后「忘记」使用的 credentials。 20 | # 如果之后需要以另外的身份(如你的 GitHub Bot)执行 git push 操作时(如部署到 GitHub Pages),必须设置为 false。 21 | persist-credentials: false 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | # 缓存 node_modules,缓存机制参见 GitHub 文档:https://help.github.com/en/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows 27 | - name: Cache node_modules 28 | uses: actions/cache@v1 # 使用 GitHub 官方的缓存 Action。 29 | env: 30 | cache-name: blogroll-node-modules 31 | with: 32 | path: node_modules 33 | key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }} # 使用 package-lock.json 的 Hash 作为缓存的 key。也可以使用 package.json 代替 34 | # Wrangler 在构建时会在 workers-site 目录下执行 npm i,因此也要缓存这里的 node_modules 35 | - name: Cache workers-site/node_modules 36 | uses: actions/cache@v1 37 | env: 38 | cache-name: workers-site-node-modules 39 | with: 40 | path: workers-site/node_modules 41 | key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('workers-site/package-lock.json') }} 42 | - run: npm i # 执行 Blogroll 的依赖安装 43 | - run: npm run gen # 相当于 node index.js,生成 opml.xml,opml.json 和 data.json 44 | - run: npm run build 45 | - name: Deploy to Cloudflare Workers 46 | uses: cloudflare/wrangler-action@1.1.0 47 | with: 48 | apiToken: ${{ secrets.CF_WORKERS_TOKEN }} # 前一步设置的 Secrets 的名称 49 | # Wrangler Action 也支持使用传统的 Global API Token + Email 的鉴权方式,但不推荐 50 | -------------------------------------------------------------------------------- /web/src/components/BlogInfoCard.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 49 | 50 | 65 | -------------------------------------------------------------------------------- /workers-site/index.js: -------------------------------------------------------------------------------- 1 | import { getAssetFromKV, mapRequestToAsset } from '@cloudflare/kv-asset-handler' 2 | 3 | /** 4 | * The DEBUG flag will do two things that help during development: 5 | * 1. we will skip caching on the edge, which makes it easier to 6 | * debug. 7 | * 2. we will return an error message on exception in your Response rather 8 | * than the default 404.html page. 9 | */ 10 | const DEBUG = false 11 | 12 | addEventListener('fetch', event => { 13 | try { 14 | event.respondWith(handleEvent(event)) 15 | } catch (e) { 16 | if (DEBUG) { 17 | return event.respondWith( 18 | new Response(e.message || e.toString(), { 19 | status: 500, 20 | }), 21 | ) 22 | } 23 | event.respondWith(new Response('Internal Error', { status: 500 })) 24 | } 25 | }) 26 | 27 | async function handleEvent(event) { 28 | const url = new URL(event.request.url) 29 | let options = {} 30 | 31 | /** 32 | * You can add custom logic to how we fetch your assets 33 | * by configuring the function `mapRequestToAsset` 34 | */ 35 | // options.mapRequestToAsset = handlePrefix(/^\/docs/) 36 | 37 | try { 38 | if (DEBUG) { 39 | // customize caching 40 | options.cacheControl = { 41 | bypassCache: true, 42 | }; 43 | } 44 | const page = await getAssetFromKV(event, options); 45 | 46 | // allow headers to be altered 47 | const response = new Response(page.body, page); 48 | 49 | response.headers.set("X-XSS-Protection", "1; mode=block"); 50 | response.headers.set("X-Content-Type-Options", "nosniff"); 51 | response.headers.set("X-Frame-Options", "DENY"); 52 | response.headers.set("Referrer-Policy", "unsafe-url"); 53 | response.headers.set("Feature-Policy", "none"); 54 | response.headers.set('Access-Control-Allow-Origin', "*"); 55 | 56 | return response; 57 | 58 | } catch (e) { 59 | // if an error is thrown try to serve the asset at 404.html 60 | if (!DEBUG) { 61 | try { 62 | let notFoundResponse = await getAssetFromKV(event, { 63 | mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/404.html`, req), 64 | }) 65 | 66 | return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 }) 67 | } catch (e) {} 68 | } 69 | 70 | return new Response(e.message || e.toString(), { status: 500 }) 71 | } 72 | } 73 | 74 | /** 75 | * Here's one example of how to modify a request to 76 | * remove a specific prefix, in this case `/docs` from 77 | * the url. This can be useful if you are deploying to a 78 | * route on a zone, or if you only want your static content 79 | * to exist at a specific path. 80 | */ 81 | function handlePrefix(prefix) { 82 | return request => { 83 | // compute the default (e.g. / -> index.html) 84 | let defaultAssetKey = mapRequestToAsset(request) 85 | let url = new URL(defaultAssetKey.url) 86 | 87 | // strip the prefix from the path for lookup 88 | url.pathname = url.pathname.replace(prefix, '/') 89 | 90 | // inherit all other props from the default request 91 | return new Request(url.toString(), defaultAssetKey) 92 | } 93 | } -------------------------------------------------------------------------------- /web/src/assets/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | overflow-x: hidden; 4 | color: #2c323c; 5 | background-color: #f6f6f6; 6 | font-family: "PingFang SC", "Microsoft YaHei", sans-serif, Arial; 7 | } 8 | 9 | html body table td, 10 | html body table th { 11 | border: 1px solid #d6d6d6; 12 | padding: 6px 13px; 13 | } 14 | 15 | html body table th { 16 | font-weight: bold; 17 | color: #000; 18 | } 19 | 20 | td { 21 | display: table-cell; 22 | vertical-align: inherit; 23 | } 24 | 25 | th { 26 | display: table-cell; 27 | vertical-align: inherit; 28 | font-weight: bold; 29 | text-align: -internal-center; 30 | } 31 | 32 | thead { 33 | display: table-header-group; 34 | vertical-align: middle; 35 | border-color: inherit; 36 | } 37 | 38 | html body table { 39 | margin: 10px 0 15px 0; 40 | border-collapse: collapse; 41 | border-spacing: 0; 42 | display: block; 43 | width: 100%; 44 | overflow: auto; 45 | word-break: normal; 46 | word-break: keep-all; 47 | } 48 | 49 | blockquote { 50 | margin: 20px auto; 51 | color: #555555; 52 | padding: 3px 3px 3px 12px; 53 | border-left: 8px solid #84b3ff; 54 | position: relative; 55 | background: #f0f0f0; 56 | } 57 | 58 | code { 59 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 60 | background-color: #f0f0f0; 61 | border-radius: 3px; 62 | padding: 3px; 63 | } 64 | 65 | pre code { 66 | background-color: transparent; 67 | } 68 | 69 | img { 70 | max-width: 100%; 71 | max-height: 100%; 72 | object-fit: cover; 73 | } 74 | 75 | ::-webkit-scrollbar { 76 | width: 5px; 77 | background-color: transparent; 78 | } 79 | 80 | ::-webkit-scrollbar-track { 81 | border-radius: 5px; 82 | margin: 5px 0; 83 | background-color: transparent; 84 | } 85 | 86 | ::-webkit-scrollbar-thumb { 87 | border-radius: 5px; 88 | background-color: rgba(175, 175, 175, 0.75); 89 | } 90 | 91 | ::-webkit-scrollbar-thumb:hover { 92 | border-radius: 5px; 93 | background-color: rgba(100, 100, 100, 0.85); 94 | } 95 | 96 | a { 97 | color: #3273dc; 98 | text-decoration: none; 99 | } 100 | 101 | a:hover { 102 | color: #2c323c; 103 | } 104 | 105 | #logo-left { 106 | flex-grow: 1; 107 | display: flex; 108 | align-items: center; 109 | } 110 | 111 | #logo-right { 112 | display: flex; 113 | align-items: center; 114 | padding-right: 5px; 115 | } 116 | 117 | #logo { 118 | height: 35px; 119 | width: 35px; 120 | } 121 | 122 | #logo-github { 123 | height: 30px; 124 | width: 30px; 125 | } 126 | 127 | #logo-text { 128 | padding: 0 10px; 129 | color: #444950; 130 | font-size: large; 131 | font-weight: bold; 132 | } 133 | 134 | #header { 135 | -webkit-box-shadow: 0 1px 3px rgb(18 18 18 / 10%); 136 | box-shadow: 0 1px 3px rgb(18 18 18 / 10%); 137 | z-index: 100; 138 | overflow: hidden; 139 | background: #fff; 140 | margin-bottom: 20px; 141 | } 142 | 143 | #banner { 144 | align-items: center; 145 | background-color: #aeb5bc; 146 | color: white; 147 | padding-top: 10px; 148 | padding-bottom: 10px; 149 | text-align: center; 150 | } 151 | 152 | #header-inner { 153 | width: 1200px; 154 | padding-left: 30px; 155 | height: 50px; 156 | margin: auto; 157 | display: flex; 158 | align-items: center; 159 | } 160 | 161 | #container { 162 | margin: auto; 163 | width: 1200px; 164 | overflow: hidden; 165 | } 166 | 167 | #main { 168 | float: left; 169 | width: 70%; 170 | } 171 | 172 | #sidebar { 173 | -webkit-box-shadow: 0 1px 3px rgb(18 18 18 / 10%); 174 | box-shadow: 0 1px 3px rgb(18 18 18 / 10%); 175 | border-radius: 5px; 176 | background-color: #fff; 177 | float: right; 178 | width: 30%; 179 | } 180 | 181 | #sidebar-content { 182 | margin: 20px 30px; 183 | } 184 | 185 | /* 适配窄框 */ 186 | @media (max-width: 1200px) { 187 | #container { 188 | width: 100%; 189 | } 190 | 191 | #header-inner { 192 | width: 100%; 193 | } 194 | 195 | #logo-right { 196 | padding-right: 45px; 197 | } 198 | 199 | #main { 200 | float: none; 201 | width: auto; 202 | } 203 | 204 | #sidebar { 205 | margin-top: 50px; 206 | float: none; 207 | width: auto; 208 | background-color: #f6f6f6; 209 | /* display: none; */ 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 101 | 102 | 122 | 123 | 228 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // 文件读取包 2 | const fs = require("fs"); 3 | // 引入 RSS 解析第三方包 4 | const Parser = require("rss-parser"); 5 | const parser = new Parser({ timeout: 12000 }); 6 | // 引入 RSS 生成器 7 | const RSS = require("rss"); 8 | // const HttpsProxyAgent = require("https-proxy-agent"); 9 | 10 | // TODO: 需要重点关注和修改的配置 11 | const opmlXmlContentTitle = "idealclover Blogroll"; 12 | const maxDataJsonItemsNumberForWeb = 120; // 保存前 120 项 13 | const maxDataJsonItemsNumberForRSS = 40; // 对RSS保存前 40 项 14 | var feed = new RSS({ 15 | title: "idealclover 友链屋", 16 | description: "翠翠和他的朋友们的blog,不代表翠翠本人观点", 17 | feed_url: "https://blogroll.icl.moe/rss.xml", 18 | site_url: "https://blogroll.icl.moe/", 19 | image_url: "https://blogroll.icl.moe/assets/logo.png", 20 | docs: "https://blogroll.icl.moe", 21 | managingEditor: "idealclover", 22 | webMaster: "idealclover", 23 | copyright: "2022 idealclover", 24 | language: "cn", 25 | ttl: "60", 26 | }); 27 | 28 | // 其他相关配置 29 | const readmeMdPath = "./README.md"; 30 | const opmlJsonPath = "./web/src/assets/opml.json"; 31 | const dataJsonPath = "./web/src/assets/data.json"; 32 | const linkListJsonPath = "./web/public/linkList.json"; 33 | const opmlXmlPath = "./web/public/opml.xml"; 34 | const rssXmlPath = "./web/public/rss.xml"; 35 | const opmlXmlContentOp = 36 | '\n \n ' + 37 | opmlXmlContentTitle + 38 | "\n \n \n\n"; 39 | const opmlXmlContentEd = "\n \n"; 40 | 41 | // 解析 README 中的表格,转为 JSON 42 | const pattern = 43 | /\| *([^\|]*) *\| *(http[^\|]*) *\| *([^\|\n]*) *\| *([^\| \n]*) *\| *([^\| \n]*) *\| *([^\| \n]*) *\|\n/g; 44 | // /\| *([^\|]*) *\| *(http[^\|]*) *\| *([^\|\n]*) *\| *([^\| \n]*) *\| *([^\| \n]*) *\| *([^\| \n]*) *\|/g; 45 | const readmeMdContent = fs.readFileSync(readmeMdPath, { encoding: "utf-8" }); 46 | 47 | const metaJson = []; 48 | let resultArray; 49 | while ((resultArray = pattern.exec(readmeMdContent)) !== null) { 50 | metaJson.push({ 51 | title: resultArray[1].trim(), 52 | htmlUrl: resultArray[2].trim(), 53 | description: resultArray[3].trim(), 54 | avatarUrl: resultArray[4].trim(), 55 | xmlUrl: resultArray[5].trim(), 56 | category: resultArray[6].trim(), 57 | }); 58 | } 59 | 60 | async function fetchWithTimeout(resource, options = {}) { 61 | // const { timeout = 12000 } = options; 62 | // options["agent"] = new HttpsProxyAgent("http://127.0.0.1:1087"); 63 | const controller = new AbortController(); 64 | setTimeout(() => controller.abort(), 12000); 65 | const response = await fetch(resource, { 66 | ...options, 67 | signal: controller.signal, 68 | }); 69 | // clearTimeout(id); 70 | return response; 71 | } 72 | 73 | // console.log(metaJson); 74 | 75 | (async () => { 76 | const linkListJson = {}; 77 | for (const meta of metaJson) { 78 | try { 79 | if(meta.category.includes("lost")){ 80 | meta.category = meta.category.substring(5); 81 | meta.avatarUrl = ""; 82 | meta.status = "lost"; 83 | continue; 84 | } 85 | // 确认网站是否可以访问 86 | const response = await fetchWithTimeout(meta.htmlUrl); 87 | const whiteList = ["Sukka"]; 88 | if (response.ok || whiteList.includes(meta["title"])) { 89 | meta.status = "active"; 90 | } else { 91 | meta.status = "lost"; 92 | meta.avatarUrl = ""; 93 | console.log("网络异常-未成功访问网站-404: " + meta.title); 94 | throw "404"; 95 | } 96 | 97 | try { 98 | // 获取网站默认URL 99 | if (meta.avatarUrl == "") { 100 | const favicon = meta.htmlUrl + "/favicon.ico"; 101 | const response = await fetchWithTimeout(favicon); 102 | if (response.ok) { 103 | meta.avatarUrl = favicon; 104 | } else { 105 | console.log("未成功获取图标: " + meta.title); 106 | } 107 | } 108 | // 获取网站默认RSS 109 | if (meta.xmlUrl == "") { 110 | const feed = meta.htmlUrl + "/feed"; 111 | const response = await fetchWithTimeout(feed); 112 | if (response.ok) { 113 | meta.xmlUrl = feed; 114 | } else { 115 | console.log("未成功获取RSS: " + meta.title); 116 | } 117 | } 118 | } catch (err) { 119 | // console.log(err); 120 | console.log("网络异常-未成功获取信息: " + meta.title); 121 | } 122 | } catch (err) { 123 | meta.status = "lost"; 124 | meta.avatarUrl = ""; 125 | console.log("网络异常-未成功访问网站-500: " + meta.title); 126 | } 127 | if (linkListJson[meta.category] == null) { 128 | linkListJson[meta.category] = { active: [], lost: [] }; 129 | } 130 | linkListJson[meta.category][meta.status].push(meta); 131 | } 132 | 133 | // 保存 linkList.json 134 | console.log(metaJson); 135 | await fs.writeFileSync( 136 | linkListJsonPath, 137 | JSON.stringify(linkListJson, null, 2), 138 | { 139 | encoding: "utf-8", 140 | } 141 | ); 142 | 143 | // 生成 opml.json 144 | const opmlJson = metaJson.map( 145 | ({ avatarUrl, description, category, ...rest }) => { 146 | return rest; 147 | } 148 | ); 149 | 150 | // 保存 opml.json 和 opml.xml 151 | fs.writeFileSync(opmlJsonPath, JSON.stringify(opmlJson, null, 2), { 152 | encoding: "utf-8", 153 | }); 154 | const opmlXmlContent = 155 | opmlXmlContentOp + 156 | opmlJson 157 | .map( 158 | (lineJson) => 159 | ` \n` 160 | ) 161 | .join("") + 162 | opmlXmlContentEd; 163 | fs.writeFileSync(opmlXmlPath, opmlXmlContent, { encoding: "utf-8" }); 164 | 165 | // 用于存储各项数据 166 | dataJson = []; 167 | 168 | for (const lineJson of metaJson) { 169 | if (lineJson.xmlUrl == "") { 170 | continue; 171 | } 172 | 173 | try { 174 | // 读取 RSS 的具体内容 175 | const feed = await parser.parseURL(lineJson.xmlUrl); 176 | console.log("xmlUrl: " + lineJson.xmlUrl); 177 | 178 | // 数组合并 179 | dataJson.push.apply( 180 | dataJson, 181 | feed.items 182 | .filter((item) => item.title) 183 | .map((item) => { 184 | // console.log(item) 185 | const pubDate = new Date(item.pubDate ?? item.published); 186 | // console.log(pubDate); 187 | return { 188 | name: lineJson.title, 189 | xmlUrl: lineJson.xmlUrl, 190 | htmlUrl: lineJson.htmlUrl, 191 | title: item.title, 192 | link: item.link, 193 | summary: item.summary ? item.summary : item.content, 194 | pubDate: pubDate, 195 | pubDateYYMMDD: pubDate.toISOString().split("T")[0], 196 | pubDateMMDD: pubDate.toISOString().split("T")[0].slice(5), 197 | pubDateYY: pubDate.toISOString().slice(0, 4), 198 | pubDateMM: pubDate.toISOString().slice(5, 7), 199 | }; 200 | }) 201 | ); 202 | } catch (err) { 203 | // 网络超时,进行 Log 报告 204 | console.log(err); 205 | console.log("-------------------------"); 206 | console.log("xmlUrl: " + lineJson.xmlUrl); 207 | console.log("-------------------------"); 208 | } 209 | } 210 | 211 | // 去重 212 | // https://stackoverflow.com/questions/23507853/remove-duplicate-objects-from-json-array 213 | dataJson = dataJson.filter( 214 | (arr, index, self) => index === self.findIndex((t) => t.title === arr.title) 215 | ); 216 | // 按时间顺序排序 217 | dataJson.sort((itemA, itemB) => (itemA.pubDate < itemB.pubDate ? 1 : -1)); 218 | // 默认为保存前 n 项的数据, 并保证不超过当前时间 219 | // for (let item of dataJson) { 220 | // console.log(item.title); 221 | // } 222 | const curDate = new Date(); 223 | const dataJsonSliced = dataJson.filter((item) => item.pubDate <= curDate); 224 | 225 | const dataJsonSlicedForWeb = dataJsonSliced 226 | .slice(0, Math.min(maxDataJsonItemsNumberForWeb, dataJson.length)) 227 | .map(({ summary, ...rest }) => { 228 | return rest; 229 | }); 230 | const dataJsonSlicedForRSS = dataJsonSliced.slice( 231 | 0, 232 | Math.min(maxDataJsonItemsNumberForRSS, dataJson.length) 233 | ); 234 | 235 | // 写 json 数据 236 | fs.writeFileSync( 237 | dataJsonPath, 238 | JSON.stringify(dataJsonSlicedForWeb, null, 2), 239 | { 240 | encoding: "utf-8", 241 | } 242 | ); 243 | 244 | feed.pubDate = dataJson[0].pubDate; 245 | 246 | //整理 RSS 数据 247 | for (let item of dataJsonSlicedForRSS) { 248 | feed.item({ 249 | title: item.title, 250 | description: item.summary, 251 | url: item.link, // link to the item 252 | author: item.name, // optional - defaults to feed author property 253 | date: item.pubDate.toISOString(), // any format that js Date can parse. 254 | }); 255 | } 256 | 257 | // 保存 rss.xml 文件 258 | const rssXmlContent = feed.xml(); 259 | fs.writeFileSync(rssXmlPath, rssXmlContent, { encoding: "utf-8" }); 260 | process.exit(); 261 | })(); 262 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # idealclover Blogroll 2 | 3 | > 🍭 翠翠和他的朋友们的博客更新 4 | 5 | [预览链接 blogroll.icl.moe](https://blogroll.icl.moe/) 6 | 7 | ![](https://s2.loli.net/2022/05/04/KTSp2DoxMEtCZWu.png) 8 | 9 | > 仅收录友链博客文章,不代表翠翠本人观点 10 | 11 | ## 这是什么 12 | 13 | 因为自己在经营着 [个人网站](https://idealclover.top) ~~别骂了别骂了 会更新的~~ 所以也认识了一些有自己独立博客的校友朋友以及网络小伙伴们。 14 | 15 | 最初的时候还会时不时去友链中逛逛,但随着这个列表越来越长,自己也就没有时间精力一个个点进去了。往往点进去发现有新的更新,但我却一直没看到。 16 | 17 | 痛定思痛,想着用 RSS 的方式订阅自己友链们的博客更新,这样可以时常去拜访拜访。 18 | 19 | 说来也巧,自己的一个学弟 [orangex4](https://orangex4.cool/) 搞了个项目,聚合了我南的一些独立博客。想着就可以魔改一下,用到自己的友链们上。因此趁着五一假期搞了搞。 20 | 21 | 看了下,原项目的原理是通过 GitHub Actions 每天两次对列表进行爬取,生成出对应的静态网站并更新到 cloudflare workers 上,也是很巧妙了。 22 | 23 | 代码很强,傻翠就是狗尾续貂改了个小样式就拿来用了~ [预览链接 blogroll.icl.moe](https://blogroll.icl.moe/) 24 | 25 | ## 如何使用 26 | 27 | 如果你也想整一个的话,其实也不难,相对还是比较好办的 28 | 29 | ### Fork 项目 30 | 31 | 这个就不用我教了吧,看见右上角那个 fork 按钮了不,点就完事了! 32 | 33 | 生成一个自己的仓库之后好方便做更新和修改。 34 | 35 | ### 配置 CloudFlare 36 | 37 | 最重要的是先配置 CloudFlare,让整个链路先跑起来,之后的具体代码再怎么改都来得及。 38 | 39 | CloudFlare 的网站在 [这里](https://cloudflare.com/),注册账号之后先在左侧选中 `workers`, 注册一个 workers 40 | 41 | 然后在 [这里](https://dash.cloudflare.com/profile/api-tokens) 注册一个 api 密钥,并且在你 fork 的 GitHub 仓库中 `Settings` 的 `Secrets` 里添加一个叫 `CF_WORKERS_TOKEN` 的密钥,把刚刚申请的 api 密钥添加进去 42 | 43 | 最后进入到 [wrangler.toml](wrangler.toml) 中,修改这个文件里面的 `account_id` 和 `zone_id`,其中 `account_id` 可以在 `workers` 中获取到,而对于 `zone_id`,如果你没有自定义域名的诉求,可以在最前面加井号注释掉 44 | 45 | 修改完成并同步到 main 分支之后,GitHub Actions 应该会自动启动,观察执行情况就可以了。正常来讲应该会执行成功的。 46 | 47 | ### 本地部署与修改 48 | 49 | 既然基础已经跑起来了,接下来需要把它变成自己的,需要修改的有几处,当然,具体修改哪些看你需求: 50 | 51 | 更便捷的配置化功能什么的,接下来交给学弟实现吧。 52 | 53 | - 修改获取的链接:直接修改这个 README.md 中下方的表格就可以了 54 | - 修改 logo 等其他前端展现(已标记 TODO) 55 | - ./web/public/favicon.ico -- 网站 icon 56 | - ./src/assets/logo.png -- 页内显示 logo 57 | - ./src/index.html -- 页面 title 58 | - ./src/APP.vue -- 页内标题及 banner 文案 59 | - 修改自动生成的 RSS 信息(已标记 TODO):index.js 60 | 61 | 在本地想部署起来的话,直接 clone 你自己 fork 出的仓库到本地,然后作为标准 npm 项目去部署 62 | 63 | ``` 64 | # 安装依赖 65 | npm install 66 | 67 | # 开发 68 | npm run dev 69 | 70 | # 测试 RSS 获取 71 | 72 | npm run gen 73 | 74 | # 构建 75 | npm run build 76 | ``` 77 | 78 | ## 想加翠翠友链? 79 | 80 | 和 [关于](https://idealclover.top/about.html) 页面中的要求一样,可以在关于页面中进行申请,也可以直接在这个项目中提 [issue](https://github.com/idealclover/blogroll/issues) 或者直接提 [pull request](https://github.com/idealclover/blogroll/pulls) 81 | 82 | 但不保证照单全收哦! 83 | 84 | ## LICENSE 85 | 86 | 项目基于 [NJU-LUG/Blogroll](https://github.com/nju-lug/blogroll),采用 [MIT Licence](./LICENSE) 87 | 88 | ## 傻翠的朋友 list 89 | 90 | | 名称 | 网站 | 描述(选填) | 头像(默认为/favicon.ico) | RSS(默认为/feed) | 分类 | 91 | | ------------------ | ------------------------------ | ------------------------------------ | ------------------------------------------------------------------------------------------------------ | ---------------------------------------- | ----------- | 92 | | idealclover | https://idealclover.top | 是翠翠的个人网站! | | | friend | 93 | | NJU-LUG | https://blogroll.njulug.org | | | | friend | 94 | | gq's Blog | https://blog.izgq.net | gq's blog | https://zgq.me/favicon.png | | friend | 95 | | bus1996 | https://bus1996.me | | https://bus1996.me/assets/img/favicon.ico | | friend | 96 | | HiKi | https://www.aneureka.cn | | https://www.aneureka.cn/img/favicon.ico | https://www.aneureka.cn/atom.xml | friend | 97 | | lizhihao6 | https://lizhihao6.online | | https://lizhihao6.online/wp-content/uploads/2019/06/cropped-22297584-32x32.jpg | | friend | 98 | | Jennifer's Blog | https://jyzhangchn.github.io | | https://jyzhangchn.github.io/images/gavin.JPG | | friend | 99 | | DIYGod | https://diygod.me | 写代码是热爱,写到世界充满爱! | https://4everland.xyz/ipfs/bafybeibefx2tyow77m2wcnsh5anaaxfy7ypxbcuapb52c4h255onqp72ye | http://diygod.me/atom.xml | net | 100 | | 咸鱼不咸 | https://lcblog.cn | | | | net | 101 | | XZYMOE'S BLOG | https://www.xzymoe.com | | | | net | 102 | | Tianyun Zhang | https://doowzs.com | doowzs's personal blog | | | friend | 103 | | 鹏鹏 | https://blog.chper.cn | | | | friend | 104 | | 毛若昕 | https://www.maorx.cn | 这里是毛若昕的个人主页 | | | friend | 105 | | 冰凌胧月的小窝 | https://imiku.me | 聆听最初的声音,向往无尽的未来 | | https://imiku.me/index.xml | net | 106 | | Zero | https://mikuac.com | 凉风乍起,一叶知秋。 | https://mikuac.com/img/avatar.jpg | | net | 107 | | VicBlog | https://ddadaal.me | | | https://ddadaal.me/rss.xml | friend | 108 | | YSZ 的个人主页 | https://yangshangzhen.com | | https://www.yangshangzhen.com/images/avatar.png | | friend | 109 | | 热铁盒 | https://rthsoftware.cn | | | | friend | 110 | | iznauy | https://iznauy.github.io | | https://avatars0.githubusercontent.com/u/22297856?s=400&u=9ac5d0437ef685b62e402ed130d67d589d234f0b&v=4 | | friend | 111 | | Literature | https://www.literature.hk | | | | net | 112 | | JBESU | https://jbesu.com | | | | friend | 113 | | 青空之蓝 | https://www.ixk.me | 站在时光一端,回忆过往记忆。 | https://ixk.me/_next/image?url=%2Fimage%2Fconfig%2Fauthor%2Favatar.400x400.png&w=1920&q=75 | https://blog.ixk.me/rss.xml | net | 114 | | 樱花庄的白猫 | https://2heng.xin | | https://2heng.xin/wp-content/static/favicon-96x96.png | | net | 115 | | 水风车 | https://blog.shuifengche.top | | | | friend | 116 | | 辣椒の酱 | https://removeif.github.io | 后端开发,技术分享 | https://removeif.github.io/images/avatar.jpg | | net | 117 | | BoBo | https://hewanyue.com/ | BOHC is just a blog of hechao | https://hewanyue.com/images/favicon.ico | https://hewanyue.com/atom.xml | friend | 118 | | Domon | https://www.domon.cn | Life is Simple. | | https://www.domon.cn/rss | net | 119 | | BOHC! | https://hewanyue.com | | https://hewanyue.com/images/favicon.ico | | net | 120 | | SangSir | https://sangsir.com | | | | net | 121 | | 恶魔菌の记事簿 | https://meow3.family.blog | | | | net | 122 | | 蓝小柠的博客 | https://bleshi.com | 是可爱的蓝孩子呀— | | | net | 123 | | 宇宙よりも遠い場所 | https://kirainmoe.com | | | https://kirainmoe.com/index.xml | net | 124 | | 小太の游乐园 | https://baka.fun | | | | net | 125 | | NoneData | https://www.nonedata.com | | https://gravatar.loli.net/avatar/8195a7772cd06cfc4fa303770d577c97 | | net | 126 | | dna049 | https://dna049.com | | | | net | 127 | | Mengzelev's Blog | https://mengzelev.github.io | | https://mengzelev.github.io/assets/moe.ico | | friend | 128 | | beyondstars | https://exploro.one | | | https://idx.best/api/feeds/atom | supporter | 129 | | Sukka | https://blog.skk.moe | | | https://blog.skk.moe/atom.xml | net | 130 | | fengkx | https://www.fengkx.top | | https://www.fengkx.top/images/icons/icon-128x128.png | https://www.fengkx.top/atom.xml | friend | 131 | | JosePhilo | https://josephilo.com | | | | net | 132 | | 蝉时雨 | https://chanshiyu.com | | | | lost-net | 133 | | ChrAlpha 的幻想乡 | https://ichr.me | | https://cdn.jsdelivr.net/npm/ckx@0.0.1/favicon/favicon-32x32.png | https://blog.ichr.me/atom.xml | net | 134 | | SpencerWoo | https://spencerwoo.com | | https://spencerwoo.com/assets/logo.png | | supporter | 135 | | LadderOperator | https://ladderoperator.top | | https://ladderoperator.top/img/favicon.jpg | https://ladderoperator.top/index.xml | friend | 136 | | 木子的博客 | https://blog.k8s.li | 垃圾佬、搬砖社畜、运维工程师 | | | net | 137 | | c0sMx | https://www.c0smx.com | | https://c0smx.lajiya.cn/favicon.ico | | net | 138 | | 云游君的小站 | https://www.yunyoujun.cn | 希望能成为一个有趣的人。 | https://www.yunyoujun.cn/favicon.svg | https://www.yunyoujun.cn/atom.xml | net | 139 | | 猫鱼的小站 | https://2cat.net | 記錄貓魚日常嘅生活感受同好玩趣事。 | https://2cat.net/wp-content/uploads/2020/04/cropped-YZSC.TAOBAO.COM-24-192x192.png | https://2cat.net/?feed=rss2 | net | 140 | | MiaoTony's 小窝 | https://miaotony.xyz | 仰望星空,脚踏实地,未来可期 | | https://miaotony.xyz/atom.xml | net | 141 | | Timegg | https://timegg.top | | https://timegg.top/images/favicon.ico | https://timegg.top/index.xml | lost-net | 142 | | Aengus Blog | https://www.aengus.top | Stay Hungry, Stay Foolish. | https://img.aengus.top/common/profile.png | | net | 143 | | ALID | https://calmtime.github.io | | https://calmtime.github.io/img/avatar-my.jpg | | friend | 144 | | klaus & laura | https://klauslaura.cn | | | | net | 145 | | Kant | https://deathfugue.com/ | | | https://deathfugue.com/index.php/feed/ | net | 146 | | Orangex4 | https://orangex4.cool | | https://orangex4.cool/images/icons/profile.jpg | https://orangex4.cool/atom.xml | lost-friend | 147 | | GeRongcun | https://www.gerongcun.xyz/blog | | | | friend | 148 | | 王荣胜 | https://sqdxwz.com | | | | net | 149 | | 小丁的个人博客 | https://tding.top | | https://tding.top/images/avatar.webp | https://tding.top/atom.xml | lost-net | 150 | | 风景工作室 | https://aspire.studio | | | | lost-net | 151 | | Manami | https://www.manami.top | | | | net | 152 | | Oasis's Blog | https://blog.imoasis.cn | | | | net | 153 | | 不鉴的安全屋 | https://ryushane.com | | | | friend | 154 | | 吴志成的博客 | https://hitigerzzz.github.io | | | | friend | 155 | | 南雍随笔 | https://ydjsir.com.cn | | https://ydjsir.com.cn/img/avatar.png | https://ydjsir.com.cn/atom.xml | friend | 156 | | Cyris | https://cyris.moe | | https://cyris.moe/images/favicon.ico | https://sound.cyris.moe/atom.xml | net | 157 | | Dejavu's Blog | https://blog.dejavu.moe | Not for success, just for growing. | https://blog.dejavu.moe/avatar.webp | https://blog.dejavu.moe/index.xml | net | 158 | | remiliacn | https://www.remiliacn.com | 东拼西凑的个人小站 | https://avatars.githubusercontent.com/remiliacn | | net | 159 | | 青鱼博客 | https://qingyu.me | | | | friend | 160 | | 太隐 | https://www.wangyurui.top | 一个人的思想发育史就是他的阅读史 | https://i.typlog.com/wangyr45/8354037003_3266735.png?x-oss-process=style/ss | https://wangyurui.com/feed.xml | net | 161 | | 送报少年 | https://okarin.cn | | | | net | 162 | | itsNekoDeng | https://dyfa.top | | https://nekodeng.gitee.io/medias/avatar.jpg | | net | 163 | | LarryZhao | https://larryzhao.com | | https://larryzhao.com/headimg.png | https://feeds.feedburner.com/larryzhao | friend | 164 | | Pemp's Blog | https://pemp.top | | https://pemp.top/images/logo.jpg | | friend | 165 | | SkyWT | https://skywt.cn | 热爱与激情永不止息。 | https://blog.skywt.cn/usr/themes/Daydream/assets/img/avatar.png | | net | 166 | | Anomie ZJU | https://dong2000.xyz | A blog of whatever goes | https://dong2000.xyz/wombo.png | https://dong2000.xyz/index.xml | net | 167 | | Cysime Moflu | https://blog.cysi.me | 再不会遇见第二个时光 | https://image.glaceon.net/uploads/202205012353016.jpg | https://blog.cysi.me/index.xml | net | 168 | | RK blog | https://blog.ohtoai.fun/ | Think N Thought | https://blog.ohtoai.fun/assets/avater.png | | net | 169 | | Good Morning | https://y-ichen.github.io | 子喵的个人博客站! | https://y-ichen.github.io/img/favicon.png | https://y-ichen.github.io/atom.xml | friend | 170 | | Chlorine | https://www.yoghurtlee.com | Como el viento. | https://img.clnya.fun/chlorine-juice-safer.avif | | net | 171 | | 烫烫烫的记事本 | https://leostudiooo.github.io/ | Stay hungry, stay foolish. | https://cdn.jsdelivr.net/gh/leostudiooo/leostudiooo.cdn/img/avatar.jpg | https://leostudiooo.github.io/atom.xml | net | 172 | | 山茶花舍 | https://irithys.com | 吕楪在记录自己的生活 | https://pic.imgdb.cn/item/63e38acc4757feff3372eb7e.webp | | net | 173 | | 张洪 Heo | https://blog.zhheo.com | 分享设计与科技生活 | https://bu.dusays.com/2022/12/28/63ac2812183aa.png | | net | 174 | | f2h2h1's blog | https://f2h2h1.github.io | 覆舟水是苍生泪,不到横流君不知 | https://f2h2h1.github.io/avatar.png | | net | 175 | | 精灵王 | https://jingling.im | 专注于全栈开发技术分享和开源项目讨论 | https://jingling.im/assets/jingling.svg | | net | 176 | | Guava’s Blog | https://www.humbleguava.top | Guava’s Blog | | https://www.humbleguava.top/rss/feed.xml | friend | 177 | | LoveApple的主页 | https://loveapple.icu | 一条喜欢苹果的水煮鱼 | https://loveapple.icu/img/machinist.jpg | https://loveapple.icu/rss.xml | friend | 178 | | 龙场茶室 | https://blog.peterchen97.cn | 一只练习时长六年半的 Web 开发练习生,这是他的一些杂文,欢迎交流~ | https://blog.peterchen97.cn/favicon.svg | https://blog.peterchen97.cn/rss.xml | net | --------------------------------------------------------------------------------