diff --git a/docs-src/0.7/src/essentials/router/index.md b/docs-src/0.7/src/essentials/router/index.md
index 8e96cbdb0..c84ae0c68 100644
--- a/docs-src/0.7/src/essentials/router/index.md
+++ b/docs-src/0.7/src/essentials/router/index.md
@@ -25,6 +25,17 @@ To create a `Routable` enum, you will need to derive the `Routable` with a `#[ro
{{#include ../docs-router/src/doc_examples/router_introduction.rs:first_router}}
```
+
+Using a different component name
+
+By default, each variant renders a component with the same name. You can specify a different component as the second argument to `#[route]`:
+
+```rust
+{{#include ../docs-router/src/doc_examples/route_customization.rs:custom_component}}
+```
+
+
+
## Rendering the router
Now that you have defined your routes, you can use the `Router` component to render them. The `Router` component takes your `Routable` enum as a generic argument to define handle parsing, and rendering routes.
diff --git a/docs-src/0.7/src/essentials/router/routes.md b/docs-src/0.7/src/essentials/router/routes.md
index 66325c34b..ff680c815 100644
--- a/docs-src/0.7/src/essentials/router/routes.md
+++ b/docs-src/0.7/src/essentials/router/routes.md
@@ -39,6 +39,21 @@ The segment can be of any type that implements `FromStr`.
{{#include ../docs-router/src/doc_examples/dynamic_segments.rs:route}}
```
+
+Parsing your own dynamic segment types
+
+Any type that implements `FromStr` + `Display` can be used as a dynamic segment. If parsing fails, the route won't match and the router moves on to the next candidate. This lets you restrict which URLs match a route — for example, only accepting known locales:
+
+```rust
+{{#include ../docs-router/src/doc_examples/route_customization.rs:custom_dynamic_segment}}
+```
+
+With this route, `/en/about` and `/fr/about` will match, but `/xyz/about` will not.
+
+See [`FromRouteSegment`](https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromRouteSegment.html) on docs.rs for the full trait definition.
+
+
+
## Catch All Segments
Catch All segments are in the form of `:..name` where `name` is the name of the field in the route variant. If the segments are parsed successfully then the route matches, otherwise the matching continues.
@@ -51,6 +66,17 @@ Catch All segments must be the _last route segment_ in the path (query segments
{{#include ../docs-router/src/doc_examples/catch_all_segments.rs:route}}
```
+
+Parsing your own catch-all segment types
+
+By default, `Vec` collects catch-all segments. You can implement [`FromRouteSegments`](https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromRouteSegments.html) and [`ToRouteSegments`](https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.ToRouteSegments.html) directly to parse the segments into a structured type and serialize them back into a URL:
+
+```rust
+{{#include ../docs-router/src/doc_examples/route_customization.rs:custom_catch_all}}
+```
+
+
+
## Query Segments
Query segments are in the form of `?:name&:othername` where `name` and `othername` are the names of fields in the route variant.
@@ -65,6 +91,25 @@ Query segments must be the _after all route segments_ and cannot be included in
{{#include ../docs-router/src/doc_examples/query_segments.rs:route}}
```
+
+Parsing your own query parameter types
+
+Individual query parameters use the [`FromQueryArgument`](https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQueryArgument.html) trait, which is auto-implemented for any `FromStr + Default` type. If the parameter is missing or fails to parse, `Default::default()` is used instead of failing the route.
+
+You can use your own types as query parameters by implementing `FromStr`, `Default`, and `Display`:
+
+```rust
+{{#include ../docs-router/src/doc_examples/route_customization.rs:custom_single_query}}
+```
+
+If you need full control over the entire query string — for example, to handle dynamic keys or custom serialization — you can capture it into one type using the spread syntax `?:..field`. The type must implement [`From<&str>`](https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQuery.html) and `Display`:
+
+```rust
+{{#include ../docs-router/src/doc_examples/route_customization.rs:custom_spread_query}}
+```
+
+
+
## Hash Segments
Hash segments are in the form of `#:field` where `field` is a field in the route variant.
@@ -79,6 +124,18 @@ Hash fragments must be the _after all route segments and any query segments_ and
{{#include ../docs-router/src/doc_examples/hash_fragments.rs:route}}
```
+
+Parsing your own hash fragment types
+
+The [`FromHashFragment`](https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromHashFragment.html) trait is auto-implemented for any `FromStr + Default` type. Parsing failures return `Default::default()` instead of causing the route to fail.
+
+You can use a custom type to parse structured data from the hash fragment:
+
+```rust
+{{#include ../docs-router/src/doc_examples/route_customization.rs:custom_hash}}
+```
+
+
## Nested Routes
diff --git a/packages/docs-router/src/doc_examples/route_customization.rs b/packages/docs-router/src/doc_examples/route_customization.rs
new file mode 100644
index 000000000..e9b529e17
--- /dev/null
+++ b/packages/docs-router/src/doc_examples/route_customization.rs
@@ -0,0 +1,311 @@
+#![allow(non_snake_case, unused)]
+use dioxus::prelude::*;
+
+// ANCHOR: custom_component
+#[derive(Routable, Clone)]
+#[rustfmt::skip]
+enum Route {
+ // By default, the component rendered for a route matches the variant name.
+ // You can specify a different component as the second argument:
+ #[route("/", HomePage)]
+ Index {},
+}
+
+// This component will be rendered for the Index route
+#[component]
+fn HomePage() -> Element {
+ rsx! { "Welcome home!" }
+}
+// ANCHOR_END: custom_component
+
+fn main() {}
+
+mod custom_segment {
+ use super::*;
+ use std::fmt;
+ use std::str::FromStr;
+
+ // ANCHOR: custom_dynamic_segment
+ /// A locale like "en", "fr", or "es" parsed from a URL segment.
+ #[derive(Clone, PartialEq, Debug)]
+ struct Locale {
+ language: String,
+ }
+
+ /// Display is required so the router can serialize the type back into a URL.
+ impl fmt::Display for Locale {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.language)
+ }
+ }
+
+ /// Any type that implements FromStr can be used as a dynamic segment.
+ /// If parsing fails, the route won't match and the router moves on.
+ impl FromStr for Locale {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result {
+ match s {
+ "en" | "fr" | "es" | "de" | "ja" => Ok(Locale {
+ language: s.to_string(),
+ }),
+ other => Err(format!("Unknown locale: {other}")),
+ }
+ }
+ }
+
+ #[derive(Routable, Clone)]
+ #[rustfmt::skip]
+ enum Route {
+ // With this route, /en/about and /fr/about will match,
+ // but /xyz/about will not.
+ #[route("/:locale/about")]
+ About { locale: Locale },
+ }
+
+ #[component]
+ fn About(locale: Locale) -> Element {
+ rsx! { "Viewing the about page in {locale}" }
+ }
+ // ANCHOR_END: custom_dynamic_segment
+}
+
+mod custom_catch_all {
+ use super::*;
+ use std::fmt;
+
+ // ANCHOR: custom_catch_all
+ /// A path like /docs/en/guide/intro parsed into structured data.
+ #[derive(Clone, PartialEq, Debug)]
+ struct DocPath {
+ locale: String,
+ sections: Vec,
+ }
+
+ impl fmt::Display for DocPath {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.locale)?;
+ for section in &self.sections {
+ write!(f, "/{section}")?;
+ }
+ Ok(())
+ }
+ }
+
+ /// For custom catch-all types, implement both FromRouteSegments (for parsing
+ /// URLs into your type) and ToRouteSegments (for serializing back to a URL).
+ impl dioxus::router::routable::FromRouteSegments for DocPath {
+ type Err = String;
+
+ fn from_route_segments(segments: &[&str]) -> Result {
+ let mut iter = segments.iter();
+ let locale = iter
+ .next()
+ .ok_or("Missing locale segment")?
+ .to_string();
+ let sections = iter.map(|s| s.to_string()).collect();
+ Ok(DocPath { locale, sections })
+ }
+ }
+
+ impl dioxus::router::routable::ToRouteSegments for DocPath {
+ fn display_route_segments(
+ &self,
+ f: &mut std::fmt::Formatter<'_>,
+ ) -> std::fmt::Result {
+ write!(f, "/{}", self.locale)?;
+ for section in &self.sections {
+ write!(f, "/{section}")?;
+ }
+ Ok(())
+ }
+ }
+
+ #[derive(Routable, Clone)]
+ #[rustfmt::skip]
+ enum Route {
+ #[route("/docs/:..path")]
+ Docs { path: DocPath },
+ }
+
+ #[component]
+ fn Docs(path: DocPath) -> Element {
+ rsx! {
+ div { "Locale: {path.locale}" }
+ div { "Sections: {path.sections:?}" }
+ }
+ }
+ // ANCHOR_END: custom_catch_all
+}
+
+mod custom_single_query {
+ use super::*;
+ use std::fmt;
+ use std::str::FromStr;
+
+ // ANCHOR: custom_single_query
+ /// A sort order parsed from a query parameter like ?sort=asc or ?sort=desc.
+ #[derive(Clone, Default, PartialEq, Debug)]
+ enum SortOrder {
+ #[default]
+ Asc,
+ Desc,
+ }
+
+ impl fmt::Display for SortOrder {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ SortOrder::Asc => write!(f, "asc"),
+ SortOrder::Desc => write!(f, "desc"),
+ }
+ }
+ }
+
+ /// Any type that implements FromStr + Default can be used as a query parameter.
+ /// If the parameter is missing or fails to parse, Default::default() is used.
+ impl FromStr for SortOrder {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result {
+ match s {
+ "asc" => Ok(SortOrder::Asc),
+ "desc" => Ok(SortOrder::Desc),
+ other => Err(format!("Unknown sort order: {other}")),
+ }
+ }
+ }
+
+ #[derive(Routable, Clone)]
+ #[rustfmt::skip]
+ enum Route {
+ #[route("/search?:query&:sort")]
+ Search {
+ query: String,
+ sort: SortOrder,
+ },
+ }
+
+ #[component]
+ fn Search(query: String, sort: SortOrder) -> Element {
+ rsx! {
+ div { "Searching for: {query}" }
+ div { "Sort order: {sort}" }
+ }
+ }
+ // ANCHOR_END: custom_single_query
+}
+
+mod custom_spread_query {
+ use super::*;
+ use std::fmt;
+
+ // ANCHOR: custom_spread_query
+ /// A custom type that parses the entire query string at once.
+ /// This is useful when you need full control over query parameter handling.
+ #[derive(Clone, Default, PartialEq, Debug)]
+ struct SearchParams {
+ query: String,
+ page: usize,
+ sort: String,
+ }
+
+ impl fmt::Display for SearchParams {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "query={}&page={}&sort={}",
+ self.query, self.page, self.sort
+ )
+ }
+ }
+
+ /// Implementing From<&str> gives you FromQuery automatically.
+ impl From<&str> for SearchParams {
+ fn from(query: &str) -> Self {
+ let mut params = SearchParams::default();
+ for pair in query.split('&') {
+ if let Some((key, value)) = pair.split_once('=') {
+ match key {
+ "query" => params.query = value.to_string(),
+ "page" => params.page = value.parse().unwrap_or(0),
+ "sort" => params.sort = value.to_string(),
+ _ => {}
+ }
+ }
+ }
+ params
+ }
+ }
+
+ #[derive(Routable, Clone)]
+ #[rustfmt::skip]
+ enum Route {
+ // Use ?:..field to capture the entire query string into a single type.
+ #[route("/search?:..params")]
+ Search { params: SearchParams },
+ }
+
+ #[component]
+ fn Search(params: SearchParams) -> Element {
+ rsx! {
+ div { "Query: {params.query}" }
+ div { "Page: {params.page}" }
+ div { "Sort: {params.sort}" }
+ }
+ }
+ // ANCHOR_END: custom_spread_query
+}
+
+mod custom_hash {
+ use super::*;
+ use std::str::FromStr;
+
+ // ANCHOR: custom_hash
+ /// A section anchor parsed from a hash fragment like #section-intro.
+ #[derive(Clone, Default, PartialEq, Debug)]
+ struct SectionAnchor {
+ section: String,
+ subsection: String,
+ }
+
+ impl std::fmt::Display for SectionAnchor {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}-{}", self.section, self.subsection)
+ }
+ }
+
+ /// Any type that implements FromStr + Default gets FromHashFragment
+ /// automatically. Parsing failures return Default::default().
+ impl FromStr for SectionAnchor {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result {
+ match s.split_once('-') {
+ Some((section, sub)) => Ok(SectionAnchor {
+ section: section.to_string(),
+ subsection: sub.to_string(),
+ }),
+ None => Ok(SectionAnchor {
+ section: s.to_string(),
+ subsection: String::new(),
+ }),
+ }
+ }
+ }
+
+ #[derive(Routable, Clone)]
+ #[rustfmt::skip]
+ enum Route {
+ #[route("/page#:anchor")]
+ Page { anchor: SectionAnchor },
+ }
+
+ #[component]
+ fn Page(anchor: SectionAnchor) -> Element {
+ rsx! {
+ div { "Section: {anchor.section}" }
+ div { "Subsection: {anchor.subsection}" }
+ }
+ }
+ // ANCHOR_END: custom_hash
+}
diff --git a/packages/docsite/assets/githubmarkdown.css b/packages/docsite/assets/githubmarkdown.css
index d5c293882..4f0fa611b 100644
--- a/packages/docsite/assets/githubmarkdown.css
+++ b/packages/docsite/assets/githubmarkdown.css
@@ -336,8 +336,24 @@
padding: 0;
}
+.markdown-body details {
+ border-left: 2px solid var(--color-border-default);
+ padding: 0.25em 1em;
+ transition: border-color 0.15s ease;
+}
+
+.markdown-body details:hover {
+ border-left-color: var(--color-fg-muted);
+}
+
.markdown-body details summary {
cursor: pointer;
+ font-weight: 500;
+ padding: 0.25em 0;
+}
+
+.markdown-body details[open] summary {
+ margin-bottom: 0.5em;
}
.markdown-body details:not([open]) > :not(summary) {
diff --git a/packages/docsite/assets/tailwind.css b/packages/docsite/assets/tailwind.css
index a702ad651..70a024fbc 100644
--- a/packages/docsite/assets/tailwind.css
+++ b/packages/docsite/assets/tailwind.css
@@ -316,6 +316,12 @@
.row-span-3 {
grid-row: span 3 / span 3;
}
+ .float-left {
+ float: left;
+ }
+ .float-right {
+ float: right;
+ }
.container {
width: 100%;
@media (width >= 40rem) {
@@ -448,6 +454,9 @@
.inline-flex {
display: inline-flex;
}
+ .table {
+ display: table;
+ }
.h-2 {
height: calc(var(--spacing) * 2);
}
@@ -622,6 +631,9 @@
.basis-0 {
flex-basis: calc(var(--spacing) * 0);
}
+ .border-collapse {
+ border-collapse: collapse;
+ }
.translate-x-px {
--tw-translate-x: 1px;
translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -1288,6 +1300,9 @@
.uppercase {
text-transform: uppercase;
}
+ .underline {
+ text-decoration-line: underline;
+ }
.placeholder-gray-400 {
&::placeholder {
color: var(--color-gray-400);
@@ -1311,10 +1326,17 @@
--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
+ .outline {
+ outline-style: var(--tw-outline-style);
+ outline-width: 1px;
+ }
.invert {
--tw-invert: invert(100%);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
+ .filter {
+ filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
+ }
.backdrop-blur-sm {
--tw-backdrop-blur: blur(var(--blur-sm));
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
@@ -2309,6 +2331,11 @@
inherits: false;
initial-value: 0 0 #0000;
}
+@property --tw-outline-style {
+ syntax: "*";
+ inherits: false;
+ initial-value: solid;
+}
@property --tw-blur {
syntax: "*";
inherits: false;
@@ -2446,6 +2473,7 @@
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
+ --tw-outline-style: solid;
--tw-blur: initial;
--tw-brightness: initial;
--tw-contrast: initial;
diff --git a/packages/include_mdbook/packages/mdbook-gen/src/rsx.rs b/packages/include_mdbook/packages/mdbook-gen/src/rsx.rs
index 5cd61a46d..916fba72d 100644
--- a/packages/include_mdbook/packages/mdbook-gen/src/rsx.rs
+++ b/packages/include_mdbook/packages/mdbook-gen/src/rsx.rs
@@ -158,7 +158,13 @@ impl<'a, I: Iterator- >> RsxMarkdownParser<'a, I> {
pulldown_cmark::Event::Start(start) => {
self.start_element(start)?;
}
- pulldown_cmark::Event::End(_) => self.end_node(),
+ pulldown_cmark::Event::End(tag_end) => {
+ // HtmlBlock Start doesn't push a node, so don't pop for its End either.
+ // Our
handling pushes via Html events instead.
+ if !matches!(tag_end, pulldown_cmark::TagEnd::HtmlBlock) {
+ self.end_node();
+ }
+ }
pulldown_cmark::Event::Text(text) => {
let text = escape_text(&text);
self.create_node(BodyNode::Text(parse_quote!(#text)));
@@ -172,13 +178,28 @@ impl<'a, I: Iterator- >> RsxMarkdownParser<'a, I> {
})
}
pulldown_cmark::Event::Html(node) | pulldown_cmark::Event::InlineHtml(node) => {
- let code = escape_text(&node);
- self.create_node(parse_quote! {
- p {
- class: "inline-html-block",
- dangerous_inner_html: #code,
- }
- })
+ let trimmed = node.trim();
+ if trimmed == "
" {
+ self.start_node(parse_quote! {
+ details {}
+ });
+ } else if trimmed == " " {
+ self.end_node();
+ } else if trimmed.starts_with("") && trimmed.ends_with("
") {
+ let inner = &trimmed["".len()..trimmed.len() - "
".len()];
+ let inner = escape_text(inner);
+ self.create_node(parse_quote! {
+ summary { #inner }
+ });
+ } else {
+ let code = escape_text(&node);
+ self.create_node(parse_quote! {
+ p {
+ class: "inline-html-block",
+ dangerous_inner_html: #code,
+ }
+ })
+ }
}
pulldown_cmark::Event::FootnoteReference(_) => {}
pulldown_cmark::Event::SoftBreak => {}