Klagarge 9ffc6a14d2 fix: apply fresh-start review findings
Independent review (second attempt) passed all 7 verification steps at
19f6007, including the fresh-clone embed check, full build from clean
state (9.7s), server curl matrix (public 200, protected + all theme
sidecars 503, searchindex 404, no content in 503 bodies), and just check.

Findings applied:
- README: prerequisite Go version corrected to 1.27.1+ to match go.mod
  (a Go 1.22-1.27 user got a hard 'requires go >= 1.27.1' failure).
- CI: upload-artifact v3 -> v4 with a fallback note for older runners;
  CI now also runs the gofmt guard like 'just check'.
- Justfile: curl -fsSL so a bad download fails at curl instead of tar;
  comment clarifying that --dest-dir is resolved relative to docs/.
- AGENT.md: version-pairing trap now covers both pin locations
  (Justfile and .gitea/workflows/build.yml).
2026-10-10 19:05:34 +02:00

Yggdrasil

A personal knowledge base written in Typst, compiled to a static site by shiroa, and served by a small Go binary that embeds the whole site and gates protected pages behind OIDC login (Authentik) with per-page group permissions.

No search, no database, no Node.js: one binary, one configuration.

How it works

flowchart TD
    A["docs/*.typ (Typst sources)"] -->|"just build-docs (shiroa)"| B["dist/ (static site)"]
    A -->|"just parse (cmd/parser, access-groups tags)"| C["acl.json"]
    B -->|"go build (//go:embed)"| E["server binary"]
    C --> E
    E --> F{"request /page.html"}
    F -->|"public page"| G["serve page"]
    F -->|"protected page"| H{"session + groups ok?"}
    H -->|"no session"| I["redirect to /login, then Authentik"]
    H -->|"not authorized"| J["403"]
    H -->|"authorized"| G
  • Pages are written in Typst under docs/ and listed in docs/book.typ.
  • A page can declare who may read it with a tag: #let access-groups = ("family", "admin"). Pages without a tag are public.
  • At build time, cmd/parser scans docs/, extracts the tags and generates acl.json mapping final page URLs to allowed groups.
  • The Go server embeds both dist/ and acl.json. For every request it reduces the URL to a page key and looks it up in the ACL: public pages are served directly; protected pages require a session whose groups include at least one of the allowed groups.
  • Users authenticate with OIDC. The server reads the groups claim of the verified ID token to build the session.

Repository layout

docs/                    Typst sources (shiroa book)
  book.typ               book metadata + chapter list (the sidebar)
  template.typ           page layout (from the shiroa minimal template)
  index.typ              home page (public)
  thermo/, security/, homelab/   example categories
cmd/parser/              generates acl.json from access-groups tags
cmd/server/              the web server
internal/acl/            ACL loading, page-key normalization, group checks
internal/oidc/           OIDC client (authorization code flow)
internal/middleware/     sessions, login/logout/callback, access gate
dist/                    compiled site (generated, embedded at build time)
tools/                   pinned shiroa binary (downloaded by `just tools`)
acl.json                 generated ACL (embedded at build time)
legacy_docs/             the old documentation base (kept for manual migration)
.gitea/workflows/        Gitea Actions CI

Quick start

Prerequisites: Go 1.27.1+ (the version required by go.mod) and just. The pinned shiroa toolchain is downloaded automatically into tools/ on the first build (needs curl + tar and network access); nothing is installed globally.

just              # show the available commands
just run          # parse ACL, compile the book, build and start the server

Then open http://localhost:8080. The home page and public pages render; protected pages (/thermo/fluids.html, /security/ssh.html, /homelab/backup.html in the example book) return 503 until OIDC is configured.

For pure writing previews without authentication, use just preview-docs.

Writing documentation

The book structure mirrors the URL structure: docs/<path>.typ is compiled to /<path>.html. To add a page:

  1. Create the file, e.g. docs/security/wireguard.typ, starting with:

    #import "../template.typ": project
    #show: project
    
    = WireGuard
    
    Your content...
    
  2. Reference it in docs/book.typ. A = heading creates a category (part) in the sidebar; each - #chapter(...) is a page:

    = Security
    - #chapter("security/ssh.typ", section: "4")[SSH hardening]
    - #chapter("security/wireguard.typ", section: "5")[WireGuard]
    
  3. Only pages listed in book.typ are built. Keep section: numbers consistent (sub-chapters like "4.1" nest under "4").

