優點#
有不少的博客基於 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>
大致就是這樣了,以上就是我膚淺的理解,希望能幫到你~