Merge asset and page server

This commit is contained in:
2025-11-08 09:33:12 -08:00
parent 6cb54c2300
commit e70170ee5b
18 changed files with 279 additions and 621 deletions

16
Cargo.lock generated
View File

@@ -139,10 +139,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47" checksum = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47"
[[package]]
name = "assetserver"
version = "0.0.1"
[[package]] [[package]]
name = "ast_node" name = "ast_node"
version = "5.0.0" version = "5.0.0"
@@ -1450,14 +1446,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "macro-assets"
version = "0.0.1"
dependencies = [
"quote",
"syn 2.0.108",
]
[[package]] [[package]]
name = "macro-sass" name = "macro-sass"
version = "0.0.1" version = "0.0.1"
@@ -1772,6 +1760,7 @@ dependencies = [
"maud", "maud",
"parking_lot", "parking_lot",
"serde", "serde",
"toolbox",
"tower-http", "tower-http",
"tracing", "tracing",
] ]
@@ -2504,13 +2493,11 @@ dependencies = [
name = "service-webpage" name = "service-webpage"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"assetserver",
"axum", "axum",
"chrono", "chrono",
"emojis", "emojis",
"lazy_static", "lazy_static",
"libservice", "libservice",
"macro-assets",
"macro-sass", "macro-sass",
"markdown-it", "markdown-it",
"maud", "maud",
@@ -2523,7 +2510,6 @@ dependencies = [
"tokio", "tokio",
"toml", "toml",
"toolbox", "toolbox",
"tower-http",
"tracing", "tracing",
] ]

View File

@@ -64,9 +64,7 @@ type_complexity = "allow"
# #
[workspace.dependencies] [workspace.dependencies]
macro-assets = { path = "crates/macro/macro-assets" }
macro-sass = { path = "crates/macro/macro-sass" } macro-sass = { path = "crates/macro/macro-sass" }
assetserver = { path = "crates/lib/assetserver" }
libservice = { path = "crates/lib/libservice" } libservice = { path = "crates/lib/libservice" }
toolbox = { path = "crates/lib/toolbox" } toolbox = { path = "crates/lib/toolbox" }
page = { path = "crates/lib/page" } page = { path = "crates/lib/page" }

View File

@@ -1,8 +0,0 @@
[package]
name = "assetserver"
version = { workspace = true }
rust-version = { workspace = true }
edition = { workspace = true }
[lints]
workspace = true

View File

@@ -1,14 +0,0 @@
/// A static asset with compile-time embedded data.
pub trait Asset {
/// The common URL prefix for all assets (e.g., "/assets")
const URL_PREFIX: &'static str;
/// The specific URL path for this asset (e.g., "/logo.png")
const URL_POSTFIX: &'static str;
/// The full URL for this asset (e.g., "/assets/logo.png")
const URL: &'static str;
/// The embedded file contents as a byte slice
const BYTES: &'static [u8];
}

View File

@@ -8,6 +8,7 @@ edition = { workspace = true }
workspace = true workspace = true
[dependencies] [dependencies]
toolbox = { workspace = true }
libservice = { workspace = true } libservice = { workspace = true }
axum = { workspace = true } axum = { workspace = true }

View File

@@ -0,0 +1,44 @@
use axum::http::{
HeaderMap, HeaderValue, StatusCode,
header::{self},
};
use std::pin::Pin;
use toolbox::mime::MimeType;
use crate::{Rendered, RenderedBody, RequestContext, Servable};
pub struct StaticAsset {
pub bytes: &'static [u8],
pub mime: MimeType,
}
impl Servable for StaticAsset {
fn render<'a>(
&'a self,
_ctx: &'a RequestContext,
) -> Pin<Box<dyn Future<Output = crate::Rendered> + 'a + Send + Sync>> {
Box::pin(async {
let mut headers = HeaderMap::with_capacity(3);
#[expect(clippy::unwrap_used)]
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str(&self.mime.to_string()).unwrap(),
);
headers.insert(
header::CACHE_CONTROL,
HeaderValue::from_str(&format!("immutable, public, max-age={}", 60 * 60 * 24 * 30))
.unwrap(),
);
return Rendered {
code: StatusCode::OK,
headers,
body: RenderedBody::Static(self.bytes),
ttl: None,
immutable: true,
};
})
}
}

View File

@@ -1,2 +1,3 @@
pub mod asset;
pub mod page; pub mod page;
pub mod redirect; pub mod redirect;

View File

