Featured image of post How to Publish a Hugo Blog Using GitHub Issues

How to Publish a Hugo Blog Using GitHub Issues

In recent years, various methods for publishing static blogs have emerged, but many are either complex or difficult to maintain long-term. This article introduces a simple and efficient approach: using GitHub Issues as the publishing endpoint for a Hugo blog, leveraging GitHub Actions to automatically convert Issues into Hugo content and deploy to GitHub Pages. This method is particularly suitable for users who prefer using the GitHub mobile app to publish blog posts anytime, anywhere. Based on Lao T’s practical experience, this tutorial addresses issues such as downloading images from private repositories and extracting tags, and is suitable for both public and private repositories.

Why Choose GitHub Issues?

  • Convenience: The GitHub mobile app is more stable than the web version in domestic network environments, and publishing requires just a few clicks—simpler than posting on WeChat Moments.
  • No Limits: Issues have virtually no quantity or capacity restrictions, making them ideal for storing blog content.
  • Automation: Through GitHub Actions, Issues can be automatically converted into Hugo content and trigger site builds.
  • Flexibility: Supports images, tags, and categories, suitable for short “thoughts” or long articles.

Prerequisites

  • A Hugo blog project already configured with GitHub Pages (public or private repository).
  • A GitHub Personal Access Token (PAT) with repo permissions (required for private repositories) and workflow permissions (to trigger workflows). Create it in GitHub SettingsDeveloper settingsPersonal access tokens.
  • Basic knowledge of GitHub Actions and Hugo.
  • A repository structure that includes a content/posts/ directory for storing generated articles.

Implementation Steps

1. Configure GitHub Actions Workflows

We need two workflow files:

  • deploy.yml: Converts Issues into Hugo content and commits it to the repository.
  • hugo.yml: Builds the Hugo site and deploys it to GitHub Pages.

1.1 Create deploy.yml

Add the following content to .github/workflows/deploy.yml to handle converting Issues into Hugo content and triggering the Hugo build:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
name: Sync Issues to Hugo Content

on:
  issues:
    types: [opened]
  workflow_dispatch:

permissions:
  contents: write

concurrency:
  group: "issues-to-hugo"
  cancel-in-progress: false

defaults:
  run:
    shell: bash

jobs:
  convert_issues:
    name: Convert GitHub Issues
    runs-on: ubuntu-latest
    if: github.event_name == 'workflow_dispatch' || github.event_name == 'issues' && contains(github.event.issue.labels.*.name, '发布') && github.actor != 'IssueBot'
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          clean: true
          
      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      
      - name: Install dependencies
        run: |
          pip install requests PyGithub==2.4.0 beautifulsoup4

      - name: Create content/posts directory
        run: |
          mkdir -p content/posts
          echo "Created content/posts directory"

      - name: Convert issues to Hugo content
        id: convert
        env:
          GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
        run: |
          find content/posts -type f 2>/dev/null | sort > /tmp/original_files.txt || touch /tmp/original_files.txt
          if [ -s /tmp/original_files.txt ]; then
            xargs sha1sum < /tmp/original_files.txt > /tmp/original_hashes.txt
          else
            touch /tmp/original_hashes.txt
          fi
          
          python .github/workflows/issue_to_hugo.py \
            --repo "${{ github.repository }}" \
            --output "content/posts" \
            --debug
          
          find content/posts -type f 2>/dev/null | sort > /tmp/new_files.txt || touch /tmp/new_files.txt
          if [ -s /tmp/new_files.txt ]; then
            xargs sha1sum < /tmp/new_files.txt > /tmp/new_hashes.txt
          else
            touch /tmp/new_hashes.txt
          fi
          
          if cmp -s /tmp/original_hashes.txt /tmp/new_hashes.txt; then
            echo "needs_build=false" >> $GITHUB_OUTPUT
            echo "🔄 No content changes, skipping commit"
          else
            echo "needs_build=true" >> $GITHUB_OUTPUT
            echo "✅ Content changes detected, proceeding with commit"
          fi
          
      - name: Commit changes (if any)
        if: steps.convert.outputs.needs_build == 'true'
        env:
          GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
        run: |
          git config --global user.name "IssueBot"
          git config --global user.email "actions@users.noreply.github.com"
          git remote set-url origin https://x-access-token:${{ secrets.PAT_TOKEN }}@github.com/${{ github.repository }}.git
          
          if [ -n "${{ github.base_ref }}" ]; then
            DEFAULT_BRANCH="${{ github.base_ref }}"
          else
            DEFAULT_BRANCH=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
          fi
          
          if [ -z "$DEFAULT_BRANCH" ]; then
            DEFAULT_BRANCH="master"
          fi
          
          git checkout $DEFAULT_BRANCH
          
          git add content/posts
          
          if ! git diff-index --quiet HEAD --; then
            git commit -m "Automated: Sync GitHub Issues as content"
            echo "Pushing changes to branch: $DEFAULT_BRANCH"
            git push origin $DEFAULT_BRANCH
          else
            echo "No changes to commit"
          fi

      - name: Trigger Hugo build
        if: steps.convert.outputs.needs_build == 'true'
        env:
          GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
        run: |
          curl -X POST \
            -H "Accept: application/vnd.github.v3+json" \
            -H "Authorization: Bearer ${{ secrets.PAT_TOKEN }}" \
            https://api.github.com/repos/${{ github.repository }}/dispatches \
            -d '{"event_type": "trigger-hugo-build"}'

