diff --git a/examples/ssr_modes/src/app.rs b/examples/ssr_modes/src/app.rs
index 8d167d4f14..9da69a2158 100644
--- a/examples/ssr_modes/src/app.rs
+++ b/examples/ssr_modes/src/app.rs
@@ -9,12 +9,13 @@ use thiserror::Error;
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
+ let fallback = || view! { "Page not found." }.into_view();
view! {
-
+
// We’ll load the home page with out-of-order streaming and
diff --git a/examples/ssr_modes_axum/src/app.rs b/examples/ssr_modes_axum/src/app.rs
index 4592375441..6b24570f2b 100644
--- a/examples/ssr_modes_axum/src/app.rs
+++ b/examples/ssr_modes_axum/src/app.rs
@@ -9,12 +9,13 @@ use thiserror::Error;
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
+ let fallback = || view! { "Page not found." }.into_view();
view! {
-
+
// We’ll load the home page with out-of-order streaming and
diff --git a/router/src/components/routes.rs b/router/src/components/routes.rs
index ab6ff734cd..561c242b5d 100644
--- a/router/src/components/routes.rs
+++ b/router/src/components/routes.rs
@@ -6,7 +6,7 @@ use crate::{
RouteDefinition, RouteMatch,
},
use_is_back_navigation, use_route, Redirect, RouteContext, RouterContext,
- SetIsRouting, TrailingSlash,
+ SetIsRouting, TrailingSlash, NavigateOptions,
};
use leptos::{leptos_dom::HydrationCtx, *};
use std::{
@@ -728,6 +728,11 @@ fn create_routes(
/// A new route that redirects to `route` with the correct trailng slash.
fn redirect_route_for(route: &RouteDefinition) -> Option {
+ if matches!(route.path.as_str(), "" | "/") {
+ // Root paths are an exception to the rule and are always equivalent:
+ return None;
+ }
+
let trailing_slash = route
.trailing_slash
.clone()
@@ -780,8 +785,12 @@ fn FixTrailingSlash(add_slash: bool) -> impl IntoView {
path.pop();
path
};
+ let options = NavigateOptions {
+ replace: true,
+ ..Default::default()
+ };
view! {
-
+
}
}
diff --git a/router/src/extract_routes.rs b/router/src/extract_routes.rs
index 6813b4b846..32f09f3ed5 100644
--- a/router/src/extract_routes.rs
+++ b/router/src/extract_routes.rs
@@ -2,7 +2,7 @@ mod test_extract_routes;
use crate::{
Branch, Method, RouterIntegrationContext, ServerIntegration, SsrMode,
- StaticDataMap, StaticMode, StaticParamsMap, StaticPath,
+ StaticDataMap, StaticMode, StaticParamsMap, StaticPath, provide_server_redirect,
};
use leptos::*;
use std::{
@@ -192,6 +192,9 @@ where
provide_context(RouterIntegrationContext::new(integration));
let branches = PossibleBranchContext::default();
provide_context(branches.clone());
+ // Suppress startup warning about using without ServerRedirectFunction:
+ provide_server_redirect(|_str| ());
+
additional_context();
diff --git a/router/src/extract_routes/test_extract_routes.rs b/router/src/extract_routes/test_extract_routes.rs
index 9417f26f4a..69b41bb6ad 100644
--- a/router/src/extract_routes/test_extract_routes.rs
+++ b/router/src/extract_routes/test_extract_routes.rs
@@ -232,6 +232,18 @@ fn test_unique_route_ids() {
.all_unique());
}
+#[test]
+fn test_unique_route_patterns() {
+ let branches = get_branches(RedirectApp);
+ assert!(!branches.is_empty());
+
+ assert!(branches
+ .iter()
+ .flat_map(|branch| &branch.routes)
+ .map(|route| route.pattern.as_str())
+ .all_unique());
+}
+
fn get_branches(app_fn: F) -> Vec
where
F: Fn() -> IV + Clone + 'static,
diff --git a/router/src/matching/matcher.rs b/router/src/matching/matcher.rs
index 3c0d1a9986..d64bde6738 100644
--- a/router/src/matching/matcher.rs
+++ b/router/src/matching/matcher.rs
@@ -45,7 +45,14 @@ impl Matcher {
}
#[doc(hidden)]
- pub fn test(&self, location: &str) -> Option {
+ pub fn test(&self, mut location: &str) -> Option {
+ // URL root paths "/" and "" are equivalent.
+ // Web servers (at least, Axum and Actix-Web) will send us a path of "/"
+ // even if we've routed "". Always treat these as equivalent:
+ if location == "/" && self.len == 0 {
+ location = ""
+ }
+
let loc_segments = get_segments(location);
let loc_len = loc_segments.len();
diff --git a/router/tests/join_paths.rs b/router/tests/join_paths.rs
index a0f6828407..f1fadfec32 100644
--- a/router/tests/join_paths.rs
+++ b/router/tests/join_paths.rs
@@ -44,5 +44,14 @@ cfg_if! {
assert_eq!(join_paths("/foo", ":bar/baz"), "/foo/:bar/baz");
assert_eq!(join_paths("", ":bar/baz"), "/:bar/baz");
}
+
+ // Additional tests NOT from Solid Router:
+ #[test]
+ fn join_paths_for_root() {
+ assert_eq!(join_paths("", ""), "");
+ assert_eq!(join_paths("", "/"), "");
+ assert_eq!(join_paths("/", ""), "");
+ assert_eq!(join_paths("/", "/"), "");
+ }
}
}
diff --git a/router/tests/trailing_slashes.rs b/router/tests/trailing_slashes.rs
index ff47f8041c..b44d1e9442 100644
--- a/router/tests/trailing_slashes.rs
+++ b/router/tests/trailing_slashes.rs
@@ -5,31 +5,48 @@ use leptos_router::{params_map, Matcher};
#[test]
fn trailing_slashes_match_exactly() {
let matcher = Matcher::new("/foo/");
- assert!(matches(&matcher, "/foo/"));
- assert!(!matches(&matcher, "/foo"));
+ assert_matches(&matcher, "/foo/");
+ assert_no_match(&matcher, "/foo");
let matcher = Matcher::new("/foo/bar/");
- assert!(matches(&matcher, "/foo/bar/"));
- assert!(!matches(&matcher, "/foo/bar"));
- assert!(!matches(&matcher, "/foo/"));
- assert!(!matches(&matcher, "/foo"));
+ assert_matches(&matcher, "/foo/bar/");
+ assert_no_match(&matcher, "/foo/bar");
+
+ let matcher = Matcher::new("/");
+ assert_matches(&matcher, "/");
+ assert_no_match(&matcher, "");
+
+ let matcher = Matcher::new("");
+ assert_matches(&matcher, "");
+
+ // Despite returning a pattern of "", web servers (known: Actix-Web and Axum)
+ // may send us a path of "/". We should match those at the root:
+ assert_matches(&matcher, "/");
}
#[test]
fn trailng_slashes_params_match_exactly() {
let matcher = Matcher::new("/foo/:bar/");
- assert!(matches(&matcher, "/foo/bar/"));
- assert!(matches(&matcher, "/foo/42/"));
- assert!(matches(&matcher, "/foo/%20/"));
+ assert_matches(&matcher, "/foo/bar/");
+ assert_matches(&matcher, "/foo/42/");
+ assert_matches(&matcher, "/foo/%20/");
- assert!(!matches(&matcher, "/foo/bar"));
- assert!(!matches(&matcher, "/foo/42"));
- assert!(!matches(&matcher, "/foo/%20"));
+ assert_no_match(&matcher, "/foo/bar");
+ assert_no_match(&matcher, "/foo/42");
+ assert_no_match(&matcher, "/foo/%20");
let m = matcher.test("/foo/asdf/").unwrap();
assert_eq!(m.params, params_map! { "bar" => "asdf" });
}
+fn assert_matches(matcher: &Matcher, path: &str) {
+ assert!(matches(matcher, path), "{matcher:?} should match path {path:?}");
+}
+
+fn assert_no_match(matcher: &Matcher, path: &str) {
+ assert!(!matches(matcher, path), "{matcher:?} should NOT match path {path:?}");
+}
+
fn matches(m: &Matcher, loc: &str) -> bool {
m.test(loc).is_some()
}