diff --git a/404.html b/404.html new file mode 100644 index 0000000..08d5bf3 --- /dev/null +++ b/404.html @@ -0,0 +1,12 @@ + + + + + + + 404 Not Found + + +

404 Not Found

+ + \ No newline at end of file diff --git a/posts/foldertest/index.md b/posts/foldertest/index.md index d50de83..7c3a5b0 100644 --- a/posts/foldertest/index.md +++ b/posts/foldertest/index.md @@ -1,6 +1,7 @@ +++ title="TOML metadata test" date=2023-03-26T11:57:00+02:00 +updated=2023-04-03T22:07:57+02:00 +++ hope it works yay diff --git a/src/feed.rs b/src/feed.rs new file mode 100644 index 0000000..1f1a138 --- /dev/null +++ b/src/feed.rs @@ -0,0 +1,47 @@ +use serde::Serialize; +use tera::Tera; +use tracing::instrument; +use serde_derive::Serialize; +use color_eyre::Result; + +use crate::{post::Post, tag::Tag}; + + +#[derive(Serialize)] +struct FeedContext<'a> { + feed_url: &'a str, + last_updated: &'a str, + tag: Option<&'a Tag>, + posts: &'a [&'a Post], +} + +#[instrument(skip(posts, tera))] +pub fn render_atom_feed(posts: &[&Post], tera: &Tera) -> Result { + let updated = posts.iter().map(|p| p.updated.or(p.date)).max().flatten(); + let feed = FeedContext { + feed_url: "https://tollyx.net/atom.xml", + last_updated: &updated.map_or_else(String::default, |d| d.to_rfc3339()), + tag: None, + posts, + }; + + let ctx = tera::Context::from_serialize(&feed)?; + + Ok(tera.render("atom.xml", &ctx)?) +} + +#[instrument(skip(tag, posts, tera))] +pub fn render_atom_tag_feed(tag: &Tag, posts: &[&Post], tera: &Tera) -> Result { + let updated = posts.iter().map(|p| p.updated.or(p.date)).max().flatten(); + let slug = &tag.slug; + let feed = FeedContext { + feed_url: &format!("https://tollyx.net/tags/{slug}/atom.xml"), + last_updated: &updated.map_or_else(String::default, |d| d.to_rfc3339()), + tag: Some(tag), + posts + }; + + let ctx = tera::Context::from_serialize(&feed)?; + + Ok(tera.render("atom.xml", &ctx)?) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 4af9194..184a254 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -9,6 +9,7 @@ use axum::{ use hyper::{header::CONTENT_TYPE, Request, StatusCode}; use lazy_static::lazy_static; use prometheus::{opts, Encoder, IntCounterVec, TextEncoder}; +use tower_http::services::ServeFile; use std::sync::Arc; use tracing::{instrument, log::*}; @@ -39,9 +40,10 @@ pub fn routes(state: &Arc) -> Router> { .route("/metrics", get(metrics)) .route_service( "/posts/:slug/*path", - tower_http::services::ServeDir::new("./"), + tower_http::services::ServeDir::new("./").fallback(ServeFile::new("./404.html")), ) - .route_service("/static/*path", tower_http::services::ServeDir::new("./")) + .route_service("/static/*path", tower_http::services::ServeDir::new("./").fallback(ServeFile::new("./404.html"))) + .fallback_service(ServeFile::new("./404.html")) } #[instrument(skip(state))] @@ -126,6 +128,7 @@ mod tests { // aliases overlap with themselves or other routes let posts = crate::post::load_all().await.unwrap(); let state = Arc::new(AppState { + base_url: "http://localhost:8180".into(), tera: tera::Tera::new("templates/**/*").unwrap(), tags: crate::tag::get_tags(posts.values()), posts, diff --git a/src/handlers/posts.rs b/src/handlers/posts.rs index da6252f..7b88333 100644 --- a/src/handlers/posts.rs +++ b/src/handlers/posts.rs @@ -2,10 +2,11 @@ use std::sync::Arc; use axum::{ extract::{Path, State}, - response::{Html, Redirect}, + response::{Html, Redirect, IntoResponse}, routing::get, Router, }; +use hyper::{header::CONTENT_TYPE, StatusCode}; use serde_derive::Serialize; use tracing::{instrument, log::*}; @@ -17,6 +18,7 @@ use crate::{ pub fn router() -> Router> { Router::new() .route("/posts", get(|| async { Redirect::permanent("/") })) + .route("/atom.xml", get(feed)) .route("/posts/", get(index)) .route("/posts/:slug", get(redirect)) .route("/posts/:slug/", get(view)) @@ -80,6 +82,23 @@ pub async fn view( Ok(Html(res)) } +pub async fn feed( + State(state): State>, +) -> Result { + + let mut posts: Vec<&Post> = state + .posts + .values() + .filter(|p| p.is_published()) + .collect(); + + posts.sort_by_key(|p| &p.date); + posts.reverse(); + posts.truncate(10); + + Ok((StatusCode::OK, [(CONTENT_TYPE, "application/atom+xml")], crate::feed::render_atom_feed(&posts, &state.tera)?)) +} + #[instrument(skip(state))] pub async fn redirect( Path(slug): Path, diff --git a/src/handlers/tags.rs b/src/handlers/tags.rs index 04c6df8..49cceec 100644 --- a/src/handlers/tags.rs +++ b/src/handlers/tags.rs @@ -2,10 +2,11 @@ use std::sync::Arc; use axum::{ extract::{Path, State}, - response::{Html, Redirect}, + response::{Html, Redirect, IntoResponse}, routing::get, Router, }; +use hyper::{header::CONTENT_TYPE, StatusCode}; use serde_derive::Serialize; use tracing::instrument; @@ -17,6 +18,7 @@ pub fn router() -> Router> { .route("/tags/", get(index)) .route("/tags/:tag", get(redirect)) .route("/tags/:tag/", get(view)) + .route("/tags/:tag/atom.xml", get(feed)) } #[derive(Serialize, Debug)] @@ -65,6 +67,25 @@ pub async fn view( Ok(Html(res)) } +pub async fn feed( + Path(slug): Path, + State(state): State>, +) -> Result { + let tag = state.tags.get(&slug).ok_or(WebsiteError::NotFound)?; + + let mut posts: Vec<&Post> = state + .posts + .values() + .filter(|p| p.is_published() && p.tags.contains(&slug)) + .collect(); + + posts.sort_by_key(|p| &p.date); + posts.reverse(); + posts.truncate(10); + + Ok((StatusCode::OK, [(CONTENT_TYPE, "application/atom+xml")], crate::feed::render_atom_tag_feed(tag, &posts, &state.tera)?)) +} + #[instrument(skip(state))] pub async fn redirect( Path(slug): Path, diff --git a/src/main.rs b/src/main.rs index f5ef2ac..d41a4b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,8 +15,10 @@ use tracing_subscriber::{prelude::*, EnvFilter}; mod handlers; mod post; mod tag; +mod feed; pub struct AppState { + base_url: String, posts: HashMap, tags: HashMap, tera: Tera, @@ -29,10 +31,11 @@ async fn main() -> Result<()> { info!("Starting server..."); + let base_url = option_env!("SITE_BASE_URL").unwrap_or("http://localhost:8180").to_string(); let tera = Tera::new("templates/**/*")?; let posts = post::load_all().await?; let tags = tag::get_tags(posts.values()); - let state = Arc::new(AppState { tera, posts, tags }); + let state = Arc::new(AppState { base_url, tera, posts, tags }); let app = handlers::routes(&state) .layer(CorsLayer::permissive()) @@ -56,8 +59,7 @@ async fn main() -> Result<()> { fn init_tracing() { let filter = EnvFilter::builder() .with_default_directive("into".parse().unwrap()) - .from_env_lossy() - .add_directive("otel=debug".parse().unwrap()); + .from_env_lossy(); tracing_subscriber::registry() .with(filter) diff --git a/src/post.rs b/src/post.rs index 8ff15ff..ce6d4d6 100644 --- a/src/post.rs +++ b/src/post.rs @@ -17,6 +17,7 @@ use crate::WebsiteError; pub struct TomlFrontMatter { pub title: String, pub date: Option, + pub updated: Option, pub draft: Option, pub aliases: Option>, pub tags: Option>, @@ -26,6 +27,7 @@ pub struct TomlFrontMatter { pub struct Post { pub title: String, pub date: Option>, + pub updated: Option>, pub aliases: Vec, pub tags: Vec, pub content: String, @@ -43,6 +45,9 @@ impl Post { date: fm .date .map(|d| DateTime::parse_from_rfc3339(&d.to_string()).expect("bad toml datetime")), + updated: fm + .updated + .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(), } diff --git a/templates/atom.xml b/templates/atom.xml new file mode 100644 index 0000000..bec96eb --- /dev/null +++ b/templates/atom.xml @@ -0,0 +1,29 @@ + + + tollyx.net + {%- if tag %} - #{{ tag.slug }}{% endif -%} + + tollyx's corner of the web + + {% if tag -%} + + {%- else -%} + + {%- endif %} + tollyx-website + {{ last_updated | date(format="%+") }} + {{ feed_url | safe }} + {%- for post in posts %} + + {{ post.title }} + {{ post.date | date(format="%+") }} + {{ post.updated | default(value=post.date) | date(format="%+") }} + + tollyx + + + {{ post.slug | safe }} + {{ post.content }} + + {%- endfor %} + \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index a7b6766..f0becdd 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,9 +1,9 @@ -{% include "partials/head.html" -%} +{% include "partials/head.html" %}
- {% include "partials/header.html" -%} +{% include "partials/header.html" -%}

