The code and data behind xeiaso.net
5
fork

Configure Feed

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

Start version 3 (#573)

* 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>

* fix nix builds

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

* fix dhall build

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

* fix non-flakes build

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

* make new mastodon share button

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

* remove the rest of the ructe templates that I can remove

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

* refactor blogposts to its own file

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

* move resume to be generated by nix

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

* write article

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

* blog/site-update-v3: hero image

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

* add site update series tag to site updates

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

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

authored by

Xe Iaso and committed by
GitHub
cc933b31 551e0384

+2439 -1771
+9 -9
Cargo.lock
··· 1551 1551 1552 1552 [[package]] 1553 1553 name = "mio" 1554 - version = "0.8.3" 1554 + version = "0.8.5" 1555 1555 source = "registry+https://github.com/rust-lang/crates.io-index" 1556 - checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" 1556 + checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" 1557 1557 dependencies = [ 1558 1558 "libc", 1559 1559 "log", 1560 1560 "wasi 0.11.0+wasi-snapshot-preview1", 1561 - "windows-sys 0.36.1", 1561 + "windows-sys 0.42.0", 1562 1562 ] 1563 1563 1564 1564 [[package]] ··· 2812 2812 2813 2813 [[package]] 2814 2814 name = "tokio" 2815 - version = "1.19.2" 2815 + version = "1.22.0" 2816 2816 source = "registry+https://github.com/rust-lang/crates.io-index" 2817 - checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" 2817 + checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" 2818 2818 dependencies = [ 2819 + "autocfg", 2819 2820 "bytes", 2820 2821 "libc", 2821 2822 "memchr", 2822 2823 "mio", 2823 2824 "num_cpus", 2824 - "once_cell", 2825 2825 "parking_lot", 2826 2826 "pin-project-lite", 2827 2827 "signal-hook-registry", ··· 2929 2929 2930 2930 [[package]] 2931 2931 name = "tower-layer" 2932 - version = "0.3.1" 2932 + version = "0.3.2" 2933 2933 source = "registry+https://github.com/rust-lang/crates.io-index" 2934 - checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" 2934 + checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" 2935 2935 2936 2936 [[package]] 2937 2937 name = "tower-service" ··· 3428 3428 3429 3429 [[package]] 3430 3430 name = "xesite" 3431 - version = "2.5.0" 3431 + version = "3.0.0" 3432 3432 dependencies = [ 3433 3433 "axum", 3434 3434 "axum-extra",
+2 -2
Cargo.toml
··· 1 1 [package] 2 2 name = "xesite" 3 - version = "2.5.0" 3 + version = "3.0.0" 4 4 authors = ["Xe Iaso <me@xeiaso.net>"] 5 - edition = "2018" 5 + edition = "2021" 6 6 build = "src/build.rs" 7 7 repository = "https://github.com/Xe/site" 8 8 license = "zlib"
+1 -1
blog/a-tool-to-aid-forgetfulness-2022-01-12.markdown
··· 1 1 --- 2 2 title: A Tool to Aid Forgetfulness 3 3 date: 2022-01-12 4 - series: stories 4 + series: short-story 5 5 --- 6 6 7 7 The Egyptian God Thoth lived in the Egyptian city of Naucratis. Thoth was the
+1 -1
blog/anbernic-win600-review.markdown
··· 1 1 --- 2 2 title: Anbernic Win600 Review 3 3 date: 2022-08-19 4 - series: review 4 + series: reviews 5 5 tags: 6 6 - anbernic 7 7 - win600
+1 -1
blog/fear-07-24-2018.markdown
··· 3 3 date: 2018-07-24 4 4 thanks: CelestialBoon, no really this guy is amazing and doesn't get enough credit, I'm so grateful for him. 5 5 for: Twilight Sparkle 6 - series: stories 6 + series: short-story 7 7 --- 8 8 9 9 _I must not fear._
+1 -1
blog/one-day-2018-11-01.markdown
··· 2 2 title: "One Day" 3 3 date: 2018-11-01 4 4 for: "Nicole" 5 - series: stories 5 + series: short-story 6 6 --- 7 7 8 8 In the beginning there was the void. All was the void and the void was all.
+1
blog/site-update-2020-07-16.markdown
··· 3 3 date: 2020-07-16 4 4 tags: 5 5 - rust 6 + series: site-update 6 7 --- 7 8 8 9 Hello there! You are reading this post thanks to a lot of effort, research and
+1
blog/site-update-2021-12-19.markdown
··· 1 1 --- 2 2 title: "Site Updates: Better Contrast Ratio and Using Xeact" 3 3 date: 2021-12-19 4 + series: site-update 4 5 --- 5 6 6 7 Happy holidays all! As the year rolls to a close I wanted to take a moment to
+1
blog/site-update-axum-2022-03-21.markdown
··· 1 1 --- 2 2 title: "Site Update: Axum" 3 3 date: 2022-03-21 4 + series: site-update 4 5 --- 5 6 6 7 I have made a bunch of huge changes to my website that hopefully you won't
+1
blog/site-update-hero-images.markdown
··· 1 1 --- 2 2 title: "Site Update: Hero Images" 3 3 date: 2022-06-08 4 + series: site-update 4 5 --- 5 6 6 7 For a while I've been wondering how I can add dramatic flair to my website with
+2 -1
blog/site-update-let-there-be-light-2021-03-13.markdown
··· 3 3 date: 2021-03-13 4 4 tags: 5 5 - a11y 6 + series: site-update 6 7 --- 7 8 8 9 In the beginning there was darkness. Darkness was all, and darkness was where the author of this site was comfortable with. However, we live in a time of (supposed) enlightenment. Thanks to the magic of CSS media queries, if you have your computer set to prefer light mode, you will get the light mode version of this website. ··· 13 14 14 15 </center> 15 16 16 - According to [caniuse.com](https://caniuse.com/?search=prefers-color-scheme) I should _probably_ be fine with this. Please contact me if this acts up for you in an odd way. It shouldn't, but knowing the internet I probably messed something up somewhere. 17 + According to [caniuse.com](https://caniuse.com/?search=prefers-color-scheme) I should _probably_ be fine with this. Please contact me if this acts up for you in an odd way. It shouldn't, but knowing the internet I probably messed something up somewhere.
+1
blog/site-update-mastodon-quoting.markdown
··· 7 7 - sonicfrontiers 8 8 - robocadey 9 9 - noxp 10 + series: site-update 10 11 --- 11 12 12 13 <xeblog-hero ai="Waifu Diffusion v1.3 (float16)" file="foxgirl-surfing" prompt="landscape, mountains, breath of the wild, 1girl, fox ears, dark blue hair, blue eyes, surfboard, surfing, beach, simple sketch, clean lines, rakugaki"></xeblog-hero>
+1
blog/site-update-patron-page-fixed.markdown
··· 1 1 --- 2 2 title: "Site Update: I Fixed the Patron Page" 3 3 date: 2022-05-18 4 + series: site-update 4 5 --- 5 6 6 7 So I fixed [the patron page](https://xeiaso.net/patrons) and the
+1
blog/site-update-rss-bandwidth-2021-01-14.markdown
··· 4 4 tags: 5 5 - devops 6 6 - optimization 7 + series: site-update 7 8 --- 8 9 9 10 Well, so I think I found out where my Kubernetes cluster cost came from. For
+2 -1
blog/site-update-salary-transparency.markdown
··· 1 1 --- 2 2 title: "Site Update: Salary Transparency Page Added" 3 3 date: 2022-06-14 4 - author: Sephie 4 + author: sephiraloveboo 5 + series: site-update 5 6 --- 6 7 7 8 <xeblog-hero file="miku-dark-souls" prompt="hatsune miku, elden ring, dark souls, concept art, crowbar"></xeblog-hero>
+417
blog/site-update-v3.markdown
··· 1 + --- 2 + title: "Site Update: Version 3.0" 3 + date: 2022-11-26 4 + author: Heartmender 5 + series: site-update 6 + tags: 7 + - dhall 8 + - LaTeX 9 + - rust 10 + - typescript 11 + - Xeact 12 + --- 13 + 14 + <xeblog-hero ai="Waifu Diffusion v1.3 (float16)" file="aoi-onsen" prompt="1girl, fox ears, dark blue hair, blue eyes, kimono, festival, landscape, makoto shinrai, arknights, fireworks, pagodas, onsen, long hair, princess, -hands, -amputee"></xeblog-hero> 15 + 16 + Hey all! Welcome to Xesite 3.0! Over the last few days I've taken a huge chunk 17 + out of my backlog and I have redone _a lot_ of this website. My hope is that 18 + this will make it faster, more reliable and ultimately make things a lot easier 19 + for everyone. There's a lot of improvements here so I'm just going to start 20 + going over them one by one. 21 + 22 + ## Project name changed 23 + 24 + This website has historically been named `site`. It's had a lot of work put into 25 + it over the years and I've never really referred to it by anything but "the 26 + website" or "my blog". However I've noticed a change in my notes and I think I 27 + should make this official. The project behind this website is now officially 28 + called `xesite`. 29 + 30 + <xeblog-conv name="Mara" mood="hmm">What about all of the custom HTML tags? 31 + Aren't they all called `xeblog-$NAME`? Are you going to change 32 + those?</xeblog-conv> 33 + 34 + <xeblog-conv name="Cadey" mood="coffee">The custom HTML tags are an 35 + implementation detail, not something that is exposed to end users. Plus, those 36 + are now in my muscle memory so I can't really change those if I wanted 37 + to.</xeblog-conv> 38 + 39 + ## Blogpost series have metadata 40 + 41 + When I implemented series support in the blog, it was kind of a hack. Series are 42 + intended to function kind of like tags, but more for tagging things that 43 + progress in a logical series. As an example, take a look at the page for my 44 + [site-to-site WireGuard VPN](https://xeiaso.net/blog/series/site-to-site-wireguard) 45 + series. It shows a little description for the series and in the future I will 46 + likely add other metadata like a series image. 47 + 48 + This is powered by my new `SeriesDescription` Dhall record type. 49 + 50 + <xeblog-conv name="Mara" mood="hacker">For context, 51 + [Dhall](https://dhall-lang.org) is a Haskell-like non-Turing-complete 52 + configuration language that you can think of like JSON or YAML but with 53 + functions, imports, and static types.</xeblog-conv> 54 + 55 + I've never really covered how Dhall works on this blog before, so I'll take a 56 + look at this and the `seriesDescriptions.dhall` file to show off how damn cool 57 + Dhall is. 58 + 59 + One of the most unique features of Dhall as a configuration language is its 60 + record completion operator `::`. This allows you to specify a record (read: 61 + object)'s type and also a set of _default values_ for that record. Let's take a 62 + look at the `SeriesDescription` record type: 63 + 64 + ```dhall 65 + -- dhall/types/SeriesDescription.dhall 66 + { Type = { name : Text, details : Text } 67 + , default = { name = "", details = "" } 68 + } 69 + ``` 70 + 71 + If I wanted to use this from inside my Dhall configuration, I would need to: 72 + 73 + * Import the type 74 + * Create a value with the `::` operator 75 + 76 + The description for the `site-to-site-wireguard` series could look something 77 + like this: 78 + 79 + ```dhall 80 + -- dhall/series/site-to-site-wireguard.dhall 81 + let xesite = ../types/package.dhall 82 + 83 + let SeriesDescription = xesite.SeriesDescription 84 + 85 + in SeriesDescription::{ 86 + , name = "site-to-site-wireguard" 87 + , details = "Instructions on setting up your own VPN with WireGuard." 88 + } 89 + ``` 90 + 91 + You declare your imports with URLs or filesystem paths and other aliases using 92 + `let`, and then you declare what to do with those variables after `in`. If 93 + you've never used Haskell or Lisp before, you can imagine `let` creating a block 94 + of scope just like an extra layer of curly braces in JavaScript or other C-like 95 + languages: 96 + 97 + ```javascript 98 + { 99 + let foo = "bar"; 100 + console.log(foo); 101 + } 102 + 103 + // foo isn't usable from here 104 + ``` 105 + 106 + As you can imagine, this gets [way more 107 + elaborate](https://github.com/Xe/site/blob/main/dhall/signalboost.dhall) when 108 + you get more detail into the mix. However, it's all really easy to understand. 109 + It's just different than you might be expecting at first glance. 110 + 111 + If you've never really used Dhall before, I really suggest taking a look at it. 112 + It's what I use for the configuration of all of my tools when I'm allowed to 113 + make those decisions. It's also got the ability to read from environment 114 + variables, which can make it easier to safely turn a bunch of raw strings from 115 + the environment into a more sensible datatype. 116 + 117 + ## New type for the Signal Boost data file 118 + 119 + One of my most successful parts of this site is the [signal boost 120 + page](https://xeiaso.net/signalboost). It was the first place I used Dhall on 121 + this website, and is one of the most contributed to things I've ever had in one 122 + of my own projects. 123 + 124 + When I designed the schema forever ago, I apparently decided that people should 125 + only have one of a few types of links. I thought a Twitter and GitHub link 126 + should be sufficient. The Dhall type used to look something like this: 127 + 128 + ```dhall 129 + { Type = 130 + { name : Text 131 + , tags : List Text 132 + , gitLink : Optional Text 133 + , twitter : Optional Text 134 + , linkedin : Optional Text 135 + , fediverse : Optional Text 136 + , coverLetter : Optional Text 137 + , website : Optional Text 138 + } 139 + , default = 140 + { name = "" 141 + , tags = [] : List Text 142 + , gitLink = None Text 143 + , twitter = None Text 144 + , linkedin = None Text 145 + , fediverse = None Text 146 + , coverLetter = None Text 147 + , website = None Text 148 + } 149 + } 150 + ``` 151 + 152 + I was wrong. That is _not_ generic enough. Recently I got [this pull 153 + request](https://github.com/Xe/site/pull/567/files) that made me really rethink 154 + how I'm doing this. I don't want to limit people to a set number of links and 155 + then deal with that, I want things to be generic enough that people can link to 156 + whatever they want. I ended up settling on this: 157 + 158 + ```dhall 159 + let Link = ./Link.dhall 160 + 161 + in { Type = { name : Text, tags : List Text, links : List Link.Type } 162 + , default = 163 + { name = "", tags = [] : List Text, links = [] : List Link.Type } 164 + } 165 + ``` 166 + 167 + This lets people link to _whatever they want_, such as like this: 168 + 169 + ```dhall 170 + -- dhall/signalboost/XeIaso.dhall 171 + let xesite = ../types/package.dhall 172 + 173 + let Link = xesite.Link 174 + 175 + let Person = xesite.Person 176 + 177 + in Person::{ 178 + , name = "Xe Iaso" 179 + , tags = 180 + [ "Go" 181 + , "Rust" 182 + , "Dhall" 183 + , "Nix" 184 + , "NixOS" 185 + ] 186 + , links = 187 + [ Link::{ url = "https://github.com/Xe", title = "GitHub" } 188 + , Link::{ url = "https://xeiaso.net", title = "Blog" } 189 + ] 190 + } 191 + ``` 192 + 193 + You can easily imagine how this would make things a lot simpler in practice. 194 + Instead of having to have specific fields for every kind of link someone could 195 + have, there's a list of links with a custom target and title. I hope this will 196 + make things simpler in practice. 197 + 198 + I have also removed a lot of the older entries with people that I have 199 + determined to be employed. If I am mistaken, please submit a pull request with 200 + corrections. 201 + 202 + ## Resume is now a PDF 203 + 204 + I've hosted [my resume](https://xeiaso.net/resume) on my blog for years. It's 205 + been a Markdown file (originally generated from a JSON document back when JSON 206 + resume was a thing) for the longest time. Whenever someone wanted a copy of my 207 + resume, I usually shoved that markdown file into Pandoc and gave them the 208 + resulting PDF. This has not scaled. 209 + 210 + Recently I changed how [my salary transpareny page 211 + works](https://xeiaso.net/blog/site-update-salary-transparency) and in the 212 + process I put all of my job history information (including salaries) into a 213 + giant heckin' Dhall document. It was always my intention to come back around and 214 + use this data for the resume page, but I've been stuck on the exact best way to 215 + do this. 216 + 217 + On Wednesday I had a bad idea. By the end of the day Thursday, I had it working. 218 + One of the features Dhall has is a string interpolation operator. If you write a 219 + document like this: 220 + 221 + ```dhall 222 + let bar = "bar" 223 + in "foo${bar}" 224 + ``` 225 + 226 + Dhall will return `foobar`. This means that you can use Dhall for both 227 + structured data _and text templating_. I've also been maintaining a separate 228 + immigration-friendly resume for a few years using Overleaf and LaTeX (turns out 229 + border agencies don't like it when you use a professional pseudonym on your 230 + resume, who knew). The bad idea was to shove the template into Dhall somehow so 231 + that Dhall would puke out LaTeX source code which would then be fed into a LaTeX 232 + compiler to turn into a PDF. 233 + 234 + Of course, if I was going to do this, I couldn't just choose pdfTeX as my LaTeX 235 + compiler of choice. That's not on-brand enough. It just so turns out that there 236 + is an international-character friendly LaTeX compiler named 237 + [XeTeX](https://en.wikipedia.org/wiki/XeTeX). I'm not kidding. It is so on-brand 238 + of me to use this that it _hurts_. 239 + 240 + So I took all of the madness in my trollish heart and poured it into my emacs 241 + frame. I eventually ended up with 242 + [`resume.dhall`](https://github.com/Xe/site/blob/v3/dhall/latex/resume.dhall). 243 + When you run `dhall text` on that file, you get all of that data transformed 244 + into LaTeX source code [like 245 + this](https://gist.github.com/Xe/59eccf3750697aba51512e571d971207). 246 + 247 + However having a bunch of LaTeX source code isn't useful by itself (unless you 248 + are also an immortal and can read LaTeX like I can). I wanted the resume to 249 + build itself so I _cannot forget how this works_. When thinking about build 250 + systems that are largely that ignorable, I usually turn to 251 + [Nix](https://nixos.org). So I wrote a Nix package for building my resume and 252 + shoved it into the resulting package for my blog. It looks something like this: 253 + 254 + ```nix 255 + # tex is texlive-medium plus a bunch of packages I need 256 + 257 + resumePDF = pkgs.stdenv.mkDerivation { 258 + pname = "xesite-resume-pdf"; 259 + inherit (bin) version; 260 + inherit src; 261 + buildInputs = with pkgs; [ dhall tex ]; 262 + 263 + phases = "installPhase"; 264 + 265 + installPhase = '' 266 + mkdir -p $out/static/resume 267 + cp -rf ${pkgs.dhallPackages.Prelude}/.cache .cache 268 + chmod -R u+w .cache 269 + export XDG_CACHE_HOME=.cache 270 + export DHALL_PRELUDE=${pkgs.dhallPackages.Prelude}/binary.dhall; 271 + 272 + ln -s $src/dhall/latex/resume.cls 273 + dhall text --file $src/dhall/latex/resume.dhall > resume.tex 274 + 275 + xelatex ./resume.tex 276 + cp resume.pdf $out/static/resume/resume.pdf 277 + ''; 278 + }; 279 + ``` 280 + 281 + Now if I want to build my resume by itself, I can run this command: 282 + 283 + ```console 284 + $ nix build .#resumePDF 285 + ``` 286 + 287 + This should make it a lot easier to deal with recruiters! 288 + 289 + ## I rewrote basically all of the templates with Maud 290 + 291 + When I ported my website from [Go to 292 + Rust](https://xeiaso.net/blog/site-update-2020-07-16) back in 2020 I needed a 293 + library like Go's [html/template](https://pkg.go.dev/html/template) to template 294 + out the HTML that my site uses. At the time there were many options I could pick 295 + from, but I ended up choosing [ructe](https://github.com/kaj/ructe) because it 296 + would compile the templates into my application binary instead of having to ship 297 + those with my website. This also means that the _optimizer_ can chew through my 298 + templates and make them _even faster_ than html/template. Native code will 299 + _always_ be faster than interpreted code. 300 + 301 + This worked for a while, but I started running into ergonomics problems as I 302 + continued to use ructe. The great part about ructe is that because the templates 303 + are compiled to Rust anyways, you can use any Rust logic or types you want. The 304 + horrible part about ructe is that your editor autocomplete and type checking 305 + logic doesn't work. Debugging compile failures of your templates requires that 306 + you understand how the generated code works. This isn't really as much of an 307 + issue as I'm making it sound like, but it's a papercut nonetheless. 308 + 309 + A while ago I found the Rust library [Maud](https://maud.lambda.xyz/). It's a 310 + procedural macro that uses a Rust-like syntax to emit HTML. It kinda looks like 311 + HTML if you squint. Compare these two identical documents: 312 + 313 + ```html 314 + <p>Hi there this is a test!</p> 315 + ``` 316 + 317 + ```rust 318 + html! { 319 + p { "Hi there this is a test!" } 320 + } 321 + ``` 322 + 323 + It also lets you use [splices](https://maud.lambda.xyz/splices-toggles.html) to 324 + add the value of variables to templates and [standard control 325 + structures](https://maud.lambda.xyz/control-structures.html) to handle Option 326 + values or whatever. 327 + 328 + I've wanted to move my site over to use them generically (and I've actually done 329 + most of the work to use them already for a lot of the shortcodes like the 330 + conversation snippets), so I took the time to do that. One of the easy to notice 331 + differences is the HTML of any page on the site. Before there was a lot of 332 + newlines and how everything is crammed into as few lines as possible. 333 + 334 + Shockingly, doing all of these changes seems to have had _no noticeable_ 335 + difference in how browsers render my website. I'm surprised too. 336 + 337 + As of right now the only things left using ructe are my RSS/Atom feeds and the 338 + first implementation of conversation snippets. I am honestly afraid to change 339 + that last one, so I'm leaving it as-is because it works and I don't want to 340 + touch it. 341 + 342 + Maybe this will make things load faster? I don't know. I'm not an HTML-ologist. 343 + 344 + ## Pictures on the Patrons page 345 + 346 + My [patrons page](https://xeiaso.net/patrons) has traditionally had a list of 347 + names. I thought I'd take advantage of rewriting the templates to make it a bit 348 + nicer looking, so I grabbed the avatar field out of everyone's entries and told 349 + my HTML template to put it in a fancy grid. This probably doesn't look nice on 350 + phones, but it looks great on my desktop and iPad so it's probably good enough. 351 + 352 + ## Everything builds using flakes 353 + 354 + I've maintained two builds of my site for a while. The Nix flakes one that I use 355 + in testing and the legacy Nix build that I use because I still haven't converted 356 + all of my projects on the server that runs this website to use flakes yet. 357 + 358 + My site uses [flake-compat](https://github.com/edolstra/flake-compat) to build 359 + the flakes build with a non-flakes environment. This is intended to make things 360 + easier, but I will still have CI build both the flakes and non-flakes build just 361 + in case. 362 + 363 + ## Xesite finally uses Xeact properly 364 + 365 + I've tried to use Xeact a few times on my blog. Nearly all of those attempts 366 + have been failures. I think this time will be different. I've had a share on 367 + Mastodon button on the bottom of every page for a few years. The old button used 368 + to use the browser `prompt()` function to query for your Mastodon instance and 369 + then it would pop a new tab to a precomposed toot that would mention me (so I 370 + can track usage of it). 371 + 372 + More people use this than you'd think. Even more since [all of the everything 373 + with Twitter](https://xeiaso.net/blog/rip-twitter) started to happen. While I 374 + was rejiggering all of the backend stuff with templates, I thought I'd mix in 375 + Xeact more properly and use [Xeact's JSX 376 + support](https://xeiaso.net/blog/xeact-jsx) to automagically compile things. 377 + 378 + I ended up with the "Share on Mastodon" dropdown you'll see at the footer of 379 + this article. It's implemented using [this TypeScript 380 + file](https://github.com/Xe/site/blob/main/src/frontend/mastodon_share_button.tsx). 381 + I'm happy with this so far, it does everything the old one did but just a bit 382 + better. It does use some invisible HTML elements to pass variables from the HTML 383 + template to the Xeact component, which I would love to change to JSON in a 384 + `<script>` tag or something, but this works for now. 385 + 386 + --- 387 + 388 + Anyways, that's what I've been up to with Xesite! Thank you all for reading 389 + this, I never thought I'd get to the point where I've been working on the same 390 + project for almost 8 years. I usually pick up and abandon projects really 391 + quickly, so it's very odd that I have a bigger one that sticks around like this. 392 + 393 + I'm super thankful for all of you reading and promoting this blog around. I've 394 + been using this blog to hone my writing talents and it's been really encouraging 395 + to see things hit the way I've wanted them to. 396 + 397 + You know, I wonder how many articles I've written: 398 + 399 + ``` 400 + $ ls ./blog ./talks | wc -l 401 + 324 402 + ``` 403 + 404 + Holy crap. I write a lot. Given that my first commit was on [January 31, 405 + 2015](https://github.com/Xe/christine.website/commit/a7092d46b4a6612fd77a85969c407043c665ed32), 406 + that means that I've had this blog up for about 2855 days. Given I have 324 407 + posts, this means that on average I have written an article about once every _9 408 + days_. This is kind of astounding. 409 + 410 + ``` 411 + $ nix run nixpkgs#lua5_3 412 + Lua 5.3.6 Copyright (C) 1994-2020 Lua.org, PUC-Rio 413 + > 2855/324 414 + 8.8117283950617 415 + ``` 416 + 417 + Here's to many more posts! Let's see if we can get that number down!
+1 -1
blog/sonic-frontiers.markdown
··· 1 1 --- 2 2 title: "My review of Sonic Frontiers" 3 3 date: 2022-11-25 4 + series: reviews 4 5 tags: 5 6 - sonicFrontiers 6 - - review 7 7 - sonicTheHedgehog 8 8 --- 9 9
+1 -1
blog/the-service-is-already-down-2018-10-13.markdown
··· 1 1 --- 2 2 title: The Service is Already Down 3 3 date: 2018-10-13 4 - series: stories 4 + series: short-story 5 5 --- 6 6 7 7 The master said to their apprentice: "come, look and let's load production". The apprentice came over confusedly, as the dashboards above showed everything is fine.
+1 -11
config.dhall
··· 1 - let xesite = ./dhall/types/package.dhall 2 - 3 - let Config = xesite.Config 4 - 5 - in Config::{ 6 - , signalboost = ./dhall/signalboost.dhall 7 - , authors = ./dhall/authors.dhall 8 - , clackSet = 9 - [ "Ashlynn", "Terry Davis", "Dennis Ritchie", "Steven Hawking" ] 10 - , jobHistory = ./dhall/jobHistory.dhall 11 - } 1 + ./dhall/package.dhall
+10 -57
default.nix
··· 1 - { sources ? import ./nix/sources.nix, pkgs ? import sources.nixpkgs { } }: 2 - with pkgs; 3 - 4 - let 5 - rust = pkgs.callPackage ./nix/rust.nix { }; 6 - 7 - srcNoTarget = dir: 8 - builtins.filterSource 9 - (path: type: type != "directory" || builtins.baseNameOf path != "target") 10 - dir; 11 - 12 - naersk = pkgs.callPackage sources.naersk { 13 - rustc = rust; 14 - cargo = rust; 15 - }; 16 - dhallpkgs = import sources.easy-dhall-nix { inherit pkgs; }; 17 - src = srcNoTarget ./.; 18 - 19 - xesite = naersk.buildPackage { 20 - inherit src; 21 - doCheck = true; 22 - buildInputs = [ pkg-config openssl git ]; 23 - remapPathPrefix = true; 24 - }; 25 - 26 - config = stdenv.mkDerivation { 27 - pname = "xesite-config"; 28 - version = "HEAD"; 29 - buildInputs = [ pkgs.dhall ]; 30 - 31 - phases = "installPhase"; 32 - 33 - installPhase = '' 34 - cd ${src} 35 - dhall resolve < ${src}/config.dhall >> $out 36 - ''; 37 - }; 38 - 39 - in pkgs.stdenv.mkDerivation { 40 - inherit (xesite) name; 41 - inherit src; 42 - phases = "installPhase"; 43 - 44 - installPhase = '' 45 - mkdir -p $out $out/bin 46 - 47 - cp -rf ${config} $out/config.dhall 48 - cp -rf $src/blog $out/blog 49 - cp -rf $src/css $out/css 50 - cp -rf $src/data $out/data 51 - cp -rf $src/gallery $out/gallery 52 - cp -rf $src/static $out/static 53 - cp -rf $src/talks $out/talks 54 - 55 - cp -rf ${xesite}/bin/xesite $out/bin/xesite 56 - ''; 57 - } 1 + (import 2 + ( 3 + let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in 4 + fetchTarball { 5 + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 6 + sha256 = lock.nodes.flake-compat.locked.narHash; 7 + } 8 + ) 9 + { src = ./.; } 10 + ).defaultNix
+3
dhall/Prelude.dhall
··· 1 + env:DHALL_PRELUDE 2 + ? https://raw.githubusercontent.com/dhall-lang/dhall-lang/v20.1.0/Prelude/package.dhall 3 + sha256:26b0ef498663d269e4dc6a82b0ee289ec565d683ef4c00d0ebdd25333a5a3c98
+36 -28
dhall/authors.dhall
··· 1 1 let Author = ./types/Author.dhall 2 2 3 - in [ Author::{ 4 - , name = "Xe Iaso" 5 - , handle = "xe" 6 - , picUrl = Some "/static/img/avatar.png" 7 - , link = Some "https://xeiaso.net" 8 - , twitter = Some "theprincessxena" 9 - , default = True 10 - , inSystem = True 11 - } 12 - , Author::{ 13 - , name = "Jessie" 14 - , handle = "Heartmender" 15 - , picUrl = Some 16 - "https://cdn.xeiaso.net/file/christine-static/img/UPRcp1pO_400x400.jpg" 17 - , twitter = Some "BeJustFine" 18 - , inSystem = True 19 - } 20 - , Author::{ 21 - , name = "Ashe" 22 - , handle = "ectamorphic" 23 - , picUrl = Some 24 - "https://cdn.xeiaso.net/file/christine-static/img/FFVV1InX0AkDX3f_cropped_smol.jpg" 25 - , inSystem = True 26 - } 27 - , Author::{ name = "Nicole", handle = "Twi", inSystem = True } 28 - , Author::{ name = "Mai", handle = "Mai", inSystem = True } 29 - , Author::{ name = "Sephira", handle = "Sephie", inSystem = True } 30 - ] 3 + let Prelude = ./Prelude.dhall 4 + 5 + let default = ./authors/xe.dhall 6 + 7 + let authors = 8 + [ default 9 + , Author::{ 10 + , name = "Jessie" 11 + , handle = "Heartmender" 12 + , image = Some 13 + "https://cdn.xeiaso.net/file/christine-static/img/UPRcp1pO_400x400.jpg" 14 + , url = Some "https://vulpine.club/@heartmender" 15 + , inSystem = True 16 + } 17 + , Author::{ 18 + , name = "Ashe" 19 + , handle = "ectamorphic" 20 + , image = Some 21 + "https://cdn.xeiaso.net/file/christine-static/img/FFVV1InX0AkDX3f_cropped_smol.jpg" 22 + , inSystem = True 23 + } 24 + , Author::{ name = "Nicole", handle = "Twi", inSystem = True } 25 + , Author::{ name = "Mai", handle = "Mai", inSystem = True } 26 + , Author::{ name = "Sephira", handle = "sephiraloveboo", inSystem = True } 27 + ] 28 + 29 + let authorToMapValue = \(a : Author.Type) -> { mapKey = a.handle, mapValue = a } 30 + 31 + let map = 32 + Prelude.List.map 33 + Author.Type 34 + (Prelude.Map.Entry Text Author.Type) 35 + authorToMapValue 36 + authors 37 + 38 + in { authors, map, default }
+19
dhall/authors/xe.dhall
··· 1 + let xesite = ../types/package.dhall 2 + 3 + let Author = xesite.Author 4 + 5 + in Author::{ 6 + , name = "Xe Iaso" 7 + , handle = "xe" 8 + , image = Some "https://xeiaso.net/static/img/avatar.png" 9 + , url = Some "https://xeiaso.net" 10 + , sameAs = 11 + [ "https://pony.social/@cadey" 12 + , "https://github.com/Xe" 13 + , "https://www.linkedin.com/in/xe-iaso-87a883254/" 14 + , "https://www.youtube.com/user/shadowh511" 15 + , "https://patreon.com/cadey" 16 + ] 17 + , jobTitle = "Archmage of Infrastructure" 18 + , inSystem = True 19 + }
+5
dhall/latex/.gitignore
··· 1 + resume.tex 2 + resume.pdf 3 + resume.aux 4 + resume.out 5 + resume.log
+131
dhall/latex/resume.cls
··· 1 + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 + % Medium Length Professional CV - RESUME CLASS FILE 3 + % 4 + % This template has been downloaded from: 5 + % http://www.LaTeXTemplates.com 6 + % 7 + % This class file defines the structure and design of the template. 8 + % 9 + % Original header: 10 + % Copyright (C) 2010 by Trey Hunner 11 + % 12 + % Copying and distribution of this file, with or without modification, 13 + % are permitted in any medium without royalty provided the copyright 14 + % notice and this notice are preserved. This file is offered as-is, 15 + % without any warranty. 16 + % 17 + % Created by Trey Hunner and modified by www.LaTeXTemplates.com 18 + % 19 + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 20 + 21 + \ProvidesClass{resume}[2010/07/10 v0.9 Resume class] 22 + 23 + \LoadClass[11pt,letterpaper]{article} % Font size and paper type 24 + 25 + \usepackage[parfill]{parskip} % Remove paragraph indentation 26 + \usepackage{array} % Required for boldface (\bf and \bfseries) tabular columns 27 + \usepackage{ifthen} % Required for ifthenelse statements 28 + 29 + \pagestyle{empty} % Suppress page numbers 30 + 31 + %---------------------------------------------------------------------------------------- 32 + % HEADINGS COMMANDS: Commands for printing name and address 33 + %---------------------------------------------------------------------------------------- 34 + 35 + \def \name#1{\def\@name{#1}} % Defines the \name command to set name 36 + \def \@name {} % Sets \@name to empty by default 37 + 38 + \def \addressSep {$\diamond$} % Set default address separator to a diamond 39 + 40 + % One, two or three address lines can be specified 41 + \let \@addressone \relax 42 + \let \@addresstwo \relax 43 + \let \@addressthree \relax 44 + 45 + % \address command can be used to set the first, second, and third address (last 2 optional) 46 + \def \address #1{ 47 + \@ifundefined{@addresstwo}{ 48 + \def \@addresstwo {#1} 49 + }{ 50 + \@ifundefined{@addressthree}{ 51 + \def \@addressthree {#1} 52 + }{ 53 + \def \@addressone {#1} 54 + }} 55 + } 56 + 57 + % \printaddress is used to style an address line (given as input) 58 + \def \printaddress #1{ 59 + \begingroup 60 + \def \\ {\addressSep\ } 61 + \centerline{#1} 62 + \endgroup 63 + \par 64 + \addressskip 65 + } 66 + 67 + % \printname is used to print the name as a page header 68 + \def \printname { 69 + \begingroup 70 + \hfil{\MakeUppercase{\namesize\bf \@name}}\hfil 71 + \nameskip\break 72 + \endgroup 73 + } 74 + 75 + %---------------------------------------------------------------------------------------- 76 + % PRINT THE HEADING LINES 77 + %---------------------------------------------------------------------------------------- 78 + 79 + \let\ori@document=\document 80 + \renewcommand{\document}{ 81 + \ori@document % Begin document 82 + \printname % Print the name specified with \name 83 + \@ifundefined{@addressone}{}{ % Print the first address if specified 84 + \printaddress{\@addressone}} 85 + \@ifundefined{@addresstwo}{}{ % Print the second address if specified 86 + \printaddress{\@addresstwo}} 87 + \@ifundefined{@addressthree}{}{ % Print the third address if specified 88 + \printaddress{\@addressthree}} 89 + } 90 + 91 + %---------------------------------------------------------------------------------------- 92 + % SECTION FORMATTING 93 + %---------------------------------------------------------------------------------------- 94 + 95 + % Defines the rSection environment for the large sections within the CV 96 + \newenvironment{rSection}[1]{ % 1 input argument - section name 97 + \sectionskip 98 + \MakeUppercase{\bf #1} % Section title 99 + \sectionlineskip 100 + \hrule % Horizontal line 101 + \begin{list}{}{ % List for each individual item in the section 102 + \setlength{\leftmargin}{1.5em} % Margin within the section 103 + } 104 + \item[] 105 + }{ 106 + \end{list} 107 + } 108 + 109 + %---------------------------------------------------------------------------------------- 110 + % WORK EXPERIENCE FORMATTING 111 + %---------------------------------------------------------------------------------------- 112 + 113 + \newenvironment{rSubsection}[4]{ % 4 input arguments - company name, year(s) employed, job title and location 114 + {\bf #1} \hfill {#2} % Bold company name and date on the right 115 + \ifthenelse{\equal{#3}{}}{}{ % If the third argument is not specified, don't print the job title and location line 116 + \\ 117 + {\em #3} \hfill {\em #4} % Italic job title and location 118 + }\smallskip 119 + \begin{list}{$\cdot$}{\leftmargin=0em} % \cdot used for bullets, no indentation 120 + \itemsep -0.5em \vspace{-0.5em} % Compress items in list together for aesthetics 121 + }{ 122 + \end{list} 123 + \vspace{0.5em} % Some space after the list of bullet points 124 + } 125 + 126 + % The below commands define the whitespace after certain things in the document - they can be \smallskip, \medskip or \bigskip 127 + \def\namesize{\huge} % Size of the name at the top of the document 128 + \def\addressskip{\smallskip} % The space between the two address (or phone/email) lines 129 + \def\sectionlineskip{\medskip} % The space above the horizontal line for each section 130 + \def\nameskip{\bigskip} % The space after your name at the top 131 + \def\sectionskip{\medskip} % The space after the heading section
+110
dhall/latex/resume.dhall
··· 1 + let xesite = ../types/package.dhall 2 + 3 + let Link = xesite.Link 4 + 5 + let Location = xesite.Location 6 + 7 + let Job = xesite.Job 8 + 9 + let Prelude = ../Prelude.dhall 10 + 11 + let xe = ../authors/xe.dhall 12 + 13 + let resume = ../resume.dhall 14 + 15 + let buzzwords = 16 + let doer = \(item : Text) -> item 17 + 18 + in Prelude.Text.concatMapSep ", " Text doer resume.buzzwords 19 + 20 + let jobHistory = 21 + let showDate = 22 + \(job : Job.Type) -> 23 + let endDate = 24 + merge 25 + { Some = \(t : Text) -> t, None = "current" } 26 + job.endDate 27 + 28 + in "${job.startDate} - ${endDate}" 29 + 30 + let showLoc = 31 + \(l : Location.Type) -> 32 + "${l.city}, ${l.stateOrProvince}, ${l.country}" 33 + 34 + let workedLocs = 35 + \(j : Job.Type) -> 36 + let doer = 37 + \(l : Location.Type) -> "\\item Work location: ${showLoc l}" 38 + 39 + in Prelude.Text.concatMapSep "\n" Location.Type doer j.locations 40 + 41 + let highlights = 42 + \(j : Job.Type) -> 43 + let doer = \(t : Text) -> "\\item ${t}" 44 + 45 + in Prelude.Text.concatMapSep "\n" Text doer j.highlights 46 + 47 + let doer = 48 + \(job : Job.Type) -> 49 + '' 50 + \begin{rSubsection}{${job.company.name}}{${showDate 51 + job}}{${job.title}}{${showLoc 52 + job.company.location}} 53 + 54 + ${workedLocs job} 55 + ${highlights job} 56 + 57 + \end{rSubsection} 58 + '' 59 + 60 + in Prelude.Text.concatMapSep "\n" Job.Type doer resume.jobs 61 + 62 + let publications = 63 + let doer = 64 + \(link : Link.Type) -> 65 + '' 66 + \begin{rSubsection}{\href{${link.url}}{${link.title}}}{}{}{} 67 + 68 + \item ${link.description} 69 + 70 + \end{rSubsection} 71 + '' 72 + 73 + in Prelude.Text.concatMapSep 74 + "\n" 75 + Link.Type 76 + doer 77 + resume.notablePublications 78 + 79 + in '' 80 + \documentclass{resume} 81 + 82 + \usepackage[left=0.75in,top=0.6in,right=0.75in,bottom=0.6in]{geometry} % Document margins 83 + \usepackage{hyperref} 84 + \newcommand{\tab}[1]{\hspace{.2667\textwidth}\rlap{#1}} 85 + \newcommand{\itab}[1]{\hspace{0em}\rlap{#1}} 86 + \name{${xe.name}} 87 + \address{https://xeiaso.net \\ me@xeiaso.net} 88 + 89 + \begin{document} 90 + 91 + \begin{rSection}{Technical Strengths} 92 + 93 + ${buzzwords} 94 + 95 + \end{rSection} 96 + 97 + \begin{rSection}{Experience} 98 + 99 + ${jobHistory} 100 + 101 + \end{rSection} 102 + 103 + \begin{rSection}{Notable Publications} 104 + 105 + ${publications} 106 + 107 + \end{rSection} 108 + 109 + \end{document} 110 + ''
+78
dhall/package.dhall
··· 1 + let xesite = ./types/package.dhall 2 + 3 + let Config = xesite.Config 4 + 5 + let Link = xesite.Link 6 + 7 + let authors = ./authors.dhall 8 + 9 + let desc = ./seriesDescriptions.dhall 10 + 11 + in Config::{ 12 + , signalboost = ./signalboost.dhall 13 + , authors = authors.map 14 + , defaultAuthor = authors.default 15 + , clackSet = 16 + [ "Ashlynn", "Terry Davis", "Dennis Ritchie", "Steven Hawking" ] 17 + , jobHistory = ./jobHistory.dhall 18 + , seriesDescriptions = desc.descriptions 19 + , seriesDescMap = desc.map 20 + , notableProjects = 21 + [ Link::{ 22 + , url = "https://github.com/PonyvilleFM/aura" 23 + , title = "Aura" 24 + , description = "PonyvilleFM live DJ recording bot" 25 + } 26 + , Link::{ 27 + , url = "https://github.com/Xe/olin" 28 + , title = "Olin" 29 + , description = "WebAssembly on the server" 30 + } 31 + , Link::{ 32 + , url = "https://printerfacts.cetacean.club/" 33 + , title = "Printer Facts" 34 + , description = "Useful facts about printers" 35 + } 36 + , Link::{ 37 + , url = "https://github.com/Xe/waifud" 38 + , title = "waifud" 39 + , description = "A VM manager for my homelab cluster" 40 + } 41 + , Link::{ 42 + , url = "https://when-then-zen.christine.website/" 43 + , title = "When Then Zen" 44 + , description = "Meditation instructions in plain English" 45 + } 46 + , Link::{ 47 + , url = "https://github.com/Xe/x" 48 + , title = "x" 49 + , description = 50 + "A monorepo of my experiments, toy programs and other interesting things of that nature." 51 + } 52 + , Link::{ 53 + , url = "https://github.com/Xe/Xeact" 54 + , title = "Xeact" 55 + , description = 56 + "My personal JavaScript femtoframework for high productivity development" 57 + } 58 + , Link::{ 59 + , url = "https://github.com/Xe/site" 60 + , title = "Xesite" 61 + , description = "The backend and templates for this website" 62 + } 63 + , Link::{ 64 + , url = "https://github.com/Xe/Xess" 65 + , title = "Xess" 66 + , description = "My personal CSS framework" 67 + } 68 + ] 69 + , contactLinks = 70 + [ Link::{ url = "https://github.com/Xe", title = "GitHub" } 71 + , Link::{ url = "https://keybase.io/xena", title = "Keybase" } 72 + , Link::{ url = "https://www.patreon.com/cadey", title = "Patreon" } 73 + , Link::{ url = "https://twitch.tv/princessxen", title = "Twitch" } 74 + , Link::{ url = "https://pony.social/@cadey", title = "Fediverse" } 75 + , Link::{ url = "https://t.me/miamorecadenza", title = "Telegram" } 76 + , Link::{ url = "irc://irc.libera.chat/#xeserv", title = "IRC" } 77 + ] 78 + }
+76 -15
dhall/resume.dhall
··· 5 5 let Link = xesite.Link 6 6 7 7 in Resume::{ 8 - , hnLinks = 8 + , buzzwords = 9 + [ "Docker" 10 + , "Git" 11 + , "Go" 12 + , "Rust" 13 + , "C" 14 + , "DevOps" 15 + , "Heroku" 16 + , "WebAssembly" 17 + , "Lua" 18 + , "Mindfulness" 19 + , "Nix" 20 + , "NixOS" 21 + , "HTTP/2" 22 + , "Ubuntu" 23 + , "Alpine Linux" 24 + , "GraphViz" 25 + , "JavaScript" 26 + , "TypeScript" 27 + , "SQLite" 28 + , "PostgreSQL" 29 + , "Dudeism" 30 + , "Technical writing" 31 + , "Emacs" 32 + , "Continuous Integration" 33 + , "Continuous Delivery" 34 + ] 35 + , jobs = ./jobHistory.dhall 36 + , notablePublications = 9 37 [ Link::{ 10 - , url = "https://news.ycombinator.com/item?id=29522941" 11 - , title = "'Open Source' is Broken" 38 + , url = "https://blog.heroku.com/how-to-make-progressive-web-app" 39 + , title = "How to Make a Progressive Web App From Your Existing Website" 40 + , description = 41 + "An article summarizing how easy it is to make a webpage into an installable Progressive Web App." 42 + } 43 + , Link::{ 44 + , url = 45 + "https://web.archive.org/web/20210318102148/https://tech.lightspeedhq.com/palisade-version-bumping-at-scale-in-ci/" 46 + , title = "Palisade: Version Bumping at Scale in CI" 47 + , description = 48 + "The release post for Palisade, a tool to automate version bumping, release tagging and more." 49 + } 50 + , Link::{ 51 + , url = "https://tailscale.com/blog/grafana-auth/" 52 + , title = "How To Seamlessly Authenticate to Grafana using Tailscale" 53 + , description = 54 + "The release post for grafana-auth, a tool that lets Grafana users automagically authenticate to Grafana using Tailscale." 12 55 } 13 56 , Link::{ 14 - , url = "https://news.ycombinator.com/item?id=29167560" 15 - , title = "The Surreal Horror of PAM" 57 + , url = "https://tailscale.com/blog/tailscale-auth-minecraft/" 58 + , title = "Tailscale Authentication for Minecraft" 59 + , description = 60 + "A post explaining how Tailscale as an authentication mechanism can be used in absurd places, such as making authentication for Minecraft servers." 61 + } 62 + , Link::{ 63 + , url = "https://tailscale.com/blog/tailscale-auth-nginx/" 64 + , title = "Tailscale Authentication for NGINX" 65 + , description = 66 + "The release post for nginx-auth, a tool that uses Tailscale's knowledge of IP address to person mappings to provide a weak authentication factor." 16 67 } 17 68 , Link::{ 18 - , url = "https://news.ycombinator.com/item?id=27175960" 19 - , title = "Systemd: The Good Parts" 69 + , url = "https://tailscale.com/blog/steam-deck/" 70 + , title = "Putting Tailscale on the Steam Deck" 71 + , description = 72 + "An engineering log of all the steps taken to run Tailscale on the Valve Steam Deck and the tradeoffs between the various methods you could use to do this." 20 73 } 21 74 , Link::{ 22 - , url = "https://news.ycombinator.com/item?id=26845355" 23 - , title = "I Implemented /dev/printerfact in Rust" 75 + , url = "https://tailscale.com/blog/gitops-acls/" 76 + , title = "GitOps for Tailscale ACLs" 77 + , description = 78 + "The release post of the Sync Tailscale ACLs GitHub Action, allowing administrators to automatically sync Tailscale ACLs from a GitHub repository." 24 79 } 25 80 , Link::{ 26 - , url = "https://news.ycombinator.com/item?id=25978511" 27 - , title = "A Model for Identity in Software" 81 + , url = "https://tailscale.com/blog/hamachi/" 82 + , title = "Tailscale: A modern replacement for Hamachi" 83 + , description = 84 + "A nostalgic piece recalling the magic of Hamachi (a product that did a similar thing to Tailscale), and how Tailscale builds on top of that to do even better things." 28 85 } 29 86 , Link::{ 30 - , url = "https://news.ycombinator.com/item?id=31390506" 31 - , title = "Fly.io: The reclaimer of Heroku's magic" 87 + , url = "https://tailscale.com/blog/magicdns-why-name/" 88 + , title = "An epic treatise on DNS, magical and otherwise" 89 + , description = 90 + "A deep dive into all of the problems that DNS has at scale and how Tailscale makes most of those problems go away, with the rest of them being easier in comparison." 32 91 } 33 92 , Link::{ 34 - , url = "https://news.ycombinator.com/item?id=31149801" 35 - , title = "Crimes with Go Generics" 93 + , url = "https://tailscale.com/blog/tsnet-virtual-private-services/" 94 + , title = "Virtual private services with tsnet" 95 + , description = 96 + "Tailscale lets you connect to your network from anywhere, but you have to set it up on individual computers for it to work. In this article Xe covers how to use tsnet to get all of the goodness of Tailscale in userspace so that you can have your services join your tailnet like they were separate computers." 36 97 } 37 98 ] 38 99 }
+135
dhall/seriesDescriptions.dhall
··· 1 + let xesite = ./types/package.dhall 2 + 3 + let Prelude = ./Prelude.dhall 4 + 5 + let Desc = xesite.SeriesDescription 6 + 7 + let descriptions 8 + : List Desc.Type 9 + = [ Desc::{ 10 + , name = "colemak" 11 + , details = 12 + "My efforts at learning to type with colemak instead of qwerty." 13 + } 14 + , Desc::{ 15 + , name = "conlangs" 16 + , details = 17 + "Information about constructed languages I've attempted to make." 18 + } 19 + , Desc::{ 20 + , name = "dreams" 21 + , details = "My attempts to write about my dreams" 22 + } 23 + , Desc::{ 24 + , name = "ethereum" 25 + , details = "Why Ethereum doesn't work in the real world." 26 + } 27 + , Desc::{ 28 + , name = "freenode" 29 + , details = 30 + "My lamentations about the collapse of the IRC network freenode." 31 + } 32 + , Desc::{ 33 + , name = "get-going" 34 + , details = "Tutorials for the Go programming language." 35 + } 36 + , Desc::{ 37 + , name = "h" 38 + , details = "Evolution of the h human/programming language." 39 + } 40 + , Desc::{ 41 + , name = "howto" 42 + , details = "Instructions on how to do various things." 43 + } 44 + , Desc::{ name = "keeb", details = "Keyboard reviews." } 45 + , Desc::{ 46 + , name = "magick" 47 + , details = 48 + "Writeups on things I've learned on my trip through chaos magick." 49 + } 50 + , Desc::{ 51 + , name = "malto" 52 + , details = "Stories from my constructed world Malto." 53 + } 54 + , Desc::{ 55 + , name = "medium-archive" 56 + , details = "Articles from my attempt at making a Medium blog long ago." 57 + } 58 + , Desc::{ 59 + , name = "nix-flakes" 60 + , details = 61 + "Instructions on how to use Nix flakes, a new way to use Nix in a more reproducible way." 62 + } 63 + , Desc::{ 64 + , name = "olin" 65 + , details = "My attempts at running WebAssembly on the server." 66 + } 67 + , Desc::{ name = "plt", details = "The saga of plt." } 68 + , Desc::{ 69 + , name = "recipes" 70 + , details = 71 + "Recipes for use in your kitchen presented in a no-bullshit fashion." 72 + } 73 + , Desc::{ 74 + , name = "reconlangmo" 75 + , details = 76 + "More details on how I tried to make a language named L'ewa." 77 + } 78 + , Desc::{ 79 + , name = "reviews" 80 + , details = "Reviews on various tech or media properties." 81 + } 82 + , Desc::{ 83 + , name = "short-stories" 84 + , details = "Flash fiction stories I've written over the years" 85 + } 86 + , Desc::{ 87 + , name = "site-update" 88 + , details = 89 + "Updates on this website. These articles will contain details on how this website is changed, new things I'm cooking up with it or more." 90 + } 91 + , Desc::{ 92 + , name = "site-to-site-wireguard" 93 + , details = "Instructions on setting up your own VPN with WireGuard." 94 + } 95 + , Desc::{ 96 + , name = "spellblade" 97 + , details = "Sections of my web novel Spellblade." 98 + } 99 + , Desc::{ 100 + , name = "templeos" 101 + , details = 102 + "Articles about TempleOS, a public domain operating system for AMD64 computers." 103 + } 104 + , Desc::{ 105 + , name = "thesource" 106 + , details = "Expansions for my TTRPG The Source." 107 + } 108 + , Desc::{ 109 + , name = "twitter" 110 + , details = 111 + "Lamentations on the death of Twitter, a microblogging community." 112 + } 113 + , Desc::{ name = "v", details = "The V programming language." } 114 + , Desc::{ name = "vtuber", details = "My experience as a VTuber." } 115 + , Desc::{ 116 + , name = "waifud" 117 + , details = "Information about my VM manager waifud." 118 + } 119 + , Desc::{ 120 + , name = "when-then-zen" 121 + , details = "Meditation information sans bullshit." 122 + } 123 + ] 124 + 125 + let descToMapValue = 126 + \(desc : Desc.Type) -> { mapKey = desc.name, mapValue = desc.details } 127 + 128 + let map = 129 + Prelude.List.map 130 + Desc.Type 131 + (Prelude.Map.Entry Text Text) 132 + descToMapValue 133 + descriptions 134 + 135 + in { descriptions, map }
+29 -157
dhall/signalboost.dhall
··· 1 + let Link = ./types/Link.dhall 2 + 1 3 let Person = ./types/Person.dhall 2 4 3 5 in [ Person::{ 4 - , name = "Chuck Nelson" 5 - , tags = [ "C++", "Python", "JSON", "GO", "Linux", "bash", "PHP", "Java" ] 6 - , gitLink = Some "https://github.com/chuckn408" 7 - } 8 - , Person::{ 9 - , name = "Christian Sullivan" 10 - , tags = 11 - [ "go" 12 - , "wasm" 13 - , "react" 14 - , "rust" 15 - , "react-native" 16 - , "swift" 17 - , "google-cloud" 18 - , "aws" 19 - , "docker" 20 - , "kubernetes" 21 - , "istio" 22 - , "typescript" 23 - ] 24 - , gitLink = Some "https://github.com/euforic" 25 - , twitter = Some "https://twitter.com/euforic" 26 - } 27 - , Person::{ 28 - , name = "David Roberts" 29 - , tags = 30 - [ "ux" 31 - , "ui" 32 - , "documentation" 33 - , "web" 34 - , "html5" 35 - , "javascript" 36 - , "python" 37 - , "qt" 38 - , "bash" 39 - , "front-end" 40 - , "full-stack" 41 - , "linux" 42 - , "embedded" 43 - , "sql" 44 - ] 45 - , gitLink = Some "https://github.com/ddr0" 46 - , twitter = Some "https://twitter.com/DDR_4" 47 - } 48 - , Person::{ 49 - , name = "Faizan Jamil" 50 - , tags = 51 - [ "java" 52 - , "c#" 53 - , "python" 54 - , "javascript" 55 - , "typescript" 56 - , "html" 57 - , "css" 58 - , "vue.js" 59 - , "express.js" 60 - , "flask" 61 - , "asp.net core" 62 - , "razor pages" 63 - , "ef core" 64 - , "front-end" 65 - , "back-end" 66 - , "full-stack" 67 - , "linux" 68 - ] 69 - , gitLink = Some "https://github.com/faizjamil" 70 - } 71 - , Person::{ 72 - , name = "Joseph Crawley" 73 - , tags = 74 - [ "javascript" 75 - , "react" 76 - , "csharp" 77 - , "python" 78 - , "full-stack" 79 - , "web" 80 - , "bash" 81 - , "linux" 82 - ] 83 - , gitLink = Some "https://github.com/espe-on" 84 - , twitter = Some "https://twitter.com/espe_on_" 85 - } 86 - , Person::{ 87 6 , name = "nicoo" 88 7 , tags = 89 8 [ "cryptography" ··· 96 15 , "security" 97 16 , "SDR" 98 17 ] 99 - , gitLink = Some "https://github.com/nbraud" 18 + , links = 19 + [ Link::{ url = "https://github.com/nbraud", title = "GitHub" } ] 100 20 } 101 21 , Person::{ 102 22 , name = "Prajjwal Singh" ··· 112 32 , "google-cloud" 113 33 , "typescript" 114 34 ] 115 - , gitLink = Some "https://github.com/Prajjwal" 116 - , twitter = Some "https://twitter.com/prajjwalsin" 35 + , links = 36 + [ Link::{ url = "https://github.com/Prajjwal", title = "GitHub" } 37 + , Link::{ url = "https://twitter.com/prajjwalsin", title = "Twitter" } 38 + ] 117 39 } 118 40 , Person::{ 119 41 , name = "Piyushh Bhutoria" ··· 125 47 , "php" 126 48 , "google-cloud" 127 49 ] 128 - , gitLink = Some "https://github.com/Piyushhbhutoria" 129 - , twitter = Some "https://twitter.com/PiyushhB" 130 - } 131 - , Person::{ 132 - , name = "Ryan Casalino" 133 - , tags = 134 - [ "golang" 135 - , "react" 136 - , "python" 137 - , "javascript" 138 - , "aws" 139 - , "vue" 140 - , "sql" 141 - , "ruby" 142 - , "rails" 143 - , "flask" 144 - , "unix" 50 + , links = 51 + [ Link::{ url = "https://github.com/Piyushhbhutoria", title = "GitHub" } 52 + , Link::{ url = "https://twitter.com/PiyushhB", title = "twitter" } 145 53 ] 146 - , gitLink = Some "https://github.com/rjpcasalino" 147 54 } 148 55 , Person::{ 149 56 , name = "Jeremy White" ··· 162 69 , "google-cloud" 163 70 , "azure" 164 71 ] 165 - , gitLink = Some "https://github.com/dudymas" 166 - , twitter = Some "https://twitter.com/dudymas" 167 - } 168 - , Person::{ 169 - , name = "Jeffin Mathew" 170 - , tags = 171 - [ "Python" 172 - , "routing&switching" 173 - , "django" 174 - , "vue" 175 - , "ansible" 176 - , "aws" 177 - , "javascript" 178 - , "iot" 72 + , links = 73 + [ Link::{ url = "https://github.com/dudymas", title = "GitHub" } 74 + , Link::{ url = "https://twitter.com/dudymas", title = "Twitter" } 179 75 ] 180 - , gitLink = Some "https://github.com/mjeffin" 181 - , twitter = Some "https://twitter.com/mpjeffin" 182 - } 183 - , Person::{ 184 - , name = "Nasir Hussain" 185 - , tags = 186 - [ "python" 187 - , "linux" 188 - , "javascript" 189 - , "ansible" 190 - , "nix" 191 - , "docker&podman" 192 - , "django" 193 - , "golang" 194 - , "rpm packaging" 195 - ] 196 - , gitLink = Some "https://github.com/nasirhm" 197 - , twitter = Some "https://twitter.com/_nasirhm_" 198 - } 199 - , Person::{ 200 - , name = "Avi Parshan" 201 - , tags = 202 - [ "python", "windows", "javascript", "html", "android", "java", "C#" ] 203 - , gitLink = Some "https://github.com/avipars" 204 - , twitter = Some "https://twitter.com/aviinfinity" 205 - } 206 - , Person::{ 207 - , name = "Krish Jain" 208 - , tags = 209 - [ "c++", "linux", "c", "python", "ios", "nlp", "machine learning" ] 210 - , gitLink = Some "https://github.com/Krish-sysadmin" 211 - , twitter = Some "https://twitter.com/krishjain02" 212 76 } 213 77 , Person::{ 214 78 , name = "Violet White" ··· 222 86 , "rust" 223 87 , "backend" 224 88 ] 225 - , gitLink = Some "https://github.com/epsilon-phase" 89 + , links = 90 + [ Link::{ url = "https://github.com/epsilon-phase", title = "GitHub" } ] 226 91 } 227 92 , Person::{ 228 93 , name = "Henri Shustak" ··· 246 111 , "R&D" 247 112 , "SRE / system adminsitration" 248 113 ] 249 - , gitLink = Some "https://github.com/henri" 250 - , twitter = Some "https://twitter.com/henri_shustak" 114 + , links = 115 + [ Link::{ url = "https://github.com/henri", title = "GitHub" } 116 + , Link::{ url = "https://twitter.com/henri_shustak", title = "Twitter" } 117 + ] 251 118 } 252 119 , Person::{ 253 120 , name = "Andrei Jiroh Halili" 254 121 , tags = [ "backend", "bash", "nodejs", "deno", "alpinelinux", "linux" ] 255 - , gitLink = Some "https://github.com/ajhalili2006" 256 - , twitter = Some "https://twitter.com/Kuys_Potpot" 257 - , fediverse = Some "https://tilde.zone/@ajhalili2006" 258 - , website = Some "https://ajhalili2006.bio.link" 122 + , links = 123 + [ Link::{ url = "https://github.com/ajhalili2006", title = "GitHub" } 124 + , Link::{ url = "https://twitter.com/Kuys_Potpot", title = "Twitter" } 125 + , Link::{ 126 + , url = "https://tilde.zone/@ajhalili2006" 127 + , title = "Fediverse" 128 + } 129 + , Link::{ url = "https://ajhalili2006.bio.link", title = "Website" } 130 + ] 259 131 } 260 132 ]
+8 -8
dhall/types/Author.dhall
··· 1 1 { Type = 2 2 { name : Text 3 3 , handle : Text 4 - , picUrl : Optional Text 5 - , link : Optional Text 6 - , twitter : Optional Text 7 - , default : Bool 4 + , image : Optional Text 5 + , url : Optional Text 6 + , sameAs : List Text 7 + , jobTitle : Text 8 8 , inSystem : Bool 9 9 } 10 10 , default = 11 11 { name = "" 12 12 , handle = "" 13 - , picUrl = None Text 14 - , link = None Text 15 - , twitter = None Text 16 - , default = False 13 + , image = None Text 14 + , url = None Text 15 + , sameAs = [] : List Text 16 + , jobTitle = "" 17 17 , inSystem = False 18 18 } 19 19 }
+17 -1
dhall/types/Config.dhall
··· 4 4 5 5 let Job = ./Job.dhall 6 6 7 + let Link = ./Link.dhall 8 + 7 9 let NagMessage = ./NagMessage.dhall 10 + 11 + let SeriesDescription = ./SeriesDescription.dhall 12 + 13 + let Prelude = ../Prelude.dhall 8 14 9 15 let defaultPort = env:PORT ? 3030 10 16 ··· 14 20 15 21 in { Type = 16 22 { signalboost : List Person.Type 17 - , authors : List Author.Type 23 + , defaultAuthor : Author.Type 24 + , authors : Prelude.Map.Type Text Author.Type 18 25 , port : Natural 19 26 , clackSet : List Text 20 27 , resumeFname : Text 21 28 , webMentionEndpoint : Text 22 29 , miToken : Text 23 30 , jobHistory : List Job.Type 31 + , seriesDescriptions : List SeriesDescription.Type 32 + , seriesDescMap : Prelude.Map.Type Text Text 33 + , notableProjects : List Link.Type 34 + , contactLinks : List Link.Type 24 35 } 25 36 , default = 26 37 { signalboost = [] : List Person.Type 38 + , defaultAuthor = Author::{=} 27 39 , authors = [] : List Author.Type 28 40 , port = defaultPort 29 41 , clackSet = [ "Ashlynn" ] ··· 31 43 , webMentionEndpoint = defaultWebMentionEndpoint 32 44 , miToken = "${env:MI_TOKEN as Text ? ""}" 33 45 , jobHistory = [] : List Job.Type 46 + , seriesDescriptions = [] : List SeriesDescription.Type 47 + , seriesDescMap = [] : Prelude.Map.Type Text Text 48 + , notableProjects = [] : List Link.Type 49 + , contactLinks = [] : List Link.Type 34 50 } 35 51 }
+3 -1
dhall/types/Link.dhall
··· 1 - { Type = { url : Text, title : Text }, default = { url = "", title = "" } } 1 + { Type = { url : Text, title : Text, description : Text } 2 + , default = { url = "", title = "", description = "" } 3 + }
+5 -20
dhall/types/Person.dhall
··· 1 - { Type = 2 - { name : Text 3 - , tags : List Text 4 - , gitLink : Optional Text 5 - , twitter : Optional Text 6 - , linkedin : Optional Text 7 - , fediverse : Optional Text 8 - , coverLetter : Optional Text 9 - , website : Optional Text 1 + let Link = ./Link.dhall 2 + 3 + in { Type = { name : Text, tags : List Text, links : List Link.Type } 4 + , default = 5 + { name = "", tags = [] : List Text, links = [] : List Link.Type } 10 6 } 11 - , default = 12 - { name = "" 13 - , tags = [] : List Text 14 - , gitLink = None Text 15 - , twitter = None Text 16 - , linkedin = None Text 17 - , fediverse = None Text 18 - , coverLetter = None Text 19 - , website = None Text 20 - } 21 - }
+8 -2
dhall/types/Resume.dhall
··· 2 2 3 3 let Link = ./Link.dhall 4 4 5 + let Job = ./Job.dhall 6 + 5 7 in { Type = 6 8 { name : Text 7 9 , tagline : Text 8 10 , location : Location.Type 9 - , hnLinks : List Link.Type 11 + , buzzwords : List Text 12 + , jobs : List Job.Type 13 + , notablePublications : List Link.Type 10 14 } 11 15 , default = 12 16 { name = "Xe Iaso" ··· 16 20 , stateOrProvince = "ON" 17 21 , country = "CAN" 18 22 } 19 - , hnLinks = [] : List Link.Type 23 + , buzzwords = [] : List Text 24 + , jobs = [] : List Job.Type 25 + , notablePublications = [] : List Link.Type 20 26 } 21 27 }
+3
dhall/types/SeriesDescription.dhall
··· 1 + { Type = { name : Text, details : Text } 2 + , default = { name = "", details = "" } 3 + }
+1
dhall/types/package.dhall
··· 8 8 , Person = ./Person.dhall 9 9 , Resume = ./Resume.dhall 10 10 , Salary = ./Salary.dhall 11 + , SeriesDescription = ./SeriesDescription.dhall 11 12 , Stock = ./Stock.dhall 12 13 , StockKind = ./StockKind.dhall 13 14 }
+13 -25
flake.lock
··· 3 3 "flake-compat": { 4 4 "flake": false, 5 5 "locked": { 6 - "lastModified": 1650374568, 7 - "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=", 6 + "lastModified": 1668681692, 7 + "narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=", 8 8 "owner": "edolstra", 9 9 "repo": "flake-compat", 10 - "rev": "b4a34015c698c7793d592d66adbab377907a2be8", 10 + "rev": "009399224d5e398d03b22badca40a37ac85412a1", 11 11 "type": "github" 12 12 }, 13 13 "original": { ··· 18 18 }, 19 19 "flake-utils": { 20 20 "locked": { 21 - "lastModified": 1659877975, 22 - "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", 21 + "lastModified": 1667395993, 22 + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", 23 23 "owner": "numtide", 24 24 "repo": "flake-utils", 25 - "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", 25 + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", 26 26 "type": "github" 27 27 }, 28 28 "original": { ··· 33 33 }, 34 34 "naersk": { 35 35 "inputs": { 36 - "nixpkgs": "nixpkgs" 36 + "nixpkgs": [ 37 + "nixpkgs" 38 + ] 37 39 }, 38 40 "locked": { 39 41 "lastModified": 1662220400, ··· 51 53 }, 52 54 "nixpkgs": { 53 55 "locked": { 54 - "lastModified": 1663634720, 55 - "narHash": "sha256-wMYXWKgysznBUHmvq5fN0uzUTNpnBLREPupM17xP8MY=", 56 - "owner": "NixOS", 57 - "repo": "nixpkgs", 58 - "rev": "998f0f7924198b2460458728de59fe738997f28e", 59 - "type": "github" 60 - }, 61 - "original": { 62 - "id": "nixpkgs", 63 - "type": "indirect" 64 - } 65 - }, 66 - "nixpkgs_2": { 67 - "locked": { 68 - "lastModified": 1663494472, 69 - "narHash": "sha256-fSowlaoXXWcAM8m9wA6u+eTJJtvruYHMA+Lb/tFi/qM=", 56 + "lastModified": 1669230746, 57 + "narHash": "sha256-+rU3DixJnygpJsGhhp6OvViruJux4TaiCz4IO+DdtZU=", 70 58 "owner": "NixOS", 71 59 "repo": "nixpkgs", 72 - "rev": "f677051b8dc0b5e2a9348941c99eea8c4b0ff28f", 60 + "rev": "42337aad353c5efff4382d7bf99deda491459845", 73 61 "type": "github" 74 62 }, 75 63 "original": { ··· 83 71 "flake-compat": "flake-compat", 84 72 "flake-utils": "flake-utils", 85 73 "naersk": "naersk", 86 - "nixpkgs": "nixpkgs_2" 74 + "nixpkgs": "nixpkgs" 87 75 } 88 76 } 89 77 },
+72 -7
flake.nix
··· 8 8 flake = false; 9 9 }; 10 10 flake-utils.url = "github:numtide/flake-utils"; 11 - naersk.url = "github:nix-community/naersk"; 11 + naersk = { 12 + url = "github:nix-community/naersk"; 13 + inputs.nixpkgs.follows = "nixpkgs"; 14 + }; 12 15 }; 13 16 14 17 outputs = { self, nixpkgs, flake-utils, naersk, ... }: ··· 17 20 pkgs = import nixpkgs { inherit system; }; 18 21 naersk-lib = naersk.lib."${system}"; 19 22 src = ./.; 23 + 24 + tex = with pkgs; 25 + texlive.combine { inherit (texlive) scheme-medium bitter titlesec; }; 20 26 in rec { 21 27 packages = rec { 22 28 bin = naersk-lib.buildPackage { 23 29 pname = "xesite-bin"; 24 30 root = src; 25 - buildInputs = with pkgs; [ pkg-config openssl git ]; 31 + buildInputs = with pkgs; [ 32 + pkg-config 33 + openssl 34 + git 35 + deno 36 + nodePackages.uglify-js 37 + ]; 26 38 }; 27 39 28 40 config = pkgs.stdenv.mkDerivation { 29 41 pname = "xesite-config"; 30 42 inherit (bin) version; 31 43 inherit src; 32 - buildInputs = with pkgs; [ dhall ]; 44 + buildInputs = with pkgs; [ dhall dhallPackages.Prelude ]; 33 45 34 46 phases = "installPhase"; 35 47 36 48 installPhase = '' 37 - cd $src 38 49 mkdir -p $out 39 - dhall resolve < $src/config.dhall >> $out/config.dhall 50 + cp -rf ${pkgs.dhallPackages.Prelude}/.cache .cache 51 + chmod -R u+w .cache 52 + export XDG_CACHE_HOME=.cache 53 + export DHALL_PRELUDE=${pkgs.dhallPackages.Prelude}/binary.dhall; 54 + dhall resolve --file $src/config.dhall >> $out/config.dhall 55 + ''; 56 + }; 57 + 58 + resumePDF = pkgs.stdenv.mkDerivation { 59 + pname = "xesite-resume-pdf"; 60 + inherit (bin) version; 61 + inherit src; 62 + buildInputs = with pkgs; [ dhall dhallPackages.Prelude tex pandoc ]; 63 + 64 + phases = "installPhase"; 65 + 66 + installPhase = '' 67 + mkdir -p $out/static/resume 68 + cp -rf ${pkgs.dhallPackages.Prelude}/.cache .cache 69 + chmod -R u+w .cache 70 + export XDG_CACHE_HOME=.cache 71 + export DHALL_PRELUDE=${pkgs.dhallPackages.Prelude}/binary.dhall; 72 + 73 + ln -s $src/dhall/latex/resume.cls 74 + dhall text --file $src/dhall/latex/resume.dhall > resume.tex 75 + 76 + xelatex ./resume.tex 77 + cp resume.pdf $out/static/resume/resume.pdf 40 78 ''; 41 79 }; 42 80 81 + frontend = pkgs.stdenv.mkDerivation { 82 + pname = "xesite-frontend"; 83 + inherit (bin) version; 84 + src = ./src/frontend; 85 + buildInputs = with pkgs; [ deno nodePackages.uglify-js ]; 86 + 87 + phases = "installPhase"; 88 + 89 + installPhase = '' 90 + mkdir -p $out/static/js 91 + mkdir -p .deno 92 + export HOME=./.deno 93 + 94 + deno bundle --config $src/deno.json $src/mastodon_share_button.tsx ./mastodon_share_button.js 95 + 96 + uglifyjs ./mastodon_share_button.js -c -m > $out/static/js/mastodon_share_button.js 97 + ''; 98 + }; 99 + 43 100 static = pkgs.stdenv.mkDerivation { 44 101 pname = "xesite-static"; 45 102 inherit (bin) version; ··· 72 129 73 130 default = pkgs.symlinkJoin { 74 131 name = "xesite-${bin.version}"; 75 - paths = [ config posts static bin ]; 132 + paths = [ config posts static bin frontend resumePDF ]; 76 133 }; 77 134 78 135 docker = pkgs.dockerTools.buildLayeredImage { ··· 100 157 openssl 101 158 pkg-config 102 159 103 - # kubernetes deployment 160 + # dhall 104 161 dhall 105 162 dhall-json 163 + dhall-lsp-server 164 + tex 165 + pandoc 166 + 167 + # frontend 168 + deno 169 + nodePackages.uglify-js 106 170 107 171 # dependency manager 108 172 niv ··· 117 181 RUST_LOG = "debug"; 118 182 RUST_BACKTRACE = "1"; 119 183 GITHUB_SHA = "devel"; 184 + DHALL_PRELUDE = "${pkgs.dhallPackages.Prelude}"; 120 185 }; 121 186 122 187 nixosModules.bot = { config, lib, ... }:
+78 -12
src/app/config.rs
··· 1 1 use crate::signalboost::Person; 2 - use maud::{html, Markup}; 2 + use maud::{html, Markup, Render}; 3 3 use serde::{Deserialize, Serialize}; 4 4 use std::{ 5 + collections::HashMap, 5 6 fmt::{self, Display}, 6 7 path::PathBuf, 7 8 }; ··· 9 10 #[derive(Clone, Deserialize, Default)] 10 11 pub struct Config { 11 12 pub signalboost: Vec<Person>, 12 - pub authors: Vec<Author>, 13 + pub authors: HashMap<String, Author>, 14 + #[serde(rename = "defaultAuthor")] 15 + pub default_author: Author, 13 16 pub port: u16, 14 17 #[serde(rename = "clackSet")] 15 18 pub clack_set: Vec<String>, ··· 19 22 pub mi_token: String, 20 23 #[serde(rename = "jobHistory")] 21 24 pub job_history: Vec<Job>, 25 + #[serde(rename = "seriesDescriptions")] 26 + pub series_descriptions: Vec<SeriesDescription>, 27 + #[serde(rename = "seriesDescMap")] 28 + pub series_desc_map: HashMap<String, String>, 29 + #[serde(rename = "notableProjects")] 30 + pub notable_projects: Vec<Link>, 31 + #[serde(rename = "contactLinks")] 32 + pub contact_links: Vec<Link>, 33 + } 34 + 35 + #[derive(Clone, Deserialize, Serialize, Default)] 36 + pub struct Link { 37 + pub url: String, 38 + pub title: String, 39 + pub description: String, 40 + } 41 + 42 + impl Render for Link { 43 + fn render(&self) -> Markup { 44 + html! { 45 + span { 46 + a href=(self.url) {(self.title)} 47 + @if !self.description.is_empty() { 48 + ": " 49 + (self.description) 50 + } 51 + } 52 + } 53 + } 22 54 } 23 55 24 56 #[derive(Clone, Deserialize, Serialize)] ··· 33 65 } 34 66 } 35 67 68 + fn schema_context() -> String { 69 + "http://schema.org/".to_string() 70 + } 71 + 72 + fn schema_person_type() -> String { 73 + "Person".to_string() 74 + } 75 + 36 76 #[derive(Clone, Deserialize, Serialize, Default)] 37 77 pub struct Author { 78 + #[serde(rename = "@context", default = "schema_context")] 79 + pub context: String, 80 + #[serde(rename = "@type", default = "schema_person_type")] 81 + pub schema_type: String, 38 82 pub name: String, 83 + #[serde(skip_serializing)] 39 84 pub handle: String, 40 - #[serde(rename = "picUrl")] 85 + #[serde(rename = "image", skip_serializing_if = "Option::is_none")] 41 86 pub pic_url: Option<String>, 42 - pub link: Option<String>, 43 - pub twitter: Option<String>, 44 - pub default: bool, 45 - #[serde(rename = "inSystem")] 87 + #[serde(rename = "inSystem", skip_serializing)] 46 88 pub in_system: bool, 89 + #[serde(rename = "jobTitle")] 90 + pub job_title: String, 91 + #[serde(rename = "sameAs")] 92 + pub same_as: Vec<String>, 93 + #[serde(skip_serializing_if = "Option::is_none")] 94 + pub url: Option<String>, 95 + } 96 + 97 + #[derive(Clone, Deserialize, Serialize, Default)] 98 + pub struct SeriesDescription { 99 + pub name: String, 100 + pub details: String, 101 + } 102 + 103 + impl Render for SeriesDescription { 104 + fn render(&self) -> Markup { 105 + html! { 106 + span { 107 + a href={"/blog/series/" (self.name)} { (self.name) } 108 + ": " 109 + (self.details) 110 + } 111 + } 112 + } 47 113 } 48 114 49 115 #[derive(Clone, Deserialize, Serialize, Default)] ··· 80 146 } 81 147 } 82 148 83 - impl Salary { 84 - pub fn html(&self) -> Markup { 149 + impl Render for Salary { 150 + fn render(&self) -> Markup { 85 151 if self.stock.is_none() { 86 152 return html! { (maud::display(self)) }; 87 153 } ··· 162 228 pub defunct: bool, 163 229 } 164 230 165 - impl Job { 166 - pub fn pay_history_row(&self) -> Markup { 231 + impl Render for Job { 232 + fn render(&self) -> Markup { 167 233 html! { 168 234 tr { 169 235 td { (self.title) } 170 236 td { (self.start_date) } 171 237 td { (self.end_date.as_ref().unwrap_or(&"current".to_string())) } 172 238 td { (if self.days_worked.is_some() { self.days_worked.as_ref().unwrap().to_string() } else { "n/a".to_string() }) } 173 - td { (self.salary.html()) } 239 + td { (self.salary) } 174 240 td { (self.leave_reason.as_ref().unwrap_or(&"n/a".to_string())) } 175 241 } 176 242 }
+2 -6
src/app/mod.rs
··· 1 1 use crate::{post::Post, signalboost::Person}; 2 2 use chrono::prelude::*; 3 3 use color_eyre::eyre::Result; 4 - use std::{fs, path::PathBuf, sync::Arc}; 4 + use std::{path::PathBuf, sync::Arc}; 5 5 use tracing::{error, instrument}; 6 6 7 7 pub mod config; ··· 48 48 pub struct State { 49 49 pub cfg: Arc<Config>, 50 50 pub signalboost: Vec<Person>, 51 - pub resume: String, 52 51 pub blog: Vec<Post>, 53 52 pub gallery: Vec<Post>, 54 53 pub talks: Vec<Post>, ··· 62 61 pub async fn init(cfg: PathBuf) -> Result<State> { 63 62 let cfg: Arc<Config> = Arc::new(serde_dhall::from_file(cfg).parse()?); 64 63 let sb = cfg.signalboost.clone(); 65 - let resume = fs::read_to_string(cfg.clone().resume_fname.clone())?; 66 - let resume: String = xesite_markdown::render(&resume)?; 67 64 let mi = mi::Client::new( 68 65 cfg.clone().mi_token.clone(), 69 66 crate::APPLICATION_NAME.to_string(), ··· 85 82 everything.sort(); 86 83 everything.reverse(); 87 84 88 - let today = Utc::today(); 85 + let today = Utc::now().date_naive(); 89 86 let everything: Vec<Post> = everything 90 87 .into_iter() 91 88 .filter(|p| today.num_days_from_ce() >= p.date.num_days_from_ce()) ··· 141 138 mi, 142 139 cfg, 143 140 signalboost: sb, 144 - resume, 145 141 blog, 146 142 gallery, 147 143 talks,
+6
src/frontend/build.sh
··· 1 + #!/usr/bin/env bash 2 + 3 + set -e 4 + 5 + export RUST_LOG=info 6 + deno bundle ./mastodon_share_button.tsx ../../static/js/mastodon_share_button.js
+8
src/frontend/deno.json
··· 1 + { 2 + "compilerOptions": { 3 + "jsx": "react-jsx", 4 + "jsxImportSource": "xeact", 5 + "lib": ["esnext", "dom", "dom.iterable"] 6 + }, 7 + "importMap": "./import_map.json", 8 + }
+8
src/frontend/import_map.json
··· 1 + { 2 + "imports": { 3 + "xeact": "./xeact/xeact.ts", 4 + "xeact/jsx-runtime": "./xeact/jsx-runtime.js", 5 + "/": "./", 6 + "./": "./" 7 + } 8 + }
+55
src/frontend/mastodon_share_button.tsx
··· 1 + import { g, r, u, x } from "xeact"; 2 + 3 + r(() => { 4 + const root = g("mastodon_share_button"); 5 + 6 + let defaultURL = localStorage["mastodon_instance"]; 7 + 8 + const title = document.querySelectorAll('meta[property="og:title"]')[0] 9 + .getAttribute("content"); 10 + let series = g("mastodon_share_series").innerText; 11 + if (series != "") { 12 + series = `#${series} `; 13 + } 14 + const tags = g("mastodon_share_tags"); 15 + const articleURL = u(); 16 + 17 + const tootTemplate = `${title} 18 + 19 + ${articleURL} 20 + 21 + ${series}${tags.innerText} @cadey@pony.social`; 22 + 23 + const instanceBox = ( 24 + <input type="text" placeholder="https://pony.social" value={defaultURL} /> 25 + ); 26 + const tootBox = <textarea rows="6" cols="40">{tootTemplate}</textarea>; 27 + 28 + const doShare = () => { 29 + const instanceURL = instanceBox.value; 30 + localStorage["mastodon_instance"] = instanceURL; 31 + const text = tootBox.value; 32 + const mastodon_url = u(instanceURL + "/share", { text }); 33 + console.log({ text, mastodon_url }); 34 + window.open(mastodon_url, "_blank"); 35 + }; 36 + 37 + const shareButton = <button onclick={doShare}>Share</button>; 38 + 39 + x(root); 40 + 41 + root.appendChild( 42 + <div> 43 + <details> 44 + <summary>Share on Mastodon</summary> 45 + <span>Instance URL (https://mastodon.example)</span> 46 + <br /> 47 + {instanceBox} 48 + <br /> 49 + {tootBox} 50 + <br /> 51 + {shareButton} 52 + </details> 53 + </div>, 54 + ); 55 + });
+16
src/frontend/xeact/jsx-runtime.js
··· 1 + import { h } from './xeact.ts'; 2 + 3 + /** 4 + * Create a DOM element, assign the properties of `data` to it, and append all `data.children`. 5 + * 6 + * @type{function(string, Object=): HTMLElement} 7 + */ 8 + export const jsx = (tag, data) => { 9 + let children = data.children; 10 + delete data.children; 11 + const result = h(tag, data, children); 12 + result.classList.value = result.class; 13 + return result; 14 + }; 15 + export const jsxs = jsx; 16 + export const jsxDEV = jsx;
+88
src/frontend/xeact/xeact.js
··· 1 + /** 2 + * Creates a DOM element, assigns the properties of `data` to it, and appends all `children`. 3 + * 4 + * @type{function(string|Function, Object=, Node|Array.<Node|string>=)} 5 + */ 6 + const h = (name, data = {}, children = []) => { 7 + const result = typeof name == "function" ? name(data) : Object.assign(document.createElement(name), data); 8 + if (!Array.isArray(children)) { 9 + children = [children]; 10 + } 11 + result.append(...children); 12 + return result; 13 + }; 14 + 15 + /** 16 + * Create a text node. 17 + * 18 + * Equivalent to `document.createTextNode(text)` 19 + * 20 + * @type{function(string): Text} 21 + */ 22 + const t = (text) => document.createTextNode(text); 23 + 24 + /** 25 + * Remove all child nodes from a DOM element. 26 + * 27 + * @type{function(Node)} 28 + */ 29 + const x = (elem) => { 30 + while (elem.lastChild) { 31 + elem.removeChild(elem.lastChild); 32 + } 33 + }; 34 + 35 + /** 36 + * Get all elements with the given ID. 37 + * 38 + * Equivalent to `document.getElementById(name)` 39 + * 40 + * @type{function(string): HTMLElement} 41 + */ 42 + const g = (name) => document.getElementById(name); 43 + 44 + /** 45 + * Get all elements with the given class name. 46 + * 47 + * Equivalent to `document.getElementsByClassName(name)` 48 + * 49 + * @type{function(string): HTMLCollectionOf.<Element>} 50 + */ 51 + const c = (name) => document.getElementsByClassName(name); 52 + 53 + /** @type{function(string): HTMLCollectionOf.<Element>} */ 54 + const n = (name) => document.getElementsByName(name); 55 + 56 + /** 57 + * Get all elements matching the given HTML selector. 58 + * 59 + * Matches selectors with `document.querySelectorAll(selector)` 60 + * 61 + * @type{function(string): Array.<HTMLElement>} 62 + */ 63 + const s = (selector) => Array.from(document.querySelectorAll(selector)); 64 + 65 + /** 66 + * Generate a relative URL from `url`, appending all key-value pairs from `params` as URL-encoded parameters. 67 + * 68 + * @type{function(string=, Object=): string} 69 + */ 70 + const u = (url = "", params = {}) => { 71 + let result = new URL(url, window.location.href); 72 + Object.entries(params).forEach((kv) => { 73 + let [k, v] = kv; 74 + result.searchParams.set(k, v); 75 + }); 76 + return result.toString(); 77 + }; 78 + 79 + /** 80 + * Takes a callback to run when all DOM content is loaded. 81 + * 82 + * Equivalent to `window.addEventListener('DOMContentLoaded', callback)` 83 + * 84 + * @type{function(function())} 85 + */ 86 + const r = (callback) => window.addEventListener('DOMContentLoaded', callback); 87 + 88 + export { h, t, x, g, c, n, u, s, r };
+9
src/frontend/xeact/xeact.ts
··· 1 + export * from "./xeact.js"; 2 + 3 + declare global { 4 + export namespace JSX { 5 + interface IntrinsicElements { 6 + [elemName: string]: any; 7 + } 8 + } 9 + }
+32 -34
src/handlers/blog.rs
··· 1 1 use super::Result; 2 - use crate::{app::State, post::Post, templates}; 2 + use crate::{app::State, post::Post, tmpl}; 3 3 use axum::{ 4 4 extract::{Extension, Path}, 5 - response::Html, 5 + http::StatusCode, 6 6 }; 7 7 use http::HeaderMap; 8 8 use lazy_static::lazy_static; 9 + use maud::Markup; 9 10 use prometheus::{opts, register_int_counter_vec, IntCounterVec}; 10 11 use std::sync::Arc; 11 - use tracing::{error, instrument}; 12 + use tracing::instrument; 12 13 13 14 lazy_static! { 14 15 static ref HIT_COUNTER: IntCounterVec = register_int_counter_vec!( ··· 19 20 } 20 21 21 22 #[instrument(skip(state))] 22 - pub async fn index(Extension(state): Extension<Arc<State>>) -> Result { 23 + pub async fn index(Extension(state): Extension<Arc<State>>) -> Result<Markup> { 23 24 let state = state.clone(); 24 - let mut result: Vec<u8> = vec![]; 25 - templates::blogindex_html(&mut result, state.blog.clone())?; 26 - Ok(Html(result)) 25 + let result = tmpl::post_index(&state.blog, "Blogposts", true); 26 + Ok(result) 27 27 } 28 28 29 29 #[instrument(skip(state))] 30 - pub async fn series(Extension(state): Extension<Arc<State>>) -> Result { 30 + pub async fn series(Extension(state): Extension<Arc<State>>) -> Result<Markup> { 31 31 let state = state.clone(); 32 - let mut series: Vec<String> = vec![]; 33 - let mut result: Vec<u8> = vec![]; 34 32 35 - for post in &state.blog { 36 - if post.front_matter.series.is_some() { 37 - series.push(post.front_matter.series.as_ref().unwrap().clone()); 38 - } 39 - } 40 - 41 - series.sort(); 42 - series.dedup(); 43 - 44 - templates::series_html(&mut result, series)?; 45 - Ok(Html(result)) 33 + Ok(tmpl::blog_series(&state.cfg.clone().series_descriptions)) 46 34 } 47 35 48 36 #[instrument(skip(state))] 49 37 pub async fn series_view( 50 38 Path(series): Path<String>, 51 39 Extension(state): Extension<Arc<State>>, 52 - ) -> Result { 40 + ) -> (StatusCode, Markup) { 53 41 let state = state.clone(); 42 + let cfg = state.cfg.clone(); 54 43 let mut posts: Vec<Post> = vec![]; 55 - let mut result: Vec<u8> = vec![]; 56 44 57 45 for post in &state.blog { 58 46 if post.front_matter.series.is_none() { ··· 64 52 posts.push(post.clone()); 65 53 } 66 54 55 + posts.reverse(); 56 + 57 + let desc = cfg.series_desc_map.get(&series); 58 + 67 59 if posts.len() == 0 { 68 - error!("series not found"); 69 - return Err(super::Error::SeriesNotFound(series)); 60 + ( 61 + StatusCode::NOT_FOUND, 62 + tmpl::error(format!("series not found: {series}")), 63 + ) 64 + } else { 65 + if let Some(desc) = desc { 66 + (StatusCode::OK, tmpl::series_view(&series, desc, &posts)) 67 + } else { 68 + ( 69 + StatusCode::INTERNAL_SERVER_ERROR, 70 + tmpl::error(format!("series metadata in dhall not found: {series}")), 71 + ) 72 + } 70 73 } 71 - 72 - templates::series_posts_html(&mut result, series, &posts).unwrap(); 73 - Ok(Html(result)) 74 74 } 75 75 76 76 #[instrument(skip(state, headers))] ··· 78 78 Path(name): Path<String>, 79 79 Extension(state): Extension<Arc<State>>, 80 80 headers: HeaderMap, 81 - ) -> Result { 81 + ) -> Result<(StatusCode, Markup)> { 82 82 let mut want: Option<Post> = None; 83 83 let want_link = format!("blog/{}", name); 84 84 ··· 96 96 }; 97 97 98 98 match want { 99 - None => Err(super::Error::PostNotFound(name)), 99 + None => Ok((StatusCode::NOT_FOUND, tmpl::not_found(want_link))), 100 100 Some(post) => { 101 101 HIT_COUNTER 102 102 .with_label_values(&[name.clone().as_str()]) 103 103 .inc(); 104 - let body = templates::Html(post.body_html.clone()); 105 - let mut result: Vec<u8> = vec![]; 106 - templates::blogpost_html(&mut result, post, body, referer)?; 107 - Ok(Html(result)) 104 + let body = maud::PreEscaped(&post.body_html); 105 + Ok((StatusCode::OK, tmpl::blog::blog(&post, body, referer))) 108 106 } 109 107 } 110 108 }
+11 -17
src/handlers/gallery.rs
··· 1 - use super::{Error::*, Result}; 2 - use crate::{app::State, post::Post, templates}; 3 - use axum::{ 4 - extract::{Extension, Path}, 5 - response::Html, 6 - }; 1 + use crate::{app::State, post::Post, tmpl}; 2 + use axum::extract::{Extension, Path}; 3 + use http::StatusCode; 7 4 use lazy_static::lazy_static; 5 + use maud::Markup; 8 6 use prometheus::{opts, register_int_counter_vec, IntCounterVec}; 9 7 use std::sync::Arc; 10 8 use tracing::instrument; ··· 18 16 } 19 17 20 18 #[instrument(skip(state))] 21 - pub async fn index(Extension(state): Extension<Arc<State>>) -> Result { 19 + pub async fn index(Extension(state): Extension<Arc<State>>) -> Markup { 22 20 let state = state.clone(); 23 - let mut result: Vec<u8> = vec![]; 24 - templates::galleryindex_html(&mut result, state.gallery.clone())?; 25 - Ok(Html(result)) 21 + tmpl::gallery_index(&state.gallery) 26 22 } 27 23 28 24 #[instrument(skip(state))] 29 25 pub async fn post_view( 30 26 Path(name): Path<String>, 31 27 Extension(state): Extension<Arc<State>>, 32 - ) -> Result { 28 + ) -> (StatusCode, Markup) { 33 29 let mut want: Option<Post> = None; 30 + let link = format!("gallery/{}", name); 34 31 35 32 for post in &state.gallery { 36 - if post.link == format!("gallery/{}", name) { 33 + if post.link == link { 37 34 want = Some(post.clone()); 38 35 } 39 36 } 40 37 41 38 match want { 42 - None => Err(PostNotFound(name)), 39 + None => (StatusCode::NOT_FOUND, tmpl::not_found(link)), 43 40 Some(post) => { 44 41 HIT_COUNTER 45 42 .with_label_values(&[name.clone().as_str()]) 46 43 .inc(); 47 - let body = templates::Html(post.body_html.clone()); 48 - let mut result: Vec<u8> = vec![]; 49 - templates::gallerypost_html(&mut result, post, body)?; 50 - Ok(Html(result)) 44 + (StatusCode::OK, tmpl::blog::gallery(&post)) 51 45 } 52 46 } 53 47 }
+35 -42
src/handlers/mod.rs
··· 1 - use crate::{app::State, templates}; 1 + use crate::{app::State, tmpl}; 2 2 use axum::{ 3 3 body, 4 4 extract::Extension, ··· 7 7 }; 8 8 use chrono::{Datelike, Timelike, Utc, Weekday}; 9 9 use lazy_static::lazy_static; 10 + use maud::Markup; 10 11 use prometheus::{opts, register_int_counter_vec, IntCounterVec}; 11 12 use std::sync::Arc; 12 13 use tracing::instrument; ··· 67 68 }; 68 69 } 69 70 70 - #[instrument] 71 - pub async fn index() -> Result { 71 + #[instrument(skip(state))] 72 + pub async fn index(Extension(state): Extension<Arc<State>>) -> Result<Markup> { 72 73 HIT_COUNTER.with_label_values(&["index"]).inc(); 73 - let mut result: Vec<u8> = vec![]; 74 - templates::index_html(&mut result)?; 75 - Ok(Html(result)) 74 + let state = state.clone(); 75 + let cfg = state.cfg.clone(); 76 + 77 + Ok(tmpl::index(&cfg.default_author, &cfg.notable_projects)) 76 78 } 77 79 78 - #[instrument] 79 - pub async fn contact() -> Result { 80 + #[instrument(skip(state))] 81 + pub async fn contact(Extension(state): Extension<Arc<State>>) -> Markup { 80 82 HIT_COUNTER.with_label_values(&["contact"]).inc(); 81 - let mut result: Vec<u8> = vec![]; 82 - templates::contact_html(&mut result)?; 83 - Ok(Html(result)) 83 + let state = state.clone(); 84 + let cfg = state.cfg.clone(); 85 + 86 + crate::tmpl::contact(&cfg.contact_links) 84 87 } 85 88 86 89 #[instrument] 87 - pub async fn feeds() -> Result { 90 + pub async fn feeds() -> Markup { 88 91 HIT_COUNTER.with_label_values(&["feeds"]).inc(); 89 - let mut result: Vec<u8> = vec![]; 90 - templates::feeds_html(&mut result)?; 91 - Ok(Html(result)) 92 + crate::tmpl::feeds() 92 93 } 93 94 94 95 #[axum_macros::debug_handler] 95 96 #[instrument(skip(state))] 96 - pub async fn salary_transparency(Extension(state): Extension<Arc<State>>) -> Result { 97 + pub async fn salary_transparency(Extension(state): Extension<Arc<State>>) -> Result<Markup> { 97 98 HIT_COUNTER 98 99 .with_label_values(&["salary_transparency"]) 99 100 .inc(); 100 101 let state = state.clone(); 101 - let mut result: Vec<u8> = vec![]; 102 - templates::salary_transparency(&mut result, state.cfg.clone())?; 103 - Ok(Html(result)) 102 + let cfg = state.cfg.clone(); 103 + 104 + Ok(tmpl::salary_transparency(&cfg.job_history)) 104 105 } 105 106 106 107 #[axum_macros::debug_handler] 107 - #[instrument(skip(state))] 108 - pub async fn resume(Extension(state): Extension<Arc<State>>) -> Result { 108 + pub async fn resume() -> Markup { 109 109 HIT_COUNTER.with_label_values(&["resume"]).inc(); 110 - let state = state.clone(); 111 - let mut result: Vec<u8> = vec![]; 112 - templates::resume_html(&mut result, templates::Html(state.resume.clone()))?; 113 - Ok(Html(result)) 110 + 111 + tmpl::resume() 114 112 } 115 113 116 114 #[instrument(skip(state))] 117 - pub async fn patrons(Extension(state): Extension<Arc<State>>) -> Result { 115 + pub async fn patrons(Extension(state): Extension<Arc<State>>) -> (StatusCode, Markup) { 118 116 HIT_COUNTER.with_label_values(&["patrons"]).inc(); 119 117 let state = state.clone(); 120 - let mut result: Vec<u8> = vec![]; 121 118 match &state.patrons { 122 - None => Err(Error::NoPatrons), 123 - Some(patrons) => { 124 - templates::patrons_html(&mut result, patrons.clone())?; 125 - Ok(Html(result)) 126 - } 119 + None => ( 120 + StatusCode::INTERNAL_SERVER_ERROR, 121 + tmpl::error("Patreon API config is broken, no patrons in ram"), 122 + ), 123 + Some(patrons) => (StatusCode::IM_A_TEAPOT, tmpl::patrons(&patrons)), 127 124 } 128 125 } 129 126 130 127 #[axum_macros::debug_handler] 131 128 #[instrument(skip(state))] 132 - pub async fn signalboost(Extension(state): Extension<Arc<State>>) -> Result { 129 + pub async fn signalboost(Extension(state): Extension<Arc<State>>) -> Markup { 133 130 HIT_COUNTER.with_label_values(&["signalboost"]).inc(); 134 131 let state = state.clone(); 135 - let mut result: Vec<u8> = vec![]; 136 - templates::signalboost_html(&mut result, state.signalboost.clone())?; 137 - Ok(Html(result)) 132 + tmpl::signalboost(&state.signalboost) 138 133 } 139 134 140 135 #[instrument] 141 - pub async fn not_found() -> Result { 136 + pub async fn not_found(uri: axum::http::Uri) -> (StatusCode, Markup) { 142 137 HIT_COUNTER.with_label_values(&["not_found"]).inc(); 143 - let mut result: Vec<u8> = vec![]; 144 - templates::notfound_html(&mut result, "some path".into())?; 145 - Ok(Html(result)) 138 + (StatusCode::NOT_FOUND, tmpl::not_found(uri.path())) 146 139 } 147 140 148 141 #[derive(Debug, thiserror::Error)] ··· 170 163 171 164 impl IntoResponse for Error { 172 165 fn into_response(self) -> Response { 173 - let mut result: Vec<u8> = vec![]; 174 - templates::error_html(&mut result, format!("{}", self)).unwrap(); 166 + let result = tmpl::error(format!("{}", self)); 167 + let result = result.0; 175 168 176 169 let body = body::boxed(body::Full::from(result)); 177 170
+11 -17
src/handlers/talks.rs
··· 1 - use super::{Error::*, Result}; 2 - use crate::{app::State, post::Post, templates}; 3 - use axum::{ 4 - extract::{Extension, Path}, 5 - response::Html, 6 - }; 7 - use http::header::HeaderMap; 1 + use super::Result; 2 + use crate::{app::State, post::Post, tmpl}; 3 + use axum::extract::{Extension, Path}; 4 + use http::{header::HeaderMap, StatusCode}; 8 5 use lazy_static::lazy_static; 6 + use maud::Markup; 9 7 use prometheus::{opts, register_int_counter_vec, IntCounterVec}; 10 8 use std::sync::Arc; 11 9 use tracing::instrument; ··· 19 17 } 20 18 21 19 #[instrument(skip(state))] 22 - pub async fn index(Extension(state): Extension<Arc<State>>) -> Result { 20 + pub async fn index(Extension(state): Extension<Arc<State>>) -> Result<Markup> { 23 21 let state = state.clone(); 24 - let mut result: Vec<u8> = vec![]; 25 - templates::talkindex_html(&mut result, state.talks.clone())?; 26 - Ok(Html(result)) 22 + Ok(tmpl::post_index(&state.talks, "Talks", false)) 27 23 } 28 24 29 25 #[instrument(skip(state, headers))] ··· 31 27 Path(name): Path<String>, 32 28 Extension(state): Extension<Arc<State>>, 33 29 headers: HeaderMap, 34 - ) -> Result { 30 + ) -> Result<(StatusCode, Markup)> { 35 31 let mut want: Option<Post> = None; 36 32 let want_link = format!("talks/{}", name); 37 33 ··· 49 45 }; 50 46 51 47 match want { 52 - None => Err(PostNotFound(name).into()), 48 + None => Ok((StatusCode::NOT_FOUND, tmpl::not_found(want_link))), 53 49 Some(post) => { 54 50 HIT_COUNTER 55 51 .with_label_values(&[name.clone().as_str()]) 56 52 .inc(); 57 - let body = templates::Html(post.body_html.clone()); 58 - let mut result: Vec<u8> = vec![]; 59 - templates::talkpost_html(&mut result, post, body, referer)?; 60 - Ok(Html(result)) 53 + let body = maud::PreEscaped(&post.body_html); 54 + Ok((StatusCode::OK, tmpl::blog::talk(&post, body, referer))) 61 55 } 62 56 } 63 57 }
+5 -7
src/main.rs
··· 4 4 use axum::{ 5 5 body, 6 6 extract::Extension, 7 + handler::Handler, 7 8 http::header::{self, HeaderValue, CONTENT_TYPE}, 8 - response::{Html, Response}, 9 + response::Response, 9 10 routing::{get, get_service}, 10 11 Router, 11 12 }; ··· 211 212 ) 212 213 }), 213 214 ) 215 + .fallback(handlers::not_found.into_service()) 214 216 .layer(middleware); 215 217 216 218 #[cfg(target_os = "linux")] ··· 276 278 .unwrap() 277 279 } 278 280 279 - async fn go_vanity() -> Html<Vec<u8>> { 280 - let mut buffer: Vec<u8> = vec![]; 281 - templates::gitea_html( 282 - &mut buffer, 281 + async fn go_vanity() -> maud::Markup { 282 + tmpl::gitea( 283 283 "christine.website/jsonfeed", 284 284 "https://tulpa.dev/Xe/jsonfeed", 285 285 "master", 286 286 ) 287 - .unwrap(); 288 - Html(buffer) 289 287 } 290 288 291 289 include!(concat!(env!("OUT_DIR"), "/templates.rs"));
+20 -4
src/post/mod.rs
··· 6 6 use tokio::fs; 7 7 8 8 pub mod frontmatter; 9 + pub mod schemaorg; 9 10 10 11 #[derive(Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] 11 12 pub struct Post { ··· 24 25 pub title: String, 25 26 pub summary: String, 26 27 pub link: String, 28 + } 29 + 30 + impl Into<schemaorg::Article> for &Post { 31 + fn into(self) -> schemaorg::Article { 32 + schemaorg::Article { 33 + context: "https://schema.org".to_string(), 34 + r#type: "Article".to_string(), 35 + headline: self.front_matter.title.clone(), 36 + image: "https://xeiaso.net/static/img/avatar.png".to_string(), 37 + url: format!("https://xeiaso.net/{}", self.link), 38 + date_published: self.date.format("%Y-%m-%d").to_string(), 39 + } 40 + } 27 41 } 28 42 29 43 impl Into<xe_jsonfeed::Item> for Post { ··· 99 113 let link = format!("{}/{}", dir, fname.file_stem().unwrap().to_str().unwrap()); 100 114 let body_html = xesite_markdown::render(&body) 101 115 .wrap_err_with(|| format!("can't parse markdown for {:?}", fname))?; 102 - let date: DateTime<FixedOffset> = 103 - DateTime::<Utc>::from_utc(NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)), Utc) 104 - .with_timezone(&Utc) 105 - .into(); 116 + let date: DateTime<FixedOffset> = DateTime::<Utc>::from_utc( 117 + NaiveDateTime::new(date, NaiveTime::from_hms_opt(0, 0, 0).unwrap()), 118 + Utc, 119 + ) 120 + .with_timezone(&Utc) 121 + .into(); 106 122 107 123 let mentions: Vec<mi::WebMention> = match cli { 108 124 Some(cli) => cli
+14
src/post/schemaorg.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[derive(Eq, PartialEq, Debug, Clone, Serialize, Deserialize)] 4 + pub struct Article { 5 + #[serde(rename = "@context")] 6 + pub context: String, 7 + #[serde(rename = "@type")] 8 + pub r#type: String, 9 + pub headline: String, 10 + pub image: String, 11 + pub url: String, 12 + #[serde(rename = "datePublished")] 13 + pub date_published: String, 14 + }
+3 -9
src/signalboost.rs
··· 1 + use crate::app::config::Link; 1 2 use serde::Deserialize; 2 3 3 - #[derive(Clone, Debug, Deserialize)] 4 + #[derive(Clone, Deserialize)] 4 5 pub struct Person { 5 6 pub name: String, 6 7 pub tags: Vec<String>, 7 - #[serde(rename = "gitLink")] 8 - pub git_link: Option<String>, 9 - pub twitter: Option<String>, 10 - pub linkedin: Option<String>, 11 - pub fediverse: Option<String>, 12 - #[serde(rename = "coverLetter")] 13 - pub cover_letter: Option<String>, 14 - pub website: Option<String>, 8 + pub links: Vec<Link>, 15 9 } 16 10 17 11 #[cfg(test)]
+45
src/tmpl/asciiart.txt
··· 1 + <!-- 2 + MMMMMMMMMMMMMMMMMMNmmNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmmd.:mmMM 3 + MMMMMMMMMMMMMMMMMNmmmNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmydmmmmmNMM 4 + MMMMMMMMMMMMMMMMNm/:mNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmms /mmmmmMMM 5 + MMMMMMMMMMMMMMMNmm:-dmMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmmmdsdmmmmNMMM 6 + MMMMMMMMMMMMMMMmmmmmmmNMMMMMMMMMMMNmmdhhddhhmNNMMMMMMMMMMMMMMMMNmy:hmmmmmmmmMMMM 7 + MMMMMMMMMMMMMMNm++mmmmNMMMMMMmdyo/::.........-:/sdNMMMMMMMMMMNmmms`smmmmmmmNMMMM 8 + MMMMMMMMMMMMMMmd.-dmmmmMMmhs/-....................-+dNMMMMMMNmmmmmmmmmmmmmmMMMMM 9 + MMMMMMMMMMMMMNmmmmmmmmho:-...........................:sNMMNmmmmmmmmmmmmmmmNMNmdd 10 + MMMMMMMMMMMMNmd+ydhs/-.................................-sNmmmmmmmmmmmmmmmdhyssss 11 + MMMMMMMMMMMNNh+`........................................:dmmmmmmmmmmmmmmmyssssss 12 + MMMMNNdhy+:-...........................................+dmmmmmmmmmmmmmmmdsssssss 13 + MMMN+-...............................................-smmmmmmmmmmmmmmmmmysyyhdmN 14 + MMMMNho:::-.--::-.......................----------..:hmmmmmmmmmmmmmmmmmmmNMMMMMM 15 + MMMMMMMMNNNmmdo:......................--------------:ymmmmmmmmmmmmmmmmmmmMMMMMMM 16 + MMMMMMMMMMds+........................-----------------+dmmmmmmmmmmmmmmmmmMMMMMMM 17 + MMMMMMMMMh+........................--------------------:smmmmmmmmmmmmmmNMMMMMMMM 18 + MMMMMMMNy/........................-------------::--------/hmmmmmmmmmmmNMMMMMMNmd 19 + MMMMMMMd/........................--------------so----------odmmmmmmmmMMNmdhhysss 20 + MMMMMMm/........................--------------+mh-----------:ymmmmdhhyysssssssss 21 + MMMMMMo.......................---------------:dmmo------------+dmdysssssssssssss 22 + yhdmNh:......................---------------:dmmmm+------------:sssssssssssyhhdm 23 + sssssy.......................--------------:hmmmmmmos++:---------/sssyyhdmNMMMMM 24 + ssssso......................--------------:hmmmNNNMNdddysso:------:yNNMMMMMMMMMM 25 + ysssss.....................--------------/dmNyy/mMMd``d/------------sNMMMMMMMMMM 26 + MNmdhy-...................--------------ommmh`o/NM/. smh+-----------:yNMMMMMMMMM 27 + MMMMMN+...................------------/hmmss: `-//-.smmmmd+----------:hMMMMMMMMM 28 + MMMMMMd:..................----------:smmmmhy+oosyysdmmy+:. `.--------/dMMMMMMMM 29 + MMMMMMMh-................---------:smmmmmmmmmmmmmmmh/` `/s:-------sMMMMMMMM 30 + MMMMMMMms:...............-------/ymmmmmmmmmmmmmmmd/ :dMMNy/-----+mMMMMMMM 31 + MMMMMMmyss/..............------ommmmmmmmmmmmmmmmd. :yMMMMMMNs:---+mMMMMMMM 32 + MMMMNdssssso-............----..odmmmmmmmmmmmmmmh:.` .sNMMMMMMMMMd/--sMMMMMMMM 33 + MMMmysssssssh/................` -odmmmmmmmmmh+. `omMMMMMMMMMMMMh/+mMMMMMMMM 34 + MNdyssssssymMNy-.............. `/sssso+:. `+mMMMMMMMMMMMMMMMdNMMMMMMMMM 35 + NhssssssshNMMMMNo:............/.` `+dMMMMMMMMMMMMMMMMMMMMMMMMMMMM 36 + ysssssssdMMMMMMMMm+-..........+ddy/.` -omMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 37 + ssssssymMMMMMMMMMMMh/.........-oNMMNmy+--` `-+dNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 38 + ssssydNMMMMMMMMMMMMMNy:........-hMMMMMMMNmdmMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 39 + sssymMMMMMMMMMMMMMMMMMm+....-..:hMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 40 + symNMMMMMMMMMMMMMMMMMMMNo.../-/dMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 41 + dNMMMMMMMMMMMMMMMMMMMMMMh:.:hyNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 42 + la budza pu cusku lu 43 + <<.i do snura .i ko kanro 44 + .i do panpi .i ko gleki>> li'u 45 + -->
+215
src/tmpl/blog.rs
··· 1 + use super::{base, nag}; 2 + use crate::post::{schemaorg::Article, Post}; 3 + use maud::{html, Markup, PreEscaped}; 4 + 5 + fn post_metadata(post: &Post) -> Markup { 6 + let art: Article = post.into(); 7 + let json = PreEscaped(serde_json::to_string(&art).unwrap()); 8 + 9 + html! { 10 + meta name="twitter:card" content="summary"; 11 + meta name="twitter:site" content="@theprincessxena"; 12 + meta name="twitter:title" content={(post.front_matter.title)}; 13 + meta property="og:type" content="website"; 14 + meta property="og:title" content={(post.front_matter.title)}; 15 + meta property="og:site_name" content="Xe's Blog"; 16 + meta name="description" content={(post.front_matter.title) " - Xe's Blog"}; 17 + meta name="author" content="Xe Iaso"; 18 + 19 + @if let Some(redirect_to) = &post.front_matter.redirect_to { 20 + link rel="canonical" href=(redirect_to); 21 + meta http-equiv="refresh" content=(format!("0;URL='{redirect_to}'")); 22 + } @else { 23 + link rel="canonical" href={"https://xeiaso.net/" (post.link)}; 24 + } 25 + 26 + script type="application/ld+json" {(json)} 27 + } 28 + } 29 + 30 + fn share_button(post: &Post) -> Markup { 31 + html! { 32 + div # mastodon_share_button {} 33 + div # mastodon_share_series style="display:none" {(post.front_matter.series.as_ref().unwrap_or(&"".to_string()))} 34 + div # mastodon_share_tags style="display:none" {@for tag in post.front_matter.tags.as_ref().unwrap_or(&Vec::new()) {"#" (tag) " "}} 35 + script r#type="module" src="/static/js/mastodon_share_button.js" {} 36 + } 37 + } 38 + 39 + fn twitch_vod(post: &Post) -> Markup { 40 + html! { 41 + @if let Some(vod) = &post.front_matter.vod { 42 + p { 43 + "This post was written live on " 44 + a href="https://twitch.tv/princessxen" {"Twitch"} 45 + ". You can check out the stream recording on " 46 + a href=(vod.twitch) {"Twitch"} 47 + " and on " 48 + a href=(vod.youtube) {"YouTube"} 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 + } 51 + } 52 + } 53 + } 54 + 55 + pub fn blog(post: &Post, body: PreEscaped<&String>, referer: Option<String>) -> Markup { 56 + base( 57 + Some(&post.front_matter.title), 58 + None, 59 + html! { 60 + (post_metadata(post)) 61 + (nag::referer(referer)) 62 + 63 + article { 64 + h1 {(post.front_matter.title)} 65 + 66 + (nag::prerelease(post)) 67 + 68 + small { 69 + "Read time in minutes: " 70 + (post.read_time_estimate_minutes) 71 + } 72 + 73 + (body) 74 + } 75 + 76 + (share_button(post)) 77 + (twitch_vod(post)) 78 + 79 + p { 80 + "This article was posted on " 81 + (post.detri()) 82 + ". Facts and circumstances may have changed since publication Please " 83 + a href="/contact" {"contact me"} 84 + " before jumping to conclusions if something seems wrong or unclear." 85 + } 86 + 87 + @if let Some(series) = &post.front_matter.series { 88 + p { 89 + "Series: " 90 + a href={"/blog/series/" (series)} {(series)} 91 + } 92 + } 93 + 94 + @if let Some(tags) = &post.front_matter.tags { 95 + p { 96 + "Tags: " 97 + @for tag in tags { 98 + code {(tag)} 99 + " " 100 + } 101 + } 102 + } 103 + 104 + @if post.mentions.is_empty() { 105 + p { 106 + "This post was not " 107 + a href="https://www.w3.org/TR/webmention/" {"WebMention"} 108 + "ed yet. You could be the first!" 109 + } 110 + } @else { 111 + ul { 112 + @for mention in &post.mentions { 113 + li { 114 + a href=(mention.source) {(mention.title.as_ref().unwrap_or(&mention.source))} 115 + } 116 + } 117 + } 118 + } 119 + 120 + p { 121 + "The art for Mara was drawn by " 122 + a href="https://selic.re/" {"Selicre"} 123 + "." 124 + } 125 + 126 + p { 127 + "The art for Cadey was drawn by " 128 + a href="https://artzorastudios.weebly.com/" {"ArtZorea Studios"} 129 + "." 130 + } 131 + }, 132 + ) 133 + } 134 + 135 + pub fn gallery(post: &Post) -> Markup { 136 + base( 137 + Some(&post.front_matter.title), 138 + None, 139 + html! { 140 + (post_metadata(post)) 141 + h1 {(post.front_matter.title)} 142 + 143 + (PreEscaped(&post.body_html)) 144 + 145 + center { 146 + img src=(post.front_matter.image.as_ref().unwrap()); 147 + } 148 + 149 + hr; 150 + 151 + p { 152 + "This artwork was posted on " 153 + (post.detri()) 154 + "." 155 + } 156 + 157 + @if let Some(tags) = &post.front_matter.tags { 158 + p { 159 + "Tags: " 160 + @for tag in tags { 161 + code {(tag)} 162 + " " 163 + } 164 + } 165 + } 166 + 167 + (share_button(post)) 168 + }, 169 + ) 170 + } 171 + 172 + pub fn talk(post: &Post, body: PreEscaped<&String>, referer: Option<String>) -> Markup { 173 + base( 174 + Some(&post.front_matter.title), 175 + None, 176 + html! { 177 + (post_metadata(post)) 178 + (nag::referer(referer)) 179 + 180 + article { 181 + {(post.front_matter.title)} 182 + 183 + (nag::prerelease(post)) 184 + 185 + (body) 186 + } 187 + 188 + @if let Some(slides) = &post.front_matter.slides_link { 189 + a href=(slides) {"Link to the slides"} 190 + } 191 + 192 + (share_button(post)) 193 + 194 + p { 195 + "This talk was posted on " 196 + (post.detri()) 197 + ". Facts and circumstances may have changed since publication Please " 198 + a href="/contact" {"contact me"} 199 + " before jumping to conclusions if something seems wrong or unclear." 200 + } 201 + 202 + p { 203 + "The art for Mara was drawn by " 204 + a href="https://selic.re/" {"Selicre"} 205 + "." 206 + } 207 + 208 + p { 209 + "The art for Cadey was drawn by " 210 + a href="https://artzorastudios.weebly.com/" {"ArtZorea Studios"} 211 + "." 212 + } 213 + }, 214 + ) 215 + }
+567 -6
src/tmpl/mod.rs
··· 1 - use crate::app::Config; 2 - use maud::{html, Markup}; 3 - use std::sync::Arc; 1 + use crate::{app::*, post::Post, signalboost::Person}; 2 + use chrono::prelude::*; 3 + use lazy_static::lazy_static; 4 + use maud::{html, Markup, PreEscaped, Render, DOCTYPE}; 5 + use patreon::Users; 4 6 7 + pub mod blog; 5 8 pub mod nag; 6 9 7 - pub fn salary_history(cfg: Arc<Config>) -> Markup { 10 + lazy_static! { 11 + static ref CACHEBUSTER: String = uuid::Uuid::new_v4().to_string().replace("-", ""); 12 + } 13 + 14 + pub fn base(title: Option<&str>, styles: Option<&str>, content: Markup) -> Markup { 15 + let now = Utc::now(); 16 + html! { 17 + (DOCTYPE) 18 + (PreEscaped(include_str!("./asciiart.txt"))) 19 + html lang="en" { 20 + head { 21 + title { 22 + @if let Some(title) = title { 23 + (title) 24 + " - Xe Iaso" 25 + } @else { 26 + "Xe Iaso" 27 + } 28 + } 29 + meta name="viewport" content="width=device-width, initial-scale=1.0"; 30 + link rel="stylesheet" href={"/css/hack.css?bustCache=" (*CACHEBUSTER)}; 31 + link rel="stylesheet" href={"/css/gruvbox-dark.css?bustCache=" (*CACHEBUSTER)}; 32 + link rel="stylesheet" href={"/css/shim.css?bustCache=" (*CACHEBUSTER)}; 33 + @match now.month() { 34 + 12|1|2 => { 35 + link rel="stylesheet" href={"/css/snow.css?bustCache=" (*CACHEBUSTER)}; 36 + } 37 + _ => {}, 38 + } 39 + link rel="manifest" href="/static/manifest.json"; 40 + link rel="alternate" title="Xe's Blog" type="application/rss+xml" href="https://xeiaso.net/blog.rss"; 41 + link rel="alternate" title="Xe's Blog" type="application/json" href="https://xeiaso.net/blog.json"; 42 + link rel="apple-touch-icon" sizes="57x57" href="/static/favicon/apple-icon-57x57.png"; 43 + link rel="apple-touch-icon" sizes="60x60" href="/static/favicon/apple-icon-60x60.png"; 44 + link rel="apple-touch-icon" sizes="72x72" href="/static/favicon/apple-icon-72x72.png"; 45 + link rel="apple-touch-icon" sizes="76x76" href="/static/favicon/apple-icon-76x76.png"; 46 + link rel="apple-touch-icon" sizes="114x114" href="/static/favicon/apple-icon-114x114.png"; 47 + link rel="apple-touch-icon" sizes="120x120" href="/static/favicon/apple-icon-120x120.png"; 48 + link rel="apple-touch-icon" sizes="144x144" href="/static/favicon/apple-icon-144x144.png"; 49 + link rel="apple-touch-icon" sizes="152x152" href="/static/favicon/apple-icon-152x152.png"; 50 + link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-icon-180x180.png"; 51 + link rel="icon" type="image/png" sizes="192x192" href="/static/favicon/android-icon-192x192.png"; 52 + link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png"; 53 + link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png"; 54 + link rel="icon" type="image/png" sizes="96x96" href="/static/favicon/favicon-96x96.png"; 55 + link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png"; 56 + meta name="msapplication-TileColor" content="#ffffff"; 57 + meta name="msapplication-TileImage" content="/static/favicon/ms-icon-144x144.png"; 58 + meta name="theme-color" content="#ffffff"; 59 + link href="https://mi.within.website/api/webmention/accept" rel="webmention"; 60 + @if let Some(styles) = styles { 61 + style { 62 + (PreEscaped(styles)) 63 + } 64 + } 65 + } 66 + body.snow.hack.gruvbox-dark { 67 + .container { 68 + header { 69 + span.logo {} 70 + nav { 71 + a href="/" { "Xe" } 72 + " - " 73 + a href="/blog" { "Blog" } 74 + " - " 75 + a href="/contact" { "Contact" } 76 + " - " 77 + a href="/resume" { "Resume" } 78 + " - " 79 + a href="/talks" { "Talks" } 80 + " - " 81 + a href="/signalboost" { "Signal Boost" } 82 + " - " 83 + a href="/feeds" { "Feeds" } 84 + " | " 85 + a target="_blank" rel="noopener noreferrer" href="https://graphviz.christine.website" { "Graphviz" } 86 + " - " 87 + a target="_blank" rel="noopener noreferrer" href="https://when-then-zen.christine.website/" { "When Then Zen" } 88 + } 89 + } 90 + 91 + br; 92 + br; 93 + 94 + .snowframe { 95 + (content) 96 + } 97 + hr; 98 + footer { 99 + blockquote { 100 + "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." 101 + } 102 + p { 103 + "Like what you see? Donate on " 104 + a href="https://www.patreon.com/cadey" { "Patreon" } 105 + " like " 106 + a href="/patrons" { "these awesome people" } 107 + "!" 108 + } 109 + p { 110 + "Looking for someone for your team? Take a look " 111 + a href="/signalboost" { "here" } 112 + "." 113 + } 114 + p { 115 + "See my salary transparency data " 116 + a href="/salary-transparency" {"here"} 117 + "." 118 + } 119 + p { 120 + "Served by " 121 + (env!("out")) 122 + "/bin/xesite, see " 123 + a href="https://github.com/Xe/site" { "source code here" } 124 + "." 125 + } 126 + } 127 + script src="/static/js/installsw.js" defer {} 128 + } 129 + } 130 + } 131 + } 132 + } 133 + 134 + pub fn post_index(posts: &Vec<Post>, title: &str, show_extra: bool) -> Markup { 135 + let today = Utc::now().date_naive(); 136 + base( 137 + Some(title), 138 + None, 139 + html! { 140 + h1 { (title) } 141 + @if show_extra { 142 + p { 143 + "If you have a compatible reader, be sure to check out my " 144 + a href="/blog.rss" { "RSS feed" } 145 + " for automatic updates. Also check out the " 146 + a href="/blog.json" { "JSONFeed" } 147 + "." 148 + } 149 + p { 150 + "For a breakdown by post series, see " 151 + a href="/blog/series" { "here" } 152 + "." 153 + } 154 + } 155 + p { 156 + ul { 157 + @for post in posts.iter().filter(|p| today.num_days_from_ce() >= p.date.num_days_from_ce()) { 158 + li { 159 + (post.detri()) 160 + " - " 161 + a href={"/" (post.link)} { (post.front_matter.title) } 162 + } 163 + } 164 + } 165 + } 166 + }, 167 + ) 168 + } 169 + 170 + pub fn gallery_index(posts: &Vec<Post>) -> Markup { 171 + base( 172 + Some("Gallery"), 173 + None, 174 + html! { 175 + h1 {"Gallery"} 176 + 177 + p {"Here are links to a lot of the art I have done in the last few years."} 178 + 179 + .grid { 180 + @for post in posts { 181 + .card.cell."-4of12".blogpost-card { 182 + header."card-header" { 183 + (post.front_matter.title) 184 + } 185 + .card-content { 186 + center { 187 + p { 188 + "Posted on " 189 + (post.detri()) 190 + br; 191 + a href={"/" (post.link)} { 192 + img src=(post.front_matter.thumb.as_ref().unwrap()); 193 + } 194 + } 195 + } 196 + } 197 + } 198 + } 199 + } 200 + }, 201 + ) 202 + } 203 + 204 + pub fn contact(links: &Vec<Link>) -> Markup { 205 + base( 206 + Some("Contact Information"), 207 + None, 208 + html! { 209 + h1 {"Contact Information"} 210 + 211 + .grid { 212 + .cell."-6of12" { 213 + h3 {"Email"} 214 + p {"me@xeiaso.net"} 215 + 216 + h3 {"Social Media"} 217 + ul { 218 + @for link in links { 219 + li {(link)} 220 + } 221 + } 222 + } 223 + .cell."-6of12" { 224 + h3 {"Other Information"} 225 + h4 {"Discord"} 226 + p { 227 + code {"Cadey~#1337"} 228 + " Please note that Discord will automatically reject friend requests if you are not in a mutual server with me. I don't have control over this behavior." 229 + } 230 + } 231 + } 232 + }, 233 + ) 234 + } 235 + 236 + pub fn patrons(patrons: &Users) -> Markup { 237 + base( 238 + Some("Patrons"), 239 + None, 240 + html! { 241 + h1 {"Patrons"} 242 + 243 + p { 244 + "These awesome people donate to me on " 245 + a href="https://patreon.com/cadey" {"Patreon"} 246 + ". If you would like to show up in this list, please donate to me on Patreon. This is refreshed every time the site is deployed." 247 + } 248 + 249 + .grid { 250 + @for patron in patrons { 251 + .cell."-3of12" { 252 + center { 253 + p {(patron.attributes.full_name)} 254 + img src=(patron.attributes.thumb_url) loading="lazy"; 255 + } 256 + } 257 + } 258 + } 259 + }, 260 + ) 261 + } 262 + 263 + pub fn signalboost(people: &Vec<Person>) -> Markup { 264 + base( 265 + Some("Signal Boosts"), 266 + None, 267 + html! { 268 + h1 {"Signal Boosts"} 269 + 270 + p {"These awesome people are currently looking for a job. If you are looking for anyone with these skills, please feel free to reach out to them."} 271 + 272 + p { 273 + "To add yourself to this list, fork " 274 + a href="https://github.com/Xe/site" {"this website's source code"} 275 + " and send a pull request with edits to " 276 + code {"/dhall/signalboost.dhall"} 277 + "." 278 + } 279 + 280 + p {"With COVID-19 raging across the world, these people are in need of a job now more than ever."} 281 + 282 + h2 {"People"} 283 + 284 + .grid.signalboost { 285 + @for person in people { 286 + .cell."-4of12".content { 287 + big {(person.name)} 288 + 289 + p { 290 + @for tag in &person.tags {(tag) " "} 291 + } 292 + 293 + p { 294 + @for link in &person.links {(link) " "} 295 + } 296 + } 297 + } 298 + } 299 + }, 300 + ) 301 + } 302 + 303 + pub fn error(why: impl Render) -> Markup { 304 + base( 305 + Some("Error"), 306 + None, 307 + html! { 308 + h1 {"Error"} 309 + 310 + pre { 311 + (why) 312 + } 313 + 314 + p { 315 + "You could try to " 316 + a href="/" {"go home"} 317 + " or " 318 + a href="https://github.com/Xe/site/issues/new" {"report this issue"} 319 + " so it can be fixed." 320 + } 321 + }, 322 + ) 323 + } 324 + 325 + pub fn not_found(path: impl Render) -> Markup { 326 + base( 327 + Some("Not found"), 328 + None, 329 + html! { 330 + h1 {"Not found"} 331 + p { 332 + "The path at " 333 + code {(path)} 334 + " could not be found. If you expected this path to exist, please " 335 + a href="https://github.com/Xe/site/issues/new" {"report this issue"} 336 + " so it can be fixed." 337 + } 338 + }, 339 + ) 340 + } 341 + 342 + pub fn gitea(pkg_name: &str, git_repo: &str, branch: &str) -> Markup { 343 + html! { 344 + (DOCTYPE) 345 + html { 346 + head { 347 + meta http-equiv="Content-Type" content="text/html; charset=utf-8"; 348 + meta name="go-import" content={(pkg_name)" git " (git_repo)}; 349 + meta name="go-source" content={(format!("{pkg_name} {git_repo} {git_repo}/src/{branch}{{/dir}} {git_repo}/src/{branch}{{/dir}}/{{file}}#L{{line}}"))}; 350 + meta http-equiv="refresh" content={(format!("0; url=https://pkg.go.dev/{pkg_name}"))}; 351 + } 352 + body { 353 + p { 354 + "Please see" 355 + a href={"https://godoc.org/" (pkg_name)} {"here"} 356 + " for documentation on this package." 357 + } 358 + } 359 + } 360 + } 361 + } 362 + 363 + pub fn resume() -> Markup { 364 + base( 365 + Some("Resume"), 366 + None, 367 + html! { 368 + h1 {"Resume"} 369 + 370 + p {"This resume is automatically generated when the website gets deployed."} 371 + 372 + iframe src="/static/resume/resume.pdf" width="100%" height="900px" {} 373 + 374 + hr; 375 + 376 + a href="/static/resume/resume.pdf" { "PDF version" } 377 + }, 378 + ) 379 + } 380 + 381 + fn schema_person(a: &Author) -> Markup { 382 + let data = PreEscaped(serde_json::to_string(&a).unwrap()); 383 + 384 + html! { 385 + script type="application/ld+json" { (data) } 386 + } 387 + } 388 + 389 + pub fn index(xe: &Author, projects: &Vec<Link>) -> Markup { 390 + base( 391 + None, 392 + None, 393 + html! { 394 + link rel="authorization_endpoint" href="https://idp.christine.website/auth"; 395 + link rel="canonical" href="https://xeiaso.net/"; 396 + meta name="google-site-verification" content="rzs9eBEquMYr9Phrg0Xm0mIwFjDBcbdgJ3jF6Disy-k"; 397 + (schema_person(&xe)) 398 + 399 + meta name="twitter:card" content="summary"; 400 + meta name="twitter:site" content="@theprincessxena"; 401 + meta name="twitter:title" content=(xe.name); 402 + meta name="twitter:description" content=(xe.job_title); 403 + meta property="og:type" content="website"; 404 + meta property="og:title" content=(xe.name); 405 + meta property="og:site_name" content=(xe.job_title); 406 + meta name="description" content=(xe.job_title); 407 + meta name="author" content=(xe.name); 408 + 409 + .grid { 410 + .cell."-3of12".content { 411 + img src="/static/img/avatar.png" alt="My Avatar"; 412 + br; 413 + a href="/contact" class="justify-content-center" { "Contact me" } 414 + } 415 + .cell."-9of12".content { 416 + h1 {(xe.name)} 417 + h4 {(xe.job_title)} 418 + h5 { "Skills" } 419 + ul { 420 + li { "Go, Lua, Haskell, C, Rust and other languages" } 421 + li { "Docker (deployment, development & more)" } 422 + li { "Mashups of data" } 423 + li { "kastermakfa" } 424 + } 425 + 426 + h5 { "Highlighted Projects" } 427 + ul { 428 + @for project in projects { 429 + li {(project)} 430 + } 431 + } 432 + 433 + h5 { "Quick Links" } 434 + ul { 435 + li {a href="https://github.com/Xe" rel="me" {"GitHub"}} 436 + li {a href="https://twitter.com/theprincessxena" rel="me" {"Twitter"}} 437 + li {a href="https://pony.social/@cadey" rel="me" {"Fediverse"}} 438 + li {a href="https://www.patreon.com/cadey" rel="me" {"Patreon"}} 439 + } 440 + 441 + p { 442 + "Looking for someone for your team? Check " 443 + a href="/signalboost" { "here" } 444 + "." 445 + } 446 + } 447 + } 448 + }, 449 + ) 450 + } 451 + 452 + pub fn blog_series(series: &Vec<SeriesDescription>) -> Markup { 453 + base( 454 + Some("Blogposts by series"), 455 + None, 456 + html! { 457 + h1 { "Blogposts by series" } 458 + p { 459 + "Some posts of mine are intended to be read in order. This is a list of all the series I have written along with a little description of what it's about." 460 + } 461 + p { 462 + ul { 463 + @for set in series { 464 + li {(set)} 465 + } 466 + } 467 + } 468 + }, 469 + ) 470 + } 471 + 472 + pub fn series_view(name: &str, desc: &str, posts: &Vec<Post>) -> Markup { 473 + base( 474 + Some(&format!("{name} posts")), 475 + None, 476 + html! { 477 + h1 {"Series: " (name)} 478 + 479 + p {(desc)} 480 + 481 + ul { 482 + @for post in posts { 483 + li { 484 + (post.detri()) 485 + " - " 486 + a href={"/" (post.link)} {(post.front_matter.title)} 487 + } 488 + } 489 + } 490 + }, 491 + ) 492 + } 493 + 494 + pub fn feeds() -> Markup { 495 + base( 496 + Some("My Feeds"), 497 + None, 498 + html! { 499 + h1 { "My Feeds" } 500 + 501 + ul { 502 + li { 503 + "Blog: " 504 + a href="/blog.atom" { "Atom" } 505 + " - " 506 + a href="/blog.rss" { "RSS" } 507 + " - " 508 + a href="/blog.json" { "JSONFeed" } 509 + } 510 + li { 511 + "Mastodon: " 512 + a href="https://pony.social/users/cadey.rss" { "RSS" } 513 + } 514 + } 515 + }, 516 + ) 517 + } 518 + 519 + pub fn salary_transparency(jobs: &Vec<Job>) -> Markup { 520 + base( 521 + Some("Salary Transparency"), 522 + None, 523 + html! { 524 + h1 {"Salary Transparency"} 525 + 526 + p { 527 + "This page lists my salary for every job I've had in tech. I have had this data open to the public " 528 + a href="https://xeiaso.net/blog/my-career-in-dates-titles-salaries-2019-03-14" {"for years"} 529 + ", but I feel this should be more prominently displayed on my website. Other people have copied my approach of having a list of every salary they have ever been paid on their websites, and I would like to set the example by making it prominent on my website." 530 + } 531 + p { 532 + "As someone who has seen pay discrimination work in action first-hand, data is one of the ways that we can end this pointless hiding of information that leads to people being uninformed and hurt by their lack of knowledge. By laying my hand out in the open like this, I hope to ensure that people are better informed about how much money they " 533 + em {"can"} 534 + " make, so that they can be paid equally for equal work." 535 + } 536 + 537 + p { 538 + "Please keep in mind that this table doesn't tell the complete story. If you feel like judging me about any entry in this table, please do not do it around me." 539 + } 540 + 541 + h2 {"Salary Data"} 542 + 543 + p { 544 + "To get this data, I have scoured over past emails, contracts and everything so that I can be sure that this information is as accurate as possible. The data on this page intentionally omits employer names. Some information may also be omitted if relevant non-disclosure agreements or similar prohibit it." 545 + } 546 + 547 + (salary_history(jobs)) 548 + 549 + p { 550 + "I typically update this page once any of the following things happens:" 551 + } 552 + 553 + ul { 554 + li {"I quit a job."} 555 + li {"I get a raise/title change at the same company."} 556 + li {"I get terminated from a job."} 557 + li {"I get converted from a contracter to a full-time employee."} 558 + li {"Other unspecified extranormal events happen."} 559 + } 560 + 561 + p { 562 + "Please consider publishing your salary data like this as well. By open, voluntary transparency we can help to end stigmas around discussing pay and help ensure that the next generations of people in tech are treated fairly. Stigmas thrive in darkness but die in the light of day. You can help end the stigma by playing your cards out in the open like this." 563 + } 564 + }, 565 + ) 566 + } 567 + 568 + fn salary_history(jobs: &Vec<Job>) -> Markup { 8 569 html! { 9 570 table.salary_history { 10 571 tr { ··· 15 576 th { "Salary" } 16 577 th { "How I Left" } 17 578 } 18 - @for job in &cfg.clone().job_history { 19 - (job.pay_history_row()) 579 + @for job in jobs { 580 + (job) 20 581 } 21 582 } 22 583 }
+1 -1
src/tmpl/nag.rs
··· 38 38 } 39 39 40 40 pub fn prerelease(post: &Post) -> Markup { 41 - if Utc::today().num_days_from_ce() < post.date.num_days_from_ce() { 41 + if Utc::now().date_naive().num_days_from_ce() < post.date.num_days_from_ce() { 42 42 html! { 43 43 .warning { 44 44 (xeblog_conv("Mara".into(), "hacker".into(), html!{
+1
static/js/.gitignore
··· 1 + mastodon_share_button.js
-36
static/js/conversation.js
··· 1 - import { g, h, x } from "./xeact.min.js"; 2 - import { div, span } from "./xeact-html.min.js"; 3 - 4 - export const mkConversation = (who, mood, message, extraClasses = "") => 5 - h("div", {className: "conversation gruvbox-dark " + extraClasses}, [ 6 - h("div", {className: "conversation-picture conversation-smol"}, [ 7 - h("picture", {}, [ 8 - h("source", {type: "image/avif", srcset: `https://cdn.xeiaso.net/file/christine-static/stickers/${who.toLowerCase()}/${mood}.avif`}), 9 - h("source", {type: "image/webp", srcset: `https://cdn.xeiaso.net/file/christine-static/stickers/${who.toLowerCase()}/${mood}.webp`}), 10 - h("img", {alt: `${who} is ${mood}`, src: `https://cdn.xeiaso.net/file/christine-static/stickers/${who.toLowerCase()}/${mood}.png`}) 11 - ]) 12 - ]), 13 - h("div", {className: "conversation-chat"}, [ 14 - h("span", {innerText: "<"}), 15 - h("b", {innerText: who}), 16 - h("span", {innerText: "> "}), 17 - span({}, Array.from(message)) 18 - ]) 19 - ]); 20 - 21 - export class Conversation extends HTMLElement { 22 - constructor() { 23 - super(); 24 - 25 - let root = this.attachShadow({mode: "open"}); 26 - let who = this.getAttribute("name"); 27 - let mood = this.getAttribute("mood"); 28 - 29 - root.appendChild(h("link", {rel: "stylesheet", href: "/css/hack.css"})); 30 - root.appendChild(h("link", {rel: "stylesheet", href: "/css/gruvbox-dark.css"})); 31 - root.appendChild(h("link", {rel: "stylesheet", href: "/css/shim.css"})); 32 - root.appendChild(mkConversation(who, mood, this.childNodes)); 33 - } 34 - } 35 - 36 - window.customElements.define("xeblog-conv", Conversation);
-35
static/js/hnwarn.js
··· 1 - import { g, x, r, t } from "./xeact.min.js"; 2 - import { div, ahref, br } from "./xeact-html.min.js"; 3 - import { mkConversation } from "./conversation.js"; 4 - 5 - // list of regexps for potentially problematic referrers to display the nag to 6 - const FLAGGED_REFERRERS = [ 7 - /^https?:\/\/((.+)\.)?reddit\.com/i, 8 - /^https?:\/\/news\.ycombinator\.com/i, 9 - ]; 10 - 11 - const addNag = () => { 12 - let root = g("refererNotice"); 13 - x(root); 14 - root.appendChild( 15 - div( 16 - {style: "padding:1em"}, 17 - mkConversation("Cadey", "coffee", [ 18 - t("Thank you for reading this article. If you have any questions or thoughts about its contents, please comment civilly on it and remember the human on the other side of the screen. Due to facts and circumstances surrounding our fundamentally subjective reality, I may experience things differently than you do. If this is somehow unacceptable to you, please feel free to "), 19 - ahref("https://zombo.com", "go somewhere else"), 20 - t(". Have a good day and be well!") 21 - ], "warning"), 22 - br(), 23 - br() 24 - ) 25 - ); 26 - }; 27 - 28 - r(() => { 29 - const ref = document.referrer; 30 - if (!ref) return; 31 - 32 - if (FLAGGED_REFERRERS.some(r => r.test(ref))) { 33 - addNag(); 34 - } 35 - });
-1
static/js/xeact-html.min.js
··· 1 - import{h,t}from"./xeact.min.js";const $tl=d=>(l,$={},s=[])=>(s.unshift(t(l)),h(d,$,s)),h1=$tl("h1"),h2=$tl("h2"),h3=$tl("h3"),h4=$tl("h4"),h5=$tl("h5"),h6=$tl("h6"),p=$tl("p"),b=$tl("b"),i=$tl("i"),u=$tl("u"),dd=$tl("dd"),dt=$tl("dt"),del=$tl("del"),sub=$tl("sub"),sup=$tl("sup"),strong=$tl("strong"),small=$tl("small"),hl=()=>h("hl"),br=()=>h("br"),img=(l,t="")=>h("img",{src:l,alt:t}),ahref=(l,$)=>h("a",{href:l},t($)),$dl=$=>(l={},t=[])=>h($,l,t),span=$dl("span"),div=$dl("div"),ul=$dl("ul"),iframe=(l,t={})=>(t.src=l,h("iframe",t));export{h1,h2,h3,h4,h5,h6,p,b,i,u,dd,dt,del,sub,sup,strong,small,hl,br,img,ahref,span,div,ul,iframe};
-1
static/js/xeact.min.js
··· 1 - const h=(e,t={},r=[])=>{let n=Object.assign(document.createElement(e),t);return Array.isArray(r)||(r=[r]),n.append(...r),n},t=e=>document.createTextNode(e),x=e=>{for(;e.lastChild;)e.removeChild(e.lastChild)},g=e=>document.getElementById(e),c=e=>document.getElementsByClassName(e),n=e=>document.getElementsByName(e),s=e=>Array.from(document.querySelectorAll(e)),u=(e="",t={})=>{let r=new URL(e,window.location.href);return Object.entries(t).forEach(e=>{var[t,e]=e;r.searchParams.set(t,e)}),r.toString()},r=e=>window.addEventListener("DOMContentLoaded",e);export{h,t,x,g,c,n,u,s,r};
+1
static/resume/.gitignore
··· 1 + resume.pdf
-171
static/resume/resume.md
··· 1 - # Xe Iaso 2 - 3 - #### Full-stack Engineer 4 - 5 - ##### Ottawa, ON &emsp; [xeiaso.net][homepage] 6 - 7 - `Docker`, `Git`, `Go`, `Rust`, `C`, `Stenography`, `DevOps`, `Heroku`, `Continuous 8 - Integration/Delivery`, `WebAssembly`, `Lua`, `Mindfulness`, `HTTP/2`, `Alpine 9 - Linux`, `Ubuntu`, `Linux`, `GraphViz`, `Progressive Web Apps`, `yaml`, `SQL`, 10 - `Postgres`, `MySQL`, `SQLite`, `Ordained Minister`, `Dudeism`, `Tech Writing`, 11 - `Kubernetes`, `Command Line Apps` 12 - 13 - ## Experience 14 - 15 - ### Tailscale - Software Designer &emsp; <small>*2020 - present*</small> 16 - 17 - > [Tailscale][tailscale] is a zero config VPN for building secure networks. 18 - > Install on any device in minutes. Remote access from any network or physical 19 - > location. 20 - 21 - #### Highlights 22 - 23 - - Go programming 24 - - Nix and NixOS 25 - - SQL integrations 26 - - End-user facing blog content and customer support 27 - 28 - ### Lightspeed - Expert principal en fiabilité du site &emsp; <small>*2019 - 2020*</small> 29 - 30 - (Senior Site Reliability Expert) 31 - 32 - > [Lightspeed][lightspeedhq] is a provider of retail, ecommerce and 33 - > point-of-sale solutions for small and medium scale businesses. 34 - 35 - #### Highlights 36 - 37 - - Migration from cloud to cloud 38 - - Work on the cloud platform initiative 39 - - Crafting reliable infrastructure for clients of customers 40 - - Creation of an internally consistent and extensible command line interface for 41 - internal tooling 42 - 43 - ### Heroku - Senior Software Engineer &emsp; <small>*2017 - 2019*</small> 44 - 45 - > [Heroku][heroku] is a cloud Platform-as-a-Service (PaaS) that created the term 46 - > "platform as a service". Heroku currently supports several programming 47 - > languages that are commonly used on the web. Heroku, one of the first cloud 48 - > platforms, has been in development since June 2007, when it supported only the 49 - > Ruby programming language, but now supports Java, Node.js, Scala, Clojure, 50 - > Python, PHP, and Go. 51 - 52 - #### Highlights 53 - 54 - - [JVM Application Metrics](https://devcenter.heroku.com/changelog-items/1133) 55 - - [Go Runtime Metrics 56 - Agent](https://github.com/heroku/x/tree/8572eb9d3d69016dabefd342506fe9951830c358/runtime-metrics) 57 - - Other backend fixes and improvements on [Threshold 58 - Autoscaling](https://blog.heroku.com/heroku-autoscaling) and [Threshold 59 - Alerting](https://devcenter.heroku.com/articles/metrics#threshold-alerting) 60 - - [How to Make a Progressive Web App From Your Existing 61 - Website](https://blog.heroku.com/how-to-make-progressive-web-app) 62 - 63 - ### Backplane.io - Software Engineer &emsp; <small>*2016 - 2016*</small> 64 - 65 - > [Backplane](https://backplane.io) (now defunct) was an innovative reverse reverse proxy that 66 - > helps administrators and startups simplify their web application routing. 67 - 68 - #### Highlights 69 - 70 - - Performance monitoring of production servers 71 - - Continuous deployment and development in Go 72 - - Learning a lot about HTTP/2 and load balancing 73 - 74 - ### Pure Storage - Member of Technical Staff &emsp; <small>*2016 - 2016*</small> 75 - 76 - > Pure Storage is a Mountain View, California-based enterprise data flash storage 77 - > company founded in 2009. It is traded on the NYSE (PSTG). 78 - 79 - #### Highlights 80 - 81 - - Code maintenance 82 - 83 - ### IMVU - Site Reliability Engineer &emsp; <small>*2015 - 2016*</small> 84 - 85 - > IMVU, inc is a company whose mission is to help people find and communicate 86 - > with eachother. Their main product is a 3D avatar-based chat client and its 87 - > surrounding infrastructure allowing creators to make content for the avatars 88 - > to wear. 89 - 90 - #### Highlights 91 - 92 - - Wrote up technical designs 93 - - Implemented technical designs on an over 800 machine cluster 94 - - Continuous learning of a lot of very powerful systems and improving upon them 95 - when it is needed 96 - 97 - ### VTCSecure - Deis Consultant (contract) &emsp; <small>*2014 - 2015*</small> 98 - 99 - > VTCSecure is a company dedicated to helping with custom and standard 100 - > audio/video conferencing solutions. They specialize in helping the deaf and 101 - > blind communicate over today's infrastructure without any trouble on their end. 102 - 103 - #### Highlights 104 - 105 - - Started groundwork for a dynamically scalable infrastructure on a project for 106 - helping the blind see things 107 - - Developed a prototype of a new website for VTCSecure 108 - - Education on best practices using Docker and CoreOS 109 - - Learning Freeswitch 110 - 111 - ### Crowdflower - Deis Consultant (Contract) &emsp; <small>*2014 - 2014*</small> 112 - 113 - > Crowdflower is a company that uses crowdsourcing to have its customers submit 114 - > tasks to be done, similar to Amazon's Mechanical Turk. CrowdFlower has over 50 115 - > labor channel partners, and its network has more than 5 million contributors 116 - > worldwide. 117 - 118 - #### Highlights 119 - 120 - - Research and development on scalable Linux deployments on AWS via CoreOS and 121 - Docker 122 - - Development of in-house tools to speed instance creation 123 - - Laid groundwork on the creation and use of better tools for managing large 124 - clusters of CoreOS and Fleet machines 125 - 126 - ### OpDemand - Software Engineering Intern &emsp; <small>*2014 - 2014*</small> 127 - 128 - > OpDemand is the company behind the open source project Deis, a distributed 129 - > platform-as-a-service (PaaS) designed from the ground up to emulate Heroku but 130 - > on privately owned servers. 131 - 132 - #### Highlights 133 - 134 - - Built new base image for Deis components 135 - - Research and development on a new builder component 136 - 137 - ## Writing 138 - 139 - > Articles listed below will be either personal or professional and do not 140 - > reflect the views of any company or group I am affiliated with. The writing is 141 - > my own, with the help of others to make things legible. 142 - 143 - - [My Blog](https://xeiaso.net/blog) 144 - - [NAS 101: An intro chat about Network Attached 145 - Storage](https://tailscale.com/blog/nas-101/) 146 - - [The Sisyphean Task Of DNS Client Config on 147 - Linux](https://tailscale.com/blog/sisyphean-dns-client-linux/) 148 - 149 - I have gotten to the front page of [Hacker News](https://news.ycombinator.com) several times. Here are a few of the comment threads: 150 - 151 - - ["Open Source" is Broken](https://news.ycombinator.com/item?id=29522941) 152 - - [The Surreal Horror of PAM](https://news.ycombinator.com/item?id=29167560) 153 - - [Systemd: The Good Parts](https://news.ycombinator.com/item?id=27175960) 154 - - [I Implemented /dev/printerfact in 155 - Rust](https://news.ycombinator.com/item?id=26845355) 156 - - [A Model for Identity in Software](https://news.ycombinator.com/item?id=25978511) 157 - - [I Put Words on This Webpage so You Have to Listen to Me now](https://news.ycombinator.com/item?id=18577758) 158 - - [TempleOS: 1 - Installation](https://news.ycombinator.com/item?id=19961082) 159 - - [WebAssembly on the Server: How System Calls Work](https://news.ycombinator.com/item?id=20066204) 160 - - [Olin: Defining a New Primitive for Event-Driven Services](https://news.ycombinator.com/item?id=17896307) 161 - 162 - ## Ordination 163 - 164 - I am an ordained minister with the [Church of the Latter-day Dude](https://dudeism.com). This allows me to officiate religious ceremonies in at least the United States. I would be honored if you were to choose me to officiate anything for any reason. Please [contact](/contact) me if you have any questions. 165 - 166 - [homepage]: https://xeiaso.net 167 - [twitter]: https://twitter.com/theprincessxena 168 - [twit]: http://cdn-careers.sstatic.net/careers/Img/icon-twitter.png?v=b1bd58ad2034 169 - [heroku]: https://www.heroku.com 170 - [lightspeedhq]: https://www.lightspeedhq.com 171 - [tailscale]: https://tailscale.com/
-94
templates/blogindex.rs.html
··· 1 - @use crate::post::Post; 2 - @use super::{header_html, footer_html}; 3 - @use chrono::prelude::*; 4 - 5 - @(posts: Vec<Post>) 6 - 7 - @:header_html(Some("Blog"), None) 8 - 9 - <h1>Blogposts</h1> 10 - 11 - <p>If you have a compatible reader, be sure to check out my <a href="/blog.rss">RSS Feed</a> for automatic updates. Also check out the <a href="/blog.json">JSONFeed</a>.</p> 12 - 13 - <p>For a breakdown by post series, see <a href="/blog/series">here</a>.</p> 14 - 15 - <p> 16 - <ul> 17 - @for post in posts.iter().filter(|p| Utc::today().num_days_from_ce() >= p.date.num_days_from_ce()) { 18 - <li>@post.detri() - <a href="/@post.link">@post.front_matter.title</a></li> 19 - } 20 - </ul> 21 - </p> 22 - 23 - <br /> 24 - 25 - <h2>Other Blogs I Find Interesting</h2> 26 - 27 - <ul> 28 - <li><a href="https://write.as/excerpts/">Excerpts</a></li> 29 - <li><a href="https://heartmender.writeas.com/">Heartmender</a></li> 30 - <li><a href="https://celestialboon.github.io/">CelestialBoon</a></li> 31 - <li><a href="https://slatestarcodex.com/">Star Slate Codex</a></li> 32 - </ul> 33 - 34 - <h2>Selected Commentary on These Blogposts</h2> 35 - 36 - <h3><a href="/blog/experimental-rilkef-2018-11-30">I Put Words on this Webpage so You Have to Listen to Me Now</a></h3> 37 - 38 - <p> 39 - <blockquote> 40 - Top tier satire. Won't be read by anyone who should read it, and will be ignored/laughed at by anyone who does/already agrees. <br /> 41 - 42 - Literally preaching to the literal choir. 43 - </blockquote> 44 - 45 - <blockquote> 46 - Hired. 47 - </blockquote> 48 - 49 - <blockquote> 50 - It’s things like this that make me realize just how bizarre this profession really is. 51 - </blockquote> 52 - 53 - <blockquote> 54 - Meanwhile, in two weeks the entire Haskell ecosystem will adapt. 55 - </blockquote> 56 - 57 - <blockquote> 58 - dont read any of the other posts if u dont want to melt ur brain backwards 59 - </blockquote> 60 - 61 - <blockquote> 62 - but yeah needless to say you've basically written the generic form of basically every time I'm subconsciously annoyed by a software dev social pattern 63 - </blockquote> 64 - 65 - <blockquote> 66 - Well executed. The only thing this is missing is a truncated y-axis on the graph. 67 - </blockquote> 68 - 69 - <blockquote> 70 - Why would you do that? Just use jRilkef and call $.flopnax() and it'll automatically flopnax your ropjar. (Marked +240345 by flopnax overflow) 71 - </blockquote> 72 - 73 - <blockquote> 74 - The comments I am reading in response to the words on the website miss the point completely. It is clear from the words on the web page on the link that the point is different than what people here are saying it is. Did you even read those words on the internet web page, accessed from the link, downloaded and subsequently rendered by your browser of choice? 75 - </blockquote> 76 - </p> 77 - 78 - <h3><a href="https://xeiaso.net/blog/templeos-2-god-the-rng-2019-05-30">TempleOS: 2 - <code>god</code>, the Random Number Generator</a> </h3> 79 - 80 - <p> 81 - <blockquote> 82 - Thank you very much for this series. 83 - </blockquote> 84 - 85 - <blockquote> 86 - I think Terry was more right than most of us would dare to admit. Playing with the thought of setting up a fund to build a statue in his honor!! 87 - </blockquote> 88 - 89 - <blockquote> 90 - I ran the voice of god generator and the first thing it did was call me gay. 91 - </blockquote> 92 - </p> 93 - 94 - @:footer_html()
-162
templates/blogpost.rs.html
··· 1 - @use super::{header_html, footer_html}; 2 - @use crate::{post::Post, tmpl::nag}; 3 - 4 - @(post: Post, body: impl ToHtml, referer: Option<String>) 5 - 6 - @:header_html(Some(&post.front_matter.title.clone()), None) 7 - 8 - <!-- Twitter --> 9 - <meta name="twitter:card" content="summary" /> 10 - <meta name="twitter:site" content="@@theprincessxena" /> 11 - <meta name="twitter:title" content="@post.front_matter.title" /> 12 - <meta name="twitter:description" content="Posted on @post.date.format("%Y-%m-%d")" /> 13 - 14 - <!-- Facebook --> 15 - <meta property="og:type" content="website" /> 16 - <meta property="og:title" content="@post.front_matter.title" /> 17 - <meta property="og:site_name" content="Xe's Blog" /> 18 - 19 - <!-- Description --> 20 - <meta name="description" content="@post.front_matter.title - Xe's Blog" /> 21 - <meta name="author" content="Xe Iaso" /> 22 - 23 - @if post.front_matter.redirect_to.is_none() { 24 - <link rel="canonical" href="https://xeiaso.net/@post.link" /> 25 - } else { 26 - <link rel="canonical" href="@post.front_matter.redirect_to.as_ref().unwrap()" /> 27 - } 28 - 29 - <script type="application/ld+json"> 30 - @{ 31 - "@@context": "http://schema.org", 32 - "@@type": "Article", 33 - "headline": "@post.front_matter.title", 34 - "image": "https://xeiaso.net/static/img/avatar.png", 35 - "url": "https://xeiaso.net/@post.link", 36 - "datePublished": "@post.date.format("%Y-%m-%d")", 37 - "mainEntityOfPage": @{ 38 - "@@type": "WebPage", 39 - "@@id": "https://xeiaso.net/@post.link" 40 - @}, 41 - "author": @{ 42 - "@@type": "Person", 43 - "name": "Xe Iaso" 44 - @}, 45 - "publisher": @{ 46 - "@@type": "Person", 47 - "name": "Xe Iaso" 48 - @} 49 - @} 50 - </script> 51 - 52 - @if let Some(to) = post.front_matter.redirect_to.clone() { 53 - <meta http-equiv="refresh" content="0;URL='@to'" /> 54 - <script> 55 - window.location.replace("@to"); 56 - </script> 57 - } 58 - 59 - @Html(nag::referer(referer).0) 60 - 61 - <article> 62 - <h1>@post.front_matter.title</h1> 63 - 64 - @Html(nag::prerelease(&post).0) 65 - 66 - <small>A @post.read_time_estimate_minutes minute read.</small> 67 - 68 - @body 69 - </article> 70 - 71 - <hr /> 72 - 73 - @if post.front_matter.vod.is_some() { 74 - <p>This post was written live on <a href="https://twitch.tv/princessxen">Twitch</a>. You can check out the stream recording on Twitch <a href="@post.front_matter.vod.as_ref().unwrap().twitch">here</a> and on YouTube <a href="@post.front_matter.vod.as_ref().unwrap().youtube">here</a>.</p> 75 - } 76 - 77 - <!-- The button that should be clicked. --> 78 - <button onclick="share_on_mastodon()">Share on Mastodon</button> 79 - 80 - <p>This article was posted on @post.detri(). Facts and circumstances may have changed since publication. Please <a href="/contact">contact me</a> before jumping to conclusions if something seems wrong or unclear.</p> 81 - 82 - @if post.front_matter.series.is_some() { 83 - <p>Series: <a href="/blog/series/@post.front_matter.series.as_ref().unwrap()">@post.front_matter.series.as_ref().unwrap()</a></p> 84 - } 85 - 86 - @if post.front_matter.tags.is_some() { 87 - <p>Tags: @for tag in post.front_matter.tags.as_ref().unwrap() { <code>@tag</code> }</p> 88 - } 89 - 90 - @if post.mentions.len() != 0 { 91 - <p>This post was <a href="https://www.w3.org/TR/webmention/">WebMention</a>ed at the following URLs: 92 - <ul> 93 - @for mention in post.mentions { 94 - <li><a href="@mention.source">@mention.title.unwrap_or(mention.source)</a></li> 95 - } 96 - </ul> 97 - </p> 98 - } else { 99 - <p>This post was not <a href="https://www.w3.org/TR/webmention/">WebMention</a>ed yet. You could be the first!</p> 100 - } 101 - 102 - <p>The art for Mara was drawn by <a href="https://selic.re/">Selicre</a>.</p> 103 - 104 - <p>The art for Cadey was drawn by <a href="https://artzorastudios.weebly.com/">ArtZora Studios</a>.</p> 105 - 106 - <script> 107 - // The actual function. Set this as an onclick function for your "Share on Mastodon" button 108 - function share_on_mastodon() @{ 109 - // Prefill the form with the user's previously-specified Mastodon instance, if applicable 110 - var default_url = localStorage['mastodon_instance']; 111 - 112 - // If there is no cached instance/domain, then insert a "https://" with no domain at the start of the prompt. 113 - if (!default_url) 114 - default_url = "https://"; 115 - 116 - var instance = prompt("Enter your instance's address: (ex: https://linuxrocks.online)", default_url); 117 - if (instance) @{ 118 - // Handle URL formats 119 - if ( !instance.startsWith("https://") && !instance.startsWith("http://") ) 120 - instance = "https://" + instance; 121 - 122 - // get the current page's url 123 - var url = window.location.href; 124 - 125 - // get the page title from the og:title meta tag, if it exists. 126 - var title = document.querySelectorAll('meta[property="og:title"]')[0].getAttribute("content"); 127 - 128 - // Otherwise, use the <title> tag as the title 129 - if (!title) var title = document.getElementsByTagName("title")[0].innerHTML; 130 - 131 - // Handle slash 132 - if ( !instance.endsWith("/") ) 133 - instance = instance + "/"; 134 - 135 - // Cache the instance/domain for future requests 136 - localStorage['mastodon_instance'] = instance; 137 - 138 - // Hashtags 139 - var hashtags = "#blogpost"; 140 - 141 - @if post.front_matter.series.is_some() { 142 - hashtags += "#@post.front_matter.series.as_ref().unwrap()"; 143 - } 144 - 145 - @if post.front_matter.tags.is_some() { 146 - hashtags += "@for tag in post.front_matter.tags.as_ref().unwrap() { #@tag }"; 147 - } 148 - 149 - // Tagging users, such as offical accounts or the author of the post 150 - var author = "@@cadey@@pony.social"; 151 - 152 - // Create the Share URL 153 - // https://someinstance.tld/share?text=URL%20encoded%20text 154 - mastodon_url = instance + "share?text=" + encodeURIComponent(title + "\n\n" + url + "\n\n" + hashtags + " " + author); 155 - 156 - // Open a new window at the share location 157 - window.open(mastodon_url, '_blank'); 158 - @} 159 - @} 160 - </script> 161 - 162 - @:footer_html()
-53
templates/contact.rs.html
··· 1 - @use super::{header_html, footer_html}; 2 - 3 - @() 4 - 5 - @:header_html(Some("Contact"), None) 6 - 7 - <h1>Contact Information</h1> 8 - <div class="grid"> 9 - <div class="cell -6of12"> 10 - <h3>Email</h3> 11 - <p>me@@xeiaso.net</p> 12 - 13 - <h3>Social Media</h3> 14 - <ul> 15 - <li><a href="https://github.com/Xe">Github</a></li> 16 - <li><a href="https://keybase.io/xena">Keybase</a></li> 17 - <li><a href="https://www.patreon.com/cadey">Patreon</a></li> 18 - <li><a href="https://twitch.tv/princessxen">Twitch</a></li> 19 - <li><a href="https://pony.social/@@cadey">Mastodon</a></li> 20 - <li><a href="https://www.linkedin.com/in/xe-iaso-87a883254/">LinkedIn</a></li> 21 - <li>Fortnite: Within Reason</li> 22 - <li><a href="irc://irc.libera.chat/#xeserv">Liberachat: #xeserv</a></li> 23 - </ul> 24 - </div> 25 - <div class="cell -6of12"> 26 - <h3>Other Information</h3> 27 - <p>I have a <a href="https://www.patreon.com/cadey">Patreon</a> if you want to send donations, otherwise my <a href="https://ko-fi.com/A265JE0">Ko-Fi</a> works too.</p> 28 - 29 - <h4>Telegram</h4> 30 - <p><a href="https://t.me/miamorecadenza">@@miamorecadenza</a></p> 31 - 32 - <h4>Discord</h4> 33 - <p><code>Cadey~#1337</code> (Please note that Discord will automatically reject friend requests if you are not in a mutual server with me. I don't have control over this behavior.)</p> 34 - 35 - <details> 36 - <summary>Cryptocurrency Addresses</summary> 37 - 38 - <h4>Ethereum</h4> 39 - <p><code>xeiaso.eth</code> (<code>0xeA223Ca8968Ca59e0Bc79Ba331c2F6f636A3fB82</code>)</p> 40 - 41 - <h4>Bitcoin</h4> 42 - <p><code>bc1qw0pa3zdus94nyehmys6g8td2xfaqtl9pmuv564</code></p> 43 - 44 - <h4>Litecoin</h4> 45 - <p><code>ltc1q3zhcs4c2wltmz97gs5a2t6sgxryp0y06s82e62</code></p> 46 - 47 - <h4>Bitcoin Cash</h4> 48 - <p><code>qrfyatcurdl8jhqgee6ukhvwl364frx7ds4zgk296c</code></p> 49 - </details> 50 - </div> 51 - </div> 52 - 53 - @:footer_html()
-13
templates/error.rs.html
··· 1 - @use super::{header_html, footer_html}; 2 - 3 - @(why: String) 4 - 5 - @:header_html(Some("Error"), None) 6 - 7 - <h1>Error</h1> 8 - 9 - <code><pre>@why</pre></code> 10 - 11 - <p>You could try to <a href="/">go home</a> or <a href="https://github.com/Xe/site/issues/new">report this issue</a> so it can be fixed.</p> 12 - 13 - @:footer_html()
-17
templates/feeds.rs.html
··· 1 - @use super::{header_html, footer_html}; 2 - 3 - @() 4 - 5 - @:header_html(Some("Feeds"), None) 6 - 7 - <h1>Feeds</h1> 8 - 9 - <ul> 10 - <li>Blog: <a href="/blog.atom">Atom</a> <a href="/blog.rss">RSS</a> - <a href="/blog.json">JSONFeed</a></li> 11 - <li>Twitter: <a href="https://rssbox.herokuapp.com/twitter/2573767249/theprincessxena">RSS</a></li> 12 - <li>Mastodon: <a href="https://pony.social/users/cadey.rss">RSS</a></li> 13 - <li>Revue Newsletter: <a href="https://www.getrevue.co/profile/theprincessxena.rss">RSS</a></li> 14 - <li>Flight Journal: Atom <a href="gemini://cetacean.club/journal/atom.xml">Gemini</a> <a href="https://portal.mozz.us/gemini/cetacean.club/journal/atom.xml">HTTPS</a></li> 15 - </ul> 16 - 17 - @:footer_html()
-16
templates/footer.rs.html
··· 1 - @() 2 - </div> 3 - <hr /> 4 - <footer> 5 - <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.</blockquote> 6 - <p>Like what you see? Donate on <a href="https://www.patreon.com/cadey">Patreon</a> like <a href="/patrons">these awesome people</a>!</p> 7 - <p>Looking for someone for your team? Take a look <a href="/signalboost">here</a>.</p> 8 - <p>Served by @env!("out")/bin/xesite</a>, see <a href="https://github.com/Xe/site">source code here</a>.</p> 9 - <p>See my <a href="/salary-transparency">salary transparency data here</a>.</p> 10 - </footer> 11 - 12 - </div> 13 - 14 - <script src="/static/js/installsw.js" defer></script> 15 - </body> 16 - </html>
-23
templates/galleryindex.rs.html
··· 1 - @use crate::post::Post; 2 - @use super::{header_html, footer_html}; 3 - 4 - @(posts: Vec<Post>) 5 - 6 - @:header_html(Some("Gallery"), None) 7 - 8 - <h1>Gallery</h1> 9 - 10 - <p>Here are links to a lot of the art I have done in the last few years.</p> 11 - 12 - <div class="grid"> 13 - @for post in posts { 14 - <div class="card cell -4of12 blogpost-card"> 15 - <header class="card-header">@post.front_matter.title</header> 16 - <div class="card-content"> 17 - <center><p>Posted on @post.date.format("%Y-%m-%d")<br /><a href="/@post.link"><img src="@post.front_matter.thumb.as_ref().unwrap()" /></a></p></center> 18 - </div> 19 - </div> 20 - } 21 - </div> 22 - 23 - @:footer_html()
-124
templates/gallerypost.rs.html
··· 1 - @use super::{header_html, footer_html}; 2 - @use crate::post::Post; 3 - 4 - @(post: Post, body: impl ToHtml) 5 - 6 - @:header_html(Some(&post.front_matter.title.clone()), None) 7 - 8 - <!-- Twitter --> 9 - <meta name="twitter:card" content="summary" /> 10 - <meta name="twitter:site" content="@@theprincessxena" /> 11 - <meta name="twitter:title" content="@post.front_matter.title" /> 12 - <meta name="twitter:description" content="Posted on @post.date.format("%Y-%m-%d")" /> 13 - 14 - <!-- Facebook --> 15 - <meta property="og:type" content="website" /> 16 - <meta property="og:title" content="@post.front_matter.title" /> 17 - <meta property="og:site_name" content="Xe's Blog" /> 18 - 19 - <!-- Description --> 20 - <meta name="description" content="@post.front_matter.title - Xe's Blog" /> 21 - <meta name="author" content="Xe Iaso"> 22 - 23 - <link rel="canonical" href="https://xeiaso.net/@post.link" /> 24 - 25 - <script type="application/ld+json"> 26 - @{ 27 - "@@context": "http://schema.org", 28 - "@@type": "Article", 29 - "headline": "@post.front_matter.title", 30 - "image": "https://xeiaso.net/static/img/avatar.png", 31 - "url": "https://xeiaso.net/@post.link", 32 - "datePublished": "@post.date.format("%Y-%m-%d")", 33 - "mainEntityOfPage": @{ 34 - "@@type": "WebPage", 35 - "@@id": "https://xeiaso.net/@post.link" 36 - @}, 37 - "author": @{ 38 - "@@type": "Person", 39 - "name": "Xe Iaso", 40 - @}, 41 - "publisher": @{ 42 - "@@type": "Person", 43 - "name": "Xe Iaso", 44 - @} 45 - @} 46 - </script> 47 - 48 - <h1>@post.front_matter.title</h1> 49 - 50 - @body 51 - 52 - <center> 53 - <img src="@post.front_matter.image.as_ref().unwrap()" /> 54 - </center> 55 - 56 - <hr /> 57 - 58 - <!-- The button that should be clicked. --> 59 - <button onclick="share_on_mastodon()">Share on Mastodon</button> 60 - 61 - <p>This artwork was posted on @post.detri().</p> 62 - 63 - @if post.front_matter.series.is_some() { 64 - <p>Series: <a href="/blog/series/@post.front_matter.series.as_ref().unwrap()">@post.front_matter.series.as_ref().unwrap()</a></p> 65 - } 66 - 67 - @if post.front_matter.tags.is_some() { 68 - <p>Tags: @for tag in post.front_matter.tags.as_ref().unwrap() { <code>@tag</code> }</p> 69 - } 70 - 71 - <script> 72 - 73 - // The actual function. Set this as an onclick function for your "Share on Mastodon" button 74 - function share_on_mastodon() @{ 75 - // Prefill the form with the user's previously-specified Mastodon instance, if applicable 76 - var default_url = localStorage['mastodon_instance']; 77 - 78 - // If there is no cached instance/domain, then insert a "https://" with no domain at the start of the prompt. 79 - if (!default_url) 80 - default_url = "https://"; 81 - 82 - var instance = prompt("Enter your instance's address: (ex: https://linuxrocks.online)", default_url); 83 - if (instance) @{ 84 - // Handle URL formats 85 - if ( !instance.startsWith("https://") && !instance.startsWith("http://") ) 86 - instance = "https://" + instance; 87 - 88 - // get the current page's url 89 - var url = window.location.href; 90 - 91 - // get the page title from the og:title meta tag, if it exists. 92 - var title = document.querySelectorAll('meta[property="og:title"]')[0].getAttribute("content"); 93 - 94 - // Otherwise, use the <title> tag as the title 95 - if (!title) var title = document.getElementsByTagName("title")[0].innerHTML; 96 - 97 - // Handle slash 98 - if ( !instance.endsWith("/") ) 99 - instance = instance + "/"; 100 - 101 - // Cache the instance/domain for future requests 102 - localStorage['mastodon_instance'] = instance; 103 - 104 - // Hashtags 105 - var hashtags = "#art"; 106 - 107 - @if post.front_matter.tags.is_some() { 108 - hashtags += "@for tag in post.front_matter.tags.as_ref().unwrap() { #@tag}"; 109 - } 110 - 111 - // Tagging users, such as offical accounts or the author of the post 112 - var author = "@@cadey@@pony.social"; 113 - 114 - // Create the Share URL 115 - // https://someinstance.tld/share?text=URL%20encoded%20text 116 - mastodon_url = instance + "share?text=" + encodeURIComponent(title + "\n\n" + url + "\n\n" + hashtags + " " + author); 117 - 118 - // Open a new window at the share location 119 - window.open(mastodon_url, '_blank'); 120 - @} 121 - @} 122 - </script> 123 - 124 - @:footer_html()
-14
templates/gitea.rs.html
··· 1 - @(pkg_name: &str, git_repo: &str, branch: &str) 2 - 3 - <!DOCTYPE html> 4 - <html> 5 - <head> 6 - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> 7 - <meta name="go-import" content="@pkg_name git @git_repo"> 8 - <meta name="go-source" content="@pkg_name @git_repo @git_repo/src/@branch@{/dir@} @git_repo/src/@branch@{/dir@}/@{file@}#L@{line@}"> 9 - <meta http-equiv="refresh" content="0; url=https://godoc.org/@pkg_name"> 10 - </head> 11 - <body> 12 - Please see <a href="https://godoc.org/@pkg_name">here</a> for documentation on this package. 13 - </body> 14 - </html>
-102
templates/header.rs.html
··· 1 - @use chrono::{Datelike, Utc}; 2 - @use crate::handlers::feeds::CACHEBUSTER; 3 - 4 - @(title: Option<&str>, styles: Option<&str>) 5 - 6 - <!DOCTYPE html> 7 - <!-- 8 - MMMMMMMMMMMMMMMMMMNmmNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmmd.:mmMM 9 - MMMMMMMMMMMMMMMMMNmmmNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmydmmmmmNMM 10 - MMMMMMMMMMMMMMMMNm/:mNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmms /mmmmmMMM 11 - MMMMMMMMMMMMMMMNmm:-dmMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmmmdsdmmmmNMMM 12 - MMMMMMMMMMMMMMMmmmmmmmNMMMMMMMMMMMNmmdhhddhhmNNMMMMMMMMMMMMMMMMNmy:hmmmmmmmmMMMM 13 - MMMMMMMMMMMMMMNm++mmmmNMMMMMMmdyo/::.........-:/sdNMMMMMMMMMMNmmms`smmmmmmmNMMMM 14 - MMMMMMMMMMMMMMmd.-dmmmmMMmhs/-....................-+dNMMMMMMNmmmmmmmmmmmmmmMMMMM 15 - MMMMMMMMMMMMMNmmmmmmmmho:-...........................:sNMMNmmmmmmmmmmmmmmmNMNmdd 16 - MMMMMMMMMMMMNmd+ydhs/-.................................-sNmmmmmmmmmmmmmmmdhyssss 17 - MMMMMMMMMMMNNh+`........................................:dmmmmmmmmmmmmmmmyssssss 18 - MMMMNNdhy+:-...........................................+dmmmmmmmmmmmmmmmdsssssss 19 - MMMN+-...............................................-smmmmmmmmmmmmmmmmmysyyhdmN 20 - MMMMNho:::-.--::-.......................----------..:hmmmmmmmmmmmmmmmmmmmNMMMMMM 21 - MMMMMMMMNNNmmdo:......................--------------:ymmmmmmmmmmmmmmmmmmmMMMMMMM 22 - MMMMMMMMMMds+........................-----------------+dmmmmmmmmmmmmmmmmmMMMMMMM 23 - MMMMMMMMMh+........................--------------------:smmmmmmmmmmmmmmNMMMMMMMM 24 - MMMMMMMNy/........................-------------::--------/hmmmmmmmmmmmNMMMMMMNmd 25 - MMMMMMMd/........................--------------so----------odmmmmmmmmMMNmdhhysss 26 - MMMMMMm/........................--------------+mh-----------:ymmmmdhhyysssssssss 27 - MMMMMMo.......................---------------:dmmo------------+dmdysssssssssssss 28 - yhdmNh:......................---------------:dmmmm+------------:sssssssssssyhhdm 29 - sssssy.......................--------------:hmmmmmmos++:---------/sssyyhdmNMMMMM 30 - ssssso......................--------------:hmmmNNNMNdddysso:------:yNNMMMMMMMMMM 31 - ysssss.....................--------------/dmNyy/mMMd``d/------------sNMMMMMMMMMM 32 - MNmdhy-...................--------------ommmh`o/NM/. smh+-----------:yNMMMMMMMMM 33 - MMMMMN+...................------------/hmmss: `-//-.smmmmd+----------:hMMMMMMMMM 34 - MMMMMMd:..................----------:smmmmhy+oosyysdmmy+:. `.--------/dMMMMMMMM 35 - MMMMMMMh-................---------:smmmmmmmmmmmmmmmh/` `/s:-------sMMMMMMMM 36 - MMMMMMMms:...............-------/ymmmmmmmmmmmmmmmd/ :dMMNy/-----+mMMMMMMM 37 - MMMMMMmyss/..............------ommmmmmmmmmmmmmmmd. :yMMMMMMNs:---+mMMMMMMM 38 - MMMMNdssssso-............----..odmmmmmmmmmmmmmmh:.` .sNMMMMMMMMMd/--sMMMMMMMM 39 - MMMmysssssssh/................` -odmmmmmmmmmh+. `omMMMMMMMMMMMMh/+mMMMMMMMM 40 - MNdyssssssymMNy-.............. `/sssso+:. `+mMMMMMMMMMMMMMMMdNMMMMMMMMM 41 - NhssssssshNMMMMNo:............/.` `+dMMMMMMMMMMMMMMMMMMMMMMMMMMMM 42 - ysssssssdMMMMMMMMm+-..........+ddy/.` -omMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 43 - ssssssymMMMMMMMMMMMh/.........-oNMMNmy+--` `-+dNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 44 - ssssydNMMMMMMMMMMMMMNy:........-hMMMMMMMNmdmMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 45 - sssymMMMMMMMMMMMMMMMMMm+....-..:hMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 46 - symNMMMMMMMMMMMMMMMMMMMNo.../-/dMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 47 - dNMMMMMMMMMMMMMMMMMMMMMMh:.:hyNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 48 - la budza pu cusku lu 49 - <<.i do snura .i ko kanro 50 - .i do panpi .i ko gleki>> li'u 51 - --> 52 - <html lang="en"> 53 - <head> 54 - @if title.is_some() { 55 - <title>@title.unwrap() - Xe</title> 56 - } else { 57 - <title>Xe</title> 58 - } 59 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 60 - <link rel="stylesheet" href="/css/hack.css?bustCache=@CACHEBUSTER" /> 61 - <link rel="stylesheet" href="/css/gruvbox-dark.css?bustCache=@CACHEBUSTER" /> 62 - <link rel="stylesheet" href="/css/shim.css?bustCache=@CACHEBUSTER" /> 63 - @if Utc::now().month() == 12 || Utc::now().month() == 1 || Utc::now().month() == 2 { <link rel="stylesheet" href="/css/snow.css?bustCache=@CACHEBUSTER" /> } 64 - <link rel="manifest" href="/static/manifest.json" /> 65 - 66 - <link rel="alternate" title="Xe's Blog" type="application/rss+xml" href="https://xeiaso.net/blog.rss" /> 67 - <link rel="alternate" title="Xe's Blog" type="application/json" href="https://xeiaso.net/blog.json" /> 68 - 69 - <link rel="apple-touch-icon" sizes="57x57" href="/static/favicon/apple-icon-57x57.png"> 70 - <link rel="apple-touch-icon" sizes="60x60" href="/static/favicon/apple-icon-60x60.png"> 71 - <link rel="apple-touch-icon" sizes="72x72" href="/static/favicon/apple-icon-72x72.png"> 72 - <link rel="apple-touch-icon" sizes="76x76" href="/static/favicon/apple-icon-76x76.png"> 73 - <link rel="apple-touch-icon" sizes="114x114" href="/static/favicon/apple-icon-114x114.png"> 74 - <link rel="apple-touch-icon" sizes="120x120" href="/static/favicon/apple-icon-120x120.png"> 75 - <link rel="apple-touch-icon" sizes="144x144" href="/static/favicon/apple-icon-144x144.png"> 76 - <link rel="apple-touch-icon" sizes="152x152" href="/static/favicon/apple-icon-152x152.png"> 77 - <link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-icon-180x180.png"> 78 - <link rel="icon" type="image/png" sizes="192x192" href="/static/favicon/android-icon-192x192.png"> 79 - <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png"> 80 - <link rel="icon" type="image/png" sizes="96x96" href="/static/favicon/favicon-96x96.png"> 81 - <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png"> 82 - <link rel="manifest" href="/static/manifest.json"> 83 - <meta name="msapplication-TileColor" content="#ffffff"> 84 - <meta name="msapplication-TileImage" content="/static/favicon/ms-icon-144x144.png"> 85 - <meta name="theme-color" content="#ffffff"> 86 - <link href="https://mi.within.website/api/webmention/accept" rel="webmention" /> 87 - @if styles.is_some() { 88 - <style> 89 - @styles.unwrap() 90 - </style> 91 - } 92 - </head> 93 - <body class="snow hack gruvbox-dark"> 94 - <div class="container"> 95 - <header> 96 - <span class="logo"></span> 97 - <nav><a href="/">Xe</a> - <a href="/blog">Blog</a> - <a href="/contact">Contact</a> - <a href="/resume">Resume</a> - <a href="/talks">Talks</a> - <a href="/signalboost">Signal Boost</a> - <a href="/feeds">Feeds</a> | <a target="_blank" rel="noopener noreferrer" href="https://graphviz.christine.website">GraphViz</a> - <a target="_blank" rel="noopener noreferrer" href="https://when-then-zen.christine.website/">When Then Zen</a></nav> 98 - </header> 99 - 100 - <br /> 101 - <br /> 102 - <div class="snowframe">
-87
templates/index.rs.html
··· 1 - @use super::{header_html, footer_html}; 2 - 3 - @() 4 - 5 - @:header_html(None, None) 6 - 7 - <link rel="authorization_endpoint" href="https://idp.christine.website/auth"> 8 - <link rel="canonical" href="https://xeiaso.net/"> 9 - <meta name="google-site-verification" content="rzs9eBEquMYr9Phrg0Xm0mIwFjDBcbdgJ3jF6Disy-k" /> 10 - <script type="application/ld+json"> 11 - @{ 12 - "@@context": "http://schema.org/", 13 - "@@type": "Person", 14 - "name": "Xe Iaso", 15 - "url": "https://xeiaso.net", 16 - "image": "https://xeiaso.net/static/img/avatar_large.png", 17 - "sameAs": [ 18 - "https://github.com/Xe", 19 - "https://tulpa.dev/cadey", 20 - "https://twitter.com/theprincessxena", 21 - "https://pony.social/@@cadey", 22 - "https://www.linkedin.com/in/xe-iaso-87a883254/", 23 - "https://www.youtube.com/user/shadowh511" 24 - ] 25 - @} 26 - </script> 27 - 28 - <!-- Twitter --> 29 - <meta name="twitter:card" content="summary" /> 30 - <meta name="twitter:site" content="@@theprincessxena" /> 31 - <meta name="twitter:title" content="Xe Iaso" /> 32 - <meta name="twitter:description" content="Full-stack Engineer" /> 33 - 34 - <!-- Facebook --> 35 - <meta property="og:type" content="website" /> 36 - <meta property="og:title" content="Xe Iaso" /> 37 - <meta property="og:site_name" content="Full-stack Engineer" /> 38 - 39 - <!-- Description --> 40 - <meta name="description" content="Full-stack Engineer" /> 41 - <meta name="author" content="Xe Iaso"> 42 - 43 - <div class="grid"> 44 - <div class="cell -3of12 content"> 45 - <img src="/static/img/avatar.png" alt="My Avatar"> 46 - <br /> 47 - <a href="/contact" class="justify-content-center">Contact Me</a> 48 - </div> 49 - <div class="cell -9of12 content"> 50 - <h1>Xe Iaso</h1> 51 - <h4>Archmage of Infrastructure</h4> 52 - <h5>Skills</h5> 53 - <ul> 54 - <li>Go, Lua, Haskell, C, Rust and other languages</li> 55 - <li>Docker (deployment, development & more)</li> 56 - <li>Mashups of data</li> 57 - <li>kastermakfa</li> 58 - </ul> 59 - 60 - <h5>Highlighted Projects</h5> 61 - <ul> 62 - <li><a href="https://github.com/PonyvilleFM/aura">Aura</a> - PonyvilleFM live DJ recording bot</li> 63 - <li><a href="https://github.com/Xe/site">This website</a> - The backend and templates for this website</li> 64 - <li><a href="https://github.com/Xe/olin">Olin</a> - WebAssembly on the server</li> 65 - <li><a href="https://github.com/Xe/when-then-zen">when-then-zen</a> - Meditation instructions in Gherkin</li> 66 - <li><a href="https://github.com/Xe/printerfacts">printerfacts</a> - Informative facts about printers</li> 67 - <li><a href="https://github.com/Xe/x">x</a> - Experiments and toys</li> 68 - <li><a href="https://h.christine.website">h</a> - A satirical programming language</li> 69 - <li><a href="https://github.com/Xe/gruvbox-css">Xess</a> - My personal CSS framework</li> 70 - <li><a href="https://github.com/Xe/Xeact">Xeact</a> - My personal JavaScript femtoframework for high productivity development</li> 71 - <li><a href="https://tulpa.dev/Xe/quickserv">quickserv</a> - A quick HTTP fileserver</li> 72 - <li><a href="https://github.com/Xe/waifud">waifud</a> - A VM manager that I use in my homelab</li> 73 - </ul> 74 - 75 - <h5>Quick Links</h5> 76 - <ul> 77 - <li><a href="https://github.com/Xe" rel="me">GitHub</a></li> 78 - <li><a href="https://twitter.com/theprincessxena" rel="me">Twitter</a></li> 79 - <li><a href="https://pony.social/@@cadey" rel="me">Mastodon</a></li> 80 - <li><a href="https://www.patreon.com/cadey" rel="me">Patreon</a></li> 81 - </ul> 82 - 83 - <p>Looking for someone for your team? Check <a href="/signalboost">here</a>. 84 - </div> 85 - </div> 86 - 87 - @:footer_html()
-11
templates/notfound.rs.html
··· 1 - @use super::{header_html, footer_html}; 2 - 3 - @(path: String) 4 - 5 - @:header_html(Some("Not Found"), None) 6 - 7 - <h1>Not Found</h1> 8 - 9 - <p>The path at <code>@path</code> could not be found. If you expected this path to exist, please <a href="https://github.com/Xe/site/issues/new">report this issue</a> so it can be fixed.</p> 10 - 11 - @:footer_html()
-20
templates/patrons.rs.html
··· 1 - @use patreon::Users; 2 - @use super::{header_html, footer_html}; 3 - 4 - @(users: Users) 5 - 6 - @:header_html(Some("Patrons"), None) 7 - 8 - <h1>Patrons</h1> 9 - 10 - <p>These awesome people donate to me on <a href="https://patreon.com/cadey">Patreon</a>. If you would like to show up in this list, please donate to me on Patreon. This is refreshed every time the site is deployed.</p> 11 - 12 - <p> 13 - <ul> 14 - @for user in users { 15 - <li>@user.attributes.full_name</li> 16 - } 17 - </ul> 18 - </p> 19 - 20 - @:footer_html()
-13
templates/resume.rs.html
··· 1 - @use super::{header_html, footer_html}; 2 - 3 - @(resume: impl ToHtml) 4 - 5 - @:header_html(Some("Resume"), None) 6 - 7 - @resume 8 - 9 - <hr /> 10 - 11 - <a href="/static/resume/resume.md">Plain-text version of this resume here</a> 12 - 13 - @:footer_html()
-44
templates/salary_transparency.rs.html
··· 1 - @use super::{header_html, footer_html}; 2 - @use crate::{app::Config, tmpl::salary_history}; 3 - @use std::sync::Arc; 4 - 5 - @(cfg: Arc<Config>) 6 - 7 - @:header_html(Some("Salary Transparency"), None) 8 - 9 - <h1>Salary Transparency</h1> 10 - 11 - <p>This page lists my salary for every job I've had in tech. I have had this data open to the public <a 12 - href="https://xeiaso.net/blog/my-career-in-dates-titles-salaries-2019-03-14">for years</a>, but I feel this 13 - should be more prominently displayed on my website. Other people have copied my approach of having a list of 14 - every salary they have ever been paid on their websites, and I would like to set the example by making it 15 - prominent on my website.</p> 16 - 17 - <p>As someone who has seen pay discrimination work in action first-hand, data is one of the ways that we can end 18 - this pointless hiding of information that leads to people being uninformed and hurt by their lack of knowledge. 19 - By laying my hand out in the open like this, I hope to ensure that people are better informed about how much 20 - money they <i>can</i> make, so that they can be paid equally for equal work.</p> 21 - 22 - <h2>Salary Data</h2> 23 - 24 - <p>To get this data, I have scoured over past emails, contracts and everything so that I can be sure that this 25 - information is as accurate as possible. The data on this page intentionally omits employer names.</p> 26 - 27 - @Html(salary_history(cfg.clone()).0) 28 - 29 - <p>I typically update this page once any of the following things happens:</p> 30 - 31 - <ul> 32 - <li>I quit a job.</li> 33 - <li>I get a raise/title change at the same company.</li> 34 - <li>I get terminated from a job.</li> 35 - <li>I get converted from a contracter to a full-time employee.</li> 36 - <li>Other unspecified extranormal events happen.</li> 37 - </ul> 38 - 39 - <p>Please consider publishing your salary data like this as well. By open, voluntary transparency we can help to end 40 - stigmas around discussing pay and help ensure that the next generations of people in tech are treated fairly. 41 - Stigmas thrive in darkness but die in the light of day. You can help end the stigma by playing your cards out in 42 - the open like this.</p> 43 - 44 - @:footer_html()
-17
templates/series.rs.html
··· 1 - @use super::{header_html, footer_html}; 2 - 3 - @(series: Vec<String>) 4 - 5 - @:header_html(Some("Blog"), None) 6 - 7 - <h1>Post Series</h1> 8 - 9 - <p> 10 - <ul> 11 - @for set in series { 12 - <li><a href="/blog/series/@set">@set</a></li> 13 - } 14 - </ul> 15 - </p> 16 - 17 - @:footer_html()
-18
templates/series_posts.rs.html
··· 1 - @use crate::post::Post; 2 - @use super::{header_html, footer_html}; 3 - 4 - @(name: String, posts: &Vec<Post>) 5 - 6 - @:header_html(Some(&name), None) 7 - 8 - <h1>Series: @name</h1> 9 - 10 - <p> 11 - <ul> 12 - @for post in posts { 13 - <li>@post.date.format("%Y-%m-%d") - <a href="/@post.link">@post.front_matter.title</a></li> 14 - } 15 - </ul> 16 - </p> 17 - 18 - @:footer_html()
-46
templates/signalboost.rs.html
··· 1 - @use super::{header_html, footer_html}; 2 - @use crate::signalboost::Person; 3 - 4 - @(people: Vec<Person>) 5 - 6 - @:header_html(Some("Signal Boosts"), None) 7 - 8 - <h1>Signal Boosts</h1> 9 - 10 - <p>These awesome people are currently looking for a job. If you are looking for anyone with these skills, please feel free to reach out to them.</p> 11 - 12 - <p>To add yourself to this list, fork <a href="https://github.com/Xe/site">this website's source code</a> and send a pull request with edits to <code>/dhall/signalboost.dhall</code>.</p> 13 - 14 - <!-- TODO(Xe): Remove this after COVID-19 is less of a thing --> 15 - <p>With COVID-19 raging across the world, these people are in need of a job now more than ever.</p> 16 - 17 - <h2>People</h2> 18 - 19 - <div class="grid signalboost"> 20 - @for person in people { 21 - <div class="cell -4of12 content"> 22 - <big>@person.name</big> 23 - <p>@for tag in person.tags { @tag }</p> 24 - @if person.git_link.is_some() { 25 - <a href="@person.git_link.unwrap()">GitHub</a> 26 - } 27 - @if person.twitter.is_some() { 28 - <a href="@person.twitter.unwrap()">Twitter</a> 29 - } 30 - @if person.linkedin.is_some() { 31 - <a href="#person.linkedin.unwrap()">LinkedIn</a> 32 - } 33 - @if person.fediverse.is_some() { 34 - <a href="@person.fediverse.unwrap()">Fediverse</a> 35 - } 36 - @if person.cover_letter.is_some() { 37 - <a href="@person.cover_letter.unwrap()">Cover letter</a> 38 - } 39 - @if person.website.is_some() { 40 - <a href="@person.website.unwrap()">Website</a> 41 - } 42 - </div> 43 - } 44 - </div> 45 - 46 - @:footer_html()
-23
templates/talkindex.rs.html
··· 1 - @use crate::post::Post; 2 - @use super::{header_html, footer_html}; 3 - @use chrono::prelude::*; 4 - 5 - @(posts: Vec<Post>) 6 - 7 - @:header_html(Some("Talks"), None) 8 - 9 - <h1>Talks</h1> 10 - 11 - <p>Here is a link to all of the talks I have done at conferences. Each of these will have links to the slides (PDF) as well as some brief information about them.</p> 12 - 13 - <p>If you have a compatible reader, be sure to check out my <a href="/blog.rss">RSS Feed</a> for automatic updates. Also check out the <a href="/blog.json">JSONFeed</a>.</p> 14 - 15 - <p> 16 - <ul> 17 - @for post in posts.iter().filter(|p| Utc::today().num_days_from_ce() >= p.date.num_days_from_ce()) { 18 - <li>@post.date.format("%Y-%m-%d") - <a href="/@post.link">@post.front_matter.title</a></li> 19 - } 20 - </ul> 21 - </p> 22 - 23 - @:footer_html()
-124
templates/talkpost.rs.html
··· 1 - @use super::{header_html, footer_html}; 2 - @use crate::{post::Post, tmpl::nag}; 3 - 4 - @(post: Post, body: impl ToHtml, referer: Option<String>) 5 - 6 - @:header_html(Some(&post.front_matter.title.clone()), None) 7 - 8 - <!-- Twitter --> 9 - <meta name="twitter:card" content="summary" /> 10 - <meta name="twitter:site" content="@@theprincessxena" /> 11 - <meta name="twitter:title" content="@post.front_matter.title" /> 12 - <meta name="twitter:description" content="Posted on @post.date.format("%Y-%m-%d")" /> 13 - 14 - <!-- Facebook --> 15 - <meta property="og:type" content="website" /> 16 - <meta property="og:title" content="@post.front_matter.title" /> 17 - <meta property="og:site_name" content="Xe's Blog" /> 18 - 19 - <!-- Description --> 20 - <meta name="description" content="@post.front_matter.title - Xe's Blog" /> 21 - <meta name="author" content="Xe Iaso" /> 22 - 23 - <link rel="canonical" href="https://xeiaso.net/@post.link" /> 24 - 25 - <script type="application/ld+json"> 26 - @{ 27 - "@@context": "http://schema.org", 28 - "@@type": "Article", 29 - "headline": "@post.front_matter.title", 30 - "image": "https://xeiaso.net/static/img/avatar.png", 31 - "url": "https://xeiaso.net/@post.link", 32 - "datePublished": "@post.date.format("%Y-%m-%d")", 33 - "mainEntityOfPage": @{ 34 - "@@type": "WebPage", 35 - "@@id": "https://xeiaso.net/@post.link" 36 - @}, 37 - "author": @{ 38 - "@@type": "Person", 39 - "name": "Xe Iaso" 40 - @}, 41 - "publisher": @{ 42 - "@@type": "Person", 43 - "name": "Xe Iaso" 44 - @} 45 - @} 46 - </script> 47 - 48 - @Html(nag::referer(referer).0) 49 - 50 - @Html(nag::prerelease(&post).0) 51 - 52 - @body 53 - 54 - <a href="@post.front_matter.slides_link.as_ref().unwrap()">Link to the slides</a> 55 - 56 - <hr /> 57 - 58 - <!-- The button that should be clicked. --> 59 - <button onclick="share_on_mastodon()">Share on Mastodon</button> 60 - 61 - <p>This article was posted on @post.detri(). Facts and circumstances may have changed since publication. Please <a href="/contact">contact me</a> before jumping to conclusions if something seems wrong or unclear.</p> 62 - 63 - @if post.front_matter.series.is_some() { 64 - <p>Series: <a href="/blog/series/@post.front_matter.series.as_ref().unwrap()">@post.front_matter.series.as_ref().unwrap()</a></p> 65 - } 66 - 67 - @if post.front_matter.tags.is_some() { 68 - <p>Tags: @for tag in post.front_matter.tags.as_ref().unwrap() { <code>@tag</code> }</p> 69 - } 70 - 71 - <script> 72 - 73 - // The actual function. Set this as an onclick function for your "Share on Mastodon" button 74 - function share_on_mastodon() @{ 75 - // Prefill the form with the user's previously-specified Mastodon instance, if applicable 76 - var default_url = localStorage['mastodon_instance']; 77 - 78 - // If there is no cached instance/domain, then insert a "https://" with no domain at the start of the prompt. 79 - if (!default_url) 80 - default_url = "https://"; 81 - 82 - var instance = prompt("Enter your instance's address: (ex: https://linuxrocks.online)", default_url); 83 - if (instance) @{ 84 - // Handle URL formats 85 - if ( !instance.startsWith("https://") && !instance.startsWith("http://") ) 86 - instance = "https://" + instance; 87 - 88 - // get the current page's url 89 - var url = window.location.href; 90 - 91 - // get the page title from the og:title meta tag, if it exists. 92 - var title = document.querySelectorAll('meta[property="og:title"]')[0].getAttribute("content"); 93 - 94 - // Otherwise, use the <title> tag as the title 95 - if (!title) var title = document.getElementsByTagName("title")[0].innerHTML; 96 - 97 - // Handle slash 98 - if ( !instance.endsWith("/") ) 99 - instance = instance + "/"; 100 - 101 - // Cache the instance/domain for future requests 102 - localStorage['mastodon_instance'] = instance; 103 - 104 - // Hashtags 105 - var hashtags = "#talk"; 106 - 107 - @if post.front_matter.tags.is_some() { 108 - hashtags += "@for tag in post.front_matter.tags.as_ref().unwrap() { #@tag}"; 109 - } 110 - 111 - // Tagging users, such as offical accounts or the author of the post 112 - var author = "@@cadey@@pony.social"; 113 - 114 - // Create the Share URL 115 - // https://someinstance.tld/share?text=URL%20encoded%20text 116 - mastodon_url = instance + "share?text=" + encodeURIComponent(title + "\n\n" + url + "\n\n" + hashtags + " " + author); 117 - 118 - // Open a new window at the share location 119 - window.open(mastodon_url, '_blank'); 120 - @} 121 - @} 122 - </script> 123 - 124 - @:footer_html()