Self-Hosted Hugo Comments with Private Gitea

Most static sites rely on third-party comment systems. While convenient, they introduce external dependencies, tracking, and long-term uncertainty.

Instead, I implemented a self-hosted comment system for Hugo backed by a private Gitea instance. The result is lightweight, secure, and fully under my control.


Architecture Overview

flowchart LR A[Browser] --> B[Nginx Reverse Proxy] B --> C[Flask Bridge Service] C --> D[Private Gitea API] D --> E[Repository Issues]

Key points:

  • Hugo renders static content
  • Only /comments/ is proxied to the bridge
  • Gitea remains on a private VLAN
  • The public site never communicates directly with Gitea

How It Works

Each post slug maps to a Gitea issue.

If the issue does not exist, the bridge creates one automatically via the Gitea API:

1
2
3
4
5
requests.post(
    f"{GITEA_URL}/repos/{OWNER}/{REPO}/issues",
    headers=HEADERS,
    json={"title": slug}
)

New comments are appended to that issue. Gitea effectively becomes the comment database — without introducing a separate database stack.


The Bridge Service

A small Flask application handles:

  • GET /comments/<slug> → fetch comments
  • POST /comments/<slug> → create comment

It runs in Docker and communicates with Gitea using a scoped API token. No credentials are ever exposed client-side.


Nginx Isolation

Only comment traffic is proxied:

1
2
3
location ^~ /comments/ {
    proxy_pass http://bridge-service:8080;
}

Everything else remains static Hugo output, keeping the attack surface minimal.


Front-End Integration

A Hugo shortcode renders the comment UI and posts data using fetch():

1
2
3
4
5
6
7
await fetch(`/comments/${slug}`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    body: `**${author}**:\n\n${comment}`
  })
});

The layout injects the comment block automatically, so comments appear on all posts without modifying each Markdown file.


Security Model

The system is designed with containment in mind:

  • Gitea runs on an internal VLAN
  • No public repository exposure
  • Limited-scope API token
  • Reverse proxy isolation
  • No external SaaS dependencies

Moderation happens directly inside Gitea by editing or deleting issue comments.


Why This Approach Works

This setup avoids:

  • Third-party tracking
  • External comment providers
  • Database management overhead
  • Over-engineering

It reuses infrastructure already present in a homelab:

  • Git
  • Issues
  • Docker
  • Nginx

Static where possible.
Dynamic only where necessary.
Private by default.


Comments