- HTML 47.3%
- Rust 33.9%
- CSS 11%
- Shell 7%
- Makefile 0.8%
All href, src, action, fetch(), and window.location references in admin,
auth, blog, and utility templates now use {{ base_path }} so they work
correctly when the site is served behind a reverse proxy at a sub-path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|---|---|---|
| .claude | ||
| crates | ||
| docs | ||
| documents | ||
| scripts | ||
| .gitignore | ||
| buildenv.sh | ||
| Cargo.toml | ||
| demo.db | ||
| LICENSE | ||
| Makefile | ||
| QUICKSTART.md | ||
| README.md | ||
Hero Website Framework
Modular Rust library for building full-featured websites with Axum, Tera templates, SQLite, and Unix socket serving. Implement one trait, merge the default routers, and you get an admin panel, user auth, blog, document sharing, visitor tracking, and more -- out of the box.
Architecture
hero_website_framework/
crates/
hero_website_lib/ # The framework library (your dependency)
demo_website/ # Working reference site
Your website is a standalone Rust binary that depends on hero_website_lib. The library provides:
HeroWebsitetrait -- implement this on your app state to unlock all built-in handlers- Router builders --
default_admin_router,default_auth_router,default_documents_router - Default templates -- admin UI, auth pages, etc. (overridable)
- Database layer -- SQLite with auto-migrations for users, groups, sessions, visits, logs, contacts, documents
- Middleware -- visitor tracking, auth injection
- Macros --
require_admin_or_redirect!,require_admin_or_401!,admin_context!,render_admin_template!
Quick Start
1. Create your crate
cargo new my_website
Add to Cargo.toml:
[dependencies]
hero_website_lib = { path = "../hero_website_lib", features = ["hero_proc"] }
tokio = { version = "1", features = ["full"] }
axum = "0.8"
axum-extra = { version = "0.10", features = ["cookie"] }
tera = "1.19"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
rust-embed = "8"
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1"
2. Implement HeroWebsite on your app state
use hero_website_lib::HeroWebsite;
use hero_website_lib::db::DbPool;
use hero_website_lib::blog::Blog;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
pub struct AppState {
pub db: DbPool,
pub templates: tera::Tera,
pub blog: Arc<RwLock<Blog>>,
}
impl HeroWebsite for AppState {
fn db(&self) -> &DbPool { &self.db }
fn templates(&self) -> &tera::Tera { &self.templates }
fn site_name(&self) -> &str { "My Site" }
fn year(&self) -> i32 { 2026 }
fn admin_password(&self) -> &str { "change-me" }
fn blog(&self) -> Option<&Arc<RwLock<Blog>>> { Some(&self.blog) }
fn document_root(&self) -> &str { "documents" }
}
3. Load templates and merge defaults
Use rust-embed to embed your site templates at compile time, then merge the library defaults (your templates take priority):
use rust_embed::Embed;
#[derive(Embed)]
#[folder = "templates/"]
struct TemplateAssets;
fn load_templates() -> tera::Tera {
let mut tera = tera::Tera::default();
tera.autoescape_on(vec![]);
let templates: Vec<(String, String)> = TemplateAssets::iter()
.map(|file| {
let data = TemplateAssets::get(&file).unwrap();
let content = std::str::from_utf8(data.data.as_ref()).unwrap().to_string();
(file.to_string(), content)
})
.collect();
let refs: Vec<(&str, &str)> = templates.iter()
.map(|(n, c)| (n.as_str(), c.as_str()))
.collect();
tera.add_raw_templates(refs).expect("Failed to load templates");
// Library defaults fill in any templates you haven't provided
hero_website_lib::merge_default_templates(&mut tera);
tera
}
4. Build the router
Merge the library's pre-built routers with your site-specific routes:
use axum::{routing::get, middleware, Router};
use hero_website_lib::middleware::visitor_middleware;
use std::sync::Arc;
fn build_router(state: Arc<AppState>) -> Router {
// Your site-specific routes
let site_routes = Router::new()
.route("/", get(index_handler))
.route("/about", get(about_handler));
// Library-provided routers
let admin = hero_website_lib::default_admin_router::<AppState>();
let auth = hero_website_lib::default_auth_router::<AppState>();
let docs = hero_website_lib::default_documents_router::<AppState>();
site_routes
.merge(admin)
.merge(auth)
.merge(docs)
// Visitor tracking middleware
.layer(middleware::from_fn_with_state(
state.clone(), visitor_middleware::<AppState>,
))
// Auth injection middleware (extracts session -> CurrentUser)
.layer(middleware::from_fn_with_state(
state.clone(), auth_inject_middleware,
))
.with_state(state)
}
5. Run the server
const WEBSITE_NAME: &str = "mysite";
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let db = hero_website_lib::db::init_sqlite("sqlite:mysite.db").await?;
let templates = load_templates();
let blog = Arc::new(RwLock::new(Blog::new(Default::default())));
let state = Arc::new(AppState { db, templates, blog });
let app = build_router(state);
// Handles --start / --stop CLI flags (hero_proc integration) or runs foreground
hero_website_lib::run_website(WEBSITE_NAME, "My website", app).await
}
The server binds to a Unix socket at ~/hero/var/sockets/website_mysite.sock. Use a reverse proxy (Caddy, nginx) to expose it over HTTP/HTTPS.
What You Get Out of the Box
| Feature | Routes | Description |
|---|---|---|
| Admin panel | /admin/* |
Dashboard, user management, logs, visits, blog editor, contacts, groups, access profiles, documents, DB export |
| User auth | /login, /register, /logout, /members/* |
Registration, login, sessions, members area |
| Contact form | POST /contact |
Saves name, email, message, IP, and an extra JSON field |
| Blog | (you add public routes) | Markdown posts with YAML frontmatter, admin CRUD |
| Document sharing | /pdfview/*, /mdview/*, /docs/* |
PDF viewer, Markdown viewer, raw/download access |
| Visitor tracking | automatic via middleware | Records path, method, IP, user-agent, user ID |
| Groups & profiles | /admin/groups/*, /admin/profiles/* |
Group membership, access profiles with path-pattern rules |
| DB export | POST /admin/api/export |
Tar/gzip export of database and documents |
| JSON-RPC | /admin/rpc |
Legacy admin RPC endpoint |
Overriding Templates
The library ships default templates under default_templates/:
admin/base.html admin/dashboard.html admin/users.html
admin/login.html admin/logs.html admin/visits.html
admin/blogs.html admin/blog_edit.html admin/contacts.html
admin/groups.html admin/group_detail.html admin/documents.html
admin/document_detail.html admin/profiles.html admin/profile_detail.html
admin/user_detail.html admin/user_edit.html
auth/login.html auth/register.html auth/check_email.html
auth/verify_success.html thank_you.html
To override any template, create a file with the same path in your site's templates/ directory. Your version is loaded first; merge_default_templates only fills in what you haven't provided.
For example, to customize the admin dashboard, create templates/admin/dashboard.html in your crate.
Adding Custom Routes
Add routes to your site router before merging the library routers:
let site_routes = Router::new()
.route("/", get(index))
.route("/pricing", get(pricing))
.route("/api/webhook", post(webhook));
For admin pages that need auth, use the macros:
use hero_website_lib::{require_admin_or_redirect, admin_context, render_admin_template};
pub async fn custom_admin_page<S: HeroWebsite>(
uri: Uri,
State(state): State<Arc<S>>,
jar: CookieJar,
) -> Response {
require_admin_or_redirect!(jar, state, uri);
let mut ctx = admin_context!(state, "custom");
ctx.insert("data", &my_data);
render_admin_template!(state, "admin/custom.html", ctx)
}
Contact Form: The extra Field
POST /contact accepts name, email, message, and an optional extra field. The extra field is stored as a raw JSON string, letting each site attach arbitrary structured data to submissions (e.g., selected plan, referral source, phone number).
<form method="POST" action="/contact">
<input name="name" required>
<input name="email" required>
<textarea name="message" required></textarea>
<!-- Site-specific data encoded as JSON -->
<input type="hidden" name="extra" value='{"plan":"pro","source":"homepage"}'>
<button type="submit">Send</button>
</form>
The admin panel at /admin/contacts displays all submissions including the extra data.
Typical Site Directory Structure
my_website/
Cargo.toml
src/
main.rs # Entry point: state, router, main()
handlers/
mod.rs
pages.rs # Public page handlers
api.rs # API endpoints
templates/
base.html # Site base layout
index.html
about.html
admin/
base.html # Override admin layout if needed
dashboard.html # Override admin dashboard if needed
auth/
login.html # Override login page if needed
static/
style.css
images/
blogs/ # Markdown blog posts with YAML frontmatter
my-first-post.md
documents/ # Uploaded/shared documents
Default Admin Password
The default admin password is planetfirst. Change it by setting admin_password in your SiteConfig or by overriding fn admin_password() in your HeroWebsite implementation.
GeoIP (Optional)
The framework supports IP geolocation in the visits dashboard. To enable it, download the free GeoLite2 Country database and place it at:
~/hero/cfg/GeoLite2-Country.mmdb
When present, the admin visits page will show country codes next to IP addresses.
Running the Demo
The crates/demo_website directory is a complete working example. See crates/demo_website/README.md for details.
# From repository root
make demo
# Or for development
cd crates/demo_website && make dev
License
MIT OR Apache-2.0