···11+---
22+maudit: patch
33+---
44+55+Add a new `base_url` setting and `canonical_url()` method on PageContext to make it easier to build absolute URLs inside pages
+25
assets/README.md
···11+# Press Resources
22+33+## Logo
44+55+The Maudit logo is available in both light and dark versions, for use on different backgrounds.
66+77+For light backgrounds, use the dark version:
88+99+
1010+1111+For dark backgrounds, use the light version:
1212+1313+
1414+1515+These logos may be used in promotional materials, websites, and other media about Maudit
1616+1717+## Colors
1818+1919+Maudit uses the following color palette:
2020+2121+| Color Name | Hex Code | Usage |
2222+| ---------- | -------- | --------------------------------------------------------- |
2323+| Primary | #ab3038 | Main brand color |
2424+| Background | #fafafa | Background color for light mode, text color for dark mode |
2525+| Background | #0d0d0d | Background color for dark mode, text color for light mode |
···11+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
22+<!-- Created with Inkscape (http://www.inkscape.org/) -->
33+44+<svg
55+ width="1500mm"
66+ height="1500mm"
77+ viewBox="0 0 1500 1500"
88+ version="1.1"
99+ id="svg1"
1010+ xml:space="preserve"
1111+ inkscape:version="1.3.2 (091e20e, 2023-11-25)"
1212+ sodipodi:docname="social-image.svg"
1313+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
1414+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
1515+ xmlns="http://www.w3.org/2000/svg"
1616+ xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
1717+ id="namedview1"
1818+ pagecolor="#505050"
1919+ bordercolor="#eeeeee"
2020+ borderopacity="1"
2121+ inkscape:showpageshadow="0"
2222+ inkscape:pageopacity="0"
2323+ inkscape:pagecheckerboard="0"
2424+ inkscape:deskcolor="#505050"
2525+ inkscape:document-units="mm"
2626+ inkscape:zoom="0.073212926"
2727+ inkscape:cx="3694.7028"
2828+ inkscape:cy="3585.4324"
2929+ inkscape:window-width="1920"
3030+ inkscape:window-height="1052"
3131+ inkscape:window-x="1"
3232+ inkscape:window-y="0"
3333+ inkscape:window-maximized="0"
3434+ inkscape:current-layer="layer1" /><defs
3535+ id="defs1" /><g
3636+ inkscape:label="Layer 1"
3737+ inkscape:groupmode="layer"
3838+ id="layer1"><rect
3939+ style="fill:#ab3038;fill-opacity:1;stroke-width:0;paint-order:stroke fill markers"
4040+ id="rect1"
4141+ width="1500"
4242+ height="1500"
4343+ x="0"
4444+ y="0" /><g
4545+ style="fill:#fafafa;fill-opacity:1"
4646+ id="g1"
4747+ transform="matrix(3.5226246,0,0,3.5226246,121.21589,255.07124)"><path
4848+ fill="currentColor"
4949+ d="m 303,267 c -51,-6 -51,-6 -83,-5 h -32 l -63,7 A 1419,1419 0 0 0 33,281 L 22,173 12,66 9,65 C 3,63 -1,55 0,46 c 0,-5 1,-6 6,-9 5,-4 5,-4 11,-3 5,0 7,1 9,3 3,3 3,3 4,12 0,8 0,10 -2,12 l -2,2 15,19 c 22,27 58,67 59,66 l 4,-15 a 1924,1924 0 0 1 13,-64 l -4,-8 c -3,-3 -7,-11 -9,-16 l -3,-10 6,-7 c 6,-6 6,-7 13,-8 9,-3 13,-1 18,5 5,8 5,11 1,23 l -4,12 7,21 c 7,19 7,19 27,52 20,32 25,39 26,36 l 3,-14 14,-60 10,-48 c 0,-2 -2,-3 -5,-6 l -5,-7 1,-6 c 1,-6 2,-7 7,-11 4,-2 6,-3 10,-2 6,0 8,1 12,7 4,7 5,15 3,19 l -6,5 -5,3 c 0,5 48,117 49,117 4,0 5,-5 18,-63 l 13,-59 -6,-4 -5,-3 -1,-10 -1,-10 6,-8 6,-8 8,-1 c 8,0 8,0 13,3 5,3 6,4 7,11 2,8 2,15 0,18 l -8,6 -6,4 8,34 8,34 8,81 c 7,80 7,81 5,81 z"
5050+ id="path1"
5151+ style="fill:#fafafa;fill-opacity:1" /></g></g></svg>
+3
crates/maudit-cli/src/dev.rs
···165165 let start_time = Instant::now();
166166167167 // Run the build command
168168+ // TODO: Right now we always run `cargo run`, but for the sake of performance, we should detect in advance
169169+ // if the change even needs a full rebuild (e.g. if only content files changed, we can skip rebuilding the Rust binary)
170170+ // Perhaps this could be done by parsing the `.d` files that cargo generates.
168171 let child = std::process::Command::new("cargo")
169172 .args(["run", "--quiet"])
170173 .envs([
···4343/// }
4444/// ```
4545pub struct BuildOptions {
4646+ /// Base URL for the site, e.g. `https://example.com` or `https://example.com/subdir`.
4747+ /// This value is used to generate canonical URLs and can be used wherever the full site URL is needed (e.g. in SEO meta tags) through [`PageContext::base_url`](crate::route::PageContext::base_url) in pages.
4848+ pub base_url: Option<String>,
4949+4650 pub output_dir: PathBuf,
4751 pub static_dir: PathBuf,
4852···126130impl Default for BuildOptions {
127131 fn default() -> Self {
128132 Self {
133133+ base_url: None,
129134 output_dir: "dist".into(),
130135 static_dir: "static".into(),
131136 clean_output_dir: true,
···1010pub fn build_website(
1111 routes: &[&dyn FullRoute],
1212 mut content_sources: ContentSources,
1313- options: BuildOptions,
1313+ options: &BuildOptions,
1414) -> Result<(), Box<dyn std::error::Error>> {
1515 // Initialize all the content sources;
1616 content_sources.init_all();
···35353636 // Every page has a PageContext, which contains information about the current page, as well as access to content and assets.
3737 let url = route.url(¶ms);
3838- let mut ctx = PageContext::from_static_route(&content, &mut page_assets, &url);
3838+ let mut ctx = PageContext::from_static_route(
3939+ &content,
4040+ &mut page_assets,
4141+ &url,
4242+ &options.base_url,
4343+ );
39444045 let content = route.build(&mut ctx)?;
4146···78837984 // Here the context is created from a dynamic route, as the context has to include the page parameters and properties.
8085 let url = route.url(params);
8181- let mut ctx =
8282- PageContext::from_dynamic_route(&page, &content, &mut page_assets, &url);
8686+ let mut ctx = PageContext::from_dynamic_route(
8787+ &page,
8888+ &content,
8989+ &mut page_assets,
9090+ &url,
9191+ &options.base_url,
9292+ );
83938494 // Everything below here is the same as for static routes.
8595
···60606161 // Every page has a PageContext, which contains information about the current route, as well as access to content and assets.
6262 let url = route.url(¶ms);
6363- let mut ctx = PageContext::from_static_route(&content, &mut route_assets, &url);
6363+ let mut ctx = PageContext::from_static_route(&content, &mut route_assets, &url, &options.base_url);
64646565 let content = route.build(&mut ctx)?;
6666···152152 &content,
153153 &mut page_assets,
154154 &url,
155155+ &options.base_url
155156 );
156157157158 // Everything after this is the same as for static routes.
+65-1
website/src/layout.rs
···1010use maudit::maud::generator;
1111use maudit::route::{PageContext, RenderResult};
12121313+pub struct SeoMeta {
1414+ pub title: String,
1515+ pub description: Option<String>,
1616+ pub canonical_url: Option<String>,
1717+}
1818+1919+impl Default for SeoMeta {
2020+ fn default() -> Self {
2121+ Self {
2222+ title: "Maudit".to_string(),
2323+ description: Some("A modern static site generator built with Rust".to_string()),
2424+ canonical_url: None,
2525+ }
2626+ }
2727+}
2828+2929+impl SeoMeta {
3030+ /// Create a new `SeoMeta` with the given title.
3131+ pub fn render(&self, base_url: &Option<String>) -> Markup {
3232+ let base_url = base_url.as_ref().unwrap();
3333+3434+ let formatted_title = if self.title == "Maudit" {
3535+ self.title.clone()
3636+ } else {
3737+ format!("{} - Maudit", self.title)
3838+ };
3939+4040+ let description = self
4141+ .description
4242+ .clone()
4343+ .unwrap_or_else(|| SeoMeta::default().description.unwrap());
4444+4545+ let canonical_url = self.canonical_url.as_ref();
4646+4747+ let social_image_url = format!("{}/social-image.png", base_url);
4848+4949+ html! {
5050+ title { (formatted_title) }
5151+ meta name="description" content=(description);
5252+5353+ // Open Graph meta tags
5454+ meta property="og:title" content=(formatted_title);
5555+ meta property="og:description" content=(description);
5656+ meta property="og:type" content="website";
5757+ meta property="og:image" content=(social_image_url);
5858+ @if let Some(canonical_url) = &canonical_url {
5959+ meta property="og:url" content=(canonical_url);
6060+ link rel="canonical" href=(canonical_url);
6161+ }
6262+6363+ // Twitter Card meta tags
6464+ meta name="twitter:card" content="summary_large_image";
6565+ meta name="twitter:title" content=(formatted_title);
6666+ meta name="twitter:description" content=(description);
6767+ meta name="twitter:image" content=(social_image_url);
6868+ }
6969+ }
7070+}
7171+1372pub fn docs_layout(
1473 main: Markup,
1574 ctx: &mut PageContext,
1675 headings: &[MarkdownHeading],
7676+ seo: Option<SeoMeta>,
1777) -> impl Into<RenderResult> {
1878 layout(
1979 html! {
···3292 true,
3393 false,
3494 ctx,
9595+ seo,
3596 )
3697}
3798···40101 bottom_border: bool,
41102 licenses: bool,
42103 ctx: &mut PageContext,
104104+ seo: Option<SeoMeta>,
43105) -> impl Into<RenderResult> {
44106 ctx.assets
45107 .include_style_with_options("assets/prin.css", StyleOptions { tailwind: true });
46108109109+ let seo_data = seo.unwrap_or_default();
110110+47111 html! {
48112 (DOCTYPE)
49113 html lang="en" {
50114 head {
51115 meta charset="utf-8";
52116 meta name="viewport" content="width=device-width, initial-scale=1";
5353- title { "Maudit" }
54117 (generator())
55118 link rel="icon" href="/favicon.svg";
119119+ (seo_data.render(&ctx.base_url))
56120 }
57121 body {
58122 div.bg-our-white {