Key Points:

  • Trigger Conditions: When an Issue is opened with the “发布” label, or manually triggered.
  • Prevent Recursion: github.actor != 'IssueBot' ensures that commits by IssueBot do not trigger the workflow again.
  • Change Detection: Commits only occur when content changes (via file hash comparison).
  • Trigger Hugo Build: Calls the Hugo workflow via the repository_dispatch event (trigger-hugo-build).

1.2 Create hugo.yml

Add the following content to .github/workflows/hugo.yml to build and deploy the Hugo site:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
name: Deploy Hugo site to Pages

on:
  push:
    branches: ["master"]
  workflow_dispatch:
  repository_dispatch:
    types: [trigger-hugo-build]

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: false

defaults:
  run:
    shell: bash

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      HUGO_VERSION: 0.128.0
    steps:
      - name: Install Hugo CLI
        run: |
          wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
          && sudo dpkg -i ${{ runner.temp }}/hugo.deb
      - name: Install Dart Sass
``````yaml
run: sudo snap install dart-sass
- name: Checkout
  uses: actions/checkout@v4
  with:
    submodules: recursive
- name: Setup Pages
  id: pages
  uses: actions/configure-pages@v5
- name: Install Node.js dependencies
  run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true"
- name: Build with Hugo
  env:
    HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache
    HUGO_ENVIRONMENT: production
  run: |
    hugo \
      --minify \
      --baseURL "${{ steps.pages.outputs.base_url }}/"
- name: Upload artifact
  uses: actions/upload-pages-artifact@v3
  with:
    path: ./public
deploy:
  environment:
    name: github-pages
    url: ${{ steps.deployment.outputs.page_url }}
  runs-on: ubuntu-latest
  needs: build
  steps:
    - name: Deploy to GitHub Pages
      id: deployment
      uses: actions/deploy-pages@v4

Key Points:

  • Trigger conditions: push to the master branch, manual triggers, or repository_dispatch events (trigger-hugo-build).
  • Permissions: Ensure pages: write and id-token: write for GitHub Pages deployment.

1.3 Add Conversion Script issue_to_hugo.py

Add the following Python script in .github/workflows/issue_to_hugo.py to convert Issues into Hugo content, supporting private repository image downloads and tag extraction:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
import os
import argparse
import re
import requests
import json
import logging
from urllib.parse import unquote
from datetime import datetime
from github import Github, Auth
from bs4 import BeautifulSoup

CATEGORY_MAP = ["Life", "Technology", "Legal", "Moments", "Society"]
PUBLISH_LABEL = "Publish"

