So much stuff
This commit is contained in:
parent
73f88fd78c
commit
3189b57a46
21 changed files with 289 additions and 158 deletions
3
.dockerignore
Normal file
3
.dockerignore
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.git
|
||||||
|
.vscode
|
||||||
|
target
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -3,7 +3,6 @@
|
||||||
"sv"
|
"sv"
|
||||||
],
|
],
|
||||||
"spellright.documentTypes": [
|
"spellright.documentTypes": [
|
||||||
"latex",
|
"latex"
|
||||||
"plaintext"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -20,7 +20,7 @@ regex = "1.7.2"
|
||||||
serde = "1.0.144"
|
serde = "1.0.144"
|
||||||
serde_derive = "1.0.144"
|
serde_derive = "1.0.144"
|
||||||
serde_json = "1.0.85"
|
serde_json = "1.0.85"
|
||||||
tera = "1.17.0"
|
tera = { version = "1.17.0", features = ["builtins"] }
|
||||||
tokio = { version = "1.19.2", features = ["full"] }
|
tokio = { version = "1.19.2", features = ["full"] }
|
||||||
toml = "0.7.3"
|
toml = "0.7.3"
|
||||||
tower = { version = "0.4.12", features = ["full"] }
|
tower = { version = "0.4.12", features = ["full"] }
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
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"]
|
|
65
Dockerfile
Normal file
65
Dockerfile
Normal 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
49
posts/dungeon/index.md
Normal 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).
|
BIN
posts/dungeon/screenshot.png
Normal file
BIN
posts/dungeon/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
|
@ -3,8 +3,6 @@ title="TOML metadata test"
|
||||||
date=2023-03-25T14:50:25+01:00
|
date=2023-03-25T14:50:25+01:00
|
||||||
+++
|
+++
|
||||||
|
|
||||||
# Testing post as index within folder
|
|
||||||
|
|
||||||
hope it works yay
|
hope it works yay
|
||||||
|
|
||||||
here have a squid miku to test relative paths:
|
here have a squid miku to test relative paths:
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
+++
|
|
||||||
title='nested post test does work!'
|
|
||||||
date=2022-03-25T14:50:25+01:00
|
|
||||||
+++
|
|
||||||
# yet again a nested post test
|
|
||||||
|
|
||||||
will it work this time, at least with the slug??
|
|
||||||
|
|
||||||
how about we go [even deeper!!](evendeeper/)
|
|
|
@ -1,7 +0,0 @@
|
||||||
+++
|
|
||||||
title='how about even deeper??'
|
|
||||||
date=2024-03-25T14:50:25+01:00
|
|
||||||
+++
|
|
||||||
# WOWOAOWOFAODWAOWOAWAOWA
|
|
||||||
|
|
||||||
SOOO DEEP BRO
|
|
11
posts/hello-world.md
Normal file
11
posts/hello-world.md
Normal 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)
|
24
posts/status-update-2020-05-16/index.md
Normal file
24
posts/status-update-2020-05-16/index.md
Normal 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
|
|
@ -1,7 +0,0 @@
|
||||||
+++
|
|
||||||
title="test page please ignore"
|
|
||||||
date=2023-03-25T15:16:10+01:00
|
|
||||||
+++
|
|
||||||
# Test page please ignore
|
|
||||||
|
|
||||||
Hello world!
|
|
|
@ -7,43 +7,26 @@ use axum::{
|
||||||
Extension,
|
Extension,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::State;
|
use crate::{State, WebsiteError};
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
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>;
|
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn index(Extension(state): Extension<Arc<State>>) -> Result {
|
pub async fn index(Extension(state): Extension<Arc<State>>) -> std::result::Result<Html<Vec<u8>>, WebsiteError> {
|
||||||
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| {
|
||||||
error!("Failed rendering index: {}", e);
|
error!("Failed rendering index: {}", e);
|
||||||
Error::NotFound
|
WebsiteError::NotFound
|
||||||
})?;
|
})?;
|
||||||
Ok(Html(res.into()))
|
Ok(Html(res.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for Error {
|
impl IntoResponse for WebsiteError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
match self {
|
match self {
|
||||||
Error::NotFound => {
|
WebsiteError::NotFound => {
|
||||||
info!("not found");
|
info!("not found");
|
||||||
(StatusCode::NOT_FOUND, ()).into_response()
|
(StatusCode::NOT_FOUND, ()).into_response()
|
||||||
}
|
}
|
||||||
Error::InternalError(e) => {
|
WebsiteError::InternalError(e) => {
|
||||||
error!("internal error: {e}");
|
error!("internal error: {e}");
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, ()).into_response()
|
(StatusCode::INTERNAL_SERVER_ERROR, ()).into_response()
|
||||||
}
|
}
|
||||||
|
|
24
src/main.rs
24
src/main.rs
|
@ -37,8 +37,6 @@ pub async fn init_app() -> Result<Router> {
|
||||||
let tera = Tera::new("templates/**/*")?;
|
let tera = Tera::new("templates/**/*")?;
|
||||||
let posts = post::load_all().await?;
|
let posts = post::load_all().await?;
|
||||||
|
|
||||||
let posts_router = post::build_router(posts.values());
|
|
||||||
|
|
||||||
let state = Arc::new(State { tera, posts });
|
let state = Arc::new(State { tera, posts });
|
||||||
|
|
||||||
let middleware = tower::ServiceBuilder::new()
|
let middleware = tower::ServiceBuilder::new()
|
||||||
|
@ -50,10 +48,30 @@ pub async fn init_app() -> Result<Router> {
|
||||||
.route("/", get(handlers::index))
|
.route("/", get(handlers::index))
|
||||||
.nest(
|
.nest(
|
||||||
"/posts",
|
"/posts",
|
||||||
posts_router.fallback_service(tower_http::services::ServeDir::new("./posts")),
|
post::router(),
|
||||||
)
|
)
|
||||||
.nest_service("/static", tower_http::services::ServeDir::new("./static"))
|
.nest_service("/static", tower_http::services::ServeDir::new("./static"))
|
||||||
|
.route("/.healthcheck", get(healthcheck))
|
||||||
.layer(middleware);
|
.layer(middleware);
|
||||||
|
|
||||||
Ok(app)
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
147
src/post.rs
147
src/post.rs
|
@ -1,10 +1,9 @@
|
||||||
use std::{collections::HashMap, path::Path, sync::Arc};
|
use std::{collections::HashMap, path::Path, sync::Arc};
|
||||||
|
|
||||||
use axum::{response::Html, routing::get, Extension, Router};
|
use axum::{response::Html, routing::get, Extension, Router, extract};
|
||||||
use chrono::{DateTime, FixedOffset};
|
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;
|
||||||
|
@ -13,26 +12,40 @@ use tokio::fs;
|
||||||
|
|
||||||
use tracing::{instrument, log::*};
|
use tracing::{instrument, log::*};
|
||||||
|
|
||||||
use crate::{handlers, State};
|
use crate::{State, WebsiteError};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Deserialize, Debug, Default)]
|
||||||
pub struct FrontMatter {
|
|
||||||
pub title: String,
|
|
||||||
pub date: DateTime<FixedOffset>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
pub struct TomlFrontMatter {
|
pub struct TomlFrontMatter {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub date: toml::value::Datetime,
|
pub date: Option<toml::value::Datetime>,
|
||||||
|
pub draft: Option<bool>,
|
||||||
|
pub aliases: Option<Vec<String>>,
|
||||||
|
pub tags: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Debug)]
|
#[derive(Serialize, Clone, Debug)]
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
|
pub title: String,
|
||||||
|
pub date: Option<DateTime<FixedOffset>>,
|
||||||
|
pub aliases: Vec<String>,
|
||||||
|
pub tags: Vec<String>,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub slug: String,
|
pub slug: String,
|
||||||
pub absolute_path: String,
|
pub absolute_path: String,
|
||||||
pub frontmatter: FrontMatter,
|
}
|
||||||
|
|
||||||
|
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]
|
#[instrument]
|
||||||
|
@ -42,16 +55,7 @@ pub async fn load_all() -> color_eyre::eyre::Result<HashMap<String, Post>> {
|
||||||
let path = path.unwrap();
|
let path = path.unwrap();
|
||||||
debug!("found page: {}", path.display());
|
debug!("found page: {}", path.display());
|
||||||
|
|
||||||
let post = load_post(&path.to_string_lossy()).await?;
|
let path = path.to_string_lossy().replace('\\', "/");
|
||||||
|
|
||||||
res.insert(post.slug.clone(), post);
|
|
||||||
}
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument]
|
|
||||||
pub async fn load_post(path: &str) -> color_eyre::eyre::Result<Post> {
|
|
||||||
let path = path.replace('\\', "/");
|
|
||||||
let slug = path
|
let slug = path
|
||||||
.trim_start_matches("posts")
|
.trim_start_matches("posts")
|
||||||
.trim_start_matches('/')
|
.trim_start_matches('/')
|
||||||
|
@ -62,6 +66,15 @@ pub async fn load_post(path: &str) -> color_eyre::eyre::Result<Post> {
|
||||||
.trim_end_matches('\\')
|
.trim_end_matches('\\')
|
||||||
.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}");
|
debug!("loading post: {slug}");
|
||||||
|
|
||||||
let file_path = Path::new("posts").join(slug);
|
let file_path = Path::new("posts").join(slug);
|
||||||
|
@ -83,25 +96,7 @@ pub async fn load_post(path: &str) -> color_eyre::eyre::Result<Post> {
|
||||||
content_html
|
content_html
|
||||||
});
|
});
|
||||||
|
|
||||||
let date = toml_date_to_chrono(tomlfm.date)?;
|
Ok(Post::new(slug.to_string(), content.unwrap_or_default(), tomlfm))
|
||||||
|
|
||||||
let frontmatter = FrontMatter {
|
|
||||||
title: tomlfm.title,
|
|
||||||
date,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Post {
|
|
||||||
absolute_path: format!("/posts/{}/", slug),
|
|
||||||
slug: slug.to_string(),
|
|
||||||
content: content.unwrap_or_default(),
|
|
||||||
frontmatter,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toml_date_to_chrono(
|
|
||||||
toml: toml::value::Datetime,
|
|
||||||
) -> color_eyre::eyre::Result<DateTime<FixedOffset>> {
|
|
||||||
Ok(DateTime::parse_from_rfc3339(&toml.to_string())?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
|
@ -125,44 +120,34 @@ fn parse_frontmatter(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(state, post))]
|
#[instrument(skip(tera, post))]
|
||||||
async fn render_post(state: &State, post: &Post) -> Result<String, handlers::Error> {
|
async fn render_post(tera: &tera::Tera, post: &Post) -> Result<String, WebsiteError> {
|
||||||
let mut ctx = tera::Context::new();
|
let mut ctx = tera::Context::new();
|
||||||
ctx.insert("title", &post.frontmatter.title);
|
ctx.insert("page", &post);
|
||||||
ctx.insert("content", &post.content);
|
|
||||||
|
|
||||||
state.tera.render("post.html", &ctx).map_err(|e| e.into())
|
tera.render("post.html", &ctx).map_err(|e| e.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(posts))]
|
pub fn router() -> Router
|
||||||
pub fn build_router<'a, I>(posts: I) -> Router
|
|
||||||
where
|
|
||||||
I: Iterator<Item = &'a Post>,
|
|
||||||
{
|
{
|
||||||
let mut router = Router::new().route("/", get(index));
|
Router::new()
|
||||||
|
.route("/", get(index))
|
||||||
for post in posts {
|
.route("/:slug/", get(view))
|
||||||
let slug = &post.slug;
|
.fallback_service(tower_http::services::ServeDir::new("./posts"))
|
||||||
let path = format!("/{slug}/");
|
|
||||||
info!("adding post route: {path}");
|
|
||||||
router = router.route(&path, get(view));
|
|
||||||
}
|
|
||||||
|
|
||||||
router
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn view(
|
pub async fn view(
|
||||||
uri: Uri,
|
extract::Path(slug): extract::Path<String>,
|
||||||
Extension(state): Extension<Arc<State>>,
|
Extension(state): Extension<Arc<State>>,
|
||||||
) -> Result<Html<String>, handlers::Error> {
|
) -> Result<Html<String>, WebsiteError> {
|
||||||
debug!("viewing post: {uri}");
|
debug!("viewing post: {slug}");
|
||||||
let post = state
|
let post = state
|
||||||
.posts
|
.posts
|
||||||
.get(uri.path().trim_matches('/'))
|
.get(&slug)
|
||||||
.ok_or(handlers::Error::NotFound)?;
|
.ok_or(WebsiteError::NotFound)?;
|
||||||
|
|
||||||
let res = render_post(&state, post).await?;
|
let res = render_post(&state.tera, post).await?;
|
||||||
|
|
||||||
Ok(Html(res))
|
Ok(Html(res))
|
||||||
}
|
}
|
||||||
|
@ -170,17 +155,39 @@ pub async fn view(
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn index(
|
pub async fn index(
|
||||||
Extension(state): Extension<Arc<State>>,
|
Extension(state): Extension<Arc<State>>,
|
||||||
) -> Result<Html<String>, handlers::Error> {
|
) -> Result<Html<String>, WebsiteError> {
|
||||||
let mut ctx = tera::Context::new();
|
let mut ctx = tera::Context::new();
|
||||||
|
|
||||||
let mut posts = state.posts.values().collect::<Vec<&Post>>();
|
let mut posts = state.posts.values().collect::<Vec<&Post>>();
|
||||||
|
|
||||||
posts.sort_by_key(|p| &p.frontmatter.date);
|
posts.sort_by_key(|p| &p.date);
|
||||||
|
posts.reverse();
|
||||||
|
|
||||||
ctx.insert("title", "Posts");
|
ctx.insert("page.title", "Posts");
|
||||||
ctx.insert("posts", &posts);
|
ctx.insert("posts", &posts);
|
||||||
|
|
||||||
let res = state.tera.render("posts_index.html", &ctx)?;
|
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))
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
{% include "partials/head.html" %}
|
{% include "partials/head.html" -%}
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
{% include "partials/header.html" %}
|
{% include "partials/header.html" -%}
|
||||||
</header>
|
</header>
|
||||||
<hr>
|
<hr>
|
||||||
<main>
|
<main>
|
||||||
{% block main %}{% endblock main %}
|
{% block main %}{% endblock main -%}
|
||||||
</main>
|
</main>
|
||||||
<hr>
|
<hr>
|
||||||
<footer>
|
<footer>
|
||||||
{% include "partials/footer.html" %}
|
{% include "partials/footer.html" -%}
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -7,9 +7,9 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li>static content ✅</li>
|
<li>static content ✅</li>
|
||||||
<li>sass compilation</li>
|
<li>sass compilation</li>
|
||||||
<li>post metadata (frontmatter)</li>
|
<li>post metadata (frontmatter) ✅</li>
|
||||||
<li>rss/atom/jsonfeed</li>
|
<li>rss/atom/jsonfeed</li>
|
||||||
<li>proper error handling</li>
|
<li>proper error handling ✅ (i guess??)</li>
|
||||||
<li>other pages???</li>
|
<li>other pages???</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock main %}
|
{% endblock main %}
|
|
@ -4,8 +4,8 @@
|
||||||
<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" />
|
||||||
{% if title -%}
|
{% if page.title -%}
|
||||||
<title>{{ title }} | tollyx.net</title>
|
<title>{{ page.title }} | tollyx.net</title>
|
||||||
{% else -%}
|
{% else -%}
|
||||||
<title>tollyx.net</title>
|
<title>tollyx.net</title>
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main -%}
|
||||||
{{ content | safe }}
|
<article>
|
||||||
{% endblock main %}
|
{% 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 -%}
|
|
@ -1,13 +1,13 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block main -%}
|
{% block main -%}
|
||||||
<h1>posts</h1>
|
<h1>Posts</h1>
|
||||||
<p>i occasionally write some stuff i guess</p>
|
<p>I occasionally write some stuff, it's quite rare but it does happen believe it or not.</p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for post in posts -%}
|
{% for post in posts -%}
|
||||||
<li><a href="{{post.absolute_path | safe}}">{% if post.frontmatter -%}
|
<li><a href="{{post.absolute_path | safe}}">{% if post.date -%}
|
||||||
{{post.frontmatter.title -}}
|
<time datetime="{{ post.date }}">{{ post.date | date(format="%Y-%m-%d") }}</time> - {{ post.title -}}
|
||||||
{% else -%}
|
{% else -%}
|
||||||
{{post.slug -}}
|
{{ post.title -}}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
</a></li>
|
</a></li>
|
||||||
{% endfor -%}
|
{% endfor -%}
|
||||||
|
|
Loading…
Reference in a new issue