
关于github-issues当做动态的想法
优点
有不少的博客基于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>
大致就是这样了,以上就是我肤浅的理解,希望能帮到你~
- 感谢你赐予我前进的力量
赞赏者名单
因为你们的支持让我意识到写文章的价值🙏
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 克喵爱吃卤面
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果