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