@@ -7,7 +7,7 @@ use maud::{Markup, Render, html};
use serde::Deserialize; use serde::Deserialize;
use std::pin::Pin; use std::pin::Pin;
use crate::{Rendered, RequestContext, Servable}; use crate::{Rendered, RenderedBody, RequestContext, Servable};
// //
// MARK: metadata // MARK: metadata
@@ -67,6 +67,7 @@ impl Render for PageMetadata {
// Some HTML // Some HTML
pub struct Page { pub struct Page {
pub meta: PageMetadata, pub meta: PageMetadata,
pub immutable: bool,
/// How long this page's html may be cached. /// How long this page's html may be cached.
/// This controls the maximum age of a page shown to the user. /// This controls the maximum age of a page shown to the user.
@@ -94,10 +95,11 @@ impl Default for Page {
fn default() -> Self { fn default() -> Self {
Page { Page {
meta: Default::default(), meta: Default::default(),
html_ttl: Some(TimeDelta::seconds(60 * 24 * 30)), html_ttl: Some(TimeDelta::seconds(60 * 60 * 24 * 30)),
//css_ttl: Duration::from_secs(60 * 24 * 30), //css_ttl: Duration::from_secs(60 * 60 * 24 * 30),
//generate_css: None, //generate_css: None,
generate_html: Box::new(|_, _| Box::pin(async { html!() })), generate_html: Box::new(|_, _| Box::pin(async { html!() })),
immutable: true,
} }
} }
} }
@@ -125,8 +127,9 @@ impl Servable for Page {
return Rendered { return Rendered {
code: StatusCode::OK, code: StatusCode::OK,
headers, headers,
body: html.0.into_bytes(), body: RenderedBody::Markup(html),
ttl: self.html_ttl, ttl: self.html_ttl,
immutable: self.immutable,
}; };
}) })
} }

View File

@@ -5,7 +5,7 @@ use axum::http::{
header::{self, InvalidHeaderValue}, header::{self, InvalidHeaderValue},
}; };
use crate::{Rendered, RequestContext, Servable}; use crate::{Rendered, RenderedBody, RequestContext, Servable};
pub struct Redirect { pub struct Redirect {
to: HeaderValue, to: HeaderValue,
@@ -31,8 +31,9 @@ impl Servable for Redirect {
return Rendered { return Rendered {
code: StatusCode::PERMANENT_REDIRECT, code: StatusCode::PERMANENT_REDIRECT,
headers, headers,
body: Vec::new(), body: RenderedBody::Empty,
ttl: None, ttl: None,
immutable: true,
}; };
}) })
} }

View File