def setup_logger(debug=False):
    logger = logging.getLogger('issue-to-hugo')
    level = logging.DEBUG if debug else logging.INFO
    logger.setLevel(level)
    
    handler = logging.StreamHandler()
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    
    return logger

def extract_cover_image(body):
    """Extract and remove the cover image (first image in the body)"""
    img_pattern = r"!\[.*?\]\((https?:\/\/[^\)]+)\)"
    match = re.search(img_pattern, body)
    
    if match:
        img_url = match.group(1)
        body = body.replace(match.group(0), "")
        return img_url, body
    return None, body

def safe_filename(filename):
    """Generate a safe filename, preserving or inferring the file extension"""
    clean_url = re.sub(r"\?.*$", "", filename)
    basename = os.path.basename(clean_url)
    decoded_name = unquote(basename)
    
    name, ext = os.path.splitext(decoded_name)
    safe_name = re.sub(r"[^a-zA-Z0-9\-_]", "_", name)
    if not ext.lower() in [".jpg", ".jpeg", ".png", ".gif", ".webp"]:
        ext = ""
    if len(safe_name) > 100 - len(ext):
        safe_name = safe_name[:100 - len(ext)]
    
    return safe_name + ext

def download_image(url, output_path, token=None):
    """Download image to the specified path, determine extension based on content type, and add GitHub authentication headers"""
    try:
        headers = {}
        if token:
            headers['Authorization'] = f'token {token}'
        
        response = requests.get(url, stream=True, headers=headers)
        if response.status_code == 200:
            content_type = response.headers.get("content-type", "").lower()
            ext = ".jpg"
            if "image/png" in content_type:
                ext = ".png"
            elif "image/jpeg" in content_type or "image/jpg" in content_type:
                ext = ".jpg"
            elif "image/gif" in content_type:
                ext = ".gif"
            elif "image/webp" in content_type:
                ext = ".webp"
            
            base, current_ext = os.path.splitext(output_path)
            if current_ext.lower() not in [".jpg", ".jpeg", ".png", ".gif", ".webp"]:
                output_path = base + ext
            else:
                output_path = base + current_ext
            
            with open(output_path, 'wb') as f:
                f.write(response.content)
            logging.info(f"Image downloaded successfully: {url} -> {output_path}")
            return output_path
        else:
            logging.error(f"Image download failed, status code: {response.status_code}, URL: {url}")
    except Exception as e:
        logging.error(f"Failed to download image: {url} - {e}")
    return None

def replace_image_urls(body, issue_number, output_dir, token=None):
    """Replace remote images in the body with local images"""
    img_pattern = r"!\[(.*?)\]\((https?:\/\/[^\)]+)\)"
    
    def replacer(match):
        alt_text = match.group(1)
        img_url = match.group(2)
        filename = f"{issue_number}_{safe_filename(img_url)}"
        output_path = os.path.join(output_dir, filename)
        final_path = download_image(img_url, output_path, token)
        if final_path:
            final_filename = os.path.basename(final_path)
            return f"![{alt_text}]({final_filename})"
        return match.group(0)
    
    return re.sub(img_pattern, replacer, body, flags=re.IGNORECASE)

def sanitize_markdown(content):
    """Sanitize unsafe content in Markdown"""
    if not content:
        return ""
    
    soup = BeautifulSoup(content, "html.parser")
    allowed_tags = ["p", "a", "code", "pre", "blockquote", "ul", "ol", "li", "strong", "em", "img", "h1", "h2", "h3", "h4", "h5", "h6"]
    for tag in soup.find_all(True):
        if tag.name not in allowed_tags:
            tag.unwrap()
    
    return str(soup)

