banner
克喵爱吃卤面

喵落阁

光就是一切的意义!
telegram
github
qq
email
x
bilibili

About the idea of using GitHub Issues as a dynamic tool

Originating from browsing different blogs, I came across an article that seemed very promising, and then I visited the blogger's GitHub repository, which gave me some insights.

Advantages#

Many blogs are based on GitHub issues, including etheral, Gmeek, and so on. Of course, besides being a blog, you can also use it to create a blog.

A very good way to write a blog; theoretically, as long as GitHub is operational, this method can be used indefinitely.

There is a GitHub app on mobile, which allows you to easily post updates from your phone.

This method can be used in various blogs, including Hugo, Astro, etc.

The general workflow is as follows:

// .github/workflows/issue.yml

name: Trigger Empty Commit on Issue Update

on:
  issue_comment:
    types: [created, edited]
  workflow_dispatch:  # Manual trigger entry

jobs:
  trigger-empty-commit:
    runs-on: ubuntu-latest
    steps:
      - name: Check trigger type and prepare commit message
        id: check-trigger
        run: |
          # Handle manual trigger
          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
          # Handle issue comment event
          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;
      
            // Get commit message from step output
            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' });

              // Safely execute empty commit
              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);
            }

What you need is to create a public repository (private repositories cannot use non-remote images), then prepare the above workflow, and add a token in the Personal access tokens section of your GitHub account settings, check the repo permission, and copy it to the secrets of the repository under secrets and variables - action, named PAT.

Publish Updates#

This step requires opening an issue and continuously posting comments in this issue as updates, and then changing the link of this issue from https://github.com/h2dcc/moments/issues/1 to something like https://api.github.com/repos/microsoft/vscode/issues/519/comments. If you want to display it on the front end, you need a key with repo permission to use it normally; otherwise, there will be significant restrictions. Regarding this, I think you need to set up a worker in Cloudflare and add the above key in the worker's environment variables. The general worker code is as follows:

// CF Worker entry
export default {
  async fetch(req, env) {
    return await handle(req, env);
  }
};

async function handle(req, env) {
  const url = new URL(req.url);

  // Only proxy /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, // ✅ Correctly read environment variable
      '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
  });
}

Then set up a custom domain and add the suffix /api/comments at the end, and you will be able to view updates with fewer restrictions.

Frontend#

Next is a simple frontend that I created, which was improved with AI, you can refer to it:

<!DOCTYPE html>
<html lang="en">
<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>Moments of Zhong Shenxiu</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); }
        }
        /* Bottom right edit button */
        .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);
        }
        /* Smaller on mobile */
        @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>Moments of Zhong Shenxiu</span>
        </div>
        <button id="theme-toggle" aria-label="Toggle theme">
            <span id="theme-icon">🌙</span>
            <span>Toggle theme</span>
        </button>
    </nav>

    <div class="header-image">
        <div class="header-title">Instant Short Text</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>Records of Zhong Shenxiu's Moments</h3>
            <p>This includes my life essays, technical thoughts, and flashes of inspiration. Each piece of text is a slice of time, capturing the real feelings of the moment.</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="Add/Edit Updates">✏️</a>

    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js "></script>
    <script>
        /* ---------- Theme toggle + Giscus reload ---------- */
        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', 'en');
            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);
        });

        /* ---------- Info modal ---------- */
        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';
        });

        /* ---------- Data loading & pagination ---------- */
        const container = document.getElementById('main-container');
        const paginationContainer = document.getElementById('pagination-container');
        const url = 'https://example.com/api/comments '; /* Replace with your 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('Network error ' + r.status);
                return r.json();
            })
            .then(list => {
                if (!Array.isArray(list) || list.length === 0) {
                    container.innerHTML = '<p class="no-content">No updates available</p>';
                    return;
                }
                allComments = list.reverse();
                initPagination(allComments.length);
                displayPage(1);
            })
            .catch(err => {
                container.innerHTML = `<p class="error">⚠️ Unable to fetch updates: ${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' : ''}>Previous</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' : ''}>Next</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">Zhong Shenxiu@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 'Just now';
            if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
            if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
            if (diff < 2592000) return `${Math.floor(diff / 86400)} days ago`;
            return date.toLocaleString('en', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false });
        }
    </script>
</body>
</html>

That's about it; the above is my shallow understanding, and I hope it can help you~

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.