To restrict a page, add the tag on the first line of its file:

// Only readable by members of these groups.
#let access-groups = ("family", "admin")
  • The groups are compared against the groups claim of the OIDC ID token (in Authentik: your user's group memberships).
  • A page without the tag is public.
  • Everything served for a protected page is protected too: the shiroa renderer fetches page content from *.multi.sir.in files, and those reduce to the same ACL entry as the page itself.
  • The search index (searchindex.json) would also contain protected text, so the server never serves it.

Configuration

Environment variables (see .env.example):

Variable Default Meaning
LISTEN_ADDR :8080 listen address
SESSION_TTL 12h session lifetime
COOKIE_SECURE true Secure cookie flag; use false only on plain-http local tests
OIDC_ISSUER_URL (unset) e.g. https://auth.example.com/application/o/yggdrasil/
OIDC_CLIENT_ID (unset) OAuth2 client id
OIDC_CLIENT_SECRET (unset) OAuth2 client secret
OIDC_REDIRECT_URL (unset) e.g. https://docs.example.com/callback
OIDC_SCOPES openid profile email groups requested scopes

Without the OIDC_* variables the server runs in public-only mode: public pages are served, protected pages answer 503. The server refuses to start on a malformed acl.json or an unreachable OIDC issuer (fail closed).

Setting up Authentik

  1. Applications -> Providers -> Create: type OAuth2/OpenID Connect.
  2. Authorization flow: implicit/explicit consent (default is fine).
  3. Redirect URI: exactly https://your-docs-host/callback.
  4. Client type: Confidential; note the client id and secret.
  5. Scope mappings: make sure the issued ID token contains a groups claim. Recent Authentik versions ship a scope mapping that adds groups; otherwise create a Property Mapping (Scope) with e.g. return {"groups": [g.name for g in user.ak_groups.all()]} and add it to the provider.
  6. Note the issuer URL shown on the provider page (.../application/o/<slug>/) and set OIDC_ISSUER_URL to it.
  7. Create groups in Directory -> Groups (e.g. family, admin) whose names match the access-groups in your pages, and add users to them.

The redirect after login returns the user to the page they originally requested. Logout is available at /logout.

Build and CI

Local commands (see Justfile): just build, just run, just parse, just check, just clean, just preview-docs. Bare just prints the command overview. The pinned shiroa lives in tools/ (gitignored) and is downloaded on demand.

.gitea/workflows/build.yml (Gitea Actions, not GitLab CI) runs on push to main and on pull requests: installs shiroa, regenerates acl.json, compiles the book, runs go vet/go test, builds the static binary and uploads it as an artifact.

A rootless Docker/Podman image is available:

just parse build-docs
docker build -t yggdrasil .
docker run --rm -p 8080:8080 --env-file .env yggdrasil

Note that the binary embeds the site at build time: publishing new or edited pages always requires rebuilding and redeploying the server.

Security notes and limitations

  • Sessions live in memory: they vanish on restart and do not scale past one instance (users simply log in again).
  • Page titles and URLs of protected pages are visible in the sidebar of public pages (that is inherent to a static site). Page content is not: neither the HTML nor the renderer data files can be fetched without authorization.
  • Directory listing is enabled (file names only, e.g. /thermo/).
  • Logout is a GET request (/logout), like most documentation sites.
  • No search is implemented (searchindex.json is explicitly blocked).
  • Static assets referenced by pages (theme CSS/JS, fonts, renderer wasm) are public; they contain no page content.

Legacy documentation

The previous documentation base lives in legacy_docs/ and is kept only for reference; migrate pages into docs/ manually, then delete the folder.

S
Description
No description provided
Readme
82 MiB
0 Stars 1 Watchers 0 Forks
Languages
HTML 51.7%
Typst 24.1%
JavaScript 14.3%
Shell 7.4%
Just 2.5%