···6677- [9cd5fdd](https://github.com/bruits/maudit/commit/9cd5fdd8abe3044bd09d48b96217e3a0d2878b13) Updates default quality for webp to 80 to match sharp — Thanks @Princesseuh!
8899-109## 0.5.0
11101211### Minor changes
···1413- [d5a7fad](https://github.com/bruits/maudit/commit/d5a7fad563e9642be46b24d8db500e753c1175f5) The data URI and average RGBA for thumbnails is now calculated lazily, as such the `average_rgba` and `data_uri` fields have been replaced by methods. — Thanks @Princesseuh!
1514- [0403ac9](https://github.com/bruits/maudit/commit/0403ac9996f9d4e79945758fe06e7510729e383e) Add `is_dev()` function to allow one to toggle off things whenever running in dev — Thanks @Princesseuh!
1615- [39db004](https://github.com/bruits/maudit/commit/39db004b63ab7aa582a92593082e1261bae55b92) Added support for shortcodes in Markdown. Shortcodes allows you to substitute custom content in your Markdown files. This feature is useful for embedding dynamic content or reusable components within your Markdown documents.
1717-1616+1817 For instance, you might define a shortcode for embedding YouTube videos using only the video ID, or for inserting custom alerts or notes.
1919-1818+2019 ```markdown
2120 {{ youtube id="FbJ63spk48s" }}
2221 ```
2323-2222+2423 Would render to:
2525-2424+2625 ```html
2726 <iframe
2827 width="560"
···3534 allowfullscreen
3635 ></iframe>
3736 ```
3838-3737+3938 To define and register shortcodes, pass a MarkdownShortcodes instance to the MarkdownOptions when rendering Markdown content.
4040-3939+4140 ```rust
4241 let mut shortcodes = MarkdownShortcodes::new();
4343-4242+4443 shortcodes.register("youtube", |args, _ctx| {
4544 let id: String = args.get_required("id");
4645 format!(
···4847 id
4948 )
5049 });
5151-5050+5251 MarkdownOptions {
5352 shortcodes,
5453 ..Default::default()
5554 }
5656-5555+5756 // Then pass options to, i.e. glob_markdown in a content source
5857 ```
5959-5858+6059 Note that shortcodes are expanded before Markdown is rendered, so you can use shortcodes anywhere in your Markdown content, for instance in your frontmatter. Additionally, shortcodes may expand to Markdown content, which will then be rendered as part of the overall Markdown rendering process. — Thanks @Princesseuh!
61606261### Patch changes
63626463- [d5a7fad](https://github.com/bruits/maudit/commit/d5a7fad563e9642be46b24d8db500e753c1175f5) Added caching mechanism to placeholder and image transformation — Thanks @Princesseuh!
6565-66646765## 0.4.0
6866···9694```
97959896See the [Assets documentation](https://maudit.org/docs/assets/) for more details. — Thanks @Princesseuh!
9797+9998- [52eda9e](https://github.com/bruits/maudit/commit/52eda9ea4eac8efd3efd945d00f39a1b99f284ab) Adds support for dynamic routes with properties. In addition to its parameters, a dynamic route can now provide additional properties that can be used during rendering.
10099101100```rs
···126125 ).into()
127126 }
128127129129- fn routes(&self, ctx: &mut DynamicRouteContext) -> Routes<Params, Props> {
128128+ fn routes(&self, ctx: &DynamicRouteContext) -> Routes<Params, Props> {
130129 vec![Route::from_params_and_props(
131130 Params {
132131 slug: "hello-world".to_string(),
···145144### Patch changes
146145147146- Updated dependencies: maudit-macros@0.4.0
148148-
+39-16
crates/maudit/src/assets.rs
···33use log::debug;
44use rustc_hash::FxHashSet;
55use std::hash::Hash;
66-use std::sync::OnceLock;
76use std::time::Instant;
87use std::{fs, path::PathBuf};
98use xxhash_rust::xxh3::xxh3_64;
···18171918#[derive(Default)]
2019pub struct PageAssets {
2121- pub(crate) images: FxHashSet<Image>,
2222- pub(crate) scripts: FxHashSet<Script>,
2323- pub(crate) styles: FxHashSet<Style>,
2424-2525- pub(crate) included_styles: Vec<Style>,
2626- pub(crate) included_scripts: Vec<Script>,
2020+ pub images: FxHashSet<Image>,
2121+ pub scripts: FxHashSet<Script>,
2222+ pub styles: FxHashSet<Style>,
27232824 pub(crate) assets_dir: PathBuf,
2925}
30263127impl PageAssets {
2828+ pub fn new(assets_dir: PathBuf) -> Self {
2929+ Self {
3030+ assets_dir,
3131+ ..Default::default()
3232+ }
3333+ }
3434+3535+ pub fn assets(&self) -> impl Iterator<Item = &dyn Asset> {
3636+ self.images
3737+ .iter()
3838+ .map(|asset| asset as &dyn Asset)
3939+ .chain(self.scripts.iter().map(|asset| asset as &dyn Asset))
4040+ .chain(self.styles.iter().map(|asset| asset as &dyn Asset))
4141+ }
4242+4343+ /// Get all styles that are marked as included
4444+ pub fn included_styles(&self) -> impl Iterator<Item = &Style> {
4545+ self.styles.iter().filter(|s| s.included)
4646+ }
4747+4848+ /// Get all scripts that are marked as included
4949+ pub fn included_scripts(&self) -> impl Iterator<Item = &Script> {
5050+ self.scripts.iter().filter(|s| s.included)
5151+ }
5252+3253 /// Add an image to the page assets, causing the file to be created in the output directory. The image is resolved relative to the current working directory.
3354 ///
3455 /// The image will not automatically be included in the page, but can be included through the `.url()` method on the returned `Image` object.
···6485 } else {
6586 Some(options)
6687 },
6767- __cache_placeholder: OnceLock::new(),
6888 };
69897090 self.images.insert(image.clone());
···94114 path: path.clone(),
95115 assets_dir: self.assets_dir.clone(),
96116 hash: calculate_hash(&path, None),
117117+ included: false,
97118 };
9811999120 self.scripts.insert(script.clone());
···115136 path: path.clone(),
116137 assets_dir: self.assets_dir.clone(),
117138 hash: calculate_hash(&path, None),
139139+ included: true,
118140 };
119141120120- self.scripts.insert(script.clone());
121121- self.included_scripts.push(script);
142142+ self.scripts.insert(script);
122143 }
123144124145 /// Add a style to the page assets, causing the file to be created in the output directory. The style is resolved relative to the current working directory.
···149170 assets_dir: self.assets_dir.clone(),
150171 hash: calculate_hash(&path, Some(HashConfig::Style(&options))),
151172 tailwind: options.tailwind,
173173+ included: false,
152174 };
153175154176 self.styles.insert(style.clone());
···184206 assets_dir: self.assets_dir.clone(),
185207 hash,
186208 tailwind: options.tailwind,
209209+ included: true,
187210 };
188211189189- self.styles.insert(style.clone());
190190- self.included_styles.push(style);
212212+ self.styles.insert(style);
191213 }
192214}
193215···333355 page_assets.include_style(temp_dir.join("style.css"));
334356335357 assert!(page_assets.styles.len() == 1);
336336- assert!(page_assets.included_styles.len() == 1);
358358+ assert!(page_assets.included_styles().count() == 1);
337359 }
338360339361 #[test]
···359381 page_assets.include_script(temp_dir.join("script.js"));
360382361383 assert!(page_assets.scripts.len() == 1);
362362- assert!(page_assets.included_scripts.len() == 1);
384384+ assert!(page_assets.included_scripts().count() == 1);
363385 }
364386365387 #[test]
···459481 let image_webp = page_assets.add_image_with_options(
460482 &image_path,
461483 ImageOptions {
462462- format: Some(ImageFormat::Webp),
484484+ format: Some(ImageFormat::WebP),
463485 ..Default::default()
464486 },
465487 );
···476498 ImageOptions {
477499 width: Some(100),
478500 height: Some(100),
479479- format: Some(ImageFormat::Webp),
501501+ format: Some(ImageFormat::WebP),
480502 },
481503 );
482504···617639 Some(HashConfig::Style(&StyleOptions::default())),
618640 ),
619641 tailwind: false,
642642+ included: false,
620643 };
621644622645 assert_ne!(
···11use core::panic;
22use std::{
33 env,
44- fs::{self, File, remove_dir_all},
55- io::{self, Write},
44+ fs::{self},
55+ io::{self},
66 path::{Path, PathBuf},
77 process::Command,
88 str::FromStr,
···1313use crate::{
1414 BuildOptions, BuildOutput,
1515 assets::{
1616- self,
1616+ self, PageAssets,
1717 image_cache::{IMAGE_CACHE_DIR, ImageCache},
1818 },
1919 build::images::process_image,
2020- content::{Content, ContentSources},
2020+ content::{ContentSources, PageContent},
2121 errors::BuildError,
2222 is_dev,
2323 logging::print_title,
2424 page::{DynamicRouteContext, FullPage, RenderResult, RouteContext, RouteParams, RouteType},
2525- route::{
2626- ParameterDef, extract_params_from_raw_route, get_route_file_path,
2727- get_route_type_from_route_params, get_route_url,
2828- },
2925};
3026use colored::{ColoredString, Colorize};
3127use log::{debug, info, trace};
···161157 // Create a directory for the output
162158 trace!(target: "build", "Setting up required directories...");
163159 let dist_dir = PathBuf::from_str(&options.output_dir)?;
164164- let assets_dir = PathBuf::from_str(&options.output_dir)?.join(&options.assets_dir);
165165- let tmp_dir = dist_dir.join("_tmp");
160160+ let final_assets_dir = PathBuf::from_str(&options.output_dir)?.join(&options.assets_dir);
166161 let static_dir = PathBuf::from_str(&options.static_dir)?;
167162168163 let old_dist_tmp_dir = if options.clean_output_dir {
···183178 });
184179185180 fs::create_dir_all(&dist_dir)?;
186186- fs::create_dir_all(&assets_dir)?;
181181+ fs::create_dir_all(&final_assets_dir)?;
187182188183 info!(target: "build", "Output directory: {}", dist_dir.to_string_lossy());
189184190185 let content_sources_start = Instant::now();
191186 print_title("initializing content sources");
192192- content_sources.0.iter_mut().for_each(|source| {
187187+ content_sources.sources_mut().iter_mut().for_each(|source| {
193188 let source_start = Instant::now();
194189 source.init();
195190···224219 ..Default::default()
225220 };
226221227227- #[allow(clippy::mutable_key_type)] // Image's Hash does not depend on mutable fields
228222 let mut build_pages_images: FxHashSet<assets::Image> = FxHashSet::default();
229223 let mut build_pages_scripts: FxHashSet<assets::Script> = FxHashSet::default();
230224 let mut build_pages_styles: FxHashSet<assets::Style> = FxHashSet::default();
···235229 // faster in all cases, making it sometimes even slower due to the overhead. It'd be great to investigate and benchmark
236230 // this.
237231 for route in routes {
238238- let params_def = extract_params_from_raw_route(&route.route_raw());
239239- let route_type = get_route_type_from_route_params(¶ms_def);
240240- match route_type {
232232+ match route.route_type() {
241233 RouteType::Static => {
242234 let route_start = Instant::now();
243243- let mut page_assets = assets::PageAssets {
244244- assets_dir: options.assets_dir.clone().into(),
245245- ..Default::default()
246246- };
247235248248- let params = RouteParams(FxHashMap::default());
236236+ let content = PageContent::new(content_sources);
237237+ let mut page_assets = PageAssets::new(options.assets_dir.clone().into());
249238250250- let mut content = Content::new(&content_sources.0);
251251- let mut ctx = RouteContext {
252252- raw_params: ¶ms,
253253- content: &mut content,
254254- assets: &mut page_assets,
255255- current_url: get_route_url(&route.route_raw(), ¶ms_def, ¶ms),
239239+ let params = RouteParams::default();
240240+ let url = route.url(¶ms);
256241257257- // Static routes have no params or props
258258- params: &(),
259259- props: &(),
260260- };
242242+ let result = route.build(&mut RouteContext::from_static_route(
243243+ &content,
244244+ &mut page_assets,
245245+ url.clone(),
246246+ ))?;
261247262262- let (file_path, mut file) =
263263- create_route_file(*route, ¶ms_def, ¶ms, &dist_dir)?;
264264- let result = route.render_internal(&mut ctx);
248248+ let file_path = &dist_dir.join(route.file_path(¶ms));
265249266266- finish_route(
267267- result,
268268- &mut file,
269269- &page_assets.included_styles,
270270- &page_assets.included_scripts,
271271- route.route_raw(),
272272- )?;
250250+ write_route_file(&result, file_path)?;
273251274274- info!(target: "pages", "{} -> {} {}", get_route_url(&route.route_raw(), ¶ms_def, ¶ms), file_path.to_string_lossy().dimmed(), format_elapsed_time(route_start.elapsed(), &route_format_options));
252252+ info!(target: "pages", "{} -> {} {}", url, file_path.to_string_lossy().dimmed(), format_elapsed_time(route_start.elapsed(), &route_format_options));
275253276254 build_pages_images.extend(page_assets.images);
277255 build_pages_scripts.extend(page_assets.scripts);
···286264 page_count += 1;
287265 }
288266 RouteType::Dynamic => {
289289- let mut dynamic_content = Content::new(&content_sources.0);
290290- let mut dynamic_route_context = DynamicRouteContext {
291291- content: &mut dynamic_content,
292292- };
293293-294294- let routes = route.routes_internal(&mut dynamic_route_context);
267267+ let routes = route.routes_internal(&DynamicRouteContext {
268268+ content: &PageContent::new(content_sources),
269269+ });
295270296271 if routes.is_empty() {
297272 info!(target: "build", "{} is a dynamic route, but its implementation of Page::routes returned an empty Vec. No pages will be generated for this route.", route.route_raw().to_string().bold());
···300275 info!(target: "build", "{}", route.route_raw().to_string().bold());
301276 }
302277303303- for (params, typed_params, props) in routes {
304304- let mut pages_assets = assets::PageAssets {
305305- assets_dir: options.assets_dir.clone().into(),
306306- ..Default::default()
307307- };
278278+ let content = PageContent::new(content_sources);
279279+ for dynamic_route in routes {
308280 let route_start = Instant::now();
309309- let mut content = Content::new(&content_sources.0);
310310- let mut ctx = RouteContext {
311311- raw_params: ¶ms,
312312- params: typed_params.as_ref(),
313313- props: props.as_ref(),
314314- content: &mut content,
315315- assets: &mut pages_assets,
316316- current_url: get_route_url(&route.route_raw(), ¶ms_def, ¶ms),
317317- };
318281319319- let (file_path, mut file) =
320320- create_route_file(*route, ¶ms_def, ¶ms, &dist_dir)?;
282282+ let mut page_assets = PageAssets::new(options.assets_dir.clone().into());
283283+284284+ let url = route.url(&dynamic_route.0);
285285+286286+ let content = route.build(&mut RouteContext::from_dynamic_route(
287287+ &dynamic_route,
288288+ &content,
289289+ &mut page_assets,
290290+ url,
291291+ ))?;
292292+293293+ let file_path = &dist_dir.join(route.file_path(&dynamic_route.0));
321294322322- let result = route.render_internal(&mut ctx);
295295+ write_route_file(&content, file_path)?;
296296+297297+ info!(target: "pages", "├─ {} {}", file_path.to_string_lossy().dimmed(), format_elapsed_time(route_start.elapsed(), &route_format_options));
298298+299299+ build_pages_images.extend(page_assets.images);
300300+ build_pages_scripts.extend(page_assets.scripts);
301301+ build_pages_styles.extend(page_assets.styles);
323302324303 build_metadata.add_page(
325304 route.route_raw().to_string(),
326305 file_path.to_string_lossy().to_string(),
327327- Some(params.0),
306306+ Some(dynamic_route.0.0),
328307 );
329308330330- finish_route(
331331- result,
332332- &mut file,
333333- &pages_assets.included_styles,
334334- &pages_assets.included_scripts,
335335- route.route_raw(),
336336- )?;
337337-338338- info!(target: "pages", "├─ {} {}", file_path.to_string_lossy().dimmed(), format_elapsed_time(route_start.elapsed(), &route_format_options));
339339-340340- build_pages_images.extend(pages_assets.images);
341341- build_pages_scripts.extend(pages_assets.scripts);
342342- build_pages_styles.extend(pages_assets.styles);
343343-344309 page_count += 1;
345310 }
346311 }
···398363 BundlerOptions {
399364 input: Some(bundler_inputs),
400365 minify: Some(rolldown::RawMinifyOptions::Bool(!is_dev())),
401401- dir: Some(assets_dir.to_string_lossy().to_string()),
366366+ dir: Some(final_assets_dir.to_string_lossy().to_string()),
402367 module_types: Some(module_types_hashmap),
403368404369 ..Default::default()
···434399 let start_time = Instant::now();
435400 build_pages_images.par_iter().for_each(|image| {
436401 let start_process = Instant::now();
437437- let dest_path = assets_dir.join(image.final_file_name());
402402+ let dest_path = final_assets_dir.join(image.final_file_name());
438403439404 if let Some(image_options) = &image.options {
440405 let final_filename = image.final_file_name();
···489454 info!(target: "build", "{}", format!("Assets copied in {}", format_elapsed_time(assets_start.elapsed(), &FormatElapsedTimeOptions::default())).bold());
490455 }
491456492492- // Remove temporary files
493493- let _ = remove_dir_all(&tmp_dir);
494494-495457 info!(target: "SKIP_FORMAT", "{}", "");
496458 info!(target: "build", "{}", format!("Build completed in {}", format_elapsed_time(build_start.elapsed(), §ion_format_options)).bold());
497459···531493 Ok(())
532494}
533495534534-fn create_route_file(
535535- route: &dyn FullPage,
536536- params_def: &Vec<ParameterDef>,
537537- params: &RouteParams,
538538- dist_dir: &Path,
539539-) -> Result<(PathBuf, File), Box<dyn std::error::Error>> {
540540- let file_path = dist_dir.join(get_route_file_path(
541541- &route.route_raw(),
542542- params_def,
543543- params,
544544- route.is_endpoint(),
545545- ));
546546-496496+fn write_route_file(content: &[u8], file_path: &PathBuf) -> Result<(), io::Error> {
547497 // Create the parent directories if it doesn't exist
548498 if let Some(parent_dir) = file_path.parent() {
549499 fs::create_dir_all(parent_dir)?
550500 }
551501552552- // Create file
553553- let file = File::create(file_path.clone())?;
502502+ fs::write(file_path, content)?;
554503555555- Ok((file_path, file))
504504+ Ok(())
556505}
557506558558-fn finish_route(
507507+pub fn finish_route(
559508 render_result: RenderResult,
560560- file: &mut File,
561561- included_styles: &[assets::Style],
562562- included_scripts: &[assets::Script],
509509+ page_assets: &assets::PageAssets,
563510 route: String,
564564-) -> Result<(), Box<dyn std::error::Error>> {
511511+) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
565512 match render_result {
566513 RenderResult::Text(html) => {
514514+ let included_styles: Vec<_> = page_assets.included_styles().collect();
515515+ let included_scripts: Vec<_> = page_assets.included_scripts().collect();
516516+567517 if included_scripts.is_empty() && included_styles.is_empty() {
568568- file.write_all(html.as_bytes())?;
569569- return Ok(());
518518+ return Ok(html.into_bytes());
570519 }
571520572521 let element_content_handlers = vec
2424+2525+## Section 3: Combining All Features
2626+2727+Here’s a sentence that combines everything:
2828+2929+- **_Check out the [Markdown Guide](https://www.markdownguide.org/) for more tips_**.
+6
examples/library/content/articles/second-post.md
···11+---
22+title: Second Post
33+description: This is the second post on the blog.
44+---
55+66+This is another post on the blog!
+6
examples/library/content/articles/third-post.md
···11+---
22+title: Third Post
33+description: This is the third post on the blog.
44+---
55+66+Well, another post is here.
···11+use std::path::PathBuf;
22+use std::str::FromStr;
33+use std::{collections::HashSet, fs};
44+55+use maudit::page::DynamicRouteContext;
66+use maudit::{
77+ assets::PageAssets,
88+ content::{ContentSources, PageContent},
99+ page::{FullPage, RouteContext, RouteParams, RouteType},
1010+ BuildOptions,
1111+};
1212+1313+pub fn build_website(
1414+ routes: &[&dyn FullPage],
1515+ mut content_sources: ContentSources,
1616+ options: BuildOptions,
1717+) -> Result<(), Box<dyn std::error::Error>> {
1818+ let dist_dir = PathBuf::from_str(&options.output_dir)?;
1919+2020+ // Initialize all the content sources;
2121+ content_sources.init_all();
2222+2323+ let mut all_assets: HashSet<(PathBuf, PathBuf)> = HashSet::new();
2424+2525+ for route in routes {
2626+ match route.route_type() {
2727+ RouteType::Static => {
2828+ // Our page does not include content or assets, but we'll set those up for future use.
2929+ let content = PageContent::new(&content_sources);
3030+ let mut page_assets = PageAssets::new(options.assets_dir.clone().into());
3131+3232+ // Static and dynamic routes share the same interface for building, but static routes do not require any parameters.
3333+ // As such, we can just pass an empty set of parameters (the default for RouteParams).
3434+ let params = RouteParams::default();
3535+3636+ // Every page has a RouteContext, which contains information about the current route, as well as access to content and assets.
3737+ let mut ctx = RouteContext::from_static_route(
3838+ &content,
3939+ &mut page_assets,
4040+ route.url(¶ms).clone(),
4141+ );
4242+4343+ let content = route.build(&mut ctx)?;
4444+4545+ // FullPage.file_path() returns a path that does not include the output directory, so we need to join it with dist_dir.
4646+ let final_filepath = dist_dir.join(route.file_path(¶ms));
4747+4848+ // On some platforms, creating a file in a nested directory requires that the directory already exists.
4949+ if let Some(parent_dir) = final_filepath.parent() {
5050+ fs::create_dir_all(parent_dir)?
5151+ }
5252+5353+ fs::write(final_filepath, content)?;
5454+5555+ // Collect all assets used by this page.
5656+ all_assets.extend(page_assets.assets().map(|asset| {
5757+ (
5858+ dist_dir.join(asset.build_path()),
5959+ asset.path().to_path_buf(),
6060+ )
6161+ }));
6262+ }
6363+ RouteType::Dynamic => {
6464+ // The `routes` method returns all the possible routes for this page, along with their parameters and properties.
6565+ // It is very common for dynamic pages to be based on content, for instance a blog post page that has one route per blog post.
6666+ // As such, we create a mini RouteContext that includes the content sources, so that the page can use them to generate its routes.
6767+6868+ let dynamic_ctx = DynamicRouteContext {
6969+ content: &PageContent::new(&content_sources),
7070+ };
7171+7272+ let routes = route.routes_internal(&dynamic_ctx);
7373+7474+ // Every page can share the same PageContent instance, as it is just a view into the content sources.
7575+ let content = PageContent::new(&content_sources);
7676+7777+ for dynamic_route in routes {
7878+ // However, since page assets is a mutable structure that tracks which assets have been used, we need a new instance for each route.
7979+ // This is especially relevant if we were to parallelize this loop in the future.
8080+ let mut page_assets = PageAssets::new(options.assets_dir.clone().into());
8181+8282+ // The dynamic route includes the parameters for this specific route.
8383+ let params = &dynamic_route.0;
8484+8585+ // Here the context is created from a dynamic route, as the context has to include the route parameters and properties.
8686+ let mut ctx = RouteContext::from_dynamic_route(
8787+ &dynamic_route,
8888+ &content,
8989+ &mut page_assets,
9090+ route.url(params),
9191+ );
9292+9393+ // Everything from here is the same as for static routes.
9494+ let content = route.build(&mut ctx)?;
9595+9696+ let final_file_path = &dist_dir.join(route.file_path(params));
9797+9898+ if let Some(parent_dir) = final_file_path.parent() {
9999+ fs::create_dir_all(parent_dir)?
100100+ }
101101+102102+ fs::write(final_file_path, content)?;
103103+104104+ // Collect all assets used by this page.
105105+ all_assets.extend(page_assets.assets().map(|asset| {
106106+ (
107107+ dist_dir.join(asset.build_path()),
108108+ asset.path().to_path_buf(),
109109+ )
110110+ }));
111111+ }
112112+ }
113113+ }
114114+ }
115115+116116+ // Copy all assets to the output directory.
117117+ for (dest_path, src_path) in all_assets {
118118+ if let Some(parent) = dest_path.parent() {
119119+ fs::create_dir_all(parent)?;
120120+ }
121121+ fs::copy(src_path, dest_path)?;
122122+ }
123123+124124+ Ok(())
125125+}
···11+use maud::html;
22+use maudit::page::prelude::*;
33+44+use crate::{
55+ content::ArticleContent,
66+ layout::layout,
77+ pages::{article::ArticleParams, Article},
88+};
99+1010+#[route("/")]
1111+pub struct Index;
1212+1313+impl Page for Index {
1414+ fn render(&self, ctx: &mut RouteContext) -> RenderResult {
1515+ let articles = ctx.content.get_source::<ArticleContent>("articles");
1616+ let logo = ctx.assets.add_image("images/logo.svg");
1717+1818+ let markup = html! {
1919+ (logo)
2020+ ul {
2121+ @for entry in &articles.entries {
2222+ li {
2323+ a href=(get_page_url(&Article, ArticleParams { article: entry.id.clone() })) {
2424+ h2 { (entry.data(ctx).title) }
2525+ }
2626+ p { (entry.data(ctx).description) }
2727+ }
2828+ }
2929+ }
3030+ }
3131+ .into_string();
3232+3333+ layout(markup).into()
3434+ }
3535+}
+248
website/content/docs/library.md
···11+---
22+title: "Maudit as a library"
33+description: "Learn how to use Maudit as a library in your Rust projects."
44+section: "advanced"
55+---
66+77+[Maudit is built as a library, not a framework](/docs/philosophy/#maudit-is-a-library-not-a-framework). It is absolutely primordial to us that Maudit does not feel like this black box that you cannot inspect or understand. You should be able to see how everything works, and make it work for you.
88+99+As such, in this guide, we'll be building our own minimal [entrypoint](/docs/entrypoint/) to replace the built-in [`coronate`](https://docs.rs/maudit/latest/maudit/fn.coronate.html) function. This will give us a better understanding of how Maudit works, and allow us to customize it to our needs.
1010+1111+> The result of this guide is available in the [library example](https://github.com/bruits/maudit/tree/main/examples/library) in the Maudit repository.
1212+1313+## Setting up the project
1414+1515+We'll start by creating a new Rust project with Maudit as a dependency:
1616+1717+```bash
1818+cargo new library --bin
1919+cd library
2020+cargo add maudit
2121+```
2222+2323+and we'll create a simple Maudit page in `src/pages/index.rs`:
2424+2525+```rs
2626+use maudit::page::prelude::*;
2727+2828+#[route("/")]
2929+pub struct Index;
3030+3131+impl Page for Index {
3232+ fn render(&self, _: &mut RouteContext) -> RenderResult {
3333+ "Hello, Maudit!".into()
3434+ }
3535+}
3636+```
3737+3838+We'll now start building our own entrypoint in `src/build.rs`, which will contain a `build_website` function, taking the same parameters as `coronate`:
3939+4040+```rs
4141+use maudit::page::FullPage;
4242+use maudit::{content::ContentSources, BuildOptions};
4343+4444+pub fn build_website(
4545+ routes: &[&dyn FullPage],
4646+ mut content_sources: ContentSources,
4747+ options: BuildOptions,
4848+) -> Result<(), Box<dyn std::error::Error>> {
4949+ // Implementation will go here
5050+5151+ Ok(())
5252+}
5353+```
5454+5555+Finally, we'll modify `src/main.rs` to call our `build_website` function:
5656+5757+```rs
5858+mod build;
5959+6060+mod pages {
6161+ mod index;
6262+ pub use index::Index;
6363+}
6464+6565+fn main() {
6666+ let _ = build_website(
6767+ routes![Index],
6868+ content_sources![],
6969+ BuildOptions::default(),
7070+ );
7171+}
7272+```
7373+7474+Now that we have our project set up, let's implement the `build_website` function step by step.
7575+7676+## Building pages
7777+7878+The first step in building our own entrypoint is to iterate over the routes and build each page. Routes can either be static (i.e. `/index`) or dynamic (i.e. `/articles/[id]`). For now, we'll only handle static routes.
7979+8080+```rs
8181+use std::fs;
8282+use std::path::PathBuf;
8383+use std::str::FromStr;
8484+8585+use maudit::{
8686+ assets::PageAssets,
8787+ content::{ContentSources, PageContent},
8888+ page::{FullPage, RouteContext, RouteParams, RouteType},
8989+ BuildOptions,
9090+};
9191+9292+pub fn build_website(
9393+ routes: &[&dyn FullPage],
9494+ mut content_sources: ContentSources,
9595+ options: BuildOptions,
9696+) -> Result<(), Box<dyn std::error::Error>> {
9797+ let dist_dir = PathBuf::from_str(&options.output_dir)?;
9898+9999+ for route in routes {
100100+ match route.route_type() {
101101+ RouteType::Static => {
102102+ // Our page does not include content or assets, but we'll set those up for future use.
103103+ let content = PageContent::new(&content_sources);
104104+ let mut page_assets = PageAssets::new(options.assets_dir.clone().into());
105105+106106+ // Static and dynamic routes share the same interface for building, but static routes do not require any parameters.
107107+ // As such, we can just pass an empty set of parameters (the default for RouteParams).
108108+ let params = RouteParams::default();
109109+110110+ // Every page has a RouteContext, which contains information about the current route, as well as access to content and assets.
111111+ let mut ctx = RouteContext::from_static_route(
112112+ &content,
113113+ &mut page_assets,
114114+ route.url(¶ms).clone(),
115115+ );
116116+117117+ let content = route.build(&mut ctx)?;
118118+119119+ // FullPage.file_path() returns a path that does not include the output directory, so we need to join it with dist_dir.
120120+ let final_filepath = dist_dir.join(route.file_path(¶ms));
121121+122122+ // On some platforms, creating a file in a nested directory requires that the directory already exists or `fs::write` will fail.
123123+ if let Some(parent_dir) = final_filepath.parent() {
124124+ fs::create_dir_all(parent_dir)?
125125+ }
126126+127127+ fs::write(final_filepath, content)?;
128128+ }
129129+ RouteType::Dynamic => {
130130+ unimplemented!("We'll handle dynamic routes later");
131131+ }
132132+ }
133133+ }
134134+135135+ Ok(())
136136+}
137137+```
138138+139139+And with just this code, we can already build our first page! Running `cargo run` should create a `dist/index.html` file with the content `Hello, Maudit!`. But, if you try to use assets, you'll notice that they are not copied to the output directory. And similarly, if you try to use content, you'll notice that it'll always throw an error saying that the content source is not found. Let's fix that!
140140+141141+## Handling assets
142142+143143+We won't be implementing any asset processing (like bundling or minification) in this guide, but we'll be implementing the logic to copy assets from the assets directory to the output directory.
144144+145145+```rs
146146+pub fn build_website(
147147+ routes: &[&dyn FullPage],
148148+ mut content_sources: ContentSources,
149149+ options: BuildOptions,
150150+) -> Result<(), Box<dyn std::error::Error>> {
151151+ let dist_dir = PathBuf::from_str(&options.output_dir)?;
152152+153153+ let mut all_assets: HashSet<(PathBuf, PathBuf)> = HashSet::new();
154154+155155+ for route in routes {
156156+ match route.route_type() {
157157+ RouteType::Static => {
158158+ // ... Same as before ...
159159+160160+ // Collect all assets used by this page.
161161+ all_assets.extend(page_assets.assets().map(|asset| {
162162+ (
163163+ dist_dir.join(asset.build_path()),
164164+ asset.path().to_path_buf(),
165165+ )
166166+ }));
167167+ }
168168+ RouteType::Dynamic => {
169169+ unimplemented!("We'll handle dynamic routes later");
170170+ }
171171+ }
172172+ }
173173+174174+ // Copy all assets to the output directory.
175175+ for (dest_path, src_path) in all_assets {
176176+ // Similar to pages, we need to ensure the parent directory exists or `fs::copy` will fail.
177177+ if let Some(parent) = dest_path.parent() {
178178+ fs::create_dir_all(parent)?;
179179+ }
180180+ fs::copy(src_path, dest_path)?;
181181+ }
182182+183183+ Ok(())
184184+}
185185+```
186186+187187+This is enough to get us started with assets. Any pages using assets will now have them copied to the output directory. This is a basic implementation, but it is all we need to get assets working. `route.build` already takes care of automatically including scripts and styles in the page, so we don't need to do anything special for that. Additionally, `asset.build_path()` already takes care of adding hashes to filenames in a performant way, so we don't need to worry about that either.
188188+189189+## Handling content
190190+191191+As said previously, our current implementation does not handle content sources. Currently any pages trying to use content would panic with an error saying the requested content source does not exist. To fix this, we need to make sure that the content sources are properly loaded before building the pages.
192192+193193+If you've copied the previous snippets, you might have noticed that Rust has been complaining about `content_sources` being mutable but never mutated. We'll fix that now by initializing each content source before building the pages:
194194+195195+```rs
196196+content_sources.init_all();
197197+```
198198+199199+That's all. Now, any content source used in a page will be properly loaded and available for use. This is the most straightforward way to initialize content sources, but a more advanced implementation could for instance initialize content sources in parallel, lazily when a page actually requests content from a source or using advanced caching strategies.
200200+201201+## Dynamic routes
202202+203203+A dynamic route is a route that generates multiple pages based on some parameters. For instance, a blog post page might have a dynamic route `/posts/[id]`, where `[id]` is a parameter that can take different values for each blog post. Each individual page is essentially a static route, but it has a slightly different context available to it.
204204+205205+```rs
206206+for route in routes {
207207+ match route.route_type() {
208208+ RouteType::Static => {
209209+ // No changes here, same as before.
210210+ }
211211+ RouteType::Dynamic => {
212212+ // The `routes` method returns all the possible routes for this page, along with their parameters and properties.
213213+ // It is very common for dynamic pages to be based on content, for instance a blog post page that has one route per blog post.
214214+ // As such, we create a mini RouteContext that includes the content sources, so that the page can use them to generate its routes.
215215+216216+ let dynamic_ctx = DynamicRouteContext {
217217+ content: &PageContent::new(&content_sources),
218218+ };
219219+220220+ let routes = route.routes_internal(&dynamic_ctx);
221221+222222+ // Every page can share the same PageContent instance, as it is just a view into the content sources.
223223+ let content = PageContent::new(&content_sources);
224224+225225+ for dynamic_route in routes {
226226+ // However, since page assets is a mutable structure that tracks which assets have been used, we need a new instance for each route.
227227+ // This is especially relevant if we were to parallelize this loop in the future.
228228+ let mut page_assets = PageAssets::new(options.assets_dir.clone().into());
229229+230230+ // The dynamic route includes the parameters for this specific route.
231231+ let params = &dynamic_route.0;
232232+233233+ // Here the context is created from a dynamic route, as the context has to include the route parameters and properties.
234234+ let mut ctx = RouteContext::from_dynamic_route(
235235+ &dynamic_route,
236236+ &content,
237237+ &mut page_assets,
238238+ route.url(params),
239239+ );
240240+241241+ // Everything after this is the same as for static routes, making sure to use the route parameters when getting the file path.
242242+ }
243243+ }
244244+ }
245245+}
246246+```
247247+248248+And with that, you've succesfully rebuilt Maudit at home! There's a few more things that can be done to improve this implementation, like adding logging, copying static assets, asset processing, better error handling, parallelization, caching, etc, etc. But, this is a fully functional implementation that can be used as a starting point for more advanced use cases... or just as a learning exercise to understand how Maudit works under the hood.