1
0
Fork 0

add atom feeds

This commit is contained in:
Adrian Hedqvist 2023-04-03 23:33:25 +02:00
parent 20f4ae0658
commit bfaa06fe5e
13 changed files with 182 additions and 21 deletions

12
404.html Normal file
View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 Not Found</title>
</head>
<body>
<h1>404 Not Found</h1>
</body>
</html>

View file

@ -1,6 +1,7 @@
+++ +++
title="TOML metadata test" title="TOML metadata test"
date=2023-03-26T11:57:00+02:00 date=2023-03-26T11:57:00+02:00
updated=2023-04-03T22:07:57+02:00
+++ +++
hope it works yay hope it works yay

47
src/feed.rs Normal file
View file

@ -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<String> {
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<String> {
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)?)
}

View file

@ -9,6 +9,7 @@ use axum::{
use hyper::{header::CONTENT_TYPE, Request, StatusCode}; use hyper::{header::CONTENT_TYPE, Request, StatusCode};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use prometheus::{opts, Encoder, IntCounterVec, TextEncoder}; use prometheus::{opts, Encoder, IntCounterVec, TextEncoder};
use tower_http::services::ServeFile;
use std::sync::Arc; use std::sync::Arc;
use tracing::{instrument, log::*}; use tracing::{instrument, log::*};
@ -39,9 +40,10 @@ pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> {
.route("/metrics", get(metrics)) .route("/metrics", get(metrics))
.route_service( .route_service(
"/posts/:slug/*path", "/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))] #[instrument(skip(state))]
@ -126,6 +128,7 @@ mod tests {
// aliases overlap with themselves or other routes // aliases overlap with themselves or other routes
let posts = crate::post::load_all().await.unwrap(); let posts = crate::post::load_all().await.unwrap();
let state = Arc::new(AppState { let state = Arc::new(AppState {
base_url: "http://localhost:8180".into(),
tera: tera::Tera::new("templates/**/*").unwrap(), tera: tera::Tera::new("templates/**/*").unwrap(),
tags: crate::tag::get_tags(posts.values()), tags: crate::tag::get_tags(posts.values()),
posts, posts,

View file

@ -2,10 +2,11 @@ use std::sync::Arc;
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{Html, Redirect}, response::{Html, Redirect, IntoResponse},
routing::get, routing::get,
Router, Router,
}; };
use hyper::{header::CONTENT_TYPE, StatusCode};
use serde_derive::Serialize; use serde_derive::Serialize;
use tracing::{instrument, log::*}; use tracing::{instrument, log::*};
@ -17,6 +18,7 @@ use crate::{
pub fn router() -> Router<Arc<AppState>> { pub fn router() -> Router<Arc<AppState>> {
Router::new() Router::new()
.route("/posts", get(|| async { Redirect::permanent("/") })) .route("/posts", get(|| async { Redirect::permanent("/") }))
.route("/atom.xml", get(feed))
.route("/posts/", get(index)) .route("/posts/", get(index))
.route("/posts/:slug", get(redirect)) .route("/posts/:slug", get(redirect))
.route("/posts/:slug/", get(view)) .route("/posts/:slug/", get(view))
@ -80,6 +82,23 @@ pub async fn view(
Ok(Html(res)) Ok(Html(res))
} }
pub async fn feed(
State(state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, WebsiteError> {
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))] #[instrument(skip(state))]
pub async fn redirect( pub async fn redirect(
Path(slug): Path<String>, Path(slug): Path<String>,

View file

@ -2,10 +2,11 @@ use std::sync::Arc;
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{Html, Redirect}, response::{Html, Redirect, IntoResponse},
routing::get, routing::get,
Router, Router,
}; };
use hyper::{header::CONTENT_TYPE, StatusCode};
use serde_derive::Serialize; use serde_derive::Serialize;
use tracing::instrument; use tracing::instrument;
@ -17,6 +18,7 @@ pub fn router() -> Router<Arc<AppState>> {
.route("/tags/", get(index)) .route("/tags/", get(index))
.route("/tags/:tag", get(redirect)) .route("/tags/:tag", get(redirect))
.route("/tags/:tag/", get(view)) .route("/tags/:tag/", get(view))
.route("/tags/:tag/atom.xml", get(feed))
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
@ -65,6 +67,25 @@ pub async fn view(
Ok(Html(res)) Ok(Html(res))
} }
pub async fn feed(
Path(slug): Path<String>,
State(state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, WebsiteError> {
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))] #[instrument(skip(state))]
pub async fn redirect( pub async fn redirect(
Path(slug): Path<String>, Path(slug): Path<String>,

View file

@ -15,8 +15,10 @@ use tracing_subscriber::{prelude::*, EnvFilter};
mod handlers; mod handlers;
mod post; mod post;
mod tag; mod tag;
mod feed;
pub struct AppState { pub struct AppState {
base_url: String,
posts: HashMap<String, Post>, posts: HashMap<String, Post>,
tags: HashMap<String, Tag>, tags: HashMap<String, Tag>,
tera: Tera, tera: Tera,
@ -29,10 +31,11 @@ async fn main() -> Result<()> {
info!("Starting server..."); info!("Starting server...");
let base_url = option_env!("SITE_BASE_URL").unwrap_or("http://localhost:8180").to_string();
let tera = Tera::new("templates/**/*")?; let tera = Tera::new("templates/**/*")?;
let posts = post::load_all().await?; let posts = post::load_all().await?;
let tags = tag::get_tags(posts.values()); 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) let app = handlers::routes(&state)
.layer(CorsLayer::permissive()) .layer(CorsLayer::permissive())
@ -56,8 +59,7 @@ async fn main() -> Result<()> {
fn init_tracing() { fn init_tracing() {
let filter = EnvFilter::builder() let filter = EnvFilter::builder()
.with_default_directive("into".parse().unwrap()) .with_default_directive("into".parse().unwrap())
.from_env_lossy() .from_env_lossy();
.add_directive("otel=debug".parse().unwrap());
tracing_subscriber::registry() tracing_subscriber::registry()
.with(filter) .with(filter)

View file

@ -17,6 +17,7 @@ use crate::WebsiteError;
pub struct TomlFrontMatter { pub struct TomlFrontMatter {
pub title: String, pub title: String,
pub date: Option<toml::value::Datetime>, pub date: Option<toml::value::Datetime>,
pub updated: Option<toml::value::Datetime>,
pub draft: Option<bool>, pub draft: Option<bool>,
pub aliases: Option<Vec<String>>, pub aliases: Option<Vec<String>>,
pub tags: Option<Vec<String>>, pub tags: Option<Vec<String>>,
@ -26,6 +27,7 @@ pub struct TomlFrontMatter {
pub struct Post { pub struct Post {
pub title: String, pub title: String,
pub date: Option<DateTime<FixedOffset>>, pub date: Option<DateTime<FixedOffset>>,
pub updated: Option<DateTime<FixedOffset>>,
pub aliases: Vec<String>, pub aliases: Vec<String>,
pub tags: Vec<String>, pub tags: Vec<String>,
pub content: String, pub content: String,
@ -43,6 +45,9 @@ impl Post {
date: fm date: fm
.date .date
.map(|d| DateTime::parse_from_rfc3339(&d.to_string()).expect("bad toml datetime")), .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(), aliases: fm.aliases.unwrap_or_default(),
tags: fm.tags.unwrap_or_default(), tags: fm.tags.unwrap_or_default(),
} }

29
templates/atom.xml Normal file
View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
<title>tollyx.net
{%- if tag %} - #{{ tag.slug }}{% endif -%}
</title>
<subtitle>tollyx's corner of the web</subtitle>
<link href="{{ feed_url | safe }}" rel="self" type="application/atom+xml"/>
{% if tag -%}
<link href="https://tollyx.net/tags/{{ tag.slug }}/"/>
{%- else -%}
<link href="https://tollyx.net"/>
{%- endif %}
<generator uri="https://tollyx.net">tollyx-website</generator>
<updated>{{ last_updated | date(format="%+") }}</updated>
<id>{{ feed_url | safe }}</id>
{%- for post in posts %}
<entry xml:lang="en">
<title>{{ post.title }}</title>
<published>{{ post.date | date(format="%+") }}</published>
<updated>{{ post.updated | default(value=post.date) | date(format="%+") }}</updated>
<author>
<name>tollyx</name>
</author>
<link rel="alternate" href="https://tollyx.net{{ post.absolute_path | safe }}" type="text/html"/>
<id>{{ post.slug | safe }}</id>
<content type="html">{{ post.content }}</content>
</entry>
{%- endfor %}
</feed>

View file

@ -1,9 +1,9 @@
<!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>

View file

@ -13,7 +13,7 @@
<li>✅ tests</li> <li>✅ tests</li>
<li>✅ page aliases (redirects, for back-compat with old routes)</li> <li>✅ page aliases (redirects, for back-compat with old routes)</li>
<li>⬜ sass compilation (using rsass? grass?)</li> <li>⬜ sass compilation (using rsass? grass?)</li>
<li>⬜ rss/atom/jsonfeed</li> <li>✅ rss/atom/jsonfeed (atom is good enough for now)</li>
<li>✅ proper error handling (i guess??)</li> <li>✅ proper error handling (i guess??)</li>
<li>⬜ fancy styling</li> <li>⬜ fancy styling</li>
<li>⬜ other pages???</li> <li>⬜ other pages???</li>

View file

@ -2,11 +2,29 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="alternate" type="application/rss+xml" href="https://tollyx.net/atom.xml" title="tollyx.net">
{% if tag -%}
<link rel="alternate" type="application/rss+xml" href="https://tollyx.net/tags/{{ tag.slug }}/atom.xml" title="tollyx.net - #{{ tag.slug }}">
{%- endif %}
<link rel="stylesheet" href="/static/site.css"> <link rel="stylesheet" href="/static/site.css">
<link rel="icon" href="/static/avatar.png" /> <link rel="icon" type="image/png" href="/static/avatar.png" />
<meta property="og:type" content="website">
<meta property="og:site_name" content="tollyx.net" />
{% if page -%}
<meta property="og:title" content="{{ page.title }} - tollyx.net" />
{%- elif tag -%}
<meta property="og:title" content="#{{ tag.slug }} - tollyx.net" />
{%- else -%}
<meta property="og:title" content="tollyx.net" />
{%- endif %}
<meta property="og:image" content="https://tollyx.net/avatar.png" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@tollyx" />
<meta name="twitter:creator" content="@tollyx" />
<meta name="twitter:dnt" content="on">
{% if page.title -%} {% if page.title -%}
<title>{{ page.title }} | tollyx.net</title> <title>{{ page.title }} | tollyx.net</title>
{% else -%} {%- else -%}
<title>tollyx.net</title> <title>tollyx.net</title>
{% endif -%} {%- endif %}
</head> </head>

View file

@ -6,8 +6,12 @@
<h1>{{ page.title }}</h1> <h1>{{ page.title }}</h1>
{% endif -%} {% endif -%}
{% if page.date -%} {% if page.date -%}
<small>Posted on <time datetime="{{ page.date }}">{{ page.date | date(format="%Y-%m-%d %H:%M") }}</time></small> <small>Posted on <time datetime="{{ page.date }}">{{ page.date | date(format="%Y-%m-%d %H:%M") }}</time>
{% endif -%} {%- if page.updated -%}
, Updated <time datetime="{{ page.updated }}">{{ page.updated | date(format="%Y-%m-%d %H:%M") }}</time>
{%- endif -%}
</small>
{%- endif %}
{{ page.content | safe -}} {{ page.content | safe -}}
{% if page.tags -%} {% if page.tags -%}
<small> <small>
@ -15,6 +19,6 @@
{% for tag in page.tags %}<li><a href="/tags/{{tag}}/">#{{ tag }}</a></li>{% endfor %} {% for tag in page.tags %}<li><a href="/tags/{{tag}}/">#{{ tag }}</a></li>{% endfor %}
</ul> </ul>
</small> </small>
{% endif -%} {%- endif %}
</article> </article>
{% endblock main -%} {% endblock main -%}