···11+use core::panic;
12use std::sync::OnceLock;
23use syntect::{
34 Error,
···7374 (Self { meta }, opening_html)
7475 }
75767676- pub fn highlight(&self, content: &str) -> Result<String, Error> {
7777+ pub fn highlight(&self, content: &str, theme_path: &str) -> Result<String, Error> {
7778 let ss = get_syntax_set();
7879 let ts = get_theme_set();
79808081 let syntax = ss
8181- .find_syntax_by_name(&self.meta.language)
8282+ .find_syntax_by_token(&self.meta.language)
8383+ // Maybe token is enough, looking around at other users of Syntect, it seems like they often just use by_token, not sure.
8484+ .or_else(|| ss.find_syntax_by_name(&self.meta.language))
8285 .or_else(|| ss.find_syntax_by_extension(&self.meta.language))
8386 .or_else(|| ss.find_syntax_by_first_line(content))
8487 .unwrap_or_else(|| ss.find_syntax_plain_text());
85888686- // TODO: Allow configuring the theme
8787- let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]);
8989+ let theme = match ts.themes.get(theme_path) {
9090+ Some(theme) => theme,
9191+ None => &match ThemeSet::get_theme(theme_path) {
9292+ Ok(theme) => theme,
9393+ Err(_) => panic!(
9494+ "Theme '{theme_path}' not found in default themes and could not be loaded from file."
9595+ ),
9696+ },
9797+ };
9898+9999+ let mut h = HighlightLines::new(syntax, theme);
8810089101 let mut highlighted = String::new();
90102 for line in LinesWithEndings::from(content) {
+24-7
crates/maudit/src/content/markdown.rs
···1313use crate::{
1414 assets::Asset,
1515 content::{
1616- ContentContext,
1616+ ContentContext, ContentEntry, Entry,
1717 shortcodes::{MarkdownShortcodes, preprocess_shortcodes},
1818 },
1919 route::PageContext,
2020};
21212222-use super::{ContentEntry, highlight::CodeBlock, slugger};
2222+use super::{highlight::CodeBlock, slugger};
23232424#[cfg(test)]
2525mod shortcodes_tests;
···146146 }
147147}
148148149149-#[derive(Default)]
150149pub struct MarkdownOptions {
150150+ pub highlight_theme: String,
151151 pub components: MarkdownComponents,
152152 pub shortcodes: MarkdownShortcodes,
153153+}
154154+155155+impl Default for MarkdownOptions {
156156+ fn default() -> Self {
157157+ Self {
158158+ highlight_theme: "base16-ocean.dark".to_string(),
159159+ components: MarkdownComponents::default(),
160160+ shortcodes: MarkdownShortcodes::default(),
161161+ }
162162+ }
153163}
154164155165impl MarkdownOptions {
···161171 Self {
162172 components,
163173 shortcodes,
174174+ ..Self::default()
164175 }
165176 }
166177}
167178168168-/// Glob for Markdown files and return a vector of [`ContentEntry`]s.
179179+/// Glob for Markdown files and return a vector of [`Entry`]s.
169180///
170181/// Typically used by [`content_sources!`](crate::content_sources) to define a Markdown content source in [`coronate()`](crate::coronate).
171182///
···190201/// )
191202/// }
192203/// ```
193193-pub fn glob_markdown<T>(pattern: &str, options: Option<MarkdownOptions>) -> Vec<ContentEntry<T>>
204204+pub fn glob_markdown<T>(pattern: &str, options: Option<MarkdownOptions>) -> Vec<Entry<T>>
194205where
195206 T: DeserializeOwned + MarkdownContent + InternalMarkdownContent + Send + Sync + 'static,
196207{
197208 let mut entries = vec![];
198209 let options = options.map(Arc::new);
199210211211+ // TODO: `glob` is kinda slow, but alternatives are either unmaintained, have annoying bugs or not faster.
200212 for entry in glob_fs(pattern).unwrap() {
201213 let entry = entry.unwrap();
202214···221233 let opts = options.clone();
222234 let path = entry.clone();
223235224224- entries.push(ContentEntry::new_lazy(
236236+ entries.push(Entry::create_lazy(
225237 id,
226238 Some(Box::new(move |content: &str, route_ctx| {
227239 render_markdown(content, opts.as_deref(), Some(&path), Some(route_ctx))
···399411400412 Event::End(TagEnd::CodeBlock) => {
401413 if let Some(ref mut code_block) = code_block {
402402- let html = code_block.highlight(&code_block_content);
414414+ let html = code_block.highlight(
415415+ &code_block_content,
416416+ &options
417417+ .unwrap_or(&MarkdownOptions::default())
418418+ .highlight_theme,
419419+ );
403420 events[i] =
404421 Event::Html(format!("{}{}", html.unwrap(), "</code></pre>\n").into());
405422 }
+195-171
crates/maudit/src/route.rs
···33//! Every route must implement the [`Route`] trait. Then, pages can be passed to [`coronate()`](crate::coronate), through the [`routes!`](crate::routes) macro, to be built.
44use crate::assets::RouteAssets;
55use crate::build::finish_route;
66-use crate::content::RouteContent;
66+use crate::content::{Entry, RouteContent};
77use crate::routing::{
88 extract_params_from_raw_route, get_route_type_from_route_params, guess_if_route_is_endpoint,
99};
···111111}
112112113113/// Pagination page for any type of items
114114-pub struct PaginationPage<'a, T> {
114114+#[derive(Clone)]
115115+pub struct PaginationPage<T> {
115116 pub page: usize,
116117 pub per_page: usize,
117118 pub total_items: usize,
···122123 pub prev_page: Option<usize>,
123124 pub start_index: usize,
124125 pub end_index: usize,
125125- pub items: &'a [T],
126126+ pub items: Vec<T>,
126127}
127128128128-impl<'a, T> PaginationPage<'a, T> {
129129- pub fn new(page: usize, per_page: usize, total_items: usize, items: &'a [T]) -> Self {
129129+impl<T> PaginationPage<T> {
130130+ pub fn new(page: usize, per_page: usize, total_items: usize, page_items: Vec<T>) -> Self {
130131 let total_pages = if total_items == 0 {
131132 1
132133 } else {
···150151 prev_page: if page > 0 { Some(page - 1) } else { None },
151152 start_index,
152153 end_index,
153153- items: &items[start_index..end_index],
154154+ items: page_items,
154155 }
155156 }
156157}
157158158158-impl<'a, T> std::fmt::Debug for PaginationPage<'a, T> {
159159+impl<T> std::fmt::Debug for PaginationPage<T> {
159160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160161 f.debug_struct("PaginationPage")
161162 .field("page", &self.page)
···174175 }
175176}
176177177177-/// Helper function to create paginated routes from any slice
178178-pub fn paginate<T, Params>(
179179- items: &[T],
178178+/// Type alias for pagination pages of content entries, for easier usage
179179+pub type PaginatedContentPage<T> = PaginationPage<Entry<T>>;
180180+181181+/// Helper function to create paginated routes from any iterator
182182+pub fn paginate<T, I, Params>(
183183+ items: I,
180184 per_page: usize,
181185 mut params_fn: impl FnMut(usize) -> Params,
182182-) -> Pages<Params, PaginationPage<'_, T>>
186186+) -> Pages<Params, PaginationPage<T>>
183187where
188188+ I: IntoIterator<Item = T>,
184189 Params: Into<PageParams>,
190190+ T: Clone,
185191{
192192+ let items: Vec<T> = items.into_iter().collect();
193193+186194 if items.is_empty() {
187195 return vec![];
188196 }
···193201194202 for page in 0..total_pages {
195203 let params = params_fn(page);
196196- let props = PaginationPage::new(page, per_page, total_items, items);
204204+205205+ // Calculate the slice for this specific page
206206+ let start_index = page * per_page;
207207+ let end_index = ((page + 1) * per_page).min(total_items);
208208+ let page_items = items[start_index..end_index].to_vec();
209209+210210+ let props = PaginationPage::new(page, per_page, total_items, page_items);
197211198212 routes.push(Page::new(params, props));
199213 }
···374388///
375389/// Can be accessed through [`PageContext`]'s `raw_params`.
376390#[derive(Clone, Default, Debug)]
377377-pub struct PageParams(pub FxHashMap<String, String>);
391391+pub struct PageParams(pub FxHashMap<String, Option<String>>);
378392379393impl PageParams {
380394 pub fn from_vec<T>(params: Vec<T>) -> Vec<PageParams>
···441455 let value = params.0.get(¶m_def.key);
442456443457 match value {
444444- Some(value) => {
445445- route.replace_range(param_def.index..param_def.index + param_def.length, value);
446446- }
458458+ Some(value) => match value {
459459+ Some(value) => {
460460+ route.replace_range(
461461+ param_def.index..param_def.index + param_def.length,
462462+ value,
463463+ );
464464+ }
465465+ None => {
466466+ route
467467+ .replace_range(param_def.index..param_def.index + param_def.length, "");
468468+ }
469469+ },
447470 None => {
448471 panic!(
449472 "Route {:?} is missing parameter {:?}",
···454477 }
455478 }
456479457457- route
480480+ // Collapse multiple slashes into single slashes
481481+ route.replace("//", "/")
458482 }
459483460484 fn file_path(&self, params: &PageParams, output_dir: &Path) -> PathBuf {
461461- let mut params_def = extract_params_from_raw_route(&self.route_raw());
462462- let mut route = self.route_raw();
485485+ let params_def = extract_params_from_raw_route(&self.route_raw());
486486+ let route_template = self.route_raw();
463487464464- // Sort params by index in reverse order to avoid index shifting issues
465465- params_def.sort_by(|a, b| b.index.cmp(&a.index));
488488+ let mut sorted_params = params_def;
489489+ sorted_params.sort_by_key(|p| p.index);
466490467467- for param_def in params_def {
468468- let value = params.0.get(¶m_def.key);
491491+ let mut path = PathBuf::from(output_dir);
492492+ let mut last_index = 0;
493493+ let mut current_component = String::new();
469494470470- match value {
471471- Some(value) => {
472472- route.replace_range(param_def.index..param_def.index + param_def.length, value);
473473- }
474474- None => {
475475- panic!(
476476- "Route {:?} is missing parameter {:?}",
477477- self.route_raw(),
478478- param_def.key
479479- );
480480- }
495495+ for param_def in sorted_params.iter() {
496496+ // Push everything before this param into current_component
497497+ current_component.push_str(&route_template[last_index..param_def.index]);
498498+499499+ // Append param value if present
500500+ let value = params.0.get(¶m_def.key).unwrap_or_else(|| {
501501+ panic!(
502502+ "Route {:?} is missing parameter {:?}",
503503+ route_template, param_def.key
504504+ )
505505+ });
506506+ if let Some(v) = value {
507507+ current_component.push_str(v);
481508 }
509509+510510+ last_index = param_def.index + param_def.length;
482511 }
483512484484- let cleaned_raw_route = route.trim_start_matches('/').to_string();
513513+ // Append remainder of the route
514514+ current_component.push_str(&route_template[last_index..]);
515515+516516+ // Split by '/' and push non-empty components into the PathBuf
517517+ for part in current_component.split('/').filter(|s| !s.is_empty()) {
518518+ path.push(part);
519519+ }
485520486486- output_dir.join(match self.is_endpoint() {
487487- true => cleaned_raw_route,
488488- false => match cleaned_raw_route.is_empty() {
489489- true => "index.html".into(),
490490- false => format!("{}/index.html", cleaned_raw_route),
491491- },
492492- })
521521+ // Handle endpoint vs. page
522522+ if !self.is_endpoint() {
523523+ path.push("index.html");
524524+ }
525525+526526+ path
493527 }
494528}
495529···555589 //! use maudit::route::prelude::*;
556590 //! ```
557591 pub use super::{
558558- DynamicRouteContext, FullRoute, Page, PageContext, PageParams, Pages, PaginationPage,
559559- RenderResult, Route, RouteExt, paginate,
592592+ DynamicRouteContext, FullRoute, Page, PageContext, PageParams, Pages, PaginatedContentPage,
593593+ PaginationPage, RenderResult, Route, RouteExt, paginate,
594594+ };
595595+ pub use crate::assets::{Asset, Image, ImageFormat, ImageOptions, Script, Style, StyleOptions};
596596+ pub use crate::content::{
597597+ ContentContext, ContentEntry, Entry, EntryInner, MarkdownContent, RouteContent,
560598 };
561561- pub use crate::assets::{Asset, Image, Style, StyleOptions};
562562- pub use crate::content::MarkdownContent;
563599 pub use maudit_macros::{Params, route};
564600}
565601···587623 };
588624589625 let mut params = FxHashMap::default();
590590- params.insert("slug".to_string(), "hello-world".to_string());
626626+ params.insert("slug".to_string(), Some("hello-world".to_string()));
591627 let route_params = PageParams(params);
592628593629 assert_eq!(page.url(&route_params), "/articles/hello-world");
···600636 };
601637602638 let mut params = FxHashMap::default();
603603- params.insert("tag".to_string(), "rust".to_string());
604604- params.insert("page".to_string(), "2".to_string());
639639+ params.insert("tag".to_string(), Some("rust".to_string()));
640640+ params.insert("page".to_string(), Some("2".to_string()));
605641 let route_params = PageParams(params);
606642607643 assert_eq!(page.url(&route_params), "/articles/tags/rust/2");
···616652 };
617653618654 let mut params = FxHashMap::default();
619619- params.insert("tag".to_string(), "development-experience".to_string()); // Long replacement
620620- params.insert("page".to_string(), "1".to_string()); // Short replacement
655655+ params.insert(
656656+ "tag".to_string(),
657657+ Some("development-experience".to_string()),
658658+ ); // Long replacement
659659+ params.insert("page".to_string(), Some("1".to_string())); // Short replacement
621660 let route_params = PageParams(params);
622661623662 assert_eq!(
···644683 };
645684646685 let mut params = FxHashMap::default();
647647- params.insert("lang".to_string(), "en".to_string());
686686+ params.insert("lang".to_string(), Some("en".to_string()));
648687 let route_params = PageParams(params);
649688650689 assert_eq!(page.url(&route_params), "/en/about");
···657696 };
658697659698 let mut params = FxHashMap::default();
660660- params.insert("id".to_string(), "123".to_string());
699699+ params.insert("id".to_string(), Some("123".to_string()));
661700 let route_params = PageParams(params);
662701663702 assert_eq!(page.url(&route_params), "/api/users/123");
···670709 };
671710672711 let mut params = FxHashMap::default();
673673- params.insert("slug".to_string(), "hello-world".to_string());
712712+ params.insert("slug".to_string(), Some("hello-world".to_string()));
674713 let route_params = PageParams(params);
675714676715 let output_dir = Path::new("/dist");
···686725 };
687726688727 let mut params = FxHashMap::default();
689689- params.insert("tag".to_string(), "rust".to_string());
690690- params.insert("page".to_string(), "2".to_string());
728728+ params.insert("tag".to_string(), Some("rust".to_string()));
729729+ params.insert("page".to_string(), Some("2".to_string()));
691730 let route_params = PageParams(params);
692731693732 let output_dir = Path::new("/dist");
···743782 };
744783745784 let mut params = FxHashMap::default();
746746- params.insert("page".to_string(), "1".to_string());
785785+ params.insert("page".to_string(), Some("1".to_string()));
747786 let route_params = PageParams(params);
748787749788 let output_dir = Path::new("/dist");
···753792 }
754793755794 #[test]
756756- fn test_pagination_page_with_entries() {
757757- // Create some mock content entries
758758- use crate::content::ContentEntry;
759759- use std::path::PathBuf;
760760-761761- let entries = vec![
762762- ContentEntry::new(
763763- "entry1".to_string(),
764764- None,
765765- Some("content1".to_string()),
766766- (),
767767- Some(PathBuf::from("file1.md")),
768768- ),
769769- ContentEntry::new(
770770- "entry2".to_string(),
771771- None,
772772- Some("content2".to_string()),
773773- (),
774774- Some(PathBuf::from("file2.md")),
775775- ),
776776- ContentEntry::new(
777777- "entry3".to_string(),
778778- None,
779779- Some("content3".to_string()),
780780- (),
781781- Some(PathBuf::from("file3.md")),
782782- ),
783783- ContentEntry::new(
784784- "entry4".to_string(),
785785- None,
786786- Some("content4".to_string()),
787787- (),
788788- Some(PathBuf::from("file4.md")),
789789- ),
790790- ContentEntry::new(
791791- "entry5".to_string(),
792792- None,
793793- Some("content5".to_string()),
794794- (),
795795- Some(PathBuf::from("file5.md")),
796796- ),
797797- ];
798798-799799- let pagination = PaginationPage::new(1, 2, 5, &entries);
800800-801801- assert_eq!(pagination.page, 1);
802802- assert_eq!(pagination.per_page, 2);
803803- assert_eq!(pagination.total_items, 5);
804804- assert_eq!(pagination.total_pages, 3);
805805- assert!(pagination.has_next);
806806- assert!(pagination.has_prev);
807807- assert_eq!(pagination.start_index, 2);
808808- assert_eq!(pagination.end_index, 4);
809809- assert_eq!(pagination.items.len(), 2);
810810- assert_eq!(pagination.items[0].id, "entry3");
811811- assert_eq!(pagination.items[1].id, "entry4");
812812- }
813813-814814- #[test]
815815- fn test_paginate_content_function() {
816816- use crate::content::ContentEntry;
817817- use std::path::PathBuf;
818818-819819- let entries = vec![
820820- ContentEntry::new(
821821- "entry1".to_string(),
822822- None,
823823- Some("content1".to_string()),
824824- (),
825825- Some(PathBuf::from("file1.md")),
826826- ),
827827- ContentEntry::new(
828828- "entry2".to_string(),
829829- None,
830830- Some("content2".to_string()),
831831- (),
832832- Some(PathBuf::from("file2.md")),
833833- ),
834834- ContentEntry::new(
835835- "entry3".to_string(),
836836- None,
837837- Some("content3".to_string()),
838838- (),
839839- Some(PathBuf::from("file3.md")),
840840- ),
841841- ];
842842-843843- let routes = paginate(&entries, 2, |page| {
844844- let mut params = FxHashMap::default();
845845- params.insert("page".to_string(), page.to_string());
846846- PageParams(params)
847847- });
848848-849849- assert_eq!(routes.len(), 2);
850850-851851- // First page
852852- assert_eq!(routes[0].props.page, 0);
853853- assert_eq!(routes[0].props.items.len(), 2);
854854- assert_eq!(routes[0].props.items[0].id, "entry1");
855855- assert_eq!(routes[0].props.items[1].id, "entry2");
856856-857857- // Second page
858858- assert_eq!(routes[1].props.page, 1);
859859- assert_eq!(routes[1].props.items.len(), 1);
860860- assert_eq!(routes[1].props.items[0].id, "entry3");
861861- }
862862-863863- #[test]
864795 fn test_paginate_generic_function() {
865796 // Test with simple strings
866797 let tags = vec!["rust", "javascript", "python", "go", "typescript"];
867798868799 let routes = paginate(&tags, 2, |page| {
869800 let mut params = FxHashMap::default();
870870- params.insert("page".to_string(), page.to_string());
801801+ params.insert("page".to_string(), Some(page.to_string()));
871802 PageParams(params)
872803 });
873804···876807 // First page
877808 assert_eq!(routes[0].props.page, 0);
878809 assert_eq!(routes[0].props.items.len(), 2);
879879- assert_eq!(routes[0].props.items[0], "rust");
880880- assert_eq!(routes[0].props.items[1], "javascript");
810810+ assert_eq!(routes[0].props.items[0], &"rust");
811811+ assert_eq!(routes[0].props.items[1], &"javascript");
881812882813 // Second page
883814 assert_eq!(routes[1].props.page, 1);
884815 assert_eq!(routes[1].props.items.len(), 2);
885885- assert_eq!(routes[1].props.items[0], "python");
886886- assert_eq!(routes[1].props.items[1], "go");
816816+ assert_eq!(routes[1].props.items[0], &"python");
817817+ assert_eq!(routes[1].props.items[1], &"go");
887818888819 // Third page
889820 assert_eq!(routes[2].props.page, 2);
890821 assert_eq!(routes[2].props.items.len(), 1);
891891- assert_eq!(routes[2].props.items[0], "typescript");
822822+ assert_eq!(routes[2].props.items[0], &"typescript");
823823+ }
824824+825825+ #[test]
826826+ fn test_url_optional_parameter_with_value() {
827827+ let page = TestPage {
828828+ route: "/articles/[slug]/[page]".to_string(),
829829+ };
830830+831831+ let mut params = FxHashMap::default();
832832+ params.insert("slug".to_string(), Some("hello-world".to_string()));
833833+ params.insert("page".to_string(), Some("2".to_string()));
834834+ let route_params = PageParams(params);
835835+836836+ assert_eq!(page.url(&route_params), "/articles/hello-world/2");
837837+ }
838838+839839+ #[test]
840840+ fn test_url_optional_parameter_none() {
841841+ let page = TestPage {
842842+ route: "/articles/[slug]/[page]".to_string(),
843843+ };
844844+845845+ let mut params = FxHashMap::default();
846846+ params.insert("slug".to_string(), Some("hello-world".to_string()));
847847+ params.insert("page".to_string(), None);
848848+ let route_params = PageParams(params);
849849+850850+ assert_eq!(page.url(&route_params), "/articles/hello-world/");
851851+ }
852852+853853+ #[test]
854854+ fn test_url_multiple_optional_parameters() {
855855+ let page = TestPage {
856856+ route: "/[lang]/articles/[category]/[page]".to_string(),
857857+ };
858858+859859+ let mut params = FxHashMap::default();
860860+ params.insert("lang".to_string(), None);
861861+ params.insert("category".to_string(), Some("rust".to_string()));
862862+ params.insert("page".to_string(), None);
863863+ let route_params = PageParams(params);
864864+865865+ assert_eq!(page.url(&route_params), "/articles/rust/");
866866+ }
867867+868868+ #[test]
869869+ fn test_file_path_optional_parameter_with_value() {
870870+ let page = TestPage {
871871+ route: "/articles/[slug]/[page]".to_string(),
872872+ };
873873+874874+ let mut params = FxHashMap::default();
875875+ params.insert("slug".to_string(), Some("hello-world".to_string()));
876876+ params.insert("page".to_string(), Some("2".to_string()));
877877+ let route_params = PageParams(params);
878878+879879+ let output_dir = Path::new("/dist");
880880+ let expected = Path::new("/dist/articles/hello-world/2/index.html");
881881+882882+ assert_eq!(page.file_path(&route_params, output_dir), expected);
883883+ }
884884+885885+ #[test]
886886+ fn test_file_path_optional_parameter_none() {
887887+ let page = TestPage {
888888+ route: "/articles/[slug]/[page]".to_string(),
889889+ };
890890+891891+ let mut params = FxHashMap::default();
892892+ params.insert("slug".to_string(), Some("hello-world".to_string()));
893893+ params.insert("page".to_string(), None);
894894+ let route_params = PageParams(params);
895895+896896+ let output_dir = Path::new("/dist");
897897+ let expected = Path::new("/dist/articles/hello-world/index.html");
898898+899899+ assert_eq!(page.file_path(&route_params, output_dir), expected);
900900+ }
901901+902902+ #[test]
903903+ fn test_file_path_optional_parameter_endpoint() {
904904+ let page = TestPage {
905905+ route: "/api/[version]/data.json".to_string(),
906906+ };
907907+908908+ let mut params = FxHashMap::default();
909909+ params.insert("version".to_string(), None);
910910+ let route_params = PageParams(params);
911911+912912+ let output_dir = Path::new("/dist");
913913+ let expected = Path::new("/dist/api/data.json");
914914+915915+ assert_eq!(page.file_path(&route_params, output_dir), expected);
892916 }
893917}
···3737}
3838```
39394040-Where `loader` and `glob_markdown` are functions returning a Vec of `ContentEntry`. Typically, a loader also accepts a type argument specifying the shape of the data for each entries it returns, which will be used inside your pages to provide typed content.
4040+Where `loader` and `glob_markdown` are functions returning a Vec of `Entry`. Typically, a loader also accepts a type argument specifying the shape of the data for each entries it returns, which will be used inside your pages to provide typed content.
41414242## Using a content source in pages
4343···96969797### Custom loaders
98989999-As said previously, a loader is simply a function returning a Vec of `ContentEntry`. This means you can create your own loaders to load content from any source you want, as long as you return the right type.
9999+As said previously, a loader is a function returning a Vec of `Entry`. This means you can create your own loaders to load content from any source you want, as long as you return the right type.
100100101101For instance, you could create a loader that fetches a remote JSON file and deserializes it into a struct, producing a content source with a single entry:
102102103103```rs
104104-use maudit::content::{ContentEntry};
104104+use maudit::content::{Entry, ContentEntry};
105105106106#[derive(serde::Deserialize)]
107107pub struct MyType {
···109109 pub name: String,
110110}
111111112112-pub fn my_loader(path: &str) -> Vec<ContentEntry<MyType>> {
112112+pub fn my_loader(path: &str) -> Vec<Entry<MyType>> {
113113 let response = reqwest::blocking::get(path).unwrap();
114114 let data = response.json::<MyType>().unwrap();
115115116116- vec![ContentEntry::new(data.id.into(), None, None, data, None)]
116116+ vec![Entry::create(data.id.into(), None, None, data, None)]
117117}
118118119119// Use it as a content source:
···149149}
150150```
151151152152-Content entries can also be rendered by passing a render function to the `render` method of `ContentEntry`.
152152+Content entries can also be rendered by passing a render function to the `render` method of `Entry`.
153153154154```rs
155155-ContentEntry::new(
155155+Entry::create(
156156 data.id.into(),
157157 Some(Box::new(|content, ctx| {
158158 // render the content string into HTML
+7-1
website/content/docs/performance.md
···37373838### Preventing build directory block
39394040-As Maudit recompile your project on every change, it is possible to run into issues where the build directory is first blocked by another process, [most commonly `rust-analyzer` in your editor.](https://github.com/rust-lang/rust-analyzer/issues/4616), slowing down builds significantly.
4040+As Maudit recompile your project on every change, it is possible to run into issues where the build directory is first blocked by another process, [most commonly `rust-analyzer` in your editor](https://github.com/rust-lang/rust-analyzer/issues/4616), slowing down builds significantly.
4141+4242+To avoid this, [you can change the build directory used by `rust-analyzer`](https://rust-analyzer.github.io/book/configuration#cargo.targetDir) to a different directory than the default `target` directory used by Cargo. For example, in VSCode you can add the following to your settings:
4343+4444+```json
4545+"rust-analyzer.cargo.targetDir": true // or a specific path like "target-ra"
4646+```
41474248While this does improve the time it takes to get feedback on changes, note that changing `rust-analyzer` settings to use a different build directory will use a lot of disk space.
4349
+11-2
website/content/docs/routing.md
···7272impl Route<Params> for Post {
7373 fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
7474 let params = ctx.params::<Params>();
7575- RenderResult::Text(format!("Hello, {}!", params.slug))
7575+7676+ format!("Hello, {}!", params.slug)
7677 }
77787879 fn pages(&self, ctx: &mut DynamicRouteContext) -> Pages<Params> {
···114115115116Like static routes, dynamic routes must be [registered](#registering-routes) in the `coronate` function in order for them to be built.
116117118118+### Optional parameters
119119+120120+Dynamic routes can also have optional parameters by using the `Option<T>` type in the parameters struct. These parameters will be completely removed from the URL and file path when they are `None`. For instance, in a route with the path `/posts/[category]/[slug]`, if the `category` parameter is `None`, the resulting URL will be `/posts/my-blog-post/`.
121121+122122+This feature is notably useful when creating paginated routes (ex: `/posts/[page]`), where the first page sometimes does not include a page number in the URL, but subsequent pages do (e.g., `/blog` for the first page and `/blog/1` for the second page).
123123+124124+Maudit will automatically collapse repeated slashes in the URL and file path into a single slash, as such `/articles/[slug]/[page]/` where `page` is `None` will result in `/articles/my-article/`, and not `/articles/my-article//`.
125125+117126## Endpoints
118127119119-Maudit supports returning other types of content besides HTML, such as JSON, plain text or binary data. To do this, simply add a file extension to the route path and return the content in the `render` method. Both static and dynamic routes can be used as endpoints.
128128+Maudit supports returning other types of content besides HTML, such as JSON, plain text or binary data. To do this, add a file extension to the route path and return the content in the `render` method. Both static and dynamic routes can be used as endpoints.
120129121130```rs
122131use maudit::route::prelude::*;