1
0
Fork 0

convert relative to absolute links everywhere

This commit is contained in:
Adrian Hedqvist 2023-07-29 15:32:05 +02:00
parent 7e2ebc4efb
commit cbfc505649
17 changed files with 143 additions and 80 deletions

1
Cargo.lock generated
View file

@ -2307,7 +2307,6 @@ dependencies = [
"chrono",
"color-eyre",
"glob",
"hyper",
"lazy_static",
"opentelemetry",
"prometheus",

View file

@ -6,12 +6,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = { version = "0.6.12", features = ["http2"] }
axum = { version = "0.6.12", features = ["http2", "original-uri"] }
cached = "0.44.0"
chrono = { version = "0.4.24", features = ["serde"] }
color-eyre = "0.6.1"
glob = "0.3.0"
hyper = { version = "0.14.19", features = ["full"] }
# hyper = { version = "0.14.19", features = ["full"] }
lazy_static = "1.4.0"
opentelemetry = { version = "0.19.0", features = ["metrics"] }
prometheus = { version = "0.13.3", features = ["process"] }

View file

@ -24,8 +24,8 @@ pub fn render_atom_feed(state: &AppState) -> Result<String> {
let updated = posts.iter().map(|p| p.updated.or(p.date)).max().flatten();
let feed = FeedContext {
feed_url: &format!("{}/atom.xml", state.base_url),
base_url: &state.base_url,
feed_url: &format!("{}atom.xml", state.base_url),
base_url: &state.base_url.to_string(),
last_updated: &updated.map_or_else(String::default, |d| d.to_rfc3339()),
tag: None,
posts: &posts,
@ -51,8 +51,8 @@ pub fn render_atom_tag_feed(tag: &Tag, state: &AppState) -> 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!("{}/tags/{}/atom.xml", state.base_url, slug),
base_url: &state.base_url,
feed_url: &format!("{}tags/{}/atom.xml", state.base_url, slug),
base_url: &state.base_url.to_string(),
last_updated: &updated.map_or_else(String::default, |d| d.to_rfc3339()),
tag: Some(tag),
posts: &posts,
@ -63,14 +63,14 @@ pub fn render_atom_tag_feed(tag: &Tag, state: &AppState) -> Result<String> {
Ok(state.tera.render("atom.xml", &ctx)?)
}
struct JsonFeed<'a> {
struct _JsonFeed<'a> {
version: &'a str,
title: &'a str,
home_page_url: &'a str,
feed_url: &'a str,
items: Vec<JsonFeedItem<'a>>,
items: Vec<_JsonFeedItem<'a>>,
}
struct JsonFeedItem<'a> {
struct _JsonFeedItem<'a> {
id: &'a str,
}

View file

@ -1,16 +1,13 @@
use axum::{
body,
extract::State,
http::{header, HeaderMap, Request, StatusCode},
middleware::Next,
response::{Html, IntoResponse, Response},
routing::get,
Router,
};
use chrono::{DateTime, FixedOffset};
use hyper::{
header::{self, CONTENT_TYPE},
HeaderMap, Request, StatusCode,
};
use lazy_static::lazy_static;
use prometheus::{opts, Encoder, IntCounterVec, TextEncoder};
use std::sync::Arc;
@ -59,7 +56,11 @@ pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> {
#[instrument(skip(state))]
pub async fn index(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> std::result::Result<Response, WebsiteError> {
if should_return_304(&headers, Some(state.startup_time.into())) {
return Ok(StatusCode::NOT_MODIFIED.into_response());
}
let ctx = tera::Context::new();
let res = state.tera.render("index.html", &ctx).map_err(|e| {
error!("Failed rendering index: {}", e);
@ -85,7 +86,7 @@ async fn metrics() -> impl IntoResponse {
Response::builder()
.status(200)
.header(CONTENT_TYPE, encoder.format_type())
.header(header::CONTENT_TYPE, encoder.format_type())
.body(body::boxed(body::Full::from(buffer)))
.unwrap()
}
@ -165,16 +166,18 @@ mod tests {
#[tokio::test]
async fn setup_routes() {
let mut state = AppState {
startup_time: chrono::offset::Utc::now(),
base_url: "http://localhost:8180".parse().unwrap(),
tera: tera::Tera::new("templates/**/*").unwrap(),
..Default::default()
};
// Load the actual posts, just to make this test fail if
// aliases overlap with themselves or other routes
let posts = crate::post::load_all().await.unwrap();
let state = Arc::new(AppState {
startup_time: chrono::offset::Utc::now(),
base_url: "http://localhost:8180".into(),
tera: tera::Tera::new("templates/**/*").unwrap(),
tags: crate::tag::get_tags(posts.values()),
posts,
});
let posts = crate::post::load_all(&state).await.unwrap();
state.tags = crate::tag::get_tags(posts.values());
state.posts = posts;
let state = Arc::new(state);
super::routes(&state).with_state(state).into_make_service();
}

View file

@ -2,14 +2,12 @@ use std::sync::Arc;
use axum::{
extract::{Path, State},
http::{header, HeaderMap, StatusCode},
response::{Html, IntoResponse, Redirect, Response},
routing::get,
Router,
};
use hyper::{
header::{self, CONTENT_TYPE},
HeaderMap, StatusCode,
};
use serde_derive::Serialize;
use tracing::{instrument, log::warn};
@ -74,7 +72,7 @@ pub async fn index(
let mut c = tera::Context::new();
c.insert("page", &ctx);
c.insert("posts", &posts);
c.insert("base_url", &state.base_url);
c.insert("base_url", &state.base_url.to_string());
let res = state.tera.render("posts_index.html", &c)?;
@ -152,7 +150,7 @@ pub async fn feed(
Ok((
StatusCode::OK,
[
(CONTENT_TYPE, "application/atom+xml"),
(header::CONTENT_TYPE, "application/atom+xml"),
(
header::LAST_MODIFIED,
&last_changed.map_or_else(

View file

@ -2,14 +2,12 @@ use std::sync::Arc;
use axum::{
extract::{Path, State},
http::{header, HeaderMap, StatusCode},
response::{Html, IntoResponse, Redirect, Response},
routing::get,
Router,
};
use hyper::{
header::{self, CONTENT_TYPE},
HeaderMap, StatusCode,
};
use serde_derive::Serialize;
use tracing::instrument;
@ -122,7 +120,7 @@ pub async fn feed(
Ok((
StatusCode::OK,
[
(CONTENT_TYPE, "application/atom+xml"),
(header::CONTENT_TYPE, "application/atom+xml"),
(
header::LAST_MODIFIED,
&last_changed.map_or_else(

33
src/helpers.rs Normal file
View file

@ -0,0 +1,33 @@
use axum::http::{uri, Uri};
pub fn uri_with_path(uri: &Uri, path: &str) -> Uri {
if path.starts_with('/') {
// 'path' is an root path, so let's just override the uri's path
return uri::Builder::new()
.scheme(uri.scheme_str().unwrap())
.authority(uri.authority().unwrap().as_str())
.path_and_query(path)
.build()
.unwrap();
}
// 'path' is a relative/local path, so let's combine it with the uri's path
let base_path = uri.path_and_query().map_or("/", |p| p.path());
if base_path.ends_with('/') {
return uri::Builder::new()
.scheme(uri.scheme_str().unwrap())
.authority(uri.authority().unwrap().as_str())
.path_and_query(format!("{base_path}{path}"))
.build()
.unwrap();
}
let (base, _) = base_path.rsplit_once('/').unwrap();
return uri::Builder::new()
.scheme(uri.scheme_str().unwrap())
.authority(uri.authority().unwrap().as_str())
.path_and_query(format!("{base}/{path}"))
.build()
.unwrap();
}

View file

@ -2,11 +2,15 @@
#![allow(clippy::unused_async)] // axum handlers needs async, even if no awaiting happens
use std::{collections::HashMap, fmt::Display, sync::Arc, time::Duration};
use axum::extract::MatchedPath;
use axum::{
body::Body,
extract::{MatchedPath, OriginalUri},
http::{uri::PathAndQuery, Request, Uri},
response::Response,
};
use chrono::DateTime;
use color_eyre::eyre::{Error, Result};
use hyper::{Body, Request, Response};
use post::Post;
use tag::Tag;
@ -18,6 +22,7 @@ use tracing_subscriber::{prelude::*, EnvFilter};
mod feed;
mod handlers;
mod helpers;
mod hilighting;
mod markdown;
mod post;
@ -26,7 +31,7 @@ mod tag;
#[derive(Default)]
pub struct AppState {
startup_time: DateTime<chrono::offset::Utc>,
base_url: String,
base_url: Uri,
posts: HashMap<String, Post>,
tags: HashMap<String, Tag>,
tera: Tera,
@ -39,19 +44,22 @@ async fn main() -> Result<()> {
info!("Starting server...");
let base_url = option_env!("SITE_BASE_URL")
let base_url: Uri = option_env!("SITE_BASE_URL")
.unwrap_or("http://localhost:8080")
.to_string();
.parse()
.unwrap();
let tera = Tera::new("templates/**/*")?;
let posts = post::load_all().await?;
let tags = tag::get_tags(posts.values());
let state = Arc::new(AppState {
let mut state = AppState {
startup_time: chrono::offset::Utc::now(),
base_url,
tera,
posts,
tags,
});
..Default::default()
};
let posts = post::load_all(&state).await?;
let tags = tag::get_tags(posts.values());
state.posts = posts;
state.tags = tags;
let state = Arc::new(state);
let app = handlers::routes(&state)
.layer(CorsLayer::permissive())
@ -84,18 +92,19 @@ fn init_tracing() {
}
fn make_span(request: &Request<Body>) -> Span {
let uri = request.uri();
let uri = if let Some(OriginalUri(uri)) = request.extensions().get::<OriginalUri>() {
uri
} else {
request.uri()
};
let route = request
.extensions()
.get::<MatchedPath>()
.map(axum::extract::MatchedPath::as_str)
.unwrap_or_default();
.map_or(uri.path(), axum::extract::MatchedPath::as_str);
let method = request.method().as_str();
let target = uri
.path_and_query()
.map(axum::http::uri::PathAndQuery::as_str)
.unwrap_or_default();
.map_or(uri.path(), PathAndQuery::as_str);
let name = format!("{method} {route}");
info_span!(
@ -108,7 +117,7 @@ fn make_span(request: &Request<Body>) -> Span {
)
}
fn on_response<B>(response: &Response<B>, _latency: Duration, span: &Span) {
fn on_response(response: &Response, _latency: Duration, span: &Span) {
span.record("http.status_code", response.status().as_str());
}

View file

@ -1,10 +1,11 @@
use crate::helpers;
use crate::hilighting;
use axum::http::Uri;
use pulldown_cmark::Event;
use pulldown_cmark::Tag;
use pulldown_cmark::{Options, Parser};
use crate::hilighting;
pub fn render_markdown_to_html(markdown: &str) -> String {
pub fn render_markdown_to_html(base_uri: Option<&Uri>, markdown: &str) -> String {
let options = Options::all();
let mut content_html = String::new();
let parser = Parser::new_ext(markdown, options);
@ -22,6 +23,24 @@ pub fn render_markdown_to_html(markdown: &str) -> String {
events.push(Event::Text(text));
}
}
Event::Start(Tag::Link(t, mut link, title)) => {
if let Some(uri) = base_uri {
if !link.contains("://") && !link.contains('@') {
// convert relative URIs to absolute URIs
link = helpers::uri_with_path(uri, &link).to_string().into();
}
}
events.push(Event::Start(Tag::Link(t, link, title)));
}
Event::Start(Tag::Image(t, mut link, title)) => {
if let Some(uri) = base_uri {
if !link.contains("://") && !link.contains('@') {
// convert relative URIs to absolute URIs
link = helpers::uri_with_path(uri, &link).to_string().into();
}
}
events.push(Event::Start(Tag::Image(t, link, title)));
}
Event::Start(Tag::CodeBlock(kind)) => {
code_block = true;
if let pulldown_cmark::CodeBlockKind::Fenced(lang) = kind {

View file

@ -14,7 +14,7 @@ use tracing::{
log::{debug, warn},
};
use crate::{markdown, AppState, WebsiteError};
use crate::{helpers, markdown, AppState, WebsiteError};
#[derive(Deserialize, Debug, Default)]
pub struct TomlFrontMatter {
@ -69,8 +69,8 @@ impl Post {
}
}
#[instrument]
pub async fn load_all() -> color_eyre::eyre::Result<HashMap<String, Post>> {
#[instrument(skip(state))]
pub async fn load_all(state: &AppState) -> color_eyre::eyre::Result<HashMap<String, Post>> {
let mut res = HashMap::<String, Post>::new();
for path in glob("posts/**/*.md")? {
let path = path.unwrap();
@ -87,15 +87,15 @@ pub async fn load_all() -> color_eyre::eyre::Result<HashMap<String, Post>> {
.trim_end_matches('\\')
.trim_end_matches('/');
let post = load_post(slug).await?;
let post = load_post(state, slug).await?;
res.insert(slug.to_string(), post);
}
Ok(res)
}
#[instrument]
pub async fn load_post(slug: &str) -> color_eyre::eyre::Result<Post> {
#[instrument(skip(state))]
pub async fn load_post(state: &AppState, slug: &str) -> color_eyre::eyre::Result<Post> {
debug!("loading post: {slug}");
let file_path = Path::new("posts").join(slug);
@ -109,7 +109,9 @@ pub async fn load_post(slug: &str) -> color_eyre::eyre::Result<Post> {
let (tomlfm, content) = parse_frontmatter(content)?;
let tomlfm = tomlfm.expect("Missing frontmatter");
let content = content.map(|c| markdown::render_markdown_to_html(&c));
let base_uri = helpers::uri_with_path(&state.base_url, &format!("/posts/{slug}/"));
let content = content.map(|c| markdown::render_markdown_to_html(Some(&base_uri), &c));
Ok(Post::new(
slug.to_string(),
@ -143,7 +145,7 @@ fn parse_frontmatter(
pub async fn render_post(state: &AppState, post: &Post) -> Result<String, WebsiteError> {
let mut ctx = tera::Context::new();
ctx.insert("page", &post);
ctx.insert("base_url", &state.base_url);
ctx.insert("base_url", &state.base_url.to_string());
state
.tera
@ -159,12 +161,12 @@ mod tests {
#[tokio::test]
async fn render_all_posts() {
let state = AppState {
base_url: "localhost:8180".into(),
posts: super::load_all().await.unwrap(),
let mut state = AppState {
base_url: "localhost:8180".parse().unwrap(),
tera: Tera::new("templates/**/*").unwrap(),
..Default::default()
};
state.posts = super::load_all(&state).await.unwrap();
for post in state.posts.values() {
super::render_post(&state, post).await.unwrap();
}

View file

@ -6,7 +6,7 @@
<subtitle>tollyx's corner of the web</subtitle>
<link href="{{ feed_url | safe }}" rel="self" type="application/atom+xml"/>
{% if tag -%}
<link href="{{ base_url | safe }}/tags/{{ tag.slug }}/"/>
<link href="{{ base_url | safe }}tags/{{ tag.slug }}/"/>
{%- else -%}
<link href="{{ base_url | safe }}"/>
{%- endif %}
@ -21,9 +21,11 @@
<author>
<name>tollyx</name>
</author>
<link rel="alternate" href="{{ base_url | safe }}{{ post.absolute_path | safe }}" type="text/html"/>
<id>{{ post.slug | safe }}</id>
<content type="html">{{ post.content }}</content>
<link rel="alternate" href="{{ base_url | trim_end_matches(pat='/') | safe }}{{ post.absolute_path | safe }}" type="text/html"/>
<id>{{ base_url | trim_end_matches(pat='/') | safe }}{{ post.absolute_path | safe }}</id>
<content type="html">
{{ post.content }}
</content>
</entry>
{%- endfor %}
</feed>

View file

@ -2,12 +2,12 @@
<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="/atom.xml" title="tollyx.net">
<link rel="alternate" type="application/rss+xml" href="{{base_url | safe}}atom.xml" title="tollyx.net">
{% if tag_slug -%}
<link rel="alternate" type="application/rss+xml" href="/tags/{{ tag_slug }}/atom.xml" title="tollyx.net: Posts tagged #{{ tag_slug }}">
<link rel="alternate" type="application/rss+xml" href="{{base_url | safe}}tags/{{ tag_slug }}/atom.xml" title="tollyx.net: Posts tagged #{{ tag_slug }}">
{%- endif %}
<link rel="stylesheet" href="/static/site.css">
<link rel="icon" type="image/png" href="/static/avatar.png" />
<link rel="stylesheet" href="{{base_url | safe}}static/site.css">
<link rel="icon" type="image/png" href="{{base_url | safe}}static/avatar.png" />
<meta property="og:type" content="website">
<meta property="og:site_name" content="tollyx.net" />
{% if page -%}
@ -17,7 +17,7 @@
{%- else -%}
<meta property="og:title" content="tollyx.net" />
{%- endif %}
<meta property="og:image" content="/avatar.png" />
<meta property="og:image" content="{{base_url | safe}}avatar.png" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@tollyx" />
<meta name="twitter:creator" content="@tollyx" />

View file

@ -1,3 +1,3 @@
<nav>
<img src="/static/avatar.png" class="avatar"/> <a href="/">tollyx</a> - <a href="/posts/">posts</a>
<img src="{{base_url | safe}}static/avatar.png" class="avatar"/> <a href="{{base_url | safe}}">tollyx</a> - <a href="{{base_url | safe}}posts/">posts</a>
</nav>

View file

@ -16,7 +16,7 @@
{% if page.tags -%}
<small>
<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="{{base_url | safe}}tags/{{tag}}/">#{{ tag }}</a></li>{% endfor %}
</ul>
</small>
{%- endif %}

View file

@ -4,7 +4,7 @@
<p>I occasionally write some stuff, it's quite rare but it does happen believe it or not.</p>
<ul>
{% for post in posts -%}
<li><a href="{{post.absolute_path | safe}}">{% if post.date -%}
<li><a href="{{base_url | trim_end_matches(pat='/') | safe}}{{post.absolute_path | safe}}">{% if post.date -%}
<time datetime="{{ post.date }}">{{ post.date | date(format="%Y-%m-%d") }}</time> - {{ post.title -}}
{% else -%}
{{ post.title -}}

View file

@ -7,7 +7,7 @@
{% endif -%}
<ul>
{% for post in posts -%}
<li><a href="{{post.absolute_path | safe}}">{% if post.date -%}
<li><a href="{{base_url | trim_end_matches(pat='/') | safe}}{{post.absolute_path | safe}}">{% if post.date -%}
<time datetime="{{ post.date }}">{{ post.date | date(format="%Y-%m-%d") }}</time> - {{ post.title -}}
{% else -%}
{{ post.title -}}

View file

@ -3,7 +3,7 @@
<h1>Tags</h1>
<ul>
{% for tag in tags -%}
<li><a href="{{tag.absolute_path | safe}}">#{{ tag.slug }}</a></li>
<li><a href="{{base_url | trim_end_matches(pat='/') | safe}}{{tag.absolute_path | safe}}">#{{ tag.slug }}</a></li>
{% endfor -%}
</ul>
{% endblock main -%}