diff --git a/Cargo.lock b/Cargo.lock index af7b8f6..97d3305 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.18" @@ -151,6 +166,22 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-compression" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "zstd", + "zstd-safe", +] + [[package]] name = "async-executor" version = "1.13.0" @@ -508,6 +539,27 @@ dependencies = [ "syn_derive", ] +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -597,6 +649,11 @@ name = "cc" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] [[package]] name = "cedar-policy" @@ -2104,6 +2161,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.70" @@ -4794,8 +4860,10 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ + "async-compression", "bitflags 2.6.0", "bytes", + "futures-core", "futures-util", "http 1.1.0", "http-body 1.0.1", @@ -5566,3 +5634,31 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index d4bfb08..459c088 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,13 +9,21 @@ crate-type = ["cdylib", "rlib"] [dependencies] axum = { version = "0.7", optional = true, features = ["macros"] } console_error_panic_hook = "0.1" -leptos = { version = "0.6", features = ["rustls"] } +leptos = { version = "0.6", features = ["rustls", "nightly"] } leptos_axum = { version = "0.6", optional = true } -leptos_meta = { version = "0.6" } -leptos_router = { version = "0.6" } +leptos_meta = { version = "0.6", features = ["nightly"] } +leptos_router = { version = "0.6", features = ["nightly"] } tokio = { version = "1", features = ["rt-multi-thread"], optional = true } tower = { version = "0.4", optional = true } -tower-http = { version = "0.5", features = ["fs", "trace"], optional = true } +tower-http = { version = "0.5", features = [ + "fs", + "trace", + "cors", + "compression-br", + "compression-deflate", + "compression-zstd", + "compression-gzip", +], optional = true } wasm-bindgen = "0.2.93" thiserror = "1" tracing = { version = "0.1", optional = true } diff --git a/src/app.rs b/src/app.rs index 58afbb5..2d92dde 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,4 @@ +use crate::error_template::{AppError, ErrorTemplate}; use chrono::{Datelike, Utc}; use icondata as i; use leptos::*; @@ -13,83 +14,118 @@ pub fn App() -> impl IntoView { provide_meta_context(); view! { - - - <Meta name="hostname" content="rust-dd.com" /> - <Meta name="expected-hostname" content="rust-dd.com" /> - <Meta - name="description" - content="Discover the Rust-DD framework, enabling the application of domain-driven design (DDD) principles in Rust. Write efficient, safe, and clean code with this modern development tool." - /> - <Meta property="og:type" content="website" /> - <Meta property="og:title" content="Tech Diaries - The Official Rust-DD Developer Blog" /> - <Meta - property="og:site_name" - content="Tech Diaries - The Official Rust-DD Developer Blog" - /> - <Meta - property="og:description" - content="Discover the Rust-DD framework, enabling the application of domain-driven design (DDD) principles in Rust. Write efficient, safe, and clean code with this modern development tool." - /> - <Meta property="og:url" content="https://rust-dd.com/" /> - <Meta property="og:image" content="https://static.rust-dd.com/rust-dd_custom_bg.png" /> - <Meta property="og:image:type" content="image/png" /> - <Meta property="og:image:width" content="1200" /> - <Meta property="og:image:height" content="627" /> - <Meta name="twitter:card" content="summary_large_image" /> - <Meta name="twitter:title" content="Tech Diaries - The Official Rust-DD Developer Blog" /> - <Meta - name="twitter:description" - content="Discover the Rust-DD framework, enabling the application of domain-driven design (DDD) principles in Rust. Write efficient, safe, and clean code with this modern development tool." - /> - <Meta name="twitter:site" content="@rust_dd" /> - <Meta name="twitter:url" content="https://rust-dd.com/" /> - <Meta name="twitter:image" content="https://static.rust-dd.com/rust-dd_custom_bg.png" /> - <Meta name="twitter:image:alt" content="Rust-DD Framework" /> - <div class="overflow-auto h-screen text-white bg-[#1e1e1e]"> - <header class="fixed top-0 right-0 left-0 z-10 py-6 px-4 md:px-6 bg-[#1e1e1e]/80 backdrop-blur-md"> - <div class="container mx-auto max-w-5xl"> - <div class="flex flex-row justify-between items-center"> - <a href="/" class="text-3xl font-bold"> - blog - </a> - <div class="flex flex-row gap-3 items-center h-10"> - <a - href="https://github.com/rust-dd/blog" - rel="noopener noreferrer" - target="_blank" - > - <Icon icon=i::IoLogoGithub class="text-white size-6" /> - </a> - <a - href="https://x.com/rust_dd" - rel="noopener noreferrer" - target="_blank" - > - <Icon icon=i::FaXTwitterBrands class="text-white size-6" /> - </a> - <a href="/rss.xml" rel="noopener noreferrer" target="_blank"> - <Icon icon=i::IoLogoRss class="text-white size-6" /> + <!DOCTYPE html> + <html lang="en"> + <body> + <Stylesheet id="leptos" href="/pkg/blog.css" /> + <Title text="Tech Diaries - The Official Rust-DD Developer Blog" /> + <Meta name="hostname" content="rust-dd.com" /> + <Meta name="expected-hostname" content="rust-dd.com" /> + <Meta + name="description" + content="Discover the Rust-DD framework, enabling the application of domain-driven design (DDD) principles in Rust. Write efficient, safe, and clean code with this modern development tool." + /> + <Meta + name="keywords" + content="rust-dd, rust, ai, mathematics, embedded, web, systems, programming" + /> + <Meta name="robots" content="index, follow" /> + <Meta name="googlebot" content="index, follow" /> + + // Facebook + <Meta property="og:type" content="website" /> + <Meta + property="og:title" + content="Tech Diaries - The Official Rust-DD Developer Blog" + /> + <Meta + property="og:site_name" + content="Tech Diaries - The Official Rust-DD Developer Blog" + /> + <Meta + property="og:description" + content="Discover the Rust-DD framework, enabling the application of domain-driven design (DDD) principles in Rust. Write efficient, safe, and clean code with this modern development tool." + /> + <Meta property="og:url" content="https://rust-dd.com/" /> + <Meta + property="og:image" + content="https://static.rust-dd.com/rust-dd_custom_bg.png" + /> + <Meta property="og:image:type" content="image/png" /> + <Meta property="og:image:width" content="1200" /> + <Meta property="og:image:height" content="627" /> + + // Twitter + <Meta name="twitter:card" content="summary_large_image" /> + <Meta + name="twitter:title" + content="Tech Diaries - The Official Rust-DD Developer Blog" + /> + <Meta + name="twitter:description" + content="Discover the Rust-DD framework, enabling the application of domain-driven design (DDD) principles in Rust. Write efficient, safe, and clean code with this modern development tool." + /> + <Meta name="twitter:site" content="@rust_dd" /> + <Meta name="twitter:url" content="https://rust-dd.com/" /> + <Meta + name="twitter:image" + content="https://static.rust-dd.com/rust-dd_custom_bg.png" + /> + <Meta name="twitter:image:alt" content="Rust-DD Framework" /> + </body> + </html> + <Router fallback=|| { + let mut outside_errors = Errors::default(); + outside_errors.insert_with_default_key(AppError::NotFound); + view! { <ErrorTemplate outside_errors /> }.into_view() + }> + <div class="overflow-auto h-screen text-white bg-[#1e1e1e]"> + <header class="fixed top-0 right-0 left-0 z-10 py-6 px-4 md:px-6 bg-[#1e1e1e]/80 backdrop-blur-md"> + <div class="container mx-auto max-w-5xl"> + <div class="flex flex-row justify-between items-center"> + <a href="/" class="text-3xl font-bold"> + blog </a> + <div class="flex flex-row gap-3 items-center h-10"> + <a + href="https://github.com/rust-dd/blog" + rel="noopener noreferrer" + target="_blank" + > + <Icon icon=i::IoLogoGithub class="text-white size-6" /> + </a> + <a + href="https://x.com/rust_dd" + rel="noopener noreferrer" + target="_blank" + > + <Icon icon=i::FaXTwitterBrands class="text-white size-6" /> + </a> + <a href="/rss.xml" rel="noopener noreferrer" target="_blank"> + <Icon icon=i::IoLogoRss class="text-white size-6" /> + </a> + </div> </div> </div> - </div> - </header> - <main class="container flex flex-col gap-8 py-12 px-4 mx-auto mt-16 max-w-5xl md:px-0"> - <Router> + </header> + <main class="container flex flex-col gap-8 py-12 px-4 mx-auto mt-16 max-w-5xl md:px-0"> <Routes> - <Route path="/" view=home::Component ssr=SsrMode::Async /> - <Route path="/post/:slug/" view=post::Component ssr=SsrMode::Async /> + <Route path="/" view=move || view! { <home::Component /> } /> + <Route + path="/post/:slug/" + view=move || view! { <post::Component /> } + ssr=SsrMode::Async + /> </Routes> - </Router> - </main> - <footer class="fixed right-0 bottom-0 left-0 z-10 py-4 text-center bg-[#1e1e1e]/80 backdrop-blur-md"> - <p class="text-gray-400"> - Powered by <a href="https://github.com/rust-dd" class="text-[#ffbd2e]"> - rust-dd - </a> {" © "} {Utc::now().year()} - </p> - </footer> - </div> + </main> + <footer class="fixed right-0 bottom-0 left-0 z-10 py-4 text-center bg-[#1e1e1e]/80 backdrop-blur-md"> + <p class="text-gray-400"> + Powered by <a href="https://github.com/rust-dd" class="text-[#ffbd2e]"> + rust-dd + </a> {" © "} {Utc::now().year()} + </p> + </footer> + </div> + </Router> } } diff --git a/src/error_template.rs b/src/error_template.rs new file mode 100644 index 0000000..0b2658a --- /dev/null +++ b/src/error_template.rs @@ -0,0 +1,84 @@ +use http::status::StatusCode; +use icondata as i; +use leptos::*; +use leptos_icons::*; +use thiserror::Error; + +#[derive(Clone, Debug, Error)] +pub enum AppError { + #[error("Not Found")] + NotFound, +} + +impl AppError { + pub fn status_code(&self) -> StatusCode { + match self { + AppError::NotFound => StatusCode::NOT_FOUND, + } + } +} + +// A basic function to display errors served by the error boundaries. +// Feel free to do more complicated things here than just displaying the error. +#[component] +pub fn ErrorTemplate( + #[prop(optional)] outside_errors: Option<Errors>, + #[prop(optional)] errors: Option<RwSignal<Errors>>, +) -> impl IntoView { + let errors = match outside_errors { + Some(e) => create_rw_signal(e), + None => match errors { + Some(e) => e, + None => panic!("No Errors found and we expected errors!"), + }, + }; + // Get Errors from Signal + let errors = errors.get_untracked(); + + // Downcast lets us take a type that implements `std::error::Error` + let errors: Vec<AppError> = errors + .into_iter() + .filter_map(|(_k, v)| v.downcast_ref::<AppError>().cloned()) + .collect(); + println!("Errors: {errors:#?}"); + + // Only the response code for the first error is actually sent from the server + // this may be customized by the specific application + #[cfg(feature = "ssr")] + { + use leptos_axum::ResponseOptions; + let response = use_context::<ResponseOptions>(); + if let Some(response) = response { + response.set_status(errors[0].status_code()); + } + } + + view! { + <div class="grid place-content-center px-4 h-screen antialiased bg-white"> + <h1 class="mb-6 text-center">{if errors.len() > 1 { "Errors" } else { "Error" }}</h1> + <For + // a function that returns the items we're iterating over; a signal is fine + each=move || { errors.clone().into_iter().enumerate() } + // a unique key for each item as a reference + key=|(index, _error)| *index + // renders each item to a view + children=move |error| { + let error_string = error.1.to_string(); + let error_code = error.1.status_code(); + view! { + <h1 class="text-xl tracking-widest text-gray-500 uppercase"> + {error_code.to_string()}| {error_string} + </h1> + <a + href="/" + class="flex gap-1 justify-center items-center mt-6 text-center duration-200 hover:text-[#68b5fc]" + > + <Icon width="1.1em" height="1.1em" icon=i::BiArrowBackRegular /> + Go back home + </a> + } + } + /> + </div> + } +} diff --git a/src/lib.rs b/src/lib.rs index 5383143..bfc11ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod api; pub mod app; +pub mod error_template; #[cfg(feature = "ssr")] pub mod fileserv; pub mod home; diff --git a/src/main.rs b/src/main.rs index cea7d02..73d4ad8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,10 @@ async fn main() { opt::auth::Root, Surreal, }; + use tower_http::compression::predicate::{NotForContentType, SizeAbove}; + use tower_http::compression::{CompressionLayer, Predicate}; use tower_http::trace::TraceLayer; + use tower_http::CompressionLevel; tracing_subscriber::fmt() .with_file(true) @@ -150,6 +153,19 @@ async fn main() { .layer(TraceLayer::new_for_http()) .layer(axum::middleware::from_fn(redirect_www)), ) + .layer( + CompressionLayer::new() + .quality(CompressionLevel::Default) + .compress_when( + SizeAbove::new(1500) + .and(NotForContentType::GRPC) + .and(NotForContentType::IMAGES) + .and(NotForContentType::const_new("application/xml")) + .and(NotForContentType::const_new("application/javascript")) + .and(NotForContentType::const_new("application/wasm")) + .and(NotForContentType::const_new("text/css")), + ), + ) .with_state(app_state); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); diff --git a/src/post.rs b/src/post.rs index 1135681..b43b49e 100644 --- a/src/post.rs +++ b/src/post.rs @@ -32,21 +32,6 @@ pub fn Component() -> impl IntoView { let post = post.clone().unwrap_or_default(); view! { <Title text=post.title.to_string() /> - <Meta name="description" content=post.summary.to_string() /> - <Meta property="og:type" content="article" /> - <Meta property="og:title" content=post.title.to_string() /> - <Meta property="og:description" content=post.summary.to_string() /> - <Meta name="twitter:site" content="@rust_dd" /> - <Meta name="twitter:card" content="summary_large_image" /> - <Meta name="twitter:title" content=post.title.to_string() /> - <Meta name="twitter:description" content=post.summary.to_string() /> - {post - .tags - .into_iter() - .map(|tag| { - view! { <Meta name="keywords" content=tag.to_string() /> } - }) - .collect::<Vec<_>>()} <article> <div class="flex flex-col gap-4 mx-auto max-w-3xl"> <p class="text-4xl font-semibold">{post.title.clone()}</p>