use std::sync::Arc; use axum::{ extract::{Path, State}, http::{header, HeaderMap, StatusCode}, response::{Html, IntoResponse, Redirect, Response}, routing::get, Router, }; use serde_derive::Serialize; use tracing::instrument; use crate::{post::Post, AppState, WebsiteError}; use super::should_return_304; pub fn router() -> Router> { Router::new() .route("/tags", get(|| async { Redirect::permanent("/") })) .route("/tags/", get(index)) .route("/tags/:tag", get(redirect)) .route("/tags/:tag/", get(view)) .route("/tags/:tag/atom.xml", get(feed)) } #[derive(Serialize, Debug)] struct TagContext<'a> { title: &'a str, } #[instrument(skip(state))] pub async fn index(State(state): State>) -> Result { let tags: Vec<_> = state.tags.values().collect(); let ctx = TagContext { title: "Tags" }; let mut c = tera::Context::new(); c.insert("page", &ctx); c.insert("tags", &tags); let res = state.tera.render("tags_index.html", &c)?; Ok(( StatusCode::OK, [(header::LAST_MODIFIED, state.startup_time.to_rfc2822())], Html(res), ) .into_response()) } #[instrument(skip(state))] pub async fn view( Path(tag): Path, State(state): State>, headers: HeaderMap, ) -> Result { let mut posts: Vec<&Post> = state .posts .values() .filter(|p| !p.draft && p.is_published() && p.tags.contains(&tag)) .collect(); let last_changed = posts.iter().filter_map(|p| p.last_modified()).max(); if should_return_304(&headers, last_changed) { return Ok(StatusCode::NOT_MODIFIED.into_response()); } posts.sort_by_key(|p| &p.date); posts.reverse(); let title = format!("Posts tagged with #{tag}"); let ctx = TagContext { title: &title }; let mut c = tera::Context::new(); c.insert("base_url", &state.base_url.to_string()); c.insert("tag_slug", &tag); c.insert("page", &ctx); c.insert("posts", &posts); let res = state.tera.render("tag.html", &c)?; Ok(( StatusCode::OK, [( header::LAST_MODIFIED, &last_changed.map_or_else(|| state.startup_time.to_rfc2822(), |d| d.to_rfc2822()), )], Html(res), ) .into_response()) } #[instrument(skip(state))] pub async fn feed( Path(slug): Path, State(state): State>, headers: HeaderMap, ) -> Result { let tag = state.tags.get(&slug).ok_or(WebsiteError::NotFound)?; let mut posts: Vec<&Post> = state .posts .values() .filter(|p| p.is_published() && p.tags.contains(&slug)) .collect(); let last_changed = posts.iter().filter_map(|p| p.last_modified()).max(); if should_return_304(&headers, last_changed) { return Ok(StatusCode::NOT_MODIFIED.into_response()); } posts.sort_by_key(|p| &p.date); posts.reverse(); posts.truncate(10); Ok(( StatusCode::OK, [ (header::CONTENT_TYPE, "application/atom+xml"), ( header::LAST_MODIFIED, &last_changed.map_or_else(|| state.startup_time.to_rfc2822(), |d| d.to_rfc2822()), ), ], crate::feed::render_atom_tag_feed(tag, &state)?, ) .into_response()) } #[instrument(skip(state))] pub async fn redirect( Path(slug): Path, State(state): State>, ) -> Result { if state.tags.contains_key(&slug) { Ok(Redirect::permanent(&format!("/tags/{slug}/"))) } else { Err(WebsiteError::NotFound) } }