diff --git a/templates/index.html b/templates/index.html index d461637..b26941c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -13,7 +13,7 @@
  • ✅ tests
  • ✅ page aliases (redirects, for back-compat with old routes)
  • ⬜ sass compilation (using rsass? grass?)
  • -
  • ⬜ rss/atom/jsonfeed
  • +
  • ✅ rss/atom/jsonfeed (atom is good enough for now)
  • ✅ proper error handling (i guess??)
  • ⬜ fancy styling
  • ⬜ other pages???
  • diff --git a/templates/partials/head.html b/templates/partials/head.html index e81ee41..8891201 100644 --- a/templates/partials/head.html +++ b/templates/partials/head.html @@ -2,11 +2,29 @@ + + {% if tag -%} + + {%- endif %} - + + + + {% if page -%} + + {%- elif tag -%} + + {%- else -%} + + {%- endif %} + + + + + {% if page.title -%} {{ page.title }} | tollyx.net - {% else -%} + {%- else -%} tollyx.net - {% endif -%} + {%- endif %} \ No newline at end of file diff --git a/templates/post.html b/templates/post.html index eaa5430..9c2a23e 100644 --- a/templates/post.html +++ b/templates/post.html @@ -6,15 +6,19 @@

    {{ page.title }}

    {% endif -%} {% if page.date -%} - Posted on - {% endif -%} + Posted on + {%- if page.updated -%} + , Updated + {%- endif -%} + + {%- endif %} {{ page.content | safe -}} {% if page.tags -%} - -
      - {% for tag in page.tags %}
    • #{{ tag }}
    • {% endfor %} -
    -
    - {% endif -%} + +
      + {% for tag in page.tags %}
    • #{{ tag }}
    • {% endfor %} +
    +
    + {%- endif %} {% endblock main -%}