Rust library to generate static websites
at fix/misc-errors 194 lines 7.8 kB view raw
1use chrono::{Datelike, Utc}; 2use maud::{DOCTYPE, Markup, PreEscaped, html}; 3mod docs_sidebars; 4mod header; 5 6use docs_sidebars::{left_sidebar, right_sidebar}; 7 8pub use header::header; 9use maudit::assets::StyleOptions; 10use maudit::content::MarkdownHeading; 11use maudit::maud::generator; 12use maudit::route::{PageContext, RenderResult}; 13 14pub struct SeoMeta { 15 pub title: String, 16 pub description: Option<String>, 17 pub canonical_url: Option<String>, 18} 19 20impl Default for SeoMeta { 21 fn default() -> Self { 22 Self { 23 title: "Maudit".to_string(), 24 description: Some("A Rust library to build static websites.".to_string()), 25 canonical_url: None, 26 } 27 } 28} 29 30impl SeoMeta { 31 /// Create a new `SeoMeta` with the given title. 32 pub fn render(&self, base_url: &Option<String>) -> Markup { 33 let base_url = base_url.as_ref().unwrap(); 34 35 let formatted_title = if self.title == "Maudit" { 36 self.title.clone() 37 } else { 38 format!("{} - Maudit", self.title) 39 }; 40 41 let description = self 42 .description 43 .clone() 44 .unwrap_or_else(|| SeoMeta::default().description.unwrap()); 45 46 let canonical_url = self.canonical_url.as_ref(); 47 48 let social_image_url = format!("{}/social-image.png", base_url); 49 50 html! { 51 title { (formatted_title) } 52 meta name="description" content=(description); 53 54 // Open Graph meta tags 55 meta property="og:title" content=(formatted_title); 56 meta property="og:description" content=(description); 57 meta property="og:type" content="website"; 58 meta property="og:image" content=(social_image_url); 59 @if let Some(canonical_url) = &canonical_url { 60 meta property="og:url" content=(canonical_url); 61 link rel="canonical" href=(canonical_url); 62 } 63 64 // Twitter Card meta tags 65 meta name="twitter:card" content="summary"; 66 meta name="twitter:title" content=(formatted_title); 67 meta name="twitter:description" content=(description); 68 meta name="twitter:image" content=(social_image_url); 69 } 70 } 71} 72 73pub fn docs_layout( 74 main: Markup, 75 ctx: &mut PageContext, 76 headings: &[MarkdownHeading], 77 seo: Option<SeoMeta>, 78) -> impl Into<RenderResult> { 79 ctx.assets.include_script("assets/docs-sidebar.ts")?; 80 81 layout( 82 html! { 83 // Second header for docs navigation (mobile only) 84 header.sticky.top-0.z-40.bg-our-white.border-b.border-borders.md:hidden.bg-linear-to-b."from-darker-white" { 85 div.flex.items-center.justify-between { 86 button id="left-sidebar-toggle" .px-4.py-3.flex.items-center.gap-x-2.text-base.font-medium.text-our-black aria-label="Toggle navigation menu" { 87 (PreEscaped(include_str!("../assets/side-menu.svg"))) 88 span { "Menu" } 89 } 90 button id="right-sidebar-toggle" .px-4.py-3.flex.items-center.gap-x-2.text-base.font-medium.text-our-black aria-label="Toggle table of contents" { 91 span { "On this page" } 92 (PreEscaped(include_str!("../assets/toc.svg"))) 93 } 94 } 95 } 96 97 // Mobile left sidebar overlay 98 div id="mobile-left-sidebar" .fixed."inset-0 bg-black/50".transition-opacity.opacity-0.pointer-events-none.z-50 { 99 div.w-80.max-w-sm.h-full.bg-our-white.overflow-y-auto.transform."-translate-x-full".transition-transform { 100 div.px-4.py-4 { 101 (left_sidebar(ctx)) 102 } 103 } 104 } 105 106 // Mobile right sidebar overlay 107 div id="mobile-right-sidebar" .fixed."inset-0 bg-black/50".transition-opacity.opacity-0.pointer-events-none.z-50.flex.justify-end { 108 div.w-80.max-w-sm.h-full.bg-our-white.overflow-y-auto.transform."translate-x-full".transition-transform { 109 div.px-4.py-4 { 110 (right_sidebar(headings)) 111 } 112 } 113 } 114 115 div.container.mx-auto."md:grid-cols-(--docs-tablet-columns)"."lg:grid-cols-(--docs-columns)".md:grid."min-h-[calc(100%-64px)]".px-6.md:px-0.pt-2.md:pt-0 { 116 aside.bg-linear-to-l."from-darker-white"."py-8"."h-full".border-r.border-r-borders.hidden.md:block."md:pr-4"."lg:px-0" { 117 (left_sidebar(ctx)) 118 } 119 main.w-full.max-w-larger-prose.mx-auto.md:pt-8.py-4.pb-8.md:pb-16.md:px-6.lg:px-0 { 120 (main) 121 } 122 aside."py-8".hidden."lg:block" { 123 (right_sidebar(headings)) 124 } 125 } 126 }, 127 true, 128 false, 129 ctx, 130 seo, 131 ) 132} 133 134pub fn layout( 135 main: Markup, 136 bottom_border: bool, 137 licenses: bool, 138 ctx: &mut PageContext, 139 seo: Option<SeoMeta>, 140) -> Result<Markup, Box<dyn std::error::Error>> { 141 ctx.assets 142 .include_style_with_options("assets/prin.css", StyleOptions { tailwind: true })?; 143 144 let seo_data = seo.unwrap_or_default(); 145 let current_year = Utc::now().year(); 146 147 Ok(html! { 148 (DOCTYPE) 149 html lang="en" { 150 head { 151 meta charset="utf-8"; 152 meta name="viewport" content="width=device-width, initial-scale=1"; 153 (generator()) 154 link rel="icon" href="/favicon.svg"; 155 (seo_data.render(ctx.base_url)) 156 } 157 body { 158 div.relative.bg-our-white { 159 (header(ctx, bottom_border)?) 160 (main) 161 footer.bg-our-black.text-white { 162 div.container.mx-auto.px-8.py-8.flex.justify-between.items-center.flex-col-reverse."sm:flex-row".gap-y-12 { 163 div.grow."basis-[0]" { 164 a.text-md.font-bold href="https://bruits.org" { 165 "Copyright © " (current_year) " Bruits." 166 } 167 @if licenses { 168 br; 169 a.text-sm href="https://www.netlify.com" { "Site powered by Netlify" } 170 p.text-sm {"Wax seal icon by " a href="https://game-icons.net/" { "Game-icons.net" } " under " a href="https://creativecommons.org/licenses/by/3.0/" { "CC BY 3.0" } } 171 } 172 } 173 div { (PreEscaped(include_str!("../assets/logo.svg")))} 174 div.flex.gap-x-6.grow.justify-end."basis-[0]".items-center { 175 a href="https://bsky.app/profile/bruits.org" { 176 span.sr-only { "Follow Maudit on Bluesky" } 177 (PreEscaped(include_str!("../assets/bsky.svg"))) 178 } 179 a href="/chat/" { 180 span.sr-only { "Join the Maudit community on Discord" } 181 (PreEscaped(include_str!("../assets/discord.svg"))) 182 } 183 a href="https://github.com/bruits/maudit" { 184 span.sr-only { "View Maudit on GitHub" } 185 (PreEscaped(include_str!("../assets/github.svg"))) 186 } 187 } 188 } 189 } 190 } 191 } 192 } 193 }) 194}