add tags
This commit is contained in:
parent
f9cdffcd24
commit
ba465f4bde
12 changed files with 157 additions and 15 deletions
|
@ -3,8 +3,6 @@ date = 2018-01-10T17:50:00+01:00
|
|||
draft = false
|
||||
title = "Yet another (traditional) roguelike written in c++"
|
||||
aliases = ["/blog/dungeon/", "/blog/dungeon.html"]
|
||||
|
||||
[taxonomies]
|
||||
tags = ["cpp", "roguelike"]
|
||||
+++
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ use tracing::{instrument, log::*};
|
|||
use crate::{AppState, WebsiteError};
|
||||
|
||||
pub mod posts;
|
||||
pub mod tags;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref HIT_COUNTER: IntCounterVec = prometheus::register_int_counter_vec!(
|
||||
|
@ -31,6 +32,7 @@ pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
|||
Router::new()
|
||||
.route("/", get(index))
|
||||
.nest("/posts", posts::router())
|
||||
.nest("/tags", tags::router())
|
||||
.merge(posts::alias_router(state.posts.values()))
|
||||
.route("/healthcheck", get(healthcheck))
|
||||
.route("/metrics", get(metrics))
|
||||
|
@ -67,17 +69,22 @@ async fn metrics() -> impl IntoResponse {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn not_found() -> Response {
|
||||
(StatusCode::NOT_FOUND, ()).into_response()
|
||||
pub async fn not_found() -> impl IntoResponse {
|
||||
(StatusCode::NOT_FOUND, ())
|
||||
}
|
||||
|
||||
pub async fn metrics_middleware<B>(request: Request<B>, next: Next<B>) -> Response {
|
||||
let path = request.uri().path().to_string();
|
||||
let method = request.method().to_string();
|
||||
|
||||
let response = next.run(request).await;
|
||||
|
||||
if !response.status().is_client_error() {
|
||||
HIT_COUNTER
|
||||
.with_label_values(&[&path, &method, response.status().as_str()])
|
||||
.inc();
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
|
@ -113,9 +120,11 @@ mod tests {
|
|||
async fn setup_routes() {
|
||||
// Load the actual posts, just to make this test fail if
|
||||
// aliases overlap with themselves or other routes
|
||||
let posts = crate::post::load_all().await.unwrap();
|
||||
let state = Arc::new(AppState {
|
||||
tera: tera::Tera::new("templates/**/*").unwrap(),
|
||||
posts: crate::post::load_all().await.unwrap(),
|
||||
tags: crate::tag::get_tags(posts.values()),
|
||||
posts,
|
||||
});
|
||||
|
||||
super::routes(&state)
|
||||
|
|
53
src/handlers/tags.rs
Normal file
53
src/handlers/tags.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::{Router, response::Html, routing::get, extract::{State, Path}};
|
||||
use serde_derive::Serialize;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::{AppState, WebsiteError, post::Post};
|
||||
|
||||
pub fn router() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/", get(index))
|
||||
.route("/:tag/", get(view))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct PageContext<'a> {
|
||||
title: &'a str,
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn index(State(state): State<Arc<AppState>>) -> Result<Html<String>, WebsiteError> {
|
||||
|
||||
let tags: Vec<_> = state.tags.values().collect();
|
||||
let ctx = PageContext { 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(Html(res))
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn view(Path(tag): Path<String>, State(state): State<Arc<AppState>>) -> Result<Html<String>, WebsiteError> {
|
||||
let mut posts: Vec<&Post> = state.posts.values().filter(|p| p.is_published() && p.tags.contains(&tag)).collect();
|
||||
|
||||
posts.sort_by_key(|p| &p.date);
|
||||
posts.reverse();
|
||||
|
||||
let title = format!("Posts tagged with #{tag}");
|
||||
|
||||
let ctx = PageContext { title: &title };
|
||||
|
||||
let mut c = tera::Context::new();
|
||||
c.insert("page", &ctx);
|
||||
c.insert("posts", &posts);
|
||||
|
||||
let res = state.tera.render("tag.html", &c)?;
|
||||
|
||||
Ok(Html(res))
|
||||
}
|
|
@ -4,15 +4,18 @@ use color_eyre::eyre::{Error, Result};
|
|||
|
||||
use post::Post;
|
||||
|
||||
use tag::Tag;
|
||||
use tera::Tera;
|
||||
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
||||
use tower_http::{compression::CompressionLayer, trace::TraceLayer, cors::CorsLayer};
|
||||
use tracing::log::*;
|
||||
|
||||
mod handlers;
|
||||
mod post;
|
||||
mod tag;
|
||||
|
||||
pub struct AppState {
|
||||
posts: HashMap<String, Post>,
|
||||
tags: HashMap<String, Tag>,
|
||||
tera: Tera,
|
||||
}
|
||||
|
||||
|
@ -24,9 +27,11 @@ async fn main() -> Result<()> {
|
|||
|
||||
let tera = Tera::new("templates/**/*")?;
|
||||
let posts = post::load_all().await?;
|
||||
let state = Arc::new(AppState { tera, posts });
|
||||
let tags = tag::get_tags(posts.values());
|
||||
let state = Arc::new(AppState { tera, posts, tags });
|
||||
|
||||
let app = handlers::routes(&state)
|
||||
.layer(CorsLayer::permissive())
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(CompressionLayer::new())
|
||||
.with_state(state);
|
||||
|
|
33
src/tag.rs
Normal file
33
src/tag.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde_derive::Serialize;
|
||||
|
||||
use crate::post::Post;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Tag {
|
||||
pub slug: String,
|
||||
pub absolute_path: String,
|
||||
pub posts: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn get_tags<'a>(posts: impl IntoIterator<Item=&'a Post>) -> HashMap<String,Tag> {
|
||||
let mut tags: HashMap<String,Tag> = HashMap::new();
|
||||
|
||||
for post in posts.into_iter().filter(|p| p.is_published()) {
|
||||
for key in &post.tags {
|
||||
if let Some(tag) = tags.get_mut(key) {
|
||||
tag.posts.push(post.slug.clone());
|
||||
}
|
||||
else {
|
||||
tags.insert(key.clone(), Tag {
|
||||
slug: key.clone(),
|
||||
absolute_path: format!("/tags/{key}/"),
|
||||
posts: vec![post.slug.clone()]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags
|
||||
}
|
|
@ -3,3 +3,12 @@ body {
|
|||
margin: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.tags > li {
|
||||
display: inline-block;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
|
|
@ -9,5 +9,12 @@
|
|||
<small>Posted on <time datetime="{{ page.date }}">{{ page.date | date(format="%Y-%m-%d %H:%M") }}</time></small>
|
||||
{% endif -%}
|
||||
{{ page.content | safe -}}
|
||||
{% if page.tags -%}
|
||||
<small>
|
||||
<ul class="tags">
|
||||
{% for tag in page.tags %}<li><a href="/tags/{{tag}}/">#{{ tag }}</a></li>{% endfor %}
|
||||
</ul>
|
||||
</small>
|
||||
{% endif -%}
|
||||
</article>
|
||||
{% endblock main -%}
|
19
templates/tag.html
Normal file
19
templates/tag.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block main -%}
|
||||
<article>
|
||||
{% if page.title -%}
|
||||
<h1>{{ page.title }}</h1>
|
||||
{% endif -%}
|
||||
<ul>
|
||||
{% for post in posts -%}
|
||||
<li><a href="{{post.absolute_path | safe}}">{% if post.date -%}
|
||||
<time datetime="{{ post.date }}">{{ post.date | date(format="%Y-%m-%d") }}</time> - {{ post.title -}}
|
||||
{% else -%}
|
||||
{{ post.title -}}
|
||||
{% endif -%}
|
||||
</a></li>
|
||||
{% endfor -%}
|
||||
</ul>
|
||||
</article>
|
||||
{% endblock main -%}
|
9
templates/tags_index.html
Normal file
9
templates/tags_index.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% extends "base.html" %}
|
||||
{% block main -%}
|
||||
<h1>Tags</h1>
|
||||
<ul>
|
||||
{% for tag in tags -%}
|
||||
<li><a href="{{tag.absolute_path | safe}}">#{{ tag.slug }}</a></li>
|
||||
{% endfor -%}
|
||||
</ul>
|
||||
{% endblock main -%}
|
Loading…
Reference in a new issue