Skip to main content

dioxus_docs_kit/components/blog/
blog_post.rs

1use dioxus::prelude::*;
2use dioxus_free_icons::Icon;
3use dioxus_free_icons::icons::ld_icons::LdClock;
4use dioxus_mdx::{DocContent, DocTableOfContents, extract_headers};
5
6use crate::BlogContext;
7use crate::blog::registry::BlogRegistry;
8
9use super::author_info::AuthorInfo;
10use super::blog_meta::BlogPostMeta;
11use super::post_nav::BlogPostNav;
12use super::progress_bar::ReadingProgressBar;
13use super::related_posts::RelatedPosts;
14
15/// Single blog post view.
16#[component]
17pub fn BlogPostView(slug: String) -> Element {
18    let registry = use_context::<&'static BlogRegistry>();
19    let ctx = use_context::<BlogContext>();
20
21    let post = match registry.get_post(&slug) {
22        Some(p) => p,
23        None => {
24            let base = ctx.base_path.clone();
25            return rsx! {
26                div { class: "max-w-4xl mx-auto px-4 py-12",
27                    div { class: "text-center",
28                        h1 { class: "text-4xl font-bold mb-4", "404" }
29                        p { class: "text-base-content/70 mb-8",
30                            "Post not found: {slug}"
31                        }
32                        Link {
33                            to: NavigationTarget::Internal(base),
34                            class: "btn btn-primary",
35                            "Back to Blog"
36                        }
37                    }
38                }
39            };
40        }
41    };
42
43    let date_display = registry.format_date(&post.frontmatter.date);
44    let headers = extract_headers(&post.raw_markdown);
45
46    rsx! {
47        ReadingProgressBar {}
48        BlogPostMeta { slug: slug.clone() }
49        div { class: "flex max-w-6xl mx-auto",
50            main { class: "flex-1 min-w-0 px-4 py-12 lg:px-12",
51                article { class: "max-w-3xl mx-auto",
52                    if let Some(ref cover) = post.frontmatter.cover_image {
53                        div { class: "mb-8 rounded-xl overflow-hidden",
54                            img {
55                                src: "{cover}",
56                                alt: "{post.frontmatter.title}",
57                                class: "w-full",
58                            }
59                        }
60                    }
61
62                    header { class: "mb-8 pb-8 border-b border-base-300",
63                        if !post.frontmatter.tags.is_empty() {
64                            div { class: "flex flex-wrap gap-1.5 mb-4",
65                                for tag in post.frontmatter.tags.iter() {
66                                    span { class: "badge badge-sm badge-outline badge-primary font-medium",
67                                        "{tag}"
68                                    }
69                                }
70                            }
71                        }
72
73                        h1 { class: "text-4xl font-bold tracking-tight mb-4",
74                            "{post.frontmatter.title}"
75                        }
76
77                        if let Some(ref desc) = post.frontmatter.description {
78                            p { class: "text-lg text-base-content/60 mb-6",
79                                "{desc}"
80                            }
81                        }
82
83                        div { class: "flex items-center gap-4 text-sm text-base-content/60",
84                            AuthorInfo { author_id: post.frontmatter.author.clone() }
85                            span { class: "text-base-content/30", "|" }
86                            span { "{date_display}" }
87                            span { class: "text-base-content/30", "|" }
88                            div { class: "flex items-center gap-1",
89                                Icon { class: "size-3.5", icon: LdClock }
90                                span { "{post.reading_time_minutes} min read" }
91                            }
92                        }
93                    }
94
95                    div { class: "prose prose-base max-w-none
96                        prose-headings:scroll-mt-20
97                        prose-h2:text-2xl prose-h2:font-semibold prose-h2:mt-10 prose-h2:mb-4
98                        prose-h3:text-xl prose-h3:font-medium prose-h3:mt-8 prose-h3:mb-3
99                        prose-p:text-base-content/80 prose-p:leading-relaxed
100                        prose-a:text-primary prose-a:no-underline hover:prose-a:underline
101                        prose-code:bg-base-200 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm",
102                        DocContent { nodes: post.content.clone() }
103                    }
104
105                    RelatedPosts { slug: slug.clone() }
106                    BlogPostNav { current_slug: slug.clone() }
107                }
108            }
109
110            if !headers.is_empty() {
111                aside { class: "w-56 shrink-0 hidden xl:block",
112                    div { class: "sticky top-20 p-6",
113                        DocTableOfContents { headers }
114                    }
115                }
116            }
117        }
118    }
119}