Add hero image support with <xeblog-hero>
Also lightens the JavaScript load and shifts ad impressions to only when people from Reddit and Hacker News visit. I may have this include Twitter in the future. Signed-off-by: Xe <me@christine.website>
This commit is contained in:
parent
396150f72b
commit
dc3f6471e7
File diff suppressed because it is too large
Load Diff
|
@ -9,7 +9,7 @@ repository = "https://github.com/Xe/site"
|
||||||
# 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 = "0.5"
|
axum = { version = "0.5", features = ["headers"] }
|
||||||
axum-macros = "0.2"
|
axum-macros = "0.2"
|
||||||
axum-extra = "0.3"
|
axum-extra = "0.3"
|
||||||
color-eyre = "0.6"
|
color-eyre = "0.6"
|
||||||
|
@ -28,9 +28,11 @@ kankyo = "0.3"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
lol_html = "0.3"
|
lol_html = "0.3"
|
||||||
|
maud = { version = "0.23.0", features = ["axum"] }
|
||||||
mime = "0.3.0"
|
mime = "0.3.0"
|
||||||
prometheus = { version = "0.13", default-features = false, features = ["process"] }
|
prometheus = { version = "0.13", default-features = false, features = ["process"] }
|
||||||
rand = "0"
|
rand = "0"
|
||||||
|
regex = "1"
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
serde_dhall = "0.11.1"
|
serde_dhall = "0.11.1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
---
|
||||||
|
title: "Site Update: Hero Images"
|
||||||
|
date: 2022-06-08
|
||||||
|
---
|
||||||
|
|
||||||
|
For a while I've been wondering how I can add dramatic flair to my website with
|
||||||
|
so-called "hero images". These images are tools that let you describe the mood a
|
||||||
|
website wants to evoke. I've been unsure how to best implement these on my
|
||||||
|
website for a while, but with the advent of MidJourney and other image
|
||||||
|
generation APIs/algorithms I think I have found a way to create these without
|
||||||
|
too much effort on my part and the results are pretty fantastic:
|
||||||
|
|
||||||
|
<xeblog-hero file="secret-to-life" prompt="the secret to life, the universe and everything, concept art"></xeblog-hero>
|
||||||
|
|
||||||
|
I have generated a bunch of other images that I'm going to use for my other
|
||||||
|
posts. I'll give out a desktop wallpaper sized version of each of these images
|
||||||
|
on my [Patreon](https://patreon.com/cadey).
|
||||||
|
|
||||||
|
Under the hood this is powered by
|
||||||
|
[lol_html](https://github.com/cloudflare/lol-html) and
|
||||||
|
[Maud](https://maud.lambda.xyz/). The magic is mostly contained in a function
|
||||||
|
that generates a `<figure>` HTML element (which I just learned exists today). I
|
||||||
|
use a function that looks like this for generating the `<xeblog-hero>` snippets:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn xeblog_hero(file: String, prompt: Option<String>) -> Markup {
|
||||||
|
html! {
|
||||||
|
figure.hero style="margin:0" {
|
||||||
|
picture style="margin:0" {
|
||||||
|
source type="image/avif" srcset={"https://cdn.xeiaso.net/file/christine-static/hero/" (file) ".avif"};
|
||||||
|
source type="image/webp" srcset={"https://cdn.xeiaso.net/file/christine-static/hero/" (file) ".webp"};
|
||||||
|
img style="padding:0" alt={"hero image " (file)} src={"https://cdn.xeiaso.net/file/christine-static/hero/" (file) "-smol.png"};
|
||||||
|
}
|
||||||
|
figcaption { "Image generated by MidJourney" @if let Some(prompt) = prompt { " -- " (prompt) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
I have it wired up with lol_html like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
lol_html::element!("xeblog-hero", |el| {
|
||||||
|
let file = el.get_attribute("file").expect("wanted xeblog-hero to contain file");
|
||||||
|
el.replace(&crate::tmpl::xeblog_hero(file, el.get_attribute("prompt")).0, ContentType::Html);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The result is that I can declare hero images with HTML fragments like this:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<xeblog-hero file="miku-dark-souls" prompt="hatsune miku, elden ring, dark souls, concept art, crowbar"></xeblog-hero>
|
||||||
|
```
|
||||||
|
|
||||||
|
And I get this:
|
||||||
|
|
||||||
|
<xeblog-hero file="miku-dark-souls" prompt="hatsune miku, elden ring, dark souls, concept art, crowbar"></xeblog-hero>
|
||||||
|
|
||||||
|
<xeblog-conv name="Mara" mood="hacker">This is powered by the
|
||||||
|
[`<figure>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure)
|
||||||
|
tag, which is a new discovery to us. This is probably one of the most useful
|
||||||
|
tags we never knew about and removed the need to write a bunch of annoying CSS
|
||||||
|
and HTML.</xeblog-conv>
|
||||||
|
|
||||||
|
The webp and AVIF versions of the hero images have a higher resolution version
|
||||||
|
so that it looks nicer on retina screens. However, the png versions of these are
|
||||||
|
locked to a resolution of 800x356 pixels because I was unable to crush them
|
||||||
|
below a size of half a megabyte at full resolution. Realistically, this should
|
||||||
|
only affect older browsers on slower hardware, so I don't expect this to have
|
||||||
|
too much impact on most users.
|
||||||
|
|
||||||
|
<xeblog-conv name="Cadey" mood="coffee">If you don't want to see these hero
|
||||||
|
images, you can remove them with a userstyle like this:
|
||||||
|
```css
|
||||||
|
figure.hero {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</xeblog-conv>
|
||||||
|
|
||||||
|
I'm likely going to convert over most of my website templates to use Maud. I'm
|
||||||
|
very happy with it and I think it is incredibly useful to express your HTML in
|
||||||
|
Rust instead of something that has to be compiled to Rust. In practice it
|
||||||
|
reminds me of the Nim library [emerald](http://flyx.github.io/emerald/), which
|
||||||
|
lets you write HTML using Nim functions similar to how you use Maud.
|
||||||
|
|
||||||
|
Here's a few more examples of hero images I have generated:
|
||||||
|
|
||||||
|
<xeblog-hero file="the-forbidden-shape" prompt="the forbidden shape"></xeblog-hero>
|
||||||
|
|
||||||
|
<xeblog-hero file="great-wave-cyberpunk" prompt="the great wave off of kanagawa, cyberpunk, hanzi inscription"></xeblog-hero>
|
||||||
|
|
||||||
|
Normally I will only have one image per post and it will usually be after the
|
||||||
|
introduction paragraph. The prompt will usually be related to the article topic,
|
||||||
|
but sometimes I will take artistic liberty. If you have suggestions for prompts,
|
||||||
|
please [contact me](/contact) with those ideas.
|
||||||
|
|
||||||
|
I hope these updates on how I've been messing with my site are interesting. I'm
|
||||||
|
trying to capture the spirit of how I'm implementing these changes as well as
|
||||||
|
details of how everything fits together.
|
|
@ -7,7 +7,7 @@ use comrak::{
|
||||||
ComrakPlugins,
|
ComrakPlugins,
|
||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use lol_html::{rewrite_str, element, RewriteStrSettings, html_content::ContentType};
|
use lol_html::{element, html_content::ContentType, rewrite_str, RewriteStrSettings};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -95,6 +95,11 @@ pub fn render(inp: &str) -> Result<String> {
|
||||||
|
|
||||||
el.remove_and_keep_content();
|
el.remove_and_keep_content();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}),
|
||||||
|
element!("xeblog-hero", |el| {
|
||||||
|
let file = el.get_attribute("file").expect("wanted xeblog-hero to contain file");
|
||||||
|
el.replace(&crate::tmpl::xeblog_hero(file, el.get_attribute("prompt")).0, ContentType::Html);
|
||||||
|
Ok(())
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
..RewriteStrSettings::default()
|
..RewriteStrSettings::default()
|
||||||
|
|
|
@ -4,6 +4,7 @@ use axum::{
|
||||||
extract::{Extension, Path},
|
extract::{Extension, Path},
|
||||||
response::Html,
|
response::Html,
|
||||||
};
|
};
|
||||||
|
use http::HeaderMap;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use prometheus::{opts, register_int_counter_vec, IntCounterVec};
|
use prometheus::{opts, register_int_counter_vec, IntCounterVec};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -72,10 +73,11 @@ pub async fn series_view(
|
||||||
Ok(Html(result))
|
Ok(Html(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state, headers))]
|
||||||
pub async fn post_view(
|
pub async fn post_view(
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
Extension(state): Extension<Arc<State>>,
|
Extension(state): Extension<Arc<State>>,
|
||||||
|
headers: HeaderMap,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
let mut want: Option<Post> = None;
|
let mut want: Option<Post> = None;
|
||||||
|
|
||||||
|
@ -85,6 +87,13 @@ pub async fn post_view(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let referer = if let Some(referer) = headers.get(http::header::REFERER) {
|
||||||
|
let referer = referer.to_str()?.to_string();
|
||||||
|
Some(referer)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
match want {
|
match want {
|
||||||
None => Err(super::Error::PostNotFound(name)),
|
None => Err(super::Error::PostNotFound(name)),
|
||||||
Some(post) => {
|
Some(post) => {
|
||||||
|
@ -93,7 +102,7 @@ pub async fn post_view(
|
||||||
.inc();
|
.inc();
|
||||||
let body = templates::Html(post.body_html.clone());
|
let body = templates::Html(post.body_html.clone());
|
||||||
let mut result: Vec<u8> = vec![];
|
let mut result: Vec<u8> = vec![];
|
||||||
templates::blogpost_html(&mut result, post, body)?;
|
templates::blogpost_html(&mut result, post, body, referer)?;
|
||||||
Ok(Html(result))
|
Ok(Html(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,6 +130,9 @@ pub enum Error {
|
||||||
|
|
||||||
#[error("axum http error: {0}")]
|
#[error("axum http error: {0}")]
|
||||||
AxumHTTP(#[from] axum::http::Error),
|
AxumHTTP(#[from] axum::http::Error),
|
||||||
|
|
||||||
|
#[error("string conversion error: {0}")]
|
||||||
|
ToStr(#[from] http::header::ToStrError),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T = Html<Vec<u8>>> = std::result::Result<T, Error>;
|
pub type Result<T = Html<Vec<u8>>> = std::result::Result<T, Error>;
|
||||||
|
|
|
@ -30,6 +30,7 @@ pub mod app;
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod signalboost;
|
pub mod signalboost;
|
||||||
|
pub mod tmpl;
|
||||||
|
|
||||||
mod domainsocket;
|
mod domainsocket;
|
||||||
use domainsocket::*;
|
use domainsocket::*;
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
use maud::{html, Markup};
|
||||||
|
|
||||||
|
pub mod nag;
|
||||||
|
|
||||||
|
pub fn xeblog_hero(file: String, prompt: Option<String>) -> Markup {
|
||||||
|
html! {
|
||||||
|
figure.hero style="margin:0" {
|
||||||
|
picture style="margin:0" {
|
||||||
|
source type="image/avif" srcset={"https://cdn.xeiaso.net/file/christine-static/hero/" (file) ".avif"};
|
||||||
|
source type="image/webp" srcset={"https://cdn.xeiaso.net/file/christine-static/hero/" (file) ".webp"};
|
||||||
|
img style="padding:0" alt={"hero image " (file)} src={"https://cdn.xeiaso.net/file/christine-static/hero/" (file) "-smol.png"};
|
||||||
|
}
|
||||||
|
figcaption { "Image generated by MidJourney" @if let Some(prompt) = prompt { " -- " (prompt) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn xeblog_conv(name: String, mood: String, body: Markup) -> Markup {
|
||||||
|
let name_lower = name.clone().to_lowercase();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
.conversation {
|
||||||
|
."conversation-picture"."conversation-smol" {
|
||||||
|
picture {
|
||||||
|
source type="image/avif" srcset={"https://cdn.xeiaso.net/file/christine-static/stickers/" (name_lower) "/" (mood) ".avif"};
|
||||||
|
source type="image/webp" srcset={"https://cdn.xeiaso.net/file/christine-static/stickers/" (name_lower) "/" (mood) ".webp"};
|
||||||
|
img alt={(name) " is " (mood)} src={"https://cdn.xeiaso.net/file/christine-static/stickers/" (name_lower) "/" (mood) ".png"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
."conversation-chat" {
|
||||||
|
"<"
|
||||||
|
b { (name) }
|
||||||
|
"> "
|
||||||
|
(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
use super::xeblog_conv;
|
||||||
|
use crate::post::Post;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use maud::{html, Markup};
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref HN: Regex = Regex::new(r#"^https?://news.ycombinator.com"#).unwrap();
|
||||||
|
static ref REDDIT: Regex = Regex::new(r#"^https?://((.+).)?reddit.com"#).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn referer(referer: Option<String>) -> Markup {
|
||||||
|
if referer.is_none() {
|
||||||
|
return html! {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let referer = referer.unwrap();
|
||||||
|
|
||||||
|
let nag = html! {
|
||||||
|
script r#async src="https://media.ethicalads.io/media/client/ethicalads.min.js" { "" }
|
||||||
|
div.adaptive data-ea-publisher="christinewebsite" data-ea-type="image" data-ea-style="stickybox" {
|
||||||
|
.warning {
|
||||||
|
(xeblog_conv(
|
||||||
|
"Cadey".into(),
|
||||||
|
"coffee".into(),
|
||||||
|
html! {
|
||||||
|
"Hello! Thank you for visiting my website. You seem to be visiting from a news aggregator and have ads disabled. These ads help pay for running the website and are done by "
|
||||||
|
a href="https://www.ethicalads.io/" { "Ethical Ads" }
|
||||||
|
". I do not receive detailed analytics on the ads and from what I understand neither does Ethical Ads. If you don't want to disable your ad blocker, please consider donating on "
|
||||||
|
a href="https://patreon.com/cadey" { "Patreon" }
|
||||||
|
". It helps fund the website's hosting bills and pay for the expensive technical editor that I use for my longer articles. Thanks and be well!"
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if HN.is_match(&referer) {
|
||||||
|
return nag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if REDDIT.is_match(&referer) {
|
||||||
|
return nag;
|
||||||
|
}
|
||||||
|
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prerelease(post: &Post) -> Markup {
|
||||||
|
if Utc::today().num_days_from_ce() < post.date.num_days_from_ce() {
|
||||||
|
html! {
|
||||||
|
.warning {
|
||||||
|
(xeblog_conv("Mara".into(), "hacker".into(), html!{
|
||||||
|
"Hey, this post is set to go live on "
|
||||||
|
(format!("{}", post.detri()))
|
||||||
|
" UTC. Right now you are reading a pre-publication version of this post. Please do not share this on social media. This post will automatically go live for everyone on the intended publication date. If you want access to these posts, please join the "
|
||||||
|
a href="https://patreon.com/cadey" { "Patreon" }
|
||||||
|
". It helps me afford the copyeditor that I contract for the technical content I write."
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
@use super::{header_html, footer_html, mara};
|
@use super::{header_html, footer_html};
|
||||||
@use crate::post::Post;
|
@use crate::{post::Post, tmpl::nag};
|
||||||
@use chrono::prelude::*;
|
|
||||||
|
|
||||||
@(post: Post, body: impl ToHtml)
|
@(post: Post, body: impl ToHtml, referer: Option<String>)
|
||||||
|
|
||||||
@:header_html(Some(&post.front_matter.title.clone()), None)
|
@:header_html(Some(&post.front_matter.title.clone()), None)
|
||||||
|
|
||||||
|
@ -27,8 +26,6 @@
|
||||||
<link rel="canonical" href="@post.front_matter.redirect_to.as_ref().unwrap()" />
|
<link rel="canonical" href="@post.front_matter.redirect_to.as_ref().unwrap()" />
|
||||||
}
|
}
|
||||||
|
|
||||||
<script type="module" src="/static/js/conversation.js"></script>
|
|
||||||
|
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
@{
|
@{
|
||||||
"@@context": "http://schema.org",
|
"@@context": "http://schema.org",
|
||||||
|
@ -59,25 +56,16 @@
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div id="refererNotice"></div>
|
@Html(nag::referer(referer).0)
|
||||||
<script type="module" src="/static/js/hnwarn.js"></script>
|
|
||||||
|
|
||||||
<h1>@post.front_matter.title</h1>
|
<h1>@post.front_matter.title</h1>
|
||||||
|
|
||||||
@if Utc::today().num_days_from_ce() < post.date.num_days_from_ce() {
|
@Html(nag::prerelease(&post).0)
|
||||||
<div class="warning">
|
|
||||||
@:mara("hacker", "Mara", Html(format!(r#"Hey, this post is set to go live to the public on {} UTC. Right now you are reading a pre-publication version of this post. Please do not share this on social media. This post will automatically go live for everyone on the intended publication date. If you want access to these posts, please join the <a href="https://patreon.com/cadey">Patreon</a>. It helps me afford the copyeditor that I contract for the technical content I write."#, post.detri())))
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<script async src="https://media.ethicalads.io/media/client/ethicalads.min.js"></script>
|
|
||||||
}
|
|
||||||
|
|
||||||
<small>A @post.read_time_estimate_minutes minute read.</small>
|
<small>A @post.read_time_estimate_minutes minute read.</small>
|
||||||
|
|
||||||
@body
|
@body
|
||||||
|
|
||||||
<div class="adaptive" data-ea-publisher="christinewebsite" data-ea-type="image" data-ea-style="stickybox"></div>
|
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
@if post.front_matter.vod.is_some() {
|
@if post.front_matter.vod.is_some() {
|
||||||
|
|
Loading…
Reference in New Issue