Skip to main content

dioxus_docs_kit/components/
docs_page.rs

1use dioxus::prelude::*;
2use dioxus_mdx::{DocContent, DocTableOfContents, EndpointPage, extract_headers};
3
4use crate::DocsContext;
5use crate::registry::DocsRegistry;
6
7use super::copy_page::CopyPageButton;
8use super::docs_layout::LayoutOffsets;
9use super::docs_meta::DocsPageMeta;
10use super::managed_head::NotFoundMeta;
11use super::page_nav::DocsPageNav;
12use super::seo::join_site_url;
13
14/// Documentation page content renderer.
15///
16/// Checks if the path is an API endpoint page or a regular MDX page and renders accordingly.
17/// Missing paths render a noindex page and return HTTP 404 during server
18/// rendering when the `server` feature is enabled.
19///
20/// # Props
21///
22/// - `path`: Current docs page path.
23/// - `article_footer`: Optional element rendered after the article body, before
24///   pagination (e.g. "Was this helpful?" widget).
25///
26/// # Stable public classes
27///
28/// Uses `dk-article`, `dk-article-title`, `dk-article-body`, `dk-article-header`,
29/// and `dk-article-footer-slot` for consumer CSS hooks.
30#[component]
31pub fn DocsPageContent(path: String, article_footer: Option<Element>) -> Element {
32    let registry = use_context::<&'static DocsRegistry>();
33    let ctx = use_context::<DocsContext>();
34
35    let offsets = try_use_context::<LayoutOffsets>().unwrap_or(LayoutOffsets {
36        sticky_top: "top-20",
37        scroll_mt: "scroll-mt-20",
38        sidebar_height: "h-[calc(100vh-5rem)]",
39    });
40
41    // Check if this is an API endpoint page
42    if let Some((operation, spec)) = registry.get_api_operation_with_spec(&path) {
43        return rsx! {
44            DocsPageMeta { path: path.clone() }
45            div { class: "dk-endpoint flex flex-col",
46                EndpointPage { operation: operation.clone(), spec: spec.clone() }
47                main { class: "px-8 lg:px-12 pb-12",
48                    div { class: "max-w-2xl",
49                        if let Some(ft) = article_footer {
50                            div { class: "dk-article-footer-slot mb-6", {ft} }
51                        }
52                        DocsPageNav { current_path: path.clone() }
53                    }
54                }
55            }
56        };
57    }
58
59    let doc = match registry.get_parsed_doc(&path) {
60        Some(d) => d,
61        None => {
62            let base = join_site_url("", &ctx.base_path, &registry.default_path);
63            return rsx! {
64                NotFoundMeta { title: "Documentation page not found", auto_meta: ctx.auto_meta }
65                div { class: "container mx-auto px-8 py-12 max-w-4xl",
66                    div { class: "text-center",
67                        h1 { class: "text-4xl font-bold mb-4", "404" }
68                        p { class: "text-base-content/70 mb-8",
69                            "Page not found: {path}"
70                        }
71                        Link {
72                            to: NavigationTarget::Internal(base),
73                            class: "btn btn-primary",
74                            "Go to Documentation"
75                        }
76                    }
77                }
78            };
79        }
80    };
81
82    let headers = extract_headers(&doc.raw_markdown);
83
84    rsx! {
85        DocsPageMeta { path: path.clone() }
86        div { class: "flex",
87            // Main content
88            main { class: "flex-1 min-w-0 px-8 py-12 lg:px-12",
89                article { class: "dk-article max-w-3xl mx-auto",
90                    // Page header
91                    header { class: "dk-article-header mb-8 pb-8 border-b border-base-300",
92                        div { class: "flex items-start justify-between gap-4",
93                            h1 { class: "dk-article-title text-4xl font-bold tracking-tight mb-3",
94                                "{doc.frontmatter.title}"
95                            }
96                            if !doc.raw_markdown.is_empty() {
97                                CopyPageButton { content: doc.raw_markdown.clone() }
98                            }
99                        }
100                        if let Some(desc) = &doc.frontmatter.description {
101                            p { class: "dk-article-description text-lg text-base-content/70",
102                                "{desc}"
103                            }
104                        }
105                    }
106
107                    // MDX content
108                    div { class: "dk-article-body prose prose-base max-w-none
109                        prose-headings:{offsets.scroll_mt}
110                        prose-h2:text-2xl prose-h2:font-semibold prose-h2:mt-10 prose-h2:mb-4
111                        prose-h3:text-xl prose-h3:font-medium prose-h3:mt-8 prose-h3:mb-3
112                        prose-p:text-base-content/80 prose-p:leading-relaxed
113                        prose-a:text-primary prose-a:no-underline hover:prose-a:underline
114                        prose-code:bg-base-200 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm",
115                        DocContent { nodes: doc.content.clone() }
116                    }
117
118                    // Optional consumer footer slot (e.g. "Was this helpful?")
119                    if let Some(ft) = article_footer {
120                        div { class: "dk-article-footer-slot mt-12", {ft} }
121                    }
122
123                    // Page navigation
124                    DocsPageNav { current_path: path.clone() }
125                }
126            }
127
128            // Table of Contents sidebar (right side)
129            if !headers.is_empty() {
130                aside { class: "dk-toc w-56 shrink-0 hidden xl:block",
131                    div { class: "sticky {offsets.sticky_top} p-6",
132                        DocTableOfContents { headers }
133                    }
134                }
135            }
136        }
137    }
138}