The code and data behind xeiaso.net
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

add stream VOD page

Signed-off-by: Xe Iaso <me@christine.website>

Xe Iaso c117eae7 757cee6f

+353 -7
+1 -2
Cargo.toml
··· 49 49 url = "2" 50 50 uuid = { version = "0.8", features = ["serde", "v4"] } 51 51 52 - xesite_types = { path = "./lib/xesite_types" } 53 - 54 52 # workspace dependencies 55 53 mastodon2text = { path = "./lib/mastodon2text" } 56 54 mi = { path = "./lib/mi" } ··· 58 56 xe_jsonfeed = { path = "./lib/jsonfeed" } 59 57 xesite_markdown = { path = "./lib/xesite_markdown" } 60 58 xesite_templates = { path = "./lib/xesite_templates" } 59 + xesite_types = { path = "./lib/xesite_types" } 61 60 62 61 [dependencies.maud] 63 62 git = "https://github.com/Xe/maud"
+1
dhall/package.dhall
··· 83 83 ] 84 84 , pronouns = ./pronouns.dhall 85 85 , characters = ./characters.dhall 86 + , vods = ./streamVOD.dhall 86 87 }
+74
dhall/streamVOD.dhall
··· 1 + let xesite = ./types/package.dhall 2 + 3 + let VOD = xesite.StreamVOD 4 + 5 + in [ VOD::{ 6 + , title = "Fixing Xesite in reader mode and RSS readers" 7 + , slug = "reader-mode-css" 8 + , description = 9 + '' 10 + When you are using reader mode in Firefox, Safari or Google Chrome, the browser rends control of the website's design and renders its own design. This is typically done in order to prevent people's bad design decisions from making webpages unreadable and also to strip away advertisements from content. As a website publisher, I rely on the ability to control the CSS of my blog a lot. This stream covers the research/implementation process for fixing some long-standing issues with the Xesite CSS and making a fix to XeDN so that the site renders acceptably in reader mode. 11 + 12 + This stream covers the following topics: 13 + 14 + * Understanding complicated CSS rules and creating fixes for issues with them 15 + * Using content distribution networks (CDNs) to help reduce page load time for readers 16 + * Implementing image resizing capabilities into an existing CDN program (XeDN) 17 + * Design with end-users in mind 18 + '' 19 + , date = "2022-01-21" 20 + , cdnPath = "talks/vod/2023/01-21-reader-mode" 21 + , tags = [ "css", "xedn", "imageProcessing", "scalability", "bugFix" ] 22 + } 23 + , VOD::{ 24 + , title = "Implementing the Pronouns service in Rust and Axum" 25 + , slug = "pronouns-service" 26 + , description = 27 + '' 28 + In this stream I implemented the [pronouns](https://pronouns.within.lgbt) service and deployed it to the cloud with [fly.io](https://fly.io). This was mostly writing a bunch of data files with [Dhall](https://dhall-lang.org) and then writing a simple Rust program to query that 'database' and then show results based on the results of those queries. 29 + 30 + This stream covers the following topics: 31 + 32 + * Starting a new Rust project from scratch with Nix flakes, Axum, and Maud 33 + * API design for human and machine-paresable outputs 34 + * DevOps deployment to the cloud via [fly.io](https://fly.io) 35 + * Writing Terraform code for the pronouns service 36 + * Building Docker images with Nix flakes and `pkgs.dockerTools.buildLayeredImage` 37 + * Writing API documentation 38 + * Writing [the writeup](https://xeiaso.net/blog/pronouns-service) on the service 39 + '' 40 + , date = "2022-01-07" 41 + , cdnPath = "talks/vod/2023/01-07-pronouns" 42 + , tags = [ "rust", "axum", "terraform", "nix", "flyio", "docker" ] 43 + } 44 + , VOD::{ 45 + , title = "Modernizing hlang with the nguh compiler" 46 + , slug = "hlang-nguh-compiler" 47 + , description = 48 + '' 49 + This stream was the last stream of 2022 and focused on modernizing the [hlang](https://xeiaso.net/blog/series/h) compiler. In this stream I reverse-engineered how WebAssembly modules work and wrote my own compiler for a trivial esoteric programming language named h. The existing compiler relied on legacy features of WebAssembly tools that don't work anymore. 50 + 51 + This stream covers the following topics: 52 + 53 + * Reverse-engineering the WebAssembly module format based on the specification and other reverse-engineering tools 54 + * Adapting an existing compiler to output WebAssembly directly 55 + * Deploying a new service to my NixOS machines in the cloud 56 + * Building a Nix flake and custom NixOS module to build and deploy the new hlang website 57 + * Terraform DNS config 58 + * Writing [the writeup on the new compiler](https://xeiaso.net/blog/hlang-nguh) 59 + '' 60 + , date = "2022-12-31" 61 + , cdnPath = "talks/vod/2022/12-31-nguh" 62 + , tags = 63 + [ "hlang" 64 + , "go" 65 + , "wasm" 66 + , "philosophy" 67 + , "devops" 68 + , "terraform" 69 + , "aws" 70 + , "route53" 71 + , "nixos" 72 + ] 73 + } 74 + ]
+4
dhall/types/Config.dhall
··· 12 12 13 13 let SeriesDescription = ./SeriesDescription.dhall 14 14 15 + let VOD = ./StreamVOD.dhall 16 + 15 17 let PronounSet = ./PronounSet.dhall 16 18 17 19 let Prelude = ../Prelude.dhall ··· 37 39 , contactLinks : List Link.Type 38 40 , pronouns : List PronounSet.Type 39 41 , characters : List Character.Type 42 + , vods : List VOD.Type 40 43 } 41 44 , default = 42 45 { signalboost = [] : List Person.Type ··· 53 56 , contactLinks = [] : List Link.Type 54 57 , pronouns = [] : List PronounSet.Type 55 58 , characters = [] : List Character.Type 59 + , vods = [] : List VOD.Type 56 60 } 57 61 }
+19
dhall/types/StreamVOD.dhall
··· 1 + let Link = ./Link.dhall 2 + 3 + in { Type = 4 + { title : Text 5 + , slug : Text 6 + , date : Text 7 + , description : Text 8 + , cdnPath : Text 9 + , tags : List Text 10 + } 11 + , default = 12 + { title = "" 13 + , slug = "" 14 + , date = "" 15 + , description = "" 16 + , cdnPath = "" 17 + , tags = [] : List Text 18 + } 19 + }
+1
dhall/types/package.dhall
··· 13 13 , SeriesDescription = ./SeriesDescription.dhall 14 14 , Stock = ./Stock.dhall 15 15 , StockKind = ./StockKind.dhall 16 + , StreamVOD = ./StreamVOD.dhall 16 17 }
+59 -1
src/app/config.rs
··· 1 1 use crate::signalboost::Person; 2 - use maud::{html, Markup, Render}; 2 + use chrono::prelude::*; 3 + use maud::{html, Markup, PreEscaped, Render}; 3 4 use serde::{Deserialize, Serialize}; 4 5 use std::{ 5 6 collections::HashMap, 6 7 fmt::{self, Display}, 7 8 }; 9 + 10 + mod markdown_string; 11 + use markdown_string::MarkdownString; 8 12 9 13 #[derive(Clone, Deserialize, Default)] 10 14 pub struct Config { ··· 29 33 pub contact_links: Vec<Link>, 30 34 pub pronouns: Vec<PronounSet>, 31 35 pub characters: Vec<Character>, 36 + pub vods: Vec<VOD>, 32 37 } 33 38 34 39 #[derive(Clone, Deserialize, Serialize, Default)] ··· 336 341 } 337 342 } 338 343 } 344 + 345 + #[derive(Clone, Deserialize, Serialize, Default)] 346 + pub struct VOD { 347 + pub title: String, 348 + pub slug: String, 349 + pub date: NaiveDate, 350 + pub description: MarkdownString, 351 + #[serde(rename = "cdnPath")] 352 + pub cdn_path: String, 353 + pub tags: Vec<String>, 354 + } 355 + 356 + impl VOD { 357 + pub fn detri(&self) -> String { 358 + self.date.format("M%m %d %Y").to_string() 359 + } 360 + } 361 + 362 + impl Render for VOD { 363 + fn render(&self) -> Markup { 364 + html! { 365 + meta name="twitter:card" content="summary"; 366 + meta name="twitter:site" content="@theprincessxena"; 367 + meta name="twitter:title" content={(self.title)}; 368 + meta property="og:type" content="website"; 369 + meta property="og:title" content={(self.title)}; 370 + meta property="og:site_name" content="Xe's Blog"; 371 + meta name="description" content={(self.title) " - Xe's Blog"}; 372 + meta name="author" content="Xe Iaso"; 373 + 374 + h1 {(self.title)} 375 + small {"Streamed on " (self.detri())} 376 + 377 + (xesite_templates::advertiser_nag(Some(html!{ 378 + (xesite_templates::conv("Cadey".into(), "coffee".into(), html!{ 379 + "Hi. This page embeds a video file that is potentially multiple hours long. Hosting this stuff is not free. Bandwidth in particular is expensive. If you really want to continue to block ads, please consider donating via " 380 + a href="https://patreon.com/cadey" {"Patreon"} 381 + " because servers and bandwidth do not grow on trees." 382 + })) 383 + }))) 384 + 385 + (xesite_templates::video(self.cdn_path.clone())) 386 + (self.description) 387 + p { 388 + "Tags: " 389 + @for tag in &self.tags { 390 + code{(tag)} 391 + " " 392 + } 393 + } 394 + } 395 + } 396 + }
+64
src/app/config/markdown_string.rs
··· 1 + use std::fmt; 2 + 3 + use maud::{html, Markup, PreEscaped, Render}; 4 + use serde::{ 5 + de::{self, Visitor}, 6 + Deserialize, Deserializer, Serialize, 7 + }; 8 + 9 + struct StringVisitor; 10 + 11 + impl<'de> Visitor<'de> for StringVisitor { 12 + type Value = MarkdownString; 13 + 14 + fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E> 15 + where 16 + E: de::Error, 17 + { 18 + Ok(MarkdownString(xesite_markdown::render(value).map_err( 19 + |why| de::Error::invalid_value(de::Unexpected::Other(&format!("{why}")), &self), 20 + )?)) 21 + } 22 + 23 + fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> 24 + where 25 + E: de::Error, 26 + { 27 + Ok(MarkdownString(xesite_markdown::render(value).map_err( 28 + |why| de::Error::invalid_value(de::Unexpected::Other(&format!("{why}")), &self), 29 + )?)) 30 + } 31 + 32 + fn visit_string<E>(self, value: String) -> Result<Self::Value, E> 33 + where 34 + E: de::Error, 35 + { 36 + Ok(MarkdownString(xesite_markdown::render(&value).map_err( 37 + |why| de::Error::invalid_value(de::Unexpected::Other(&format!("{why}")), &self), 38 + )?)) 39 + } 40 + 41 + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 42 + formatter.write_str("a string with xesite-flavored markdown") 43 + } 44 + } 45 + 46 + #[derive(Serialize, Clone, Default)] 47 + pub struct MarkdownString(String); 48 + 49 + impl<'de> Deserialize<'de> for MarkdownString { 50 + fn deserialize<D>(deserializer: D) -> Result<MarkdownString, D::Error> 51 + where 52 + D: Deserializer<'de>, 53 + { 54 + deserializer.deserialize_string(StringVisitor) 55 + } 56 + } 57 + 58 + impl Render for MarkdownString { 59 + fn render(&self) -> Markup { 60 + html! { 61 + (PreEscaped(&self.0)) 62 + } 63 + } 64 + }
+27
src/frontend/components/ConvSnippet.tsx
··· 1 + export interface ConvSnippetProps { 2 + name: string; 3 + mood: string; 4 + children: HTMLElement[]; 5 + } 6 + 7 + const ConvSnippet = ({name, mood, children}: ConvSnippetProps) => { 8 + const nameLower = name.toLowerCase(); 9 + name = name.replace(" ", "_"); 10 + 11 + return ( 12 + <div className="conversation"> 13 + <div className="conversation-standalone"> 14 + <picture> 15 + <source type="image/avif" srcset={`https://cdn.xeiaso.net/file/christine-static/stickers/${nameLower}/${mood}.avif`} /> 16 + <source type="image/webp" srcset={`https://cdn.xeiaso.net/file/christine-static/stickers/${nameLower}/${mood}.webp`} /> 17 + <img style="max-height:4.5rem" alt={`${name} is ${mood}`} loading="lazy" src={`https://cdn.xeiaso.net/file/christine-static/stickers/${nameLower}/${mood}.png`} /> 18 + </picture> 19 + </div> 20 + <div className="conversation-chat"> 21 + &lt;<a href={`/characters#${nameLower}`}><b>{name}</b></a>&gt; {children} 22 + </div> 23 + </div> 24 + ); 25 + }; 26 + 27 + export default ConvSnippet;
+1
src/handlers/mod.rs
··· 16 16 pub mod blog; 17 17 pub mod feeds; 18 18 pub mod gallery; 19 + pub mod streams; 19 20 pub mod talks; 20 21 21 22 fn weekday_to_name(w: Weekday) -> &'static str {
+94
src/handlers/streams.rs
··· 1 + use crate::{ 2 + app::{State, VOD}, 3 + tmpl::{base, nag}, 4 + }; 5 + use axum::{extract::Path, Extension}; 6 + use chrono::prelude::*; 7 + use http::StatusCode; 8 + use lazy_static::lazy_static; 9 + use maud::{html, Markup, Render}; 10 + use prometheus::{opts, register_int_counter_vec, IntCounterVec}; 11 + use serde::{Deserialize, Serialize}; 12 + use std::sync::Arc; 13 + 14 + lazy_static! { 15 + static ref HIT_COUNTER: IntCounterVec = register_int_counter_vec!( 16 + opts!("streams_hits", "Number of hits to stream vod pages"), 17 + &["name"] 18 + ) 19 + .unwrap(); 20 + } 21 + 22 + pub async fn list(Extension(state): Extension<Arc<State>>) -> Markup { 23 + let state = state.clone(); 24 + let cfg = state.cfg.clone(); 25 + 26 + crate::tmpl::base( 27 + Some("Stream VODs"), 28 + None, 29 + html! { 30 + h1 {"Stream VODs"} 31 + p { 32 + "I'm a VTuber and I stream every other weekend on " 33 + a href="https://twitch.tv/princessxen" {"Twitch"} 34 + " about technology, the weird art of programming, and sometimes video games. This page will contain copies of my stream recordings/VODs so that you can watch your favorite stream again. All VOD pages support picture-in-picture mode so that you can have the recordings open in the background while you do something else." 35 + } 36 + p { 37 + "Please note that to save on filesize, all videos are rendered at 720p and optimized for viewing at that resolution or on most mobile phone screens. If you run into video quality issues, please contact me as I am still trying to find the correct balance between video quality and filesize. These videos have been tested and known to work on most of the browser and OS combinations that visit this site." 38 + } 39 + ul { 40 + @for vod in &cfg.vods { 41 + li { 42 + (vod.detri()) 43 + " - " 44 + a href={ 45 + "/vods/" 46 + (vod.date.year()) 47 + "/" 48 + (vod.date.month()) 49 + "/" 50 + (vod.slug) 51 + } {(vod.title)} 52 + } 53 + } 54 + } 55 + }, 56 + ) 57 + } 58 + 59 + #[derive(Serialize, Deserialize)] 60 + pub struct ShowArgs { 61 + pub year: i32, 62 + pub month: u32, 63 + pub slug: String, 64 + } 65 + 66 + pub async fn show( 67 + Extension(state): Extension<Arc<State>>, 68 + Path(args): Path<ShowArgs>, 69 + ) -> (StatusCode, Markup) { 70 + let state = state.clone(); 71 + let cfg = state.cfg.clone(); 72 + 73 + let mut found: Option<&VOD> = None; 74 + 75 + for vod in &cfg.vods { 76 + if vod.date.year() == args.year && vod.date.month() == args.month && vod.slug == args.slug { 77 + found = Some(vod); 78 + } 79 + } 80 + 81 + if found.is_none() { 82 + return ( 83 + StatusCode::NOT_FOUND, 84 + crate::tmpl::error(html! { 85 + "What you requested may not exist. Good luck." 86 + }), 87 + ); 88 + } 89 + 90 + let vod = found.unwrap(); 91 + HIT_COUNTER.with_label_values(&[&vod.slug]).inc(); 92 + 93 + (StatusCode::OK, base(Some(&vod.title), None, vod.render())) 94 + }
+4
src/main.rs
··· 175 175 .route("/signalboost", get(handlers::signalboost)) 176 176 .route("/salary-transparency", get(handlers::salary_transparency)) 177 177 .route("/pronouns", get(handlers::pronouns)) 178 + // vods 179 + .route("/vods", get(handlers::streams::list)) 180 + .route("/vods/", get(handlers::streams::list)) 181 + .route("/vods/:year/:month/:slug", get(handlers::streams::show)) 178 182 // feeds 179 183 .route("/blog.json", get(handlers::feeds::jsonfeed)) 180 184 .route("/blog.atom", get(handlers::feeds::atom))
+3 -3
src/tmpl/blog.rs
··· 41 41 @if let Some(vod) = &post.front_matter.vod { 42 42 p { 43 43 "This post was written live on " 44 - a href="https://twitch.tv/princessxen" {"Twitch"} 44 + a href="https://twitch.tv/princessxen" {"Twitch"} 45 45 ". You can check out the stream recording on " 46 - a href=(vod.twitch) {"Twitch"} 46 + a href=(vod.twitch) {"Twitch"} 47 47 " and on " 48 - a href=(vod.youtube) {"YouTube"} 48 + a href=(vod.youtube) {"YouTube"} 49 49 ". If you are reading this in the first day or so of this post being published, you will need to watch it on Twitch." 50 50 } 51 51 }
+1 -1
src/tmpl/mod.rs
··· 80 80 " - " 81 81 a href="/signalboost" { "Signal Boost" } 82 82 " - " 83 - a href="/feeds" { "Feeds" } 83 + a href="/vods" { "VODs" } 84 84 " | " 85 85 a target="_blank" rel="noopener noreferrer" href="https://graphviz.christine.website" { "Graphviz" } 86 86 " - "