aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorXe Iaso <me@christine.website>2022-11-24 15:46:42 -0500
committerXe Iaso <me@christine.website>2022-11-24 15:46:42 -0500
commit18fcf051499912f17dd49bc0413e1de58b2d44f2 (patch)
treed16ed58127bfcb8b1b4ee9bdb88fd35cb687c8c9 /src
parent551e0384c923ff3ee98cfddf7e3eb42c6dbb2941 (diff)
downloadxesite-18fcf051499912f17dd49bc0413e1de58b2d44f2.tar.xz
xesite-18fcf051499912f17dd49bc0413e1de58b2d44f2.zip
Start version 3
* Change version to 3.0.0 in Cargo.toml * Add metadata for series * Change types for signal boosts * Add start of LaTeX resume generation at Nix time * Add start of proper author tagging for posts in JSONFeed and ldjson * Convert templates to use Maud * Add start of dynamic resume generation from dhall * Make patrons page embed thumbnails TODO: * [ ] Remove the rest of the old templates * [ ] Bring in Xeact for the share on mastodon button * [ ] Site update post Signed-off-by: Xe <me@christine.website>
Diffstat (limited to 'src')
-rw-r--r--src/app/config.rs90
-rw-r--r--src/app/mod.rs2
-rw-r--r--src/handlers/blog.rs55
-rw-r--r--src/handlers/gallery.rs28
-rw-r--r--src/handlers/mod.rs75
-rw-r--r--src/handlers/talks.rs9
-rw-r--r--src/main.rs12
-rw-r--r--src/post/mod.rs10
-rw-r--r--src/signalboost.rs12
-rw-r--r--src/tmpl/asciiart.txt45
-rw-r--r--src/tmpl/mod.rs602
-rw-r--r--src/tmpl/nag.rs2
12 files changed, 813 insertions, 129 deletions
diff --git a/src/app/config.rs b/src/app/config.rs
index e0a4d90..6499e72 100644
--- a/src/app/config.rs
+++ b/src/app/config.rs
@@ -1,7 +1,8 @@
use crate::signalboost::Person;
-use maud::{html, Markup};
+use maud::{html, Markup, Render};
use serde::{Deserialize, Serialize};
use std::{
+ collections::HashMap,
fmt::{self, Display},
path::PathBuf,
};
@@ -9,7 +10,9 @@ use std::{
#[derive(Clone, Deserialize, Default)]
pub struct Config {
pub signalboost: Vec<Person>,
- pub authors: Vec<Author>,
+ pub authors: HashMap<String, Author>,
+ #[serde(rename = "defaultAuthor")]
+ pub default_author: Author,
pub port: u16,
#[serde(rename = "clackSet")]
pub clack_set: Vec<String>,
@@ -19,6 +22,35 @@ pub struct Config {
pub mi_token: String,
#[serde(rename = "jobHistory")]
pub job_history: Vec<Job>,
+ #[serde(rename = "seriesDescriptions")]
+ pub series_descriptions: Vec<SeriesDescription>,
+ #[serde(rename = "seriesDescMap")]
+ pub series_desc_map: HashMap<String, String>,
+ #[serde(rename = "notableProjects")]
+ pub notable_projects: Vec<Link>,
+ #[serde(rename = "contactLinks")]
+ pub contact_links: Vec<Link>,
+}
+
+#[derive(Clone, Deserialize, Serialize, Default)]
+pub struct Link {
+ pub url: String,
+ pub title: String,
+ pub description: String,
+}
+
+impl Render for Link {
+ fn render(&self) -> Markup {
+ html! {
+ span {
+ a href=(self.url) {(self.title)}
+ @if !self.description.is_empty() {
+ ": "
+ (self.description)
+ }
+ }
+ }
+ }
}
#[derive(Clone, Deserialize, Serialize)]
@@ -33,17 +65,51 @@ impl Default for StockKind {
}
}
+fn schema_context() -> String {
+ "http://schema.org/".to_string()
+}
+
+fn schema_person_type() -> String {
+ "Person".to_string()
+}
+
#[derive(Clone, Deserialize, Serialize, Default)]
pub struct Author {
+ #[serde(rename = "@context", default = "schema_context")]
+ pub context: String,
+ #[serde(rename = "@type", default = "schema_person_type")]
+ pub schema_type: String,
pub name: String,
+ #[serde(skip_serializing)]
pub handle: String,
- #[serde(rename = "picUrl")]
+ #[serde(rename = "image", skip_serializing_if = "Option::is_none")]
pub pic_url: Option<String>,
- pub link: Option<String>,
- pub twitter: Option<String>,
- pub default: bool,
- #[serde(rename = "inSystem")]
+ #[serde(rename = "inSystem", skip_serializing)]
pub in_system: bool,
+ #[serde(rename = "jobTitle")]
+ pub job_title: String,
+ #[serde(rename = "sameAs")]
+ pub same_as: Vec<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub url: Option<String>,
+}
+
+#[derive(Clone, Deserialize, Serialize, Default)]
+pub struct SeriesDescription {
+ pub name: String,
+ pub details: String,
+}
+
+impl Render for SeriesDescription {
+ fn render(&self) -> Markup {
+ html! {
+ span {
+ a href={"/blog/series/" (self.name)} { (self.name) }
+ ": "
+ (self.details)
+ }
+ }
+ }
}
#[derive(Clone, Deserialize, Serialize, Default)]
@@ -80,8 +146,8 @@ impl Display for Salary {
}
}
-impl Salary {
- pub fn html(&self) -> Markup {
+impl Render for Salary {
+ fn render(&self) -> Markup {
if self.stock.is_none() {
return html! { (maud::display(self)) };
}
@@ -162,15 +228,15 @@ pub struct Company {
pub defunct: bool,
}
-impl Job {
- pub fn pay_history_row(&self) -> Markup {
+impl Render for Job {
+ fn render(&self) -> Markup {
html! {
tr {
td { (self.title) }
td { (self.start_date) }
td { (self.end_date.as_ref().unwrap_or(&"current".to_string())) }
td { (if self.days_worked.is_some() { self.days_worked.as_ref().unwrap().to_string() } else { "n/a".to_string() }) }
- td { (self.salary.html()) }
+ td { (self.salary) }
td { (self.leave_reason.as_ref().unwrap_or(&"n/a".to_string())) }
}
}
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 73320e1..8c7cc4e 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -85,7 +85,7 @@ pub async fn init(cfg: PathBuf) -> Result<State> {
everything.sort();
everything.reverse();
- let today = Utc::today();
+ let today = Utc::now().date_naive();
let everything: Vec<Post> = everything
.into_iter()
.filter(|p| today.num_days_from_ce() >= p.date.num_days_from_ce())
diff --git a/src/handlers/blog.rs b/src/handlers/blog.rs
index 012a7ad..09cef8b 100644
--- a/src/handlers/blog.rs
+++ b/src/handlers/blog.rs
@@ -1,14 +1,16 @@
use super::Result;
-use crate::{app::State, post::Post, templates};
+use crate::{app::State, post::Post, templates, tmpl};
use axum::{
extract::{Extension, Path},
+ http::StatusCode,
response::Html,
};
use http::HeaderMap;
use lazy_static::lazy_static;
+use maud::Markup;
use prometheus::{opts, register_int_counter_vec, IntCounterVec};
use std::sync::Arc;
-use tracing::{error, instrument};
+use tracing::instrument;
lazy_static! {
static ref HIT_COUNTER: IntCounterVec = register_int_counter_vec!(
@@ -19,40 +21,27 @@ lazy_static! {
}
#[instrument(skip(state))]
-pub async fn index(Extension(state): Extension<Arc<State>>) -> Result {
+pub async fn index(Extension(state): Extension<Arc<State>>) -> Result<Markup> {
let state = state.clone();
- let mut result: Vec<u8> = vec![];
- templates::blogindex_html(&mut result, state.blog.clone())?;
- Ok(Html(result))
+ let result = tmpl::post_index(&state.blog, "Blogposts", true);
+ Ok(result)
}
#[instrument(skip(state))]
-pub async fn series(Extension(state): Extension<Arc<State>>) -> Result {
+pub async fn series(Extension(state): Extension<Arc<State>>) -> Result<Markup> {
let state = state.clone();
- let mut series: Vec<String> = vec![];
- let mut result: Vec<u8> = vec![];
- for post in &state.blog {
- if post.front_matter.series.is_some() {
- series.push(post.front_matter.series.as_ref().unwrap().clone());
- }
- }
-
- series.sort();
- series.dedup();
-
- templates::series_html(&mut result, series)?;
- Ok(Html(result))
+ Ok(tmpl::blog_series(&state.cfg.clone().series_descriptions))
}
#[instrument(skip(state))]
pub async fn series_view(
Path(series): Path<String>,
Extension(state): Extension<Arc<State>>,
-) -> Result {
+) -> (StatusCode, Markup) {
let state = state.clone();
+ let cfg = state.cfg.clone();
let mut posts: Vec<Post> = vec![];
- let mut result: Vec<u8> = vec![];
for post in &state.blog {
if post.front_matter.series.is_none() {
@@ -64,13 +53,25 @@ pub async fn series_view(
posts.push(post.clone());
}
+ posts.reverse();
+
+ let desc = cfg.series_desc_map.get(&series);
+
if posts.len() == 0 {
- error!("series not found");
- return Err(super::Error::SeriesNotFound(series));
+ (
+ StatusCode::NOT_FOUND,
+ tmpl::error(format!("series not found: {series}")),
+ )
+ } else {
+ if let Some(desc) = desc {
+ (StatusCode::OK, tmpl::series_view(&series, desc, &posts))
+ } else {
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ tmpl::error(format!("series metadata in dhall not found: {series}")),
+ )
+ }
}
-
- templates::series_posts_html(&mut result, series, &posts).unwrap();
- Ok(Html(result))
}
#[instrument(skip(state, headers))]
diff --git a/src/handlers/gallery.rs b/src/handlers/gallery.rs
index ae6c411..fd3ed1b 100644
--- a/src/handlers/gallery.rs
+++ b/src/handlers/gallery.rs
@@ -1,10 +1,8 @@
-use super::{Error::*, Result};
-use crate::{app::State, post::Post, templates};
-use axum::{
- extract::{Extension, Path},
- response::Html,
-};
+use crate::{app::State, post::Post, tmpl};
+use axum::extract::{Extension, Path};
+use http::StatusCode;
use lazy_static::lazy_static;
+use maud::Markup;
use prometheus::{opts, register_int_counter_vec, IntCounterVec};
use std::sync::Arc;
use tracing::instrument;
@@ -18,36 +16,32 @@ lazy_static! {
}
#[instrument(skip(state))]
-pub async fn index(Extension(state): Extension<Arc<State>>) -> Result {
+pub async fn index(Extension(state): Extension<Arc<State>>) -> Markup {
let state = state.clone();
- let mut result: Vec<u8> = vec![];
- templates::galleryindex_html(&mut result, state.gallery.clone())?;
- Ok(Html(result))
+ tmpl::gallery_index(&state.gallery)
}
#[instrument(skip(state))]
pub async fn post_view(
Path(name): Path<String>,
Extension(state): Extension<Arc<State>>,
-) -> Result {
+) -> (StatusCode, Markup) {
let mut want: Option<Post> = None;
+ let link = format!("gallery/{}", name);
for post in &state.gallery {
- if post.link == format!("gallery/{}", name) {
+ if post.link == link {
want = Some(post.clone());
}
}
match want {
- None => Err(PostNotFound(name)),
+ None => (StatusCode::NOT_FOUND, tmpl::not_found(link)),
Some(post) => {
HIT_COUNTER
.with_label_values(&[name.clone().as_str()])
.inc();
- let body = templates::Html(post.body_html.clone());
- let mut result: Vec<u8> = vec![];
- templates::gallerypost_html(&mut result, post, body)?;
- Ok(Html(result))
+ (StatusCode::OK, tmpl::gallery_post(&post))
}
}
}
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
index 37fcac1..0bcebe7 100644
--- a/src/handlers/mod.rs
+++ b/src/handlers/mod.rs
@@ -1,4 +1,4 @@
-use crate::{app::State, templates};
+use crate::{app::State, tmpl};
use axum::{
body,
extract::Extension,
@@ -7,6 +7,7 @@ use axum::{
};
use chrono::{Datelike, Timelike, Utc, Weekday};
use lazy_static::lazy_static;
+use maud::{Markup, PreEscaped};
use prometheus::{opts, register_int_counter_vec, IntCounterVec};
use std::sync::Arc;
use tracing::instrument;
@@ -67,82 +68,76 @@ lazy_static! {
};
}
-#[instrument]
-pub async fn index() -> Result {
+#[instrument(skip(state))]
+pub async fn index(Extension(state): Extension<Arc<State>>) -> Result<Markup> {
HIT_COUNTER.with_label_values(&["index"]).inc();
- let mut result: Vec<u8> = vec![];
- templates::index_html(&mut result)?;
- Ok(Html(result))
+ let state = state.clone();
+ let cfg = state.cfg.clone();
+
+ Ok(tmpl::index(&cfg.default_author, &cfg.notable_projects))
}
-#[instrument]
-pub async fn contact() -> Result {
+#[instrument(skip(state))]
+pub async fn contact(Extension(state): Extension<Arc<State>>) -> Markup {
HIT_COUNTER.with_label_values(&["contact"]).inc();
- let mut result: Vec<u8> = vec![];
- templates::contact_html(&mut result)?;
- Ok(Html(result))
+ let state = state.clone();
+ let cfg = state.cfg.clone();
+
+ crate::tmpl::contact(&cfg.contact_links)
}
#[instrument]
-pub async fn feeds() -> Result {
+pub async fn feeds() -> Markup {
HIT_COUNTER.with_label_values(&["feeds"]).inc();
- let mut result: Vec<u8> = vec![];
- templates::feeds_html(&mut result)?;
- Ok(Html(result))
+ crate::tmpl::feeds()
}
#[axum_macros::debug_handler]
#[instrument(skip(state))]
-pub async fn salary_transparency(Extension(state): Extension<Arc<State>>) -> Result {
+pub async fn salary_transparency(Extension(state): Extension<Arc<State>>) -> Result<Markup> {
HIT_COUNTER
.with_label_values(&["salary_transparency"])
.inc();
let state = state.clone();
- let mut result: Vec<u8> = vec![];
- templates::salary_transparency(&mut result, state.cfg.clone())?;
- Ok(Html(result))
+ let cfg = state.cfg.clone();
+
+ Ok(tmpl::salary_transparency(&cfg.job_history))
}
#[axum_macros::debug_handler]
#[instrument(skip(state))]
-pub async fn resume(Extension(state): Extension<Arc<State>>) -> Result {
+pub async fn resume(Extension(state): Extension<Arc<State>>) -> Result<Markup> {
HIT_COUNTER.with_label_values(&["resume"]).inc();
let state = state.clone();
- let mut result: Vec<u8> = vec![];
- templates::resume_html(&mut result, templates::Html(state.resume.clone()))?;
- Ok(Html(result))
+
+ Ok(tmpl::resume(PreEscaped(&state.resume)))
}
#[instrument(skip(state))]
-pub async fn patrons(Extension(state): Extension<Arc<State>>) -> Result {
+pub async fn patrons(Extension(state): Extension<Arc<State>>) -> (StatusCode, Markup) {
HIT_COUNTER.with_label_values(&["patrons"]).inc();
let state = state.clone();
- let mut result: Vec<u8> = vec![];
match &state.patrons {
- None => Err(Error::NoPatrons),
- Some(patrons) => {
- templates::patrons_html(&mut result, patrons.clone())?;
- Ok(Html(result))
- }
+ None => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ tmpl::error("Patreon API config is broken, no patrons in ram"),
+ ),
+ Some(patrons) => (StatusCode::IM_A_TEAPOT, tmpl::patrons(&patrons)),
}
}
#[axum_macros::debug_handler]
#[instrument(skip(state))]
-pub async fn signalboost(Extension(state): Extension<Arc<State>>) -> Result {
+pub async fn signalboost(Extension(state): Extension<Arc<State>>) -> Markup {
HIT_COUNTER.with_label_values(&["signalboost"]).inc();
let state = state.clone();
- let mut result: Vec<u8> = vec![];
- templates::signalboost_html(&mut result, state.signalboost.clone())?;
- Ok(Html(result))
+ tmpl::signalboost(&state.signalboost)
}
#[instrument]
-pub async fn not_found() -> Result {
+pub async fn not_found(uri: axum::http::Uri) -> (StatusCode, Markup) {
HIT_COUNTER.with_label_values(&["not_found"]).inc();
- let mut result: Vec<u8> = vec![];
- templates::notfound_html(&mut result, "some path".into())?;
- Ok(Html(result))
+ (StatusCode::NOT_FOUND, tmpl::not_found(uri.path()))
}
#[derive(Debug, thiserror::Error)]
@@ -170,8 +165,8 @@ pub type Result<T = Html<Vec<u8>>> = std::result::Result<T, Error>;
impl IntoResponse for Error {
fn into_response(self) -> Response {
- let mut result: Vec<u8> = vec![];
- templates::error_html(&mut result, format!("{}", self)).unwrap();
+ let result = tmpl::error(format!("{}", self));
+ let result = result.0;
let body = body::boxed(body::Full::from(result));
diff --git a/src/handlers/talks.rs b/src/handlers/talks.rs
index 59d8676..1b27a16 100644
--- a/src/handlers/talks.rs
+++ b/src/handlers/talks.rs
@@ -1,11 +1,12 @@
use super::{Error::*, Result};
-use crate::{app::State, post::Post, templates};
+use crate::{app::State, post::Post, templates, tmpl};
use axum::{
extract::{Extension, Path},
response::Html,
};
use http::header::HeaderMap;
use lazy_static::lazy_static;
+use maud::Markup;
use prometheus::{opts, register_int_counter_vec, IntCounterVec};
use std::sync::Arc;
use tracing::instrument;
@@ -19,11 +20,9 @@ lazy_static! {
}
#[instrument(skip(state))]
-pub async fn index(Extension(state): Extension<Arc<State>>) -> Result {
+pub async fn index(Extension(state): Extension<Arc<State>>) -> Result<Markup> {
let state = state.clone();
- let mut result: Vec<u8> = vec![];
- templates::talkindex_html(&mut result, state.talks.clone())?;
- Ok(Html(result))
+ Ok(tmpl::post_index(&state.talks, "Talks", false))
}
#[instrument(skip(state, headers))]
diff --git a/src/main.rs b/src/main.rs
index a35db42..737ab45 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,8 +4,9 @@ extern crate tracing;
use axum::{
body,
extract::Extension,
+ handler::Handler,
http::header::{self, HeaderValue, CONTENT_TYPE},
- response::{Html, Response},
+ response::Response,
routing::{get, get_service},
Router,
};
@@ -211,6 +212,7 @@ async fn main() -> Result<()> {
)
}),
)
+ .fallback(handlers::not_found.into_service())
.layer(middleware);
#[cfg(target_os = "linux")]
@@ -276,16 +278,12 @@ async fn metrics() -> Response {
.unwrap()
}
-async fn go_vanity() -> Html<Vec<u8>> {
- let mut buffer: Vec<u8> = vec![];
- templates::gitea_html(
- &mut buffer,
+async fn go_vanity() -> maud::Markup {
+ tmpl::gitea(
"christine.website/jsonfeed",
"https://tulpa.dev/Xe/jsonfeed",
"master",
)
- .unwrap();
- Html(buffer)
}
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
diff --git a/src/post/mod.rs b/src/post/mod.rs
index 8ed99bf..2046a89 100644
--- a/src/post/mod.rs
+++ b/src/post/mod.rs
@@ -99,10 +99,12 @@ async fn read_post(dir: &str, fname: PathBuf, cli: &Option<mi::Client>) -> Resul
let link = format!("{}/{}", dir, fname.file_stem().unwrap().to_str().unwrap());
let body_html = xesite_markdown::render(&body)
.wrap_err_with(|| format!("can't parse markdown for {:?}", fname))?;
- let date: DateTime<FixedOffset> =
- DateTime::<Utc>::from_utc(NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)), Utc)
- .with_timezone(&Utc)
- .into();
+ let date: DateTime<FixedOffset> = DateTime::<Utc>::from_utc(
+ NaiveDateTime::new(date, NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
+ Utc,
+ )
+ .with_timezone(&Utc)
+ .into();
let mentions: Vec<mi::WebMention> = match cli {
Some(cli) => cli
diff --git a/src/signalboost.rs b/src/signalboost.rs
index 6adfc8f..d4638d9 100644
--- a/src/signalboost.rs
+++ b/src/signalboost.rs
@@ -1,17 +1,11 @@
+use crate::app::config::Link;
use serde::Deserialize;
-#[derive(Clone, Debug, Deserialize)]
+#[derive(Clone, Deserialize)]
pub struct Person {
pub name: String,
pub tags: Vec<String>,
- #[serde(rename = "gitLink")]
- pub git_link: Option<String>,
- pub twitter: Option<String>,
- pub linkedin: Option<String>,
- pub fediverse: Option<String>,
- #[serde(rename = "coverLetter")]
- pub cover_letter: Option<String>,
- pub website: Option<String>,
+ pub links: Vec<Link>,
}
#[cfg(test)]
diff --git a/src/tmpl/asciiart.txt b/src/tmpl/asciiart.txt
new file mode 100644
index 0000000..88fe61e
--- /dev/null
+++ b/src/tmpl/asciiart.txt
@@ -0,0 +1,45 @@
+<!--
+MMMMMMMMMMMMMMMMMMNmmNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmmd.:mmMM
+MMMMMMMMMMMMMMMMMNmmmNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmydmmmmmNMM
+MMMMMMMMMMMMMMMMNm/:mNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmms /mmmmmMMM
+MMMMMMMMMMMMMMMNmm:-dmMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmmmdsdmmmmNMMM
+MMMMMMMMMMMMMMMmmmmmmmNMMMMMMMMMMMNmmdhhddhhmNNMMMMMMMMMMMMMMMMNmy:hmmmmmmmmMMMM
+MMMMMMMMMMMMMMNm++mmmmNMMMMMMmdyo/::.........-:/sdNMMMMMMMMMMNmmms`smmmmmmmNMMMM
+MMMMMMMMMMMMMMmd.-dmmmmMMmhs/-....................-+dNMMMMMMNmmmmmmmmmmmmmmMMMMM
+MMMMMMMMMMMMMNmmmmmmmmho:-...........................:sNMMNmmmmmmmmmmmmmmmNMNmdd
+MMMMMMMMMMMMNmd+ydhs/-.................................-sNmmmmmmmmmmmmmmmdhyssss
+MMMMMMMMMMMNNh+`........................................:dmmmmmmmmmmmmmmmyssssss
+MMMMNNdhy+:-...........................................+dmmmmmmmmmmmmmmmdsssssss
+MMMN+-...............................................-smmmmmmmmmmmmmmmmmysyyhdmN
+MMMMNho:::-.--::-.......................----------..:hmmmmmmmmmmmmmmmmmmmNMMMMMM
+MMMMMMMMNNNmmdo:......................--------------:ymmmmmmmmmmmmmmmmmmmMMMMMMM
+MMMMMMMMMMds+........................-----------------+dmmmmmmmmmmmmmmmmmMMMMMMM
+MMMMMMMMMh+........................--------------------:smmmmmmmmmmmmmmNMMMMMMMM
+MMMMMMMNy/........................-------------::--------/hmmmmmmmmmmmNMMMMMMNmd
+MMMMMMMd/........................--------------so----------odmmmmmmmmMMNmdhhysss
+MMMMMMm/........................--------------+mh-----------:ymmmmdhhyysssssssss
+MMMMMMo.......................---------------:dmmo------------+dmdysssssssssssss
+yhdmNh:......................---------------:dmmmm+------------:sssssssssssyhhdm
+sssssy.......................--------------:hmmmmmmos++:---------/sssyyhdmNMMMMM
+ssssso......................--------------:hmmmNNNMNdddysso:------:yNNMMMMMMMMMM
+ysssss.....................--------------/dmNyy/mMMd``d/------------sNMMMMMMMMMM
+MNmdhy-...................--------------ommmh`o/NM/. smh+-----------:yNMMMMMMMMM
+MMMMMN+...................------------/hmmss: `-//-.smmmmd+----------:hMMMMMMMMM
+MMMMMMd:..................----------:smmmmhy+oosyysdmmy+:. `.--------/dMMMMMMMM
+MMMMMMMh-................---------:smmmmmmmmmmmmmmmh/` `/s:-------sMMMMMMMM
+MMMMMMMms:...............-------/ymmmmmmmmmmmmmmmd/ :dMMNy/-----+mMMMMMMM
+MMMMMMmyss/..............------ommmmmmmmmmmmmmmmd. :yMMMMMMNs:---+mMMMMMMM
+MMMMNdssssso-............----..odmmmmmmmmmmmmmmh:.` .sNMMMMMMMMMd/--sMMMMMMMM
+MMMmysssssssh/................` -odmmmmmmmmmh+. `omMMMMMMMMMMMMh/+mMMMMMMMM
+MNdyssssssymMNy-.............. `/sssso+:. `+mMMMMMMMMMMMMMMMdNMMMMMMMMM
+NhssssssshNMMMMNo:............/.` `+dMMMMMMMMMMMMMMMMMMMMMMMMMMMM
+ysssssssdMMMMMMMMm+-..........+ddy/.` -omMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
+ssssssymMMMMMMMMMMMh/.........-oNMMNmy+--` `-+dNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
+ssssydNMMMMMMMMMMMMMNy:........-hMMMMMMMNmdmMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
+sssymMMMMMMMMMMMMMMMMMm+....-..:hMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
+symNMMMMMMMMMMMMMMMMMMMNo.../-/dMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
+dNMMMMMMMMMMMMMMMMMMMMMMh:.:hyNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
+la budza pu cusku lu
+ <<.i do snura .i ko kanro
+ .i do panpi .i ko gleki>> li'u
+-->
diff --git a/src/tmpl/mod.rs b/src/tmpl/mod.rs
index b2ffd0d..9535be2 100644
--- a/src/tmpl/mod.rs
+++ b/src/tmpl/mod.rs
@@ -1,10 +1,600 @@
-use crate::app::Config;
-use maud::{html, Markup};
-use std::sync::Arc;
+use crate::{app::*, post::Post, signalboost::Person};
+use chrono::prelude::*;
+use lazy_static::lazy_static;
+use maud::{html, Markup, PreEscaped, Render, DOCTYPE};
+use patreon::Users;
pub mod nag;
-pub fn salary_history(cfg: Arc<Config>) -> Markup {
+lazy_static! {
+ static ref CACHEBUSTER: String = uuid::Uuid::new_v4().to_string().replace("-", "");
+}
+
+pub fn base(title: Option<&str>, styles: Option<&str>, content: Markup) -> Markup {
+ let now = Utc::now();
+ html! {
+ (DOCTYPE)
+ (PreEscaped(include_str!("./asciiart.txt")))
+ html lang="en" {
+ head {
+ title {
+ @if let Some(title) = title {
+ (title)
+ " - Xe Iaso"
+ } @else {
+ "Xe Iaso"
+ }
+ }
+ meta name="viewport" content="width=device-width, initial-scale=1.0";
+ link rel="stylesheet" href={"/css/hack.css?bustCache=" (*CACHEBUSTER)};
+ link rel="stylesheet" href={"/css/gruvbox-dark.css?bustCache=" (*CACHEBUSTER)};
+ link rel="stylesheet" href={"/css/shim.css?bustCache=" (*CACHEBUSTER)};
+ @match now.month() {
+ 12|1|2 => {
+ link rel="stylesheet" href={"/css/snow.css?bustCache=" (*CACHEBUSTER)};
+ }
+ _ => {},
+ }
+ link rel="manifest" href="/static/manifest.json";
+ link rel="alternate" title="Xe's Blog" type="application/rss+xml" href="https://xeiaso.net/blog.rss";
+ link rel="alternate" title="Xe's Blog" type="application/json" href="https://xeiaso.net/blog.json";
+ link rel="apple-touch-icon" sizes="57x57" href="/static/favicon/apple-icon-57x57.png";
+ link rel="apple-touch-icon" sizes="60x60" href="/static/favicon/apple-icon-60x60.png";
+ link rel="apple-touch-icon" sizes="72x72" href="/static/favicon/apple-icon-72x72.png";
+ link rel="apple-touch-icon" sizes="76x76" href="/static/favicon/apple-icon-76x76.png";
+ link rel="apple-touch-icon" sizes="114x114" href="/static/favicon/apple-icon-114x114.png";
+ link rel="apple-touch-icon" sizes="120x120" href="/static/favicon/apple-icon-120x120.png";
+ link rel="apple-touch-icon" sizes="144x144" href="/static/favicon/apple-icon-144x144.png";
+ link rel="apple-touch-icon" sizes="152x152" href="/static/favicon/apple-icon-152x152.png";
+ link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-icon-180x180.png";
+ link rel="icon" type="image/png" sizes="192x192" href="/static/favicon/android-icon-192x192.png";
+ link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png";
+ link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png";
+ link rel="icon" type="image/png" sizes="96x96" href="/static/favicon/favicon-96x96.png";
+ link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png";
+ meta name="msapplication-TileColor" content="#ffffff";
+ meta name="msapplication-TileImage" content="/static/favicon/ms-icon-144x144.png";
+ meta name="theme-color" content="#ffffff";
+ link href="https://mi.within.website/api/webmention/accept" rel="webmention";
+ @if let Some(styles) = styles {
+ style {
+ (PreEscaped(styles))
+ }
+ }
+ }
+ body.snow.hack.gruvbox-dark {
+ .container {
+ header {
+ span.logo {}
+ nav {
+ a href="/" { "Xe" }
+ " - "
+ a href="/blog" { "Blog" }
+ " - "
+ a href="/contact" { "Contact" }
+ " - "
+ a href="/resume" { "Resume" }
+ " - "
+ a href="/talks" { "Talks" }
+ " - "
+ a href="/signalboost" { "Signal Boost" }
+ " - "
+ a href="/feeds" { "Feeds" }
+ " | "
+ a target="_blank" rel="noopener noreferrer" href="https://graphviz.christine.website" { "Graphviz" }
+ " - "
+ a target="_blank" rel="noopener noreferrer" href="https://when-then-zen.christine.website/" { "When Then Zen" }
+ }
+ }
+
+ br;
+ br;
+
+ .snowframe {
+ (content)
+ }
+ hr;
+ footer {
+ blockquote {
+ "Copyright 2012-2022 Xe Iaso (Christine Dodrill). Any and all opinions listed here are my own and not representative of my employers; future, past and present."
+ }
+ p {
+ "Like what you see? Donate on "
+ a href="https://www.patreon.com/cadey" { "Patreon" }
+ " like "
+ a href="/patrons" { "these awesome people" }
+ "!"
+ }
+ p {
+ "Looking for someone for your team? Take a look "
+ a href="/signalboost" { "here" }
+ "."
+ }
+ p {
+ "See my salary transparency data "
+ a href="/salary-transparency" {"here"}
+ "."
+ }
+ p {
+ "Served by "
+ (env!("out"))
+ "/bin/xesite, see "
+ a href="https://github.com/Xe/site" { "source code here" }
+ "."
+ }
+ }
+ script src="/static/js/installsw.js" defer {}
+ }
+ }
+ }
+ }
+}
+
+pub fn post_index(posts: &Vec<Post>, title: &str, show_extra: bool) -> Markup {
+ let today = Utc::now().date_naive();
+ base(
+ Some(title),
+ None,
+ html! {
+ h1 { (title) }
+ @if show_extra {
+ p {
+ "If you have a compatible reader, be sure to check out my "
+ a href="/blog.rss" { "RSS feed" }
+ " for automatic updates. Also check out the "
+ a href="/blog.json" { "JSONFeed" }
+ "."
+ }
+ p {
+ "For a breakdown by post series, see "
+ a href="/blog/series" { "here" }
+ "."
+ }
+ }
+ p {
+ ul {
+ @for post in posts.iter().filter(|p| today.num_days_from_ce() >= p.date.num_days_from_ce()) {
+ li {
+ (post.detri())
+ " - "
+ a href={"/" (post.link)} { (post.front_matter.title) }
+ }
+ }
+ }
+ }
+ },
+ )
+}
+
+pub fn gallery_index(posts: &Vec<Post>) -> Markup {
+ base(
+ Some("Gallery"),
+ None,
+ html! {
+ h1 {"Gallery"}
+
+ p {"Here are links to a lot of the art I have done in the last few years."}
+
+ .grid {
+ @for post in posts {
+ .card.cell."-4of12".blogpost-card {
+ header."card-header" {
+ (post.front_matter.title)
+ }
+ .card-content {
+ center {
+ p {
+ "Posted on "
+ (post.detri())
+ br;
+ a href={"/" (post.link)} {
+ img src=(post.front_matter.thumb.as_ref().unwrap());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ )
+}
+
+pub fn gallery_post(post: &Post) -> Markup {
+ base