blog post display and metadata

This commit is contained in:
Michal Vanko 2024-01-11 22:16:38 +01:00
parent ae1b65957d
commit 03fefda519
5 changed files with 72 additions and 21 deletions

View File

@ -1 +1,2 @@
pub mod index; pub mod index;
pub mod post;

View File

@ -0,0 +1,34 @@
use askama::Template;
use axum::{extract::Path, http::StatusCode};
use chrono::{DateTime, Utc};
use serde::Deserialize;
use crate::post_parser::{deserialize_date, parse_post};
#[derive(Deserialize, Debug)]
pub struct PostMetadata {
pub layout: String,
pub title: String,
pub segments: Vec<String>,
pub published: bool,
#[serde(deserialize_with = "deserialize_date")]
pub date: DateTime<Utc>,
pub thumbnail: String,
pub tags: Vec<String>,
}
#[derive(Template)]
#[template(path = "post.html")]
pub struct PostTemplate {
pub title: String,
pub content: String,
}
pub async fn render_post(Path(post_id): Path<String>) -> Result<PostTemplate, StatusCode> {
let path = format!("../_posts/blog/{}.md", post_id);
let parsed = parse_post::<PostMetadata>(&path).await?;
Ok(PostTemplate {
title: parsed.metadata.title,
content: parsed.content,
})
}

View File

@ -1,11 +1,11 @@
use axum::{extract::Path, http::StatusCode, response::Html}; use axum::http::StatusCode;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use gray_matter::{engine::YAML, Matter}; use gray_matter::{engine::YAML, Matter};
use markdown::{to_html_with_options, CompileOptions, Constructs, Options, ParseOptions}; use markdown::{to_html_with_options, CompileOptions, Constructs, Options, ParseOptions};
use serde::{Deserialize, Deserializer}; use serde::{de::DeserializeOwned, Deserialize, Deserializer};
use tokio::fs; use tokio::fs;
fn deserialize_date<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error> pub fn deserialize_date<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
@ -19,20 +19,14 @@ where
} }
} }
#[derive(Deserialize, Debug)] pub struct ParseResult<Metadata> {
pub struct Metadata { pub content: String,
pub layout: String, pub metadata: Metadata,
pub title: String,
pub segments: Vec<String>,
pub published: bool,
#[serde(deserialize_with = "deserialize_date")]
pub date: DateTime<Utc>,
pub thumbnail: String,
pub tags: Vec<String>,
} }
pub async fn parse_post(Path(post_id): Path<String>) -> Result<Html<String>, StatusCode> { pub async fn parse_post<'de, Metadata: DeserializeOwned>(
let path = format!("../_posts/blog/{}.md", post_id); path: &str,
) -> Result<ParseResult<Metadata>, StatusCode> {
let contents = fs::read_to_string(path).await; let contents = fs::read_to_string(path).await;
let raw_content = match contents { let raw_content = match contents {
@ -58,10 +52,7 @@ pub async fn parse_post(Path(post_id): Path<String>) -> Result<Html<String>, Sta
..Default::default() ..Default::default()
}; };
let matter = Matter::<YAML>::new();
let _metadata = matter.parse_with_struct::<Metadata>(&raw_content);
let parsed = to_html_with_options(&raw_content, &markdown_options); let parsed = to_html_with_options(&raw_content, &markdown_options);
let content = match parsed { let content = match parsed {
Err(reason) => { Err(reason) => {
tracing::error!(reason); tracing::error!(reason);
@ -70,5 +61,19 @@ pub async fn parse_post(Path(post_id): Path<String>) -> Result<Html<String>, Sta
Ok(content) => content, Ok(content) => content,
}; };
return Ok(Html(content)); let matter = Matter::<YAML>::new();
let parsed_metadata = matter.parse_with_struct::<Metadata>(&raw_content);
let metadata = match parsed_metadata {
None => {
tracing::error!("Failed to parse metadata");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
Some(metadata) => metadata,
};
return Ok(ParseResult {
content,
metadata: metadata.data,
});
} }

View File

@ -1,4 +1,4 @@
use crate::{pages::index::render_index, post_parser::parse_post}; use crate::pages::{index::render_index, post::render_post};
use axum::{extract::MatchedPath, http::Request, routing::get, Router}; use axum::{extract::MatchedPath, http::Request, routing::get, Router};
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing::info_span; use tracing::info_span;
@ -6,7 +6,7 @@ use tracing::info_span;
pub fn get_router() -> Router { pub fn get_router() -> Router {
Router::new() Router::new()
.route("/", get(render_index)) .route("/", get(render_index))
.route("/blog/:post_id", get(parse_post)) .route("/blog/:post_id", get(render_post))
.layer( .layer(
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
// Log the matched route's path (with placeholders not filled in). // Log the matched route's path (with placeholders not filled in).

View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %}{{title}}{% endblock %}
{% block content %}
<h1>{{title}}</h1>
<article>{{content|escape("none")}}</article>
{# footer #}
{% endblock %}