|
| 1 | +--- |
| 2 | +title: "Hugo + PaperMod Blog Setup: From Zero to Comments Live — The Potholes I Hit" |
| 3 | +date: 2026-08-30T17:17:00+08:00 |
| 4 | +draft: false |
| 5 | +slug: hugo-blog-build-pitfalls |
| 6 | +description: "Real problems I hit while building this Hugo blog: the race condition that polluted my git history when parallel agents ran, and the four-hour debug of giscus's 'Unable to create discussion' — which turned out to be a repoId format mismatch." |
| 7 | +tags: |
| 8 | + - Hugo |
| 9 | + - PaperMod |
| 10 | + - Giscus |
| 11 | + - Debugging |
| 12 | +categories: |
| 13 | + - Essays |
| 14 | +series: |
| 15 | + - Blog Setup |
| 16 | +ShowToc: true |
| 17 | +TocOpen: true |
| 18 | +--- |
| 19 | + |
| 20 | +## Why this post |
| 21 | + |
| 22 | +In my previous post I said I wanted to start a blog to record what I learn. This one is about how I actually built the blog itself — and the real problems I hit along the way. |
| 23 | + |
| 24 | +The stack I picked is plain: **Hugo** (static site generator) + **PaperMod** (theme) + **GitHub Pages** (hosting) + **Giscus** (comments, backed by GitHub Discussions). |
| 25 | + |
| 26 | +In theory this is a 30-minute setup. In practice it took an entire afternoon plus an evening — most of it burned on a problem that looked like a config issue but was actually something deeper: Giscus kept reporting `Unable to create discussion` no matter what I changed. |
| 27 | + |
| 28 | +Here's the play-by-play. |
| 29 | + |
| 30 | +## 1. Sixteen features, eleven parallel agents |
| 31 | + |
| 32 | +After the first post went live, I lined up a long wishlist: |
| 33 | + |
| 34 | +- Series support (taxonomy + homepage cloud + prev/next nav) |
| 35 | +- Search box click bug |
| 36 | +- Homepage taxonomy cloud (categories + tags) |
| 37 | +- Custom shortcodes (alert / card / video) |
| 38 | +- Syntax highlighting (chroma CSS) |
| 39 | +- Code block copy buttons |
| 40 | +- Giscus comments |
| 41 | +- Mermaid diagrams + KaTeX math |
| 42 | +- Share buttons (Weibo + copy link) |
| 43 | +- Multi-language support (add English) |
| 44 | +- Rainbow-colored tags |
| 45 | +- Tech-feel icon glow |
| 46 | +- …and a few more |
| 47 | + |
| 48 | +That's 16 items. I dispatched **eleven agents in parallel**, each with a disjoint file scope, each told to commit + push when finished. |
| 49 | + |
| 50 | +### First pothole: race condition |
| 51 | + |
| 52 | +The biggest problem with running agents in parallel is that **their commits can interleave**. |
| 53 | + |
| 54 | +Say agent A and agent B both touch `hugo.yaml`. A commits first. B then cherry-picks A's work — but A's working tree has files that A *intended to commit later*. B's `git add` happily grabs those files too. |
| 55 | + |
| 56 | +The result: two commits whose messages don't match their actual diffs. Git history gets corrupted. |
| 57 | + |
| 58 | +{{< alert type="warning" >}} |
| 59 | +**Lesson**: when multiple agents share a working tree, either give each one its own git worktree, or enforce atomic "edit → `git add` → `git commit`" with no intermediate state. |
| 60 | +{{< /alert >}} |
| 61 | + |
| 62 | +In the end I used `git reset --hard` + cherry-picked all six commits back, rewriting the messages with a NOTE clarifying what each one really contained. After force-push, history was clean. |
| 63 | + |
| 64 | +## 2. Giscus "Unable to create discussion" — a 30-minute debug |
| 65 | + |
| 66 | +I picked Giscus for comments because: |
| 67 | + |
| 68 | +- No backend to run |
| 69 | +- Comments live in GitHub Discussions — **never get lost** |
| 70 | +- Comments are issues — searchable by Google |
| 71 | +- Open source, free |
| 72 | + |
| 73 | +Following Giscus's official steps: install the giscus app → enable Discussions → copy config — should take three minutes. |
| 74 | + |
| 75 | +After deploy, the comment widget kept showing: |
| 76 | + |
| 77 | +``` |
| 78 | +Unable to create discussion |
| 79 | +``` |
| 80 | + |
| 81 | +Opened Edge DevTools, found a sea of red: |
| 82 | + |
| 83 | +``` |
| 84 | +POST https://giscus.app/api/discussions 400 (Bad Request) |
| 85 | +onDiscussionCreateRequest @ widget-...js:1 |
| 86 | +``` |
| 87 | + |
| 88 | +### Diagnosis 1: Category permissions |
| 89 | + |
| 90 | +First instinct: is it a category permission issue? GitHub's default **Announcements** category is maintainer-only — regular users can't post. |
| 91 | + |
| 92 | +Switched from `announcements` to `general` (open to all logged-in GitHub users), pushed, redeployed, refreshed — **still 400**. |
| 93 | + |
| 94 | +### Diagnosis 2: Browser settings |
| 95 | + |
| 96 | +Second instinct: is Edge's Tracking Prevention blocking third-party cookies? |
| 97 | + |
| 98 | +Changed it to "Basic" level, restarted the browser — **still 400**. |
| 99 | + |
| 100 | +### Diagnosis 3: Giscus backend session |
| 101 | + |
| 102 | +Third instinct: does Giscus's backend not have my session? |
| 103 | + |
| 104 | +Hit the Giscus API directly with curl: |
| 105 | + |
| 106 | +```bash |
| 107 | +curl -X POST https://giscus.app/api/discussions \ |
| 108 | + -H "Content-Type: application/json" \ |
| 109 | + -d '{}' |
| 110 | +``` |
| 111 | + |
| 112 | +Got back: |
| 113 | + |
| 114 | +```json |
| 115 | +{ "error": "Invalid or missing access token." } |
| 116 | +``` |
| 117 | + |
| 118 | +But I'd already clicked "Sign in" in the browser. The session should be there. The issue isn't the session. |
| 119 | + |
| 120 | +### Diagnosis 4: Read the source |
| 121 | + |
| 122 | +I pulled Giscus's source from GitHub and read through it. Found the key code in `pages/api/discussions/index.ts`: |
| 123 | + |
| 124 | +```typescript |
| 125 | +async function post(req, res) { |
| 126 | + // 1. Validate user token — passed ✅ |
| 127 | + const userToken = req.headers.authorization?.split('Bearer ')[1]; |
| 128 | + if (!(await check(userToken))) { |
| 129 | + res.status(403).json({ error: 'Invalid or missing access token.' }); |
| 130 | + return; |
| 131 | + } |
| 132 | + |
| 133 | + // 2. Get giscus-app's installation token for this repo — passed ✅ |
| 134 | + let token: string; |
| 135 | + try { |
| 136 | + token = await getAppAccessToken(repo); |
| 137 | + } catch (error) { |
| 138 | + res.status(403).json({ error: error.message }); |
| 139 | + return; |
| 140 | + } |
| 141 | + |
| 142 | + // 3. Call GitHub GraphQL createDiscussion mutation |
| 143 | + const response = await createDiscussion(token, params); |
| 144 | + const id = response?.data?.createDiscussion?.discussion?.id; |
| 145 | + |
| 146 | + if (!id) { |
| 147 | + res.status(400).json({ error: 'Unable to create discussion with request body.' }); |
| 148 | + return; |
| 149 | + } |
| 150 | + |
| 151 | + res.status(200).json({ id }); |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +**Key finding**: the 400 comes from **step 3** — `createDiscussion` returns JSON with an empty `discussion.id`. |
| 156 | + |
| 157 | +In other words: **GitHub's GraphQL API is rejecting the mutation**. |
| 158 | + |
| 159 | +### Diagnosis 5: repoId format |
| 160 | + |
| 161 | +Queried my repo's node ID directly via GraphQL: |
| 162 | + |
| 163 | +```bash |
| 164 | +gh api graphql -F query=' |
| 165 | +query { |
| 166 | + repository(owner:"LiMingCoding", name:"LiMingCoding.github.io") { |
| 167 | + id |
| 168 | + } |
| 169 | +}' |
| 170 | +``` |
| 171 | + |
| 172 | +Got back: |
| 173 | + |
| 174 | +```json |
| 175 | +{ "data": { "repository": { "id": "MDEwOlJlcG9zaXRvcnkzODczMjk0MDY=" } } } |
| 176 | +``` |
| 177 | + |
| 178 | +But what I had in my config was: |
| 179 | + |
| 180 | +```yaml |
| 181 | +giscus: |
| 182 | + repoId: "387329406" # ❌ wrong! |
| 183 | +``` |
| 184 | +
|
| 185 | +**The actual root cause**: |
| 186 | +
|
| 187 | +| ID format | Use | |
| 188 | +|---|---| |
| 189 | +| `387329406` | REST API numeric database ID | |
| 190 | +| `MDEwOlJlcG9zaXRvcnkzODczMjk0MDY=` | GraphQL node ID (base64) | |
| 191 | + |
| 192 | +Giscus's "Configure" page **gave me the REST numeric ID**, but the backend calls **GraphQL mutation** — which needs the node ID format. |
| 193 | + |
| 194 | +After fixing it, pushing, deploying, and refreshing — the comment went through 🎉 |
| 195 | + |
| 196 | +{{< alert type="success" >}} |
| 197 | +**Lesson**: the `repoId` on giscus.app's config page is buggy — it displays the numeric format but the backend wants base64. **Look it up yourself via `gh api graphql`** — that's the reliable way. |
| 198 | +{{< /alert >}} |
| 199 | + |
| 200 | +## 3. A few smaller potholes |
| 201 | + |
| 202 | +| Problem | Fix | |
| 203 | +|---|---| |
| 204 | +| GitHub Discussions not enabled | Repo Settings → General → Features → check Discussions | |
| 205 | +| Giscus app not installed or wrong perms | github.com/apps/giscus → Install → pick repo → check Read+Write | |
| 206 | +| Edge's default Tracking Prevention blocking | edge://settings/privacy → switch to Basic | |
| 207 | +| PaperMod version too old for `series` taxonomy | PaperMod ≥ some-recent-version supports it | |
| 208 | + |
| 209 | +## Retrospective |
| 210 | + |
| 211 | +1. **Race conditions with parallel agents can't be fully avoided** — unless you use git worktrees or stricter protocols. Next time I dispatch agents, I'll write "commit atomicity" into the prompt explicitly. |
| 212 | + |
| 213 | +2. **Debugging third-party SaaS bugs** — services like Giscus that wrap GitHub's API hide their stack traces. Be willing to **read their source code** directly — it's faster than guessing. |
| 214 | + |
| 215 | +3. **GraphQL and REST have two different ID systems** — for any future GitHub API call, confirm which one you're using and what ID format it needs. |
| 216 | + |
| 217 | +## Next steps |
| 218 | + |
| 219 | +- Full-text RSS output |
| 220 | +- Self-host a lightweight comment mirror (in case Giscus ever shuts down) |
| 221 | +- Flesh out the rest of the series posts (the opening one is too short 😅) |
0 commit comments