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", "chrono",
"color-eyre", "color-eyre",
"glob", "glob",
"hyper",
"lazy_static", "lazy_static",
"opentelemetry", "opentelemetry",
"prometheus", "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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
axum = { version = "0.6.12", features = ["http2"] } axum = { version = "0.6.12", features = ["http2", "original-uri"] }
cached = "0.44.0" cached = "0.44.0"
chrono = { version = "0.4.24", features = ["serde"] } chrono = { version = "0.4.24", features = ["serde"] }
color-eyre = "0.6.1" color-eyre = "0.6.1"
glob = "0.3.0" glob = "0.3.0"
hyper = { version = "0.14.19", features = ["full"] } # hyper = { version = "0.14.19", features = ["full"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
opentelemetry = { version = "0.19.0", features = ["metrics"] } opentelemetry = { version = "0.19.0", features = ["metrics"] }
prometheus = { version = "0.13.3", features = ["process"] } 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 updated = posts.iter().map(|p| p.updated.or(p.date)).max().flatten();
let feed = FeedContext { let feed = FeedContext {
feed_url: &format!("{}/atom.xml", state.base_url), feed_url: &format!("{}atom.xml", state.base_url),
base_url: &state.base_url, base_url: &state.base_url.to_string(),
last_updated: &updated.map_or_else(String::default, |d| d.to_rfc3339()), last_updated: &updated.map_or_else(String::default, |d| d.to_rfc3339()),
tag: None, tag: None,
posts: &posts, 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 updated = posts.iter().map(|p| p.updated.or(p.date)).max().flatten();
let slug = &tag.slug; let slug = &tag.slug;
let feed = FeedContext { let feed = FeedContext {
feed_url: &format!("{}/tags/{}/atom.xml", state.base_url, slug), feed_url: &format!("{}tags/{}/atom.xml", state.base_url, slug),
base_url: &state.base_url, base_url: &state.base_url.to_string(),
last_updated: &updated.map_or_else(String::default, |d| d.to_rfc3339()), last_updated: &updated.map_or_else(String::default, |d| d.to_rfc3339()),
tag: Some(tag), tag: Some(tag),
posts: &posts, 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)?) Ok(state.tera.render("atom.xml", &ctx)?)
} }
struct JsonFeed<'a> { struct _JsonFeed<'a> {
version: &'a str, version: &'a str,
title: &'a str, title: &'a str,
home_page_url: &'a str, home_page_url: &'a str,
feed_url: &'a str, feed_url: &'a str,
items: Vec<JsonFeedItem<'a>>, items: Vec<_JsonFeedItem<'a>>,
} }
struct JsonFeedItem<'a> { struct _JsonFeedItem<'a> {
id: &'a str, id: &'a str,
} }

View file

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

View file

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

View file

