Move the site from eleventy to hugo (#2179)

* 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.
This commit is contained in:
Dmitry Verkhoturov
2026-08-21 18:05:56 -05:00
committed by GitHub
parent ff77f41a3a
commit 4c9ef37cf1
113 changed files with 1495 additions and 3617 deletions
+4 -8
View File
@@ -56,15 +56,11 @@ updates:
dependency-type: "production"
"NPM modules updates for tests":
dependency-type: "development"
- package-ecosystem: "npm"
- package-ecosystem: "docker"
directory: "/site"
open-pull-requests-limit: 0
ignore:
- dependency-name: "*"
schedule:
interval: "monthly"
groups:
"NPM modules updates":
dependency-type: "production"
"NPM modules updates for tests":
dependency-type: "development"
"Site image updates":
patterns:
- "*"
+28 -1
View File
@@ -19,6 +19,33 @@ concurrency:
cancel-in-progress: true
jobs:
validate:
name: Build site image (pull request)
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: checkout
uses: actions/checkout@v7
with:
persist-credentials: false
- name: set up docker buildx
uses: docker/setup-buildx-action@v4
- name: build image without pushing
uses: docker/build-push-action@v7
with:
context: ./site
load: true
push: false
cache-from: |
type=gha,scope=site-pr
type=gha,scope=site-linux/amd64
cache-to: type=gha,scope=site-pr,mode=max,ignore-error=true
build:
name: Build site image (${{ matrix.platform }})
if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')
@@ -60,7 +87,7 @@ jobs:
context: ./site
platforms: ${{ matrix.platform }}
cache-from: type=gha,scope=site-${{ matrix.platform }}
cache-to: type=gha,scope=site-${{ matrix.platform }},mode=max
cache-to: type=gha,scope=site-${{ matrix.platform }},mode=max,ignore-error=true
outputs: type=image,name=ghcr.io/umputun/remark42-site,push-by-digest=true,name-canonical=true,push=true
- name: export digest
+1 -1
View File
@@ -15,7 +15,7 @@ on:
- "README.md"
- "LICENSE"
- "CLAUDE.md"
- "site/src/docs/getting-started/installation/index.md"
- "site/content/docs/getting-started/installation/index.md"
permissions:
contents: read
+1 -1
View File
@@ -1 +1 @@
../site/src/docs/contributing/backend/index.md
../site/content/docs/contributing/backend/index.md
@@ -0,0 +1,19 @@
---
worth: yes
where: repository settings, not a file
added: 2026-08-21
---
# nothing on master is a required status check
`master` is protected, but `required_status_checks` is `{"checks": [], "contexts": [], "enforcement_level": "off"}`,
and the only ruleset on the branch ("Copilot review for default branch", id 15225632) carries just
`deletion`, `non_fast_forward` and `copilot_code_review`. Verified with
`gh api repos/umputun/remark42/branches/master`.
Every workflow in the repository is therefore advisory. A red X on any backend, frontend or site job
shows in the checks list and does not stop the merge button, so a pull request that fails CI can still
be merged by anyone who does not read the list.
Fix: configure required status checks on `master` for the jobs that should block. Worth deciding
deliberately rather than by default, since it also governs what happens to Dependabot pull requests
and to any job that turns out to be flaky.
-35
View File
@@ -1,35 +0,0 @@
---
worth: yes
where: .github/workflows/ci-site.yml:24
added: 2026-08-11
---
# site/** pull requests get no build validation
`ci-site.yml` declares a `pull_request` trigger on `site/**` (lines 12-15), but the only job that
installs and builds is gated:
```yaml
if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')
```
On a pull request `github.ref` is `refs/pull/N/merge`, so `build` skips, and `merge` (`needs: build`)
and `deploy` skip with it. The trigger is dead: it produces a run in which every job is skipped, which
renders in the checks list the same way a pass does. No required status checks are configured on
master either, so nothing else catches it.
Consequence: a `site/yarn.lock` that fails `yarn --frozen-lockfile` first fails on the master run
after merge, which is the same run that rebuilds and deploys remark42.com. `site/Dockerfile:5-6` is
the only place the lockfile is exercised, and it runs post-merge.
Fix: add a PR-only job mirroring `.github/workflows/ci-build.yml`'s existing pattern, using
`docker/build-push-action` with `context: ./site`, `load: true`, no `outputs:`, and no
`docker/login-action`.
Constraint any fix must preserve: the current gate exists because `build` pushes to ghcr using
`secrets.PKG_TOKEN`, which must not run for pull requests, since fork PRs receive no secrets.
Relaxing the `if` on its own is not sufficient and would break fork PRs.
Surfaced while reviewing PR #2141 (a dependabot js-yaml lockfile bump), where all three site jobs
reported as skipped and nothing verified the lockfile before merge. `site/**` PRs are mostly
generated lockfile bumps, which is exactly the class a frozen-lockfile install catches and a human
reviewer cannot.
+4 -10
View File
@@ -7,12 +7,10 @@ Non-obvious constraints in the frontend toolchain and widget. Read before bumpin
CI staying green does **not** mean every pin is consistent — `.nvmrc` in particular is never read by CI, so it can silently drift. After changing the node or pnpm version, grep the whole repo and update every one of these, not just the ones CI exercises:
- `Dockerfile` (production image) — `FROM node:X-alpine` and `npm i -g pnpm@X.Y.Z`
- `site/Dockerfile`, `site/Dockerfile.dev``FROM node:X-alpine` (site uses yarn, not pnpm)
- `frontend/.nvmrc`, `site/.nvmrc` — not read by CI at all; only matters to a human running `nvm use` locally. This is the one that drifted unnoticed: it sat at `16` through the whole node-20 migration because nothing red ever pointed at it.
- `frontend/.nvmrc` — not read by CI at all; only matters to a human running `nvm use` locally. This is the one that drifted unnoticed: it sat at `16` through the whole node-20 migration because nothing red ever pointed at it.
- Every `package.json`'s `packageManager` field (`frontend/package.json`, `frontend/apps/remark42/package.json`) and `frontend/apps/remark42/package.json`'s `engines` block
- `pnpm/action-setup@vN` blocks in `.github/workflows/ci-frontend.yml` (5) and `release.yml` (2) — pin `version:` to the **exact** patch (e.g. `10.10.0`), matching `packageManager`, not just the major. A floating major here is silent in CI (it just resolves to whatever the latest patch is at run time) but breaks the "Dockerfile and CI use the same pnpm" guarantee.
- `node:` matrices in `.github/workflows/ci-frontend.yml` (every entry, not just the first) and the `node-version:` values in `release.yml`
- `site/package.json`'s `engines.node` and `engines.yarn` (site uses yarn, so its `packageManager` moves independently)
When bumping pnpm/node, also re-check `frontend/apps/remark42/package.json`'s `engines` field — it's separate from `packageManager` and won't update itself.
@@ -55,21 +53,17 @@ without `import { h }` would type-check and lint clean, then throw at runtime, b
These were deliberately not bumped because each is a config-migration or bundle-changing major, not a drop-in update — don't bump them opportunistically inside an unrelated dependency PR:
- `eslint` 8 (9/10 need flat-config migration), `stylelint` 14 (16 has breaking rule changes), `babel` 7, `jest` 28 (30 needs config changes)
- `redux` 4, `tailwindcss` 3.4 (v4 is a full config rewrite), `@11ty/eleventy` 2 (v3 is an ESM migration) in `site/`
## `html-minifier` is abandoned — use `html-minifier-terser`
`site/.eleventy.js` uses `html-minifier-terser` (a maintained fork), not `html-minifier` (unpatched ReDoS advisory, no fix ever released). The eleventy transform had to become `async` for this fork's API.
- `redux` 4
## Verifying a build didn't regress
There's no automated build-output diff in CI. Before merging a dependency PR that touches the bundler/build tooling, manually diff the build output against a clean `master` checkout:
- `apps/remark42`: expect webpack module-id numbers and css-module class tokens (e.g. `.F_A``.L_A`) to differ — that's normal churn from a webpack/css-loader bump. HTML, CSS values, and translation content should be byte-identical.
- `site`: expect the `?v=<timestamp>` cache-bust query string to differ on every HTML file — that's expected. Anything else differing is a real regression.
- `site`: asset URLs carry a content hash, so a stylesheet or script change moves the filename on every page referencing it. Anything else differing is a real regression.
## Where the alerts actually were
When clearing Dependabot/audit alerts, check whether the flagged package is actually reachable from production code or only from the dev/test toolchain — `pnpm audit`/`yarn audit` don't distinguish. Several alerts here were in build-time-only tooling (webpack-dev-server, laravel-mix-equivalent dev deps) with no patched release available; those are lower-risk than a runtime dependency with the same severity label.
When clearing Dependabot/audit alerts, check whether the flagged package is actually reachable from production code or only from the dev/test toolchain — `pnpm audit` does not distinguish. Several alerts here were in build-time-only tooling (webpack-dev-server, laravel-mix-equivalent dev deps) with no patched release available; those are lower-risk than a runtime dependency with the same severity label.
## Don't import `preact/compat`
+1 -1
View File
@@ -1 +1 @@
../../../site/src/docs/contributing/frontend/index.md
../../../site/content/docs/contributing/frontend/index.md
+6 -2
View File
@@ -1,5 +1,9 @@
node_modules
.git
.gitignore
.prettierrc
.dockerignore
Dockerfile
Dockerfile.dev
docker-compose.yml
build
public
resources
-118
View File
@@ -1,118 +0,0 @@
const { format } = require('date-fns')
const htmlmin = require('html-minifier-terser')
const syntaxHighlightPlugin = require('@11ty/eleventy-plugin-syntaxhighlight')
function noteContainer() {
const { utils } = require('markdown-it')()
const elementRegexp = /^note\s+(.*)$/
return {
validate(params) {
return params.trim().match(elementRegexp)
},
render(tokens, idx) {
const { info, nesting } = tokens[idx]
const matches = info.trim().match(elementRegexp)
if (nesting === 1) {
const icon = utils.escapeHtml(matches[1])
return `<aside class="relative pr-4 pl-12 py-1 bg-gray-50 dark:bg-gray-800"><span class="absolute left-4 top-6 text-xl">${icon}</span>`
}
return `</aside>`
},
}
}
function markdownTableWrapper(md) {
md.renderer.rules.table_open = function (tokens, idx, options, _, self) {
return (
`<div class="overflow-x-auto">` + self.renderToken(tokens, idx, options)
)
}
md.renderer.rules.table_close = function (tokens, idx, options, _, self) {
return self.renderToken(tokens, idx, options) + `</div>`
}
}
function getMarkdownLib() {
const markdownIt = require('markdown-it')
const markdownItAnchor = require('markdown-it-anchor')
const markdownItContainer = require('markdown-it-container')
return markdownIt({
html: true,
breaks: true,
linkify: true,
})
.use(markdownItAnchor, {
permalink: markdownItAnchor.permalink.linkInsideHeader({
placement: 'before',
class: 'header-anchor',
symbol: '',
}),
})
.use(markdownItContainer, 'note', noteContainer())
.use(markdownTableWrapper)
}
module.exports = function (eleventyConfig) {
// TODO: create version with commit sha and current version of Remark42
eleventyConfig.addShortcode('version', () => `${Date.now()}`)
eleventyConfig.addShortcode('year', () => `${new Date().getFullYear()}`)
eleventyConfig.setUseGitIgnore(false)
eleventyConfig.addWatchTarget('./.tmp/style.css')
eleventyConfig.addPassthroughCopy({ './.tmp/style.css': './style.css' })
eleventyConfig.addPassthroughCopy({ './public': './' })
eleventyConfig.addPassthroughCopy('./src/**/*.{gif,jpg,png,svg}')
eleventyConfig.addCollection('pages', (collection) =>
collection.getFilteredByGlob('pages/*.md')
)
eleventyConfig.addFilter('humanizeDate', (date) =>
format(new Date(date), 'LLL dd, yyyy')
)
eleventyConfig.addFilter('robotizeDate', (date) =>
format(new Date(date), 'yyyy-MM-dd')
)
eleventyConfig.addFilter(
'debug',
(content = {}) => `<pre>${JSON.stringify(content, null, 2)}</pre>`
)
// Minify HTML output
eleventyConfig.addTransform('htmlmin', async function (content, outputPath) {
// outputPath is falsy for templates rendered without a written file (e.g. permalink: false)
if (!outputPath || !outputPath.endsWith('.html')) {
return content
}
return htmlmin.minify(content, {
removeComments: true,
collapseWhitespace: true,
})
})
eleventyConfig.setLibrary('md', getMarkdownLib())
eleventyConfig.addPlugin(syntaxHighlightPlugin)
eleventyConfig.addCollection('docs', (collection) =>
collection.getFilteredByGlob('src/docs/**/*.md')
)
return {
markdownTemplateEngine: false,
dir: {
input: 'src',
output: 'build',
data: 'data',
layouts: 'layouts',
includes: 'includes',
},
}
}
-3
View File
@@ -1,3 +0,0 @@
build
node_modules
yarn-*.log
+3 -3
View File
@@ -1,4 +1,4 @@
node_modules
yarn-*.log
.tmp
public
build
resources
.hugo_build.lock
-1
View File
@@ -1 +0,0 @@
20
-3
View File
@@ -1,3 +0,0 @@
build
public/site.webmanifest
src/docs/configuration/parameters/index.md
-14
View File
@@ -1,14 +0,0 @@
{
"semi": false,
"singleQuote": true,
"arrowParens": "always",
"useTabs": true,
"overrides": [
{
"files": "*.md",
"options": {
"singleQuote": false
}
}
]
}
+28 -5
View File
@@ -1,10 +1,33 @@
FROM node:20-alpine AS build
FROM alpine:3.22 AS build
ARG HUGO_VERSION=0.165.0
ARG TARGETARCH
# the extended build is glibc-linked and will not run on alpine; nothing here needs
# sass or webp, so the plain static build is the one to use
RUN apk add --no-cache ca-certificates \
&& case "${TARGETARCH}" in \
amd64) HUGO_ARCH=amd64 ;; \
arm64) HUGO_ARCH=arm64 ;; \
"") echo "TARGETARCH is empty; build with buildkit" >&2; exit 1 ;; \
*) echo "unsupported arch ${TARGETARCH}" >&2; exit 1 ;; \
esac \
&& HUGO_TAR="hugo_${HUGO_VERSION}_linux-${HUGO_ARCH}.tar.gz" \
&& BASE="https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}" \
&& wget -qO "/tmp/${HUGO_TAR}" "${BASE}/${HUGO_TAR}" \
&& wget -qO /tmp/checksums.txt "${BASE}/hugo_${HUGO_VERSION}_checksums.txt" \
&& grep " ${HUGO_TAR}$" /tmp/checksums.txt > /tmp/expected.sha256 \
&& test -s /tmp/expected.sha256 \
&& (cd /tmp && sha256sum -c expected.sha256) \
&& tar -xzf "/tmp/${HUGO_TAR}" -C /usr/local/bin hugo \
&& rm -f "/tmp/${HUGO_TAR}" /tmp/checksums.txt /tmp/expected.sha256 \
&& hugo version
WORKDIR /site
COPY ./ /site
RUN yarn --frozen-lockfile
RUN yarn build
RUN ls -la /site
RUN hugo --minify --destination /site/build \
&& test -s /site/build/index.html \
&& test -s /site/build/docs/getting-started/installation/index.html
FROM ghcr.io/umputun/reproxy
LABEL org.opencontainers.image.authors="Umputun <umputun@gmail.com>" \
@@ -18,4 +41,4 @@ LABEL org.opencontainers.image.authors="Umputun <umputun@gmail.com>" \
COPY --from=build /site/build /srv/site
EXPOSE 8080
USER app
ENTRYPOINT ["/srv/reproxy", "--assets.location=/srv/site"]
ENTRYPOINT ["/srv/reproxy", "--assets.location=/srv/site", "--assets.not-found=404.html"]
+25 -4
View File
@@ -1,7 +1,28 @@
FROM node:20-alpine AS build
FROM alpine:3.22
ARG HUGO_VERSION=0.165.0
ARG TARGETARCH
# the extended build is glibc-linked and will not run on alpine; nothing here
# needs sass or webp, so the plain static build is the one to use
RUN apk add --no-cache ca-certificates \
&& case "${TARGETARCH}" in \
amd64) HUGO_ARCH=amd64 ;; \
arm64) HUGO_ARCH=arm64 ;; \
"") echo "TARGETARCH is empty; build with buildkit" >&2; exit 1 ;; \
*) echo "unsupported arch ${TARGETARCH}" >&2; exit 1 ;; \
esac \
&& HUGO_TAR="hugo_${HUGO_VERSION}_linux-${HUGO_ARCH}.tar.gz" \
&& BASE="https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}" \
&& wget -qO "/tmp/${HUGO_TAR}" "${BASE}/${HUGO_TAR}" \
&& wget -qO /tmp/checksums.txt "${BASE}/hugo_${HUGO_VERSION}_checksums.txt" \
&& grep " ${HUGO_TAR}$" /tmp/checksums.txt > /tmp/expected.sha256 \
&& test -s /tmp/expected.sha256 \
&& (cd /tmp && sha256sum -c expected.sha256) \
&& tar -xzf "/tmp/${HUGO_TAR}" -C /usr/local/bin hugo \
&& rm -f "/tmp/${HUGO_TAR}" /tmp/checksums.txt /tmp/expected.sha256
WORKDIR /site
COPY ./ /site
RUN yarn --frozen-lockfile
CMD yarn start
EXPOSE 1313
CMD ["hugo", "server", "--bind", "0.0.0.0", "--port", "1313", "--baseURL", "http://127.0.0.1:1313/", "--appendPort=false"]
+47 -23
View File
@@ -1,44 +1,68 @@
# Remark42 site
## Work on your local environment
Sources for [remark42.com](https://remark42.com), built with [Hugo](https://gohugo.io).
Requirements:
## Requirements
- [Node.js v20](https://nodejs.org/en/) or higher - install from package or with Homebrew
- Yarn 1.22 or higher - once you have Node.js, run `npm i -g yarn`
Hugo, the plain build; nothing here needs the extended one. The version the published image builds with is `ARG HUGO_VERSION` in `Dockerfile`, and any release at or above it works locally. Nothing updates that pin automatically: Dependabot's docker ecosystem reads `FROM` references, and the Hugo version is a bare string in a download URL, so bumping it is a manual edit. `brew install hugo`, or see the [installation guide](https://gohugo.io/installation/). Nothing else: no Node, no package manager.
### Development
Install dependencies and start the development server:
## Development
```shell
yarn
yarn dev
hugo server
```
### Build
Serves the site on <http://localhost:1313> and rebuilds on change.
Alternatively, without installing Hugo:
```shell
yarn build
docker compose up
```
## Work with Docker Compose
### Build
Install dependencies and run development server inside Docker:
## Build
```shell
docker compose build
docker compose up server
hugo --minify --cleanDestinationDir --destination build
```
Then serve files from `./build` with your favorite server
Writes the static site to `build/`, which is what the Docker image serves. `--cleanDestinationDir` matters on a rebuild: without it a page you deleted, and the fingerprinted stylesheets of earlier builds, stay behind.
### Development
## Layout
```shell
docker compose up --build server
| Path | Contents |
| --------------------- | -------------------------------------------------------------------- |
| `content/` | Pages as markdown. `content/docs/` is the documentation tree |
| `layouts/` | Templates. `partials/`, `shortcodes/` and `_markup/` render hooks |
| `assets/` | `styles.css` and the scripts, fingerprinted at build time |
| `static/` | Files copied to the site root as-is: favicons, manifest, `robots.txt` |
| `data/nav.json` | The documentation sidebar |
| `hugo.toml` | Site configuration |
## Writing docs
A page is a markdown file with a `title` in its front matter. A directory becomes a section when it holds `_index.md`, and a page that carries its own images is a directory with `index.md` and the images beside it.
Adding a page to the sidebar means adding an entry to `data/nav.json`; the paths there are relative to `/docs`.
Callouts use the `note` shortcode, which takes the emoji to show:
```markdown
{{< note "💡" >}}
Anything markdown here.
{{< /note >}}
```
Then head to http://localhost:8080
Two documentation pages are symlinked into the repository as `backend/README.md` and `frontend/apps/remark42/README.md`, so moving or renaming `content/docs/contributing/backend/index.md` or `content/docs/contributing/frontend/index.md` means repointing those symlinks.
## Styling
`assets/styles.css` is hand-written, with the palette and light/dark values as custom properties at the top. The dark theme is applied by a `dark` class on `<html>`, set before first paint by `assets/inline.js` and toggled by `assets/script.js`.
Code highlighting is Hugo's built-in Chroma. The rules at the end of the stylesheet come from `hugo gen chromastyles --style=github` and `--style=github-dark`.
## Dates
The "Updated" line on a documentation page comes from `.Lastmod`. `hugo.toml` resolves it through `[frontmatter] lastmod`, which tries git, then a `lastmod` in the page's front matter, then the file's modification time; without that chain it would resolve to `.Date` and every page would read `Jan 01, 0001`. A deployed page therefore shows the build date, since a checkout does not preserve modification times.
`HUGO_ENABLEGITINFO=true hugo` gives real per-page commit dates. It is off by default because the image build context is `site/` alone, where there is no `.git` for Hugo to read and it fails rather than falling back.
@@ -54,7 +54,7 @@ const cachedVersion = readVersionCache()
if (cachedVersion) {
applyVersion(cachedVersion)
} else {
fetch('{{ site.githubApiUrl }}/releases/latest')
fetch('{{ site.Params.githubApiUrl }}/releases/latest')
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status))))
.then((d) => {
if (!d || !d.tag_name) return
+987
View File
@@ -0,0 +1,987 @@
/* ---------------------------------------------------------------- tokens */
:root {
--brand-100: #e0fbf8;
--brand-300: #4be7dc;
--brand-400: #1ccac1;
--brand-500: #16a29f;
--brand-600: #157f7f;
--brand-800: #125254;
--brand-900: #134b4e;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--neutral-800: #262626;
--neutral-900: #171717;
--bg: #fff;
--bg-raised: var(--gray-50);
--bg-code: var(--gray-100);
--fg: var(--gray-800);
--fg-muted: var(--gray-500);
--fg-heading: var(--brand-900);
--fg-subheading: var(--gray-700);
--accent: var(--brand-500);
--accent-hover: var(--brand-600);
--accent-soft-bg: var(--brand-100);
--accent-soft-fg: var(--brand-600);
--code-fg: var(--brand-800);
--border: var(--gray-200);
--border-soft: var(--gray-100);
--marker: var(--gray-300);
--container-padding: 1rem;
--header-height: 6rem;
--radius: 0.25rem;
--font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}
.dark {
--bg: var(--neutral-900);
--bg-raised: var(--neutral-800);
--bg-code: var(--gray-800);
--fg: var(--gray-100);
--fg-muted: var(--gray-400);
--fg-heading: var(--brand-100);
--fg-subheading: var(--gray-200);
--accent: var(--brand-400);
--accent-hover: var(--brand-300);
--accent-soft-bg: var(--brand-900);
--accent-soft-fg: var(--brand-400);
--code-fg: var(--brand-300);
--border: var(--gray-600);
--border-soft: var(--gray-800);
}
/* ------------------------------------------------------------------ base */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
height: 100%;
-webkit-text-size-adjust: 100%;
scroll-behavior: smooth;
}
body {
margin: 0;
min-height: 100%;
display: flex;
flex-direction: column;
background-color: var(--bg);
color: var(--fg);
font-family: var(--font-sans);
line-height: 1.5;
}
a {
color: inherit;
}
img,
svg {
max-width: 100%;
height: auto;
display: block;
}
button {
font: inherit;
color: inherit;
background: none;
border: 0;
padding: 0;
cursor: pointer;
}
code,
pre,
kbd {
font-family: var(--font-mono);
}
:where(a, button, input, summary):focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
/* ---------------------------------------------------------------- layout */
.container {
width: 100%;
margin-inline: auto;
padding-inline: var(--container-padding);
}
@media (min-width: 768px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1200px;
}
}
.main {
position: relative;
flex: 1 0 auto;
display: flex;
flex-wrap: wrap;
padding-top: var(--header-height);
}
@media (min-width: 768px) {
.main {
padding-top: 5rem;
}
}
/* ---------------------------------------------------------------- header */
.site-header {
position: fixed;
inset: 0 0 auto 0;
z-index: 20;
background-color: color-mix(in srgb, var(--bg) 80%, transparent);
backdrop-filter: blur(4px);
color: var(--fg-muted);
}
@media (min-width: 768px) {
.site-header {
padding-block: 0.5rem;
}
}
.site-header__inner {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.site-header__brand {
order: 1;
margin: 0.25rem 1rem 0 0;
padding-block: 0.25rem;
font-size: 1.5rem;
font-weight: 800;
text-decoration: none;
color: var(--accent);
transition: color 0.15s;
}
.site-header__brand:hover {
color: var(--accent-hover);
}
.site-header__nav {
order: 3;
width: 100%;
margin-top: 0.25rem;
padding-block: 0.25rem;
display: flex;
gap: 0.5rem;
}
@media (min-width: 768px) {
.site-header__nav {
order: 2;
width: auto;
}
}
.site-header__nav a {
font-weight: 500;
line-height: 1.25rem;
text-decoration: none;
border-bottom: 1px solid var(--border);
transition:
color 0.15s,
border-color 0.15s;
}
.site-header__nav a:hover {
color: var(--accent-hover);
border-bottom-color: var(--accent);
}
.site-header__actions {
order: 2;
margin-left: auto;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 1rem;
}
@media (min-width: 768px) {
.site-header__actions {
order: 3;
}
}
.site-header__icon {
display: flex;
align-items: center;
text-decoration: none;
}
.site-header__icon:hover,
.site-header__theme:hover {
color: var(--fg);
}
.site-header__version {
margin-right: 0.5rem;
font-size: 0.875rem;
}
.site-header__theme:hover {
color: #facc15;
}
/* ---------------------------------------------------------------- footer */
.site-footer {
flex-shrink: 0;
margin-top: 1.5rem;
padding-block: 1rem;
text-align: center;
font-size: 0.875rem;
}
.site-footer a {
opacity: 0.7;
text-decoration: underline;
}
/* ------------------------------------------------------------ docs aside */
.docs-aside {
flex-shrink: 0;
width: 100%;
max-width: 260px;
}
@media (min-width: 768px) {
.docs-aside {
position: relative;
width: 25%;
}
}
.docs-aside__state {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.docs-aside__state:focus-visible + .docs-aside__toggle {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.docs-aside__toggle {
display: flex;
align-items: center;
margin-bottom: 2rem;
padding-block: 0.5rem;
border-block: 1px solid var(--border-soft);
color: var(--fg-muted);
font-weight: 500;
cursor: pointer;
}
.docs-aside__nav {
display: none;
}
.docs-aside__state:checked ~ .docs-aside__nav {
display: block;
}
@media (min-width: 768px) {
.docs-aside__toggle {
display: none;
}
.docs-aside__nav {
display: block;
margin-right: 2.5rem;
}
}
.docs-aside__section {
margin: 0 0 0.5rem;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
color: var(--fg-muted);
}
.docs-aside__list {
margin: 0 0 2rem;
padding: 0;
list-style: none;
font-weight: 500;
}
.docs-aside__list--nested {
margin: 0 0 0 1rem;
font-weight: 400;
}
.docs-aside__list li {
margin-block: 0.25rem;
}
.docs-aside__link {
display: block;
margin-inline: -0.5rem;
padding: 0.25rem 0.5rem;
border-radius: var(--radius);
text-decoration: none;
color: var(--fg-muted);
}
.docs-aside__link:hover {
color: var(--fg);
}
.docs-aside__link.is-current {
background-color: var(--accent-soft-bg);
color: var(--accent-soft-fg);
}
.burger-icon {
position: relative;
display: inline-block;
width: 1rem;
height: 1rem;
margin-right: 0.75rem;
}
.burger-icon span {
position: absolute;
left: 0;
width: 100%;
height: 2px;
border-radius: var(--radius);
background-color: var(--gray-400);
transition: all 0.15s;
}
.burger-icon span:nth-child(1) {
top: 0;
}
.burger-icon span:nth-child(2) {
top: 7px;
}
.burger-icon span:nth-child(3) {
bottom: 0;
}
.docs-aside__state:checked ~ .docs-aside__toggle .burger-icon span:nth-child(1) {
top: 7px;
left: -3px;
transform: rotate(-45deg) scaleX(0.7);
}
.docs-aside__state:checked ~ .docs-aside__toggle .burger-icon span:nth-child(2) {
transform: scale(0);
transition-duration: 0.075s;
}
.docs-aside__state:checked ~ .docs-aside__toggle .burger-icon span:nth-child(3) {
top: 7px;
left: 4px;
bottom: auto;
transform: rotate(45deg) scaleX(0.7);
}
/* ---------------------------------------------------------- docs article */
.docs {
overflow: hidden;
}
@media (min-width: 768px) {
.docs {
width: 75%;
}
}
.docs__article {
min-width: 0;
}
.docs__title {
margin: 0 0 1.5rem;
font-size: 1.5rem;
font-weight: 700;
line-height: 1.111;
color: var(--fg-heading);
}
@media (min-width: 768px) {
.docs__title {
margin-bottom: 2rem;
font-size: 2.25rem;
}
}
.docs__meta {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1rem;
margin-right: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
color: var(--fg-muted);
}
.docs__meta a {
border-bottom: 1px solid transparent;
text-decoration: none;
transition: border-color 0.15s;
}
.docs__meta a:hover {
border-bottom-color: currentColor;
}
/* -------------------------------------------------------- page and home */
.page {
flex: 1 1 auto;
max-width: 48rem;
margin-inline: auto;
}
.page__header {
padding-block: 3rem 2.5rem;
margin-bottom: 2.5rem;
border-bottom: 1px solid var(--border-soft);
text-align: center;
}
.page__title {
margin: 0 0 0.5rem;
font-size: 1.5rem;
font-weight: 700;
}
@media (min-width: 768px) {
.page__title {
font-size: 1.875rem;
}
}
.page__description {
margin: 0;
color: var(--fg-muted);
}
.home {
margin-top: 2.5rem;
margin-inline: auto;
}
.prose h1.home__title {
text-align: center;
font-weight: 800;
}
.home__signature {
text-align: right;
font-style: italic;
}
.demo-widget {
max-width: 42rem;
margin-inline: auto;
}
.not-found {
position: relative;
margin: auto;
text-align: center;
}
.not-found__code {
display: block;
margin-bottom: 1rem;
font-size: 8rem;
font-weight: 900;
line-height: 1;
color: var(--border-soft);
}
@media (min-width: 768px) {
.not-found__code {
font-size: 200px;
}
}
.not-found__text {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
font-size: 2.25rem;
font-weight: 700;
color: var(--fg);
}
/* ------------------------------------------------------------------ note */
.note {
position: relative;
display: flex;
gap: 0.75rem;
margin-block: 1.25rem;
padding: 0.75rem 1rem;
background-color: var(--bg-raised);
border-radius: var(--radius);
}
.note__icon {
flex-shrink: 0;
font-size: 1.25rem;
line-height: 1.6;
}
.note__body > :first-child {
margin-top: 0;
}
.note__body > :last-child {
margin-bottom: 0;
}
/* ----------------------------------------------------------------- prose */
.prose {
max-width: 65ch;
color: var(--fg);
line-height: 1.75;
}
.docs .prose {
max-width: 100%;
}
.prose > :first-child {
margin-top: 0;
}
.prose > :last-child {
margin-bottom: 0;
}
.prose p,
.prose ul,
.prose ol,
.prose dl,
.prose blockquote,
.prose pre,
.prose .table-wrapper {
margin-block: 1.25em;
}
.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
margin-top: 2em;
margin-bottom: 1em;
scroll-margin-top: calc(var(--header-height) + 1rem);
}
.prose h1,
.prose h2 {
color: var(--fg-heading);
}
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
color: var(--fg-subheading);
}
.prose h1 {
margin-top: 0;
font-size: 2.25em;
font-weight: 700;
line-height: 1.111;
}
.prose h2 {
font-size: 1.5em;
font-weight: 700;
line-height: 1.333;
}
.prose h3 {
font-size: 1.25em;
font-weight: 600;
line-height: 1.6;
}
.prose h4,
.prose h5,
.prose h6 {
font-size: 1em;
font-weight: 600;
line-height: 1.5;
}
.prose a {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 2px;
}
.prose a:hover {
color: var(--accent-hover);
}
.prose strong {
font-weight: 600;
color: var(--fg-subheading);
}
.prose ul,
.prose ol {
padding-left: 1.625em;
}
.prose li {
margin-block: 0.5em;
}
.prose li::marker {
color: var(--marker);
}
.prose li > ul,
.prose li > ol {
margin-block: 0.5em;
}
.prose ul {
list-style: disc;
}
.prose ol {
list-style: decimal;
}
.prose blockquote {
padding-left: 1em;
border-left: 4px solid var(--border);
color: var(--fg-muted);
font-style: italic;
}
.prose hr {
margin-block: 3em;
border: 0;
border-top: 1px solid var(--border);
}
.prose code {
font-size: 0.875em;
font-weight: 300;
color: var(--code-fg);
overflow-wrap: break-word;
}
.prose strong code {
font-weight: 700;
color: #ca8a04;
}
.dark .prose strong code {
color: #facc15;
}
.prose pre {
/* an unlabelled fence gets no .chroma wrapper, so the panel is set here and the
highlighted blocks inherit the same background from it */
background-color: var(--bg-code);
color: var(--fg);
padding: 0.85em 1.15em;
border-radius: var(--radius);
overflow-x: auto;
font-size: 0.875em;
line-height: 1.7;
}
.prose pre code {
font-size: inherit;
font-weight: inherit;
color: inherit;
background: none;
padding: 0;
}
.prose img {
margin-block: 2em;
border-radius: var(--radius);
}
.prose kbd {
padding: 0.15em 0.4em;
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 0.85em;
}
/* ---------------------------------------------------------------- tables */
.table-wrapper {
overflow-x: auto;
}
.prose table {
width: 100%;
border-collapse: collapse;
font-size: 0.875em;
line-height: 1.6;
}
.prose thead {
border-bottom: 1px solid var(--border);
color: var(--fg-subheading);
}
.prose th {
padding: 0.6em;
font-weight: 600;
text-align: left;
vertical-align: bottom;
}
.prose td {
padding: 0.6em;
vertical-align: top;
}
.prose tbody tr {
border-bottom: 1px solid var(--border-soft);
}
/* -------------------------------------------------------- header anchors */
/* an empty target carrying a heading's previous id; needs the heading's own offset so
landing on it does not put the heading under the fixed header */
.legacy-anchor {
display: block;
height: 0;
scroll-margin-top: calc(var(--header-height) + 1rem);
}
/* ------------------------------------------------- syntax highlighting */
/* generated with `hugo gen chromastyles --style=github` and `--style=github-dark`,
with each theme background swapped for the --bg-code token. the two sets are
scoped to opposite sides of the theme switch rather than layered, because the
themes do not declare the same properties on the same tokens: github styles
Punctuation and github-dark does not, and github gives Error a background that
github-dark leaves alone. Layered, either of those leaves a light value applying
on a dark page. */
:root:not(.dark) .chroma { background-color:var(--bg-code);-webkit-text-size-adjust:none; }
:root:not(.dark) .chroma .err { color:#f6f8fa;background-color:#82071e }
:root:not(.dark) .chroma .lnlinks { outline:none;text-decoration:none;color:inherit }
:root:not(.dark) .chroma .lntd { vertical-align:top;padding:0;margin:0;border:0; }
:root:not(.dark) .chroma .lntable { border-spacing:0;padding:0;margin:0;border:0; }
:root:not(.dark) .chroma .hl { background-color:#dedede }
:root:not(.dark) .chroma .lnt { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f }
:root:not(.dark) .chroma .ln { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f }
:root:not(.dark) .chroma .line { display:flex; }
:root:not(.dark) .chroma .k { color:#cf222e }
:root:not(.dark) .chroma .kc { color:#cf222e }
:root:not(.dark) .chroma .kd { color:#cf222e }
:root:not(.dark) .chroma .kn { color:#cf222e }
:root:not(.dark) .chroma .kp { color:#cf222e }
:root:not(.dark) .chroma .kr { color:#cf222e }
:root:not(.dark) .chroma .kt { color:#cf222e }
:root:not(.dark) .chroma .na { color:#1f2328 }
:root:not(.dark) .chroma .nc { color:#1f2328 }
:root:not(.dark) .chroma .no { color:#0550ae }
:root:not(.dark) .chroma .nd { color:#0550ae }
:root:not(.dark) .chroma .ni { color:#6639ba }
:root:not(.dark) .chroma .nl { color:#900;font-weight:bold }
:root:not(.dark) .chroma .nn { color:#24292e }
:root:not(.dark) .chroma .nx { color:#1f2328 }
:root:not(.dark) .chroma .nt { color:#0550ae }
:root:not(.dark) .chroma .nb { color:#6639ba }
:root:not(.dark) .chroma .bp { color:#6a737d }
:root:not(.dark) .chroma .nv { color:#953800 }
:root:not(.dark) .chroma .vc { color:#953800 }
:root:not(.dark) .chroma .vg { color:#953800 }
:root:not(.dark) .chroma .vi { color:#953800 }
:root:not(.dark) .chroma .vm { color:#953800 }
:root:not(.dark) .chroma .nf { color:#6639ba }
:root:not(.dark) .chroma .fm { color:#6639ba }
:root:not(.dark) .chroma .s { color:#0a3069 }
:root:not(.dark) .chroma .sa { color:#0a3069 }
:root:not(.dark) .chroma .sb { color:#0a3069 }
:root:not(.dark) .chroma .sc { color:#0a3069 }
:root:not(.dark) .chroma .dl { color:#0a3069 }
:root:not(.dark) .chroma .sd { color:#0a3069 }
:root:not(.dark) .chroma .s2 { color:#0a3069 }
:root:not(.dark) .chroma .se { color:#0a3069 }
:root:not(.dark) .chroma .sh { color:#0a3069 }
:root:not(.dark) .chroma .si { color:#0a3069 }
:root:not(.dark) .chroma .sx { color:#0a3069 }
:root:not(.dark) .chroma .sr { color:#0a3069 }
:root:not(.dark) .chroma .s1 { color:#0a3069 }
:root:not(.dark) .chroma .ss { color:#032f62 }
:root:not(.dark) .chroma .m { color:#0550ae }
:root:not(.dark) .chroma .mb { color:#0550ae }
:root:not(.dark) .chroma .mf { color:#0550ae }
:root:not(.dark) .chroma .mh { color:#0550ae }
:root:not(.dark) .chroma .mi { color:#0550ae }
:root:not(.dark) .chroma .il { color:#0550ae }
:root:not(.dark) .chroma .mo { color:#0550ae }
:root:not(.dark) .chroma .o { color:#0550ae }
:root:not(.dark) .chroma .ow { color:#0550ae }
:root:not(.dark) .chroma .or { color:#0550ae }
:root:not(.dark) .chroma .p { color:#1f2328 }
:root:not(.dark) .chroma .c { color:#57606a }
:root:not(.dark) .chroma .ch { color:#57606a }
:root:not(.dark) .chroma .cm { color:#57606a }
:root:not(.dark) .chroma .c1 { color:#57606a }
:root:not(.dark) .chroma .cs { color:#57606a }
:root:not(.dark) .chroma .cp { color:#57606a }
:root:not(.dark) .chroma .cpf { color:#57606a }
:root:not(.dark) .chroma .gd { color:#82071e;background-color:#ffebe9 }
:root:not(.dark) .chroma .ge { color:#1f2328 }
:root:not(.dark) .chroma .gi { color:#116329;background-color:#dafbe1 }
:root:not(.dark) .chroma .go { color:#1f2328 }
:root:not(.dark) .chroma .gl { text-decoration:underline }
:root:not(.dark) .chroma .w { color:#fff }
:root.dark .chroma { color:#e6edf3;background-color:var(--bg-code);-webkit-text-size-adjust:none; }
:root.dark .chroma .err { color:#f85149 }
:root.dark .chroma .lnlinks { outline:none;text-decoration:none;color:inherit }
:root.dark .chroma .lntd { vertical-align:top;padding:0;margin:0;border:0; }
:root.dark .chroma .lntable { border-spacing:0;padding:0;margin:0;border:0; }
:root.dark .chroma .hl { background-color:#6e7681 }
:root.dark .chroma .lnt { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679 }
:root.dark .chroma .ln { white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#6e7681 }
:root.dark .chroma .line { display:flex; }
:root.dark .chroma .k { color:#ff7b72 }
:root.dark .chroma .kc { color:#79c0ff }
:root.dark .chroma .kd { color:#ff7b72 }
:root.dark .chroma .kn { color:#ff7b72 }
:root.dark .chroma .kp { color:#79c0ff }
:root.dark .chroma .kr { color:#ff7b72 }
:root.dark .chroma .kt { color:#ff7b72 }
:root.dark .chroma .nc { color:#f0883e;font-weight:bold }
:root.dark .chroma .no { color:#79c0ff;font-weight:bold }
:root.dark .chroma .nd { color:#d2a8ff;font-weight:bold }
:root.dark .chroma .ni { color:#ffa657 }
:root.dark .chroma .ne { color:#f0883e;font-weight:bold }
:root.dark .chroma .nl { color:#79c0ff;font-weight:bold }
:root.dark .chroma .nn { color:#ff7b72 }
:root.dark .chroma .nx { color:#e6edf3 }
:root.dark .chroma .py { color:#79c0ff }
:root.dark .chroma .nt { color:#7ee787 }
:root.dark .chroma .nv { color:#79c0ff }
:root.dark .chroma .vc { color:#79c0ff }
:root.dark .chroma .vg { color:#79c0ff }
:root.dark .chroma .vi { color:#79c0ff }
:root.dark .chroma .vm { color:#79c0ff }
:root.dark .chroma .nf { color:#d2a8ff;font-weight:bold }
:root.dark .chroma .fm { color:#d2a8ff;font-weight:bold }
:root.dark .chroma .l { color:#a5d6ff }
:root.dark .chroma .ld { color:#79c0ff }
:root.dark .chroma .s { color:#a5d6ff }
:root.dark .chroma .sa { color:#79c0ff }
:root.dark .chroma .sb { color:#a5d6ff }
:root.dark .chroma .sc { color:#a5d6ff }
:root.dark .chroma .dl { color:#79c0ff }
:root.dark .chroma .sd { color:#a5d6ff }
:root.dark .chroma .s2 { color:#a5d6ff }
:root.dark .chroma .se { color:#79c0ff }
:root.dark .chroma .sh { color:#79c0ff }
:root.dark .chroma .si { color:#a5d6ff }
:root.dark .chroma .sx { color:#a5d6ff }
:root.dark .chroma .sr { color:#79c0ff }
:root.dark .chroma .s1 { color:#a5d6ff }
:root.dark .chroma .ss { color:#a5d6ff }
:root.dark .chroma .m { color:#a5d6ff }
:root.dark .chroma .mb { color:#a5d6ff }
:root.dark .chroma .mf { color:#a5d6ff }
:root.dark .chroma .mh { color:#a5d6ff }
:root.dark .chroma .mi { color:#a5d6ff }
:root.dark .chroma .il { color:#a5d6ff }
:root.dark .chroma .mo { color:#a5d6ff }
:root.dark .chroma .o { color:#ff7b72;font-weight:bold }
:root.dark .chroma .ow { color:#ff7b72;font-weight:bold }
:root.dark .chroma .or { color:#ff7b72;font-weight:bold }
:root.dark .chroma .c { color:#8b949e;font-style:italic }
:root.dark .chroma .ch { color:#8b949e;font-style:italic }
:root.dark .chroma .cm { color:#8b949e;font-style:italic }
:root.dark .chroma .c1 { color:#8b949e;font-style:italic }
:root.dark .chroma .cs { color:#8b949e;font-weight:bold;font-style:italic }
:root.dark .chroma .cp { color:#8b949e;font-weight:bold;font-style:italic }
:root.dark .chroma .cpf { color:#8b949e;font-weight:bold;font-style:italic }
:root.dark .chroma .gd { color:#ffa198;background-color:#490202 }
:root.dark .chroma .ge { font-style:italic }
:root.dark .chroma .gr { color:#ffa198 }
:root.dark .chroma .gh { color:#79c0ff;font-weight:bold }
:root.dark .chroma .gi { color:#56d364;background-color:#0f5323 }
:root.dark .chroma .go { color:#8b949e }
:root.dark .chroma .gp { color:#8b949e }
:root.dark .chroma .gs { font-weight:bold }
:root.dark .chroma .gu { color:#79c0ff }
:root.dark .chroma .gt { color:#ff7b72 }
:root.dark .chroma .gl { text-decoration:underline }
:root.dark .chroma .w { color:#6e7681 }
@@ -1,10 +1,8 @@
---
layout: home.njk
permalink: index.html
title: Remark42 Privacy-focused lightweight commenting engine
---
<h1 class="text-center !text-4xl !md:text-5xl">Privacy-focused lightweight commenting engine</h1>
<h1 class="home__title">Privacy-focused lightweight commenting engine</h1>
Remark42 allows you to have a self-hosted, lightweight, and simple (yet functional) comment engine, which doesn't spy on users. It can be embedded into blogs, articles or any other place where readers add comments.
@@ -45,4 +43,4 @@ Remark42 allows you to have a self-hosted, lightweight, and simple (yet function
- Cookie lifespan can be restricted to session-only.
- All potentially sensitive data stored by Remark42 hashed and encrypted.
<div class="text-right italic">— The Remark42 Team</div>
<div class="home__signature">— The Remark42 Team</div>
@@ -1,6 +1,5 @@
---
layout: demo.njk
permalink: demo/index.html
layout: demo
title: Demo
description: This is a page with working Remark42. Feel free to sign in and try its features.
---
+6
View File
@@ -0,0 +1,6 @@
---
title: Documentation
build:
render: never
list: never
---
+6
View File
@@ -0,0 +1,6 @@
---
title: Backup
build:
render: never
list: never
---
@@ -1,5 +1,7 @@
---
title: Automatic and Manual Backup
aliases:
- /docs/backup/
---
## Automatic
@@ -0,0 +1,6 @@
---
title: Configuration
build:
render: never
list: never
---

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

@@ -1,5 +1,7 @@
---
title: Frontend Configuration
aliases:
- /docs/configuration/
---
## Configuration
@@ -62,9 +64,9 @@ Add following snippet in the place where you want to see Remark42 widget. The co
<div id="remark42"></div>
```
::: note 💡
{{< note "💡" >}}
**Note:** The initialization script should be placed after the code mentioned above.
:::
{{< /note >}}
You can place any placeholder content inside the `remark42` div — it will be automatically removed once the comments widget has loaded. This is useful for showing a loading indicator or message while the widget initialises:
@@ -106,9 +108,9 @@ Add this snippet to the bottom of web page, or adjust already present `remark_co
</script>
```
::: note 💡
{{< note "💡" >}}
**Note:** If you want to render not only last comments widget you need to add all of the names of widget that you want to initialize.
:::
{{< /note >}}
And then add this node in the place where you want to see last comments widget:
@@ -133,9 +135,9 @@ Add this snippet to the bottom of web page, or adjust already present `remark_co
</script>
```
::: note 💡
{{< note "💡" >}}
**Note:** If you want to render not only comments widget you need to add all of the names of widget that you want to initialize.
:::
{{< /note >}}
And then add a node like this in the place where you want to see a number of comments:
@@ -1,4 +1,6 @@
## API for Single-Page Application
---
title: API for Single-Page Application
---
Add the following JavaScript to your `index.html`, which in this case, it is identical to `<script defer src="$HOST/web/embed.js"></script>`
@@ -79,7 +79,7 @@ services:
| image.resize-height | IMAGE_RESIZE_HEIGHT | `900` | height of a resized image |
| auth.ttl.jwt | AUTH_TTL_JWT | `5m` | JWT TTL |
| auth.ttl.cookie | AUTH_TTL_COOKIE | `200h` | cookie TTL |
| auth.send-jwt-header | AUTH_SEND_JWT_HEADER | `false` | send JWT as a header instead of a server-set cookie; with this enabled, frontend stores the JWT in a client-side cookie. [See security considerations](#security-considerations-for-auth.send-jwt-header). |
| auth.send-jwt-header | AUTH_SEND_JWT_HEADER | `false` | send JWT as a header instead of a server-set cookie; with this enabled, frontend stores the JWT in a client-side cookie. [See security considerations](#security-considerations-for-authsend-jwt-header). |
| auth.same-site | AUTH_SAME_SITE | `default` | set same site policy for cookies (`default`, `none`, `lax` or `strict`) |
| auth.apple.cid | AUTH_APPLE_CID | | Apple client ID (App ID or Services ID) |
| auth.apple.tid | AUTH_APPLE_TID | | Apple service ID |
+6
View File
@@ -0,0 +1,6 @@
---
title: Contributing
build:
render: never
list: never
---
@@ -1,5 +1,7 @@
---
title: Backend Development Guidelines
aliases:
- /docs/contributing/
---
You can use a fully functional local version to develop and test both frontend and backend. It requires at least 2GB RAM or swap enabled.
@@ -16,9 +18,9 @@ docker compose -f compose-private.yml up --build
It starts Remark42 on `127.0.0.1:8080` and adds local OAuth2 provider "Dev". To access the UI demo page go to <http://127.0.0.1:8080/web/>. By default, you would be logged in as `dev_user`, defined as admin. You can tweak any of the [supported parameters](https://remark42.com/docs/configuration/parameters/) in corresponded yml file.
::: note 🚨
{{< note "🚨" >}}
Please use `127.0.0.1` and not `localhost` to access the server; otherwise, CORS will prevent your browser from authentication to work correctly. You could alter the address for dev auth with the `REMARK_URL` environment variable.
:::
{{< /note >}}
Backend Docker Compose config (`compose-dev-backend.yml`) by default skips running frontend related tests. Frontend Docker Compose config (`compose-dev-frontend.yml`) by default skips running backend related tests and sets `NODE_ENV=development` for frontend build.
@@ -6,11 +6,11 @@ title: Frontend Development Guidelines
Frontend for Remark42 is built with [Preact](https://preactjs.com) and [Redux](https://redux.js.org).
::: note 💡
{{< note "💡" >}}
We highly recommend checking out Preact [documentation](https://preactjs.com/guide/v10/getting-started).
React libraries are not usable here: the store bindings and the i18n helpers are small local
modules under `app/`.
:::
{{< /note >}}
In order to inject Remark42 widgets into websites we use `iframe` and `postMessage` for communication between a site and the widget.
Simple widgets like [counter widget](https://remark42.com/docs/configuration/frontend/#counter-widget) can be injected as a script because it doesn't have its own interface.
@@ -27,9 +27,9 @@ You must have at least 2GB RAM or swap enabled for building.
Running `pnpm i` will set up pre-commit hooks into your git repository. They are used to reformat your frontend code using `prettier` and lint with `eslint` and `stylelint` before every commit.
::: note 🚨
{{< note "🚨" >}}
Please use `127.0.0.1` and not `localhost` to access the server; otherwise, CORS will prevent your browser from authentication to work correctly. You could alter the address for dev auth with the `REMARK_URL` environment variable.
:::
{{< /note >}}
## Development
@@ -45,9 +45,9 @@ You can attach the frontend to the locally running backend from `frontend/apps/r
npx cross-env REMARK_URL=http://127.0.0.1:8080 pnpm dev:custom
```
::: note
{{< note "" >}}
If you want to redefine env variables such as `PORT` on your local instance, you can add the `.env` file to the `./frontend` folder and rewrite variables as you wish. For such functional, we use `dotenv`.
:::
{{< /note >}}
### Run frontend with backend locally
@@ -78,9 +78,9 @@ It starts Remark42 backend on `127.0.0.1:8080` and adds local OAuth2 provider "D
Frontend Docker Compose config (`compose-dev-frontend.yml`) by default skips running backend related tests.
::: note 🚨
{{< note "🚨" >}}
Before submitting your changes as a Pull Request, run the backend using the `docker compose -f compose-dev-frontend.yml build --build-arg SKIP_FRONTEND_BUILD=""; docker compose -f compose-private.yml up` command and test your changes against <http://127.0.0.1:8080/web/>, frontend, built statically (unlike frontend on port 9000, which runs dynamically). That is how Remark42 authors will test your changes once you submit them.
:::
{{< /note >}}
## Static build
@@ -16,7 +16,7 @@ directory with `.json` extension and content like following:
}
```
::: note 🚨
{{< note "🚨" >}}
Translations support `{name}` placeholders and paired tags such as `<a>text</a>`. ICU plural,
select and typed-argument syntax is not supported: a message using one either falls back to
English or shows the raw syntax on the page, depending on the message. Apostrophe quoting is not
@@ -31,7 +31,7 @@ CI checks every value's tags and placeholders against the English string it tran
renders the two messages that carry a link. It cannot tell that an ICU form is unsupported,
since that is ordinary text to it, so open your translation in the interface before sending
it.
:::
{{< /note >}}
### Add a new translation
@@ -0,0 +1,6 @@
---
title: Getting Started
build:
render: never
list: never
---
@@ -1,5 +1,8 @@
---
title: Installation
aliases:
- /docs/getting-started/
- /docs/
---
## System Requirements
@@ -107,9 +110,9 @@ After that place the code snippet right after config.
```
<!-- prettier-ignore-end -->
::: note 💡
{{< 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:
+6
View File
@@ -0,0 +1,6 @@
---
title: Manuals
build:
render: never
list: never
---

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

@@ -1,4 +1,6 @@
# Using Remark42 in Astro
---
title: Using Remark42 in Astro
---
[Astro](https://astro.build/) is an all-in-one web framework for building fast, content-focused websites.
@@ -1,4 +1,6 @@
# Using Remark42 in Gatsby projects
---
title: Using Remark42 in Gatsby projects
---
## React component written in Javascript
@@ -1,5 +1,7 @@
---
title: Configure Instance without Subdomain
aliases:
- /docs/manuals/
---
## How to configure remark42 without a subdomain
@@ -1,6 +1,4 @@
---
layout: page.njk
permalink: privacy/index.html
title: Privacy Policy
---
+36
View File
@@ -0,0 +1,36 @@
{
"docs/backup/restore/index.md": {
"importrestore-without-removing-existing-comments": "import/restore-without-removing-existing-comments"
},
"docs/configuration/email/index.md": {
"email_confirmation_loginhtmltmpl--login-confirmation": "email_confirmation_login.html.tmpl-—-login-confirmation",
"email_confirmation_subscriptionhtmltmpl--subscription-confirmation": "email_confirmation_subscription.html.tmpl-—-subscription-confirmation",
"email_replyhtmltmpl--comment-notification": "email_reply.html.tmpl-—-comment-notification",
"fgribreausmtp-to-sendgrid-gateway": "fgribreau/smtp-to-sendgrid-gateway",
"mailgun-or-sendgrid-without-exposing-your-servers-ip": "mailgun-or-sendgrid-without-exposing-your-server's-ip",
"stevenolenmailgun-smtp-server": "stevenolen/mailgun-smtp-server"
},
"docs/configuration/parameters/index.md": {
"security-considerations-for-authsend-jwt-header": "security-considerations-for-auth.send-jwt-header"
},
"docs/manuals/integration-with-astro/index.md": {
"astro-component-card": "astro-component-(card)",
"astro-layout-page": "astro-layout-(page)",
"blogcardastro-partial-astro-component": "blogcard.astro-(partial-astro-component)",
"blogpagelayoutastro-partial-astro-layout": "blogpagelayout.astro-(partial-astro-layout)",
"commenttsx": "comment.tsx",
"here-how-to-use-it-in-your-astro-component": "here-how-to-use-it-in-your-astro-component.",
"remark42-countersvelte": "remark42-counter.svelte",
"remark42-embedsvelte": "remark42-embed.svelte",
"svelte-component-counter": "svelte-component-(counter)",
"svelte-component-embedded-frame": "svelte-component-(embedded-frame)",
"w-reactpreact-components": "w/-react/preact-components",
"w-svelte-components": "w/-svelte-components"
},
"docs/manuals/separate-domain/index.md": {
"what-doesnt-work-so-far": "what-doesn't-work-so-far?"
},
"privacy.md": {
"childrens-privacy": "childrens-privacy"
}
}
+6 -4
View File
@@ -12,10 +12,12 @@ services:
max-size: '10m'
max-file: '5'
# without this the bind mount below leaves root-owned public/ and resources/
# in the checkout on linux; docker desktop remaps ownership and hides it
user: '${UID:-1000}:${GID:-1000}'
ports:
- '3001:3001'
- '8080:8080'
- '1313:1313'
volumes:
- ./src:/site/src
- ./build:/site/build
- ./:/site
+39
View File
@@ -0,0 +1,39 @@
baseURL = "https://remark42.com/"
locale = "en-us"
title = "Remark42"
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
# off by default: the image build context is site/ alone, so there is no .git for
# hugo to read and it fails rather than degrading. HUGO_ENABLEGITINFO=true turns it
# on for a local build, where it gives real per-page commit dates instead of mtimes
enableGitInfo = false
[frontmatter]
# without this lastmod resolves to .Date, which is zero when a page carries no
# date, and every docs page renders "Jan 01, 0001"
lastmod = [":git", "lastmod", ":fileModTime", ":default"]
[params]
description = "Remark42 is a self-hosted, lightweight, and simple (yet functional) comment engine, which doesn't spy on users. It can be embedded into blogs, articles or any other place where readers add comments."
githubUrl = "https://github.com/umputun/remark42"
githubApiUrl = "https://api.github.com/repos/umputun/remark42"
githubBranch = "master"
remark42Host = "https://demo.remark42.com"
[markup.goldmark.renderer]
# the docs embed raw html, and a single newline is a line break as it was before
unsafe = true
hardWraps = true
[markup.goldmark.extensions]
# quotes and dashes stay as written: the docs quote shell snippets inline,
# where a curly quote would be wrong
typographer = false
[markup.highlight]
# emit classes rather than inline styles, so the stylesheet can theme them
noClasses = false
# an unlabelled fence renders plain. guessing gets it wrong: a systemd unit is
# detected as gdscript and an env-var block as mysql, and colouring a snippet
# as the wrong language is worse than not colouring it
guessSyntax = false
+6
View File
@@ -0,0 +1,6 @@
{{ define "main" }}
<h1 class="not-found">
<span class="not-found__code">404</span>
<span class="not-found__text">Page Not Found</span>
</h1>
{{ end }}
+12
View File
@@ -0,0 +1,12 @@
{{- /* markdown-it built anchors that kept punctuation goldmark strips, so a heading whose id
changed also carries its previous one as an empty target. data/anchor_aliases.json maps
content path -> current anchor -> previous anchor. */ -}}
{{- $legacy := "" -}}
{{- with .Page.File -}}
{{- with index (hugo.Data.anchor_aliases | default dict) .Path -}}
{{- $legacy = index . $.Anchor -}}
{{- end -}}
{{- end -}}
{{- with $legacy }}<span id="{{ . }}" class="legacy-anchor"></span>
{{ end -}}
<h{{ .Level }} id="{{ .Anchor }}">{{ .Text }}</h{{ .Level }}>
+26
View File
@@ -0,0 +1,26 @@
<div class="table-wrapper">
<table>
{{- with .THead }}
<thead>
{{- range . }}
<tr>
{{- range . }}
<th{{ with .Alignment }} style="text-align: {{ . }}"{{ end }}>{{ .Text }}</th>
{{- end }}
</tr>
{{- end }}
</thead>
{{- end }}
{{- with .TBody }}
<tbody>
{{- range . }}
<tr>
{{- range . }}
<td{{ with .Alignment }} style="text-align: {{ . }}"{{ end }}>{{ .Text }}</td>
{{- end }}
</tr>
{{- end }}
</tbody>
{{- end }}
</table>
</div>
+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html class="h-full" lang="en">
{{ partial "head.html" . }}
<body>
{{ partial "header.html" . }}
<main id="main" class="main container">
{{ block "main" . }}{{ end }}
</main>
{{ partial "footer.html" . }}
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
{{ define "main" }}
<article class="page">
<header class="page__header">
<h1 class="page__title" id="{{ .Title | anchorize }}">{{ .Title }}</h1>
{{ with .Description }}<p class="page__description">{{ . }}</p>{{ end }}
</header>
<div class="prose">{{ .Content }}</div>
<div id="remark42" class="demo-widget"></div>
<noscript>You use your browser with disabled JavaScript. Please enable JavaScript for comments.</noscript>
<script>
window.remark_config = {
host: {{ site.Params.remark42Host }},
site_id: 'remark',
components: ['embed'],
url: {{ .Permalink }},
max_shown_comments: 20,
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light'
}
</script>
<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>
</article>
{{ end }}
+13
View File
@@ -0,0 +1,13 @@
{{ define "main" }}
{{ partial "docs-aside.html" . }}
<section class="docs">
<article class="docs__article">
<h1 class="docs__title" id="{{ .Title | anchorize }}">{{ .Title }}</h1>
<div class="prose">{{ .Content }}</div>
<footer class="docs__meta">
<span>Updated <time datetime="{{ .Lastmod.Format "2006-01-02" }}">{{ .Lastmod.Format "Jan 02, 2006" }}</time></span>
<a target="_blank" rel="noopener noreferrer" href="{{ site.Params.githubUrl }}/edit/{{ site.Params.githubBranch }}/site/content/{{ with .File }}{{ .Path }}{{ end }}">Edit</a>
</footer>
</article>
</section>
{{ end }}
+3
View File
@@ -0,0 +1,3 @@
{{ define "main" }}
<article class="prose home">{{ .Content }}</article>
{{ end }}
+9
View File
@@ -0,0 +1,9 @@
{{ define "main" }}
<article class="page">
<header class="page__header">
<h1 class="page__title" id="{{ .Title | anchorize }}">{{ .Title }}</h1>
{{ with .Description }}<p class="page__description">{{ . }}</p>{{ end }}
</header>
<div class="prose">{{ .Content }}</div>
</article>
{{ end }}
+29
View File
@@ -0,0 +1,29 @@
<aside class="docs-aside">
<input id="menu-toggle" type="checkbox" class="docs-aside__state">
<label for="menu-toggle" class="docs-aside__toggle">
<span class="burger-icon" aria-hidden="true"><span></span><span></span><span></span></span>
Open Menu
</label>
<nav class="docs-aside__nav" aria-label="Documentation">
{{- $current := .RelPermalink -}}
{{- range hugo.Data.nav }}
{{- with .section }}<h5 class="docs-aside__section">{{ . }}</h5>{{ end }}
<ul class="docs-aside__list">
{{- range .children }}
{{- $href := printf "/docs%s" .href }}
<li>
<a class="docs-aside__link{{ if eq $current $href }} is-current{{ end }}" href="{{ $href }}">{{ .title }}</a>
{{- with .children }}
<ul class="docs-aside__list docs-aside__list--nested">
{{- range . }}
{{- $childHref := printf "/docs%s" .href }}
<li><a class="docs-aside__link{{ if eq $current $childHref }} is-current{{ end }}" href="{{ $childHref }}">{{ .title }}</a></li>
{{- end }}
</ul>
{{- end }}
</li>
{{- end }}
</ul>
{{- end }}
</nav>
</aside>
+3
View File
@@ -0,0 +1,3 @@
<footer class="site-footer">
Remark42 {{ now.Year }} | <a href="/privacy/">Privacy Policy</a>
</footer>
+16
View File
@@ -0,0 +1,16 @@
{{- $style := resources.Get "styles.css" | resources.Minify | resources.Fingerprint -}}
{{- $script := resources.Get "script.js" | resources.Minify | resources.Fingerprint -}}
{{- $inline := resources.Get "inline.js" | resources.ExecuteAsTemplate "inline.js" . | resources.Minify -}}
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<title>{{ with .Title }}{{ . }} | {{ site.Title }}{{ else }}{{ site.Title }}{{ end }}</title>
<meta name="description" content="{{ with .Params.metaDescription }}{{ . }}{{ else }}{{ with .Description }}{{ . }}{{ else }}{{ site.Params.description }}{{ end }}{{ end }}">
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
<script>{{ $inline.Content | safeJS }}</script>
<script defer src="{{ $script.RelPermalink }}"></script>
</head>
+22
View File
@@ -0,0 +1,22 @@
<header class="site-header">
<div class="container site-header__inner">
<a class="site-header__brand" href="/">{{ site.Title }}</a>
<nav class="site-header__nav" aria-label="Main">
<a href="/demo/">Demo</a>
<a href="/docs/getting-started/installation/">Docs</a>
</nav>
<div class="site-header__actions">
<a class="site-header__icon" aria-label="{{ site.Title }} releases on GitHub" rel="noopener noreferrer" href="{{ site.Params.githubUrl }}/releases" target="_blank">
<span class="site-header__version" data-remark42-version hidden></span>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path fill="currentColor" d="M12 3C7.0275 3 3 7.12937 3 12.2276C3 16.3109 5.57625 19.7597 9.15374 20.9824C9.60374 21.0631 9.77249 20.7863 9.77249 20.5441C9.77249 20.3249 9.76125 19.5982 9.76125 18.8254C7.5 19.2522 6.915 18.2602 6.735 17.7412C6.63375 17.4759 6.19499 16.6569 5.8125 16.4378C5.4975 16.2647 5.0475 15.838 5.80124 15.8264C6.51 15.8149 7.01625 16.4954 7.18499 16.7723C7.99499 18.1679 9.28875 17.7758 9.80625 17.5335C9.885 16.9337 10.1212 16.53 10.38 16.2993C8.3775 16.0687 6.285 15.2728 6.285 11.7432C6.285 10.7397 6.63375 9.9092 7.20749 9.26326C7.1175 9.03257 6.8025 8.08674 7.2975 6.81794C7.2975 6.81794 8.05125 6.57571 9.77249 7.76377C10.4925 7.55615 11.2575 7.45234 12.0225 7.45234C12.7875 7.45234 13.5525 7.55615 14.2725 7.76377C15.9937 6.56418 16.7475 6.81794 16.7475 6.81794C17.2424 8.08674 16.9275 9.03257 16.8375 9.26326C17.4113 9.9092 17.76 10.7281 17.76 11.7432C17.76 15.2843 15.6563 16.0687 13.6537 16.2993C13.98 16.5877 14.2613 17.1414 14.2613 18.0065C14.2613 19.2407 14.25 20.2326 14.25 20.5441C14.25 20.7863 14.4188 21.0746 14.8688 20.9824C16.6554 20.364 18.2079 19.1866 19.3078 17.6162C20.4077 16.0457 20.9995 14.1611 21 12.2276C21 7.12937 16.9725 3 12 3Z"></path>
</svg>
</a>
<button class="site-header__theme" title="Toggle Site Theme" aria-label="Toggle Site Theme" onclick="toggleTheme()">
<svg width="28" height="28" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.67 8.658a3.661 3.661 0 0 0-.781 1.114 3.28 3.28 0 0 0-.268 1.329v1.6a1.304 1.304 0 0 1-.794 1.197 1.282 1.282 0 0 1-.509.102H7.712a1.285 1.285 0 0 1-.922-.379 1.303 1.303 0 0 1-.38-.92v-1.6c0-.479-.092-.921-.274-1.329a3.556 3.556 0 0 0-.776-1.114 4.689 4.689 0 0 1-1.006-1.437A4.187 4.187 0 0 1 4 5.5a4.432 4.432 0 0 1 .616-2.27c.197-.336.432-.64.705-.914a4.6 4.6 0 0 1 .911-.702c.338-.196.7-.348 1.084-.454a4.45 4.45 0 0 1 1.2-.16 4.476 4.476 0 0 1 2.276.614 4.475 4.475 0 0 1 1.622 1.616 4.438 4.438 0 0 1 .616 2.27c0 .617-.117 1.191-.353 1.721a4.69 4.69 0 0 1-1.006 1.437zM9.623 10.5H7.409v2.201c0 .081.028.15.09.212a.29.29 0 0 0 .213.09h1.606a.289.289 0 0 0 .213-.09.286.286 0 0 0 .09-.212V10.5z"></path>
</svg>
</button>
</div>
</div>
</header>
+5
View File
@@ -0,0 +1,5 @@
{{- $icon := .Get 0 | default "💡" -}}
<aside class="note">
<span class="note__icon" aria-hidden="true">{{ $icon }}</span>
<div class="note__body">{{ .Inner | .Page.RenderString (dict "display" "block") }}</div>
</aside>
-51
View File
@@ -1,51 +0,0 @@
{
"name": "remark42-site",
"version": "0.0.0",
"repository": "https://github.com/umputun/remark42/site",
"homepage": "https://remark42.com",
"author": "Pavel Mineev <pavel@mineev.me>",
"license": "MIT",
"private": true,
"engines": {
"node": ">=20.11.1",
"yarn": ">=1.22"
},
"packageManager": "yarn@1.22.22",
"scripts": {
"start": "npm-run-all dev",
"dev": "npm-run-all -l clean build:css -p dev:css dev:11ty",
"dev:css": "tailwindcss -i ./src/styles.css -o .tmp/style.css -w",
"dev:11ty": "eleventy --serve --watch --quiet",
"build": "cross-env NODE_ENV=production run-s clean build:css build:11ty --print-label",
"build:css": "tailwindcss -i ./src/styles.css -o .tmp/style.css",
"build:11ty": "eleventy",
"clean": "rm -rf .tmp/* build/*"
},
"devDependencies": {
"@11ty/eleventy": "^2.0.1",
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
"@tailwindcss/typography": "^0.5.20",
"cross-env": "^10.1.0",
"date-fns": "^4.4.0",
"markdown-it": "^14.2.0",
"markdown-it-anchor": "^9.2.0",
"markdown-it-container": "^4.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.9.4",
"tailwindcss": "^3.4.15",
"html-minifier-terser": "^7.2.0"
},
"resolutions": {
"cross-spawn": ">=7.0.6",
"minimatch": ">=3.1.4",
"shell-quote": ">=1.8.4",
"brace-expansion": ">=1.1.12",
"glob": ">=10.5.0 <11.0.0",
"liquidjs": ">=10.26.0",
"linkify-it": ">=5.0.2 <6.0.0",
"ws": ">=8.21.0",
"postcss": ">=8.5.23 <9.0.0",
"yaml": ">=2.8.3",
"markdown-it": ">=14.2.0"
}
}
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
-2
View File
@@ -1,2 +0,0 @@
User-agent: *
Disallow: /404/index.html
-10
View File
@@ -1,10 +0,0 @@
{
"name": "Remark42",
"subtitle": "Privacy focused lightweight commenting system",
"description": "Remark42 is a self-hosted, lightweight, and simple (yet functional) comment engine, which doesn't spy on users. It can be embedded into blogs, articles or any other place where readers add comments.",
"url": "https://remark42.com",
"githubUrl": "https://github.com/umputun/remark42",
"githubApiUrl": "https://api.github.com/repos/umputun/remark42",
"githubBranch": "master",
"remark42Host": "https://demo.remark42.com"
}
-4
View File
@@ -1,4 +0,0 @@
---
layout: redirect.njk
destination: /docs/backup/backup/
---
-4
View File
@@ -1,4 +0,0 @@
---
layout: redirect.njk
destination: /docs/configuration/frontend/
---
-4
View File
@@ -1,4 +0,0 @@
---
layout: redirect.njk
destination: /docs/contributing/development/
---
-4
View File
@@ -1,4 +0,0 @@
module.exports = {
nav: require('./nav.json'),
layout: 'docs.njk',
}
-4
View File
@@ -1,4 +0,0 @@
---
layout: redirect.njk
destination: /docs/getting-started/installation/
---
-4
View File
@@ -1,4 +0,0 @@
---
layout: redirect.njk
destination: /docs/manuals/subdomain/
---
@@ -1,42 +0,0 @@
{% macro navItem(entry) %}
<li class="my-1">
<a class="py-1 px-2 -mx-2 block rounded text-gray-600 dark:text-gray-300 {% if page.url === '/docs' + entry.href %} bg-brand-100 dark:bg-brand-900 text-brand-600 dark:text-brand-400 hover:text-brand-600{% else %} hover:text-gray-900 dark:hover:text-gray-400{% endif %}" href="/docs{{ entry.href | url }}">
{{ entry.title }}
</a>
{% if entry.children %}
<ul class="ml-4 font-normal">
{% for entry in entry.children %}
<li class="my-1">
<a class="py-1 px-2 -mx-2 block rounded text-gray-600 dark:text-gray-300 {% if page.url === '/docs' + entry.href %} bg-brand-100 dark:bg-brand-900 text-brand-600 dark:text-brand-400 hover:text-brand-600{% else %} hover:text-gray-900 dark:hover:text-gray-400{% endif %}" href="/docs{{ entry.href | url }}">
{{ entry.title }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endmacro %}
<aside class="flex-shrink-0 top-24 md:top-0 md:relative w-full md:w-1/4 max-w-[260px]">
<input id="menu-toggle" type="checkbox" class="menu-state absolute invisible h-0 w-0 md:hidden"/>
<label for="menu-toggle" aria-title="Open Menu" class="flex items-center md:hidden py-2 border-t border-b border-gray-100 dark:border-gray-800 text-gray-500 dark:text-gray-300 mb-8 font-medium">
<div class="burger-icon" role="icon">
<div></div>
<div></div>
<div></div>
</div>
Open Menu
</label>
<nav class="hidden md:block mr-10">
{% for entry in nav %}
{% if entry.section %}
<h5 class="uppercase font-semibold text-sm text-gray-400 mb-2">{{ entry.section }}</h5>
{% endif %}
<ul class="mb-8 font-medium">
{% for entry in entry.children %}
{{ navItem(entry) }}
{% endfor %}
</ul>
{% endfor %}
</nav>
</aside>
-3
View File
@@ -1,3 +0,0 @@
<footer class="mt-6 py-4 text-center text-sm">
Remark42 {% year %} | <a href="/privacy/" class="opacity-70 underline">Privacy Policy</a>
</footer>
-20
View File
@@ -1,20 +0,0 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
{% if title %}
<title>{{ title }} | {{ site.name }}</title>
{% else %}
<title>{{ renderData.title or site.name }}</title>
{% endif %}
<meta name="description" content="{{ metaDescription or renderData.metaDescription or site.description }}">
<link rel="stylesheet" href="/style.css?v={% version %}"/>
<script>{% include "inline.njk" %}</script>
{% set js %}
{% include "script.js" %}
{% endset %}
<script defer>{{ js | safe }}</script>
</head>
-24
View File
@@ -1,24 +0,0 @@
<header class="md:py-2 bg-white dark:bg-neutral-900 bg-opacity-80 dark:bg-opacity-60 backdrop-filter backdrop-blur-sm z-20 fixed top-0 left-0 right-0 text-gray-600 dark:text-gray-300">
<div class="container flex flex-wrap items-center">
<a class="order-1 py-1 mt-1 mr-4 no-underline text-2xl font-extrabold transition-colors text-brand-500 hover:text-brand-600 dark:text-brand-400" href="/">
{{ site.name }}
</a>
<nav class="order-3 md:order-2 py-1 mt-1 w-full md:w-auto flex-shrink-0 space-x-2">
<a class="inline-block leading-5 font-medium border-b dark:border-gray-600 transition hover:text-brand-600 dark:hover:text-brand-300 hover:border-brand-300 dark:hover:border-brand-500" href="/demo/">Demo</a>
<a class="inline-block leading-5 font-medium border-b dark:border-gray-600 transition hover:text-brand-600 dark:hover:text-brand-300 hover:border-brand-300 dark:hover:border-brand-500" href="/docs/getting-started/installation/">Docs</a>
</nav>
<div class="order-2 md:order-3 flex space-x-2 ml-auto py-1 px-4">
<a class="hover:text-gray-800 dark:hover:text-gray-300 flex items-center" aria-label="{{ site.name }} releases on GitHub" rel="noopener noreferrer" href="{{ site.githubUrl }}/releases" target="_blank">
<span class="mr-2" data-remark42-version hidden></span>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<path fill="currentColor" d="M12 3C7.0275 3 3 7.12937 3 12.2276C3 16.3109 5.57625 19.7597 9.15374 20.9824C9.60374 21.0631 9.77249 20.7863 9.77249 20.5441C9.77249 20.3249 9.76125 19.5982 9.76125 18.8254C7.5 19.2522 6.915 18.2602 6.735 17.7412C6.63375 17.4759 6.19499 16.6569 5.8125 16.4378C5.4975 16.2647 5.0475 15.838 5.80124 15.8264C6.51 15.8149 7.01625 16.4954 7.18499 16.7723C7.99499 18.1679 9.28875 17.7758 9.80625 17.5335C9.885 16.9337 10.1212 16.53 10.38 16.2993C8.3775 16.0687 6.285 15.2728 6.285 11.7432C6.285 10.7397 6.63375 9.9092 7.20749 9.26326C7.1175 9.03257 6.8025 8.08674 7.2975 6.81794C7.2975 6.81794 8.05125 6.57571 9.77249 7.76377C10.4925 7.55615 11.2575 7.45234 12.0225 7.45234C12.7875 7.45234 13.5525 7.55615 14.2725 7.76377C15.9937 6.56418 16.7475 6.81794 16.7475 6.81794C17.2424 8.08674 16.9275 9.03257 16.8375 9.26326C17.4113 9.9092 17.76 10.7281 17.76 11.7432C17.76 15.2843 15.6563 16.0687 13.6537 16.2993C13.98 16.5877 14.2613 17.1414 14.2613 18.0065C14.2613 19.2407 14.25 20.2326 14.25 20.5441C14.25 20.7863 14.4188 21.0746 14.8688 20.9824C16.6554 20.364 18.2079 19.1866 19.3078 17.6162C20.4077 16.0457 20.9995 14.1611 21 12.2276C21 7.12937 16.9725 3 12 3Z"></path>
</svg>
</a>
<button class="hover:text-yellow-400" title="Toggle Site Theme" onclick="toggleTheme()">
<svg width="28" height="28" viewBox="0 0 16 16" fill="currentColor">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.67 8.658a3.661 3.661 0 0 0-.781 1.114 3.28 3.28 0 0 0-.268 1.329v1.6a1.304 1.304 0 0 1-.794 1.197 1.282 1.282 0 0 1-.509.102H7.712a1.285 1.285 0 0 1-.922-.379 1.303 1.303 0 0 1-.38-.92v-1.6c0-.479-.092-.921-.274-1.329a3.556 3.556 0 0 0-.776-1.114 4.689 4.689 0 0 1-1.006-1.437A4.187 4.187 0 0 1 4 5.5a4.432 4.432 0 0 1 .616-2.27c.197-.336.432-.64.705-.914a4.6 4.6 0 0 1 .911-.702c.338-.196.7-.348 1.084-.454a4.45 4.45 0 0 1 1.2-.16 4.476 4.476 0 0 1 2.276.614 4.475 4.475 0 0 1 1.622 1.616 4.438 4.438 0 0 1 .616 2.27c0 .617-.117 1.191-.353 1.721a4.69 4.69 0 0 1-1.006 1.437zM9.623 10.5H7.409v2.201c0 .081.028.15.09.212a.29.29 0 0 0 .213.09h1.606a.289.289 0 0 0 .213-.09.286.286 0 0 0 .09-.212V10.5z"></path>
</svg>
</button>
</div>
</div>
</header>
-8
View File
@@ -1,8 +0,0 @@
---
layout: base.njk
---
<h1 class="m-auto text-center relative">
<div class="text-9xl md:text-[200px] font-black mb-4 text-gray-100">404</div>
<div class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 whitespace-nowrap text-4xl font-bold text-gray-800">Page Not Found</div>
</h2>
-11
View File
@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html {% if section %} data-current="{{ section }}"{% endif %} class="h-full" lang="en">
{% include "components/head.njk" %}
<body class="flex flex-col min-h-full bg-white dark:bg-neutral-900 dark:text-gray-100">
{% include "components/header.njk" %}
<main id="main" class="relative pt-24 md:pt-20 container flex-grow min-h-full flex flex-wrap">
{{ layoutContent | safe }}
</main>
{% include "components/footer.njk" %}
</body>
</html>
-19
View File
@@ -1,19 +0,0 @@
---
layout: page.njk
---
{{ content | safe }}
<div id="remark42" class="max-w-2xl mx-auto"></div>
<noscript>You use your browser with disabled JavaScript. Please enable JavaScript for comments.</noscript>
<script>
window.remark_config = {
host: '{{ site.remark42Host }}',
site_id: 'remark',
components: ['embed'],
url: '{{ site.url }}/demo/',
max_shown_comments: 20,
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light'
}
</script>
<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>
-32
View File
@@ -1,32 +0,0 @@
---
layout: base.njk
---
{% include "components/docs-aside.njk" %}
<section class="md:w-3/4 overflow-hidden">
<article class="flex flex-row flex-wrap">
{# TODO: implement table of content
<aside class="order-2 mt-4">
<nav aria-labelledby="table-of-content">
<h3 class="uppercase font-semibold text-gray-600 text-sm" id="table-of-content">On this page</h3>
<ol></ol>
</nav>
</aside>
#}
<div class="order-1 max-w-full flex-grow">
<h1 class="text-2xl md:text-4xl font-bold mb-6 md:mb-8 text-brand-900 dark:text-brand-100">{{ title }}</h1>
<div class="prose dark:prose-dark max-w-full">{{ layoutContent | safe }}</div>
</div>
<footer class="order-3 min-w-full flex mt-4 justify-end mr-1 text-sm text-gray-500 dark:text-gray-300 space-x-3 leading-5">
<span>
Updated <time datetime="{{ page.date | robotizeDate }}">{{ page.date | humanizeDate }}</time>
</span>
<a class="border-b border-transparent transition-colors hover:border-current" target="_blank" rel="noopener noreferrer" href="{{ site.githubUrl }}/edit/{{ site.githubBranch }}/site/{{ page.inputPath }}">Edit</a>
</footer>
</article>
{# TODO: implement navigation to next page of the docs
<nav class="flex justify-between mt-10 mb-8 p-2 border-t dark:border-gray-700 text-gray-800 dark:text-gray-200">
<a>Prev page</a>
<a>Next page</a>
</nav> #}
</section>
-7
View File
@@ -1,7 +0,0 @@
---
layout: base.njk
---
<article class="prose dark:prose-dark mx-auto mt-10">
{{ content |safe }}
</article>
-13
View File
@@ -1,13 +0,0 @@
---
layout: base.njk
---
<article class="mx-auto max-w-3xl flex-grow">
<header class="pt-12 pb-10 mb-10 border-b border-gray-100 dark:border-gray-600 text-center">
<h1 class="text-2xl md:text-3xl font-bold mb-2">{{ title }}</h1>
<p class="text-gray-500 dark:text-gray-400">{{ description }}</p>
</header>
<div class="mx-auto prose dark:prose-dark">
{{ content | safe }}
</div>
</article>
-13
View File
@@ -1,13 +0,0 @@
{% if not destination %}
{% set destination = redirect.destination %}
{% endif %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url='{{ destination }}'" />
</head>
<body>
you should be redirecting to <a href="{{ destination }}">{{ destination }}</a>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More