1
0
Fork 0

Compare commits

...

2 commits

Author SHA1 Message Date
Adrian Hedqvist 4ed0341a69 cargo fmt 2023-04-03 23:33:43 +02:00
Adrian Hedqvist bfaa06fe5e add atom feeds 2023-04-03 23:33:25 +02:00
13 changed files with 191 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"
date=2023-03-26T11:57:00+02:00
updated=2023-04-03T22:07:57+02:00
+++
hope it works yay

45
src/feed.rs Normal file
View file

@ -0,0 +1,45 @@
use color_eyre::Result;
use serde_derive::Serialize;
use tera::Tera;
use tracing::instrument;
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

@ -10,6 +10,7 @@ use hyper::{header::CONTENT_TYPE, Request, StatusCode};
use lazy_static::lazy_static;
use prometheus::{opts, Encoder, IntCounterVec, TextEncoder};
use std::sync::Arc;
use tower_http::services::ServeFile;
use tracing::{instrument, log::*};
use crate::{AppState, WebsiteError};
@ -39,9 +40,13 @@ 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 +131,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,

View file

@ -2,10 +2,11 @@ use std::sync::Arc;
use axum::{
extract::{Path, State},
response::{Html, Redirect},
response::{Html, IntoResponse, Redirect},
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,20 @@ 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>,

View file

@ -2,10 +2,11 @@ use std::sync::Arc;
use axum::{
extract::{Path, State},
response::{Html, Redirect},
response::{Html, IntoResponse, Redirect},
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,29 @@ 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>,

View file

@ -12,11 +12,13 @@ use tower_http::{compression::CompressionLayer, cors::CorsLayer};
use tracing::{info_span, log::*, Span};
use tracing_subscriber::{prelude::*, EnvFilter};
mod feed;
mod handlers;
mod post;
mod tag;
pub struct AppState {
base_url: String,
posts: HashMap<String, Post>,
tags: HashMap<String, Tag>,
tera: Tera,
@ -29,10 +31,18 @@ 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 +66,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)

View file

@ -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
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>
<html lang="en">
{% include "partials/head.html" -%}
{% include "partials/head.html" %}
<body>
<header>
{% include "partials/header.html" -%}
{% include "partials/header.html" -%}
</header>
<hr>
<main>

View file

@ -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>

View file

@ -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>

View file

@ -6,15 +6,19 @@
<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>
<ul class="tags">
{% for tag in page.tags %}<li><a href="/tags/{{tag}}/">#{{ tag }}</a></li>{% endfor %}
</ul>
</small>
{% endif -%}
<small>
<ul class="tags">
{% for tag in page.tags %}<li><a href="/tags/{{tag}}/">#{{ tag }}</a></li>{% endfor %}
</ul>
</small>
{%- endif %}
</article>
{% endblock main -%}