162 lines
7.2 KiB
Markdown
162 lines
7.2 KiB
Markdown
# AGENTS.md
|
||
|
||
## Development Commands
|
||
|
||
```bash
|
||
make dev # tailwindcss watch + dx serve (needs PostgreSQL)
|
||
make build # build-editor → highlight-css → tailwindcss → dx build --release
|
||
make build-linux # same as build but targets x86_64-unknown-linux-musl
|
||
make css # one-shot CSS
|
||
make css-watch # watch mode
|
||
make test # cargo test
|
||
make clean # cargo clean + rm public/style.css
|
||
```
|
||
|
||
**Build order matters**: `make build` runs `build-editor` → `highlight-css` (`cargo run --bin generate_highlight_css`) → `tailwindcss --minify` → `dx build --release`. Do not run `dx build --release` alone.
|
||
|
||
## Prerequisites
|
||
|
||
- Rust 1.95+ with `wasm32-unknown-unknown` target
|
||
- `dx` CLI (`cargo install dioxus-cli`)
|
||
- `tailwindcss` CLI v4 (standalone binary)
|
||
- PostgreSQL running locally
|
||
|
||
## Environment
|
||
|
||
Create `.env` (not committed):
|
||
|
||
```
|
||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/yggdrasil
|
||
RUST_LOG=info
|
||
```
|
||
|
||
Optional tuning via env vars (all have sane defaults):
|
||
|
||
```
|
||
WEBP_QUALITY=85.0 # 0.0–100.0, clamped
|
||
WEBP_METHOD=2 # 0–6, clamped
|
||
RATE_LIMIT_STRICT_PER_SEC=1
|
||
RATE_LIMIT_STRICT_BURST=5
|
||
RATE_LIMIT_UPLOAD_PER_SEC=2
|
||
RATE_LIMIT_UPLOAD_BURST=15
|
||
RATE_LIMIT_IMAGE_PER_SEC=10
|
||
RATE_LIMIT_IMAGE_BURST=50
|
||
```
|
||
|
||
Run migrations before first dev server start:
|
||
|
||
```bash
|
||
./migrate.sh # auto-creates DB, runs migrations/ in order
|
||
```
|
||
|
||
## Architecture: Conditional Compilation
|
||
|
||
Dioxus 0.7 fullstack project with **two independent gates** — the most common source of compilation errors.
|
||
|
||
| Gate | Applies to | Used for |
|
||
|------|-----------|----------|
|
||
| `#[cfg(feature = "server")]` | Server binary only | DB, env loading, background tasks, server function bodies, highlight, WebP, caching |
|
||
| `#[cfg(target_arch = "wasm32")]` | WASM frontend only | localStorage, DOM APIs, web_sys calls, theme detection |
|
||
|
||
**Critical**: Both default features (`web` + `server`) are enabled in `Cargo.toml`. The `dx` CLI handles feature selection during builds.
|
||
|
||
**Stub pattern**: `src/db/mod.rs` provides a `DummyPool` when `server` feature is disabled — do not remove.
|
||
|
||
**Dead code allowances**: `src/auth/password.rs` and `src/auth/session.rs` carry `#[allow(dead_code)]` because the compiler sees them as unused in WASM builds where server function bodies are stripped.
|
||
|
||
## Dual API Architecture
|
||
|
||
The server exposes two distinct API patterns:
|
||
|
||
1. **Dioxus server functions** (`#[server(Name, "/api")]` in `src/api/`) — auto-routed, callable from both client and server Rust. Spread across `src/api/auth.rs` and `src/api/posts/`.
|
||
|
||
2. **Axum routes** (registered in `src/main.rs`) — manual `axum::Router` for endpoints that don't fit the server-function model:
|
||
- `POST /api/upload` — image upload (multipart, auth-required, rate-limited)
|
||
- `GET /uploads/{*path}` — image serving with on-the-fly resize/rotate/convert (query params: `w`, `h`, `thumb`, `rotate`, `format`, `quality`)
|
||
|
||
## Server Module Structure
|
||
|
||
```
|
||
src/api/ — server functions + Axum handlers
|
||
auth.rs — login, register, session validation
|
||
markdown.rs — Markdown→HTML rendering (pulldown-cmark + ammonia sanitization)
|
||
image.rs — image serving with processing pipeline + disk+memory cache
|
||
upload.rs — image upload, auto-converts to WebP
|
||
rate_limit.rs — governor-based rate limiting (3 tiers: strict/upload/image)
|
||
slug.rs — URL slug generation
|
||
posts/ — CRUD server functions for blog posts
|
||
src/auth/ — password hashing (Argon2) + session token management
|
||
src/bin/ — generate_highlight_css (build-time CSS generation)
|
||
src/cache.rs — moka future-based caches for posts, tags, stats
|
||
src/components/ — Dioxus UI components
|
||
src/db/ — PostgreSQL pool (deadpool-postgres, LazyLock global)
|
||
src/hooks/ — shared Dioxus hooks
|
||
src/models/ — Post, User, Tag data models
|
||
src/pages/ — route page components (frontend + admin)
|
||
src/tasks/ — background tokio tasks (session cleanup)
|
||
src/theme.rs — light/dark theme with SSR cookie + WASM localStorage
|
||
src/webp.rs — zenwebp encode/decode (image crate has no WebP)
|
||
```
|
||
|
||
## Tiptap Editor Subproject
|
||
|
||
Rich-text editor in `libs/tiptap-editor/`, built as an IIFE library exposing `window.TiptapEditor`.
|
||
|
||
- Output: `public/tiptap/`
|
||
- `make build` runs `npm install && npx vite build` inside `libs/tiptap-editor`
|
||
- `src/pages/admin/write.rs` initializes via `js_sys::eval`, polls `window.__tiptap_ready`
|
||
|
||
Do not edit `public/tiptap/` — they are build artifacts.
|
||
|
||
## Syntax Highlighting Pipeline
|
||
|
||
- `themes/` contains Catppuccin Latte (light) and Mocha (dark) `.tmTheme` files
|
||
- `syntaxes/` has custom Sublime syntax definitions (Kotlin, Swift)
|
||
- `src/bin/generate_highlight_css.rs` generates `public/highlight.css` with class-based rules scoped under `.md-content pre code`, with `.dark` prefix for dark mode
|
||
- `src/highlight.rs` uses syntect at runtime for code block highlighting
|
||
- All gated behind `#[cfg(feature = "server")]`
|
||
|
||
## Auth & Session
|
||
|
||
- **Registration**: first user becomes `admin`; subsequent registrations rejected with `"Registration is closed"`
|
||
- **Login**: sets an HttpOnly cookie via `FullstackContext::add_response_header`
|
||
- **Session validation**: `get_current_user` reads `session` cookie, queries `sessions` + `users` tables
|
||
- **Background cleanup**: `tasks::session_cleanup::run_cleanup()` deletes expired sessions every hour
|
||
|
||
## Caching
|
||
|
||
- **Post/tag caches** (`src/cache.rs`): moka future-based, TTL varies by data type (60s–600s). Invalidated on writes.
|
||
- **Image processing cache** (`src/api/image.rs`): two-tier — in-memory moka cache + disk cache in `uploads/.cache/`. Keyed by path + query params.
|
||
|
||
## Testing
|
||
|
||
```bash
|
||
cargo test # standard Rust test suite
|
||
dx check # Dioxus type-check (catches component/Router issues)
|
||
cargo clippy # lint
|
||
```
|
||
|
||
Most tests use `#[cfg(all(test, feature = "server"))]` — they only run when the server feature is active (which is the default). No integration tests requiring a database connection.
|
||
|
||
## Image Processing Constraints
|
||
|
||
- The `image` crate is configured **without** WebP support (`default-features = false, features = ["jpeg", "png", "gif"]`). Do not add WebP to the image crate features.
|
||
- All WebP encode/decode goes through `zenwebp` via `src/webp.rs`.
|
||
- Upload pipeline auto-converts non-GIF/non-WebP images to WebP, keeping original format if WebP is larger.
|
||
- Image serving supports on-the-fly resize (`w`, `h`), thumbnail (`thumb=WxH`), rotation (90/180/270), and format conversion.
|
||
|
||
## Build Artifacts (gitignored)
|
||
|
||
- `public/style.css` — Tailwind output
|
||
- `public/highlight.css` — generated by `generate_highlight_css` binary
|
||
- `public/tiptap/` — Vite build output
|
||
- `/dist`, `/.dioxus`, `/target`
|
||
- `node_modules` (inside `libs/tiptap-editor/`)
|
||
- `uploads/.cache/` — image processing disk cache
|
||
|
||
## Notes
|
||
|
||
- `rand` + `getrandom` with `js` feature are required for Argon2 salt generation in WASM builds.
|
||
- `#[allow(unused_mut, unused_variables)]` on `Write` component is intentional — `mut` signals are used in `#[cfg(target_arch = "wasm32")]` blocks stripped in server builds.
|
||
- Server uses incremental rendering with 300s cache (`IncrementalRendererConfig` in `src/main.rs`).
|