def extract_tags_from_body(body, logger):
    """Extract tags from the last line of the body using $tag$ format and return the cleaned body"""
    if not body:
        logger.debug("Body is empty, no tags to extract")
        return [], body
    
    body = body.replace('\r\n', '\n').rstrip()
    lines = body.split('\n')
    if not lines:
        logger.debug("No lines in body, no tags to extract")
        return [], body
    
    last_line = lines[-1].strip()
    logger.debug(f"Last line for tag extraction: '{last_line}'")
    
    tags = re.findall(r'\$(.+?)\$', last_line, re.UNICODE)
    tags = [tag.strip() for tag in tags if tag.strip()]
    
    if tags:
        logger.debug(f"Extracted tags: {tags}")
        body = '\n'.join(lines[:-1]).rstrip()
    else:
        logger.debug("No tags found in last li
```")
    
    return tags, body

def convert_issue(issue, output_dir, token, logger):
    """Convert a single issue to Hugo content"""
    try:
        labels = [label.name for label in issue.labels]
        if PUBLISH_LABEL not in labels or issue.state != "open":
            logger.debug(f"Skipping issue #{issue.number} - not marked for publishing")
            return False
        
        pub_date = issue.created_at.strftime("%Y%m%d")
        slug = f"{pub_date}_{issue.number}"
        post_dir = os.path.join(output_dir, slug)
        
        if os.path.exists(post_dir):
            logger.info(f"Skipping issue #{issue.number} - directory {post_dir} already exists")
            return False
        
        os.makedirs(post_dir, exist_ok=True)
        
        body = issue.body or ""
        logger.debug(f"Raw issue body: '{body}'")
        cover_url, body = extract_cover_image(body)
        tags, body = extract_tags_from_body(body, logger)
        body = sanitize_markdown(body)
        body = replace_image_urls(body, issue.number, post_dir, token)
        logger.info(f"Image processing completed for issue #{issue.number}")
        
        categories = [tag for tag in labels if tag in CATEGORY_MAP]
        category = categories[0] if categories else "Life"
        
        cover_name = None
        if cover_url:
            try:
                cover_filename = f"cover_{safe_filename(cover_url)}"
                cover_path = os.path.join(post_dir, cover_filename)
                final_cover_path = download_image(cover_url, cover_path, token)
                if final_cover_path:
                    cover_name = os.path.basename(final_cover_path)
                    logger.info(f"Cover image downloaded successfully: {cover_url} > {cover_name}")
                else:
                    logger.error(f"Failed to download cover image: {cover_url}")
            except Exception as e:
                logger.error(f"Failed to download cover image: {cover_url} - {e}")
        
        title_escaped = issue.title.replace('"', '\\"')
        category_escaped = category.replace('"', '\\"')
        frontmatter_lines = [
            "---",
            f'title: "{title_escaped}"',
            f"date: \"{issue.created_at.strftime('%Y-%m-%d')}\"",
            f"slug: \"{slug}\"",
            f"categories: [\"{category_escaped}\"]",
            f"tags: {json.dumps(tags, ensure_ascii=False)}"
        ]
        
        if cover_name:
            frontmatter_lines.append(f"image: \"{cover_name}\"")
        
        frontmatter_lines.append("---\n")
        frontmatter = "\n".join(frontmatter_lines)
        
        md_file = os.path.join(post_dir, "index.md")
        with open(md_file, "w", encoding="utf-8") as f:
            f.write(frontmatter + body)
        
        logger.info(f"Successfully converted issue #{issue.number} to {md_file}")
        return True
    except Exception as e:
        logger.exception(f"Critical error occurred while converting issue #{issue.number}")
        error_file = os.path.join(output_dir, f"ERROR_{issue.number}.tmp")
        with open(error_file, "w") as f:
            f.write(f"Conversion failed: {str(e)}")
        return False

def main():
    args = parse_arguments()
    logger = setup_logger(args.debug)
    
    token = args.token or os.getenv("GITHUB_TOKEN")
    if not token:
        logger.error("Missing GitHub token")
        return
    
    try:
        auth = Auth.Token(token)
        g = Github(auth=auth)
        repo = g.get_repo(args.repo)
        logger.info(f"Connected to GitHub repository: {args.repo}")
    except Exception as e:
        logger.error(f"Failed to connect to GitHub: {str(e)}")
        return
    
    os.makedirs(args.output, exist_ok=True)
    logger.info(f"Output directory: {os.path.abspath(args.output)}")
    
    processed_count = 0
    error_count = 0
    
    try:
        issues = repo.get_issues(state="open")
        total_issues = issues.totalCount
        logger.info(f"Starting to process {total_issues} open issues")
        
        for issue in issues:
            if issue.pull_request:
                continue
            try:
                if convert_issue(issue, args.output, token, logger):
                    processed_count += 1
            except Exception as e:
                error_count += 1
                logger.error(f"Error processing issue #{issue.number}: {str(e)}")
                try:
                    error_comment = f"⚠️ Failed to convert to Hugo content, please check format errors:\n\n```\n{str(e)}\n```"
                    if len(error_comment) > 65536:
                        error_comment = error_comment[:65000] + "\n```\n...(content too long, partially omitted)"
                    
                    issue.create_comment(error_comment)
                    try:
                        error_label = repo.get_label("conversion-error")
                    except:
                        error_label = repo.create_label("conversion-error", "ff0000")
                    issue.add_to_labels(error_label)
                except Exception as inner_e:
                    logger.error(f"Error creating comment or adding label: {inner_e}")
    except Exception as e:
        logger.exception(f"Error fetching issues: {e}")
        
    summary = f"Processing completed! Successfully converted {processed_count} issues, {error_count} errors"
    if processed_count == 0:
        logger.info(summary + " - No content changes to process")
    else:
        logger.info(summary)
        
    if args.debug:
        logger.debug("Content directory status:")
        logger.debug(os.listdir(args.output))

def parse_arguments():
    parser = argparse.ArgumentParser(description='Convert GitHub issues to Hugo content')
    parser.add_argument('--token', type=str, default=None, help='GitHub access token')
    parser.add_argument('--repo', type=str, required=True, help='GitHub repository in format owner/repo')
    parser.add_argument('--output', type=str, default='content/posts', help='Output directory')
    parser.add_argument('--debug', action='store_true', help='Enable debug logging')
    return parser.parse_args()

if __name__ == "__main__":
    main()

Key Points:

  • Image Download: download_image uses PAT authentication header (Authorization: token {token}), supporting private repository attachment downloads.
  • Tag Extraction: Extracts $tag$ format tags from the last line of the Issue body (supports spaces, e.g., $what's up$).
  • Duplicate Check: Skips existing article directories (YYYYMMDD_X).
  • Markdown Generation: Generates standard Hugo frontmatter andContent, images converted to local paths.

2. Setting Up PAT

  1. Create PAT:

    • Go to GitHub SettingsDeveloper settingsPersonal access tokensTokens (classic).
    • Create a new PAT, check the boxes for repo (including access to private repositories) and workflow (to trigger workflows).
    • Copy the generated token.
  2. Add to Repository:

    • Navigate to the repository (e.g., h2dcc/lawtee.github.io) → SettingsSecrets and variablesActionsSecrets.
    • Add a new secret named PAT_TOKEN and paste the PAT value.

3. Writing an Issue to Publish a Blog Post

Compose your blog post in a GitHub Issue using the following format:

  1. Add a “Publish” Label:
    • Create a new Issue and add the 发布 label along with category labels (e.g., 技术, 生活).
  2. Body Format:
    • Title: The Issue title serves as the blog post title.
    • Cover Image: The first image in the body acts as the cover (in Markdown: ![Image](URL)).
    • Body: Written in Markdown format, supporting images, headings, links, etc.
    • Tags: Add tags in the last line using the $tag$ format (spaces are allowed, e.g., $Hugo Post$).
  3. Example Issue:
    1
    2
    3
    4
    5
    6
    7
    
    ![Image](https://github.com/user-attachments/assets/5cc86d74-ff70-401f-820d-520a99a504b9)
    ## My First Hugo Blog Post
    This is a blog post published via a GitHub Issue.
    Supports **Markdown** format, and images will be automatically downloaded locally.
    <!--more-->
    ![Another Image](https://github.com/user-attachments/assets/926e7e9b-d279-4db9-bbb9-60bdcedd1804)
    $Hugo$ $博客$ $Hugo Post$
    
  4. Submit: Save the Issue to trigger the deploy.yml workflow.

4. Workflow Execution Process

  1. Trigger deploy.yml:

    • When an Issue is opened (with the “发布” label) or triggered manually.
    • The script issue_to_hugo.py runs:
      • Extracts the title, categories (from labels), body, cover image, and tags.
      • Downloads images (using PAT authentication for private repositories).
      • Generates a Markdown file at content/posts/YYYYMMDD_X/index.md.
      • Commits to the master branch.
    • Triggers hugo.yml via a repository_dispatch event.
  2. Trigger hugo.yml:

    • Builds the Hugo site (hugo --minify).
    • Deploys to GitHub Pages.

5. Verifying Publication

  1. Check Actions Logs:

    • Open the GitHub Actions tab.
    • Confirm Sync Issues to Hugo Content runs:
      • Logs show 图片下载成功 (Images downloaded successfully) and 成功转换 issue #X (Successfully converted issue #X).
      • Commits to master (e.g., Automated: Sync GitHub Issues as content).
    • Confirm Deploy Hugo site to Pages runs:
      • Check the Build with Hugo and Deploy to GitHub Pages steps.
  2. Check Generated Files:

    • Open content/posts/YYYYMMDD_X/:
      • index.md contains frontmatter (title, date, slug, categories, tags, image) and the body.
      • Local image files exist (e.g., X_uuid.jpg).
    • Example index.md:
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      
      ---
      title: "My First Hugo Blog Post"
      date: "2025-10-27"
      slug: "20251027_2"
      categories: ["技术"]
      tags: ["Hugo", "博客", "Hugo Post"]
      image: "cover_5cc86d74-ff70-401f-820d-520a99a504b9.jpg"
      ---
      ## My First Hugo Blog Post
      This is a blog post published via a GitHub Issue.
      Supports **Markdown** format, and images are automatically downloaded locally.
      <!--more-->
      ![Another Image](2_926e7e9b-d279-4db9-bbb9-60bdcedd1804.jpg)
      
  3. Visit the Site:

    • Go to the GitHub Pages site (e.g., https://h2dcc.github.io or the Pages URL for private repositories).
    • Confirm the new post appears, images load correctly, and tags and categories are accurate.

6. Common Issues and Solutions

  • Image Download Fails (404):
    • Cause: Private repository images require PAT authentication.
    • Solution: Ensure PAT_TOKEN has repo permissions and the script includes authentication headers.
  • Hugo Build Not Triggered:
    • Cause: repository_dispatch event failure.
    • Solution: Check the Trigger Hugo build step logs in deploy.yml to confirm the curl request returns 204.
  • Tags Not Extracted:
    • Cause: Incorrect tag format or not placed in the last line.
    • Solution: Ensure tags are in the $tag$ format on the last line, with spaces included within $ (e.g., $搞啥 呢$).
  • Duplicate Posts:
    • Cause: The script skips existing directories (YYYYMMDD_X).
    • Solution: Delete content/posts/YYYYMMDD_X/ and rerun the process.

7. Optimization Suggestions

  • Single Issue Handling: Modify deploy.yml and issue_to_hugo.py to process only the triggering Issue, reducing runtime.
  • Error Notifications: Add comments to the Issue to notify users of conversion failures.
  • Mobile Optimization: Use the GitHub mobile app + Shortcut Maker to simplify the publishing process.
  • Log Inspection: Enable --debug mode to check detailed logs.

Summary

Publishing Hugo blog posts via GitHub Issues enables a streamlined workflow from quick mobile publishing to automatic deployment. Compared to traditional methods, this approach is simple, efficient, and ideal for bloggers who need to capture ideas on the go. Whether for public or private repositories, with PAT authentication and GitHub Actions, images, tags, and categories are handled seamlessly. Give it a try!

Built with Hugo, Powered by Github.
Total Posts: 371, Total Words: 516882.