方案 A(clang + lld + 本地 sysroot)。rustc 为 freebsd target 链接的系统库 (libc/libexecinfo/libgcc_s/libthr)及 sysinfo 依赖的 freebsd 专有库 (libkvm/libmemstat/libprocstat/libdevstat)需 FreeBSD 运行时,无法纯靠 clang+lld 无 sysroot 完成;故从 FreeBSD 15.1 base.txz 解出 crt 对象 + 系统库 到 .freebsd-sysroot/(gitignored,machine-local)。 - .cargo/config.toml: freebsd target 配置作为注释模板(sysroot 路径机器相关, 不硬编码进仓库);linker 经 rustflags 的 -C linker=clang 设置,使 Makefile 能用 CARGO_TARGET_*_RUSTFLAGS 环境变量整体注入。 - Makefile: 新增 freebsd-sysroot(幂等下载+解压 base.txz)与 build-freebsd (release server 二进制交叉编译)target。server 用 cargo 直出而非 dx CLI, 因 dx 对该 target 的支持未经验证。 - .gitignore: 忽略 .freebsd-sysroot/。 - AGENTS.md: 补充构建命令说明。 实测 release 二进制 16MB,stripped ELF for FreeBSD 15.1,动态依赖均为 FreeBSD base 自带库(libc.so.7 libthr.so.3 libkvm.so.7 等)。
17 KiB
AGENTS.md
Workflow
- 每完成一个功能点立即提交。Agent 自主判断提交时机——当一个逻辑完整的改动通过验证(编译通过 / 测试通过)后,无需等待用户指令,直接
git add+git commit。 - 提交粒度按"功能点"而非"文件":相关联的多文件改动合并为一个提交,不相关的改动拆成多个提交。
- 提交信息遵循现有风格:
type(scope): 简述,正文(可选)说明动机与关键改动。常见 type:feat/fix/docs/refactor/chore/perf。 - 只在用户明确要求时才
git push。提交到本地即可,不主动推送。
Development Commands
make dev # tailwindcss watch + dx serve (needs PostgreSQL)
make build # build-editor → highlight-css → tailwindcss → doc → dx build --release
make build-linux # same as build but targets x86_64-unknown-linux-musl
make build-freebsd # cross-compile FreeBSD x86_64 server binary (clang + lld + sysroot)
make freebsd-sysroot # download/extract FreeBSD base.txz → .freebsd-sysroot/ (idempotent)
make css # one-shot CSS
make css-watch # watch mode
make test # cargo test + vitest (tiptap-editor + lightbox + yggdrasil-core libs)
make doc # cargo doc (ayu 主题) → 拷贝到 public/doc/,随 build 发布
make doc-open # 同 doc,生成后自动用浏览器打开(本地预览,不拷贝)
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 → doc (cargo doc + 拷贝到 public/doc/) → dx build --release. Do not run dx build --release alone.
Prerequisites
- Rust 1.95+ with
wasm32-unknown-unknowntarget dxCLI (cargo install dioxus-cli)tailwindcssCLI v4 — install vianpm install -g @tailwindcss/cli(v4 splits the CLI into its own package; thetailwindcsscore package has nobin), or use the 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
MAX_IMAGE_DIMENSION=8192 # max single side in px, min 512, no upper limit
MAX_IMAGE_PIXELS=50000000 # max total pixels (~7k×7k), min 1M, no upper limit
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
RATE_LIMIT_COMMENT_PER_SEC=1 # comment posting
RATE_LIMIT_COMMENT_BURST=5
RATE_LIMIT_UNKNOWN_PER_SEC=30 # fallback bucket when real client IP can't be determined
RATE_LIMIT_UNKNOWN_BURST=100
DB_POOL_SIZE=20 # database connection pool size
MIGRATE_STARTUP_TIMEOUT_SECS=30 # how long startup waits for PostgreSQL before giving up
STATEMENT_TIMEOUT_SECS=30 # per-query timeout; slow queries are canceled to protect the pool
SSR_CACHE_SECS=3600 # incremental SSR cache TTL
Session / security tuning:
COOKIE_SECURE=false # set true/1/yes to add Secure flag to session cookie
TRUSTED_PROXY_COUNT=0 # number of reverse proxies in front of the app; used to extract real client IP from X-Forwarded-For
APP_BASE_URL= # e.g. https://your-domain.example — trusted origin for CSRF checks on write requests; unset falls back to Host header + X-Forwarded-Proto
Run migrations before first dev server start:
./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.
Server-only helpers: src/auth/password.rs, src/auth/session.rs, src/api/auth.rs, src/api/comments/helpers.rs, and several model helper methods are gated with #[cfg(feature = "server")] because they are only called from server function bodies, which are stripped in WASM builds.
Server-only dependencies: Crates that are only used behind #[cfg(feature = "server")] (e.g., argon2, uuid, regex, pulldown-cmark, rand, http, sha2, hex, plus the pre-existing optional server stack) are declared as optional = true in Cargo.toml and enabled only through the server feature. They are not compiled into the WASM frontend.
Dual API Architecture
The server exposes two distinct API patterns:
-
Dioxus server functions (
#[server(Name, "/api")]insrc/api/) — auto-routed, callable from both client and server Rust. Spread acrosssrc/api/auth.rs,src/api/posts/,src/api/comments/, andsrc/api/settings.rs. -
Axum routes (registered in
src/main.rs) — manualaxum::Routerfor 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
comments/ — comment CRUD + approval/spam/trash server functions
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 (5 tiers: strict/upload/image/comment/unknown)
settings.rs — site settings server functions (trash retention, etc.)
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 buildrunsnpm ci --include=dev && npm run buildinsidelibs/tiptap-editor; thebuildscript istsc --noEmit && vite build(Vite 8 / Rolldown, type-check before bundle)- Unit tests:
npm test(Vitest 4 + happy-dom), coveringUploadCoordinatorcounts/lifecycle,UploadImageNodeViewrendering/callbacks, andisValidUrl src/pages/admin/write.rsinitializes viasrc/tiptap_bridge.rs(wasm-bindgen bindings): injectsClosurecallbacks (onUpdate/onReady/onUploadEvent/onImageUpload) intoTiptapEditor.create, holds the instance + closures inEditorHandle(Drop callsdestroy()). Nojs_sys::eval, nowindowglobals, no polling.
Do not edit public/tiptap/ — they are build artifacts.
Lightbox Subproject
Image lightbox (click-to-zoom) in libs/lightbox/, built as an IIFE library. Unlike the Tiptap editor, it is not wired through wasm-bindgen; the built lightbox.js is injected globally via Dioxus.toml (script = ["/lightbox/lightbox.js"]), and src/components/post/post_content.rs sets window.__lightboxSelectors (e.g. ['.post-content', '.entry-cover']) before the script loads. The IIFE tail reads this config and self-initializes; post_content.rs also calls it directly as a fallback if the script was already loaded.
- Output:
public/lightbox/(lightbox.js,lightbox.css,lightbox.js.map) make buildrunsnpm ci --include=dev && npm run buildinsidelibs/lightbox; thebuildscript istsc --noEmit && vite build- Unit tests:
npm test(Vitest, 23 tests), coveringgeometrymath andlightboxrendering/lifecycle
Do not edit public/lightbox/ — they are build artifacts.
Yggdrasil-Core Subproject
Core JavaScript bundle in libs/yggdrasil-core/, built as an IIFE library exposing window.__initPostContent and window.__startThemeTransition (and future core JS entry points). This is the designated home for all new core JS — add to it rather than creating new public/js/ scripts.
- Output:
public/yggdrasil-core/(yggdrasil-core.js,yggdrasil-core.css,yggdrasil-core.js.map) make buildrunsnpm ci --include=dev && npm run buildinsidelibs/yggdrasil-core; thebuildscript istsc --noEmit && vite build(Vite 8 / Rolldown, type-check before bundle). Source is ES module, output is IIFE (formats: ['iife']invite.config.ts) because Dioxus[web.resource] scriptinjects bare<script src>withouttype="module"support.- Injected globally via
Dioxus.toml(script+stylearrays). Rust calls the entry points viajs_sys::eval("window.__xxx(...)")with anif (window.__xxx)guard to survive script-load ordering races. - Unit tests:
npm test(Vitest 4 + happy-dom), coveringpost-contentcopy-button lifecycle andtheme-transitionfallback paths (happy-dom lacksstartViewTransition, naturally covering the degradation path). - Theme reveal animation uses the View Transitions API: JS synchronously toggles the
darkclass insidestartViewTransition's callback (so the browser snapshots the new state), CSS@keyframes tt-revealexpands::view-transition-new(root)viaclip-path: circle()from the click point; Rust'stheme.set()aligns state afterward (idempotent —use_effectsets the same class again).prefers-reduced-motionand browsers without VT fall back to instant switch.
Do not edit public/yggdrasil-core/ — they are build artifacts.
CodeMirror Editor Subproject
CodeMirror 6 code editor in libs/codemirror-editor/, built as an IIFE library exposing window.CodeMirrorEditor (object literal { create(containerId, options) }) and window.EditorOptions (a class so new EditorOptions() survives TS erasure from wasm). Mirrors the tiptap-editor packaging pattern exactly.
- Output:
public/codemirror/(editor.js,editor.js.map). No CSS file — themes are JSExtensions from@catppuccin/codemirror(Latte light / Mocha dark, matching the syntax-highlightingthemes/). make buildrunspnpm ci --include=dev && pnpm run buildinsidelibs/codemirror-editor; thebuildscript istsc --noEmit && vite build(Vite 8 / Rolldown, type-check before bundle). Output is IIFE (formats: ['iife'],exports: 'default').- Features:
@codemirror/lang-sqlwith live schema autocomplete (schema injected viaset_schemafromget_db_schemaserver function),@replit/codemirror-vimkeymap (injected before other keymaps), Catppuccin theme hot-swap viaCompartment.reconfigure(no instance rebuild — preserves Vim state/cursor/undo). - Rust bridge:
src/codemirror_bridge.rsmirrorssrc/tiptap_bridge.rs—get_module()usesjs_sys::Reflect::get+unchecked_into(object literal, not a constructor, so NOT an extern fn call and NOTdyn_into);EditorHandleholds the instance + allClosures;impl Dropcallsdestroy(). - Shared data types
SqlSchema/SqlTable(serde) compile on both targets; the wasm-bindgen externs live in a#[cfg(target_arch = "wasm32")] mod wasm. - Unit tests:
pnpm test(Vitest 4 + happy-dom), covering getValue/setValue, setTheme (Compartment), setSchema, vim toggle, onChange. - Injected via
Dioxus.tomlscriptarray (/codemirror/editor.js).
Do not edit public/codemirror/ — they are build artifacts.
Database Management (/admin/system)
Admin area at /admin/system (menu "系统") with 5 tabs: 数据库状态 / 服务器状态 / SQL 控制台 / 数据导出 / 备份恢复. All gated by get_current_admin_user (admin-only). Backend in src/api/database/ (status/system_status/sql_console/schema/export/backup/tasks), page in src/pages/admin/system.rs.
- SQL 控制台 is full read-write with 4 guards: (1)
sqlparserAST gates —DROP DATABASE/DROP SCHEMA/CREATE DATABASEabsolutely forbidden (string pre-check);DROP/TRUNCATE/ALTERrequire aconfirm_dangerouscheckbox; (2)UPDATE/DELETEwithoutWHERErejected; (3)STATEMENT_TIMEOUT_SECSquery timeout (pool-level GUC); (4) frontend write-confirm dialog. Multi-statement disabled by default. Results capped at 500 rows. - 备份恢复 uses
dashmaptask-progress table;create_backup/restore_backupreturn a task_id immediately and pollget_task_progress. Backup preferspg_dump(full, incl. schema), falls back to per-tableCOPY TO STDOUT(data only) whenpg_dumpis unavailable. Backup files carry a-- YGGDRASIL BACKUP v1signature header; restore rejects non-system files.backups/is gitignored and served only viaGET /api/database/backups/{filename}(admin-gated, path-allowlist). - 服务器状态 uses
sysinfo(optional, server feature) with a background sampler (SYSINFO_SAMPLE_SECS, default 0.5s) writing to aRwLock<SystemSnapshot>; server functions read the snapshot (zero sampling cost), so frontend can poll high-frequency.src/cache.rsexposes moka hit-rate viaAtomicU64hit/miss counters per cache +cache_stats(). - New env var:
SYSINFO_SAMPLE_SECS(sysinfo sampling interval in seconds, supports decimals). - New deps (all
optional = true, server feature):sysinfo,sqlparser,dashmap.
Syntax Highlighting Pipeline
themes/contains Catppuccin Latte (light) and Mocha (dark).tmThemefilessyntaxes/has custom Sublime syntax definitions (Kotlin, Swift)src/bin/generate_highlight_css.rsgeneratespublic/highlight.csswith class-based rules scoped under.md-content pre code, with.darkprefix for dark modesrc/highlight.rsuses 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_userreadssessioncookie, queriessessions+userstables - 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 inuploads/.cache/. Keyed by path + query params.
Testing
make test # cargo test (Rust, 402 tests) + vitest (tiptap-editor 46 tests, lightbox 23 tests, yggdrasil-core 9 tests)
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. The two libs/ subprojects (tiptap-editor, lightbox) run their own vitest suites.
Image Processing Constraints
- The
imagecrate 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
zenwebpviasrc/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 outputpublic/highlight.css— generated bygenerate_highlight_cssbinarypublic/tiptap/— Vite build output (editor)public/lightbox/— Vite build output (lightbox)public/yggdrasil-core/— Vite build output (core JS bundle)/dist,/.dioxus,/targetnode_modules(insidelibs/tiptap-editor/,libs/lightbox/, andlibs/yggdrasil-core/)uploads/.cache/— image processing disk cache
Notes
randis optional and only enabled by theserverfeature; it is not compiled into the WASM frontend.#[allow(unused_mut, unused_variables)]onWritecomponent is intentional —mutsignals are used in#[cfg(target_arch = "wasm32")]blocks stripped in server builds.- Server uses incremental rendering with 300s cache (
IncrementalRendererConfiginsrc/main.rs).