1
0
Fork 0

Compare commits

...

3 commits

Author SHA1 Message Date
Adrian Hedqvist 3189b57a46 So much stuff 2023-03-25 21:38:16 +01:00
Adrian Hedqvist 73f88fd78c lotsastuff 2023-03-25 16:15:05 +01:00
Adrian Hedqvist 5b6dd2330d things 2023-03-25 12:24:34 +01:00
22 changed files with 567 additions and 177 deletions

3
.dockerignore Normal file
View file

@ -0,0 +1,3 @@
.git
.vscode
target

3
.markdownlint.json Normal file
View file

@ -0,0 +1,3 @@
{
"MD025": false
}

View file

@ -3,7 +3,6 @@
"sv"
],
"spellright.documentTypes": [
"latex",
"plaintext"
"latex"
]
}

122
Cargo.lock generated
View file

@ -50,6 +50,12 @@ dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
[[package]]
name = "async-compression"
version = "0.3.15"
@ -68,13 +74,13 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.67"
version = "0.1.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86ea188f25f0255d8f92797797c97ebf5631fa88178beb1a46fdf5622c9a00e4"
checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.6",
"syn 2.0.10",
]
[[package]]
@ -283,8 +289,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"serde",
"time",
"wasm-bindgen",
"winapi",
]
@ -355,9 +365,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cpufeatures"
version = "0.2.5"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181"
dependencies = [
"libc",
]
@ -405,7 +415,7 @@ dependencies = [
"proc-macro2",
"quote",
"scratch",
"syn 2.0.6",
"syn 2.0.10",
]
[[package]]
@ -422,7 +432,7 @@ checksum = "631569015d0d8d54e6c241733f944042623ab6df7bc3be7466874b05fcdb1c5f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.6",
"syn 2.0.10",
]
[[package]]
@ -600,7 +610,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
@ -817,9 +827,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "indexmap"
version = "1.9.2"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
@ -959,7 +969,7 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
dependencies = [
"libc",
"log",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
]
@ -1265,9 +1275,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.7.2"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cce168fea28d3e05f158bda4576cf0c844d5045bc2cc3620fa0292ed5bb5814c"
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
dependencies = [
"aho-corasick",
"memchr",
@ -1282,9 +1292,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "rustc-demangle"
version = "0.1.21"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b"
[[package]]
name = "rustversion"
@ -1336,7 +1346,7 @@ checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.6",
"syn 2.0.10",
]
[[package]]
@ -1359,6 +1369,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@ -1459,9 +1478,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.6"
version = "2.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ece519cfaf36269ea69d16c363fa1d59ceba8296bbfbfc003c3176d01f2816ee"
checksum = "5aad1363ed6d37b84299588d62d3a7d95b5a5c2d9aad5c85609fda12afaa1f40"
dependencies = [
"proc-macro2",
"quote",
@ -1523,7 +1542,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.6",
"syn 2.0.10",
]
[[package]]
@ -1535,6 +1554,17 @@ dependencies = [
"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]]
name = "tokio"
version = "1.26.0"
@ -1580,6 +1610,40 @@ dependencies = [
"tracing",
]
[[package]]
name = "toml"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tower"
version = "0.4.13"
@ -1851,6 +1915,12 @@ dependencies = [
"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]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -1915,17 +1985,22 @@ checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
name = "website"
version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"cached",
"chrono",
"color-eyre",
"glob",
"hyper",
"lazy_static",
"pulldown-cmark",
"regex",
"serde",
"serde_derive",
"serde_json",
"tera",
"tokio",
"toml",
"tower",
"tower-http",
"tracing",
@ -2038,6 +2113,15 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "winnow"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28"
dependencies = [
"memchr",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"

View file

@ -6,17 +6,23 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.70"
axum = { version = "0.6.12", features = ["http2"] }
cached = "0.42.0"
chrono = { version = "0.4.24", features = ["serde"] }
color-eyre = "0.6.1"
glob = "0.3.0"
#grass = { version = "0.12.3", features = ["random"] } # not really needed yet
hyper = { version = "0.14.19", features = ["full"] }
lazy_static = "1.4.0"
pulldown-cmark = "0.9.2"
regex = "1.7.2"
serde = "1.0.144"
serde_derive = "1.0.144"
serde_json = "1.0.85"
tera = "1.17.0"
tera = { version = "1.17.0", features = ["builtins"] }
tokio = { version = "1.19.2", features = ["full"] }
toml = "0.7.3"
tower = { version = "0.4.12", features = ["full"] }
tower-http = { version = "0.4.0", features = ["full"] }
tracing = "0.1.35"

65
Dockerfile Normal file
View file

@ -0,0 +1,65 @@
FROM rust:slim AS chef
RUN cargo install cargo-chef
WORKDIR app
####################################################################################################
## Planner
####################################################################################################
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
####################################################################################################
## Builder
####################################################################################################
FROM chef AS builder
RUN rustup target add x86_64-unknown-linux-musl
RUN apt update && apt install -y musl-tools musl-dev
RUN update-ca-certificates
# Create appuser
ENV USER=website
ENV UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
WORKDIR /app
COPY --from=planner /app/recipe.json .
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --target x86_64-unknown-linux-musl --release
####################################################################################################
## Final image
####################################################################################################
FROM scratch
# Import from builder.
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
WORKDIR /app
# Copy our build
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/website ./
COPY --from=builder /app/posts ./posts
COPY --from=builder /app/static ./static
COPY --from=builder /app/templates ./templates
EXPOSE 8180
# Use an unprivileged user.
USER website:website
CMD ["./website"]

49
posts/dungeon/index.md Normal file
View file

@ -0,0 +1,49 @@
+++
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"]
+++
![work-in-progress screenshot](screenshot.png)
My current go-to pet project is [dungeon](https://github.com/tollyx/dungeon), a
roguelike which is currently pretty incomplete and basic, but I think I've
gotten far enough to start showing it off a bit more.
It's a roguelike with your usual fantasy-setting (for now, anyway)
and mechanically it's going to be pretty typical as well. Eventually though,
I'm planning on implementing some kind of bodypart system where potions, food,
traps, enemy attacks and so on can transform your character, giving you both
positive and negative effects.
Also, Lua scripting. I want as much as possible outside the main game engine be
scriptable in Lua. Which means items, status effects, skills, traps, actors and
whatever else that might be fun to have easily modifiable. I'm also using Lua
for basic data loading but that's just because I'm lazy and why the hell not
when Lua is already implemented.
The main things that are currently missing are:
- Menus
- In-game UI (including a log)
- Items (with lua-scripted effects)
- Loading enemies from data
After those and a bit more content I'll be implementing whatever cool ideas that
I come up with.
I'm mainly making it for learning purposes (and for fun), trying out new things
as I go. That lack of planning combined with not actually spending too much
time on it means that progress is slow. The fact that I find myself rewriting
things quite often due to the mentioned lack of planning doesn't help much,
either.
I'll try to write blog posts about whatever in the game that might be
interesting for others, but it will probably take a while before they pop up as
the game is currently pretty basic and will stay so for a while longer.
[You can find the source on github](https://github.com/tollyx/dungeon).

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -1,4 +1,7 @@
# Testing post as index within folder
+++
title="TOML metadata test"
date=2023-03-25T14:50:25+01:00
+++
hope it works yay

11
posts/hello-world.md Normal file
View file

@ -0,0 +1,11 @@
+++
date = 2017-06-25T22:40:00+01:00
draft = false
title = "Hello world, again"
aliases = ["/blog/hello-world/", "/blog/hello-world.html"]
tags = ["hugo", "jekyll"]
+++
So I've yet again remade this website. This time I've moved from [jekyll](https://jekyllrb.com/) to [hugo](https://gohugo.io/), because I've had to reinstall ruby far too many times and there's always some issue with it preventing me from just getting it up and running so I can update this site. Hugo is just a binary, no need to install anything to get it to run. It's far less of a hassle.
If you for whatever reason want to see the old posts (which were basically just dev logs required for some of my school courses), you can find them [here.](@/old/_index.md)

View file

@ -0,0 +1,24 @@
+++
date = 2020-05-16T13:43:00+02:00
draft = false
title = 'Status update: 2020-05-16'
tags = ["hugo", "zola", "roguelike", "spork"]
+++
Oh boy, it's been a while since I wrote anything on this website, over two years ago!
But I've been thinking of starting to write posts more often, try this proggramming-blogging thing out that seems to have had some sort of resurgance. I don't know if I have anything others will find interesting to read, but I've got a few drafts lying around that I'm gonna try finishing up. We'll see how it goes. Just don't expect me to post things too often - maybe once or twice a month, at most.
And with that, I've moved the website to yet another static website generator, this time I'm using [zola]. Why? It's written in rust. That's pretty much all I got. It's actually very similar to [hugo], which I used previously, but I don't know - zola kinda feels nicer to work with when it comes to templates/themes?
My latest free-time project I've been working on is yet another roguelike - this time, I'm following a tutorial so that I won't get stuck for too long figuring out architecture stuff. It's being written in rust and the tutorial I'm following is [this one][roguelike-tutorial]. It's pretty great - it's using [specs] which I've tried out multiple times but I never really got a good hang of figuring out how to do stuff with an ECS, so learning more about that has been nice. But I did the stupid mistake of not writing down some small errors in the tutorial, so I'll probably go back and try to find them again and open a few PR's for them. I'll try to write a post or two about the roguelike here as well. But for now, [you can find the source code for it over here][roguelike-src].
In other news, I bought myself an apartment. I'm moving at the start of June, so that's pretty exciting.
Also, in case someone actually got my rss feed in their reader - sorry, it broke again when I switched to zola, and from what I've read it'll break again in version 0.11 due to a change I agree with. You'll have to fix the feed link, twice. Again, sorry about that.
[zola]: https://getzola.org
[hugo]: https://gohugo.io
[roguelike-tutorial]: https://bfnightly.bracketproductions.com/rustbook/chapter_0.html
[specs]: https://crates.io/crates/specs
[roguelike-src]: https://gitlab.com/tollyx/roguelike-tutorial-rs

View file

@ -1,3 +0,0 @@
# Test page please ignore
Hello world!

View file

@ -1,42 +1,35 @@
use serde_derive::Serialize;
use hyper::StatusCode;
use std::sync::Arc;
use tracing::log::*;
use tracing::{instrument, log::*};
use axum::{
response::{Html, IntoResponse, Response},
Extension,
};
use crate::State;
use crate::{State, WebsiteError};
pub mod posts;
pub enum Error {
NotFound,
}
pub type Result<T = Html<Vec<u8>>> = std::result::Result<T, Error>;
#[derive(Serialize)]
struct PageContext {
content: String,
}
pub async fn index(Extension(state): Extension<Arc<State>>) -> Result {
#[instrument(skip(state))]
pub async fn index(Extension(state): Extension<Arc<State>>) -> std::result::Result<Html<Vec<u8>>, WebsiteError> {
let ctx = tera::Context::new();
let res = state.tera.render("index.html", &ctx).map_err(|e| {
error!("Failed rendering index: {}", e);
Error::NotFound
WebsiteError::NotFound
})?;
Ok(Html(res.into()))
}
impl IntoResponse for Error {
impl IntoResponse for WebsiteError {
fn into_response(self) -> Response {
let result: Vec<u8> = "not found".into();
let body = axum::body::boxed(axum::body::Full::from(result));
match self {
Error::NotFound => Response::builder().status(404).body(body).unwrap(),
WebsiteError::NotFound => {
info!("not found");
(StatusCode::NOT_FOUND, ()).into_response()
}
WebsiteError::InternalError(e) => {
error!("internal error: {e}");
(StatusCode::INTERNAL_SERVER_ERROR, ()).into_response()
}
}
}
}

View file

@ -1,49 +0,0 @@
use std::sync::Arc;
use axum::{extract::Path, response::Html, Extension};
use cached::proc_macro::cached;
use pulldown_cmark::{html, Options, Parser};
use tracing::log::*;
use super::{Error, Result};
use crate::{handlers::PageContext, State};
pub async fn view(Path(path): Path<String>, Extension(state): Extension<Arc<State>>) -> Result {
let post = path.trim_end_matches('/');
info!("Requested post: {}", post);
let res = render_post(&state, post).await.ok_or(Error::NotFound)?;
Ok(Html(res.into()))
}
#[cached(time = 60, key = "String", convert = r"{ path.to_owned() }")]
async fn render_post(state: &State, path: &str) -> Option<String> {
info!("Rendering post...");
let post = state.posts.iter().find(|p| p.slug == path)?;
let options = Options::all();
let parser = Parser::new_ext(&post.content, options);
let mut out = String::new();
html::push_html(&mut out, parser);
let ctx = tera::Context::from_serialize(PageContext { content: out }).ok()?;
let res = match state.tera.render("post.html", &ctx) {
Ok(res) => res,
Err(e) => {
error!("Failed rendering post: {}", e);
return None;
}
};
Some(res)
}
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()))
}

View file

@ -1,78 +1,27 @@
use std::sync::Arc;
use std::{collections::HashMap, sync::Arc};
use axum::{
routing::get,
Extension, Router,
};
use axum::{routing::get, Extension, Router};
use color_eyre::eyre::Result;
use glob::glob;
use serde_derive::Serialize;
use post::Post;
use tera::Tera;
use tower_http::trace::TraceLayer;
use tracing::log::*;
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
use tracing::{instrument, log::*};
mod handlers;
mod post;
pub struct State {
posts: Vec<Post>,
posts: HashMap<String, Post>,
tera: Tera,
}
#[derive(Serialize)]
pub struct Post {
pub name: String,
pub slug: String,
pub content: String,
}
#[tokio::main]
async fn main() -> Result<()> {
color_eyre::install()?;
tracing_subscriber::fmt::init();
info!("Starting server...");
let tera = Tera::new("templates/**/*")?;
let posts = glob("posts/**/*.md")?
.map(|p| {
let path = p.unwrap();
info!("found page: {}", path.display());
let filename = path.file_name().unwrap().to_string_lossy();
let (filename, _) = filename.rsplit_once('.').unwrap();
let slug = if filename.eq_ignore_ascii_case("index") {
path.parent()
.unwrap()
.file_name()
.unwrap()
.to_string_lossy()
.into_owned()
} else {
filename.to_owned()
};
Post {
name: slug.clone(),
slug,
content: std::fs::read_to_string(&path).unwrap(),
}
})
.collect();
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 app = init_app().await?;
info!("Now listening at http://localhost:8180");
@ -82,3 +31,47 @@ async fn main() -> Result<()> {
Ok(())
}
#[instrument]
pub async fn init_app() -> Result<Router> {
let tera = Tera::new("templates/**/*")?;
let posts = post::load_all().await?;
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",
post::router(),
)
.nest_service("/static", tower_http::services::ServeDir::new("./static"))
.route("/.healthcheck", get(healthcheck))
.layer(middleware);
Ok(app)
}
async fn healthcheck() -> &'static str {
"OK"
}
#[derive(Debug)]
pub enum WebsiteError {
NotFound,
InternalError(anyhow::Error),
}
impl<E> From<E> for WebsiteError
where
E: Into<anyhow::Error>,
{
fn from(value: E) -> Self {
WebsiteError::InternalError(value.into())
}
}

193
src/post.rs Normal file
View file

@ -0,0 +1,193 @@
use std::{collections::HashMap, path::Path, sync::Arc};
use axum::{response::Html, routing::get, Extension, Router, extract};
use chrono::{DateTime, FixedOffset};
use glob::glob;
use lazy_static::lazy_static;
use pulldown_cmark::{html, Options, Parser};
use regex::Regex;
use serde_derive::{Deserialize, Serialize};
use tokio::fs;
use tracing::{instrument, log::*};
use crate::{State, WebsiteError};
#[derive(Deserialize, Debug, Default)]
pub struct TomlFrontMatter {
pub title: String,
pub date: Option<toml::value::Datetime>,
pub draft: Option<bool>,
pub aliases: Option<Vec<String>>,
pub tags: Option<Vec<String>>,
}
#[derive(Serialize, Clone, Debug)]
pub struct Post {
pub title: String,
pub date: Option<DateTime<FixedOffset>>,
pub aliases: Vec<String>,
pub tags: Vec<String>,
pub content: String,
pub slug: String,
pub absolute_path: String,
}
impl Post {
pub fn new(slug: String, content: String, fm: TomlFrontMatter) -> Post {
Post {
absolute_path: format!("/posts/{}/", slug),
slug,
content,
title: fm.title,
date: fm.date.map(|d| DateTime::parse_from_rfc3339(&d.to_string()).expect("bad toml datetime")),
aliases: fm.aliases.unwrap_or_default(),
tags: fm.tags.unwrap_or_default(),
}
}
}
#[instrument]
pub async fn load_all() -> color_eyre::eyre::Result<HashMap<String, Post>> {
let mut res = HashMap::<String, Post>::new();
for path in glob("posts/**/*.md")? {
let path = path.unwrap();
debug!("found page: {}", path.display());
let path = path.to_string_lossy().replace('\\', "/");
let slug = path
.trim_start_matches("posts")
.trim_start_matches('/')
.trim_start_matches('\\')
.trim_end_matches(".html")
.trim_end_matches(".md")
.trim_end_matches("index")
.trim_end_matches('\\')
.trim_end_matches('/');
let post = load_post(slug).await?;
res.insert(slug.to_string(), post);
}
Ok(res)
}
#[instrument]
pub async fn load_post(slug: &str) -> color_eyre::eyre::Result<Post> {
debug!("loading post: {slug}");
let file_path = Path::new("posts").join(slug);
let content = if let Ok(content) = fs::read_to_string(file_path.with_extension("md")).await {
content
} else {
fs::read_to_string(file_path.join("index.md")).await?
};
let (tomlfm, content) = parse_frontmatter(content)?;
let tomlfm = tomlfm.expect("Missing frontmatter");
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
});
Ok(Post::new(slug.to_string(), content.unwrap_or_default(), tomlfm))
}
#[instrument]
fn parse_frontmatter(
src: String,
) -> color_eyre::eyre::Result<(Option<TomlFrontMatter>, Option<String>)> {
lazy_static! {
static ref FRONTMATTER_REGEX: Regex = regex::Regex::new(
r"^[\s]*\+{3}(\r?\n(?s).*?(?-s))\+{3}[\s]*(?:$|(?:\r?\n((?s).*(?-s))$))"
)
.unwrap();
};
Ok(if let Some(captures) = FRONTMATTER_REGEX.captures(&src) {
(
Some(toml::from_str(captures.get(1).unwrap().as_str())?),
captures.get(2).map(|m| m.as_str().to_owned()),
)
} else {
(None, Some(src))
})
}
#[instrument(skip(tera, post))]
async fn render_post(tera: &tera::Tera, post: &Post) -> Result<String, WebsiteError> {
let mut ctx = tera::Context::new();
ctx.insert("page", &post);
tera.render("post.html", &ctx).map_err(|e| e.into())
}
pub fn router() -> Router
{
Router::new()
.route("/", get(index))
.route("/:slug/", get(view))
.fallback_service(tower_http::services::ServeDir::new("./posts"))
}
#[instrument(skip(state))]
pub async fn view(
extract::Path(slug): extract::Path<String>,
Extension(state): Extension<Arc<State>>,
) -> Result<Html<String>, WebsiteError> {
debug!("viewing post: {slug}");
let post = state
.posts
.get(&slug)
.ok_or(WebsiteError::NotFound)?;
let res = render_post(&state.tera, post).await?;
Ok(Html(res))
}
#[instrument(skip(state))]
pub async fn index(
Extension(state): Extension<Arc<State>>,
) -> Result<Html<String>, WebsiteError> {
let mut ctx = tera::Context::new();
let mut posts = state.posts.values().collect::<Vec<&Post>>();
posts.sort_by_key(|p| &p.date);
posts.reverse();
ctx.insert("page.title", "Posts");
ctx.insert("posts", &posts);
let res = match state.tera.render("posts_index.html", &ctx) {
Ok(r) => r,
Err(e) => {
error!("failed to render posts index: {}", e);
return Err(e.into());
}
};
Ok(Html(res))
}
#[cfg(test)]
mod tests {
use tera::Tera;
#[tokio::test]
async fn render_all() {
let tera = Tera::new("templates/**/*").unwrap();
let posts = super::load_all().await.unwrap();
for (_slug, post) in posts {
super::render_post(&tera, &post).await.unwrap();
}
}
}

View file

@ -1,17 +1,17 @@
<!DOCTYPE html>
<html lang="en">
{% include "partials/head.html" %}
{% include "partials/head.html" -%}
<body>
<header>
{% include "partials/header.html" %}
{% include "partials/header.html" -%}
</header>
<hr>
<main>
{% block main %}{% endblock main %}
{% block main %}{% endblock main -%}
</main>
<hr>
<footer>
{% include "partials/footer.html" %}
{% include "partials/footer.html" -%}
</footer>
</body>
</html>

View file

@ -7,9 +7,9 @@
<ul>
<li>static content ✅</li>
<li>sass compilation</li>
<li>post metadata (frontmatter)</li>
<li>post metadata (frontmatter)</li>
<li>rss/atom/jsonfeed</li>
<li>proper error handling</li>
<li>proper error handling ✅ (i guess??)</li>
<li>other pages???</li>
</ul>
{% endblock main %}

View file

@ -4,5 +4,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/site.css">
<link rel="icon" href="/static/avatar.png" />
<title>Document</title>
{% if page.title -%}
<title>{{ page.title }} | tollyx.net</title>
{% else -%}
<title>tollyx.net</title>
{% endif -%}
</head>

View file

@ -1,5 +1,13 @@
{% extends "base.html" %}
{% block main %}
{{ content | safe }}
{% endblock main %}
{% block main -%}
<article>
{% if page.title -%}
<h1>{{ page.title }}</h1>
{% endif -%}
{% if page.date -%}
<small>Posted on <time datetime="{{ page.date }}">{{ page.date | date(format="%Y-%m-%d %H:%M") }}</time></small>
{% endif -%}
{{ page.content | safe -}}
</article>
{% endblock main -%}

View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block main -%}
<h1>Posts</h1>
<p>I occasionally write some stuff, it's quite rare but it does happen believe it or not.</p>
<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>
{% endblock main -%}

View file

@ -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="/posts/{{post.name}}/">{{post.name}}</a></li>
{% endfor %}
</ul>
{% endblock main %}