dioxus_docs_kit/components/blog/
category_page.rs1use dioxus::prelude::*;
2
3use super::blog_meta::listing_tags;
4use super::{BlogCard, TagFilter};
5use crate::components::managed_head::{ManagedPageHead, NotFoundMeta};
6use crate::components::seo::join_site_url;
7use crate::{BlogCategory, BlogContext, BlogRegistry};
8
9#[component]
16pub fn BlogCategoryPage(slug: String, #[props(default = 1)] page: usize) -> Element {
17 let registry = use_context::<&'static BlogRegistry>();
18 let ctx = use_context::<BlogContext>();
19 let Some((category, posts)) = registry
20 .get_category(&slug)
21 .zip(registry.category_posts_page(&slug, page))
22 else {
23 return rsx! {
24 NotFoundMeta { title: "Category page not found", auto_meta: ctx.auto_meta }
25 main { class: "max-w-6xl mx-auto px-4 py-12",
26 h1 { class: "text-4xl font-bold mb-4", "Category page not found" }
27 p { class: "text-base-content/70 mb-8", "This topic or page doesn't exist." }
28 Link { to: NavigationTarget::Internal(ctx.base_path), class: "btn btn-primary", "Back to Blog" }
29 }
30 };
31 };
32 let total_pages = registry.total_pages_for_tag(&category.tag);
33 let count = registry.tag_count(&category.tag);
34 rsx! {
35 CategoryMeta { category: category.clone(), page }
36 main { class: "dk-blog-category max-w-6xl mx-auto px-4 py-12",
37 header { class: "max-w-3xl mb-8",
38 Link { to: NavigationTarget::Internal(ctx.base_path), class: "text-sm text-base-content/60 hover:text-primary", "Blog" }
39 h1 { class: "text-4xl font-bold tracking-tight mt-4 mb-3", "{category.title}" }
40 p { class: "text-lg text-base-content/60 leading-relaxed", "{category.description}" }
41 p { class: "text-sm text-base-content/50 mt-4", "{count} articles" }
42 if let Some(image) = &category.image {
43 img { src: "{image}", alt: "", class: "w-full rounded-xl mt-6", loading: "lazy" }
44 }
45 }
46 div { class: "mb-8", TagFilter {} }
47 div { class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6",
48 for post in posts {
49 BlogCard { key: "{post.slug}", post: post.clone() }
50 }
51 }
52 if total_pages > 1 {
53 nav { class: "flex flex-wrap items-center justify-center gap-2 mt-12", aria_label: "Category pagination",
54 if page > 1 {
55 Link { to: NavigationTarget::Internal(registry.category_url(&slug, page - 1).unwrap()), class: "btn btn-ghost btn-sm", "Previous" }
56 }
57 span { class: "text-sm text-base-content/60", "Page {page} of {total_pages}" }
58 if page < total_pages {
59 Link { to: NavigationTarget::Internal(registry.category_url(&slug, page + 1).unwrap()), class: "btn btn-ghost btn-sm", "Next" }
60 }
61 }
62 }
63 }
64 }
65}
66
67#[component]
69fn CategoryMeta(category: BlogCategory, page: usize) -> Element {
70 let ctx = use_context::<BlogContext>();
71 let registry = use_context::<&'static BlogRegistry>();
72 if !ctx.auto_meta {
73 return rsx! {};
74 }
75 let title = if page == 1 {
76 category.title.clone()
77 } else {
78 format!("{} — Page {page}", category.title)
79 };
80 let description = category.description;
81 let path = registry.category_url(&category.slug, page).unwrap();
82 let canonical = ctx
83 .site_url
84 .as_deref()
85 .map(|site| join_site_url(site, &path, ""));
86 let image = category.image.map(|image| {
87 if image.starts_with('/') && !image.starts_with("//") {
88 ctx.site_url
89 .as_deref()
90 .map(|site| join_site_url(site, &image, ""))
91 .unwrap_or(image)
92 } else {
93 image
94 }
95 });
96 let tags = listing_tags(&title, &description, canonical.as_deref(), image.as_deref());
97 rsx! { ManagedPageHead { title, tags } }
98}