The code and data behind xeiaso.net
5
fork

Configure Feed

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

add _xesite_frontmatter extension

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

Xe Iaso 7f6de2cb 8b6056fc

+179 -95
+10
Cargo.lock
··· 3205 3205 "serde", 3206 3206 "serde_derive", 3207 3207 "serde_json", 3208 + "xesite_types", 3208 3209 ] 3209 3210 3210 3211 [[package]] ··· 3260 3261 "url", 3261 3262 "uuid 0.8.2", 3262 3263 "xe_jsonfeed", 3264 + "xesite_types", 3263 3265 "xml-rs", 3266 + ] 3267 + 3268 + [[package]] 3269 + name = "xesite_types" 3270 + version = "0.1.0" 3271 + dependencies = [ 3272 + "chrono", 3273 + "serde", 3264 3274 ] 3265 3275 3266 3276 [[package]]
+2
Cargo.toml
··· 48 48 url = "2" 49 49 uuid = { version = "0.8", features = ["serde", "v4"] } 50 50 51 + xesite_types = { path = "./lib/xesite_types" } 52 + 51 53 # workspace dependencies 52 54 cfcache = { path = "./lib/cfcache" } 53 55 xe_jsonfeed = { path = "./lib/jsonfeed" }
+27
docs/jsonfeed_extensions.markdown
··· 1 + # JSON Feed Extensions 2 + 3 + Here is the documentation of all of my JSON Feed extensions. I have created 4 + these JSON Feed extensions in order to give users more metadata about my 5 + articles and talks. 6 + 7 + ## `_xesite_frontmatter` 8 + 9 + This extension is added to [JSON Feed 10 + Items](https://www.jsonfeed.org/version/1.1/#items-a-name-items-a) and gives 11 + readers a copy of the frontmatter data that I annotate my posts with. The 12 + contents of this will vary by post, but will have any of the following fields: 13 + 14 + * `about` (required, string) is a link to this documentation. It gives readers 15 + of the JSON Feed information about what this extension does. This is for 16 + informational purposes only and can safely be ignored by programs. 17 + * `series` (optional, string) is the optional blogpost series name that this 18 + item belongs to. When I post multiple posts about the same topic, I will 19 + usually set the `series` to the same value so that it is more discoverable [on 20 + my series index page](https://xeiaso.net/blog/series). 21 + * `slides_link` (optional, string) is a link to the PDF containing the slides 22 + for a given talk. This is always set on talks, but is technically optional 23 + because not everything I do is a talk. 24 + * `vod` (optional, string) is an object that describes where you can watch the 25 + Video On Demand (vod) for the writing process of a post. This is an object 26 + that always contains the fields `twitch` and `youtube`. These will be URLs to 27 + the videos so that you can watch them on demand.
+2
lib/jsonfeed/Cargo.toml
··· 13 13 serde = "1" 14 14 serde_derive = "1" 15 15 serde_json = "1" 16 + 17 + xesite_types = { path = "../xesite_types" }
+8
lib/jsonfeed/src/builder.rs
··· 90 90 pub author: Option<Author>, 91 91 pub tags: Option<Vec<String>>, 92 92 pub attachments: Option<Vec<Attachment>>, 93 + pub xesite_frontmater: Option<xesite_types::Frontmatter>, 93 94 } 94 95 95 96 impl ItemBuilder { ··· 108 109 author: None, 109 110 tags: None, 110 111 attachments: None, 112 + xesite_frontmater: None, 111 113 } 112 114 } 113 115 ··· 180 182 self 181 183 } 182 184 185 + pub fn xesite_frontmatter(mut self, fm: xesite_types::Frontmatter) -> ItemBuilder { 186 + self.xesite_frontmater = Some(fm); 187 + self 188 + } 189 + 183 190 pub fn build(self) -> Result<Item> { 184 191 if self.id.is_none() || self.content.is_none() { 185 192 return Err("missing field 'id' or 'content_*'".into()); ··· 198 205 author: self.author, 199 206 tags: self.tags, 200 207 attachments: self.attachments, 208 + xesite_frontmatter: self.xesite_frontmater, 201 209 }) 202 210 } 203 211 }
+9
lib/jsonfeed/src/item.rs
··· 1 + use std::collections::HashMap; 1 2 use std::default::Default; 2 3 use std::fmt; 3 4 ··· 31 32 pub author: Option<Author>, 32 33 pub tags: Option<Vec<String>>, 33 34 pub attachments: Option<Vec<Attachment>>, 35 + 36 + // xesite extensions 37 + pub xesite_frontmatter: Option<xesite_types::Frontmatter>, 34 38 } 35 39 36 40 impl Item { ··· 55 59 author: None, 56 60 tags: None, 57 61 attachments: None, 62 + xesite_frontmatter: None, 58 63 } 59 64 } 60 65 } ··· 112 117 } 113 118 if self.attachments.is_some() { 114 119 state.serialize_field("attachments", &self.attachments)?; 120 + } 121 + if self.xesite_frontmatter.is_some() { 122 + state.serialize_field("_xesite_frontmatter", &self.xesite_frontmatter)?; 115 123 } 116 124 state.end() 117 125 } ··· 319 327 author, 320 328 tags, 321 329 attachments, 330 + xesite_frontmatter: None, 322 331 }) 323 332 } 324 333 }
+10
lib/xesite_types/Cargo.toml
··· 1 + [package] 2 + name = "xesite_types" 3 + version = "0.1.0" 4 + edition = "2021" 5 + 6 + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 + 8 + [dependencies] 9 + chrono = { version = "0.4", features = [ "serde" ] } 10 + serde = { version = "1.0", features = [ "derive" ] }
+37
lib/xesite_types/src/lib.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)] 4 + pub struct Frontmatter { 5 + #[serde(default = "frontmatter_about")] 6 + pub about: String, 7 + #[serde(skip_serializing)] 8 + pub title: String, 9 + #[serde(skip_serializing)] 10 + pub date: String, 11 + #[serde(skip_serializing)] 12 + pub author: Option<String>, 13 + #[serde(skip_serializing_if = "Option::is_none")] 14 + pub series: Option<String>, 15 + #[serde(skip_serializing)] 16 + pub tags: Option<Vec<String>>, 17 + #[serde(skip_serializing_if = "Option::is_none")] 18 + pub slides_link: Option<String>, 19 + #[serde(skip_serializing_if = "Option::is_none")] 20 + pub image: Option<String>, 21 + #[serde(skip_serializing_if = "Option::is_none")] 22 + pub thumb: Option<String>, 23 + #[serde(skip_serializing)] 24 + pub redirect_to: Option<String>, 25 + #[serde(skip_serializing_if = "Option::is_none")] 26 + pub vod: Option<Vod>, 27 + } 28 + 29 + fn frontmatter_about() -> String { 30 + "https://xeiaso.net/blog/api-jsonfeed-extensions#_xesite_frontmatter".to_string() 31 + } 32 + 33 + #[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)] 34 + pub struct Vod { 35 + pub twitch: String, 36 + pub youtube: String, 37 + }
+70 -92
src/post/frontmatter.rs
··· 1 1 /// This code was borrowed from @fasterthanlime. 2 2 use color_eyre::eyre::Result; 3 - use serde::{Deserialize, Serialize}; 4 - 5 - #[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)] 6 - pub struct Data { 7 - pub title: String, 8 - pub date: String, 9 - pub series: Option<String>, 10 - pub tags: Option<Vec<String>>, 11 - pub slides_link: Option<String>, 12 - pub image: Option<String>, 13 - pub thumb: Option<String>, 14 - pub show: Option<bool>, 15 - pub redirect_to: Option<String>, 16 - pub vod: Option<Vod>, 17 - } 18 - 19 - #[derive(Eq, PartialEq, Deserialize, Default, Debug, Serialize, Clone)] 20 - pub struct Vod { 21 - pub twitch: String, 22 - pub youtube: String, 23 - } 3 + pub use xesite_types::Frontmatter as Data; 24 4 25 5 enum State { 26 6 SearchForStart, ··· 37 17 Yaml(#[from] serde_yaml::Error), 38 18 } 39 19 40 - impl Data { 41 - pub fn parse(input: &str) -> Result<(Data, usize)> { 42 - let mut state = State::SearchForStart; 20 + pub fn parse(input: &str) -> Result<(Data, usize)> { 21 + let mut state = State::SearchForStart; 43 22 44 - let mut payload = None; 45 - let offset; 23 + let mut payload = None; 24 + let offset; 46 25 47 - let mut chars = input.char_indices(); 48 - 'parse: loop { 49 - let (idx, ch) = match chars.next() { 50 - Some(x) => x, 51 - None => return Err(Error::EOF)?, 52 - }; 53 - match &mut state { 54 - State::SearchForStart => match ch { 55 - '-' => { 56 - state = State::ReadingMarker { 57 - count: 1, 58 - end: false, 59 - }; 60 - } 61 - '\n' | '\t' | ' ' => { 62 - // ignore whitespace 63 - } 64 - _ => { 65 - panic!("Start of frontmatter not found"); 66 - } 67 - }, 68 - State::ReadingMarker { count, end } => match ch { 69 - '-' => { 70 - *count += 1; 71 - if *count == 3 { 72 - state = State::SkipNewline { end: *end }; 73 - } 74 - } 75 - _ => { 76 - panic!("Malformed frontmatter marker"); 77 - } 78 - }, 79 - State::SkipNewline { end } => match ch { 80 - '\n' => { 81 - if *end { 82 - offset = idx + 1; 83 - break 'parse; 84 - } else { 85 - state = State::ReadingFrontMatter { 86 - buf: String::new(), 87 - line_start: true, 88 - }; 89 - } 26 + let mut chars = input.char_indices(); 27 + 'parse: loop { 28 + let (idx, ch) = match chars.next() { 29 + Some(x) => x, 30 + None => return Err(Error::EOF)?, 31 + }; 32 + match &mut state { 33 + State::SearchForStart => match ch { 34 + '-' => { 35 + state = State::ReadingMarker { 36 + count: 1, 37 + end: false, 38 + }; 39 + } 40 + '\n' | '\t' | ' ' => { 41 + // ignore whitespace 42 + } 43 + _ => { 44 + panic!("Start of frontmatter not found"); 45 + } 46 + }, 47 + State::ReadingMarker { count, end } => match ch { 48 + '-' => { 49 + *count += 1; 50 + if *count == 3 { 51 + state = State::SkipNewline { end: *end }; 90 52 } 91 - _ => panic!("Expected newline, got {:?}", ch), 92 - }, 93 - State::ReadingFrontMatter { buf, line_start } => match ch { 94 - '-' if *line_start => { 95 - let mut state_temp = State::ReadingMarker { 96 - count: 1, 97 - end: true, 53 + } 54 + _ => { 55 + panic!("Malformed frontmatter marker"); 56 + } 57 + }, 58 + State::SkipNewline { end } => match ch { 59 + '\n' => { 60 + if *end { 61 + offset = idx + 1; 62 + break 'parse; 63 + } else { 64 + state = State::ReadingFrontMatter { 65 + buf: String::new(), 66 + line_start: true, 98 67 }; 99 - std::mem::swap(&mut state, &mut state_temp); 100 - if let State::ReadingFrontMatter { buf, .. } = state_temp { 101 - payload = Some(buf); 102 - } else { 103 - unreachable!(); 104 - } 105 68 } 106 - ch => { 107 - buf.push(ch); 108 - *line_start = ch == '\n'; 69 + } 70 + _ => panic!("Expected newline, got {:?}", ch), 71 + }, 72 + State::ReadingFrontMatter { buf, line_start } => match ch { 73 + '-' if *line_start => { 74 + let mut state_temp = State::ReadingMarker { 75 + count: 1, 76 + end: true, 77 + }; 78 + std::mem::swap(&mut state, &mut state_temp); 79 + if let State::ReadingFrontMatter { buf, .. } = state_temp { 80 + payload = Some(buf); 81 + } else { 82 + unreachable!(); 109 83 } 110 - }, 111 - } 84 + } 85 + ch => { 86 + buf.push(ch); 87 + *line_start = ch == '\n'; 88 + } 89 + }, 112 90 } 91 + } 113 92 114 - // unwrap justification: option set in state machine, Rust can't statically analyze it 115 - let payload = payload.unwrap(); 93 + // unwrap justification: option set in state machine, Rust can't statically analyze it 94 + let payload = payload.unwrap(); 116 95 117 - let fm: Self = serde_yaml::from_str(&payload)?; 96 + let fm: Data = serde_yaml::from_str(&payload)?; 118 97 119 - Ok((fm, offset)) 120 - } 98 + Ok((fm, offset)) 121 99 }
+4 -3
src/post/mod.rs
··· 30 30 impl Into<xe_jsonfeed::Item> for Post { 31 31 fn into(self) -> xe_jsonfeed::Item { 32 32 let mut result = xe_jsonfeed::Item::builder() 33 - .title(self.front_matter.title) 33 + .title(self.front_matter.title.clone()) 34 34 .content_html(self.body_html) 35 35 .id(format!("https://xeiaso.net/{}", self.link)) 36 36 .url(format!("https://xeiaso.net/{}", self.link)) ··· 40 40 .name("Xe Iaso") 41 41 .url("https://xeiaso.net") 42 42 .avatar("https://xeiaso.net/static/img/avatar.png"), 43 - ); 43 + ) 44 + .xesite_frontmatter(self.front_matter.clone()); 44 45 45 46 let mut tags: Vec<String> = vec![]; 46 47 ··· 96 97 let body = fs::read_to_string(fname.clone()) 97 98 .await 98 99 .wrap_err_with(|| format!("can't read {:?}", fname))?; 99 - let (front_matter, content_offset) = frontmatter::Data::parse(body.clone().as_str()) 100 + let (front_matter, content_offset) = frontmatter::parse(body.clone().as_str()) 100 101 .wrap_err_with(|| format!("can't parse frontmatter of {:?}", fname))?; 101 102 let body = &body[content_offset..]; 102 103 let date = NaiveDate::parse_from_str(&front_matter.clone().date, "%Y-%m-%d")