A commenting system for Docusaurus pages. A small Go backend that stores comments and reactions in SQLite, and @danielpeinhopf/commentosaurus, a React component for the client-side.
This repository also contains a sample Docusaurus site demonstrating the whole thing end-to-end by wiring that component in through theme swizzling.
Auth is handled upstream by a proxy (traefik, see configs/), which forwards the signed-in user's identity as X-AuthorId/X-AuthorName headers. The server trusts those headers rather than doing its own login.
Note
This project is completely vibe-coded for my own needs using Claude Code and not very well tested yet. Use at your own risk.
packages/commentosaurus/ the CommentosaurusSection React component + API client
example-site/ a sample Docusaurus site consuming that package, with the comment UI wired in via theme swizzling
server/ the Go backend for storing comments and reactions
configs/ Traefik + OIDC auth-proxy config used in front of both
The repository uses Task.
# 1. Install JS dependencies
task install
# 2. Start up Keycloak and the traefik proxy
task proxy:start
# 3. Terminal A β the Go backend
task server
# 4. Terminal B β The example site
task siteThen open http://localhost:9080, login with a user from below, visit a doc page or blog post, and post a comment.
The following user accounts exist:
| Username | Password | Comments |
|---|---|---|
admin |
admin |
Admin account with all permissions |
bob |
bob |
A normal user account |
jane |
jane |
A normal user account |
| Var | Default | Purpose |
|---|---|---|
PORT |
8080 |
HTTP port the Go server listens on |
DB_PATH |
./comments.db |
SQLite file path |
ADMIN_AUTHOR_IDS |
(empty) | Comma-separated author ids allowed to see every private comment and delete any comment |
PROXY_SECRET |
(required) | Shared secret the auth proxy sends as X-ProxySecret; requests without it are rejected before X-AuthorId/X-AuthorName are trusted. Must match the header value in configs/http.yml. The server refuses to start if unset |
COMMENTS_ENABLED |
true |
Default for the comments feature; a page can override it (see "How comments attach to a page"). Disabled: posting/listing comments is rejected with 403, and the frontend hides the comment list and form |
REACTIONS_ENABLED |
true |
Default for the reactions feature, independently of comments; overridable per page. Disabled: reacting is rejected with 403, and the frontend hides the reaction buttons |
REACTION_EMOJIS |
π,π,β€οΈ,π,π |
Default set of emoji a page can be reacted to with, in display order; overridable per page. Reacting with anything outside the effective set is rejected; the frontend renders exactly that set (fetched from the API, not hardcoded) |
MAX_COMMENTS_PER_PAGE |
10 |
Number of comments returned per request, oldest first -- global only, not overridable per page. When a thread has more, the frontend shows a "Load more comments" button that re-requests with a growing offset. 0 means no limit. |
The docs site and the /commentosaurus/* API must be served from the same origin, through a single reverse proxy doing path-based routing in front of both -- exactly what configs/traefik-config.yml + configs/http.yml set up. Two things depend on this:
CommentosaurusSection's client code only ever calls relative paths (e.g./commentosaurus/api/comments), not an absolute API URL, so it only works same-origin.- The auth proxy's session cookie and the
X-AuthorId/X-AuthorName/X-ProxySecretheaders it attaches are scoped to that one origin. There's no cross-origin login flow, so a separately-hosted API (its own domain, with CORS) isn't supported.
Wiring Commentosaurus into your own Docusaurus site is three steps: install the package, swizzle it into your theme, and put a same-origin proxy in front that authenticates users and forwards commentosaurus traffic to the backend.
npm install @danielpeinhopf/commentosaurusThe package exports the CommentosaurusSection component which we'll use in the next step.
The package deliberately does not wire itself into the DocItem/BlogPostItem theme components for you. That's on the consuming site, via swizzling. Eject a wrapper for whichever page type(s) you want comments on:
npm run swizzle @docusaurus/theme-classic DocItem/Footer -- --wrap
npm run swizzle @docusaurus/theme-classic BlogPostItem/Footer -- --wrapThen, in each generated file, call the matching hook and render CommentosaurusSection with the config it returns:
// src/theme/DocItem/Footer/index.tsx
import React from 'react';
import type {ReactNode} from 'react';
import Footer from '@theme-original/DocItem/Footer';
import {CommentosaurusSection, useDocConfig} from '@danielpeinhopf/commentosaurus';
export default function FooterWrapper(props: object): ReactNode {
const config = useDocConfig();
return (
<>
<Footer {...props} />
<CommentosaurusSection config={config} />
</>
);
}// src/theme/BlogPostItem/Footer/index.tsx
import React from 'react';
import type {ReactNode} from 'react';
import Footer from '@theme-original/BlogPostItem/Footer';
import {CommentosaurusSection, useBlogPostConfig} from '@danielpeinhopf/commentosaurus';
export default function FooterWrapper(props: object): ReactNode {
const config = useBlogPostConfig();
// useBlogPostConfig() returns null on list/archive/tag pages, where
// BlogPostItem renders a truncated preview rather than the actual post.
return (
<>
<Footer {...props} />
{config && <CommentosaurusSection config={config} />}
</>
);
}This adds the CommentosaurusSection to the bottom of each page.
CommentosaurusSection's client only ever calls relative paths (e.g. /commentosaurus/api/comments). It has no notion of an API base URL, so the docs site and the Go backend must be served from the same origin. Put a reverse proxy in front of both that:
- Routes
/commentosaurus/*to the Go backend, and everything else to the Docusaurus site - Authenticates the user (e.g. via OIDC) and forwards their identity as
X-AuthorId/X-AuthorNameheaders - Attaches a shared-secret
X-ProxySecretheader, so the backend knows those identity headers actually came from the proxy and not a spoofed request
configs/traefik-config.yml + configs/http.yml set exactly this up with Traefik and the traefik-oidc-auth plugin, and docker-compose.yml runs it alongside a local Keycloak for testing (see Development Quickstart above).
Any proxy/auth stack works as long as it satisfies the three bullets above.
Traefik + OIDC is just what this repo demonstrates. Set the Go server's PROXY_SECRET env var to match the X-ProxySecret value configured on the proxy; see the Configuration table for the rest of its env vars, and Deployment above for more on why same-origin is required (and what it rules out).
By default CommentosaurusSection uses each page's route (permalink) as the comment thread id. If you rename or move a doc/blog post, its existing comments won't follow β set a comment_id front-matter field to pin a stable thread id across renames:
---
comment_id: my-stable-thread-id
---A page can also override the COMMENTS_ENABLED/REACTIONS_ENABLED/REACTION_EMOJIS defaults for itself via front matter:
---
comments_enabled: false
reactions_enabled: true
reaction_emojis: [π, π]
---All three are optional; an unset field falls back to the server's global default. The frontend reads these (example-site/src/theme/{DocItem,BlogPostItem}/Footer) and sends them with every request for that page, and the server resolves and enforces the effective value -- but the server has no independent notion of "pages" or their front matter, so, like pageId and a comment's body, these overrides are client-supplied and only structurally validated, not a hardened anti-abuse boundary. That's consistent with this project's overall stance on abuse today (see Spam prevention/Captcha below), not a gap specific to this feature.
MAX_COMMENTS_PER_PAGE is deliberately not in that list -- it's a global-only setting, see the Configuration table.
