Browse documentation
Documentation

Typed links

Edit this guide on GitHub

Builds a URL for a page or route. Use href!(handler, ...) for links, form actions, and redirects to Hearth handlers so their URLs follow the registered paths.

Pass a handler as the first argument to use its registered path, including when linking to the handler from its own body. A Path expression or path string also works. A bare Rust path is interpreted as a handler name, so pass Path constants through the href function instead.

Import the macro from hearth::router. Pass the returned Href directly to href, action, or another URL attribute:

use hearth::{Result, router::{href, page}, view::{View, view}};

#[page]
async fn home() -> Result<impl View> {
    Ok(view! { <a href=(href!(about::page))>"About"</a> })
}

mod about {
    use super::*;

    #[page]
    pub async fn page() -> Result<impl View> {
        Ok(view! { <h1>"About"</h1> })
    }
}

With module routing, this links to /about. The same syntax works for handlers with explicit paths. Use ordinary URLs for external destinations and standalone fragment links such as href="#details".

Path parameters

Every further argument fills in one of the path's parameters, with one path_param! value per parameter in the order the path declares them, so a link to a post reads as href!(post, PostId(post.id)). Each value is written with Display and percent-encoded, so a parameter declared with a type, like path_param!(post_id: u64), needs that type to implement Display.

Use the returned Href to configure the query, fragment, or URL form. Render it in a view, or call resolve to obtain a string. Use is_current to mark a navigation link as active.

use serde::Serialize;
use hearth::{
    Result,
    context::Cx,
    router::{
        error::{SeeOther, bad_request, see_other},
        href, module_param, page, path_param, route,
    },
    view::{View, view},
};

// src/app/posts.rs
#[derive(Serialize)]
struct Pagination {
    page: u32,
}

#[page]
async fn posts(cx: &Cx) -> Result<impl View> {
    Ok(view! {
        <a href=(href!(post_id::post, post_id::PostId(1)))>"The first post"</a>
        <a href=(href!(post_id::post, post_id::PostId(1)).fragment("comments"))>"Its comments"</a>
        <a href=(href!(posts).query(Pagination { page: 2 }))>"Next page"</a>
    })
}

pub mod post_id {
    use super::*;

    module_param!(pub post_id: u64, error = bad_request);

    #[page]
    pub async fn post(cx: &Cx) -> Result<impl View> {
        let post_id = path_param::<PostId>(cx)?;

        Ok(view! {
            <form method="post" action=(href!(publish::publish, PostId(*post_id)))>
                <button>"Publish"</button>
            </form>
        })
    }

    pub mod publish {
        use super::*;

        #[route(POST)]
        pub async fn publish(cx: &Cx) -> Result<SeeOther> {
            let post_id = path_param::<PostId>(cx)?;

            Ok(see_other(href!(post, PostId(*post_id)).resolve(cx)))
        }
    }
}

Use the href function to pass parameters as a tuple.

Active navigation

Use Href::is_current to check whether a link targets the current path. Use class! to add its active class alongside its base class:

use hearth::{
    Result,
    context::Cx,
    router::href,
    view::{View, class, component, view},
};

#[component]
async fn posts_link(cx: &Cx) -> Result<impl View> {
    let link = href!(posts);
    let current = link.is_current(cx);

    Ok(view! {
        <a
            href=(link)
            aria-current=(current.then_some("page"))
            class=(class!("nav-link", "active" if current))
        >
            "Posts"
        </a>
    })
}