No description
  • HTML 47.3%
  • Rust 33.9%
  • CSS 11%
  • Shell 7%
  • Makefile 0.8%
Find a file
despiegk 29864aef71 fix: add base_path prefix to all hardcoded absolute links in default templates
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>
2026-03-25 11:08:30 +01:00
.claude Add initial commit 2026-03-07 08:28:04 +01:00
crates fix: add base_path prefix to all hardcoded absolute links in default templates 2026-03-25 11:08:30 +01:00
docs Add hero_website skill definition for scaffolding new sites 2026-03-21 17:41:54 +01:00
documents Extract resolve_and_validate_document() helper to reduce boilerplate 2026-03-07 17:41:36 +01:00
scripts Initial hero_website_lib framework setup 2026-03-06 09:47:11 +01:00
.gitignore Move repeatable code from demo website to framework library 2026-03-21 16:01:18 +01:00
buildenv.sh Add admin features: user management and log cleanup 2026-03-06 10:28:37 +01:00
Cargo.toml feat(lifecycle): use restart_service + kill_other for clean --start 2026-03-22 16:23:03 +01:00
demo.db feat(lifecycle): use restart_service + kill_other for clean --start 2026-03-22 16:23:03 +01:00
LICENSE Initial commit 2026-03-07 07:27:40 +00:00
Makefile Move repeatable code from demo website to framework library 2026-03-21 16:01:18 +01:00
QUICKSTART.md Move repeatable code from demo website to framework library 2026-03-21 16:01:18 +01:00
README.md Add visit tracking, time-on-page, visitor gate, dark mode polish, and consolidate admin templates into library 2026-03-21 21:58:49 +01:00

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:

  • HeroWebsite trait -- 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