Add MkDocs documentation: architecture, deployment, development, decisions

This commit is contained in:
brutsalvadi
2026-05-08 10:07:20 +02:00
parent fd2d81328f
commit ccec23c3a3
10 changed files with 591 additions and 102 deletions
+115
View File
@@ -0,0 +1,115 @@
# Authentication & Invites
## Shared database
BincioWiki shares its user database with `bincio_activity`. Both services read from and write to the same SQLite file at `/var/bincio/data/instance.db`. There is no separate user store for the wiki.
The relevant tables:
### `users`
```sql
CREATE TABLE users (
handle TEXT PRIMARY KEY,
display_name TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
wiki_access INTEGER NOT NULL DEFAULT 1,
activity_access INTEGER NOT NULL DEFAULT 0
);
```
`wiki_access = 1` grants access to BincioWiki. `activity_access = 1` grants access to BincioActivity. The two flags are independent; a user can have one, both, or neither.
### `sessions`
```sql
CREATE TABLE sessions (
token TEXT PRIMARY KEY,
handle TEXT NOT NULL REFERENCES users(handle) ON DELETE CASCADE,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL
);
```
Sessions expire after **30 days**. The cookie is `bincio_session`, HTTP-only, `SameSite=Lax`, domain `.bincio.org` (shared across all subdomains).
### `invites`
```sql
CREATE TABLE invites (
code TEXT PRIMARY KEY,
created_by TEXT NOT NULL REFERENCES users(handle) ON DELETE CASCADE,
used_by TEXT REFERENCES users(handle) ON DELETE SET NULL,
created_at INTEGER NOT NULL,
used_at INTEGER,
grants_activity INTEGER NOT NULL DEFAULT 0,
grants_wiki INTEGER NOT NULL DEFAULT 0
);
```
Wiki invites are created with `grants_wiki = 1, grants_activity = 0`. The `grants_wiki` column was added by a migration in `edit/server.py` (`_migrate_db()` runs at sidecar startup).
## Session flow
```
POST /api/auth/login → bcrypt.checkpw → INSERT sessions → Set-Cookie: bincio_session
GET /api/me → SELECT sessions JOIN users → 200 + user JSON | 401
POST /api/auth/logout → DELETE sessions → Clear cookie
```
Every protected Astro page calls `GET /api/me` on load via inline JavaScript. A 401 or 403 response triggers an immediate `window.location.replace('/login/')`.
## Security model
This is **client-side enforcement only**. The static HTML is served publicly by nginx without any HTTP-level authentication. A user without JavaScript, or one who crafts direct HTTP requests, can read the raw HTML.
This is an intentional, documented tradeoff. The content is community memories (cycling routes, gear notes, trip reports) — not financial or medical data. The goal is keeping search engine crawlers and casual visitors out, not resisting determined attackers. All members of the community are trusted.
For future pages that require stricter enforcement, the architecture supports adding an Astro SSR mode with server-side cookie validation before any HTML is rendered. This would require migrating from `output: "static"` to `output: "server"`.
## Invite system
### Creating an invite
Any wiki user can generate an invite link from `/invites/`. The sidecar creates a single-use token:
```
POST /api/invites
→ { "code": "abc123def456ghi7" }
```
The invite URL is `https://wiki.bincio.org/join/?code=<token>`.
### Registration
The invitee visits the URL and fills in a handle, optional display name, and password. On submit:
```
POST /api/auth/register
{ code, handle, display_name, password }
```
The sidecar:
1. Validates the code exists and `used_by` is null.
2. Checks `grants_wiki = 1`.
3. Validates the handle (lowercase, 220 chars, `[a-z][a-z0-9_-]{1,19}`).
4. Validates password length ≥ 8 characters.
5. Checks handle uniqueness.
6. Inserts the user (`wiki_access=1, activity_access=0`) and marks the invite as used — in a single transaction.
7. Creates a session and sets the cookie. The user is immediately logged in.
### Revoking an invite
A user can revoke any of their own unused invites via the `/invites/` page. Used invites cannot be revoked.
### User limits
The `settings` table stores a `max_wiki_users` value (currently `100`). The `/api/auth/register` endpoint reads this value and rejects registration if the current wiki user count has reached the limit.
```sql
SELECT value FROM settings WHERE key = 'max_wiki_users';
-- 100
```
+83
View File
@@ -0,0 +1,83 @@
# Content Model
## Collections
Astro 6 content collections are defined in `site/src/content.config.ts`. All three load files from **outside** the `site/` submodule — Vite is configured with `server.fs.allow: ['..']` to allow this.
| Collection | Source directory | URL pattern |
|------------|-----------------|-------------|
| `entries` | `pages/*.md` | `/entries/{slug}/` |
| `blog` | `blog/*.md` | `/blog/{slug}/` |
| `index` | `config/*.md` | internal only (wikibonsai tree) |
### `entries` — wiki pages
Plain Markdown with minimal frontmatter:
```markdown
---
title: Nome della pagina
---
Contenuto in markdown...
```
Slugs are derived from the filename minus extension. Subdirectory slugs are supported: `pages/bincio-tech/gps-e-ciclocomputer.md` → slug `bincio-tech/gps-e-ciclocomputer` → URL `/entries/bincio-tech/gps-e-ciclocomputer/`.
A `generateId` override in `content.config.ts` strips the `.md` extension and preserves slashes, so Astro does not apply any githubSlug mangling.
### `blog` — stories and posts
Blog frontmatter is richer:
```markdown
---
title: Titolo del post
description: Breve descrizione
pubDate: 2024-05-01
heroImage: /assets/foto.jpg # optional
---
```
### `index` — wikibonsai semantic tree
`config/i.bonsai.md` defines the hierarchical semantic tree used by the wikibonsai integration. It is not rendered as a page; Astro reads it to resolve `[[WikiLink]]` relationships and generate forward/back reference data.
## Sections
`config/sections.json` defines the top-level wiki sections (e.g. BincioTech, BincioOfficina) and their subsections. This file is imported by:
- The Astro pages/entries index to group and display pages
- The `PageEditor.svelte` component to populate the section picker when creating a new page
Current sections: `bincio-tech`, `bincio-officina`, `bincio-tour`, `bincio-corsa`, `bincio-abbigliamento`.
## WikiLinks and wikibonsai
Pages can reference each other with `[[WikiLink]]` syntax. The remark pipeline resolves these at build time:
1. `remark-caml` parses CAML attribute syntax in frontmatter.
2. `remark-wikirefs` resolves `[[target]]` to `/entries/{slug}/` links using `resolveHtmlHref` and `resolveHtmlText` from `site/src/wikibonsai/wikirefs.ts`.
3. `generateForeRefsRemarkPlugin` collects all forward references so they can be stored in the content store.
4. Back-references (pages that link to the current page) are computed at build time in `site/src/wikibonsai/backrefs.ts` and rendered by the `BackRefs.astro` component.
Invalid wikilinks (targets that don't exist) are rendered as dimmed grey text rather than broken links.
## Assets
User-uploaded images live in `assets/` at the repo root. This directory is:
- **Gitignored** — not versioned, not part of any git commit.
- **Served** by FastAPI's `StaticFiles` mount at `/assets/{filename}` (nginx proxies `/assets/` to FastAPI in production).
- **Uploaded** via `POST /api/assets` (multipart, images only, max 10 MB). The editor inserts `![name](/assets/filename)` into the Markdown on upload.
On the VPS, assets are separate from the git push/pull cycle and must be managed independently if ever migrated.
## Delete behaviour
Deleting a page via the editor calls `DELETE /pages/{slug}` or `DELETE /stories/{slug}`. The sidecar:
1. Unlinks the file.
2. Commits the deletion with `git rm`.
In Astro dev mode, a `contentDeleteWatcher` Vite plugin watches `pages/` and `blog/` for `unlink` events. When a `.md` file is deleted it clears `.astro/data-store.json` and restarts the Astro dev server, forcing a full re-scan. Without this, Astro's persisted data store would keep serving the deleted entry.
+129
View File
@@ -0,0 +1,129 @@
# Edit Sidecar
The edit sidecar (`edit/server.py`) is a FastAPI application that handles all write operations. It runs alongside the static Astro build and is the only process that modifies content on disk.
## API endpoints
### Auth
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/api/me` | session | Return current user info |
| `POST` | `/api/auth/login` | — | Create session, set cookie |
| `POST` | `/api/auth/logout` | session | Destroy session, clear cookie |
| `POST` | `/api/auth/register` | — | Register via invite token |
### Wiki pages
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/pages` | List all page slugs |
| `GET` | `/pages/{slug}` | Get page content + `base_hash` |
| `POST` | `/pages/{slug}` | Save page (with 3-way merge) |
| `DELETE` | `/pages/{slug}` | Delete page |
### Blog / stories
Same as pages, with `/stories` prefix.
### Invites
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/invites` | List own invites |
| `POST` | `/api/invites` | Create a new wiki invite |
| `DELETE` | `/api/invites/{code}` | Revoke an unused invite |
### Other
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/api/assets` | Upload image (multipart) |
| `POST` | `/rebuild` | Trigger Astro rebuild + rsync |
| `GET` | `/api/log` | Last 50 web-editor commits |
## Git attribution
Every page save or delete is committed to git with the editor's handle as the author:
```
git add pages/bincio-tech/gps-e-ciclocomputer.md
git commit -m "brut: edited gps-e-ciclocomputer" \
--author="brut <brut@bincio.wiki>"
git update-ref refs/heads/main HEAD
```
The committer is always `bincio-wiki <wiki@bincio.wiki>`. The author field carries the human attribution.
`GIT_COMMITTER_NAME` and `GIT_COMMITTER_EMAIL` are set in the environment passed to each subprocess. On the VPS, `GIT_DIR` points to the bare repo at `/opt/bincio-wiki-repo.git` and `GIT_WORK_TREE` to `/opt/bincio_wiki`.
### The `update-ref` fix
The VPS post-receive hook runs `git checkout -f <SHA>` to check out the new tree. This detaches `HEAD` in the bare repo: subsequent sidecar commits advance the detached HEAD pointer but not `refs/heads/main`. On the next `git push` from a developer's machine, those sidecar commits become unreachable and are eventually garbage-collected.
The fix: after every `git commit`, the sidecar immediately runs:
```
git update-ref refs/heads/main HEAD
```
This keeps `refs/heads/main` in sync with the detached HEAD, so sidecar commits are always reachable via the branch and survive a future push.
### Serialisation
All git operations are serialised with a module-level `asyncio.Lock()`. This prevents two concurrent saves from interleaving `git add` + `git commit` calls and producing a corrupted index or commit history.
## Optimistic concurrency
When a page is opened for editing, the sidecar returns the current HEAD commit hash alongside the content:
```json
{ "slug": "gps-e-ciclocomputer", "content": "...", "base_hash": "a85a2ee" }
```
The editor holds this hash. On save, it sends it back:
```json
{ "content": "...", "base_hash": "a85a2ee" }
```
The sidecar compares `base_hash` against the current HEAD for that file. If they differ, another edit landed while the user was writing.
## 3-way merge
Rather than rejecting the save with a 409, the sidecar attempts a 3-way merge using `git merge-file`:
```
base = content at base_hash (what the user started with)
current = content at HEAD (what's on disk now)
user = content from request (what the user wrote)
```
If the edits touched different lines, `git merge-file` produces a clean merged result and the save proceeds transparently. If the same lines were changed by both, `git merge-file` exits with a non-zero code and embeds conflict markers (`<<<<<<< attuale / ======= / >>>>>>> handle`). The sidecar returns a 409 with the conflict-marked content so the user can resolve manually.
```python
# exit code 0 → clean merge; > 0 → conflicts present
proc = await asyncio.create_subprocess_exec(
"git", "merge-file",
"-L", "attuale", "-L", "base", "-L", handle,
str(p_current), str(p_base), str(p_user),
...
)
await proc.wait()
merged_content = p_current.read_text()
has_conflict = proc.returncode > 0
```
## Rebuild
`POST /rebuild` does three things:
1. Deletes `.astro/data-store.json` — Astro's persisted content cache. Without this, changes to files in `pages/` and `blog/` (which live outside `site/`) are not picked up by a cached build.
2. Runs `npm run build -- --force` in `site/`. The `--force` flag tells Astro to ignore any remaining stale cache.
3. If `WIKI_WEBROOT` is set, rsyncs `site/dist/` to the webroot so nginx serves the updated build immediately.
The rebuild is triggered automatically by the `PageEditor.svelte` after every successful save.
## Slug validation
Slugs are validated against `^[a-zA-Z0-9_][a-zA-Z0-9_\-/]*$` and checked for path traversal before any file operation. The resolved path must be a child of the configured base directory.
+57
View File
@@ -0,0 +1,57 @@
# System Overview
## Components
```
Browser
│ HTTPS (443)
nginx ─── static files ──► /var/www/bincio-wiki/ (Astro build output)
│ proxy_pass (127.0.0.1:4042)
FastAPI sidecar (edit/server.py)
├── reads/writes ──► pages/ (wiki markdown)
├── reads/writes ──► blog/ (blog markdown)
├── reads/writes ──► assets/ (uploaded images)
├── git add + commit ──► /opt/bincio-wiki-repo.git (bare repo)
└── reads/writes ──► /var/bincio/data/instance.db (shared SQLite)
```
## Request flow
1. Browser requests `wiki.bincio.org/entries/some-page/`.
2. nginx serves the pre-built static HTML from `/var/www/bincio-wiki/`.
3. The page's inline JavaScript calls `GET /api/me` on load. nginx proxies this to FastAPI.
4. If FastAPI returns 401/403 (no valid session), JS redirects to `/login/`.
5. On a page edit, the browser `POST /pages/{slug}` with new Markdown content. FastAPI writes the file, runs `git commit`, then `POST /rebuild` triggers `astro build --force`.
6. nginx serves the rebuilt static output immediately on next request.
## Technology stack
| Layer | Technology |
|-------|------------|
| Frontend framework | [Astro 6](https://astro.build) (static output) |
| CSS | Tailwind CSS |
| Interactive editor | Svelte (PageEditor component) |
| Markdown extensions | `remark-wikirefs`, `remark-caml` |
| Wikibonsai integration | Custom TypeScript (`site/src/wikibonsai/`) |
| API / sidecar | FastAPI + uvicorn |
| Python packaging | uv |
| Database | SQLite (shared with `bincio_activity`) |
| Passwords | bcrypt |
| VCS | Git (two bare repos on VPS) |
| Web server | nginx + Let's Encrypt (Certbot) |
| Process manager | systemd |
| VPS | Hetzner, Debian 12 |
## Port map
| Port | Service |
|------|---------|
| 443 | nginx (HTTPS, wiki.bincio.org) |
| 4042 | bincio-wiki FastAPI (production) |
| 8001 | bincio-wiki FastAPI (local dev) |
| 4321 | Astro dev server (local dev only) |
| 4041 | bincio_activity FastAPI |
-102
View File
@@ -1,102 +0,0 @@
---
title: Content location and structure
---
# Content location and structure
Two open design questions about where wiki content lives and how it can be
organised on disk.
---
## Q1 — Content in `site/` vs `bincio_wiki/`
### The tension
Ideally wiki content (the `.md` files the community edits) would live in
`bincio_wiki/`, the container repo. The Astro app is a reusable engine;
content is wiki-specific. Mixing them means content commits go into the
`astro-bloomz` submodule history, and updating the engine requires care not
to disturb content.
Currently content is at `site/src/content/entries/` (inside the submodule).
### Why symlinks failed
Symlinking `bincio_wiki/pages/``site/src/content/entries/` was tried but
abandoned: macOS's fsevents (used by Vite's file watcher) does not follow
symlinks reliably, causing Astro's hot-reload and content layer to miss
changes or crash.
### Options
**Option A — Astro glob `base` pointing outside `site/`**
Change `content.config.ts` to use a relative or absolute path that escapes
the `site/` directory:
```ts
loader: glob({ pattern: '**/*.md', base: '../pages' })
```
`../pages` from the Astro project root (`site/`) resolves to
`bincio_wiki/pages/`. No symlinks. Content stays in the container repo.
Risk: Vite (which Astro uses internally) may refuse to watch files outside
the project root by default. This can be overridden with
`server.watch.ignored` / `server.fs.allow` in `astro.config.mjs`, but needs
testing. Build-time collection (no watch) is likely fine regardless.
**Option B — Copy/sync at dev time**
Keep content source at `bincio_wiki/pages/` (source of truth). Add a step to
`dev.sh` that watches `pages/` and syncs changes to
`site/src/content/entries/` (gitignored there). Build step copies before
`astro build`.
Edit server already reads `WIKI_PAGES_DIR` from the environment, so pointing
it at `bincio_wiki/pages/` is a one-line change. The sync is the only new
piece.
Downside: a two-directory sync is extra moving parts; a crash or missed sync
during dev means stale content in the browser.
**Option C — Absorb Astro app into `bincio_wiki`**
Remove the submodule entirely. Move Astro source into `bincio_wiki/src/`.
Content and engine live together, content at `src/content/entries/`.
Cleanest at runtime, but loses `astro-bloomz` as a reusable starting point
for other sites. Only worth it if we never intend to fork the engine again.
### Recommendation
Try **Option A** first — it is a two-line change and, if Vite's file watcher
cooperates, gives us exactly the right separation with minimal risk. If the
watcher proves problematic in dev, fall back to **Option B**.
---
## Q2 — Subdirectory support in the edit panel
### Current state
The backend already handles subdirectory slugs fully:
- `_SAFE_SLUG` regex allows `/` in slugs
- Routes use `{slug:path}` so FastAPI preserves slashes
- `_list()` uses `rglob("*.md")` — nested files are already listed
- `_save()` calls `path.parent.mkdir(parents=True, exist_ok=True)`
The frontend edit panel, however, only offers flat slug input. Work needed:
- Allow the "new page" field to accept a slug with `/` (e.g. `archivio/2024/festa`)
- Show nested entries grouped by prefix in the page list
- Possibly add a folder picker or breadcrumb
### Pictures
Not yet addressed. Open questions:
- Where are images stored? (options: `site/public/img/`, alongside content,
or a dedicated `bincio_wiki/assets/` tree)
- How does the edit panel upload/reference them?
- How are they referenced from markdown (`/img/foo.jpg` vs relative `./foo.jpg`)?
- Are images versioned in git or stored out-of-band?
+42
View File
@@ -0,0 +1,42 @@
# Content Location (resolved)
!!! success "Resolved"
Option A was implemented. Content lives in `pages/` and `blog/` at the container repo root. Astro loads them via glob loaders with `base: '../pages'` and `base: '../blog'`. Vite is configured with `server.fs.allow: ['..']`.
---
This document records the design question that was open during early development and the options considered.
## The question
Where should wiki content (the `.md` files the community edits) live, and how should Astro load them?
The constraint: Astro's rendering engine lives in the `site/` submodule (`brutsalvadi/astro-bloomz`). Content is wiki-specific and should not be committed to the engine's history.
## Why symlinks failed
Symlinking `bincio_wiki/pages/``site/src/content/entries/` was tried and abandoned. macOS's `fsevents` (used by Vite's file watcher) does not follow symlinks reliably, causing Astro's hot-reload and content layer to miss changes or crash in dev mode.
## Options considered
**Option A — Astro glob `base` pointing outside `site/`***chosen*
```ts
loader: glob({ pattern: '**/*.md', base: '../pages' })
```
`../pages` relative to `site/` resolves to `bincio_wiki/pages/`. No symlinks. Content stays in the container repo. Vite's `server.fs.allow: ['..']` is required to permit serving files outside the project root in dev.
**Option B — Copy/sync at dev time**
Keep source at `bincio_wiki/pages/`. Add a watcher step to `dev.sh` that syncs changes to `site/src/content/entries/` (gitignored there). Edit server already uses `WIKI_PAGES_DIR`.
Downside: two-directory sync is extra moving parts; a crash means stale content.
**Option C — Absorb Astro into `bincio_wiki`**
Remove the submodule. Move Astro source into `bincio_wiki/src/`. Cleanest at runtime but loses the reusable engine submodule.
## Outcome
Option A works correctly. Both Vite's file watcher and Astro's content layer see changes to `pages/` and `blog/` in dev. The `contentDeleteWatcher` plugin handles the edge case of deleted files (which require clearing the data store to prevent stale entries).
+88
View File
@@ -0,0 +1,88 @@
# Getting Started
## Prerequisites
- **Node.js** ≥ 18 and **npm**
- **Python** ≥ 3.11
- **uv** — Python package manager (`curl -LsSf https://astro.sh/uv/install.sh | sh` or `pip install uv`)
- A running instance of **bincio_activity** with a seeded dev database, or a manually created `instance.db`
## Shared database
The wiki shares its user/session/invite database with `bincio_activity`. For local development, the default path is `/tmp/bincio_dev_test/instance.db`.
The easiest way to seed it is to run `bincio_activity`'s dev setup script:
```bash
cd ../bincio_activity
uv run python scripts/dev_test.py --fresh
```
Alternatively, set `SHARED_DB_PATH` to point to an existing `instance.db`:
```bash
export SHARED_DB_PATH=/path/to/instance.db
```
## Running locally
```bash
# Clone with submodule
git clone --recurse-submodules <repo-url>
cd bincio_wiki
# Astro dev server only (port 4321)
bash scripts/dev.sh
# Astro + FastAPI sidecar (ports 4321 + 8001)
bash scripts/dev.sh --edit
```
`uv sync` is called automatically by `dev.sh --edit` — no manual Python setup needed.
The Astro dev server proxies all `/api/`, `/pages/`, `/stories/`, `/assets/`, and `/rebuild` requests to `http://localhost:8001`.
## Adding Python dependencies
```bash
uv add <package> # updates pyproject.toml and uv.lock
```
## Adding JS dependencies
```bash
cd site
npm install <package>
```
## Submodule notes
The `site/` directory is a git submodule pointing to `brutsalvadi/astro-bloomz`. To update it:
```bash
cd site
git pull origin main
cd ..
git add site
git commit -m "Update site submodule"
```
To push submodule changes to the VPS independently:
```bash
cd site && git push vps main
```
## Environment variables
| Variable | Default | Description |
|----------|---------|-------------|
| `SHARED_DB_PATH` | `/tmp/bincio_dev_test/instance.db` | Path to shared SQLite DB |
| `WIKI_PAGES_DIR` | `pages` | Pages directory (relative to repo root) |
| `WIKI_STORIES_DIR` | `blog` | Blog directory |
| `WIKI_ASSETS_DIR` | `assets` | Assets directory |
| `WIKI_WEBROOT` | _(unset)_ | If set, rsync `dist/` here after rebuild |
| `SESSION_DOMAIN` | _(unset)_ | Cookie domain (e.g. `.bincio.org` in prod) |
| `GIT_DIR` | _(unset)_ | Bare repo path (VPS only) |
In production all of these are set in the systemd service file (`deploy/vps/bincio-wiki.service`).
+38
View File
@@ -0,0 +1,38 @@
# BincioWiki — Developer Documentation
BincioWiki is a private, invite-only wiki for the Bincio group of friends. It is built around a small set of deliberate constraints:
- **Git-native content.** Every page edit is a git commit with author attribution. The wiki's history is the edit history.
- **No CMS complexity.** Content is plain Markdown files on disk. No database for content, no object storage, no CDN.
- **Shared identity.** Authentication is shared with `bincio_activity` via a single SQLite database. One account, two apps, one session cookie.
- **Client-side auth enforcement.** The HTML is publicly served by nginx; JavaScript redirects unauthenticated users to `/login/`. This is an intentional tradeoff — the content is community memories, not financial data, and the goal is keeping crawlers and casual visitors out, not resisting determined attackers.
- **Self-hosted on a cheap VPS.** Everything runs on a single Hetzner Debian 12 VPS.
## Repository structure
```
bincio_wiki/
pages/ wiki content (*.md — edited by community)
blog/ blog/stories content (*.md)
config/ bincio-specific config (outside the submodule)
sections.json wiki section + subsection definitions
i.bonsai.md wikibonsai semantic tree
assets/ user-uploaded images (gitignored, rsync'd separately)
site/ Astro 6 app (git submodule → brutsalvadi/astro-bloomz)
edit/ FastAPI edit sidecar (Python, port 4042 prod / 8001 dev)
docs/ this documentation
scripts/ dev.sh, sync-vps.sh
deploy/ VPS config files (nginx, systemd, post-receive hook)
pyproject.toml Python dependencies (managed by uv)
```
## Two-repo model
The project uses two git repositories:
| Repo | Purpose |
|------|---------|
| `bincio_wiki` (container) | Content (`pages/`, `blog/`), sidecar, config, scripts |
| `brutsalvadi/astro-bloomz` (submodule at `site/`) | Astro rendering engine |
This separation keeps content history out of the engine submodule and allows the engine to be reused or forked independently.
+39
View File
@@ -0,0 +1,39 @@
site_name: BincioWiki — Developer Docs
site_description: Technology stack, architecture, and operational notes for bincio_wiki
site_url: ""
docs_dir: docs
nav:
- Overview: index.md
- Architecture:
- System overview: architecture/overview.md
- Content model: architecture/content-model.md
- Edit sidecar: architecture/edit-sidecar.md
- Authentication & invites: architecture/auth.md
- Deployment:
- VPS setup: deployment/vps.md
- Daily operations: deployment/operations.md
- Development:
- Getting started: development/getting-started.md
- Design decisions:
- Language / i18n: decisions/lingua.md
- Content location: decisions/content-location.md
theme:
name: material
palette:
scheme: slate
primary: blue
features:
- navigation.sections
- navigation.expand
- content.code.copy
markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
- pymdownx.highlight:
anchor_linenums: true
- toc:
permalink: true