add atom feeds
This commit is contained in:
parent
20f4ae0658
commit
bfaa06fe5e
13 changed files with 182 additions and 21 deletions
12
404.html
Normal file
12
404.html
Normal 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>
|
|
@ -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
47
src/feed.rs
Normal 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)?)
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
29
templates/atom.xml
Normal 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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -6,15 +6,19 @@
|
||||||
<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>
|
||||||
<ul class="tags">
|
<ul class="tags">
|
||||||
{% 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 -%}
|
||||||
|
|
Loading…
Reference in a new issue