This document covers everything needed to develop OxideSEO locally: prerequisites, setup, common workflows, and troubleshooting.
For the project overview, see README.md. For architecture details, see ARCHITECTURE.md.
| Tool | Version | Notes |
|---|---|---|
| Rust | 1.85+ (stable) | Install via rustup |
| Node.js | 22 LTS | Install via nodejs.org or a version manager |
| npm | Included with Node.js | Used as the package manager |
macOS:
- Xcode Command Line Tools:
xcode-select --install
Windows:
- Microsoft C++ Build Tools
- WebView2 (pre-installed on Windows 11; download from Microsoft for Windows 10)
Linux (Debian/Ubuntu):
sudo apt install libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelfFor other distributions, see the Tauri v2 prerequisites.
# Clone the repository
git clone https://github.com/chris-c-thomas/OxideSEO.git
cd OxideSEO
# Install frontend dependencies
npm install
# Run in development mode (starts both Vite dev server and Rust backend)
npx tauri devThe app window opens automatically. The Vite dev server runs on http://localhost:1420 with hot module replacement. The Rust backend recompiles on source changes.
OxideSEO uses no environment variables for runtime configuration. All user-facing settings are managed through the in-app Settings view, persisted in SQLite, and API keys are stored in the OS-native credential store.
The only env var that affects the backend is the standard RUST_LOG variable for log verbosity:
# Override log level (default: info)
RUST_LOG=debug npx tauri devAll npm scripts are defined in package.json:
| Script | Command | Purpose |
|---|---|---|
npm run dev |
vite |
Frontend dev server only (port 1420) |
npm run build |
tsc -b && vite build |
TypeScript check + production bundle |
npm run preview |
vite preview |
Preview production build locally |
npm run lint |
eslint src/ --max-warnings 0 |
Lint frontend (zero-warning policy) |
npm run lint:fix |
eslint src/ --fix |
Auto-fix ESLint violations |
npm run format |
prettier --write "src/**/*.{ts,tsx,css}" |
Auto-format frontend code |
npm run format:check |
prettier --check "src/**/*.{ts,tsx,css}" |
Check formatting without modifying |
npm run test |
vitest run |
Run frontend tests (single pass) |
npm run test:watch |
vitest |
Run frontend tests in watch mode |
npm run typecheck |
tsc --noEmit |
TypeScript type checking only |
npm run changelog |
git cliff -o CHANGELOG.md |
Generate changelog from commits |
npm run changelog:preview |
git cliff --unreleased |
Preview unreleased changelog |
Rust commands run from the src-tauri/ directory:
| Command | Purpose |
|---|---|
cargo test |
Run Rust unit and integration tests |
cargo clippy --all-targets -- -D warnings |
Rust lint (zero-warning policy) |
cargo fmt --all -- --check |
Check Rust formatting |
cargo fmt --all |
Auto-format Rust code |
cargo bench |
Run Criterion benchmarks |
All checks run in CI on every PR. Run them locally before pushing:
# Frontend
npm run lint
npm run format:check
npm run typecheck
npm run test
# Rust (from src-tauri/)
cd src-tauri
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo testPre-commit hooks (husky + lint-staged) automatically check ESLint and Prettier on staged .ts, .tsx, and .css files.
- Framework: Vitest 3.2.4 + Testing Library
- Environment: jsdom
- Setup:
tests/setup.tsmocks@tauri-apps/api/core(invoke) and@tauri-apps/api/event(listen/emit)
npm run test # Single pass
npm run test:watch # Watch modeTest files live alongside source files as *.test.tsx or *.test.ts.
- Unit tests:
#[cfg(test)] mod testsblocks in 17+ source files - Integration tests:
src-tauri/tests/(crawl integration, AI integration) - Benchmarks:
src-tauri/benches/(html_parsing, frontier_ops via Criterion)
cd src-tauri
cargo test # All unit + integration tests
cargo test --lib # Unit tests only
cargo test --test crawl_integration # Specific integration test
cargo bench # Run benchmarks- Frontend: Mock Tauri IPC using the setup in
tests/setup.ts. Test user-visible behavior, not implementation. Query by role, label, or text content. - Rust: Place unit tests in the same file under
#[cfg(test)] mod tests. Name tests descriptively:test_normalize_url_removes_fragment, nottest_1.
The project uses strict TypeScript with these non-default options:
| Option | Value | Effect |
|---|---|---|
strict |
true |
All strict checks enabled |
noUncheckedIndexedAccess |
true |
Array/object access returns T | undefined |
noUnusedLocals |
true |
Errors on unused local variables |
noUnusedParameters |
true |
Errors on unused function parameters |
moduleResolution |
bundler |
Vite-compatible module resolution |
Path aliases configured:
| Alias | Target |
|---|---|
@/* |
./src/* |
@/components/* |
./src/components/* |
@/hooks/* |
./src/hooks/* |
@/lib/* |
./src/lib/* |
@/stores/* |
./src/stores/* |
@/types/* |
./src/types/* |
Step-by-step instructions for common development tasks are maintained in CLAUDE.md (the authoritative source). This includes:
- Adding a new SEO rule
- Adding a new Tauri command
- Adding a new frontend view
- Adding a new SQLite migration
- Adding a new export format
npx tauri icon app-icon.pngThis generates all platform-specific icon formats from the 1024x1024 source PNG into src-tauri/icons/.
Plugins extend OxideSEO with custom rules, export formats, and post-crawl processors. See ARCHITECTURE.md for the system design.
Every plugin is a directory containing a plugin.toml manifest:
my-plugin/
├── plugin.toml # Required: manifest
├── my_plugin.wasm # WASM plugins
└── libmy_plugin.dylib # Native plugins (macOS)
name = "my-plugin"
version = "0.1.0"
description = "What this plugin does"
author = "Your Name"
license = "MIT"
min_app_version = ">=0.3.0"
kind = "rule" # rule | exporter | post_processor | ui_extension
capabilities = ["log"] # log | http_read | db_read | fs_read_plugin_dir
# For WASM plugins:
[wasm]
module = "my_plugin.wasm"
fuel_limit = 10000000 # Optional, default: 10M instructions
memory_limit_mb = 64 # Optional, default: 64MB
# For native plugins:
[native]
library = "libmy_plugin.dylib" # .dylib (macOS), .so (Linux), .dll (Windows)
trusted = true # Must be true to load- Install the WASM target:
rustup target add wasm32-wasip2 - Create a Rust crate with
crate-type = ["cdylib"] - Add
wit-bindgen = "0.36"to dependencies - Implement the
seo-rule-pluginworld fromsrc-tauri/wit/oxide-seo-plugin.wit - Build:
cargo build --target wasm32-wasip2 --release
See plugins/examples/schema-validator/ for a complete example.
- Create a Rust crate with
crate-type = ["cdylib"] - Export the
SeoRuletrait via the C-ABI constructoroxide_seo_create_rule - Build:
cargo build --release
See plugins/examples/markdown-exporter/ for a complete example.
| Field | Type | Description |
|---|---|---|
url |
string |
Page URL |
title |
option<string> |
Page title |
meta_description |
option<string> |
Meta description |
meta_robots |
option<string> |
Meta robots directive |
canonical |
option<string> |
Canonical URL |
viewport |
option<string> |
Viewport meta tag |
h1s |
list<string> |
H1 heading texts |
h2s |
list<string> |
H2 heading texts |
word_count |
u32 |
Body word count |
body_text |
option<string> |
Body text (first ~8000 chars) |
body_size |
option<u32> |
Response body size in bytes |
response_time_ms |
option<u32> |
Response time in milliseconds |
links_count |
u32 |
Number of links on page |
images_count |
u32 |
Number of images on page |
scripts |
list<string> |
Script URLs |
stylesheets |
list<string> |
Stylesheet URLs |
| Capability | Description | WASM | Native |
|---|---|---|---|
log |
Write log messages to host tracing | Yes | N/A (full access) |
http_read |
Outbound HTTP GET requests | Yes | N/A |
db_read |
Read-only SQL queries | Yes | N/A |
fs_read_plugin_dir |
Read files in plugin directory | Yes | N/A |
- Stable:
PluginParsedPagefields (append-only), WIT world names, manifest format, issue namespace (plugin.*), C-ABI constructor symbol (oxide_seo_create_rule) - Experimental: UI extension slots, post-processor SQL interface, exporter data format
Copy the plugin directory to {app_data_dir}/plugins/{plugin-name}/. The app discovers plugins on startup and when you click Reload in the Plugin Manager.
Plugin-generated issues are automatically namespaced: plugin.{plugin-name}.{rule-id}.
Icons must exist before build. Run npx tauri icon app-icon.png to generate them.
Rust commands must run from src-tauri/, not the project root:
cd src-tauri && cargo testThe pre-commit hook runs ESLint and Prettier on staged .ts/.tsx files. Format before committing:
npx prettier --write src/path/to/file.tsxAlways run cargo fmt --all after cargo fix --allow-dirty.
Both tsconfig.json (for the editor) and vite.config.ts (for the bundler) must define the same path aliases. The @ alias maps to ./src.
Add use tauri::Manager; to the file. Tauri v2's Manager trait provides methods like .manage(), .path(), and .emit(), but the compiler error does not suggest the missing import.
tsconfig.node.json must use emitDeclarationOnly: true, not noEmit: true. These conflict when composite: true is set.