@@ -8,6 +8,7 @@ use axum::{
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use libservice::ServiceConnectInfo; use libservice::ServiceConnectInfo;
use lru::LruCache; use lru::LruCache;
use maud::Markup;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{collections::HashMap, num::NonZero, pin::Pin, sync::Arc, time::Instant}; use std::{collections::HashMap, num::NonZero, pin::Pin, sync::Arc, time::Instant};
use tower_http::compression::{CompressionLayer, DefaultPredicate}; use tower_http::compression::{CompressionLayer, DefaultPredicate};
@@ -15,13 +16,21 @@ use tracing::trace;
use crate::{ClientInfo, RequestContext}; use crate::{ClientInfo, RequestContext};
#[derive(Clone)]
pub enum RenderedBody {
Markup(Markup),
Static(&'static [u8]),
Empty,
}
#[derive(Clone)] #[derive(Clone)]
pub struct Rendered { pub struct Rendered {
pub code: StatusCode, pub code: StatusCode,
pub headers: HeaderMap, pub headers: HeaderMap,
pub body: Vec<u8>, pub body: RenderedBody,
pub ttl: Option<TimeDelta>, pub ttl: Option<TimeDelta>,
pub immutable: bool,
} }
pub trait Servable: Send + Sync { pub trait Servable: Send + Sync {
@@ -209,20 +218,31 @@ impl PageServer {
#[expect(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
let (mut html, expires) = html_expires.unwrap(); let (mut html, expires) = html_expires.unwrap();
let max_age = match expires { if !html.headers.contains_key(header::CACHE_CONTROL) {
Some(expires) => (expires - now).num_seconds().max(1), let max_age = match expires {
None => 1, Some(expires) => (expires - now).num_seconds().max(1),
}; None => 1,
};
#[expect(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
html.headers.insert( let mut value = String::new();
header::CACHE_CONTROL, if html.immutable {
// immutable; public/private value.push_str("immutable, ");
HeaderValue::from_str(&format!("immutable, public, max-age={}", max_age)).unwrap(), }
);
html.headers value.push_str("public, ");
.insert("Accept-CH", HeaderValue::from_static("Sec-CH-UA-Mobile")); value.push_str(&format!("max-age={}, ", max_age));
html.headers.insert(
header::CACHE_CONTROL,
HeaderValue::from_str(&value.trim().trim_end_matches(',')).unwrap(),
);
}
if !html.headers.contains_key("Accept-CH") {
html.headers
.insert("Accept-CH", HeaderValue::from_static("Sec-CH-UA-Mobile"));
}
trace!( trace!(
message = "Served route", message = "Served route",
@@ -233,7 +253,11 @@ impl PageServer {
time_ns = start.elapsed().as_nanos() time_ns = start.elapsed().as_nanos()
); );
return (html.code, html.headers, html.body).into_response(); return match html.body {
RenderedBody::Markup(markup) => (html.code, html.headers, markup.0).into_response(),
RenderedBody::Static(data) => (html.code, html.headers, data).into_response(),
RenderedBody::Empty => (html.code, html.headers).into_response(),
};
} }
pub fn into_router(self: Arc<Self>) -> Router<()> { pub fn into_router(self: Arc<Self>) -> Router<()> {

View File

@@ -1,15 +0,0 @@
[package]
name = "macro-assets"
version = { workspace = true }
rust-version = { workspace = true }
edition = { workspace = true }
[lints]
workspace = true
[lib]
proc-macro = true
[dependencies]
syn = { workspace = true }
quote = { workspace = true }

View File

@@ -1,309 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{
Expr, Ident, LitStr, Result, Token, braced,
parse::{Parse, ParseStream},
parse_macro_input,
};
/// A macro for generating static asset handlers with compile-time embedding.
///
/// This macro generates:
/// - Individual structs for each asset that implement the `assetserver::Asset` trait
/// - Compile-time embedding of asset files using `include_bytes!`
/// - Optionally, an Axum router function that serves all assets
///
/// # Syntax
///
/// ```notrust
/// assets! {
/// prefix: "/assets"
/// router: router_function_name()
///
/// AssetName {
/// source: "path/to/file.ext",
/// target: "/public-url.ext"
/// }
///
/// AnotherAsset {
/// source: "path/to/another.ext",
/// target: "/another-url.ext"
/// }
/// }
/// ```
///
/// # Arguments
///
/// - `prefix`: The URL prefix for all assets (e.g., "/assets")
/// - `router`: (Optional) The name of a function to generate that returns `(&'static str, Router<()>)`
/// with routes for all assets
/// - Asset blocks: Each block defines an asset with:
/// - A name (identifier) for the generated struct
/// - `source`: The file system path to the asset (relative to the current file)
/// - `target`: The URL path where the asset will be served
///
/// # Generated Code
///
/// For each asset, the macro generates:
/// - A struct with the specified name
/// - An `assetserver::Asset` trait implementation containing:
/// - `URL_PREFIX`: The common prefix for all assets
/// - `URL`: The specific URL path for this asset
/// - `BYTES`: The embedded file contents as a byte slice
/// - Documentation showing the original asset definition
///
/// If `router` is specified, also generates a function that returns an Axum router
/// with all assets mounted at their target URLs.
///
/// # Example
///
/// ```notrust
/// assets! {
/// prefix: "/static"
/// router: static_router()
///
/// Logo {
/// source: "../images/logo.png",
/// target: "/logo.png"
/// }
/// }
/// ```
///
/// This generates structs implementing `assetserver::Asset` and optionally a router function:
///
/// ```notrust
/// pub fn static_router() -> (&'static str, ::axum::Router<()>) {
/// let router = ::axum::Router::new()
/// .route(Logo::URL, ::axum::routing::get(|| async {
/// (::axum::http::StatusCode::OK, Logo::BYTES)
/// }));
/// ("/static", router)
/// }
/// ```
#[proc_macro]
pub fn assets(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as AssetsInput);
let prefix = &input.prefix;
let asset_impls = input.assets.iter().map(|asset| {
let name = &asset.name;
let source = &asset.source;
let target = &asset.target;
// Generate documentation showing the original asset definition
let doc = format!(
"This is an `asset!`\n```notrust\n{} {{\n\tsource: \"{:?}\",\n\ttarget: \"{}\"\n}}\n```",
name, source, target
);
quote! {
#[expect(clippy::allow_attributes)]
#[allow(non_camel_case_types)]
#[doc = #doc]
pub struct #name {}
impl ::assetserver::Asset for #name {
const URL_PREFIX: &'static str = #prefix;
const URL_POSTFIX: &'static str = #target;
const URL: &'static str = concat!(#prefix, #target);
const BYTES: &'static [u8] = #source;
}
}
});
// Generate the router function if specified
let router_fn = if let Some(router_name) = &input.router {
let route_definitions = input.assets.iter().map(|asset| {
let name = &asset.name;
let headers = asset
.headers
.as_ref()
.map(|x| quote! { #x })
.unwrap_or(quote! { [] });
quote! {
.route(#name::URL_POSTFIX, ::axum::routing::get(|| async {
(
::axum::http::StatusCode::OK,
#headers,
#name::BYTES
)
}))
}
});
let router_doc = format!(
"Generated router function that serves {} asset(s) with prefix \"{}\"",
input.assets.len(),
prefix
);
quote! {
#[doc = #router_doc]
pub fn #router_name() -> (&'static str, ::axum::Router<()>) {
use ::tower_http::compression::{CompressionLayer, DefaultPredicate};
let compression: CompressionLayer = CompressionLayer::new()
.br(true)
.deflate(true)
.gzip(true)
.zstd(true)
.compress_when(DefaultPredicate::new());
let router = ::axum::Router::new()
#(#route_definitions)*
.layer(compression);
(#prefix, router)
}
}
} else {
quote! {}
};
let expanded = quote! {
#(#asset_impls)*
#router_fn
};
TokenStream::from(expanded)
}
/// Represents the complete input to the `assets!` macro
struct AssetsInput {
prefix: String,
router: Option<Ident>,
assets: Vec<AssetDefinition>,
}
/// Represents a single asset definition within the macro
struct AssetDefinition {
name: Ident,
source: Expr,
target: String,
headers: Option<Expr>,
}
impl Parse for AssetsInput {
fn parse(input: ParseStream<'_>) -> Result<Self> {
// Parse "prefix:"
let _prefix_ident: Ident = input.parse()?;
let _colon: Token![:] = input.parse()?;
let prefix_lit: LitStr = input.parse()?;
let prefix = prefix_lit.value();
// Try to parse optional "router:" parameter
let router = if input.peek(Ident) {
let peek_ident: Ident = input.fork().parse()?;
if peek_ident == "router" {
let _router_ident: Ident = input.parse()?;
let _colon: Token![:] = input.parse()?;
let router_name: Ident = input.parse()?;
// Parse the parentheses after the function name
let _paren_content;
syn::parenthesized!(_paren_content in input);
Some(router_name)
} else {
None
}
} else {
None
};
let mut assets = Vec::new();
// Parse asset definitions until we reach the end
while !input.is_empty() {
let asset = input.parse::<AssetDefinition>()?;
assets.push(asset);
}
Ok(AssetsInput {
prefix,
router,
assets,
})
}
}
impl Parse for AssetDefinition {
fn parse(input: ParseStream<'_>) -> Result<Self> {
// Parse the asset name
let name: Ident = input.parse()?;
// Parse the braced content
let content;
braced!(content in input);
// Parse fields in any order
let mut source: Option<Expr> = None;
let mut target: Option<String> = None;
let mut headers: Option<Expr> = None;
while !content.is_empty() {
// Parse field name
let field_name: Ident = content.parse()?;
let _colon: Token![:] = content.parse()?;
// Parse field value based on name
match field_name.to_string().as_str() {
"source" => {
if source.is_some() {
return Err(syn::Error::new(
field_name.span(),
"duplicate 'source' field",
));
}
source = Some(content.parse()?);
}
"target" => {
if target.is_some() {
return Err(syn::Error::new(
field_name.span(),
"duplicate 'target' field",
));
}
let target_lit: LitStr = content.parse()?;
target = Some(target_lit.value());
}
"headers" => {
if headers.is_some() {
return Err(syn::Error::new(
field_name.span(),
"duplicate 'headers' field",
));
}
headers = Some(content.parse()?);
}
_ => {
return Err(syn::Error::new(
field_name.span(),
format!(
"unknown field '{}', expected 'source', 'target', or 'headers'",
field_name
),
));
}
}
// Parse comma if not at end
if !content.is_empty() {
content.parse::<Token![,]>()?;
}
}
// Validate required fields
let source = source
.ok_or_else(|| syn::Error::new(name.span(), "missing required field 'source'"))?;
let target = target
.ok_or_else(|| syn::Error::new(name.span(), "missing required field 'target'"))?;
Ok(AssetDefinition {
name,
source,
target,
headers,
})
}
}

View File

@@ -9,9 +9,7 @@ workspace = true
[dependencies] [dependencies]
libservice = { workspace = true } libservice = { workspace = true }
macro-assets = { workspace = true }
macro-sass = { workspace = true } macro-sass = { workspace = true }
assetserver = { workspace = true }
toolbox = { workspace = true } toolbox = { workspace = true }
page = { workspace = true } page = { workspace = true }
@@ -29,5 +27,4 @@ lazy_static = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
reqwest = { workspace = true } reqwest = { workspace = true }
tower-http = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }

View File

@@ -4,7 +4,6 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use assetserver::Asset;
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use maud::{Markup, PreEscaped, html}; use maud::{Markup, PreEscaped, html};
use page::{DeviceType, RequestContext, page::Page}; use page::{DeviceType, RequestContext, page::Page};
@@ -18,7 +17,6 @@ use crate::{
misc::FarLink, misc::FarLink,
}, },
pages::page_wrapper, pages::page_wrapper,
routes::assets::Image_Icon,
}; };
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -194,7 +192,7 @@ pub fn handouts() -> Page {
let mut meta = meta_from_markdown(&md).unwrap().unwrap(); let mut meta = meta_from_markdown(&md).unwrap().unwrap();
if meta.image.is_none() { if meta.image.is_none() {
meta.image = Some(Image_Icon::URL.to_owned()); meta.image = Some("/assets/img/icon.png".to_owned());
} }
let html = PreEscaped(md.render()); let html = PreEscaped(md.render());
@@ -202,6 +200,7 @@ pub fn handouts() -> Page {
Page { Page {
meta, meta,
html_ttl: Some(TimeDelta::seconds(300)), html_ttl: Some(TimeDelta::seconds(300)),
immutable: false,
generate_html: Box::new(move |page, ctx| { generate_html: Box::new(move |page, ctx| {
let html = html.clone(); // TODO: find a way to not clone here let html = html.clone(); // TODO: find a way to not clone here

View File

@@ -1,4 +1,3 @@
use assetserver::Asset;
use maud::html; use maud::html;
use page::page::{Page, PageMetadata}; use page::page::{Page, PageMetadata};
@@ -10,7 +9,6 @@ use crate::{
misc::FarLink, misc::FarLink,
}, },
pages::page_wrapper, pages::page_wrapper,
routes::assets::{Image_Cover, Image_Icon},
}; };
pub fn index() -> Page { pub fn index() -> Page {
@@ -19,7 +17,7 @@ pub fn index() -> Page {
title: "Betalupi: About".into(), title: "Betalupi: About".into(),
author: Some("Mark".into()), author: Some("Mark".into()),
description: Some("Description".into()), description: Some("Description".into()),
image: Some(Image_Icon::URL.into()), image: Some("/assets/img/icon.png".to_owned()),
backlinks: Some(false), backlinks: Some(false),
}, },
@@ -30,7 +28,7 @@ pub fn index() -> Page {
div { div {
img img
src=(Image_Cover::URL) src="/assets/img/cover-small.jpg"
style="float:left;margin:10px 10px 10px 10px;display:block;width:25%;" style="float:left;margin:10px 10px 10px 10px;display:block;width:25%;"
{} {}

View File

@@ -1,15 +1,11 @@
use assetserver::Asset;
use chrono::TimeDelta; use chrono::TimeDelta;
use maud::{DOCTYPE, Markup, PreEscaped, html}; use maud::{DOCTYPE, Markup, PreEscaped, html};
use page::page::{Page, PageMetadata}; use page::page::{Page, PageMetadata};
use std::pin::Pin; use std::pin::Pin;
use crate::{ use crate::components::{
components::{ md::{Markdown, backlinks, meta_from_markdown},
md::{Markdown, backlinks, meta_from_markdown}, misc::FarLink,
misc::FarLink,
},
routes::assets::{Image_Icon, Styles_Main},
}; };
mod handouts; mod handouts;
@@ -26,20 +22,23 @@ pub fn links() -> Page {
http://www.3dprintmath.com/ http://www.3dprintmath.com/
*/ */
page_from_markdown(include_str!("links.md"), Some(Image_Icon::URL.to_owned())) page_from_markdown(
include_str!("links.md"),
Some("/assets/img/icon.png".to_string()),
)
} }
pub fn betalupi() -> Page { pub fn betalupi() -> Page {
page_from_markdown( page_from_markdown(
include_str!("betalupi.md"), include_str!("betalupi.md"),
Some(Image_Icon::URL.to_owned()), Some("/assets/img/icon.png".to_string()),
) )
} }
pub fn htwah_typesetting() -> Page { pub fn htwah_typesetting() -> Page {
page_from_markdown( page_from_markdown(
include_str!("htwah-typesetting.md"), include_str!("htwah-typesetting.md"),
Some(Image_Icon::URL.to_owned()), Some("/assets/img/icon.png".to_string()),
) )
} }
@@ -66,6 +65,7 @@ fn page_from_markdown(md: impl Into<String>, default_image: Option<String>) -> P
Page { Page {
meta, meta,
immutable: true,
html_ttl: Some(TimeDelta::seconds(60 * 24 * 30)), html_ttl: Some(TimeDelta::seconds(60 * 24 * 30)),
generate_html: Box::new(move |page, ctx| { generate_html: Box::new(move |page, ctx| {
@@ -104,7 +104,7 @@ pub fn page_wrapper<'a>(
meta content="text/html; charset=UTF-8" http-equiv="content-type" {} meta content="text/html; charset=UTF-8" http-equiv="content-type" {}
meta property="og:type" content="website" {} meta property="og:type" content="website" {}
link rel="stylesheet" href=(Styles_Main::URL) {} link rel="stylesheet" href=("/assets/css/main.css") {}
(&meta) (&meta)
title { (PreEscaped(meta.title.clone())) } title { (PreEscaped(meta.title.clone())) }

View File

@@ -1,206 +0,0 @@
use assetserver::Asset;
use axum::http::header;
use macro_assets::assets;
use macro_sass::sass;
use toolbox::mime::MimeType;
assets! {
prefix: "/assets"
router: asset_router()
//
// MARK: styles
//
Styles_Main {
source: sass!("css/main.scss").as_bytes(),
target: "/css/main.css",
headers: [
(header::CONTENT_TYPE, "text/css")
]
}
//
// MARK: images
//
Image_Cover {
source: include_bytes!("../../assets/images/cover-small.jpg"),
target: "/img/face.jpg",
headers: [
(header::CONTENT_TYPE, "image/jpg")
]
}
Image_Betalupi {
source: include_bytes!("../../assets/images/betalupi-map.png"),
target: "/img/betalupi.png",
headers: [
(header::CONTENT_TYPE, "image/png")
]
}
Image_Icon {
source: include_bytes!("../../assets/images/icon.png"),
target: "/img/icon.png",
headers: [
(header::CONTENT_TYPE, "image/png")
]
}
//
// MARK: fonts
//
FiraCode_Bold_woff2 {
source: include_bytes!("../../assets/fonts/fira/FiraCode-Bold.woff2"),
target: "/fonts/FiraCode-Bold.woff2",
headers: [
(header::CONTENT_TYPE, "application/font-woff2")
]
}
FiraCode_Light_woff2 {
source: include_bytes!("../../assets/fonts/fira/FiraCode-Light.woff2"),
target: "/fonts/FiraCode-Light.woff2",
headers: [
(header::CONTENT_TYPE, "application/font-woff2")
]
}
FiraCode_Medium_woff2 {
source: include_bytes!("../../assets/fonts/fira/FiraCode-Medium.woff2"),
target: "/fonts/FiraCode-Medium.woff2",
headers: [
(header::CONTENT_TYPE, "application/font-woff2")
]
}
FiraCode_Regular_woff2 {
source: include_bytes!("../../assets/fonts/fira/FiraCode-Regular.woff2"),
target: "/fonts/FiraCode-Regular.woff2",
headers: [
(header::CONTENT_TYPE, "application/font-woff2")
]
}
FiraCode_SemiBold_woff2 {
source: include_bytes!("../../assets/fonts/fira/FiraCode-SemiBold.woff2"),
target: "/fonts/FiraCode-SemiBold.woff2",
headers: [
(header::CONTENT_TYPE, "application/font-woff2")
]
}
FiraCode_VF_woff2 {
source: include_bytes!("../../assets/fonts/fira/FiraCode-VF.woff2"),
target: "/fonts/FiraCode-VF.woff2",
headers: [
(header::CONTENT_TYPE, "application/font-woff2")
]
}
//
// MARK: icons
//
Fa_Brands_woff2 {
source: include_bytes!("../../assets/fonts/fa/fa-brands-400.woff2"),
target: "/fonts/fa/fa-brands-400.woff2",
headers: [
(header::CONTENT_TYPE, "application/font-woff2")
]
}
Fa_Regular_woff2 {
source: include_bytes!("../../assets/fonts/fa/fa-regular-400.woff2"),
target: "/fonts/fa/fa-regular-400.woff2",
headers: [
(header::CONTENT_TYPE, "application/font-woff2")
]
}
Fa_Solid_woff2 {
source: include_bytes!("../../assets/fonts/fa/fa-solid-900.woff2"),
target: "/fonts/fa/fa-solid-900.woff2",
headers: [
(header::CONTENT_TYPE, "application/font-woff2")
]
}
Fa_Brands_ttf {
source: include_bytes!("../../assets/fonts/fa/fa-brands-400.ttf"),
target: "/fonts/fa/fa-brands-400.ttf",
headers: [
(header::CONTENT_TYPE, "application/font-ttf")
]
}
Fa_Regular_ttf {
source: include_bytes!("../../assets/fonts/fa/fa-regular-400.ttf"),
target: "/fonts/fa/fa-regular-400.ttf",
headers: [
(header::CONTENT_TYPE, "application/font-ttf")
]
}
Fa_Solid_ttf {
source: include_bytes!("../../assets/fonts/fa/fa-solid-900.ttf"),
target: "/fonts/fa/fa-solid-900.ttf",
headers: [
(header::CONTENT_TYPE, "application/font-ttf")
]
}
//
// MARK: htwah
//
Htwah_Definitions {
source: include_bytes!("../../assets/htwah/definitions.pdf"),
target: "/htwah/definitions.pdf",
headers: [
(header::CONTENT_TYPE, MimeType::Pdf.to_string())
]
}
Htwah_Numbering {
source: include_bytes!("../../assets/htwah/numbering.pdf"),
target: "/htwah/numbering.pdf",
headers: [
(header::CONTENT_TYPE, MimeType::Pdf.to_string())
]
}
Htwah_SolsA {
source: include_bytes!("../../assets/htwah/sols-a.pdf"),
target: "/htwah/sols-a.pdf",
headers: [
(header::CONTENT_TYPE, MimeType::Pdf.to_string())
]
}
Htwah_SolsB {
source: include_bytes!("../../assets/htwah/sols-b.pdf"),
target: "/htwah/sols-b.pdf",
headers: [
(header::CONTENT_TYPE, MimeType::Pdf.to_string())
]
}
Htwah_SpacingA {
source: include_bytes!("../../assets/htwah/spacing-a.pdf"),
target: "/htwah/spacing-a.pdf",
headers: [
(header::CONTENT_TYPE, MimeType::Pdf.to_string())
]
}
Htwah_SpacingB {
source: include_bytes!("../../assets/htwah/spacing-b.pdf"),
target: "/htwah/spacing-b.pdf",
headers: [
(header::CONTENT_TYPE, MimeType::Pdf.to_string())
]
}
}

View File

@@ -1,19 +1,13 @@
use axum::Router; use axum::Router;
use page::{PageServer, redirect::Redirect}; use macro_sass::sass;
use page::{PageServer, asset::StaticAsset, redirect::Redirect};
use std::sync::Arc; use std::sync::Arc;
use tracing::info; use toolbox::mime::MimeType;
use crate::pages; use crate::pages;
pub mod assets;
pub(super) fn router() -> Router<()> { pub(super) fn router() -> Router<()> {
let (asset_prefix, asset_router) = assets::asset_router(); build_server().into_router()
info!("Serving assets at {asset_prefix}");
let router = build_server().into_router();
Router::new().merge(router).nest(asset_prefix, asset_router)
} }
fn build_server() -> Arc<PageServer> { fn build_server() -> Arc<PageServer> {
@@ -26,7 +20,171 @@ fn build_server() -> Arc<PageServer> {
.add_page("/whats-a-betalupi", pages::betalupi()) .add_page("/whats-a-betalupi", pages::betalupi())
.add_page("/handouts", pages::handouts()) .add_page("/handouts", pages::handouts())
.add_page("/htwah", Redirect::new("/handouts").unwrap()) .add_page("/htwah", Redirect::new("/handouts").unwrap())
.add_page("/htwah/typesetting", pages::htwah_typesetting()); .add_page("/htwah/typesetting", pages::htwah_typesetting())
//
.add_page(
"/assets/css/main.css",
StaticAsset {
bytes: sass!("css/main.scss").as_bytes(),
mime: MimeType::Css,
},
)
.add_page(
"/assets/img/cover-small.jpg",
StaticAsset {
bytes: include_bytes!("../../assets/images/cover-small.jpg"),
mime: MimeType::Css,
},
)
.add_page(
"/assets/img/betalupi.png",
StaticAsset {
bytes: include_bytes!("../../assets/images/betalupi-map.png"),
mime: MimeType::Css,
},
)
.add_page(
"/assets/img/icon.png",
StaticAsset {
bytes: include_bytes!("../../assets/images/icon.png"),
mime: MimeType::Css,
},
)
//
// MARK: fonts
//
.add_page(
"/assets/fonts/FiraCode-Bold.woff2",
StaticAsset {
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-Bold.woff2"),
mime: MimeType::Woff2,
},
)
.add_page(
"/assets/fonts/FiraCode-Light.woff2",
StaticAsset {
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-Light.woff2"),
mime: MimeType::Woff2,
},
)
.add_page(
"/assets/fonts/FiraCode-Medium.woff2",
StaticAsset {
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-Medium.woff2"),
mime: MimeType::Woff2,
},
)
.add_page(
"/assets/fonts/FiraCode-Regular.woff2",
StaticAsset {
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-Regular.woff2"),
mime: MimeType::Woff2,
},
)
.add_page(
"/assets/fonts/FiraCode-SemiBold.woff2",
StaticAsset {
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-SemiBold.woff2"),
mime: MimeType::Woff2,
},
)
.add_page(
"/assets/fonts/FiraCode-VF.woff2",
StaticAsset {
bytes: include_bytes!("../../assets/fonts/fira/FiraCode-VF.woff2"),
mime: MimeType::Woff2,
},
)
//
// MARK: icons
//
.add_page(
"/assets/fonts/fa/fa-brands-400.woff2",
StaticAsset {
bytes: include_bytes!("../../assets/fonts/fa/fa-brands-400.woff2"),
mime: MimeType::Woff2,
},
)
.add_page(
"/assets/fonts/fa/fa-regular-400.woff2",
StaticAsset {
bytes: include_bytes!("../../assets/fonts/fa/fa-regular-400.woff2"),
mime: MimeType::Woff2,
},
)
.add_page(
"/assets/fonts/fa/fa-solid-900.woff2",
StaticAsset {
bytes: include_bytes!("../../assets/fonts/fa/fa-solid-900.woff2"),
mime: MimeType::Woff2,
},
)
.add_page(
"/assets/fonts/fa/fa-brands-400.ttf",
StaticAsset {
bytes: include_bytes!("../../assets/fonts/fa/fa-brands-400.ttf"),
mime: MimeType::Ttf,
},
)
.add_page(
"/assets/fonts/fa/fa-regular-400.ttf",
StaticAsset {
bytes: include_bytes!("../../assets/fonts/fa/fa-regular-400.ttf"),
mime: MimeType::Ttf,
},
)
.add_page(
"/assets/fonts/fa/fa-solid-900.ttf",
StaticAsset {
bytes: include_bytes!("../../assets/fonts/fa/fa-solid-900.ttf"),
mime: MimeType::Ttf,
},
)
//
// MARK: htwah
//
.add_page(
"/assets/htwah/definitions.pdf",
StaticAsset {
bytes: include_bytes!("../../assets/htwah/definitions.pdf"),
mime: MimeType::Pdf,
},
)
.add_page(
"/assets/htwah/numbering.pdf",
StaticAsset {
bytes: include_bytes!("../../assets/htwah/numbering.pdf"),
mime: MimeType::Pdf,
},
)
.add_page(
"/assets/htwah/sols-a.pdf",
StaticAsset {
bytes: include_bytes!("../../assets/htwah/sols-a.pdf"),
mime: MimeType::Pdf,
},
)
.add_page(
"/assets/htwah/sols-b.pdf",
StaticAsset {
bytes: include_bytes!("../../assets/htwah/sols-b.pdf"),
mime: MimeType::Pdf,
},
)
.add_page(
"/assets/htwah/spacing-a.pdf",
StaticAsset {
bytes: include_bytes!("../../assets/htwah/spacing-a.pdf"),
mime: MimeType::Pdf,
},
)
.add_page(
"/assets/htwah/spacing-b.pdf",
StaticAsset {
bytes: include_bytes!("../../assets/htwah/spacing-b.pdf"),
mime: MimeType::Pdf,
},
);
server server
} }