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
|
draft = false
|
||||||
title = "Yet another (traditional) roguelike written in c++"
|
title = "Yet another (traditional) roguelike written in c++"
|
||||||
aliases = ["/blog/dungeon/", "/blog/dungeon.html"]
|
aliases = ["/blog/dungeon/", "/blog/dungeon.html"]
|
||||||
|
|
||||||
[taxonomies]
|
|
||||||
tags = ["cpp", "roguelike"]
|
tags = ["cpp", "roguelike"]
|
||||||
+++
|
+++
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ use tracing::{instrument, log::*};
|
||||||
use crate::{AppState, WebsiteError};
|
use crate::{AppState, WebsiteError};
|
||||||
|
|
||||||
pub mod posts;
|
pub mod posts;
|
||||||
|
pub mod tags;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref HIT_COUNTER: IntCounterVec = prometheus::register_int_counter_vec!(
|
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()
|
Router::new()
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.nest("/posts", posts::router())
|
.nest("/posts", posts::router())
|
||||||
|
.nest("/tags", tags::router())
|
||||||
.merge(posts::alias_router(state.posts.values()))
|
.merge(posts::alias_router(state.posts.values()))
|
||||||
.route("/healthcheck", get(healthcheck))
|
.route("/healthcheck", get(healthcheck))
|
||||||
.route("/metrics", get(metrics))
|
.route("/metrics", get(metrics))
|
||||||
|
@ -67,17 +69,22 @@ async fn metrics() -> impl IntoResponse {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn not_found() -> Response {
|
pub async fn not_found() -> impl IntoResponse {
|
||||||
(StatusCode::NOT_FOUND, ()).into_response()
|
(StatusCode::NOT_FOUND, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn metrics_middleware<B>(request: Request<B>, next: Next<B>) -> Response {
|
pub async fn metrics_middleware<B>(request: Request<B>, next: Next<B>) -> Response {
|
||||||
let path = request.uri().path().to_string();
|
let path = request.uri().path().to_string();
|
||||||
let method = request.method().to_string();
|
let method = request.method().to_string();
|
||||||
|
|
||||||
let response = next.run(request).await;
|
let response = next.run(request).await;
|
||||||
HIT_COUNTER
|
|
||||||
.with_label_values(&[&path, &method, response.status().as_str()])
|
if !response.status().is_client_error() {
|
||||||
.inc();
|
HIT_COUNTER
|
||||||
|
.with_label_values(&[&path, &method, response.status().as_str()])
|
||||||
|
.inc();
|
||||||
|
}
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,9 +120,11 @@ mod tests {
|
||||||
async fn setup_routes() {
|
async fn setup_routes() {
|
||||||
// Load the actual posts, just to make this test fail if
|
// Load the actual posts, just to make this test fail if
|
||||||
// aliases overlap with themselves or other routes
|
// aliases overlap with themselves or other routes
|
||||||
|
let posts = crate::post::load_all().await.unwrap();
|
||||||
let state = Arc::new(AppState {
|
let state = Arc::new(AppState {
|
||||||
tera: tera::Tera::new("templates/**/*").unwrap(),
|
tera: tera::Tera::new("templates/**/*").unwrap(),
|
||||||
posts: crate::post::load_all().await.unwrap(),
|
tags: crate::tag::get_tags(posts.values()),
|
||||||
|
posts,
|
||||||
});
|
});
|
||||||
|
|
||||||
super::routes(&state)
|
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 post::Post;
|
||||||
|
|
||||||
|
use tag::Tag;
|
||||||
use tera::Tera;
|
use tera::Tera;
|
||||||
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
use tower_http::{compression::CompressionLayer, trace::TraceLayer, cors::CorsLayer};
|
||||||
use tracing::log::*;
|
use tracing::log::*;
|
||||||
|
|
||||||
mod handlers;
|
mod handlers;
|
||||||
mod post;
|
mod post;
|
||||||
|
mod tag;
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
posts: HashMap<String, Post>,
|
posts: HashMap<String, Post>,
|
||||||
|
tags: HashMap<String, Tag>,
|
||||||
tera: Tera,
|
tera: Tera,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,9 +27,11 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let tera = Tera::new("templates/**/*")?;
|
let tera = Tera::new("templates/**/*")?;
|
||||||
let posts = post::load_all().await?;
|
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)
|
let app = handlers::routes(&state)
|
||||||
|
.layer(CorsLayer::permissive())
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.layer(CompressionLayer::new())
|
.layer(CompressionLayer::new())
|
||||||
.with_state(state);
|
.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
|
||||||
|
}
|
|
@ -2,4 +2,13 @@ body {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags > li {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
|
@ -14,4 +14,4 @@
|
||||||
{% include "partials/footer.html" -%}
|
{% include "partials/footer.html" -%}
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -21,4 +21,4 @@
|
||||||
<li>⬜ image processing?? (resizing, conversion)</li>
|
<li>⬜ image processing?? (resizing, conversion)</li>
|
||||||
<li>⬜ opentelemetry?</li>
|
<li>⬜ opentelemetry?</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock main %}
|
{% endblock main %}
|
||||||
|
|
|
@ -9,5 +9,12 @@
|
||||||
<small>Posted on <time datetime="{{ page.date }}">{{ page.date | date(format="%Y-%m-%d %H:%M") }}</time></small>
|
<small>Posted on <time datetime="{{ page.date }}">{{ page.date | date(format="%Y-%m-%d %H:%M") }}</time></small>
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{{ page.content | safe -}}
|
{{ 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>
|
</article>
|
||||||
{% endblock main -%}
|
{% endblock main -%}
|
||||||
|
|
|
@ -12,4 +12,4 @@
|
||||||
</a></li>
|
</a></li>
|
||||||
{% endfor -%}
|
{% endfor -%}
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock main -%}
|
{% 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