Skip to main content

dioxus_docs_kit/components/blog/
category_page.rs

1use 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/// A shareable topic page showing published posts with the category's tag.
10///
11/// Enable with `BlogConfig::with_category_base_path` and mount on
12/// `<base>/:slug` and `<base>/:slug/page/:page`. Page numbers start at one.
13/// Unknown categories and invalid pages render a noindex not-found page and
14/// return HTTP 404 when the `server` feature is enabled.
15#[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// Category-specific canonical URL and social metadata.
68#[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}