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"
|
||||
date=2023-03-26T11:57:00+02:00
|
||||
updated=2023-04-03T22:07:57+02:00
|
||||
+++
|
||||
|
||||
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 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<AppState>) -> Router<Arc<AppState>> {
|
|||
.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,
|
||||
|
|
|
@ -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<Arc<AppState>> {
|
||||
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<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))]
|
||||
pub async fn redirect(
|
||||
Path(slug): Path<String>,
|
||||
|
|
|
@ -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<Arc<AppState>> {
|
|||
.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<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))]
|
||||
pub async fn redirect(
|
||||
Path(slug): Path<String>,
|
||||
|
|
|
@ -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<String, Post>,
|
||||
tags: HashMap<String, Tag>,
|
||||
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)
|
||||
|
|
|
@ -17,6 +17,7 @@ use crate::WebsiteError;
|
|||
pub struct TomlFrontMatter {
|
||||
pub title: String,
|
||||
pub date: Option<toml::value::Datetime>,
|
||||
pub updated: Option<toml::value::Datetime>,
|
||||
pub draft: Option<bool>,
|
||||
pub aliases: Option<Vec<String>>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
|
@ -26,6 +27,7 @@ pub struct TomlFrontMatter {
|
|||
pub struct Post {
|
||||
pub title: String,
|
||||
pub date: Option<DateTime<FixedOffset>>,
|
||||
pub updated: Option<DateTime<FixedOffset>>,
|
||||
pub aliases: Vec<String>,
|
||||
pub tags: Vec<String>,
|
||||
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(),
|
||||
}
|
||||
|
|
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>
|
||||
<html lang="en">
|
||||
{% include "partials/head.html" -%}
|
||||
{% include "partials/head.html" %}
|
||||
<body>
|
||||
<header>
|
||||
{% include "partials/header.html" -%}
|
||||
{% include "partials/header.html" -%}
|
||||
</header>
|
||||
<hr>
|
||||
<main>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<li>✅ tests</li>
|
||||
<li>✅ page aliases (redirects, for back-compat with old routes)</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>⬜ fancy styling</li>
|
||||
<li>⬜ other pages???</li>
|
||||
|
|
|
@ -2,11 +2,29 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<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="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 -%}
|
||||
<title>{{ page.title }} | tollyx.net</title>
|
||||
{% else -%}
|
||||
{%- else -%}
|
||||
<title>tollyx.net</title>
|
||||
{% endif -%}
|
||||
{%- endif %}
|
||||
</head>
|
|
@ -6,8 +6,12 @@
|
|||
<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 -%}
|
||||
<small>Posted on <time datetime="{{ page.date }}">{{ page.date | date(format="%Y-%m-%d %H:%M") }}</time>
|
||||
{%- if page.updated -%}
|
||||
, Updated <time datetime="{{ page.updated }}">{{ page.updated | date(format="%Y-%m-%d %H:%M") }}</time>
|
||||
{%- endif -%}
|
||||
</small>
|
||||
{%- endif %}
|
||||
{{ page.content | safe -}}
|
||||
{% if page.tags -%}
|
||||
<small>
|
||||
|
@ -15,6 +19,6 @@
|
|||
{% for tag in page.tags %}<li><a href="/tags/{{tag}}/">#{{ tag }}</a></li>{% endfor %}
|
||||
</ul>
|
||||
</small>
|
||||
{% endif -%}
|
||||
{%- endif %}
|
||||
</article>
|
||||
{% endblock main -%}
|
||||
|
|
Loading…
Reference in a new issue