//! A project's blog index, described. //! //! `/p/{slug}/blog`: a heading, who wrote it, a link to the project, the feed, //! and the posts. It replaces `templates/pages/project_blog.html` and //! `ProjectBlogTemplate`. //! //! The first consumer of [`quasi_router::Discovery::feed`], which is why it was //! converted: the page wrote its `` by hand in a //! `{% block head %}`, spelling `application/rss+xml` at one of the three sites //! that spell it. The screen says it has a feed and //! [`quasi_router::FeedKind::media_type`] spells it, so the type cannot drift //! from one page to the next. //! //! # Why the route stays an axum handler //! //! [`super::auth_pages`]'s reason, one step milder: the handler resolves a slug //! to a project, that project to its creator, and the creator to their posts, //! then reads an optional unverified session for the header. None of that needs //! a described route, and the mount's [`Viewer`](super::Viewer) offers nothing //! it is missing. What is described is the document. use makeover_layout as layout; use quasi_router::screen::Row; use quasi_router::{Action, Document, Feed, FeedKind, Node, RegionKind, Screen as Described, Slot}; use quasi_webview::Webview; use crate::types::BlogPostSummary; /// The page's own region, and what the skip link points at. pub const PAGE_REGION: &str = "project-blog"; /// How wide it runs. The template wrote this on the body. const MEASURE: layout::Measure = layout::Measure::Wide; /// Where a project's blog feed answers. #[must_use] pub fn feed_path(project_slug: &str) -> String { format!("/p/{project_slug}/blog/feed.xml") } /// The whole document. #[must_use] pub fn screen( project_title: &str, project_slug: &str, creator_username: &str, posts: &[BlogPostSummary], ) -> Described { let feed = feed_path(project_slug); let listing = if posts.is_empty() { Node::empty("No blog posts yet.") } else { Node::list(posts.iter().map(|post| { Row::new(post.title.clone()) .meta(post.published_at.clone()) .activate(Action::get(format!("/p/{project_slug}/blog/{}", post.slug)).navigating()) })) }; let page = Slot::new(PAGE_REGION, RegionKind::Pane) .with(Node::page(format!("{project_title} Blog"))) .with(Node::Link { text: creator_username.to_owned(), action: Action::get(format!("/u/{creator_username}")).navigating(), }) .with(Node::Link { text: "View project".to_owned(), action: Action::get(format!("/p/{project_slug}")).navigating(), }) // The visible offer, beside the autodiscovery tag rather than instead // of it: one is what a reader clicks and the other is what a reader's // app finds, and a page that has a feed owes both. .with(Node::act( "RSS Feed", Action::get(feed.clone()).navigating(), )) .with(listing) // The attribution the template's footer carried. Content rather than // decoration: it is where a reader on a creator's blog finds out whose // platform they are on. .with(Node::Link { text: "Powered by Makenot.work".to_owned(), action: Action::get("/").navigating(), }); Described::single(format!("Blog - {project_title}")) .measured(MEASURE) .documented( Document::default().classed(crate::shell::body_class(MEASURE, &["project-blog-page"])), ) .summarised(format!( "Posts from {project_title}, by {creator_username}." )) .about(quasi_router::SocialKind::Article) .syndicating(Feed::new( FeedKind::Rss, format!("{project_title} - Blog RSS"), feed, )) .with(page) } /// The document this screen is drawn in. #[must_use] pub fn renderer(user: Option<&crate::auth::SessionUser>, csrf: Option<&str>) -> Webview { let csrf = csrf.unwrap_or_default(); Webview::new().with_shell( crate::shell::described() .sending("X-CSRF-Token", csrf) .with_body_last(crate::shell::body_last()) .with_body_first(format!( "{}{}", crate::shell::skip_link(PAGE_REGION), crate::shell::site_header(user) )) .with_head(format!( "", crate::helpers::escape_html(csrf) )), ) } /// Render it. #[must_use] pub fn document( user: Option<&crate::auth::SessionUser>, csrf: Option<&str>, screen: &Described, ) -> String { use quasi_axum::Serves as _; renderer(user, csrf).screen(screen) } #[cfg(test)] mod tests { use super::*; fn posts() -> Vec { vec![BlogPostSummary { title: "First light".to_owned(), slug: "first-light".to_owned(), published_at: "2026-08-01".to_owned(), }] } fn html(screen: &Described) -> String { document(None, Some("t"), screen) } /// The tag the template wrote by hand, said by the screen instead. The /// media type comes off `FeedKind` rather than out of a template, which is /// the whole of what typing it bought. #[test] fn the_feed_is_declared_once_and_spelled_by_the_vocabulary() { let screen = screen("Blue Hour", "blue-hour", "maxj", &posts()); let feed = screen.discovery.feed.as_ref().expect("declared"); assert_eq!(feed.href, "/p/blue-hour/blog/feed.xml"); assert_eq!(feed.title, "Blue Hour - Blog RSS"); let rendered = html(&screen); assert!( rendered.contains( "" ), "{rendered}" ); // Once. The visible link is an anchor, not a second head tag. assert_eq!( rendered.matches("rel=\"alternate\"").count(), 1, "{rendered}" ); } /// A reader clicks a link and a reader's app finds a tag. A page with a /// feed owes both, and the template offered both. #[test] fn the_feed_is_offered_to_a_reader_as_well_as_to_their_app() { let rendered = html(&screen("Blue Hour", "blue-hour", "maxj", &posts())); assert!(rendered.contains(">RSS Feed<"), "{rendered}"); } /// Every post the template listed is still listed, and still reachable. #[test] fn every_post_keeps_its_row_and_its_address() { let rendered = html(&screen("Blue Hour", "blue-hour", "maxj", &posts())); assert!(rendered.contains("First light"), "{rendered}"); assert!( rendered.contains("/p/blue-hour/blog/first-light"), "{rendered}" ); assert!(rendered.contains("2026-08-01"), "{rendered}"); } /// A project with nothing published says so, rather than drawing an empty /// list. `ui::empty_state` is what the template called. #[test] fn a_blog_with_no_posts_says_so() { let rendered = html(&screen("Blue Hour", "blue-hour", "maxj", &[])); assert!(rendered.contains("No blog posts yet."), "{rendered}"); } /// `2790e5c4`. The template wrote the measure and the page token on the /// body; a described document has no container, so both land there. #[test] fn the_document_carries_the_classes_the_template_carried() { let screen = screen("Blue Hour", "blue-hour", "maxj", &posts()); assert_eq!( screen.document.body_class.as_deref(), Some("padded-page project-blog-page") ); } /// The footer attribution the template carried. A reader on a creator's /// blog finds out whose platform they are on from it. #[test] fn the_page_still_says_whose_platform_it_is() { let rendered = html(&screen("Blue Hour", "blue-hour", "maxj", &posts())); assert!(rendered.contains("Powered by Makenot.work"), "{rendered}"); } /// `736f45a5`: none of the four spellings. #[test] fn the_page_spells_no_spinner() { let rendered = html(&screen("Blue Hour", "blue-hour", "maxj", &posts())); for spelling in ["htmx-indicator", "spinner", "loading-text", "loading-state"] { assert!(!rendered.contains(spelling), "{spelling} survives"); } } }