Content history
Every save recorded as a version - view, diff and restore any of them.
Content history
Opt-in per-file version history for the site content (SM085). With the plugin enabled, every save - from the manager, WebDAV or an AI connector - becomes a recorded version, and each file gains a History panel on the Files page.
Enabling
One switch: tick the Content history plugin on Plugin Manager. Its
enable hook sets git_history: enabled in lazysite.conf, initialises
the repository and takes an adoption commit of the current site.
Unticking pauses recording; every recorded version is kept, and
re-enabling resumes on top of them. The Status / Enable / Pause actions
on Plugin Config are the inspection and recovery surface, driving the
same machinery as the git-init control-API action (manage_config).
The repository lives at lazysite/git/ - inside the never-served infra
tree, so no .git sits under the docroot for a web server to leak -
with the docroot as the work tree.
What gets recorded
Every content write is an automatic commit with the acting user as
author (user <user@lazysite>) and the action as the message ("edit
about.md", "move a.md -> b.md"): manager save/delete/move/copy/migrate,
uploads, WebDAV PUT/DELETE/MOVE/COPY, MCP writes, nav and site-config
saves, and a content-backup restore. A batched operation is one commit.
A git failure never breaks the write - the save proceeds with a
warning, and the failure is made visible through the status probe.
Versioned: the content tree plus the two operator-authored config files
(lazysite/lazysite.conf, lazysite/nav.conf).
Deliberately excluded (written to the repo's exclude list at init):
lazysite/auth/, lazysite/forms/ (submissions), notify-xmpp.conf,
git-sync.conf, cache/logs/backups/locks, the manager CSRF secret and
upload-rate store, aliases.json, the repo itself, generated *.html
and lazysite-assets/ mirrors, and installer state. This exclude list
is the security boundary that keeps the history free of secrets and
personal data, and therefore safe to sync to a private remote.
Full-system backups remain the disaster-recovery mechanism - they carry
exactly what the history excludes.
View, diff and restore
Each file row on the Files page gains a History panel: the commit list
(when, who, what), a read-only View of any version, a unified Diff
against the current file, and Restore. A restore writes the old content
back through the normal save path - cache invalidation including host
copies, alias re-indexing, audit - so the restore is itself the newest
version and nothing is ever lost. The read actions are git-history,
git-show and git-status (token clients: manage_content,
audit-skipped); git-restore shares the content grant and is audited.
Renames and lineage
History follows a rename (SM175): a move records a structured commit trailer naming the source path, and the per-file log walks across it into the old path's history. The walk stops at the commit that added the file, so history never leaks across a delete-and-recreate at a reused path.
History overview and statistics
Beyond the per-file panel, a git-history-summary action (and the
list_content_history MCP tool, SM199) drive a Files-page History
overview: per-file revision count, first and last commit dates, last
author, and site totals across the tracked tree. It enumerates the
committed tree at HEAD and aggregates each path's lineage-aware
timeline, so it is rename-aware and leak-safe by the same rules as the
per-file view. Each file's count is bounded at 200 revisions, the same
cap the per-file view honours.
Status is a health probe
The plugin's Status reports a genuine verdict, beyond a boolean:
enabled-and-healthy, paused, half-enabled/inconsistent (the config says
on but the repository is missing or unusable), or degraded (the last
attempt to record a version failed, or a stale lock is present). A
failed commit leaves a breadcrumb file that the status action, the
Files page and lazysite check all read - so a history that is
silently failing to record is visible without log-diving, and
lazysite check --fix repairs it.
Remote sync (the git-sync plugin)
With content history enabled, the opt-in Remote sync plugin syncs
it with a private remote repository (Forgejo, Gitea, or similar).
Configure the remote address (https://…, git@host:path or ssh://
only), branch (default main) and access token (password-typed;
lazysite/git-sync.conf is itself excluded from the versioned set) on
Plugin Config, then use Test connection, Push and Pull. Push refuses
cleanly when the remote is ahead - it never forces. On a two-sided
change, Pull names the pages and offers Keep mine or Take theirs; every
apply is preceded by a safety snapshot, invalidates the render caches
and re-indexes aliases. The https token reaches git through a transient
askpass helper, never a command line or the stored URL.