* Move the site from eleventy to hugo The site is built by a single static binary. No node, no package manager, no lockfile, and the toolchain it needed is gone: eleventy, tailwind, postcss, markdown-it and its three plugins, date-fns, prism, npm-run-all, cross-env and html-minifier-terser. Hugo covers most of that itself. Chroma replaces prism, goldmark replaces markdown-it, `--minify` replaces html-minifier-terser, and fingerprinted asset URLs replace the cache-busting `version` shortcode that stamped `Date.now()` into every stylesheet link. `assets/styles.css` is hand-written, since tailwind was the only reason left to keep a package manager. The palette and the light and dark values are custom properties at the top of the file; the minified stylesheet is 15 kB against tailwind's 46 kB, and the whole build 1.0 MB against 1.2 MB. It was matched to the old one by comparing computed styles rather than by eye, which is how the heading weights and line heights, the list marker colour, and the home page heading and sign-off were caught: the last of those had been carried by tailwind utilities written into the markup. The `::: note` container becomes a `note` shortcode taking the emoji to show. Its closer needs a blank line after it, because a shortcode is not a block rule the way `markdown-it-container` was, and without one goldmark keeps the callout inside the open paragraph. The `overflow-x` wrapper around tables and the heading anchors are goldmark render hooks. Syntax guessing is off. Chroma detected a systemd unit file as gdscript and a chat transcript as mysql, and colouring a snippet as the wrong language is worse than not colouring it. The two chroma themes are scoped to opposite sides of the theme switch rather than layered, because they do not declare the same properties on the same tokens: github gives Error a background github-dark never overrides, and styles Punctuation where github-dark leaves it alone. Layered, either leaves a light value applying on a dark page. `[frontmatter] lastmod` resolves through git, then front matter, then file modification time. Without that chain `.Lastmod` falls back to `.Date`, which is zero when a page carries no date, and every page reads `Jan 01, 0001`. `enableGitInfo` is off because the image build context is `site/` alone, where hugo fails hard rather than degrading; `HUGO_ENABLEGITINFO=true` gives real per-page commit dates locally. Three fixes fall out of the move rather than being sought: - `/docs/` redirected nowhere. The stub was a markdown file whose permalink was a template expression while `markdownTemplateEngine` was false, so it never rendered and the URL 404'd. It is an alias now - `/docs/contributing/` pointed at `/docs/contributing/development/`, which has never existed. It points at the backend page - the 404 page was built to `/404/` and nothing served it. Hugo writes it to `/404.html` and reproxy is told to use it The mobile documentation menu is a checkbox and label. `visibility: hidden` on the checkbox, which is what the old `invisible` utility set, takes it out of the tab order, and a label is not focusable on its own, so the menu could not be opened from the keyboard at all. The checkbox is clipped rather than hidden, and its label shows a focus ring. Content is unchanged. Every code block on every page is byte-identical to the eleventy output; the only prose difference is that two example values, `mysite.com` and a quoted `https://demo.remark42.com`, are no longer turned into links, goldmark's linkify being narrower than markdown-it's. `backend/README.md` and `frontend/apps/remark42/README.md` are symlinks into the docs tree and follow it to `site/content/`, as does the path `release.yml` watches. `frontend/CLAUDE.md` described the site as a node and yarn project in four places. * Keep the heading anchors markdown-it generated Goldmark strips punctuation markdown-it kept, so 22 headings holding a dot, slash, apostrophe, question mark, bracket or em dash would take a new id and any link into one from outside the repository would stop resolving. Those headings carry their previous id as well, as an empty target emitted ahead of the heading by the render hook, from a map of content path to old anchor in `data/anchor_aliases.json`. The map was built by matching heading text between the two builds rather than by position, so it survives a heading being added or moved. The hook rather than markdown, because goldmark's `{#id}` attribute syntax cannot express these: it accepts dots, apostrophes and em dashes but treats a slash, a question mark, a bracket or a percent sign as heading text, which is 11 of the 22. The ids are stored percent-decoded, since a browser decodes a fragment before matching, so `#children%E2%80%99s-privacy` finds `children’s-privacy`. Verified by navigating to the awkward ones against the built image and measuring where the page settles: each lands 112px down, which is the header offset the target carries. Three pages carried no title, so the docs template rendered an empty `<h1>` above the heading their markdown already had. They take their titles from that heading text, so neither the wording nor its anchor changes, and the template's `<h1>` carries an id. One in-page link pointed at an anchor goldmark no longer generates. The heading render hook emits no permalink anchor. The one it replaced was an empty `<a href>` with `pointer-events: none`, so it could not be clicked, and its only job was a `::before` spacer that `scroll-margin-top` on the heading already does. Being an `<a href>` it stayed in the tab order, so every heading was an unexplained keyboard stop: eight on the installation page alone. Fragment navigation still lands 112px down, clear of the fixed header. * Harden the site image build and its CI The architecture guard could not fire. `${TARGETARCH:-amd64}` defaulted before the `unsupported arch` branch was reachable, so a build without buildkit put an amd64 hugo inside an aarch64 image and ran only because Docker Desktop emulates it. Reproduced with `--build-arg TARGETARCH=`: `/etc/apk/arch` reported aarch64 and `hugo version` linux/amd64. An empty value is an error now. `Dockerfile.dev` had the same defect and no smoke step to catch it, so it would have failed at `compose up`. The hugo tarball is verified against the release's own `checksums.txt`, and the match is asserted present before it is used: piping grep straight into `sha256sum -c` left the guarantee resting on what the checker does with empty input. Busybox exits 1 there, so it did fail closed, but nothing in the line said so. Verified against a checksums file that does not list the tarball: the build stops before the install. Hugo exits 0 on an empty content tree and emits a two-page shell, which would have been copied, pushed and deployed. The build asserts the home page and a docs page exist. `site/**` pull requests were never built. The only building job is gated on `github.ref == 'refs/heads/master'`, so on a pull request every job skipped and rendered in the checks list the same way a pass does, and the image was first built on the run that also deploys it. A `validate` job builds it with `push: false`, needing no secrets so it works on a fork. `.github/dependabot.yml` watched `/site` for npm packages that are gone. That entry is a docker one, which tracks the alpine base. It does not track the hugo pin and cannot: the docker ecosystem reads `FROM` references, and `ARG HUGO_VERSION` is a bare string in a download URL, so that one is a manual bump and `site/README.md` says so. `Dockerfile.dev` carries a `COPY`, so the dev image works without the compose bind mount, and compose runs as the invoking user rather than root, which on linux left root-owned `public/` and `resources/` in the checkout. Recorded in the backlog: `master` has `required_status_checks` off with an empty check list, so the new job surfaces a red X and does not block a merge. That is a settings decision rather than a code fix.
5.2 KiB
title, aliases
| title | aliases | ||
|---|---|---|---|
| Installation |
|
System Requirements
Remark42 is designed to be lightweight and efficient. Based on production usage statistics from busy installations:
- CPU: Minimal usage (typically under 0.1%)
- Memory: ~80MiB RAM (less than 4% of 2GB)
- Network: Moderate bandwidth requirements
- Disk: Small footprint (under 200MB for a 5-year-old installation with regular activity)
These requirements make Remark42 suitable for running on small VPS instances, shared hosting environments, or even on Raspberry Pi and similar devices.
Setup Remark42 Instance on Your Server
Installation in Docker
This is the recommended way to run Remark42
- copy provided
docker-compose.ymland customize for your needs - make sure you don't keep
ADMIN_PASSWD=something...for any non-development deployments - pull prepared images from the Docker Hub and start -
docker compose pull && docker compose up -d - alternatively, compile from the sources -
docker compose build && docker compose up -d
Installation with Binary
- download archive for the stable release
- unpack with
gunzip(Linux, macOS) or withzip(Windows) - run as
remark42.{os}-{arch} server {parameters...}, i.e.,remark42.linux-amd64 server --secret=12345 --url=http://127.0.0.1:8080 - alternatively compile from the sources -
make OS=[linux|darwin|windows] ARCH=[amd64,386,arm64,arm]. Source binary builds require Go 1.25, Node 20+, PNPM 10, and Perl because the frontend assets are built and embedded locally.
Installation as a systemd Service
For a clean persistent setup without lengthy command line parameters:
-
Create an environment file
/etc/remark42.env:SECRET=12345 REMARK_URL=http://127.0.0.1:8080 -
Create a systemd service file
/etc/systemd/system/remark42.service:[Unit] Description=Remark42 Commenting Server After=syslog.target After=network.target [Service] Type=simple EnvironmentFile=/etc/remark42.env ExecStart=/usr/local/bin/remark42 server WorkingDirectory=/var/www/remark42 # directory where data files are stored and automatic backups will be created Restart=on-failure User=nobody # another good alternative is `www-data` Group=nogroup # another good alternative is `www-data` [Install] WantedBy=multi-user.target -
Enable and start the service:
sudo systemctl enable remark42.service sudo systemctl start remark42.service -
To update configuration, edit the environment file and restart the service:
sudo systemctl restart remark42.service
Setup on Your Website
Add config for Remark on a page of your site (here is the full reference):
REMARK_URL– the URL where is Remark42 instance is served, passed asREMARK_URLto backendYOUR_SITE_ID- theSITEthat you passed to Remark42 instance on start,remarkby default.
<script>
var remark_config = {
host: "REMARK_URL",
site_id: "YOUR_SITE_ID",
}
</script>
For example:
<script>
var remark_config = {
host: "https://demo.remark42.com",
site_id: "remark",
}
</script>
After that place the code snippet right after config.
<script>!function(e,n){for(var o=0;o<e.length;o++){var r=n.createElement("script"),c=".js",d=n.head||n.body;"noModule"in r?(r.type="module",c=".mjs"):r.async=!0,r.defer=!0,r.src=remark_config.host+"/web/"+e[o]+c,d.appendChild(r)}}(remark_config.components||["embed"],document);</script>
{{< note "💡" >}}
Note: You can place the config with the snippet in any place of the HTML code of your site. If it is closer to start of the HTML (for example in <head>) it will start loading sooner and show comments faster.
{{< /note >}}
Put the next code snippet on a page of your site where you want to have comments:
<div id="remark42">Comments loading...</div>
After that widget will be rendered inside this node. Any content you place inside the div (such as "Comments loading..." above) is automatically removed once the widget initialises, so you can use it as a loading placeholder.
For more information about frontend configuration please learn about other parameters here If you want to set this up on a Single Page App, see the appropriate doc page.
Quick installation test
To verify if Remark42 has been properly installed, check a demo page at ${REMARK_URL}/web URL. Make sure to include remark site ID to the ${SITE} list.
Build from the source
- to build Docker container -
make docker. This command will produce containerghcr.io/umputun/remark42 - to build a single binary for direct execution -
make OS=<linux|windows|darwin> ARCH=<amd64|386>. This requires Go 1.25, Node 20+, PNPM 10, and Perl, builds frontend assets locally, and produces an executableremark42file with everything embedded