This guide is for site admins of an FDS Documents instance — people who log in to edit content, manage permissions, change server config, and run backups. The body is intentionally English-only: it references Rust source paths and JSON keys that do not translate cleanly, and a single source of truth keeps drift out of the operational instructions. The page’s title and description still localize to JA and ZH so the navigation card looks right in every language.
Every editing action is reached by appending ?view=<mode> to a page URL, dispatched in src/routes/view.rs:
?view=edit — the content editor. Requires the Write role on the resource.?view=download — export the resource (see the downloads section).?view=permissions — per-resource access control. Requires the Owner role.?view= parameter renders the page normally.Log in first; role gates are checked against your session against the perms.json next to each resource. If you see a page but not its Edit link, you have Read but not Write.
The editor in src/routes/edit.rs varies by type in ctx.json:
icon, set/clear desc, add a child slug, remove a child, reorder children.file is a string, every language shares the same .md; if it is a {en, ja, zh} dict, each language gets its own.content is a list of chapter entries with name and file.file field to point at the new slug. Switch language before uploading the JA or ZH variant.name and desc accept either:
{en, ja, zh} dict (per-language values).Prefer the dict for anything user-visible — navigation labels, descriptions, chapter names. A plain string is fine for purely structural nodes that have no good translation, but it leaves JA/ZH visitors reading English in their sidebar. The renderer escapes <, &, ", and ' at the boundary, so names containing those characters are safe.
Content markdown is CommonMark. Two rules to remember:
## and ### (and #### if needed) — never #. The breadcrumb supplies the page’s <h1>; a second <h1> is an SEO regression.To add a new resource:
programfiles/content/.ctx.json into it with the right type, multi-language name, optional desc, and any type-specific fields.?view=edit) and add the new directory’s slug to the parent’s content array. The node will not appear in navigation until the parent lists it.For singlemd, also create the .md file(s) referenced by file. For mdbook, create each chapter file as you add it to the content list.
Uploads land at programfiles/content/<resource_path>/<slug>. The file writer (fop::write_file_bytes) prepends programfiles/content/ internally, so paths in ctx.json are relative to that root. Filenames are sanitized: basename only (no directory traversal), . and .. are rejected outright, and a leading . is rejected (no hidden files). If a name fails sanitization the upload is refused; rename and retry.
?view=download)Implemented in src/routes/download.rs, gated by the same Read role as viewing:
| Type | Output |
|---|---|
singlemd | .md attachment in the active language |
mdbook | .zip named <folder>.zip, containing 01-<slug>.md, 02-<slug>.md, … (zero-padded for sort stability) |
file | Raw bytes served under the original filename |
| Anything else | HTTP 400 |
?view=permissions)Owner-only editor for the perms.json stored next to each resource (src/routes/permissions.rs). Four scopes:
uid@server (e.g. 1@local) mapped to a role.Roles, low to high: None < Read < Append < Write < Delete < Owner. To restrict a resource to specific users: set Public and Authenticated to None, then grant individual users Read or Write per-user.
config.json)Server-wide settings live at programfiles/op/config.json and are edited directly on disk — there is no ?view=edit for them. Current keys:
| Key | Effect | Empty / 0 / missing |
|---|---|---|
public_origin | Base URL used in canonical, og:url, hreflang alternates, and sitemap entries | Those SEO tags are suppressed (not emitted with empty values) |
default_icon | Fallback icon path for nodes without an icon field | No icon rendered (row left iconless) |
default_og_image | Site-wide og:image fallback | og:image is omitted entirely |
backup_interval_secs | Seconds between snapshots | Backup writer disabled — no task spawned |
backup_retain | Max number of backup-*.zip archives kept; older ones pruned oldest-first | Backup writer disabled |
backup_dir | Where archives land. Relative paths resolve from the working directory, so this can point at an external mount | Defaults to programfiles/backup |
Invalid JSON in config.json silently falls back to defaults — validate before saving (a JSON linter is enough). Backup keys are re-read each cycle, so they take effect at the next interval without a restart. public_origin, default_icon, and default_og_image are read at request time, but some template caches make a restart the safer choice when changing them.
Going from cargo run on localhost to https://your-domain/ touches four files under programfiles/op/ plus one piece of infrastructure (the reverse proxy). None of it requires a code change.
config.json → public_originSet this to the full canonical URL of the deploy, with scheme, no trailing slash. Examples: "https://doc.fds.moe", "https://docs.example.org". This value is interpolated into:
<link rel="canonical"> and the page’s <link rel="alternate" hreflang> entries<meta property="og:url">, <meta property="og:image"> (when relative)<loc> entriesitem URLsIf you leave public_origin empty or set the wrong scheme, those tags don’t break — they simply get suppressed, and search engines lose the canonical signal. Always set it before announcing the URL externally.
hosts.json → the trusted-origin listA JSON array of hostnames the server treats as itself. Used by SFX’s CSRF / is_trusted(host) checks. For a single-host deploy:
[
"doc.fds.moe"
]
The first entry is the default host (op::get_default_host()), so put your canonical host first. The string "local" is always implicitly trusted in addition to whatever you list — keeps cargo run working without re-edits.
If you front the same backend with multiple domains (e.g. doc.fds.moe and docs.fds.moe), list them all here; otherwise the second one will be refused on POST.
binding.txt → where the process listensA one-line file with host:port. Do not put a trailing newline — SFX’s parser does not strip it, and the misparse falls back to localhost:3003. Use printf not echo if you regenerate the file from the shell:
printf '127.0.0.1:3031' > programfiles/op/binding.txt
Two deployment shapes:
127.0.0.1:3031 (or any free localhost port). The proxy — nginx, Caddy, Cloudflare Tunnel, Traefik — terminates TLS, forwards X-Forwarded-* headers, and proxies to localhost. The app itself only speaks plain HTTP, which is fine because it’s bound to loopback.0.0.0.0:<port>. You’ll still need TLS upstream — the framework doesn’t ship an HTTPS listener — so this only makes sense if something else (a load balancer, a managed cert offload) terminates TLS in front. Avoid for production.The app does not terminate TLS. Configure your reverse proxy to:
certbot --nginx)Host, X-Forwarded-Proto, X-Forwarded-For headersproxy_pass to whatever binding.txt says (default http://127.0.0.1:3031)http:// → https://Once that’s in place, set public_origin to the https:// form. Cookies in turn auto-scope to the deploy host so they don’t leak between subdomains.
backup_interval_secs and backup_retain are the only knobs needed; the defaults 86400 (daily) and 7 (one week) are reasonable. Consider:
backup_dir at a mounted volume outside the deploy directory so a git clean or container rebuild doesn’t wipe historyRun through this before announcing the URL:
config.json public_origin set to the https:// canonical URLhosts.json lists the deploy domain as the first (canonical) entrybinding.txt matches what the reverse proxy is forwarding to, with no trailing newlinecurl -s https://your-domain/sitemap.xml | head returns a sitemap with <loc>https://your-domain/... (not http://, not localhost)curl -sI https://your-domain/policies/ returns 200 with Content-Type: text/html/op/switch_lang/zh-Hans 302s to /zh-Hans/... with Set-Cookie: lang=zh-Hans; Path=/backup_dir is writable by the server process and writes survive a restart/auth/login) works from the deploy host/robots.txt returns the expected directives (currently SFX’s default — see Bug 3 in SFX_TICKET.md for the override story)ctx.json files — all internal links are relative and lang-tag-keyed.support_lang.json at build time, host-independent.navbar.json / footer.json — display strings, not host-specific.The background task in src/backup.rs (spawned from main.rs alongside the sitemap writer) archives programfiles/content/ and programfiles/local_auth/ into backup-<unix_timestamp>.zip. programfiles/op/ is not archived — config is checked in and lossless to recreate.
Writes are atomic: the task writes to a .tmp file and renames, so a half-written zip is never visible to other processes. An initial snapshot is taken at startup, so a fresh restart always has a recent backup. Pruning keeps the newest backup_retain archives by lexical order; the unix-timestamp filenames make that the same as chronological order.
backup_interval_secs or backup_retain to 0 (or remove the key).unzip backup-<timestamp>.zip at the repo root (archive paths recreate the programfiles/content and programfiles/local_auth layouts), then restart.backup_dir at a mounted external location — relative paths resolve against the working directory.Keep the brand spelled “FDS Documents” in English, “FDS 文書” in JA, and “FDS 文档” in ZH — consistently. For JA section names, prefer kanji-mixed forms over full-katakana transliterations (e.g. 案内 over ナビゲーション, 言語 over ランゲージ). Loanwords that are customary in Japanese — ダウンロード, Cookie — are fine.
perms.json but not enforced at runtime.