No description
  • Go 56.8%
  • TypeScript 39.1%
  • CSS 2.4%
  • HTML 1.6%
Find a file
Dominik Polakovics Polakovics 5123337df5
All checks were successful
govulncheck / govulncheck (push) Successful in 1m38s
ci / build-and-test (push) Successful in 4m38s
ci / release (push) Successful in 1m25s
Merge pull request 'feat(preview): starting state with clock, size and Hugo output; startedAt; Hugo before the startup sync' (#366) from afk/362 into main
2026-11-01 02:27:39 +01:00
.forgejo/workflows ci: install the latest Go 1.26 patch release in the workflows 2026-10-23 06:20:56 +00:00
cmd/harness Merge pull request 'feat(preview): starting state with clock, size and Hugo output; startedAt; Hugo before the startup sync' (#366) from afk/362 into main 2026-11-01 02:27:39 +01:00
docs Merge pull request 'feat(preview): starting state with clock, size and Hugo output; startedAt; Hugo before the startup sync' (#366) from afk/362 into main 2026-11-01 02:27:39 +01:00
internal Merge pull request 'feat(preview): starting state with clock, size and Hugo output; startedAt; Hugo before the startup sync' (#366) from afk/362 into main 2026-11-01 02:27:39 +01:00
scaffold fix(theme): compute site-wide selections once per build, not once per page 2026-11-01 00:39:45 +00:00
testdata Merge branch 'main' into afk/195 2026-10-15 15:11:21 +00:00
web Merge pull request 'feat(preview): starting state with clock, size and Hugo output; startedAt; Hugo before the startup sync' (#366) from afk/362 into main 2026-11-01 02:27:39 +01:00
.gitignore feat: add testsite to gitignore 2026-07-16 00:43:48 +02:00
CLAUDE.md docs: add styleguide usage to ai instructions 2026-10-17 22:11:28 +02:00
go.mod fix(server): tag the poll answers by their bytes and answer 304 to a matching If-None-Match 2026-11-01 01:03:53 +00:00
go.sum deps: bump golang.org/x/net to v0.61.0 and golang.org/x/image to v0.47.0 2026-10-22 08:49:28 +00:00
Makefile docs: rewrite README for v1; pin repo CI Hugo; spec-align alt requirement 2026-07-13 06:55:49 +02:00
README.md ci: daily govulncheck scan reported to CVE Watch 2026-10-21 17:28:11 +00:00

deckle

Deckle turns a plain Hugo site in git into a safe CMS for a non-technical editor. It ships as one Go binary — harness — that supervises hugo server for a live preview and serves an embedded editing UI, while a content engine and a set of per-element constraints keep the editor from ever producing markup the site cannot build.

It serves two audiences from one repository:

  • The developer owns a normal Hugo site: templates, a theme, config, and content files in git. Nothing about deckle is proprietary — the repository is vanilla Hugo, builds with stock hugo, and deploys however you like. Remove the harness and the site still builds.
  • The client editor gets a locked-down editing surface. They can only place the elements the developer declared, fill the fields the developer defined, and write rich text within a per-field allowlist. They never touch templates, never write raw HTML, and cannot leave the site in a state that fails to build: every publish is gated on a real Hugo build first.

There is no lock-in in either direction. The editing constraints live in plain YAML sidecar files next to the content; the content stays canonical Hugo markdown; and the whole editing session is ordinary git commits on main.

Quick start

make build                         # build ./harness (frontend + Go binary),
                                   #   or download a release (below)
./harness version                  # "dev" for a local build, else the release tag
./harness init --site mysite       # scaffold a complete Hugo site: theme,
                                   #   element library, page types, CI workflows
./harness serve --site mysite      # supervise Hugo and open the editor

serve prints the editor's address and, once Hugo answers, the preview's; see Serving for its output and the other commands.

Releases

Without a checkout, take the harness from a release. CI cuts one on every push to main that passes its build and tests, so the latest release is always main's head; there are no hand-made tags. A release's tag is its version — v<YYYYMMDD>.<HHMMSS>-<sha7>, the commit's UTC time and short hash (e.g. v20261011.143005-0c86c9a) — and is what that binary's harness version prints. Each release carries one bare executable and a checksums file:

Asset Host
harness-linux-amd64 Linux, x86-64
SHA256SUMS sha256sum format, one line per binary

The binary is fully static (CGO_ENABLED=0 -tags nodynamic -trimpath; nodynamic keeps the AVIF decoder's optional purego path from linking dynamically). Linux x86-64 is the only release target — the host every site's agent and deploy runs on — which keeps the release job to a single build; other hosts build from source (go build ./cmd/harness, after npm run build in web/). The repository is public: the release API (https://git.cloonar.com/api/v1/repos/Cloonar/deckle/releases/latest, whose tag_name is the latest tag) and the downloads (https://git.cloonar.com/Cloonar/deckle/releases/download/<tag>/<asset>) need no token.

Every scaffolded site documents how its agent gets and updates the harness: the "Installing the harness" section of the site's AGENTS.md (from scaffold/site/AGENTS.md) is a short shell procedure that compares ~/.deckle/harness/harness version with the latest tag and, only when they differ, downloads the binary, verifies it against SHA256SUMS and moves it into ~/.deckle/harness/harness. Sites never pin a harness version — they follow the latest release — and nothing is linked onto PATH; a harness already on PATH (a developer's own build) is used instead.

Documentation

Every fact has one owning document; this README only points at them (docs/agents/docs-map.md says which owns what).

Document For
docs/harness.md the CLI: every command and flag, exit codes, --json output, the receipt, harness.yaml
docs/api.md the HTTP API the editor talks to: one section per route, coded errors, translations on the wire
docs/host-navigation.md the versioned contract for a host that proxies the editor
docs/features.md scaffolding and feature bundles: manifest, dependencies, install, remove, the shipped bundles
docs/architecture.md how it works: content engine, sidecars, git backbone, branch mode, publish, multilingual sites
docs/adr/ architecture decision records
scaffold/site/AGENTS.md the site manual: sidecars, field types, body policies, placement, content grammar
scaffold/themes/deckle/README.md the theme's template contract

Commands

One line per command, as harness prints it (a test keeps the two equal); docs/harness.md is the reference for every flag, exit code and --json shape.

Command Does
harness init scaffold a new deckle site into an empty directory
harness features list the embedded feature bundles (and, with --site, their installed state)
harness add vendor a feature bundle (embedded, e.g. news, or --dir) onto an existing site
harness remove uninstall a feature bundle; refuses while it is in use unless --purge
harness lang add a content language to the site, or remove one (refuses while it has translations unless --purge)
harness reconcile rebase the draft branch onto main with the publish policy (host use; branch mode only)
harness serve supervise a Hugo dev server and serve the harness UI
harness sidecar scaffold a draft element sidecar from an element template
harness check report sidecar, template and content problems without starting the editor
harness migrate rewrite every sidecar and language file to the current version in canonical form
harness version print the harness version (the release tag; "dev" for an unstamped build)

Development

The frontend lives in web/ (SolidJS + TypeScript, Vite) and is embedded into the Go binary via web/embed.go. Because of that embedding, web/dist must exist before go build or go test — build the frontend first.

Makefile targets:

Target Does
make web npm install + npm run build in web/
make build make web, then go build -o harness ./cmd/harness (default); unstamped, so harness version prints dev
make test frontend tests (vitest), then go test ./...
make run make build, then serve testdata/site
make clean remove harness and web/dist

Toolchain (standard, no nix required):

  • Node 22+ — for the web/ frontend build and tests.
  • Go per go.mod (currently 1.26) — for the harness.
  • Hugo extended on PATH — required by the theme build test in internal/theme and handy for previewing the demo sites; pin 0.167.0 to match the theme and the scaffold.

For UI work, web/ has its own dev server (cd web && npm run dev) that proxies /api to a harness on :8090, so run ./harness serve --site testdata/site alongside it for live API data.

Editor language catalogs

The editor's interface strings live in web/src/i18n/, separate from web/src/l10n.tsx, which is the content-language axis:

  • index.ts is the runtime: t(key, params), the Solid I18nProvider/useI18n, and formatters over Intl (dates, relative times, lists, sorting), which run under the formatting locale: the UI language with the region the request or browser named (en-GB), see "The editor's language" in docs/host-navigation.md.
  • rich.tsx holds tRich(key, params, slots), for a sentence with an element inside it (a <code> file name, a <b> count, a link button): the sentence stays one key, and each {slot} placeholder renders as the element given for it. It is kept out of index.ts, which must stay importable without JSX.
  • en.json and de.json are the shipped catalogs; there is one flat JSON file per language, and English is the reference every other one falls back to. Every catalog must carry every key the sources use, with the same placeholders. Keys are dotted and grouped by screen (tree.noPages, dialog.unsaved.title), common.* holds what screens share, and server.<code> holds one entry per server error code.
  • glossary.md is the translator's glossary and register rule: impersonal phrasing, and the formal "Sie" where an address is unavoidable. Its Terms table holds the binding German terms; use them rather than a new rendering.
  • servermessages.json is generated from the Go error registry; regenerate it with go test ./internal/apimsg -run TestExport -update.

To add a string, add the key to en.json (and to every other catalog), then call t("ns.key", {…}) with a literal key. Build a whole sentence as one parameterized key, never by concatenating pieces, so word order can differ per language. Placeholders are {name}; {name|q} quotes the value and {name|list} joins a list. Plurals are key.one and key.other entries chosen with Intl.PluralRules: do not append an s by hand. Site-authored labels are data and never go through t().

Two vitest suites enforce this:

  • web/src/test/i18nParity.test.ts: every literal t() and tRich() key exists in every catalog (English and German alike); no catalog key is unused, and no catalog uses a placeholder English lacks; every code in the server registry has a server.* entry whose English equals the registry's template; no hand-made plurals; t() and tRich() keys are string literals.
  • web/src/test/i18nLeak.test.ts: scans the .tsx sources for JSX text nodes and title, placeholder, aria-label and alt literals that hold raw English, and names the file. HTML entities such as &mdash; are not counted as words. A short allowlist sits at the top of the test.

The test setup forces the English catalog, so component tests assert English.

v1 limitations

Stated honestly — these are deliberate scope boundaries, not bugs:

  • Single-user, local-only. The harness binds loopback exclusively; there is no auth, no multi-user coordination, and operations assume one editor.
  • Translations beside the page, no fallback. The content root is content/ itself, never content/<lang>/: a translation is a file beside its source (_index.de.md), so a page missing in a language has no address there — there is no fallback to the default language, and a connected translation cannot be reconnected after it is disconnected. The editor's language UI is a separate change (see Multilingual sites).
  • Plain git, no LFS. Media lives in the repository, sized for brochure-scale sites, not asset-heavy ones.
  • Named shortcode params only. Positional shortcode parameters (.Get 0) are rejected by the content engine and cannot be modeled as fields.
  • Org front matter is read-only. Any edit that would rewrite it fails with an actionable error; YAML, TOML, and JSON front matter are editable.
  • Single-step undo. Undo restores the open page from the previous WIP snapshot (for the first change on a fresh clone, the baseline taken just before it); there is no multi-step history. It is offered only on a page the last change touched (either page of a cross-page move), and Discard all cannot be undone. Undo is per file: undoing a translation restores only that file, and undoing a source page restores its translations only when the last change propagated into them (and only those the same step changed; one edited by hand since keeps its edit and is repaired to its source's structure the next time it is opened).
  • SVG and video uploads are rejected. The media allowlist is JPEG, PNG, WebP, AVIF, and PDF only.
  • GitHub status polling is not implemented yet. Publish feedback detects and polls Forgejo/Gitea only; the client abstraction is in place for GitHub to be added as a new case without touching callers.
  • renderer.unsafe = true is required by the theme. Inline chips inside rich-text bodies are spliced into a shortcode's .Inner as raw HTML, so the theme needs goldmark's unsafe renderer. Consequently the server's body validation — rejecting raw HTML on every save — is the raw-HTML defense, not a UX nicety.

Repository layout

  • cmd/harness — the CLI entry point (init, features, add, remove, lang, serve, sidecar, check, reconcile, version)
  • internal/ — content engine, sidecars, page ops, media, git backbone, forge client, Hugo resolver, site config, supervisor, and HTTP server; the feature lifecycle lives in internal/addcmd (manifests, vendoring, hugo.toml and children edits, receipts) and internal/removecmd (what content a feature is used by, and the purge); internal/langcmd is harness lang (the language list, its hugo.toml tables, the address flip, the translation files); internal/checkcmd is the harness check report
  • web/ — the SolidJS editing UI, embedded into the binary
  • scaffold/ — the theme, element library, page types, and init templates, plus the feature bundles under scaffold/features/<name>/ (categories, forms, news), each described by its feature.yaml manifest
  • testdata/site — a self-contained editing demo (its own sidecars + layouts)
  • testdata/library-site — a theme/render demo exercising every element
  • .forgejo/workflows — this repository's own CI, the release job that publishes the harness binaries on every push to main (see Releases), and the daily govulncheck scan reported to CVE Watch