1
0
Fork 0

too much stuff

This commit is contained in:
Adrian Hedqvist 2023-07-29 20:22:13 +02:00
parent cbfc505649
commit e0a3f35caf
21 changed files with 262 additions and 86 deletions

View file

@ -1,12 +0,0 @@
<!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>

126
Cargo.lock generated
View file

@ -17,6 +17,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "1.0.2"
@ -153,6 +164,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.2"
@ -354,6 +371,25 @@ dependencies = [
"tracing-error",
]
[[package]]
name = "config"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7"
dependencies = [
"async-trait",
"json5",
"lazy_static",
"nom",
"pathdiff",
"ron",
"rust-ini",
"serde",
"serde_json",
"toml 0.5.11",
"yaml-rust",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
@ -471,6 +507,12 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "dlv-list"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]]
name = "equivalent"
version = "1.0.1"
@ -708,6 +750,9 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
@ -942,6 +987,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "json5"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
dependencies = [
"pest",
"pest_derive",
"serde",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -1034,6 +1090,12 @@ dependencies = [
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.1"
@ -1054,6 +1116,16 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -1166,6 +1238,16 @@ dependencies = [
"thiserror",
]
[[package]]
name = "ordered-multimap"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
dependencies = [
"dlv-list",
"hashbrown 0.12.3",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -1210,6 +1292,12 @@ dependencies = [
"regex",
]
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "percent-encoding"
version = "2.3.0"
@ -1343,7 +1431,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06"
dependencies = [
"base64",
"base64 0.21.2",
"indexmap 1.9.3",
"line-wrap",
"quick-xml",
@ -1515,6 +1603,27 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
[[package]]
name = "ron"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
dependencies = [
"base64 0.13.1",
"bitflags 1.3.2",
"serde",
]
[[package]]
name = "rust-ini"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df"
dependencies = [
"cfg-if",
"ordered-multimap",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
@ -1891,6 +2000,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml"
version = "0.7.6"
@ -1953,7 +2071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ae70283aba8d2a8b411c695c437fe25b8b5e44e23e780662002fc72fb47a82"
dependencies = [
"async-compression",
"base64",
"base64 0.21.2",
"bitflags 2.3.3",
"bytes",
"futures-core",
@ -2306,8 +2424,8 @@ dependencies = [
"cached",
"chrono",
"color-eyre",
"config",
"glob",
"lazy_static",
"opentelemetry",
"prometheus",
"pulldown-cmark",
@ -2318,7 +2436,7 @@ dependencies = [
"syntect",
"tera",
"tokio",
"toml",
"toml 0.7.6",
"tower",
"tower-http",
"tracing",

View file

@ -10,9 +10,8 @@ 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"
config = "0.13.3"
glob = "0.3.0"
# 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"] }
pulldown-cmark = "0.9.2"

3
config.toml Normal file
View file

@ -0,0 +1,3 @@
base_url = "http://localhost:8080/"
bind_address = "0.0.0.0:8080"
logging = "info"

7
posts/draft-test.md Normal file
View file

@ -0,0 +1,7 @@
+++
title="draft test"
draft=true
date=2023-07-29T17:25:20+02:00
+++
wow look it's a hidden post because it's marked as a draft

View file

@ -13,6 +13,8 @@ here have a squid kid miku to test relative paths:
modified post test, see if docker skips build using
its cache if only a post has changed.
testing "smart" punctuation --- I don't know if I want it. 'it should' do some fancy stuff.
code hilighting test:
```rs
@ -20,3 +22,11 @@ fn main() {
println!("Hello world!")
}
```
uh oh, here comes a screenshot from a different post!
![dungeon screenshot](../dungeon/screenshot.png)
and here it is again, except it should 404!
![missing dungeon screenshot](../dungeon/screenshot.jpeg)

View file

@ -16,7 +16,7 @@ struct FeedContext<'a> {
#[instrument(skip(state))]
pub fn render_atom_feed(state: &AppState) -> Result<String> {
let mut posts: Vec<_> = state.posts.values().filter(|p| p.is_published()).collect();
let mut posts: Vec<_> = state.posts.values().filter(|p| !p.draft && p.is_published()).collect();
posts.sort_by_key(|p| &p.date);
posts.reverse();
@ -41,7 +41,7 @@ pub fn render_atom_tag_feed(tag: &Tag, state: &AppState) -> Result<String> {
let mut posts: Vec<_> = state
.posts
.values()
.filter(|p| p.is_published() && p.tags.contains(&tag.slug))
.filter(|p| !p.draft && p.is_published() && p.tags.contains(&tag.slug))
.collect();
posts.sort_by_key(|p| &p.date);

View file

@ -7,14 +7,14 @@ use axum::{
routing::get,
Router,
};
use cached::once_cell::sync::Lazy;
use chrono::{DateTime, FixedOffset};
use lazy_static::lazy_static;
use prometheus::{opts, Encoder, IntCounterVec, TextEncoder};
use std::sync::Arc;
use tower_http::services::ServeFile;
use tower_http::services::ServeDir;
use tracing::{
instrument,
log::{error, info},
log::{error, info, debug},
};
use crate::{AppState, WebsiteError};
@ -22,16 +22,14 @@ use crate::{AppState, WebsiteError};
pub mod posts;
pub mod tags;
lazy_static! {
pub static ref HIT_COUNTER: IntCounterVec = prometheus::register_int_counter_vec!(
opts!(
"http_requests_total",
"Total amount of http requests received"
),
&["route", "method", "status"]
)
.unwrap();
}
pub static HIT_COUNTER: Lazy<IntCounterVec> = Lazy::new(|| prometheus::register_int_counter_vec!(
opts!(
"http_requests_total",
"Total amount of http requests received"
),
&["route", "method", "status"]
)
.unwrap());
pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> {
Router::new()
@ -44,13 +42,12 @@ pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> {
.route("/metrics", get(metrics))
.route_service(
"/posts/:slug/*path",
tower_http::services::ServeDir::new("./").fallback(ServeFile::new("./404.html")),
ServeDir::new("./"),
)
.route_service(
"/static/*path",
tower_http::services::ServeDir::new("./").fallback(ServeFile::new("./404.html")),
ServeDir::new("./"),
)
.fallback_service(ServeFile::new("./404.html"))
}
#[instrument(skip(state))]
@ -61,10 +58,11 @@ pub async fn index(
if should_return_304(&headers, Some(state.startup_time.into())) {
return Ok(StatusCode::NOT_MODIFIED.into_response());
}
let ctx = tera::Context::new();
let mut ctx = tera::Context::new();
ctx.insert("base_url", &state.base_url.to_string());
let res = state.tera.render("index.html", &ctx).map_err(|e| {
error!("Failed rendering index: {}", e);
WebsiteError::NotFound
WebsiteError::InternalError(e.into())
})?;
Ok((
StatusCode::OK,
@ -97,17 +95,17 @@ pub async fn not_found() -> impl IntoResponse {
pub async fn metrics_middleware<B>(request: Request<B>, next: Next<B>) -> Response {
let path = request.uri().path().to_string();
let method = request.method().to_string();
let method = request.method().clone();
let response = next.run(request).await;
if !response.status().is_client_error() {
HIT_COUNTER
.with_label_values(&[&path, &method, response.status().as_str()])
.with_label_values(&[&path, method.as_str(), response.status().as_str()])
.inc();
} else if response.status() == StatusCode::NOT_FOUND {
HIT_COUNTER
.with_label_values(&["not found", &method, response.status().as_str()])
.with_label_values(&["not found", method.as_str(), response.status().as_str()])
.inc();
}
@ -116,16 +114,16 @@ pub async fn metrics_middleware<B>(request: Request<B>, next: Next<B>) -> Respon
fn should_return_304(headers: &HeaderMap, last_changed: Option<DateTime<FixedOffset>>) -> bool {
let Some(date) = last_changed else {
info!("no last modified date");
debug!("no last modified date");
return false;
};
let Some(since) = headers.get(header::IF_MODIFIED_SINCE) else {
info!("no IF_MODIFIED_SINCE header");
debug!("no IF_MODIFIED_SINCE header");
return false;
};
let Ok(parsed) = DateTime::<FixedOffset>::parse_from_rfc2822(since.to_str().unwrap()) else {
info!("failed to parse IF_MODIFIED_SINCE header");
debug!("failed to parse IF_MODIFIED_SINCE header");
return false;
};
@ -160,7 +158,8 @@ mod tests {
#[test]
fn render_index() {
let tera = tera::Tera::new("templates/**/*").unwrap();
let ctx = tera::Context::new();
let mut ctx = tera::Context::new();
ctx.insert("base_url", "http://localhost/");
let _res = tera.render("index.html", &ctx).unwrap();
}

View file

@ -56,7 +56,7 @@ pub async fn index(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Response, WebsiteError> {
let mut posts: Vec<&Post> = state.posts.values().filter(|p| p.is_published()).collect();
let mut posts: Vec<&Post> = state.posts.values().filter(|p| !p.draft && p.is_published()).collect();
let last_changed = posts.iter().filter_map(|p| p.last_modified()).max();
@ -135,7 +135,7 @@ pub async fn feed(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Response, WebsiteError> {
let mut posts: Vec<&Post> = state.posts.values().filter(|p| p.is_published()).collect();
let mut posts: Vec<&Post> = state.posts.values().filter(|p| !p.draft && p.is_published()).collect();
let last_changed = posts.iter().filter_map(|p| p.last_modified()).max();
@ -203,6 +203,7 @@ mod tests {
];
let page = PageContext { title: "Posts" };
let mut ctx = tera::Context::new();
ctx.insert("base_url", "http://localhost/");
ctx.insert("page", &page);
ctx.insert("posts", &posts);

View file

@ -57,7 +57,7 @@ pub async fn view(
let mut posts: Vec<&Post> = state
.posts
.values()
.filter(|p| p.is_published() && p.tags.contains(&tag))
.filter(|p| !p.draft && p.is_published() && p.tags.contains(&tag))
.collect();
let last_changed = posts.iter().filter_map(|p| p.last_modified()).max();

View file

@ -31,3 +31,28 @@ pub fn uri_with_path(uri: &Uri, path: &str) -> Uri {
.build()
.unwrap();
}
#[cfg(test)]
mod tests {
use axum::http::Uri;
use crate::helpers::uri_with_path;
#[test]
fn uri_with_relative_path() {
let uri: Uri = "http://localhost/baba/".parse().unwrap();
assert_eq!(uri_with_path(&uri, "is/you").to_string(), "http://localhost/baba/is/you");
}
#[test]
fn uri_with_absolute_path() {
let uri: Uri = "http://localhost/baba/is/you".parse().unwrap();
assert_eq!(uri_with_path(&uri, "/keke/is/move").to_string(), "http://localhost/keke/is/move");
}
#[test]
fn uri_with_index_relative_path() {
let uri: Uri = "http://localhost/baba/is/you".parse().unwrap();
assert_eq!(uri_with_path(&uri, "happy").to_string(), "http://localhost/baba/is/happy");
}
}

View file

@ -11,6 +11,7 @@ use axum::{
use chrono::DateTime;
use color_eyre::eyre::{Error, Result};
use config::Config;
use post::Post;
use tag::Tag;
@ -39,15 +40,20 @@ pub struct AppState {
#[tokio::main]
async fn main() -> Result<()> {
let cfg = Config::builder()
.add_source(config::File::with_name("config.toml"))
.add_source(config::Environment::with_prefix("APP"))
.build()?;
color_eyre::install()?;
init_tracing();
init_tracing(&cfg);
info!("Starting server...");
let base_url: Uri = option_env!("SITE_BASE_URL")
.unwrap_or("http://localhost:8080")
let base_url: Uri = cfg.get_string("base_url")?
.parse()
.unwrap();
let tera = Tera::new("templates/**/*")?;
let mut state = AppState {
startup_time: chrono::offset::Utc::now(),
@ -55,6 +61,7 @@ async fn main() -> Result<()> {
tera,
..Default::default()
};
let posts = post::load_all(&state).await?;
let tags = tag::get_tags(posts.values());
state.posts = posts;
@ -73,17 +80,23 @@ async fn main() -> Result<()> {
info!("Now listening at {}", state.base_url);
axum::Server::bind(&"0.0.0.0:8080".parse().unwrap())
axum::Server::bind(&cfg.get_string("bind_address")?.parse().unwrap())
.serve(app.into_make_service())
.await?;
Ok(())
}
fn init_tracing() {
let filter = EnvFilter::builder()
.with_default_directive("into".parse().unwrap())
.from_env_lossy();
fn init_tracing(cfg: &Config) {
let filter = if let Ok(filter) = cfg.get_string("logging") {
EnvFilter::builder()
.with_default_directive("info".parse().unwrap())
.parse_lossy(filter)
} else {
EnvFilter::builder()
.with_default_directive("info".parse().unwrap())
.from_env_lossy()
};
tracing_subscriber::registry()
.with(filter)

View file

@ -1,14 +1,26 @@
use crate::helpers;
use crate::hilighting;
use axum::http::Uri;
use cached::once_cell::sync::Lazy;
use pulldown_cmark::Event;
use pulldown_cmark::Tag;
use pulldown_cmark::{Options, Parser};
use regex::Regex;
static STARTS_WITH_SCHEMA_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\w+:").unwrap());
static EMAIL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^.+?@\w+(\.\w+)*$").unwrap());
pub fn render_markdown_to_html(base_uri: Option<&Uri>, markdown: &str) -> String {
let options = Options::all();
let mut opt = Options::empty();
opt.insert(Options::ENABLE_FOOTNOTES);
opt.insert(Options::ENABLE_HEADING_ATTRIBUTES);
opt.insert(Options::ENABLE_STRIKETHROUGH);
opt.insert(Options::ENABLE_TABLES);
opt.insert(Options::ENABLE_TASKLISTS);
opt.insert(Options::ENABLE_SMART_PUNCTUATION);
let mut content_html = String::new();
let parser = Parser::new_ext(markdown, options);
let parser = Parser::new_ext(markdown, opt);
let mut code_block = false;
let mut code_lang = None;
@ -25,7 +37,7 @@ pub fn render_markdown_to_html(base_uri: Option<&Uri>, markdown: &str) -> String
}
Event::Start(Tag::Link(t, mut link, title)) => {
if let Some(uri) = base_uri {
if !link.contains("://") && !link.contains('@') {
if !link.starts_with('#') && !STARTS_WITH_SCHEMA_RE.is_match(&link) && !EMAIL_RE.is_match(&link) {
// convert relative URIs to absolute URIs
link = helpers::uri_with_path(uri, &link).to_string().into();
}

View file

@ -1,10 +1,9 @@
use std::{collections::HashMap, path::Path};
use cached::once_cell::sync::Lazy;
use chrono::{DateTime, FixedOffset};
use glob::glob;
use lazy_static::lazy_static;
use regex::Regex;
use serde_derive::{Deserialize, Serialize};
use tokio::fs;
@ -16,6 +15,11 @@ use tracing::{
use crate::{helpers, markdown, AppState, WebsiteError};
static FRONTMATTER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(
r"^[\s]*\+{3}(\r?\n(?s).*?(?-s))\+{3}[\s]*(?:$|(?:\r?\n((?s).*(?-s))$))"
)
.unwrap());
#[derive(Deserialize, Debug, Default)]
pub struct TomlFrontMatter {
pub title: String,
@ -29,6 +33,7 @@ pub struct TomlFrontMatter {
#[derive(Serialize, Clone, Debug, Default)]
pub struct Post {
pub title: String,
pub draft: bool,
pub date: Option<DateTime<FixedOffset>>,
pub updated: Option<DateTime<FixedOffset>>,
pub aliases: Vec<String>,
@ -45,6 +50,7 @@ impl Post {
slug,
content,
title: fm.title,
draft: fm.draft.unwrap_or(false),
date: fm
.date
.map(|d| DateTime::parse_from_rfc3339(&d.to_string()).expect("bad toml datetime")),
@ -124,13 +130,6 @@ pub async fn load_post(state: &AppState, slug: &str) -> color_eyre::eyre::Result
fn parse_frontmatter(
src: String,
) -> color_eyre::eyre::Result<(Option<TomlFrontMatter>, Option<String>)> {
lazy_static! {
static ref FRONTMATTER_REGEX: Regex = regex::Regex::new(
r"^[\s]*\+{3}(\r?\n(?s).*?(?-s))\+{3}[\s]*(?:$|(?:\r?\n((?s).*(?-s))$))"
)
.unwrap();
};
Ok(if let Some(captures) = FRONTMATTER_REGEX.captures(&src) {
(
Some(toml::from_str(captures.get(1).unwrap().as_str())?),
@ -162,10 +161,11 @@ mod tests {
#[tokio::test]
async fn render_all_posts() {
let mut state = AppState {
base_url: "localhost:8180".parse().unwrap(),
base_url: "http://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

@ -2,16 +2,10 @@
<html lang="en">
{% include "partials/head.html" %}
<body>
<header>
{% include "partials/header.html" -%}
</header>
<hr>
{% include "partials/header.html" -%}
<main>
{% block main %}{% endblock main -%}
</main>
<hr>
<footer>
{% include "partials/footer.html" -%}
</footer>
{% include "partials/footer.html" -%}
</body>
</html>

View file

@ -1 +1,4 @@
<p>footer stuff goes here</p>
<footer>
<hr style="border-style: dashed;">
<p>footer stuff goes here</p>
</footer>

View file

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

View file

@ -6,9 +6,11 @@
<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>
{%- if page.draft %}Draft {% endif -%}
Published <time datetime="{{ page.date }}">{{ page.date | date(format="%F %R%:::z") }}</time>
{%- if page.updated -%}
, Updated <time datetime="{{ page.updated }}">{{ page.updated | date(format="%Y-%m-%d %H:%M") }}</time>
, Updated <time datetime="{{ page.updated }}">{{ page.updated | date(format="%F %R%:::z") }}</time>
{%- endif -%}
</small>
{%- endif %}

View file

@ -5,7 +5,7 @@
<ul>
{% for post in posts -%}
<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="%F") }}</time> &ndash; {{ post.title -}}
{% else -%}
{{ post.title -}}
{% endif -%}

View file

@ -8,7 +8,7 @@
<ul>
{% for post in posts -%}
<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="%F") }}</time> - {{ post.title -}}
{% else -%}
{{ post.title -}}
{% endif -%}

View file

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block main -%}
<h1>Tags</h1>
<h2>Tags</h2>
<ul>
{% for tag in tags -%}
<li><a href="{{base_url | trim_end_matches(pat='/') | safe}}{{tag.absolute_path | safe}}">#{{ tag.slug }}</a></li>