lotsastuff
This commit is contained in:
parent
5b6dd2330d
commit
73f88fd78c
33
Cargo.lock
generated
33
Cargo.lock
generated
|
@ -50,6 +50,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.70"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-compression"
|
name = "async-compression"
|
||||||
version = "0.3.15"
|
version = "0.3.15"
|
||||||
|
@ -283,8 +289,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
|
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"time",
|
||||||
|
"wasm-bindgen",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -600,7 +610,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -959,7 +969,7 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1544,6 +1554,17 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.1.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.26.0"
|
version = "1.26.0"
|
||||||
|
@ -1894,6 +1915,12 @@ dependencies = [
|
||||||
"try-lock",
|
"try-lock",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
@ -1958,8 +1985,10 @@ checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
|
||||||
name = "website"
|
name = "website"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
"cached",
|
"cached",
|
||||||
|
"chrono",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"glob",
|
"glob",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
|
|
@ -6,8 +6,10 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1.0.70"
|
||||||
axum = { version = "0.6.12", features = ["http2"] }
|
axum = { version = "0.6.12", features = ["http2"] }
|
||||||
cached = "0.42.0"
|
cached = "0.42.0"
|
||||||
|
chrono = { version = "0.4.24", features = ["serde"] }
|
||||||
color-eyre = "0.6.1"
|
color-eyre = "0.6.1"
|
||||||
glob = "0.3.0"
|
glob = "0.3.0"
|
||||||
#grass = { version = "0.12.3", features = ["random"] } # not really needed yet
|
#grass = { version = "0.12.3", features = ["random"] } # not really needed yet
|
||||||
|
|
11
Containerfile
Normal file
11
Containerfile
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
FROM rust:slim as build-env
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . /app
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/cc
|
||||||
|
COPY --from=build-env /app/target/release/website /
|
||||||
|
COPY --from=build-env /templates /
|
||||||
|
COPY --from=build-env /posts /
|
||||||
|
COPY --from=build-env /static /
|
||||||
|
CMD ["./website"]
|
|
@ -1,5 +1,6 @@
|
||||||
+++
|
+++
|
||||||
title="TOML metadata test"
|
title="TOML metadata test"
|
||||||
|
date=2023-03-25T14:50:25+01:00
|
||||||
+++
|
+++
|
||||||
|
|
||||||
# Testing post as index within folder
|
# Testing post as index within folder
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
+++
|
+++
|
||||||
title='nested post test does not work'
|
title='nested post test does work!'
|
||||||
|
date=2022-03-25T14:50:25+01:00
|
||||||
+++
|
+++
|
||||||
# yet again a nested post test
|
# yet again a nested post test
|
||||||
|
|
||||||
will it work this time, at least with the slug??
|
will it work this time, at least with the slug??
|
||||||
|
|
||||||
|
how about we go [even deeper!!](evendeeper/)
|
||||||
|
|
7
posts/foldertest/nestedpost/evendeeper.md
Normal file
7
posts/foldertest/nestedpost/evendeeper.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
+++
|
||||||
|
title='how about even deeper??'
|
||||||
|
date=2024-03-25T14:50:25+01:00
|
||||||
|
+++
|
||||||
|
# WOWOAOWOFAODWAOWOAWAOWA
|
||||||
|
|
||||||
|
SOOO DEEP BRO
|
|
@ -1,3 +1,7 @@
|
||||||
|
+++
|
||||||
|
title="test page please ignore"
|
||||||
|
date=2023-03-25T15:16:10+01:00
|
||||||
|
+++
|
||||||
# Test page please ignore
|
# Test page please ignore
|
||||||
|
|
||||||
Hello world!
|
Hello world!
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
use hyper::StatusCode;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::log::*;
|
use tracing::{instrument, log::*};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
response::{Html, IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
|
@ -8,14 +9,24 @@ use axum::{
|
||||||
|
|
||||||
use crate::State;
|
use crate::State;
|
||||||
|
|
||||||
pub mod posts;
|
#[derive(Debug)]
|
||||||
|
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
NotFound,
|
NotFound,
|
||||||
|
InternalError(anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> From<E> for Error
|
||||||
|
where
|
||||||
|
E: Into<anyhow::Error>,
|
||||||
|
{
|
||||||
|
fn from(value: E) -> Self {
|
||||||
|
Error::InternalError(value.into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T = Html<Vec<u8>>> = std::result::Result<T, Error>;
|
pub type Result<T = Html<Vec<u8>>> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[instrument(skip(state))]
|
||||||
pub async fn index(Extension(state): Extension<Arc<State>>) -> Result {
|
pub async fn index(Extension(state): Extension<Arc<State>>) -> Result {
|
||||||
let ctx = tera::Context::new();
|
let ctx = tera::Context::new();
|
||||||
let res = state.tera.render("index.html", &ctx).map_err(|e| {
|
let res = state.tera.render("index.html", &ctx).map_err(|e| {
|
||||||
|
@ -27,10 +38,15 @@ pub async fn index(Extension(state): Extension<Arc<State>>) -> Result {
|
||||||
|
|
||||||
impl IntoResponse for Error {
|
impl IntoResponse for Error {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let result: Vec<u8> = "not found".into();
|
|
||||||
let body = axum::body::boxed(axum::body::Full::from(result));
|
|
||||||
match self {
|
match self {
|
||||||
Error::NotFound => Response::builder().status(404).body(body).unwrap(),
|
Error::NotFound => {
|
||||||
|
info!("not found");
|
||||||
|
(StatusCode::NOT_FOUND, ()).into_response()
|
||||||
|
}
|
||||||
|
Error::InternalError(e) => {
|
||||||
|
error!("internal error: {e}");
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, ()).into_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use axum::{extract::Path, response::Html, Extension};
|
|
||||||
use tracing::log::*;
|
|
||||||
|
|
||||||
use super::{Error, Result};
|
|
||||||
use crate::{post::render_post, State};
|
|
||||||
|
|
||||||
pub async fn view(Path(path): Path<String>, Extension(state): Extension<Arc<State>>) -> Result {
|
|
||||||
info!("Requested post: {}", path);
|
|
||||||
let post = state
|
|
||||||
.posts
|
|
||||||
.iter()
|
|
||||||
.find(|p| p.slug.eq_ignore_ascii_case(&path))
|
|
||||||
.ok_or(Error::NotFound)?;
|
|
||||||
//let post = load_post(&path).await.ok_or(Error::NotFound)?;
|
|
||||||
let res = render_post(&state, post).await.ok_or(Error::NotFound)?;
|
|
||||||
Ok(Html(res.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn index(Extension(state): Extension<Arc<State>>) -> Result {
|
|
||||||
let mut ctx = tera::Context::new();
|
|
||||||
ctx.insert("posts", &state.posts);
|
|
||||||
let res = state.tera.render("postsindex.html", &ctx).map_err(|e| {
|
|
||||||
error!("Failed rendering posts index: {:?}", e);
|
|
||||||
Error::NotFound
|
|
||||||
})?;
|
|
||||||
Ok(Html(res.into()))
|
|
||||||
}
|
|
60
src/main.rs
60
src/main.rs
|
@ -1,17 +1,17 @@
|
||||||
use std::{sync::Arc, time};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use axum::{routing::get, Extension, Router};
|
use axum::{routing::get, Extension, Router};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use post::Post;
|
use post::Post;
|
||||||
use tera::Tera;
|
use tera::Tera;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
||||||
use tracing::log::*;
|
use tracing::{instrument, log::*};
|
||||||
|
|
||||||
mod handlers;
|
mod handlers;
|
||||||
mod post;
|
mod post;
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
posts: Vec<Post>,
|
posts: HashMap<String, Post>,
|
||||||
tera: Tera,
|
tera: Tera,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,32 +21,8 @@ async fn main() -> Result<()> {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
info!("Starting server...");
|
info!("Starting server...");
|
||||||
|
|
||||||
let ts = time::Instant::now();
|
let app = init_app().await?;
|
||||||
|
|
||||||
let tera = Tera::new("templates/**/*")?;
|
|
||||||
let posts = post::load_all()?;
|
|
||||||
|
|
||||||
let state = Arc::new(State { tera, posts });
|
|
||||||
|
|
||||||
let middleware = tower::ServiceBuilder::new()
|
|
||||||
.layer(TraceLayer::new_for_http())
|
|
||||||
.layer(Extension(state.clone()));
|
|
||||||
|
|
||||||
let app = Router::new()
|
|
||||||
.route("/", get(handlers::index))
|
|
||||||
.nest(
|
|
||||||
"/posts",
|
|
||||||
Router::new()
|
|
||||||
.route("/", get(handlers::posts::index))
|
|
||||||
.route("/:route/", get(handlers::posts::view))
|
|
||||||
.fallback_service(tower_http::services::ServeDir::new("./posts")),
|
|
||||||
)
|
|
||||||
.nest_service("/static", tower_http::services::ServeDir::new("./static"))
|
|
||||||
.layer(middleware);
|
|
||||||
|
|
||||||
let duration = time::Instant::now() - ts;
|
|
||||||
|
|
||||||
info!("loaded server in {}ms", duration.as_secs_f64() * 1000.0);
|
|
||||||
info!("Now listening at http://localhost:8180");
|
info!("Now listening at http://localhost:8180");
|
||||||
|
|
||||||
axum::Server::bind(&"0.0.0.0:8180".parse().unwrap())
|
axum::Server::bind(&"0.0.0.0:8180".parse().unwrap())
|
||||||
|
@ -55,3 +31,29 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
pub async fn init_app() -> Result<Router> {
|
||||||
|
let tera = Tera::new("templates/**/*")?;
|
||||||
|
let posts = post::load_all().await?;
|
||||||
|
|
||||||
|
let posts_router = post::build_router(posts.values());
|
||||||
|
|
||||||
|
let state = Arc::new(State { tera, posts });
|
||||||
|
|
||||||
|
let middleware = tower::ServiceBuilder::new()
|
||||||
|
.layer(TraceLayer::new_for_http())
|
||||||
|
.layer(Extension(state))
|
||||||
|
.layer(CompressionLayer::new());
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", get(handlers::index))
|
||||||
|
.nest(
|
||||||
|
"/posts",
|
||||||
|
posts_router.fallback_service(tower_http::services::ServeDir::new("./posts")),
|
||||||
|
)
|
||||||
|
.nest_service("/static", tower_http::services::ServeDir::new("./static"))
|
||||||
|
.layer(middleware);
|
||||||
|
|
||||||
|
Ok(app)
|
||||||
|
}
|
||||||
|
|
218
src/post.rs
218
src/post.rs
|
@ -1,20 +1,30 @@
|
||||||
use std::path::Path;
|
use std::{collections::HashMap, path::Path, sync::Arc};
|
||||||
|
|
||||||
use cached::proc_macro::cached;
|
use axum::{response::Html, routing::get, Extension, Router};
|
||||||
use color_eyre::eyre::Result;
|
use chrono::{DateTime, FixedOffset};
|
||||||
use glob::glob;
|
use glob::glob;
|
||||||
|
|
||||||
|
use hyper::Uri;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use pulldown_cmark::{html, Options, Parser};
|
use pulldown_cmark::{html, Options, Parser};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tracing::log::*;
|
|
||||||
|
|
||||||
use crate::State;
|
use tracing::{instrument, log::*};
|
||||||
|
|
||||||
|
use crate::{handlers, State};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct FrontMatter {
|
pub struct FrontMatter {
|
||||||
pub title: Option<String>,
|
pub title: String,
|
||||||
|
pub date: DateTime<FixedOffset>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct TomlFrontMatter {
|
||||||
|
pub title: String,
|
||||||
|
pub date: toml::value::Datetime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug)]
|
#[derive(Serialize, Clone, Debug)]
|
||||||
|
@ -22,74 +32,48 @@ pub struct Post {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub slug: String,
|
pub slug: String,
|
||||||
pub absolute_path: String,
|
pub absolute_path: String,
|
||||||
pub frontmatter: Option<FrontMatter>,
|
pub frontmatter: FrontMatter,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_all() -> Result<Vec<Post>> {
|
#[instrument]
|
||||||
Ok(glob("posts/**/*.md")?
|
pub async fn load_all() -> color_eyre::eyre::Result<HashMap<String, Post>> {
|
||||||
.map(|p| {
|
let mut res = HashMap::<String, Post>::new();
|
||||||
let path = p.unwrap();
|
for path in glob("posts/**/*.md")? {
|
||||||
info!("found page: {}", path.display());
|
let path = path.unwrap();
|
||||||
|
debug!("found page: {}", path.display());
|
||||||
|
|
||||||
let filename = path.file_name().unwrap();
|
let post = load_post(&path.to_string_lossy()).await?;
|
||||||
let slug = if filename.eq_ignore_ascii_case("index.md") {
|
|
||||||
path.parent().unwrap().to_string_lossy()
|
|
||||||
} else {
|
|
||||||
path.to_string_lossy()
|
|
||||||
}
|
|
||||||
.trim_start_matches("posts")
|
|
||||||
.trim_start_matches(std::path::MAIN_SEPARATOR)
|
|
||||||
.trim_end_matches(".md")
|
|
||||||
.trim_end_matches(std::path::MAIN_SEPARATOR)
|
|
||||||
.replace('\\', "/");
|
|
||||||
|
|
||||||
info!("slug: {slug}");
|
res.insert(post.slug.clone(), post);
|
||||||
|
}
|
||||||
let raw = std::fs::read_to_string(&path).unwrap();
|
Ok(res)
|
||||||
let (frontmatter, content) = parse_frontmatter(raw);
|
|
||||||
|
|
||||||
let content = content.map(|c| {
|
|
||||||
let options = Options::all();
|
|
||||||
let mut content_html = String::new();
|
|
||||||
let parser = Parser::new_ext(&c, options);
|
|
||||||
html::push_html(&mut content_html, parser);
|
|
||||||
content_html
|
|
||||||
});
|
|
||||||
|
|
||||||
Post {
|
|
||||||
absolute_path: format!("/posts/{slug}/"),
|
|
||||||
slug,
|
|
||||||
content: content.unwrap_or_default(),
|
|
||||||
frontmatter,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cached(time = 60, key = "String", convert = r#"{ String::from(path) }"#)]
|
#[instrument]
|
||||||
pub async fn load_post(path: &str) -> Option<Post> {
|
pub async fn load_post(path: &str) -> color_eyre::eyre::Result<Post> {
|
||||||
let path = path
|
let path = path.replace('\\', "/");
|
||||||
.trim_end_matches('/')
|
let slug = path
|
||||||
|
.trim_start_matches("posts")
|
||||||
|
.trim_start_matches('/')
|
||||||
|
.trim_start_matches('\\')
|
||||||
|
.trim_end_matches(".html")
|
||||||
.trim_end_matches(".md")
|
.trim_end_matches(".md")
|
||||||
.trim_end_matches(".html");
|
.trim_end_matches("index")
|
||||||
|
.trim_end_matches('\\')
|
||||||
|
.trim_end_matches('/');
|
||||||
|
|
||||||
info!("loading post: {path}");
|
debug!("loading post: {slug}");
|
||||||
|
|
||||||
let path = if path.starts_with("posts/") {
|
let file_path = Path::new("posts").join(slug);
|
||||||
Path::new(path).to_owned()
|
|
||||||
} else {
|
|
||||||
Path::new("posts").join(path)
|
|
||||||
};
|
|
||||||
|
|
||||||
let content = if let Ok(content) = fs::read_to_string(path.with_extension("md")).await {
|
let content = if let Ok(content) = fs::read_to_string(file_path.with_extension("md")).await {
|
||||||
content
|
|
||||||
} else if let Ok(content) = fs::read_to_string(path.join("index.md")).await {
|
|
||||||
content
|
content
|
||||||
} else {
|
} else {
|
||||||
return None;
|
fs::read_to_string(file_path.join("index.md")).await?
|
||||||
};
|
};
|
||||||
|
|
||||||
let (frontmatter, content) = parse_frontmatter(content);
|
let (tomlfm, content) = parse_frontmatter(content)?;
|
||||||
|
let tomlfm = tomlfm.expect("Missing frontmatter");
|
||||||
|
|
||||||
let content = content.map(|c| {
|
let content = content.map(|c| {
|
||||||
let options = Options::all();
|
let options = Options::all();
|
||||||
|
@ -99,15 +83,31 @@ pub async fn load_post(path: &str) -> Option<Post> {
|
||||||
content_html
|
content_html
|
||||||
});
|
});
|
||||||
|
|
||||||
Some(Post {
|
let date = toml_date_to_chrono(tomlfm.date)?;
|
||||||
absolute_path: format!("/{}/", path.to_string_lossy()),
|
|
||||||
slug: path.to_string_lossy().into(),
|
let frontmatter = FrontMatter {
|
||||||
|
title: tomlfm.title,
|
||||||
|
date,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Post {
|
||||||
|
absolute_path: format!("/posts/{}/", slug),
|
||||||
|
slug: slug.to_string(),
|
||||||
content: content.unwrap_or_default(),
|
content: content.unwrap_or_default(),
|
||||||
frontmatter,
|
frontmatter,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_frontmatter(src: String) -> (Option<FrontMatter>, Option<String>) {
|
fn toml_date_to_chrono(
|
||||||
|
toml: toml::value::Datetime,
|
||||||
|
) -> color_eyre::eyre::Result<DateTime<FixedOffset>> {
|
||||||
|
Ok(DateTime::parse_from_rfc3339(&toml.to_string())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
fn parse_frontmatter(
|
||||||
|
src: String,
|
||||||
|
) -> color_eyre::eyre::Result<(Option<TomlFrontMatter>, Option<String>)> {
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref FRONTMATTER_REGEX: Regex = regex::Regex::new(
|
static ref FRONTMATTER_REGEX: Regex = regex::Regex::new(
|
||||||
r"^[\s]*\+{3}(\r?\n(?s).*?(?-s))\+{3}[\s]*(?:$|(?:\r?\n((?s).*(?-s))$))"
|
r"^[\s]*\+{3}(\r?\n(?s).*?(?-s))\+{3}[\s]*(?:$|(?:\r?\n((?s).*(?-s))$))"
|
||||||
|
@ -115,36 +115,72 @@ fn parse_frontmatter(src: String) -> (Option<FrontMatter>, Option<String>) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(fm) = FRONTMATTER_REGEX.captures(&src) {
|
Ok(if let Some(captures) = FRONTMATTER_REGEX.captures(&src) {
|
||||||
(
|
(
|
||||||
fm.get(1)
|
Some(toml::from_str(captures.get(1).unwrap().as_str())?),
|
||||||
.and_then(|m| toml::from_str(m.as_str()).expect("invalid toml")),
|
captures.get(2).map(|m| m.as_str().to_owned()),
|
||||||
fm.get(2).map(|m| m.as_str().to_owned()),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(None, Some(src))
|
(None, Some(src))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Postcontext {
|
|
||||||
content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cached(time = 60, key = "String", convert = r"{ post.absolute_path.clone() }")]
|
|
||||||
pub async fn render_post(state: &State, post: &Post) -> Option<String> {
|
|
||||||
info!("rendering post: {}", post.absolute_path);
|
|
||||||
|
|
||||||
let ctx = tera::Context::from_serialize(Postcontext {
|
|
||||||
content: post.content.clone(),
|
|
||||||
})
|
})
|
||||||
.ok()?;
|
}
|
||||||
let res = match state.tera.render("post.html", &ctx) {
|
|
||||||
Ok(res) => res,
|
#[instrument(skip(state, post))]
|
||||||
Err(e) => {
|
async fn render_post(state: &State, post: &Post) -> Result<String, handlers::Error> {
|
||||||
error!("Failed rendering post: {}", e);
|
let mut ctx = tera::Context::new();
|
||||||
return None;
|
ctx.insert("title", &post.frontmatter.title);
|
||||||
}
|
ctx.insert("content", &post.content);
|
||||||
};
|
|
||||||
Some(res)
|
state.tera.render("post.html", &ctx).map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(posts))]
|
||||||
|
pub fn build_router<'a, I>(posts: I) -> Router
|
||||||
|
where
|
||||||
|
I: Iterator<Item = &'a Post>,
|
||||||
|
{
|
||||||
|
let mut router = Router::new().route("/", get(index));
|
||||||
|
|
||||||
|
for post in posts {
|
||||||
|
let slug = &post.slug;
|
||||||
|
let path = format!("/{slug}/");
|
||||||
|
info!("adding post route: {path}");
|
||||||
|
router = router.route(&path, get(view));
|
||||||
|
}
|
||||||
|
|
||||||
|
router
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(state))]
|
||||||
|
pub async fn view(
|
||||||
|
uri: Uri,
|
||||||
|
Extension(state): Extension<Arc<State>>,
|
||||||
|
) -> Result<Html<String>, handlers::Error> {
|
||||||
|
debug!("viewing post: {uri}");
|
||||||
|
let post = state
|
||||||
|
.posts
|
||||||
|
.get(uri.path().trim_matches('/'))
|
||||||
|
.ok_or(handlers::Error::NotFound)?;
|
||||||
|
|
||||||
|
let res = render_post(&state, post).await?;
|
||||||
|
|
||||||
|
Ok(Html(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(state))]
|
||||||
|
pub async fn index(
|
||||||
|
Extension(state): Extension<Arc<State>>,
|
||||||
|
) -> Result<Html<String>, handlers::Error> {
|
||||||
|
let mut ctx = tera::Context::new();
|
||||||
|
|
||||||
|
let mut posts = state.posts.values().collect::<Vec<&Post>>();
|
||||||
|
|
||||||
|
posts.sort_by_key(|p| &p.frontmatter.date);
|
||||||
|
|
||||||
|
ctx.insert("title", "Posts");
|
||||||
|
ctx.insert("posts", &posts);
|
||||||
|
|
||||||
|
let res = state.tera.render("posts_index.html", &ctx)?;
|
||||||
|
|
||||||
|
Ok(Html(res))
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,5 +4,9 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="/static/site.css">
|
<link rel="stylesheet" href="/static/site.css">
|
||||||
<link rel="icon" href="/static/avatar.png" />
|
<link rel="icon" href="/static/avatar.png" />
|
||||||
<title>Document</title>
|
{% if title -%}
|
||||||
|
<title>{{ title }} | tollyx.net</title>
|
||||||
|
{% else -%}
|
||||||
|
<title>tollyx.net</title>
|
||||||
|
{% endif -%}
|
||||||
</head>
|
</head>
|
15
templates/posts_index.html
Normal file
15
templates/posts_index.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block main -%}
|
||||||
|
<h1>posts</h1>
|
||||||
|
<p>i occasionally write some stuff i guess</p>
|
||||||
|
<ul>
|
||||||
|
{% for post in posts -%}
|
||||||
|
<li><a href="{{post.absolute_path | safe}}">{% if post.frontmatter -%}
|
||||||
|
{{post.frontmatter.title -}}
|
||||||
|
{% else -%}
|
||||||
|
{{post.slug -}}
|
||||||
|
{% endif -%}
|
||||||
|
</a></li>
|
||||||
|
{% endfor -%}
|
||||||
|
</ul>
|
||||||
|
{% endblock main -%}
|
|
@ -1,11 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block main %}
|
|
||||||
<h1>posts</h1>
|
|
||||||
<p>i occasionally write some stuff i guess</p>
|
|
||||||
<ul>
|
|
||||||
{% for post in posts %}
|
|
||||||
<li><a href="{{post.absolute_path}}">{% if post.frontmatter %}{{post.frontmatter.title}}{% else %}{{post.slug}}{% endif %}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endblock main %}
|
|
Loading…
Reference in a new issue