@ -2,14 +2,12 @@ use std::sync::Arc;
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
http::{header, HeaderMap, StatusCode},
response::{Html, IntoResponse, Redirect, Response}, response::{Html, IntoResponse, Redirect, Response},
routing::get, routing::get,
Router, Router,
}; };
use hyper::{
header::{self, CONTENT_TYPE},
HeaderMap, StatusCode,
};
use serde_derive::Serialize; use serde_derive::Serialize;
use tracing::instrument; use tracing::instrument;
@ -122,7 +120,7 @@ pub async fn feed(
Ok(( Ok((
StatusCode::OK, StatusCode::OK,
[ [
(CONTENT_TYPE, "application/atom+xml"), (header::CONTENT_TYPE, "application/atom+xml"),
( (
header::LAST_MODIFIED, header::LAST_MODIFIED,
&last_changed.map_or_else( &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 #![allow(clippy::unused_async)] // axum handlers needs async, even if no awaiting happens
use std::{collections::HashMap, fmt::Display, sync::Arc, time::Duration}; 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 chrono::DateTime;
use color_eyre::eyre::{Error, Result}; use color_eyre::eyre::{Error, Result};
use hyper::{Body, Request, Response};
use post::Post; use post::Post;
use tag::Tag; use tag::Tag;
@ -18,6 +22,7 @@ use tracing_subscriber::{prelude::*, EnvFilter};
mod feed; mod feed;
mod handlers; mod handlers;
mod helpers;
mod hilighting; mod hilighting;
mod markdown; mod markdown;
mod post; mod post;
@ -26,7 +31,7 @@ mod tag;
#[derive(Default)] #[derive(Default)]
pub struct AppState { pub struct AppState {
startup_time: DateTime<chrono::offset::Utc>, startup_time: DateTime<chrono::offset::Utc>,
base_url: String, base_url: Uri,
posts: HashMap<String, Post>, posts: HashMap<String, Post>,
tags: HashMap<String, Tag>, tags: HashMap<String, Tag>,
tera: Tera, tera: Tera,
@ -39,19 +44,22 @@ async fn main() -> Result<()> {
info!("Starting server..."); 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") .unwrap_or("http://localhost:8080")
.to_string(); .parse()
.unwrap();
let tera = Tera::new("templates/**/*")?; let tera = Tera::new("templates/**/*")?;
let posts = post::load_all().await?; let mut state = AppState {
let tags = tag::get_tags(posts.values());
let state = Arc::new(AppState {
startup_time: chrono::offset::Utc::now(), startup_time: chrono::offset::Utc::now(),
base_url, base_url,
tera, tera,
posts, ..Default::default()
tags, };
}); 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) let app = handlers::routes(&state)
.layer(CorsLayer::permissive()) .layer(CorsLayer::permissive())
@ -84,18 +92,19 @@ fn init_tracing() {
} }
fn make_span(request: &Request<Body>) -> Span { 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 let route = request
.extensions() .extensions()
.get::<MatchedPath>() .get::<MatchedPath>()
.map(axum::extract::MatchedPath::as_str) .map_or(uri.path(), axum::extract::MatchedPath::as_str);
.unwrap_or_default();
let method = request.method().as_str(); let method = request.method().as_str();
let target = uri let target = uri
.path_and_query() .path_and_query()
.map(axum::http::uri::PathAndQuery::as_str) .map_or(uri.path(), PathAndQuery::as_str);
.unwrap_or_default();
let name = format!("{method} {route}"); let name = format!("{method} {route}");
info_span!( 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()); 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::Event;
use pulldown_cmark::Tag; use pulldown_cmark::Tag;
use pulldown_cmark::{Options, Parser}; use pulldown_cmark::{Options, Parser};
use crate::hilighting; pub fn render_markdown_to_html(base_uri: Option<&Uri>, markdown: &str) -> String {
pub fn render_markdown_to_html(markdown: &str) -> String {
let options = Options::all(); let options = Options::all();
let mut content_html = String::new(); let mut content_html = String::new();
let parser = Parser::new_ext(markdown, options); 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)); 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)) => { Event::Start(Tag::CodeBlock(kind)) => {
code_block = true; code_block = true;
if let pulldown_cmark::CodeBlockKind::Fenced(lang) = kind { if let pulldown_cmark::CodeBlockKind::Fenced(lang) = kind {

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
<nav> <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> </nav>

View file

@ -16,7 +16,7 @@
{% 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="{{base_url | safe}}tags/{{tag}}/">#{{ tag }}</a></li>{% endfor %}
</ul> </ul>
</small> </small>
{%- endif %} {%- 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> <p>I occasionally write some stuff, it's quite rare but it does happen believe it or not.</p>
<ul> <ul>
{% for post in posts -%} {% 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 -}} <time datetime="{{ post.date }}">{{ post.date | date(format="%Y-%m-%d") }}</time> - {{ post.title -}}
{% else -%} {% else -%}
{{ post.title -}} {{ post.title -}}

View file

@ -7,7 +7,7 @@
{% endif -%} {% endif -%}
<ul> <ul>
{% for post in posts -%} {% 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 -}} <time datetime="{{ post.date }}">{{ post.date | date(format="%Y-%m-%d") }}</time> - {{ post.title -}}
{% else -%} {% else -%}
{{ post.title -}} {{ post.title -}}

View file

@ -3,7 +3,7 @@
<h1>Tags</h1> <h1>Tags</h1>
<ul> <ul>
{% for tag in tags -%} {% 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 -%} {% endfor -%}
</ul> </ul>
{% endblock main -%} {% endblock main -%}