Gitmentario is a comment service for websites built with Static Site Generators (SSG). Comments are pushed as Markdown resources into your website’s Git repository.
Gitmentario currently only supports Hugo as SSG and Gitlab as software forge. At least Jekyll and Github are planned to be supported, too.
- Comments are true resources of your website
- No vendor lock-in: you don’t lose your comments when switching approaches
- Comment moderation via merge requests
- Minimal JavaScript footprint
- A visitor writes a comment on your website
- A small JavaScript snippet sends the data to Gitmentario
- Gitmentario creates a Markdown file from the comment
- The service either:
- pushes the Markdown file directly to the default branch (
GIT_PUSH=true) - creates a new branch with the file and opens a merge request (
GIT_PUSH=false)
- pushes the Markdown file directly to the default branch (
- Python 3.13+
- uv (recommended) or pip
fastapi run src/gitmentario/main.pyYou can use e.g. Docker to run Gitmentario.
Copy compose.yml and .env.example to your setup, rename the latter to .env and fill in at least the required values:
cp .env.example .envEvery variable in .env overrides the corresponding default baked into compose.yml; anything you leave out falls back to that default.
Then start it:
docker compose upThe published port is bound to 127.0.0.1, so Gitmentario is not reachable from outside the host.
This is deliberate: Gitmentario enforces neither rate limits nor a request body limit itself, so it must run behind a reverse proxy that does — see Security.
Put your proxy in front of 127.0.0.1:8000, or, if the proxy runs as a container on the same Docker network, let it reach the gitmentario service by name on port 80.
All settings are read from environment variables or a .env file in the working directory.
Nested settings use __ as the delimiter (e.g. FORGE__AUTH_TOKEN).
| Variable | Default | Description |
|---|---|---|
CONTENT_DIR |
(required) | Repo-relative path to the SSG content directory (e.g. content) |
COMMENTS_DIR |
comments |
Subdirectory within CONTENT_DIR where comment files are stored |
GIT_PUSH |
true |
true: push directly to the default branch; false: create a branch and open a merge request |
TARGET_BRANCH |
main |
Branch used as base when creating merge requests |
ALLOWED_ORIGINS |
(empty) | Comma-separated browser origins allowed to submit comments (e.g. https://example.com) |
LOG_LEVEL |
INFO |
Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
FORGE__TYPE |
(required) | Forge type — currently only gitlab |
FORGE__BASE_URL |
(required) | Base URL of the GitLab instance |
FORGE__PROJECT_ID |
(required) | Numeric GitLab project ID |
FORGE__AUTH_TOKEN |
(required) | GitLab personal access token |
FORGE__AUTH_TOKEN may be a personal, group, or project access token.
Gitmentario only ever touches the one project you configured, so a project access token or a fine-grained token restricted to your project is the tightest fit.
Fine-grained personal access tokens let you grant exactly the actions Gitmentario performs, scoped to a single project:
| Resource | Action | Needed for | Endpoint |
|---|---|---|---|
Project |
Read |
Looking up the repository's default branch | GET /projects/:id |
Repository |
Read |
Checking whether a comment file already exists (409) |
GET /projects/:id/repository/files/:file_path |
Repository |
Create |
Writing the comment Markdown file | POST /projects/:id/repository/files/:file_path |
Branch |
Create |
Only when GIT_PUSH=false |
POST /projects/:id/repository/branches |
Merge Request |
Create |
Only when GIT_PUSH=false |
POST /projects/:id/merge_requests |
With GIT_PUSH=true the last two rows can be dropped.
On older GitLab versions the only scope that covers these endpoints is api.
write_repository is not sufficient:
it grants Git-over-HTTP access, but not the REST Repository Files API that Gitmentario uses.
Independently of scopes, the token’s role must allow the write:
GIT_PUSH=false(recommended): Developer is enough – the branch is new and unprotected, and the merge request is reviewed by you.GIT_PUSH=true: the token commits straight to the default branch. If that branch is protected (default), the role must be one that is allowed to push to it (default: Maintainer).
Gitmentario accepts writes to your repository from anonymous visitors. Its defaults are chosen to keep that safe, but a few things are the operator’s responsibility.
Gitmentario enforces neither rate limiting nor a request body limit.
That is intentional.
Both belong in the proxy in front of it, which should reject abusive requests before they occupy a worker.
Our example compose.yml binds the published port to 127.0.0.1 so the service is not reachable without one.
- Without a rate limit, a script can flood your repository with commits, branches and merge requests.
- Without a body limit, the whole JSON payload is parsed into memory, so a large request is a cheap denial of service.
| Proxy | Body limit | Rate limit |
|---|---|---|
| nginx | client_max_body_size 64k; (defaults to 1m) |
limit_req_zone + limit_req |
| Caddy | request_body { max_size 64KB } (no default limit) |
rate_limit (community module) |
| Traefik | buffering.maxRequestBodyBytes (no default limit) |
rateLimit middleware |
With GIT_PUSH=true, every accepted comment is committed directly to your default branch and appears on your live site without review.
Allowing anonymous input to reach your published site without review can turn an ordinary bug into a security problem.
With GIT_PUSH=false, a merge request is opened instead, and so a human approves each comment.
There is no known identity behind a comment:
author is whatever the submitter typed, so anyone can post under any name, including yours.
The current version of Gitmentario doesn’t support blocklists.
The comment body is written to the Markdown file verbatim, and the author name is stored in the YAML frontmatter. Gitmentario does not attempt to sanitize either, because what is dangerous depends entirely on how your site renders it. For Hugo:
- Keep Goldmark’s
unsafesetting atfalse(the default). Withunsafe = true, raw HTML in a comment is rendered as-is, which is stored cross-site scripting on your own domain. - Do not pass
authorthroughsafeHTMLin your templates! - Consider that Markdown alone still allows links and remote images in comments.
The one exception is Hugo shortcodes, which Gitmentario neutralizes on write.
Hugo expands shortcodes in a content file before Markdown is rendered, and does so even inside fenced code blocks, so unsafe = false is no protection against them.
An unknown shortcode can cause the entire site build to fail, so a single comment could prevent your site from being built.
Well-formed shortcodes are therefore rewritten to Hugo’s literal form ({{</* … */>}}), and unpaired openers, which have no literal form, are broken with a character reference.
Either way the visitor’s text still reads as they wrote it.
Accepts a comment submission and stores it as a Markdown file in the repository.
I ran my personal website on WordPress for a long time. When I considered switching to Hugo, I wanted to preserve the comments my readers had made. I looked for a service that could accept comment submissions and commit them directly to a static site’s Git repository — but found nothing suitable.
A few years later I discovered Staticman, which solves exactly this problem and had already existed for years. Sadly, it is no longer maintained and is written in JavaScript, which kept me from taking over. Gitmentario is my Python-based answer to the same idea.
GPLv3