the cms for static site generators on atproto
lichen.page README-extended.md
16 kB
Markdown
at main

Extended usage #

Multi-Site Mode #

Multi-site mode allows running multiple independent sites from a single lichen.page server instance. Sites are routed based on the HTTP Host header, and a dashboard provides centralized management.

Quickstart #

cargo run --features "mdbook" -- server run --multi

This starts the server in multi-site mode with default settings. Visit http://localhost:9000/ to access the dashboard.

Local Development #

For local multi-site development, use .local or .localhost domains which are routed to 127.0.0.1 via /etc/hosts or dnsmasq.

On nixos here is an example configuration to make a wildcard subdomain route to 127.0.0.1 with dnsmasq:

services.dnsmasq = {
 enable = true;
 settings = {
    # answer *.lichen.local with 127.0.0.1
    address = "/.lichen.local/127.0.0.1";
  };
};

On mac .localhost may automatically resolve to 127.0.0.1, but on linux you need to specify this somehow.

Then create a lichen.toml in your lichen root directory for local development which uses this domain as the dashboard_domain. The default location of the lichen root when run in multi-site mode is ./lichen-root, so the lichen.toml should be at ./lichen-root/lichen.toml.

server_port = 9000
development_mode = true
hot_reload_templates = true
dashboard_domain = "lichen.local"
  • development_mode - generates http:// URLs instead of https://
  • hot_reload_templates - reloads server templates on each request for faster iteration

You can then access the dashboard at http://lichen.local:9000.

And create new sites using *.lichen.local subdomains (e.g. sitea.lichen.local, siteb.lichen.local).

You can then view these sites at http://sitea.lichen.local:9000/ and http://siteb.lichen.local:9000/.

In a live deployment things would work similarly, but DNS records need to be wired up to your server. In production, Caddy can be used as a reverse proxy for TLS termination. See the deploy configs in references/lichen-deploy/ for examples.

If you also want support for hugo, then ensure a hugo binary is on your path and run with all features:

cargo run --all-features -- server run --multi

Directory Structure #

Multi-site mode uses a root directory containing all sites and shared user data:

lichen-root/
├── lichen.toml        # server configuration
├── users/             # shared user authentication
│   ├── alice.toml
│   └── bob.toml
├── site-a.com/        # site directories named by domain
│   ├── settings.toml
│   ├── content/
│   └── dist/
└── site-b.com/
    ├── settings.toml
    ├── content/
    └── dist/

Configuration #

Settings are loaded from lichen.toml in the root directory. Configuration sources are applied in this order (highest precedence first):

  1. CLI arguments (--root-dir, --settings)
  2. Environment variables (LM_ROOT_DIR, LM_SETTINGS_PATH, LM_*)
  3. lichen.toml file
  4. Default values

The root directory defaults to ./lichen-root. The settings path, if provided via CLI or env var, is resolved relative to the root directory.

Configuration Options #

