How to Use the `font!` Macro and Fontsource for Web Fonts in Topcoat

Topcoat's font! and fontsource_font! macros compile web font declarations into Font constants that the router automatically discovers and serves via the topcoat::font::link component.

The tokio-rs/topcoat framework provides first-class support for web fonts through the topcoat-font crate. By using the font! macro for custom @font-face rules or the fontsource_font! macro for the Fontsource catalog, you can embed type-safe font definitions directly in your Rust code and serve them with content-hashed URLs.

Declaring Custom Fonts with the font! Macro

The font! macro, defined in crates/topcoat-font/src/font.rs, allows you to write raw @font-face CSS rules in Rust. It automatically injects the family name into every rule and produces a Font constant that the router can register.

To declare a custom font, wrap your @font-face blocks inside the macro, specifying the family name as the first argument:

use topcoat::{
    font::{Font, font},
    router::{Router, RouterBuilderDiscoverExt},
    view::view,
};

/// Declare the "Orbitron" font manually.
const ORBITRON: Font = font! {
    "Orbitron",
    @font-face {
        src: url(
            "https://cdn.jsdelivr.net/fontsource/fonts/orbitron:vf@latest/latin-wght-normal.woff2"
        ) format("woff2") tech("variations");
        font-weight: 100 900;
        font-display: swap;
    }
};

The macro validates the syntax at compile time and generates a Font struct containing the family name and a vector of FontFace definitions. You can then register this constant automatically using the router's .discover() method, which scans for font constants annotated with #[font]:

#[tokio::main]
async fn main() {
    // `.discover()` picks up `ORBITRON` automatically.
    let router = Router::builder().discover().build();
    topcoat::start(router).await.unwrap();
}

Using Fontsource with the fontsource_font! Macro

For fonts available in the Fontsource catalog, use the fontsource_font! macro implemented in crates/topcoat-font/src/fontsource.rs. This macro validates the requested family, weights, styles, and subsets at compile time against the built-in fontsource::families module.

First, enable the font-fontsource feature in your Cargo.toml:

[dependencies]
topcoat = { version = "0.5", features = ["font-fontsource"] }

Then declare your font using the family name:

#[cfg(feature = "font-fontsource")]
use topcoat::font::{Font, fontsource::fontsource_font};

#[cfg(feature = "font-fontsource")]
const ROBOTO: Font = fontsource_font!(ROBOTO);

The macro generates the same Font struct as font!, but automatically assembles the required file URLs from the Fontsource registry. The constant can be registered and linked exactly like custom fonts declared with font!.

Configuring Weights, Styles, and Subsets

Both macros support selective inclusion of font variants to reduce bundle size. With fontsource_font!, you can specify exactly which weights, styles, and character subsets to include:

#[cfg(feature = "font-fontsource")]
const ROBOTO: Font = fontsource_font!(
    ROBOTO,
    weight: [400, 700],
    style: Normal,
    subset: [Latin, Cyrillic],
);

Only the requested files are included in the generated Font struct. The macro performs compile-time validation to ensure the requested combinations exist in the Fontsource catalog, preventing runtime 404 errors.

Self-Hosting Fonts with the Asset System

By default, Fontsource fonts load from CDNs. To serve them from your own domain with content-hashed URLs, use Topcoat's asset integration. In crates/topcoat-font/src/fontsource.rs, the macro supports a host: Asset parameter that triggers build-time downloading.

When you wrap URLs with url(asset!(…)) or supply host: Asset to fontsource_font!, the topcoat-asset crate downloads the font files during compilation and registers them in the AssetBundle:

#[cfg(feature = "font-fontsource")]
use topcoat::{
    asset::{AssetBundle, RouterBuilderAssetExt},
    font::{Font, fontsource::fontsource_font},
    router::{Router, RouterBuilderDiscoverExt},
};

#[cfg(feature = "font-fontsource")]
const ROBOTO: Font = fontsource_font!(ROBOTO, host: Asset);

#[cfg(feature = "font-fontsource")]
fn build_router() -> Router {
    Router::builder()
        .assets(AssetBundle::load().unwrap()) // serves the downloaded fonts
        .discover()
        .build()
}

The router then serves the font files from your origin, eliminating external CDN dependencies and improving cache control.

Rendering Fonts in Views

Once registered, reference your font in a view! block using topcoat::font::link. This component injects a <link rel="stylesheet"> containing the generated @font-face blocks into your page's <head>:

#[page("/")]
async fn home() -> topcoat::Result {
    view! {
        <!DOCTYPE html>
        <html>
            <head>
                topcoat::font::link(font: ORBITRON)
            </head>
            <body>
                <h1 style="font-family: 'Orbitron'">"Hello!"</h1>
            </body>
        </html>
    }
}

The generated HTML contains the exact @font-face rules defined in your font! or fontsource_font! declarations, making the font available to any CSS on the page via the font-family property.

Summary

  • The font! macro in crates/topcoat-font/src/font.rs compiles custom @font-face rules into type-safe Font constants.
  • The fontsource_font! macro in crates/topcoat-font/src/fontsource.rs pulls validated fonts from the Fontsource catalog at compile time.
  • Enable the font-fontsource Cargo feature to access Fontsource integration.
  • Use host: Asset with fontsource_font! to download and self-host font files with content-hashed URLs.
  • Register fonts automatically via Router::builder().discover() or manually with .font(...).
  • Render font stylesheets in views using topcoat::font::link(font: CONSTANT).

Frequently Asked Questions

What is the difference between font! and fontsource_font!?

The font! macro accepts raw CSS @font-face blocks and is ideal for custom fonts or CDN URLs you define manually. The fontsource_font! macro queries the Fontsource catalog at compile time, validating that the family exists and generating the correct URLs automatically. Both produce a Font struct that integrates identically with the router and view system.

How do I self-host Fontsource fonts instead of using a CDN?

Add the host: Asset parameter to your fontsource_font! invocation, as implemented in crates/topcoat-font/src/fontsource.rs. Then load an AssetBundle in your router with .assets(AssetBundle::load().unwrap()). The build script downloads the font files, hashes them for cache-busting, and serves them from your origin.

Can I use variable fonts or multiple weights with these macros?

Yes. With font!, specify variable font ranges in the font-weight property (e.g., 100 900). With fontsource_font!, pass an array to the weight: parameter (e.g., weight: [400, 700]). The macros include only the specified variants, optimizing your bundle size.

Why does my font constant need #[cfg(feature = "font-fontsource")]?

The fontsource_font! macro and its associated imports are only available when the font-fontsource feature is enabled in your Cargo.toml. The conditional compilation prevents compilation errors if the feature is disabled, ensuring your code remains portable across different build configurations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →