优点
有不少的博客基于github-issues,包括etheral、Gmeek等等,当然,除了当博客,你也可以使用其来搞博客。
很好的一种博客写作方式,理论上GitHub不倒,这个方式可以一直使用。
手机上有GitHub的APP,你可以比较简单地在手机上发布动态。
这种方式可以被用来在各种博客里使用,包括Hugo、astro等等。
大致的工作流如下:
// .github/workflows/issue.yml name: Trigger Empty Commit on Issue Update on: issue_comment: types: [created, edited] workflow_dispatch: # 手动触发入口 jobs: trigger-empty-commit: runs-on: ubuntu-latest steps: - name: Check trigger type and prepare commit message id: check-trigger run: | # 处理手动触发 if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then echo "should_trigger=true" >> $GITHUB_OUTPUT echo "commit_msg='[Manual] Trigger update from moments/issues/1'" >> $GITHUB_OUTPUT # 处理issue评论事件 elif [ "${{ github.event.issue.number }}" -eq 1 ]; then echo "should_trigger=true" >> $GITHUB_OUTPUT echo "commit_msg='Trigger update from moments/issues/1'" >> $GITHUB_OUTPUT else echo "should_trigger=false" >> $GITHUB_OUTPUT echo "commit_msg=''" >> $GITHUB_OUTPUT fi - name: Trigger empty commit in lawtee.github.io if: steps.check-trigger.outputs.should_trigger == 'true' uses: actions/github-script@v6 env: PAT: ${{ secrets.PAT }} with: script: | const { execSync } = require('child_process'); const repo = 'h2dcc/lawtee.github.io'; const token = process.env.PAT; // 从步骤输出获取提交信息 const commitMsg = `${{ steps.check-trigger.outputs.commit_msg }}`; try { const repoUrl = `https://x-access-token:${token}@github.com/${repo}.git`; execSync(`git clone ${repoUrl}`, { stdio: 'inherit' }); process.chdir('lawtee.github.io'); execSync('git config user.name "github-actions[bot]"', { stdio: 'inherit' }); execSync('git config user.email "41898282+github-actions[bot]@users.noreply.github.com"', { stdio: 'inherit' }); // 安全执行空提交 execSync(`git commit --allow-empty -m "${commitMsg.replace(/"/g, '\\"')}"`, { stdio: 'inherit' }); execSync(`git push ${repoUrl} master`, { stdio: 'inherit' }); console.log('✅ Empty commit pushed successfully!'); } catch (error) { console.error('❌ Error:', error.message); process.exit(1); }
你需要的,是搞个公开的仓库(私有仓库不能使用非远程图片),然后准备上述的工作流,然后在 Github 账号设置 Personal access tokens
中添加一个 token , 勾选 repo
权限,复制到说说仓库 secrets and variables - action
中,名称为 PAT
。
发布说说
这一步需要的是开启一个issue,然后在这个issue里面不断发布评论来当做动态,然后就是把这个issue的链接如https://github.com/h2dcc/moments/issues/1,改为类似https://api.github.com/repos/microsoft/vscode/issues/519/comments,如果你要在前端展示,你需要一个密钥,要有repo
权限,你才能正常使用,否则会有较大的限制。关于这个,我觉得要在cloudflare里搞个worker然后再worker的环境变量里添加上面的密钥,大致worker代码如下:
// CF Worker 入口 export default { async fetch(req, env) { return await handle(req, env); } }; async function handle(req, env) { const url = new URL(req.url); // 只代理 /api/comments if (url.pathname !== '/api/comments') { return new Response('Not Found', { status: 404 }); } const upstream = 'https://api.github.com/repos/microsoft/vscode/issues/519/comments'; const res = await fetch(upstream, { headers: { 'Authorization': 'token ' + env.GH_TOKEN, // ✅ 正确读取环境变量 'User-Agent': 'CF-Worker-Giscus-Proxy' } }); const headers = new Headers(res.headers); headers.set('Access-Control-Allow-Origin', '*'); return new Response(res.body, { status: res.status, statusText: res.statusText, headers }); }
然后再搞个自定义域名,然后在后面加后缀/api/comments
,你就能比较不受限制的观看动态了,
前端
接下来就是我自己搞的一个html的简单前端,靠着AI完善了一下,可以参考参考:
<!DOCTYPE html> <html lang="zh-CN"> <head> <link rel="icon" type="image/png" href="https://img.314926.xyz/images/2025/09/20/zsx-avatar.webp " sizes="32x32"> <meta charset="UTF-8"> <title>钟神秀的瞬间</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> :root { --bg: #f5f5f5; --fg: #333333; --card: #ffffff; --link: #576b95; --border: #e1e1e1; --avatar-border: #f0f0f0; --time-color: #888888; --like-color: #ff2442; --comment-bg: #f7f7f7; --shadow: 0 1px 3px rgba(0, 0, 0, 0.1); --active-page-bg: #576b95; --active-page-fg: #ffffff; --action-btn-color: #7d7d7d; --divider-color: #f0f0f0; --header-image-height: 180px; --content-max-width: 600px; } [data-theme="dark"] { --bg: #1a1a1a; --fg: #e6e6e6; --card: #242424; --link: #7d9fd3; --border: #3a3a3a; --avatar-border: #3a3a3a; --time-color: #a0a0a0; --like-color: #ff5c7a; --comment-bg: #2d2d2d; --shadow: 0 1px 3px rgba(0, 0, 0, 0.3); --active-page-bg: #7d9fd3; --active-page-fg: #ffffff; --action-btn-color: #a0a0a0; --divider-color: #3a3a3a; --header-image-height: 200px; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; background: var(--bg); color: var(--fg); line-height: 1.6; transition: background .3s, color .3s; padding-bottom: 40px; } a { color: var(--link); text-decoration: none; } a:hover { text-decoration: underline; } nav { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: var(--card); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 100; box-shadow: var(--shadow); } .nav-left { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 18px; } .icon { width: 24px; height: 24px; fill: currentColor; } #theme-toggle { cursor: pointer; background: transparent; border: 1px solid var(--border); color: var(--fg); padding: 6px 12px; border-radius: 16px; font-size: 14px; display: flex; align-items: center; gap: 6px; } .header-image { width: 100%; max-width: var(--content-max-width); height: var(--header-image-height); background: linear-gradient(135deg, #6e8efb, #a777e3); position: relative; overflow: hidden; margin: 0 auto 15px; border-radius: 12px; border: 1px solid var(--border); } .header-image::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: url('https://img.314926.xyz/images/2025/09/22/20250922193025414.webp ') center/cover; opacity: 0.9; } .header-title { position: absolute; left: 20px; bottom: 20px; color: white; font-size: 24px; font-weight: bold; text-shadow: 0 2px 4px rgba(0,0,0,0.3); z-index: 2; } .header-info { position: absolute; right: 20px; bottom: 20px; color: white; z-index: 2; cursor: pointer; font-size: 20px; } @media (max-width: 640px) { .header-image { border-radius: 0; margin-bottom: 10px; } :root { --header-image-height: 160px; } [data-theme="dark"] { --header-image-height: 180px; } } .info-modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 1000; justify-content: center; align-items: center; } .info-content { background: var(--card); padding: 20px; border-radius: 12px; max-width: 80%; box-shadow: 0 4px 20px rgba(0,0,0,0.15); position: relative; } .close-modal { position: absolute; top: 10px; right: 15px; font-size: 24px; cursor: pointer; } main { max-width: var(--content-max-width); margin: 0 auto; padding: 0 10px; width: 100%; } .moment-article { background: var(--card); border-radius: 12px; padding: 0; margin-bottom: 15px; box-shadow: var(--shadow); border: 1px solid var(--border); overflow: hidden; } .article-header { display: flex; align-items: center; padding: 12px 15px; } .avatar { width: 40px; height: 40px; border-radius: 50%; margin-right: 12px; border: 2px solid var(--avatar-border); object-fit: cover; } .user-info { flex: 1; } .user-name { font-weight: 500; font-size: 16px; margin-bottom: 2px; } .post-time { font-size: 12px; color: var(--time-color); } .moment-content { padding: 0 15px 15px 15px; margin-left: 52px; margin-top: -10px; font-size: 15px; line-height: 1.5; } .moment-content p { margin-bottom: 10px; } .moment-content pre { background: var(--bg); padding: 12px; border-radius: 6px; overflow: auto; font-size: 14px; margin: 10px 0; } .moment-content blockquote { border-left: 3px solid var(--border); padding-left: 12px; margin: 10px 0; color: var(--fg); opacity: .8; font-size: 14px; background: var(--comment-bg); border-radius: 0 6px 6px 0; padding: 8px 12px; } .moment-content code { background-color: var(--bg); padding: 2px 4px; border-radius: 3px; font-size: 14px; } .moment-content img { max-width: 100%; border-radius: 6px; margin: 8px 0; } .error, .no-content { text-align: center; margin-top: 40px; font-size: 16px; color: var(--time-color); } .pagination { display: flex; justify-content: center; margin: 20px 0; gap: 8px; } .page-btn { padding: 6px 12px; border: 1px solid var(--border); background: var(--card); color: var(--fg); border-radius: 4px; cursor: pointer; font-size: 14px; transition: all 0.2s; } .page-btn:hover { background: var(--bg); } .page-btn.active { background: var(--active-page-bg); color: var(--active-page-fg); border-color: var(--active-page-bg); } .page-btn.disabled { opacity: 0.5; cursor: not-allowed; } .giscus-container { max-width: var(--content-max-width); margin: 30px auto 0 auto; padding: 0 10px; } .loading { display: flex; justify-content: center; padding: 20px; } .loading-spinner { width: 24px; height: 24px; border: 3px solid var(--border); border-top-color: var(--link); border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* 右下角编辑按钮 */ .edit-btn { position: fixed; right: 20px; bottom: 20px; width: 48px; height: 48px; border-radius: 50%; background: var(--card); color: var(--fg); border: 1px solid var(--border); box-shadow: var(--shadow); display: flex; align-items: center; justify-content: center; font-size: 20px; cursor: pointer; transition: all .3s; z-index: 999; } .edit-btn:hover { transform: scale(1.1); box-shadow: 0 4px 12px rgba(0,0,0,.15); } /* 手机端缩小一点 */ @media (max-width: 640px) { .edit-btn { width: 44px; height: 44px; font-size: 18px; right: 16px; bottom: 16px; } } </style> </head> <body> <nav> <div class="nav-left"> <svg class="icon" viewBox="0 0 16 16"> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/> </svg> <span>钟神秀的瞬间</span> </div> <button id="theme-toggle" aria-label="切换主题"> <span id="theme-icon">🌙</span> <span>切换主题</span> </button> </nav> <div class="header-image"> <div class="header-title">即刻短文</div> <div class="header-info" id="info-button">❗</div> </div> <div class="info-modal" id="info-modal"> <div class="info-content"> <div class="close-modal" id="close-modal">×</div> <h3>钟神秀的瞬间记录</h3> <p>这里收录了我的生活随笔、技术思考和灵感闪现。每一段文字都是时光的切片,记录当下的真实感受。</p> </div> </div> <main id="main-container"> <div class="loading"> <div class="loading-spinner"></div> </div> </main> <div id="pagination-container" style="display: none;"></div> <div class="giscus-container"></div> <a href="https://github.com/zsxsw/github-issues-moments/issues/1" target="_blank" class="edit-btn" title="添加/编辑说说">✏️</a> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js "></script> <script> /* ---------- 主题切换 + Giscus 重载 ---------- */ const toggle = document.getElementById('theme-toggle'); const themeIcon = document.getElementById('theme-icon'); const html = document.documentElement; function loadGiscus(theme) { const container = document.querySelector('.giscus-container'); container.innerHTML = ''; const script = document.createElement('script'); script.src = 'https://giscus.app/client.js '; script.async = true; script.setAttribute('crossorigin', 'anonymous'); script.setAttribute('data-repo', 'zsxsw/github-issues-moments'); script.setAttribute('data-repo-id', 'R_kgDOP0jWOA'); script.setAttribute('data-category', 'Announcements'); script.setAttribute('data-category-id', 'DIC_kwDOP0jWOM4Cvv6S'); script.setAttribute('data-mapping', 'pathname'); script.setAttribute('data-strict', '0'); script.setAttribute('data-reactions-enabled', '1'); script.setAttribute('data-emit-metadata', '0'); script.setAttribute('data-input-position', 'top'); script.setAttribute('data-lang', 'zh-CN'); script.setAttribute('data-theme', theme); container.appendChild(script); } (function initTheme() { const saved = localStorage.getItem('theme'); const preferDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const initialTheme = (saved === 'dark' || (!saved && preferDark)) ? 'dark' : 'light'; html.setAttribute('data-theme', initialTheme); themeIcon.textContent = initialTheme === 'dark' ? '☀️' : '🌙'; loadGiscus(initialTheme); })(); toggle.addEventListener('click', () => { const current = html.getAttribute('data-theme'); const next = current === 'dark' ? 'light' : 'dark'; html.setAttribute('data-theme', next); localStorage.setItem('theme', next); themeIcon.textContent = next === 'dark' ? '☀️' : '🌙'; loadGiscus(next); }); /* ---------- 信息模态框 ---------- */ const infoButton = document.getElementById('info-button'); const infoModal = document.getElementById('info-modal'); const closeModal = document.getElementById('close-modal'); infoButton.addEventListener('click', () => infoModal.style.display = 'flex'); closeModal.addEventListener('click', () => infoModal.style.display = 'none'); infoModal.addEventListener('click', e => { if (e.target === infoModal) infoModal.style.display = 'none'; }); /* ---------- 数据加载 & 分页 ---------- */ const container = document.getElementById('main-container'); const paginationContainer = document.getElementById('pagination-container'); const url = 'https://example.com/api/comments '; /* 替换为你的 API URL */ let allComments = []; let currentPage = 1; const itemsPerPage = 10; const headers = new Headers(); headers.append('Accept', 'application/vnd.github.v3+json'); headers.append('User-Agent', 'Hugo Static Site Generator'); fetch(url, { headers }) .then(r => { if (!r.ok) throw new Error('网络错误 ' + r.status); return r.json(); }) .then(list => { if (!Array.isArray(list) || list.length === 0) { container.innerHTML = '<p class="no-content">暂无动态</p>'; return; } allComments = list.reverse(); initPagination(allComments.length); displayPage(1); }) .catch(err => { container.innerHTML = `<p class="error">⚠️ 无法获取动态:${err.message}</p>`; }); function initPagination(totalItems) { const totalPages = Math.ceil(totalItems / itemsPerPage); if (totalPages <= 1) { paginationContainer.style.display = 'none'; return; } let html = ` <div class="pagination"> <button class="page-btn prev-btn" onclick="changePage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>上一页</button> `; const max = 5, half = Math.floor(max / 2); let start, end; if (totalPages <= max) { start = 1; end = totalPages; } else if (currentPage <= half + 1) { start = 1; end = max; } else if (currentPage >= totalPages - half) { start = totalPages - max + 1; end = totalPages; } else { start = currentPage - half; end = currentPage + half; } for (let i = start; i <= end; i++) { html += `<button class="page-btn ${i === currentPage ? 'active' : ''}" onclick="changePage(${i})">${i}</button>`; } html += `<button class="page-btn next-btn" onclick="changePage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}>下一页</button></div>`; paginationContainer.innerHTML = html; paginationContainer.style.display = 'block'; } function displayPage(page) { const start = (page - 1) * itemsPerPage, end = Math.min(start + itemsPerPage, allComments.length); const html = allComments.slice(start, end).map(c => ` <article class="moment-article"> <header class="article-header"> <img class="avatar" src="${c.user.avatar_url}" alt="${c.user.login}" onerror="this.src='https://avatars.githubusercontent.com/u/0?s=80&v=4 '"> <div class="user-info"> <div class="user-name">钟神秀@zsxsw</div> <div class="post-time">${formatTime(c.created_at)}</div> </div> </header> <section class="moment-content">${marked.parse(c.body)}</section> </article> `).join(''); container.innerHTML = `<div class="moments-feed">${html}</div>`; window.scrollTo({ top: 0, behavior: 'smooth' }); } window.changePage = function (page) { const total = Math.ceil(allComments.length / itemsPerPage); if (page < 1 || page > total || page === currentPage) return; currentPage = page; displayPage(page); initPagination(allComments.length); }; function formatTime(dateStr) { const date = new Date(dateStr), now = new Date(), diff = (now - date) / 1000; if (diff < 60) return '刚刚'; if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`; if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`; if (diff < 2592000) return `${Math.floor(diff / 86400)}天前`; return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }); } </script> </body> </html>
大致就是这样了,以上就是我肤浅的理解,希望能帮到你~
评论区
评论加载中...