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.
@@ -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:
|
||||
- "*"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
../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.
|
||||
@@ -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.
|
||||
@@ -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 @@
|
||||
../../../site/src/docs/contributing/frontend/index.md
|
||||
../../../site/content/docs/contributing/frontend/index.md
|
||||
@@ -1,5 +1,9 @@
|
||||
node_modules
|
||||
.git
|
||||
.gitignore
|
||||
.prettierrc
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
Dockerfile.dev
|
||||
docker-compose.yml
|
||||
build
|
||||
public
|
||||
resources
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
build
|
||||
node_modules
|
||||
yarn-*.log
|
||||
@@ -1,4 +1,4 @@
|
||||
node_modules
|
||||
yarn-*.log
|
||||
.tmp
|
||||
public
|
||||
build
|
||||
resources
|
||||
.hugo_build.lock
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
20
|
||||
@@ -1,3 +0,0 @@
|
||||
build
|
||||
public/site.webmanifest
|
||||
src/docs/configuration/parameters/index.md
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "always",
|
||||
"useTabs": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.md",
|
||||
"options": {
|
||||
"singleQuote": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
---
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Documentation
|
||||
build:
|
||||
render: never
|
||||
list: never
|
||||
---
|
||||
@@ -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 |
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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": "children’s-privacy"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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 }}>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -0,0 +1,3 @@
|
||||
{{ define "main" }}
|
||||
<article class="prose home">{{ .Content }}</article>
|
||||
{{ end }}
|
||||
@@ -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 }}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
<footer class="site-footer">
|
||||
Remark42 {{ now.Year }} | <a href="/privacy/">Privacy Policy</a>
|
||||
</footer>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow: /404/index.html
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
layout: redirect.njk
|
||||
destination: /docs/backup/backup/
|
||||
---
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
layout: redirect.njk
|
||||
destination: /docs/configuration/frontend/
|
||||
---
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
layout: redirect.njk
|
||||
destination: /docs/contributing/development/
|
||||
---
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
nav: require('./nav.json'),
|
||||
layout: 'docs.njk',
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
layout: redirect.njk
|
||||
destination: /docs/getting-started/installation/
|
||||
---
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
layout: base.njk
|
||||
---
|
||||
|
||||
<article class="prose dark:prose-dark mx-auto mt-10">
|
||||
{{ content |safe }}
|
||||
</article>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||