Setting Default Description
server_port 9000 Port the server listens on
use_auth false Require authentication for protected routes
dashboard_domain none Domain that serves the dashboard (e.g., dashboard.example.com)
admin_user "admin" Default username when auth is disabled
development_mode false Use http:// URLs instead of https://
hot_reload_templates false Reload server templates on each request
auth_providers ["file"] List of auth providers (e.g. "file", "atproto", "oidc")
public_url auto-generated Public URL for the server (defaults to http(s)://localhost:{port})
default_storage_limit none Default storage limit per user (e.g. "500MB", "1GB")

CLI Commands #

The following hosting commands use the --multi flag:

# start the server
cargo run -- server run --multi

# build all sites
cargo run -- server build --multi

# build a specific site
cargo run -- server build --multi --domain example.com

# export a site to zip
cargo run -- server export --multi --domain example.com

# manage users
cargo run -- server users --multi add alice
cargo run -- server users --multi set-password alice

CLI Options #

  • --root-dir <DIR> - Override the root directory
  • --settings <FILE> - Override the settings file path

Domain-Based Routing #

When dashboard_domain is configured, the server routes requests based on the Host header:

  • Requests to dashboard_domain go to the management dashboard
  • Requests to site domains (e.g., site-a.com) serve that site's content
  • Requests to unknown domains return 404

When dashboard_domain is not set, unrecognized domains fall back to the dashboard.

Authentication #

When use_auth = true, users must log in to access:

  • The dashboard
  • CMS routes for any site

Users are stored in the shared users/ directory. Session cookies are configured to work across subdomains when dashboard_domain is set.

Site Ownership #

Each site tracks its creator. Only the site owner can access that site's CMS routes. This is enforced by the ACL guard middleware.

Invite Codes #

By default, anyone with an ATProto account can sign up. To restrict signups, add an [invite_codes] section to lichen.toml:

[invite_codes]
codes = [
  { code = "welcome-2026", max_uses = 10 },
  { code = "open-invite", max_uses = 0 },  # 0 = unlimited
]

New users are prompted for a code on their first login. Existing users can log in freely without one. Codes can also be generated from the command line:

# random code, unlimited uses
cargo run -- server invites create --multi

# custom code with a use limit
cargo run -- server invites create --multi --code "my-code" --max-uses 5

When no [invite_codes] section is present, signups are open.

Static Site Generators & Features #

Multi-site mode can scaffold new domains with different SSGs. The available generators depend on the Cargo features you build with:

  • lichen (default, always available)
  • hugo (requires --features hugo at build/run time)
  • mdbook (requires --features mdbook)

When you enable additional features, the dashboard's Create site form exposes those options in the SSG dropdown. The generated settings.toml stores the chosen ssg so future builds use the correct pipeline. Example:

cargo run --package lichen-cli --features "hugo mdbook" -- server run --multi

Without a feature enabled, its option is hidden and API requests using that ssg value will fail.

Publishing Targets #

Sites can publish their built output to external hosting platforms. Configure a [publish] section in the site's settings.toml and click "Publish" in the CMS, or enable auto_publish = true to publish on every save. Supported targets: GitHub Pages, Codeberg Pages, and wisp.place (not yet implemented).

By default, clicking Publish also pushes any unpushed git commits to the remote. To skip the git push and only run other publishing targets (e.g. standard.site), set push_on_publish = false in the [git] section:

[git]
auto_push = true
push_on_publish = false

standard.site #

When built with the atproto feature, lichen can publish site content as standard.site records on the user's ATProto PDS.

For ATProto users, a [standard_site] section is auto-scaffolded in settings.toml when a site is created. You can also configure it manually:

[standard_site]
enabled = true
name = "My Site"
description = "A short description"

[standard_site.frontmatter]
title = "title"
description = "description"
publish_date = "date"
updated_at = "updated"
draft = "draft"
tags = "tags"
cover_image = "cover_image"

The frontmatter mapping tells lichen which frontmatter field names in your markdown files correspond to standard.site document fields. Defaults are provided per SSG (e.g., Hugo uses lastmod for updated_at).

On publish, lichen creates a site.standard.publication record for the site and a site.standard.document record for each non-draft post.

Title extraction — if no title is set in frontmatter, lichen falls back to the first markdown heading (# ... through ######), then the first HTML heading (<h1> through <h6>), then the filename.

Cover image — if no cover_image is set in frontmatter, lichen looks for the first local image in the body (markdown ![alt](path) or HTML <img src="path">). Remote URLs (http://...) are skipped. The image is uploaded as a blob to the PDS and attached to the document record.

textContent — the document body is converted to plain text (markdown and HTML formatting stripped). Link URLs are preserved in text (url) format so they are not lost.

The "Sync to PDS" button on the site settings page triggers a publish manually.

Filtering which posts are published

include_paths / exclude_paths — filter by path prefix. Leading slashes are optional. If no include is set, then all markdown files which do not have draft: true in their fronmatter are included.

[standard_site]
enabled = true
include_paths = ["posts/"]          # only publish posts under posts/
exclude_paths = ["posts/drafts/"]   # but skip posts/drafts/

When both are set, include_paths is applied first, then exclude_paths.

require_standard_site_gate — requires explicit opt-in per post. Only posts with standard_site: true in their frontmatter are published; all others (including posts with the field absent) are skipped:

[standard_site]
enabled = true
require_standard_site_gate = true
---
title: "My Post"
standard_site: true
---

When the gate is not set to true, the standard_site field, can also be used to specifically filter a post out.

value gate off gate on
absent (default) published not published
standard_site: true published published
standard_site: false not published not published

Setting standard_site: false in a post's frontmatter always excludes it, regardless of any other setting.

Full Command-line interface #

Lichen installs one executable. remote interacts with another server (clone, pull and sync also modify local files). server runs or administers local hosting; offline commands do not require a server process to be running.

lichen remote auth login | logout | status
lichen remote sites list | create | delete | info
lichen remote clone <domain> [local-dir]
lichen remote push [domain] [--all]
lichen remote pull [domain] [--all]
lichen remote sync
lichen remote debug pds-get-sites <handle>
lichen remote debug pds-get-repos <handle>
lichen remote debug pds-get-publications <handle>
lichen remote debug pds-get-documents <handle> [--site <publication>]
lichen remote debug pds-delete-publication <handle> <rkey>
lichen remote debug pds-delete-site <handle> <domain>
lichen server run [--port <PORT>] [--open]
lichen server init [--ssg <SSG>]
lichen server build [--domain <DOMAIN>]
lichen server export [--domain <DOMAIN>]
lichen server users add <username> [--password <PASSWORD>]
lichen server users set-password <username> [--password <PASSWORD>]
lichen server invites create --multi [--code <CODE>] [--max-uses <COUNT>]
lichen update [--local]

Use --help on any command for its arguments. Remote site creation accepts --ssg; deletion accepts --delete-git. Login accepts --server, --handle, --password, --token, and --invite-code; OAuth is the default in builds with ATProto support. Without that feature, use password or token login; the PDS debug group is unavailable. Do not combine a named domain with push/pull --all.

--json is supported by remote sites list/create/info and clone; other commands reject it. Logs go to stderr. Client credentials remain in ~/.lichen/cli-config.toml, with LM_SERVER_URL and LM_API_TOKEN overrides. LM_ROOT_DIR relocates both this client data and the local manager's root. Server user files are separate from client credentials.

Server modes #

Invocation Target and configuration
lichen server run Local manager at ~/.lichen, loopback-only, port 9000; no hosting settings file
lichen server run --site ./site Standalone site and its settings.toml
lichen server run --multi Hosting root ./lichen-root and its lichen.toml
lichen server run --multi --root-dir ./hosting Explicit hosting root

--site and --multi are mutually exclusive. --root-dir requires --multi. For run, --settings <FILE> requires an explicit --site or --multi. The local manager never becomes publicly exposed because hosting settings are inherited. Its port precedence is --port, LM_SERVER_PORT, then 9000; hosted modes retain their configured port unless --port is supplied. Port 0 selects an available port. The server prints its UI URL; --open opens it only after initialization succeeds. Explicit public_url values are preserved (for example, behind a reverse proxy); otherwise the port in generated URLs follows the actual listener.

Offline init/build/export/users commands default to the standalone ./site, not the local manager root. Give --site to select another directory, or --multi for hosting. Multi-site build selects all sites unless --domain is given; export requires --domain. Init is standalone-only; invites require --multi. Server --site always means a path, not a domain. Passwords are prompted when omitted; an invite max-uses of 0 means unlimited.

For example:

lichen remote auth login --server dash.lichen.page
lichen remote clone example.com
lichen server run --open

lichen server build --multi --domain example.com
lichen server export --site ./site

The CLI crate owns the executable, while the server library remains reusable by the desktop app. The normal build enables Git, ATProto and shell support. Use cargo build -p lichen-cli --no-default-features for a minimal server; add features explicitly (e.g. --features hugo,mdbook). Optional iroh behavior is described in SITE-IROH.md.

Optional Features #

Beyond the SSG features (lichen, hugo, mdbook), the unified executable has a few other optional compilation features:

git #

Enables git version history and smart HTTP endpoints. Sites become git repositories with automatic commits on each save. Clients can clone, push, and pull via standard git over HTTPS.

atproto #

Enables OAuth login with an ATProto handle. User data is automatically backed up to their PDS via git-remote-pds.

oidc #

Generic OpenID Connect authentication provider. Allows login via any OIDC-compatible identity provider (e.g. Keycloak, Authentik).

iroh #

Note: the iroh feature was thoroughly tested in lichen 0.2, but has not been thoroughly tested tested in lichen 0.3. If people have a desire to use this, then I think it could be brought back with some polishing, and contributions are welcome, but for now it is in standby unless there is a strong desire to use it.

The iroh feature provides peer-to-peer sync via iroh for real-time collaboration without a server. peers exchange git pack files via iroh blobs, using a conflict-free merge driver to avoid conflicts, and using lowest sha wins as a tiebreaker so that git HEADs converge once their filetrees are identical. See docs/SITE-IROH.md for details.

shell #

In-browser terminal using xterm.js. Spawns a sandboxed PTY (via bubblewrap) accessible over WebSocket.

Deployment #

Docker images are published on Docker Hub:

Image Description
notplants/lichen-full All features enabled (recommended)
notplants/lichen-min Lichen SSG only, no external dependencies

The Dockerfile is here.

For deployment guides (Docker Compose, Ansible, Helm), see the lichen-deploy repo.