Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: generate proper urls depending on trailing slashes (#66)

* fix: generate proper urls depending on trailing slashes

* fix: just normalize urls and paths instead of supporting everything

* nit: add comment about per-route trailing slash

* chore: changeset

authored by

Erika and committed by
GitHub
492953d6 4542384d

+94 -9
+5
.sampo/changesets/venerable-lord-goulven.md
··· 1 + --- 2 + cargo/maudit: patch 3 + --- 4 + 5 + Normalize the urls returned by `url()` to always properly reflect what the final path would look like
+89 -9
crates/maudit/src/route.rs
··· 475 475 476 476 fn url(&self, params: &PageParams) -> String { 477 477 let params_def = extract_params_from_raw_route(&self.route_raw()); 478 - build_url_with_params(&self.route_raw(), &params_def, params) 478 + build_url_with_params(&self.route_raw(), &params_def, params, self.is_endpoint()) 479 479 } 480 480 481 481 fn file_path(&self, params: &PageParams, output_dir: &Path) -> PathBuf { ··· 545 545 route_template: &str, 546 546 params_def: &[ParameterDef], 547 547 params: &PageParams, 548 + is_endpoint: bool, 548 549 ) -> String { 549 550 if params_def.is_empty() { 550 551 return route_template.to_string(); ··· 567 568 ); 568 569 } 569 570 570 - result.replace("//", "/") 571 + // Collapse consecutive slashes 572 + let parts: Vec<&str> = result.split('/').filter(|s| !s.is_empty()).collect(); 573 + result = parts.join("/"); 574 + 575 + // Ensure leading slash 576 + if !result.starts_with('/') { 577 + result.insert(0, '/'); 578 + } 579 + 580 + // Ensure trailing slash for non-endpoints 581 + // TODO: Remove this if we implement per-route trailing slash behavior, see build_file_path_with_params comment 582 + if !is_endpoint && !result.ends_with('/') { 583 + result.push('/'); 584 + } 585 + 586 + result 571 587 } 572 588 573 589 fn build_file_path_with_params( ··· 601 617 602 618 if !is_endpoint { 603 619 path.push("index.html"); 620 + 621 + // TODO: Trailing slash behavior should be respected per route, so for instance if the user define `/blog` (no trailing slash) it should generate `/blog.html` instead of `/blog/index.html`. 622 + // However, right now we don't support pretty URLs, a lot of servers don't support it either and so it's better to have a consistent behavior of always generating `index.html` files. 623 + // if route.ends_with("/") { 624 + // path.push("index.html"); 625 + // } else { 626 + // path.set_extension("html"); 627 + // } 604 628 } 605 629 606 630 path ··· 644 668 } 645 669 646 670 fn url(&self, params: &PageParams) -> String { 647 - build_url_with_params(&self.route_raw(), self.get_cached_params(), params) 671 + build_url_with_params( 672 + &self.route_raw(), 673 + self.get_cached_params(), 674 + params, 675 + self.is_endpoint(), 676 + ) 648 677 } 649 678 650 679 fn file_path(&self, params: &PageParams, output_dir: &Path) -> PathBuf { ··· 772 801 params.insert("slug".to_string(), Some("hello-world".to_string())); 773 802 let route_params = PageParams(params); 774 803 775 - assert_eq!(page.url(&route_params), "/articles/hello-world"); 804 + assert_eq!(page.url(&route_params), "/articles/hello-world/"); 776 805 } 777 806 778 807 #[test] ··· 786 815 params.insert("page".to_string(), Some("2".to_string())); 787 816 let route_params = PageParams(params); 788 817 789 - assert_eq!(page.url(&route_params), "/articles/tags/rust/2"); 818 + assert_eq!(page.url(&route_params), "/articles/tags/rust/2/"); 790 819 } 791 820 792 821 #[test] ··· 807 836 808 837 assert_eq!( 809 838 page.url(&route_params), 810 - "/articles/tags/development-experience/1" 839 + "/articles/tags/development-experience/1/" 811 840 ); 812 841 } 813 842 ··· 832 861 params.insert("lang".to_string(), Some("en".to_string())); 833 862 let route_params = PageParams(params); 834 863 835 - assert_eq!(page.url(&route_params), "/en/about"); 864 + assert_eq!(page.url(&route_params), "/en/about/"); 836 865 } 837 866 838 867 #[test] ··· 845 874 params.insert("id".to_string(), Some("123".to_string())); 846 875 let route_params = PageParams(params); 847 876 848 - assert_eq!(page.url(&route_params), "/api/users/123"); 877 + assert_eq!(page.url(&route_params), "/api/users/123/"); 849 878 } 850 879 851 880 #[test] ··· 979 1008 params.insert("page".to_string(), Some("2".to_string())); 980 1009 let route_params = PageParams(params); 981 1010 982 - assert_eq!(page.url(&route_params), "/articles/hello-world/2"); 1011 + assert_eq!(page.url(&route_params), "/articles/hello-world/2/"); 983 1012 } 984 1013 985 1014 #[test] ··· 1057 1086 1058 1087 let output_dir = Path::new("/dist"); 1059 1088 let expected = Path::new("/dist/api/data.json"); 1089 + 1090 + assert_eq!(page.file_path(&route_params, output_dir), expected); 1091 + } 1092 + 1093 + #[test] 1094 + fn test_url_collapse_consecutive_slashes() { 1095 + let page = TestPage { 1096 + route: "/articles/[category]/[slug]".to_string(), 1097 + }; 1098 + 1099 + let mut params = FxHashMap::default(); 1100 + // Empty category should result in consecutive slashes that get collapsed 1101 + params.insert("category".to_string(), None); 1102 + params.insert("slug".to_string(), Some("hello-world".to_string())); 1103 + let route_params = PageParams(params); 1104 + 1105 + // Should collapse // to / 1106 + assert_eq!(page.url(&route_params), "/articles/hello-world/"); 1107 + } 1108 + 1109 + #[test] 1110 + fn test_url_collapse_multiple_consecutive_slashes() { 1111 + let page = TestPage { 1112 + route: "/articles/[cat1]/[cat2]/[cat3]/[slug]".to_string(), 1113 + }; 1114 + 1115 + let mut params = FxHashMap::default(); 1116 + // Multiple empty parameters should result in many slashes that get collapsed 1117 + params.insert("cat1".to_string(), None); 1118 + params.insert("cat2".to_string(), None); 1119 + params.insert("cat3".to_string(), None); 1120 + params.insert("slug".to_string(), Some("hello-world".to_string())); 1121 + let route_params = PageParams(params); 1122 + 1123 + // Should collapse //// to / 1124 + assert_eq!(page.url(&route_params), "/articles/hello-world/"); 1125 + } 1126 + 1127 + #[test] 1128 + fn test_file_path_collapse_consecutive_slashes() { 1129 + let page = TestPage { 1130 + route: "/articles/[category]/[slug]".to_string(), 1131 + }; 1132 + 1133 + let mut params = FxHashMap::default(); 1134 + params.insert("category".to_string(), None); 1135 + params.insert("slug".to_string(), Some("hello-world".to_string())); 1136 + let route_params = PageParams(params); 1137 + 1138 + let output_dir = Path::new("/dist"); 1139 + let expected = Path::new("/dist/articles/hello-world/index.html"); 1060 1140 1061 1141 assert_eq!(page.file_path(&route_params, output_dir), expected); 1062 1142 }