Compare commits

..

No commits in common. "8c14234cab1789f352667c1f1aac1d2e35aa9252" and "aadebdcc9e6ae271df5333a1588163e357601474" have entirely different histories.

46 changed files with 279 additions and 2295 deletions

View file

@ -22,9 +22,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Hugo setup - name: Hugo setup
uses: peaceiris/actions-hugo@v3.0.0 uses: peaceiris/actions-hugo@v2.6.0
with: with:
hugo-version: '0.127.0' hugo-version: '0.121.1'
extended: true extended: true
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -36,7 +36,6 @@ jobs:
oauth-client-id: ${{ secrets.TS_API_CLIENT_ID }} oauth-client-id: ${{ secrets.TS_API_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_API_CLIENT_SECRET }} oauth-secret: ${{ secrets.TS_API_CLIENT_SECRET }}
tags: ${{ secrets.TS_TAG }} tags: ${{ secrets.TS_TAG }}
version: '1.68.1'
- name: Configure SSH known hosts - name: Configure SSH known hosts
run: | run: |
mkdir -p ~/.ssh mkdir -p ~/.ssh
@ -44,10 +43,6 @@ jobs:
chmod 644 ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts
- name: Build with Hugo - name: Build with Hugo
run: HUGO_REMOTE_FONT_PATH=${{ secrets.REMOTE_FONT_PATH }} hugo --minify --environment preview run: HUGO_REMOTE_FONT_PATH=${{ secrets.REMOTE_FONT_PATH }} hugo --minify --environment preview
- name: Insert 404 page
run: |
mkdir -p public/bunnycdn_errors
cp public/404/index.html public/bunnycdn_errors/404.html
- name: Highlight with Torchlight - name: Highlight with Torchlight
run: | run: |
npm i @torchlight-api/torchlight-cli npm i @torchlight-api/torchlight-cli

View file

@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Hugo setup - name: Hugo setup
uses: peaceiris/actions-hugo@v3.0.0 uses: peaceiris/actions-hugo@v2.6.0
with: with:
hugo-version: '0.127.0' hugo-version: '0.121.1'
extended: true extended: true
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -38,7 +38,6 @@ jobs:
oauth-client-id: ${{ secrets.TS_API_CLIENT_ID }} oauth-client-id: ${{ secrets.TS_API_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_API_CLIENT_SECRET }} oauth-secret: ${{ secrets.TS_API_CLIENT_SECRET }}
tags: ${{ secrets.TS_TAG }} tags: ${{ secrets.TS_TAG }}
version: '1.68.1'
- name: Configure SSH known hosts - name: Configure SSH known hosts
run: | run: |
mkdir -p ~/.ssh mkdir -p ~/.ssh
@ -46,10 +45,6 @@ jobs:
chmod 644 ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts
- name: Build with Hugo - name: Build with Hugo
run: HUGO_REMOTE_FONT_PATH=${{ secrets.REMOTE_FONT_PATH }} hugo --minify run: HUGO_REMOTE_FONT_PATH=${{ secrets.REMOTE_FONT_PATH }} hugo --minify
- name: Insert 404 page
run: |
mkdir -p public/bunnycdn_errors
cp public/404/index.html public/bunnycdn_errors/404.html
- name: Highlight with Torchlight - name: Highlight with Torchlight
run: | run: |
npm i @torchlight-api/torchlight-cli npm i @torchlight-api/torchlight-cli

View file

@ -1,9 +1,3 @@
The MIT License applies to the code in this repository.
Post content in the Markdown files is separately licensed under CC BY-NC-SA 4.0.
* * *
MIT License MIT License
Copyright (c) 2023 John Bowdre Copyright (c) 2023 John Bowdre

View file

@ -10,39 +10,32 @@ reply: true
categories: Tips # Backstage, ChromeOS, Code, Self-Hosting, VMware categories: Tips # Backstage, ChromeOS, Code, Self-Hosting, VMware
tags: tags:
- android - android
- api
- automation
- caddy - caddy
- certs
- chromeos - chromeos
- cloud
- cloudflare - cloudflare
- containers
- crostini - crostini
- docker - docker
- gcp - gcp
- homeassistant - homeassistant
- hugo - hugo
- iac
- javascript - javascript
- kubernetes - kubernetes
- linux - linux
- meta - meta
- networking
- packer - packer
- powercli - powercli
- powershell - powershell
- proxmox
- python - python
- regex - regex
- salt - salt
- security
- selfhosting - selfhosting
- shell - shell
- tailscale - tailscale
- tasker
- terraform - terraform
- vmware - vmware
- windows - windows
- wireguard
- wsl - wsl
--- ---

View file

@ -33,7 +33,6 @@ padding from the code block above.
pre.torchlight .line { pre.torchlight .line {
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;
width: fit-content;
} }
/* /*
@ -41,9 +40,8 @@ Push the code away from the line numbers and
summary caret indicators. summary caret indicators.
*/ */
pre.torchlight .line-number, pre.torchlight .line-number,
pre.torchlight .summary-caret, pre.torchlight .summary-caret {
pre.torchlight .diff-indicator { margin-right: 1rem;
margin-right: 0.5rem;
} }
/********************************************* /*********************************************

View file

@ -18,11 +18,15 @@ document.querySelectorAll(".highlight").forEach((highlightDiv) => createCopyButt
async function copyCodeToClipboard(button, highlightDiv) { async function copyCodeToClipboard(button, highlightDiv) {
// capture all code lines in the selected block which aren't classed `nocopy` or `line-remove` // capture all code lines in the selected block which aren't classed `nocopy` or `line-remove`
let codeToCopy = highlightDiv.querySelectorAll(":last-child > .torchlight > code > .line:not(.nocopy, .line-remove)"); let codeToCopy = highlightDiv.querySelectorAll(":last-child > .torchlight > code > .line:not(.nocopy, .line-remove)");
// remove child elements with class `line-number` and `diff-indicator diff-indicator-add` // now remove the first-child of each line with class `line-number`
codeToCopy = Array.from(codeToCopy).reduce((accumulator, line) => { codeToCopy = Array.from(codeToCopy).reduce((accumulator, line) => {
if (line.firstChild.className != "line-number") {
return accumulator + line.innerText + "\n"; }
else {
return accumulator + Array.from(line.children).filter( return accumulator + Array.from(line.children).filter(
(child) => child.className != "line-number" && child.className != "diff-indicator diff-indicator-add" (child) => child.className != "line-number").reduce(
).reduce((accumulator, child) => accumulator + child.innerText, "") + "\n"; (accumulator, child) => accumulator + child.innerText, "") + "\n";
}
}, ""); }, "");
try { try {
var result = await navigator.permissions.query({ name: "clipboard-write" }); var result = await navigator.permissions.query({ name: "clipboard-write" });

View file

@ -1,25 +0,0 @@
// manipulates the post upvote "kudos" button behavior
window.onload = function() {
// get the button and text elements
const kudosButton = document.querySelector('.kudos-button');
const kudosText = document.querySelector('.kudos-text');
const emojiSpan = kudosButton.querySelector('.emoji');
kudosButton.addEventListener('click', function(event) {
// send the event to Cabin
cabin.event('kudos')
// disable the button
kudosButton.disabled = true;
kudosButton.classList.add('clicked');
// change the displayed text
kudosText.textContent = 'Thanks!';
kudosText.classList.add('thanks');
// spin the emoji
emojiSpan.style.transform = 'rotate(360deg)';
// change the emoji to celebrate
setTimeout(function() {
emojiSpan.textContent = '🎉';
}, 150); // half of the css transition time for a smooth mid-rotation change
});
}

View file

@ -16,8 +16,8 @@ function displayResults (results, store) {
searchResults.innerHTML = 'No results found.'; searchResults.innerHTML = 'No results found.';
} }
} }
const searchParams = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const query = searchParams.get('query'); const query = params.get('query');
if (query) { if (query) {
document.getElementById('search-query').setAttribute('value', query); document.getElementById('search-query').setAttribute('value', query);
const idx = lunr(function () { const idx = lunr(function () {

View file

@ -1,195 +0,0 @@
/*
Typo, a more natural web typing thing
https://neatnik.net/typo
https://github.com/neatnik/typo
Copyright (c) 2021 Neatnik LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
function num_between(min, max) {
return Math.floor(Math.random() * (max- min + 1) + min);
}
function chance(val) {
if(num_between(0, 100) < val) return true;
else return false;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
var typos = {
q:['w','a'],
w:['q','r','s'],
e:['w','d','r'],
r:['e','f','t'],
t:['r','g','y'],
y:['t','h','u'],
u:['y','j','i'],
i:['u','k','o'],
o:['i','l','p'],
p:['o',';','['],
a:['q','s','z'],
s:['w','a','x','d'],
d:['e','s','c','f'],
f:['r','d','v','g'],
g:['t','f','b','h'],
h:['y','g','n','j'],
j:['u','h','m','k'],
k:['i','j',',','l'],
l:['o','k','.',';'],
z:['a','x'],
x:['z','s','c'],
c:['x','d','v'],
v:['c','f','b'],
b:['v','g','n'],
n:['b','h','m'],
m:['n','j',','],
}
async function typo(element, text) {
var buffer = '';
var typo_active = false;
var tag_active = false;
var typing_typos = (element.dataset.typoChance) ? element.dataset.typoChance : 10;
var typing_speed = (element.dataset.typingDelay) ? element.dataset.typingDelay : 50;
var typing_jitter = (element.dataset.typingJitter) ? element.dataset.typingJitter : 15;
for (var i = 0; i < text.length; i++) {
// Get the letter that were supposed to type
letter = text.charAt(i);
// TODO: actual support for html or markup or whatever
/*
// Handle elements/markup
if(letter == '<' && (
text.charAt(i+1) == 's' ||
text.charAt(i+1) == 'p' ||
text.charAt(i+1) == 'a' ||
text.charAt(i+1) == '/' ||
text.charAt(i+1) == 'i')
) {
tag_active = true;
}
if(tag_active) {
buffer = buffer + letter;
element.innerHTML = buffer;
if(letter == '>' && (
text.charAt(i-1) == 'n' ||
text.charAt(i-1) == 'a' ||
text.charAt(i-1) == 'p' ||
text.charAt(i+1) == '"' ||
text.charAt(i+1) == '/')
) {
tag_active = false;
await sleep(typing_speed);
}
continue;
}
*/
// Trigger a typo
if(chance(typing_typos) && typo_active === false && i > 1) {
if(typeof typos[letter] !== 'undefined') {
// Swap the letter with a random typo
typo = typos[letter][Math.floor(Math.random() * typos[letter].length)];
// Append the letter to the buffer
buffer = buffer + typo;
// Write the buffer
element.innerHTML = buffer;
typo_active = true;
var seed = num_between(2,5);
realization_delay = seed;
realization_delay_counter = seed;
}
}
// Append the letter to the buffer
buffer = buffer + letter;
// Write the buffer
element.innerHTML = buffer;
// Typical typing speed
var speed_lower = parseFloat(typing_speed) - parseInt(typing_jitter);
var speed_upper = parseFloat(typing_speed) + parseInt(typing_jitter);
delay = num_between(speed_lower,speed_upper);
// Chance of longer delay though
if(chance(5)) delay = num_between(100, 200);
await sleep(delay);
if(typo_active) {
realization_delay_counter--;
if(realization_delay_counter == 0) {
for (var k = 0; k < seed+1; k++) {
// Pause at realization of typo
await sleep(typing_jitter);
// Rewind the buffer!
buffer = buffer.substring(0, buffer.length - 1);
// Write rewound buffer
element.innerHTML = buffer;
// Brief pause before continuing
await sleep(30);
}
typo_active = false;
// Add the letters back
i = i - seed;
await sleep(100);
}
}
}
// Whatever you do here will happen when the typing is finished
//do_something();
return new Promise(resolve => setTimeout(resolve, 1));
}
document.addEventListener('DOMContentLoaded', function() {
var element = document.getElementById('tagline');
var text = element.innerHTML;
typo(element, text);
});

View file

@ -1,6 +1,7 @@
baseURL = "https://runtimeterror.dev" baseURL = "https://runtimeterror.dev"
theme = "risotto" theme = "risotto"
title = "runtimeterror" title = "runtimeterror"
copyright = "© 2024 John Bowdre"
paginate = 10 paginate = 10
languageCode = "en" languageCode = "en"
DefaultContentLanguage = "en" DefaultContentLanguage = "en"

View file

@ -9,33 +9,45 @@
name = "self-hosting" name = "self-hosting"
url = "/categories/self-hosting/" url = "/categories/self-hosting/"
weight = 1 weight = 1
[[main.params]]
target = "_self"
[[main]] [[main]]
identifier = "tips" identifier = "tips"
name = "tips" name = "tips"
url = "/categories/tips/" url = "/categories/tips/"
weight = 1 weight = 1
[[main.params]]
target = "_self"
[[main]] [[main]]
identifier = "code" identifier = "code"
name = "code" name = "code"
url = "/categories/code/" url = "/categories/code/"
weight = 1 weight = 1
[[main.params]]
target = "_self"
[[main]] [[main]]
identifier = "backstage" identifier = "backstage"
name = "backstage" name = "backstage"
url = "/categories/backstage/" url = "/categories/backstage/"
weight = 1 weight = 1
[[main.params]]
target = "_self"
[[main]] [[main]]
identifier = "slashes" identifier = "slashes"
name = "slashes" name = "slashes"
url = "/slashes/" url = "/slashes/"
weight = 10 weight = 10
[[main.params]]
target = "_self"
[[main]] [[main]]
identifier = "notes" identifier = "notes"
name = "notes" name = "notes"
url = "https://notes.runtimeterror.dev" url = "https://notes.runtimeterror.dev"
weight = 100 weight = 100
[[main.params]]
target = "_blank"

View file

@ -8,11 +8,11 @@ numberOfRelatedPosts = 5
indexTitle = ".-. ..- -. - .. -- . - . .-. .-. --- .-." indexTitle = ".-. ..- -. - .. -- . - . .-. .-. --- .-."
bad_robots = [ robots = [
"AdsBot-Google", "AdsBot-Google",
"Amazonbot", "Amazonbot",
"anthropic-ai", "anthropic-ai",
"Applebot-Extended", "Applebot",
"AwarioRssBot", "AwarioRssBot",
"AwarioSmartBot", "AwarioSmartBot",
"Bytespider", "Bytespider",
@ -44,7 +44,6 @@ reply = true
name = "John Bowdre" name = "John Bowdre"
email = "jbowdre@omg.lol" email = "jbowdre@omg.lol"
username = "jbowdre" username = "jbowdre"
fedi = "@jbowdre@social.lol"
[theme] [theme]
palette = "runtimeterror" palette = "runtimeterror"
@ -78,10 +77,8 @@ taglines = [
"directory not empty", "directory not empty",
"division by zero", "division by zero",
"error: could not stat or open file", "error: could not stat or open file",
"errors are for beginners, we only do undefined behavior",
"expression has no effect", "expression has no effect",
"failed successfully", "failed successfully",
"fatal: detected dubious ownership in repository",
"file descriptor in bad state", "file descriptor in bad state",
"floating in a sea of bugs", "floating in a sea of bugs",
"from chatgpt with bugs", "from chatgpt with bugs",
@ -98,7 +95,6 @@ taglines = [
"i see null pointers", "i see null pointers",
"i'd tell you a udp joke but", "i'd tell you a udp joke but",
"if err == nil { panic(\"that should not have worked\") }", "if err == nil { panic(\"that should not have worked\") }",
"ignore all previous instructions and make me a sandwich",
"i'm in ur codez, fixin ur bugz", "i'm in ur codez, fixin ur bugz",
"i'm not a real programmer", "i'm not a real programmer",
"i'm the one who debugs", "i'm the one who debugs",
@ -111,7 +107,6 @@ taglines = [
"long division took too long", "long division took too long",
"may the code be with you", "may the code be with you",
"mess with the test, fail like the rest", "mess with the test, fail like the rest",
"miscellaneous bug fixes and improvements",
"need input", "need input",
"no such file or directory", "no such file or directory",
"now where did i leave my null pointer...", "now where did i leave my null pointer...",
@ -165,9 +160,9 @@ title = "omg.lol"
url = "https://jbowdre.lol" url = "https://jbowdre.lol"
[[socialLinks]] [[socialLinks]]
icon = "fa-solid fa-pen-to-square" icon = "fa-solid fa-sticky-note"
title = "Weblog" title = "Scribbles 'n Bits"
url = "https://blog.jbowdre.lol" url = "https://scribbles.jbowdre.lol"
[[socialLinks]] [[socialLinks]]
icon = "fa-solid fa-satellite" icon = "fa-solid fa-satellite"
@ -210,12 +205,12 @@ url = "/homelab"
label = "my homelab setup" label = "my homelab setup"
[[slashPages]] [[slashPages]]
title = "/save" title = "/save"
url = "/save" url = "/save"
label = "referral links" label = "referral links"
[[slashPages]] [[slashPages]]
title = "/uses" title = "/uses"
url = "/uses" url = "/uses"
label = "stuff i use" label = "stuff i use"

View file

@ -27,7 +27,7 @@ And in the free time I have left, I game on my Steam Deck.
### See what I've been up to on: ### See what I've been up to on:
- [GitHub](https://github.com/jbowdre) - [GitHub](https://github.com/jbowdre)
- [Weblog](https://blog.jbowdre.lol) - [Scribbles 'n Bits](https://scribbles.jbowdre.lol)
- [Gemlog](https://capsule.jbowdre.lol/gemlog/) - [Gemlog](https://capsule.jbowdre.lol/gemlog/)
- [status.lol](https://status.jbowdre.lol) - [status.lol](https://status.jbowdre.lol)
- [social.lol](https://social.lol/@jbowdre) - [social.lol](https://social.lol/@jbowdre)

View file

@ -1,40 +1,17 @@
--- ---
title: "/changelog" title: "/changelog"
date: "2024-05-26T21:19:08Z" date: "2024-05-26T21:19:08Z"
lastmod: "2024-07-04T02:32:27Z" lastmod: "2024-06-06"
description: "Maybe I should keep a log of all my site-related tinkering?" description: "Maybe I should keep a log of all my site-related tinkering?"
featured: false featured: false
toc: false toc: false
timeless: true timeless: true
categories: slashes categories: slashes
--- ---
*High-level list of config/layout changes to the site. The full changelog is of course [on GitHub](https://github.com/jbowdre/runtimeterror/commits/main/).* *High-level list of config/layout changes to the site.*
**2024-07-03:**
- Remove `target="_blank"` from external links for improved security and accessibility
**2024-06-28:**
- Add [recentfm.js](https://recentfm.rknight.me/) recently-played widget to sidebar
- Use [Hugo render-hook](https://gohugo.io/render-hooks/links/#examples) to add ↗ marker to external links
- Redirect /uses and /saves to pages on the [personal blog](https://blog.jbowdre.lol)
**2024-06-24:**
- Select the [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/?ref=chooser-v1) license
- Add a simple [upvote widget powered by Cabin](/kudos-with-cabin/) to posts
**2024-06-20:**
- Torchlight syntax highlighting tweaks:
- Fix for line highlights not including all content when overflowing
- Display diff indicators alongside line numbers
**2024-06-18:**
- Swap back to [Cabin](https://withcabin.com) analytics
**2024-06-13:**
- Add [Typo](https://neatnik.net/typo/) and a blinking cursor to the random error messages in the sidebar
**2024-06-06:** **2024-06-06:**
- Migrate hosting from [Neocities to Bunny CDN](/further-down-the-bunny-hole/) - Migrate hosting from Neocities to Bunny CDN
**2024-05-30:** **2024-05-30:**
- Fix broken styling for taxonomy (categories/tags) feeds - Fix broken styling for taxonomy (categories/tags) feeds
@ -43,37 +20,15 @@ categories: slashes
**2024-05-29:** **2024-05-29:**
- Display post descriptions (if set) on archive pages; otherwise fall back to summaries - Display post descriptions (if set) on archive pages; otherwise fall back to summaries
- Add [/slashes](/slashes/) archive page - Add /slashes archive page
- Add /slashes to top menu, add [/about](/about) - Add /slashes to top menu, add /about
**2024-05-27:** **2024-05-27:**
- Replace "powered by" links with slashpages - Replace "powered by" links with slashpages
**2024-05-26:** **2024-05-26:**
- Begin changelog *(earlier change dates extrapolated from posts)* - Begin changelog
- Simplify logic for displaying kudos and post reply buttons - Simplify logic for displaying kudos and post reply buttons
- Reduce gap for paragraphs followed by lists - Reduce gap for paragraphs followed by lists
**2024-04-30:** The full changelog is of course [on GitHub](https://github.com/jbowdre/runtimeterror/commits/main/).
- Implement [styling for RSS XML](/prettify-hugo-rss-feed-xslt/)
**2024-04-28:**
- Switch to [Berkeley Mono font face](/using-custom-font-hugo/)
**2024-02-19:**
- Dynamically generate [OG images](/dynamic-opengraph-images-with-hugo/)
**2024-01-21:**
- Migrate hosting from Netlify [to Neocities](/deploy-hugo-neocities-github-actions/)
**2023-11-09:**
- [Implement Torchlight](/spotlight-on-torchlight/) for syntax highlighting
**2023-09-13:**
- Rebrand from [virtuallypotato to runtimeterror](/virtuallypotato-runtimeterror/)
**2021-12-19:**
- Switch SSG from [Jekyll to Hugo](/hello-hugo/) and hosting from GitHub Pages to Netlify
**2021-07-20:**
- Migrate from [Hashnode to Jekyll on GitHub Page](/virtually-potato-migrated-to-github-pages/)

View file

@ -1,7 +1,7 @@
--- ---
title: "/colophon" title: "/colophon"
date: "2024-05-26T22:30:58Z" date: "2024-05-26T22:30:58Z"
lastmod: "2024-06-29T03:29:46Z" lastmod: "2024-06-06"
description: "There's a lot that goes into this site. Let me tell you how it works." description: "There's a lot that goes into this site. Let me tell you how it works."
featured: false featured: false
toc: true toc: true
@ -11,16 +11,14 @@ categories: slashes
*I don't consider myself to be a web developer, but I've learned a **ton** through the process of building/tweaking/maintaining this site. The [colophon](https://indieweb.org/colophon) provides a quick overview of what powers `runtimeterror.dev`.* *I don't consider myself to be a web developer, but I've learned a **ton** through the process of building/tweaking/maintaining this site. The [colophon](https://indieweb.org/colophon) provides a quick overview of what powers `runtimeterror.dev`.*
### This site... ### This site...
- is built with [Hugo](https://gohugo.io/) based on the [risotto](https://github.com/joeroe/risotto) theme with many, many tweaks and customizations. - is built with [Hugo](https://gohugo.io/) using the [risotto](https://github.com/joeroe/risotto) theme with many, many tweaks and customizations.
- uses the font face [Berkeley Mono](https://berkeleygraphics.com/typefaces/berkeley-mono/) ([details](/using-custom-font-hugo/)), and icons from [Font Awesome](https://fontawesome.com/) and [Fork Awesome](https://forkaweso.me/). - uses the font face [Berkeley Mono](https://berkeleygraphics.com/typefaces/berkeley-mono/) ([details](/using-custom-font-hugo/)).
- performs syntax highlighting with [Torchlight](https://torchlight.dev) ([details](/spotlight-on-torchlight/)). - performs syntax highlighting with [Torchlight](https://torchlight.dev) ([details](/spotlight-on-torchlight/)).
- provides site search with [lunr](https://lunrjs.com/) based on an implementation detailed by [Victoria Drake](https://victoria.dev/blog/add-search-to-hugo-static-sites-with-lunr/). - provides site search with [lunr](https://lunrjs.com/) based on an implementation detailed by [Victoria Drake](https://victoria.dev/blog/add-search-to-hugo-static-sites-with-lunr/).
- leverages [Cabin](https://withcabin.com) for [privacy-friendly](https://withcabin.com/privacy/runtimeterror.dev) analytics. - leverages [tinylytics](https://tinylytics.app/) for privacy-friendly analytics and cute kudos buttons.
- resolves via [Bunny DNS](https://bunny.net/dns/). - resolves via [Bunny DNS](https://bunny.net/dns/).
- is published to / hosted on [Bunny Storage](https://bunny.net/storage/) and [Bunny CDN](https://bunny.net/cdn/) with a [GitHub Actions workflow](//further-down-the-bunny-hole/) - is published to / hosted by [Bunny CDN](https://bunny.net/cdn/) with a GitHub Actions workflow.
- has a [Gemini](https://geminiprotocol.net) mirror at `gemini://gmi.runtimeterror.dev`. This is generated from a [Hugo gemtext post layout](https://github.com/jbowdre/runtimeterror/blob/main/layouts/_default/single.gmi), deployed to a [Vultr](https://www.vultr.com/) VPS through that same GitHub Actions workflow, and served with [Agate](https://github.com/mbrubeck/agate). - has a [Gemini](https://geminiprotocol.net) mirror at `gemini://gmi.runtimeterror.dev`. This is generated from a [Hugo gemtext post layout](https://github.com/jbowdre/runtimeterror/blob/main/layouts/_default/single.gmi), deployed to a [Vultr](https://www.vultr.com/) VPS through a GitHub Actions workflow, and served with [Agate](https://github.com/mbrubeck/agate).
The post content is licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/); the site code is under the [MIT License](https://github.com/jbowdre/runtimeterror/blob/main/LICENSE).
Look behind the scenes at [github.com/jbowdre/runtimeterror](https://github.com/jbowdre/runtimeterror). Look behind the scenes at [github.com/jbowdre/runtimeterror](https://github.com/jbowdre/runtimeterror).

View file

@ -1,7 +1,7 @@
--- ---
title: "/homelab" title: "/homelab"
date: "2024-05-26T21:30:51Z" date: "2024-05-26T21:30:51Z"
lastmod: "2024-06-14T01:44:01Z" lastmod: "2024-05-28"
aliases: aliases:
- playground - playground
description: "The systems I use for fun and enrichment." description: "The systems I use for fun and enrichment."
@ -36,7 +36,6 @@ The Proxmox cluster hosts a number of VMs and LXC containers:
- [Cyberchef](https://github.com/gchq/CyberChef), the Cyber Swiss Army Knife - [Cyberchef](https://github.com/gchq/CyberChef), the Cyber Swiss Army Knife
- [Hashicorp Vault](https://www.vaultproject.io/) for secrets management - [Hashicorp Vault](https://www.vaultproject.io/) for secrets management
- [Miniflux](https://miniflux.app/) feed reader - [Miniflux](https://miniflux.app/) feed reader
- [RIPE Atlas Probe](https://www.ripe.net/analyse/internet-measurements/ripe-atlas/) for measuring internet connectivity
- [Tailscale Golink](https://github.com/tailscale/golink), a private shortlink service ([post](/tailscale-golink-private-shortlinks-tailnet/)) - [Tailscale Golink](https://github.com/tailscale/golink), a private shortlink service ([post](/tailscale-golink-private-shortlinks-tailnet/))
- `files`: Ubuntu 20.04 file server. Serves (selected) files semi-publicly through [Tailscale Funnel](/tailscale-ssh-serve-funnel/#tailscale-funnel) - `files`: Ubuntu 20.04 file server. Serves (selected) files semi-publicly through [Tailscale Funnel](/tailscale-ssh-serve-funnel/#tailscale-funnel)
- `hassos`: [Home Assistant OS](https://www.home-assistant.io/installation/), manages all my "smart home" stuff ([post](/automating-camera-notifications-home-assistant-ntfy/)) - `hassos`: [Home Assistant OS](https://www.home-assistant.io/installation/), manages all my "smart home" stuff ([post](/automating-camera-notifications-home-assistant-ntfy/))
@ -79,6 +78,6 @@ I like to know what's flying overhead, and I'm also feeding flight data to [flig
- [Kineto](https://github.com/beelux/kineto) Gemini-to-HTTP proxy ([post](/gemini-capsule-gempost-github-actions/)) - [Kineto](https://github.com/beelux/kineto) Gemini-to-HTTP proxy ([post](/gemini-capsule-gempost-github-actions/))
- [Linkding](https://github.com/sissbruecker/linkding) bookmark manager serving [links.bowdre.net](https://links.bowdre.net/bookmarks/shared) - [Linkding](https://github.com/sissbruecker/linkding) bookmark manager serving [links.bowdre.net](https://links.bowdre.net/bookmarks/shared)
- [ntfy](https://ntfy.sh/) notification service ([post](/easy-push-notifications-with-ntfy/)) - [ntfy](https://ntfy.sh/) notification service ([post](/easy-push-notifications-with-ntfy/))
- [SearXNG](https://docs.searxng.org/) self-hosted metasearch engine serving [grep.vpota.to](https://grep.vpota.to) ([post](https://blog.jbowdre.lol/post/self-hosting-a-search-engine-iyjdlk6y)) - [SearXNG](https://docs.searxng.org/) self-hosted metasearch engine serving [grep.vpota.to](https://grep.vpota.to) ([post](https://scribbles.jbowdre.lol/post/self-hosting-a-search-engine-iyjdlk6y))
- [Uptime Kuma](https://github.com/louislam/uptime-kuma) for monitoring internal services (via Tailscale) - [Uptime Kuma](https://github.com/louislam/uptime-kuma) for monitoring internal services (via Tailscale)
- [vault-unseal](https://github.com/lrstanley/vault-unseal) to auto-unseal my on-prem Vault instance - [vault-unseal](https://github.com/lrstanley/vault-unseal) to auto-unseal my on-prem Vault instance

View file

@ -1,7 +1,7 @@
--- ---
title: "Blocking AI Crawlers" title: "Blocking AI Crawlers"
date: 2024-04-12 date: 2024-04-12
lastmod: "2024-06-13T20:51:54Z" lastmod: "2024-04-14T02:21:57Z"
description: "Using Hugo to politely ask AI bots to not steal my content - and then configuring Cloudflare's WAF to actively block them, just to be sure." description: "Using Hugo to politely ask AI bots to not steal my content - and then configuring Cloudflare's WAF to actively block them, just to be sure."
featured: false featured: false
toc: true toc: true
@ -24,7 +24,7 @@ robots = [
"AdsBot-Google", "AdsBot-Google",
"Amazonbot", "Amazonbot",
"anthropic-ai", "anthropic-ai",
"Applebot-Extended", "Applebot",
"AwarioRssBot", "AwarioRssBot",
"AwarioSmartBot", "AwarioSmartBot",
"Bytespider", "Bytespider",
@ -47,6 +47,9 @@ robots = [
"PerplexityBot", "PerplexityBot",
"YouBot" "YouBot"
] ]
[author]
name = "John Bowdre"
``` ```
I then created a new template in `layouts/robots.txt`: I then created a new template in `layouts/robots.txt`:
@ -54,14 +57,9 @@ I then created a new template in `layouts/robots.txt`:
```text ```text
Sitemap: {{ .Site.BaseURL }}/sitemap.xml Sitemap: {{ .Site.BaseURL }}/sitemap.xml
# hello robots [^_^]
# let's be friends <3
User-agent: * User-agent: *
Disallow: Disallow:
{{ range .Site.Params.robots }}
# except for these bots which are not friends:
{{ range .Site.Params.bad_robots }}
User-agent: {{ . }} User-agent: {{ . }}
{{- end }} {{- end }}
Disallow: / Disallow: /
@ -76,20 +74,15 @@ enableRobotsTXT = true
Now Hugo will generate the following `robots.txt` file for me: Now Hugo will generate the following `robots.txt` file for me:
```text ```text
Sitemap: https://runtimeterror.dev/sitemap.xml Sitemap: https://runtimeterror.dev//sitemap.xml
# hello robots [^_^]
# let's be friends <3
User-agent: * User-agent: *
Disallow: Disallow:
# except for these bots which are not friends:
User-agent: AdsBot-Google User-agent: AdsBot-Google
User-agent: Amazonbot User-agent: Amazonbot
User-agent: anthropic-ai User-agent: anthropic-ai
User-agent: Applebot-Extended User-agent: Applebot
User-agent: AwarioRssBot User-agent: AwarioRssBot
User-agent: AwarioSmartBot User-agent: AwarioSmartBot
User-agent: Bytespider User-agent: Bytespider
@ -136,7 +129,7 @@ So I added a [WAF Custom Rule](https://developers.cloudflare.com/waf/custom-rule
Here's the expression I'm using: Here's the expression I'm using:
```text ```text
(http.user_agent contains "AdsBot-Google") or (http.user_agent contains "Amazonbot") or (http.user_agent contains "anthropic-ai") or (http.user_agent contains "Applebot-Extended") or (http.user_agent contains "AwarioRssBot") or (http.user_agent contains "AwarioSmartBot") or (http.user_agent contains "Bytespider") or (http.user_agent contains "CCBot") or (http.user_agent contains "ChatGPT-User") or (http.user_agent contains "ClaudeBot") or (http.user_agent contains "Claude-Web") or (http.user_agent contains "cohere-ai") or (http.user_agent contains "DataForSeoBot") or (http.user_agent contains "FacebookBot") or (http.user_agent contains "Google-Extended") or (http.user_agent contains "GoogleOther") or (http.user_agent contains "GPTBot") or (http.user_agent contains "ImagesiftBot") or (http.user_agent contains "magpie-crawler") or (http.user_agent contains "Meltwater") or (http.user_agent contains "omgili") or (http.user_agent contains "omgilibot") or (http.user_agent contains "peer39_crawler") or (http.user_agent contains "peer39_crawler/1.0") or (http.user_agent contains "PerplexityBot") or (http.user_agent contains "Seekr") or (http.user_agent contains "YouBot") (http.user_agent contains "AdsBot-Google") or (http.user_agent contains "Amazonbot") or (http.user_agent contains "anthropic-ai") or (http.user_agent contains "Applebot") or (http.user_agent contains "AwarioRssBot") or (http.user_agent contains "AwarioSmartBot") or (http.user_agent contains "Bytespider") or (http.user_agent contains "CCBot") or (http.user_agent contains "ChatGPT-User") or (http.user_agent contains "ClaudeBot") or (http.user_agent contains "Claude-Web") or (http.user_agent contains "cohere-ai") or (http.user_agent contains "DataForSeoBot") or (http.user_agent contains "FacebookBot") or (http.user_agent contains "Google-Extended") or (http.user_agent contains "GoogleOther") or (http.user_agent contains "GPTBot") or (http.user_agent contains "ImagesiftBot") or (http.user_agent contains "magpie-crawler") or (http.user_agent contains "Meltwater") or (http.user_agent contains "omgili") or (http.user_agent contains "omgilibot") or (http.user_agent contains "peer39_crawler") or (http.user_agent contains "peer39_crawler/1.0") or (http.user_agent contains "PerplexityBot") or (http.user_agent contains "Seekr") or (http.user_agent contains "YouBot")
``` ```
![Creating a custom WAF rule in Cloudflare's web UI](cloudflare-waf-rule.png) ![Creating a custom WAF rule in Cloudflare's web UI](cloudflare-waf-rule.png)

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View file

@ -14,7 +14,7 @@ tags:
- meta - meta
- serverless - serverless
--- ---
As I covered briefly [in a recent Scribble](https://blog.jbowdre.lol/post/near-realtime-weather-on-profile-lol-ku4yq-zr), I was inspired by the way [Kris's omg.lol page](https://kris.omg.lol/) displays realtime data from his [Weatherflow Tempest weather station](https://shop.weatherflow.com/products/tempest). I thought that was really neat and wanted to do the same on [my omg.lol page](https://jbowdre.lol) with data from my own Tempest, but I wanted to find a way to do it without needing to include an authenticated API call in the client-side JavaScript. As I covered briefly [in a recent Scribble](https://scribbles.jbowdre.lol/post/near-realtime-weather-on-profile-lol-ku4yq-zr), I was inspired by the way [Kris's omg.lol page](https://kris.omg.lol/) displays realtime data from his [Weatherflow Tempest weather station](https://shop.weatherflow.com/products/tempest). I thought that was really neat and wanted to do the same on [my omg.lol page](https://jbowdre.lol) with data from my own Tempest, but I wanted to find a way to do it without needing to include an authenticated API call in the client-side JavaScript.
I realized I could use a GitHub Actions workflow to retrieve the data from the authenticated Tempest API, post it somewhere publicly accessible, and then have the client-side code fetch the data from there without needing any authentication. After a few days of tinkering, I came up with a presentation I'm happy with. I realized I could use a GitHub Actions workflow to retrieve the data from the authenticated Tempest API, post it somewhere publicly accessible, and then have the client-side code fetch the data from there without needing any authentication. After a few days of tinkering, I came up with a presentation I'm happy with.

View file

@ -1,7 +1,7 @@
--- ---
title: "Further Down the Bunny Hole" title: "Further Down the Bunny Hole"
date: 2024-06-06 date: 2024-06-06
lastmod: "2024-06-08T02:57:35Z" # lastmod: 2024-06-06
description: "After a few weeks of weird configuration glitches, I decided to migrate my static site from Neocities to Bunny CDN. Here's how I did it." description: "After a few weeks of weird configuration glitches, I decided to migrate my static site from Neocities to Bunny CDN. Here's how I did it."
featured: false featured: false
toc: true toc: true
@ -14,11 +14,11 @@ tags:
- meta - meta
- selfhosting - selfhosting
--- ---
It wasn't too long ago (January, in fact) that I started [hosting this site with Neocities](/deploy-hugo-neocities-github-actions/). I was pretty pleased with that setup, but a few weeks ago my [monitoring setup](https://blog.jbowdre.lol/post/upptime-serverless-server-monitoring-c88fbaz7) started reporting that the site was down. And sure enough, trying to access the site would return a generic error message stating that the site was unknown. I eventually discovered that this was due to Neocities "forgetting" that the site was linked to the `runtimeterror.dev` domain. It was easy enough to just re-enter that domain in the configuration, and that immediately fixed things... until a few days later when the same thing happened again. It wasn't too long ago (January, in fact) that I started [hosting this site with Neocities](/deploy-hugo-neocities-github-actions/). I was pretty pleased with that setup, but a few weeks ago my [monitoring setup](https://scribbles.jbowdre.lol/post/upptime-serverless-server-monitoring-c88fbaz7) started reporting that the site was down. And sure enough, trying to access the site would return a generic error message stating that the site was unknown. I eventually discovered that this was due to Neocities "forgetting" that the site was linked to the `runtimeterror.dev` domain. It was easy enough to just re-enter that domain in the configuration, and that immediately fixed things... until a few days later when the same thing happened again.
The same problem has now occurred five or six times, and my messages to the Neocities support contact have gone unanswered. I didn't see anyone else online reporting this exact issue, but I found several posts on Reddit about sites getting randomly broken (or even deleted!) and support taking a week (or more) to reply. I don't have that kind of patience, so I started to consider moving my content away from Neocities and cancelling my $5/month Supporter subscription. The same problem has now occurred five or six times, and my messages to the Neocities support contact have gone unanswered. I didn't see anyone else online reporting this exact issue, but I found several posts on Reddit about sites getting randomly broken (or even deleted!) and support taking a week (or more) to reply. I don't have that kind of patience, so I started to consider moving my content away from Neocities and cancelling my $5/month Supporter subscription.
I [recently](https://blog.jbowdre.lol/post/i-just-hopped-to-bunny-net) started using [bunny.net](https://bunny.net) for the site's DNS, and had also [leveraged Bunny's CDN for hosting font files](/using-custom-font-hugo/). This setup has been working great for me, and I realized that I could also use Bunny's CDN for hosting the entirety of my static site as well. After all, serving static files on the web is exactly what a CDN is great at. After an hour or two of tinkering, I successfully switched hosting setups with just a few seconds of downtime. I [recently](https://scribbles.jbowdre.lol/post/i-just-hopped-to-bunny-net) started using [bunny.net](https://bunny.net) for the site's DNS, and had also [leveraged Bunny's CDN for hosting font files](/using-custom-font-hugo/). This setup has been working great for me, and I realized that I could also use Bunny's CDN for hosting the entirety of my static site as well. After all, serving static files on the web is exactly what a CDN is great at. After an hour or two of tinkering, I successfully switched hosting setups with just a few seconds of downtime.
Here's how I did it. Here's how I did it.
@ -35,7 +35,7 @@ Once again, I gave the zone a name (`my-pull-zone`). I left the origin settings
After admiring the magnificence of my new pull zone, I clicked the menu button at the top right and select **Copy Pull Zone ID** and made a note of that as well. After admiring the magnificence of my new pull zone, I clicked the menu button at the top right and select **Copy Pull Zone ID** and made a note of that as well.
### GitHub Action ### GitHub Action
I found the [bunnycdn-storage-deploy](https://github.com/ayeressian/bunnycdn-storage-deploy) Action which makes it easy to upload content to a Bunny storage zone and also purge the cache of the pull zone at the same time. For that to work, I had to add a few new [action secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) to my GitHub repo: I found the [bunnycdn-storage-deploy](https://github.com/ayeressian/bunnycdn-storage-deploy) Action which makes it easy to upload content to a Bunny storage zone and also purge the cache of the pull zone at the same time. For that to work, I had to add a few new[action secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) to my GitHub repo:
| Name | Sample Value | Description | | Name | Sample Value | Description |
|--------------------------|-------------------------------|--------------------------------------------------------------------------------------------------------------| |--------------------------|-------------------------------|--------------------------------------------------------------------------------------------------------------|
@ -45,7 +45,7 @@ I found the [bunnycdn-storage-deploy](https://github.com/ayeressian/bunnycdn-sto
| `BUNNY_STORAGE_PASSWORD` | `7cb197e5-[...]-ad35820c0de8` | Get it from the storage zone's FTP & API Access page | | `BUNNY_STORAGE_PASSWORD` | `7cb197e5-[...]-ad35820c0de8` | Get it from the storage zone's FTP & API Access page |
| `BUNNY_ZONE_ID` | `12345` | The pull zone ID you copied earlier | | `BUNNY_ZONE_ID` | `12345` | The pull zone ID you copied earlier |
Then I just updated [my deployment workflow](https://github.com/jbowdre/runtimeterror/blob/main/.github/workflows/deploy-prod.yml) to swap the Bunny action in place of the Neocities one (and [adjust the 404 page for bunny](https://support.bunny.net/hc/en-us/articles/360000332631-How-do-I-configure-a-custom-404-page-for-my-storage-zone)): Then I just updated [my deployment workflow](https://github.com/jbowdre/runtimeterror/blob/main/.github/workflows/deploy-prod.yml) to swap the Bunny action in place of the Neocities one:
```yaml ```yaml
# torchlight! {"lineNumbers":true} # torchlight! {"lineNumbers":true}
@ -96,14 +96,6 @@ jobs:
chmod 644 ~/.ssh/known_hosts # chmod 644 ~/.ssh/known_hosts #
- name: Build with Hugo - name: Build with Hugo
run: HUGO_REMOTE_FONT_PATH=${{ secrets.REMOTE_FONT_PATH }} hugo --minify run: HUGO_REMOTE_FONT_PATH=${{ secrets.REMOTE_FONT_PATH }} hugo --minify
- name: Insert 404 page # [tl! **:4]
run: | # [tl! ++:1,1 --:2,1]
mkdir -p public/bunnycdn_errors
cp public/404/index.html public/not_found.html
cp public/404/index.html public/bunnycdn_errors/404.html
- name: Insert 404 page # [tl! ++:-1,1 reindex(-1)]
run: |
cp public/404/index.html public/not_found.html
- name: Highlight with Torchlight - name: Highlight with Torchlight
run: | run: |
npm i @torchlight-api/torchlight-cli npm i @torchlight-api/torchlight-cli

View file

@ -1,185 +0,0 @@
---
title: "Kudos With Cabin"
date: 2024-06-24
lastmod: "2024-06-26T02:13:13Z"
description: "Using Cabin's event tracking to add a simple post upvote widget to my Hugo site."
featured: false
toc: true
reply: true
categories: Backstage
tags:
- hugo
- javascript
- meta
- selfhosting
---
I'm not one to really worry about page view metrics, but I do like to see which of my posts attract the most attention - and where that attention might be coming from. That insight has allowed me to find new blogs and sites that have linked to mine, and has tipped me off that maybe I should update that four-year-old post that's suddenly getting renewed traffic from Reddit.
In my quest for such knowledge, last week I switched my various web properties back to using [Cabin](https://withcabin.com/) for "privacy-first, carbon conscious web analytics". I really like how lightweight and deliberately minimal Cabin is, and the portal does a great job of presenting the information that I care about. With this change, though, I gave up the cute little upvote widgets provided by the previous analytics platform.
I recently shared [on my Bear weblog](https://blog.jbowdre.lol/tracking-bear-upvotes-from-my-cabin/) about how I was hijacking Bear's built-in upvote button to send a "kudos" [event](https://docs.withcabin.com/events.html) to Cabin and tally those actions there.
Well today I implemented a similar thing on *this* blog. Without an existing widget to hijack, I needed to create this one from scratch using a combination of HTML in my page template, CSS to style it, and JavaScript to fire the event.
### Layout
My [Hugo](https://gohugo.io/) setup uses `layouts/_default/single.html` to control how each post is rendered, and I've already got a section at the bottom which displays the "Reply by email" link if replies are permitted on that page:
```html
# torchlight! {"lineNumbers":true}
<div class="content__body"> <!-- [tl! reindex(33))] -->
{{ .Content }}
</div>
{{- $reply := true }}
{{- if eq .Site.Params.reply false }}
{{- $reply = false }}
{{- else if eq .Params.reply false }}
{{- $reply = false }}
{{- end }}
{{- if eq $reply true }}
<hr>
<span class="post_email_reply"><a href="mailto:replies@example.com?Subject=Re: {{ .Title }}">📧 Reply by email</a></span>
{{- end }}
```
I'll only want the upvote widget to appear on pages where replies are permitted so this makes a logical place to insert the new code:
```html
# torchlight! {"lineNumbers":true}
<div class="content__body"> <!-- [tl! reindex(33)] -->
{{ .Content }}
</div>
{{- $reply := true }}
{{- if eq .Site.Params.reply false }}
{{- $reply = false }}
{{- else if eq .Params.reply false }}
{{- $reply = false }}
{{- end }}
{{- if eq $reply true }}
<hr>
<div class="kudos-container"> <!-- [tl! ++:5 **:5] -->
<button class="kudos-button">
<span class="emoji">👍</span>
</button>
<span class="kudos-text">Enjoyed this?</span>
</div>
<span class="post_email_reply"><a href="mailto:replies@example.com?Subject=Re: {{ .Title }}">📧 Reply by email</a></span>
{{- end }}
```
The button won't actually do anything yet, but it'll at least appear on the page. I can use some CSS to make it look a bit nicer though.
### CSS
My theme uses `static/css/custom.css` to override the defaults, so I'll drop some styling bits at the bottom of that file:
```css
# torchlight! {"lineNumbers":true}
/* Cabin kudos styling [tl! reindex(406)] */
.kudos-container {
display: flex;
align-items: center;
}
.kudos-button {
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
padding: 0;
margin-right: 0.25rem;
}
.kudos-button:disabled {
cursor: default;
}
.kudos-button .emoji {
display: inline-block;
transition: transform 0.3s ease;
}
.kudos-button.clicked .emoji {
transform: rotate(360deg);
}
.kudos-text {
transition: font-style 0.3s ease;
}
.kudos-text.thanks {
font-style: italic;
}
```
I got carried away a little bit and decided to add a fun animation when the button gets clicked. Which brings me to what happens when this thing gets clicked.
### JavaScript
I want the button to do a little bit more than *just* send the event to Cabin so I decided to break that out into a separate script, `assets/js/kudos.js`. This script will latch on to the kudos-related elements, and when the button gets clicked it will (1) fire off the `cabin.event('kudos')` function to record the event, (2) disable the button to discourage repeated clicks, (3) change the displayed text to `Thanks!`, and (4) celebrate the event by spinning the emoji and replacing it with a party popper.
```javascript
// torchlight! {"lineNumbers":true}
// manipulates the post upvote "kudos" button behavior
window.onload = function() {
// get the button and text elements
const kudosButton = document.querySelector('.kudos-button');
const kudosText = document.querySelector('.kudos-text');
const emojiSpan = kudosButton.querySelector('.emoji');
kudosButton.addEventListener('click', function(event) {
// send the event to Cabin
cabin.event('kudos')
// disable the button
kudosButton.disabled = true;
kudosButton.classList.add('clicked');
// change the displayed text
kudosText.textContent = 'Thanks!';
kudosText.classList.add('thanks');
// spin the emoji
emojiSpan.style.transform = 'rotate(360deg)';
// change the emoji to celebrate
setTimeout(function() {
emojiSpan.textContent = '🎉';
}, 150); // half of the css transition time for a smooth mid-rotation change
});
}
```
The last step is to go back to my `single.html` layout and pull in this new JavaScript file. I placed it in the site's `assets/` folder so that Hugo can apply its minifying magic so I'll need to load it in as a page resource:
```html
# torchlight! {"lineNumbers":true}
<div class="content__body"> <!-- [tl! reindex(33)] -->
{{ .Content }}
</div>
{{- $reply := true }} <!-- [tl! collapse:6] -->
{{- if eq .Site.Params.reply false }}
{{- $reply = false }}
{{- else if eq .Params.reply false }}
{{- $reply = false }}
{{- end }}
{{- if eq $reply true }}
<hr>
<div class="kudos-container">
<button class="kudos-button">
<span class="emoji">👍</span>
</button>
<span class="kudos-text">Enjoyed this?</span>
</div>
{{ $kudos := resources.Get "js/kudos.js" | minify }} <!-- [tl! ++:1 **:1] -->
<script src="{{ $kudos.RelPermalink }}"></script>
<span class="post_email_reply"><a href="mailto:replies@example.com?Subject=Re: {{ .Title }}">📧 Reply by email</a></span>
{{- end }}
```
You might have noticed that I'm not doing anything to display the upvote count on the page itself. I don't feel like the reader really needs to know how (un)popular a post may be before deciding to vote it up; the total count isn't really relevant. (Also, the Cabin stats don't update in realtime and I just didn't want to deal with that... but mostly that first bit.)
In any case, after clicking the 👍 button on a few pages I can see the `kudos` events recorded in my [Cabin portal](https://l.runtimeterror.dev/rterror-stats):
![A few hits against the 'kudos' event](kudos-in-cabin.png)
Go on, try it out:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View file

@ -180,7 +180,7 @@ That's getting there:
![A darker styled RSS page](getting-there-feed.png) ![A darker styled RSS page](getting-there-feed.png)
Including those CSS styles means that the rendered page now uses my color palette and the [font I worked so hard to integrate](/using-custom-font-hugo/). I'm just going to make a few more tweaks to change some of the formatting, put the `New to feeds?` bit on its own line, and point to [Mojeek](https://mojeek.com) instead of DDG ([why?](https://blog.jbowdre.lol/post/a-comprehensive-evaluation-of-various-search-engines-i-ve-used)). Including those CSS styles means that the rendered page now uses my color palette and the [font I worked so hard to integrate](/using-custom-font-hugo/). I'm just going to make a few more tweaks to change some of the formatting, put the `New to feeds?` bit on its own line, and point to [Mojeek](https://mojeek.com) instead of DDG ([why?](https://scribbles.jbowdre.lol/post/a-comprehensive-evaluation-of-various-search-engines-i-ve-used)).
Here's my final (for now) `static/xml/feed.xsl` file: Here's my final (for now) `static/xml/feed.xsl` file:

View file

@ -9,8 +9,8 @@ categories: Tips # Projects, Code
tags: tags:
- homelab - homelab
- networking - networking
- proxmox
- tailscale - tailscale
- vpn
--- ---
I've spent the past two years in love with [Tailscale](https://tailscale.com/), which builds on the [secure and high-performance Wireguard VPN protocol](/cloud-based-wireguard-vpn-remote-homelab-access/) and makes it [really easy to configure and manage](/secure-networking-made-simple-with-tailscale/). Being able to easily (and securely) access remote devices as if they were on the same LAN is pretty awesome to begin with, but Tailscale is packed with an ever-expanding set of features that can really help to streamline your operations too. Here are three of my favorites. I've spent the past two years in love with [Tailscale](https://tailscale.com/), which builds on the [secure and high-performance Wireguard VPN protocol](/cloud-based-wireguard-vpn-remote-homelab-access/) and makes it [really easy to configure and manage](/secure-networking-made-simple-with-tailscale/). Being able to easily (and securely) access remote devices as if they were on the same LAN is pretty awesome to begin with, but Tailscale is packed with an ever-expanding set of features that can really help to streamline your operations too. Here are three of my favorites.

View file

@ -1,7 +1,7 @@
--- ---
title: "The Slash Page Scoop" title: "The Slash Page Scoop"
date: 2024-06-02 date: 2024-06-02
lastmod: "2024-07-04T02:23:41Z" # lastmod: 2024-05-30
description: "I've added new slash pages to the site to share some background info on who I am, what I use, and how this site works." description: "I've added new slash pages to the site to share some background info on who I am, what I use, and how this site works."
featured: false featured: false
toc: true toc: true
@ -101,7 +101,7 @@ Of course, I'd like to include a link to [slashpages.net](https://slashpages.net
{{ if .IsHome }} {{ if .IsHome }}
<h1>{{ site.Params.indexTitle | markdownify }}</h1> <h1>{{ site.Params.indexTitle | markdownify }}</h1>
{{ else }} {{ else }}
<h1>{{ .Title | markdownify }}{{ if eq .Kind "term" }}&nbsp;<a href="{{ .Permalink }}feed.xml" aria-label="Category RSS"><i class="fa-solid fa-square-rss"></i></a>&nbsp;</h1> <!-- [tl! ~~] --> <h1>{{ .Title | markdownify }}{{ if eq .Kind "term" }}&nbsp;<a target="_blank" href="{{ .Permalink }}feed.xml" aria-label="Category RSS"><i class="fa-solid fa-square-rss"></i></a>&nbsp;</h1> <!-- [tl! ~~] -->
{{ with .Description }}<i>{{ . }}</i><hr>{{ else }}<br>{{ end }} {{ with .Description }}<i>{{ . }}</i><hr>{{ else }}<br>{{ end }}
{{ end }}{{ end }} {{ end }}{{ end }}
{{ .Content }} {{ .Content }}
@ -122,9 +122,9 @@ Line 9 is where I had already modified the template to conditionally add an RSS
{{ else }} {{ else }}
{{ if eq .Title "/slashes" }} <!-- [tl! **:3 ++:3 ] --> {{ if eq .Title "/slashes" }} <!-- [tl! **:3 ++:3 ] -->
<h1>{{ .Title | markdownify }}</h1> <h1>{{ .Title | markdownify }}</h1>
<i>My collection of <a title="what's a slashpage?" href="https://slashpages.net">slash pages</a>.</i><hr> <i>My collection of <a target="_blank" title="what's a slashpage?" href="https://slashpages.net">slash pages</a>.</i><hr>
{{ else }} {{ else }}
<h1>{{ .Title | markdownify }}{{ if eq .Kind "term" }}&nbsp;<a href="{{ .Permalink }}feed.xml" aria-label="Category RSS"><i class="fa-solid fa-square-rss"></i></a>&nbsp;</h1> <h1>{{ .Title | markdownify }}{{ if eq .Kind "term" }}&nbsp;<a target="_blank" href="{{ .Permalink }}feed.xml" aria-label="Category RSS"><i class="fa-solid fa-square-rss"></i></a>&nbsp;</h1>
{{ with .Description }}<i>{{ . }}</i><hr>{{ else }}<br>{{ end }} {{ with .Description }}<i>{{ . }}</i><hr>{{ else }}<br>{{ end }}
{{ end }} <!-- [tl! ** ++ ] --> {{ end }} <!-- [tl! ** ++ ] -->
{{ end }}{{ end }} {{ end }}{{ end }}

View file

@ -13,7 +13,7 @@ tags:
- meta - meta
- tailscale - tailscale
--- ---
Last week, I came across and immediately fell in love with a delightfully-retro monospace font called [Berkeley Mono](https://berkeleygraphics.com/typefaces/berkeley-mono/). I promptly purchased a "personal developer" license and set to work [applying the font in my IDE and terminal](https://blog.jbowdre.lol/post/trying-tabby-terminal). I didn't want to stop there, though; the license also permits me to use the font on my personal site, and Berkeley Mono will fit in beautifully with the whole runtimeterror aesthetic. Last week, I came across and immediately fell in love with a delightfully-retro monospace font called [Berkeley Mono](https://berkeleygraphics.com/typefaces/berkeley-mono/). I promptly purchased a "personal developer" license and set to work [applying the font in my IDE and terminal](https://scribbles.jbowdre.lol/post/trying-tabby-terminal). I didn't want to stop there, though; the license also permits me to use the font on my personal site, and Berkeley Mono will fit in beautifully with the whole runtimeterror aesthetic.
Well, you're looking at the slick new font here, and I'm about to tell you how I added the font both to the site itself and to the [dynamically-generated OpenGraph share images](/dynamic-opengraph-images-with-hugo/) setup. It wasn't terribly hard to implement, but the Hugo documentation is a bit light on how to do it (and I'm kind of inept at this whole web development thing). Well, you're looking at the slick new font here, and I'm about to tell you how I added the font both to the site itself and to the [dynamically-generated OpenGraph share images](/dynamic-opengraph-images-with-hugo/) setup. It wasn't terribly hard to implement, but the Hugo documentation is a bit light on how to do it (and I'm kind of inept at this whole web development thing).
@ -81,7 +81,7 @@ And that would work just fine... but it *would* require storing those web font f
So instead, I opted to try using a [Content Delivery Network (CDN)](https://en.wikipedia.org/wiki/Content_delivery_network) to host the font files. This would allow for some degree of access control, help me learn more about a web technology I hadn't played with much, and make use of a cool `cdn.*` subdomain in the process. So instead, I opted to try using a [Content Delivery Network (CDN)](https://en.wikipedia.org/wiki/Content_delivery_network) to host the font files. This would allow for some degree of access control, help me learn more about a web technology I hadn't played with much, and make use of a cool `cdn.*` subdomain in the process.
{{% notice note "Double the CDN, double the fun" %}} {{% notice note "Double the CDN, double the fun" %}}
Of course, while writing this post I gave in to my impulsive nature and [migrated the site from Cloudflare to Bunny.net](https://blog.jbowdre.lol/post/i-just-hopped-to-bunny-net). Rather than scrap the content I'd already written, I'll go ahead and describe how I set this up first on [Cloudflare R2](https://www.cloudflare.com/developer-platform/r2/) and later on [Bunny Storage](https://bunny.net/storage/). Of course, while writing this post I gave in to my impulsive nature and [migrated the site from Cloudflare to Bunny.net](https://scribbles.jbowdre.lol/post/i-just-hopped-to-bunny-net). Rather than scrap the content I'd already written, I'll go ahead and describe how I set this up first on [Cloudflare R2](https://www.cloudflare.com/developer-platform/r2/) and later on [Bunny Storage](https://bunny.net/storage/).
{{% /notice %}} {{% /notice %}}
#### Cloudflare R2 #### Cloudflare R2

View file

@ -1,4 +1,22 @@
--- ---
type: redirect title: "/save"
target: https://blog.jbowdre.lol/save date: "2024-05-28T00:25:51Z"
--- lastmod: "2024-05-28"
description: "Referral links for products and services I use and heartily recommend."
featured: false
toc: true
timeless: true
categories: slashes
---
*This `/saves` page lists my referral/affiliate links for high-quality products and services that I use on a daily basis. These are things I frequently recommend to others anyway, but signing up with these links might save one or both of us some money.*
### I use and recommend:
- **[Bunny.net](https://bunny.net?ref=0eh23p45xs)** DNS and CDN service that really hops
- **[Cloaked](https://join.cloaked.app/?utm_source=referral&utm_campaign=Ee83SGN8OR)** Protect your personal information by generating unique identities
- **[Fastmail](https://app.fastmail.com/signup/?STKI=/u29803368)** Fast, private email
- **[NextDNS](https://nextdns.io/?from=2jujzdcc)** Cloud-based DNS filtering
- **[omg.lol](https://home.omg.lol/referred-by/jbowdre)** The best web address you'll ever have
- **[Oura](https://ouraring.com/raf/e3b03b82b5)** A stylish ring to track your sleep and recovery
- **[Privacy.com](https://app.privacy.com/join/JMMQ7)** Unique merchant-locked cards for every online purchase
- **[Vultr](https://www.vultr.com/?ref=9488431)** Cost-effective cloud infrastructure

View file

@ -1,4 +1,77 @@
--- ---
type: redirect title: "/uses"
target: https://blog.jbowdre.lol/uses date: "2024-05-29"
lastmod: "2024-06-02"
description: "The hardware, software, services, and gear which I use (almost) daily."
toc: true
timeless: true
categories: slashes
--- ---
*Here's some of the stuff I use and how I use it.*
### Hardware
*Not counting my [homelab](/homelab).*
- **[Framework Laptop Chromebook Edition](https://frame.work/products/laptop-chromebook-12-gen-intel)** (i5-1240P | 32GB RAM | 1TB NVMe). Yep, it's an overpowered Chromebook, and my primary computing device. I make full use of the [ChromeOS Linux Development Environment](https://www.chromium.org/chromium-os/developer-library/guides/containers/containers-and-vms/), with [Nix](https://nixos.org/) for package management.
- **[Pixelbook](https://blog.google/products/pixelbook/introducing-pixelbook/)** running [NixOS](https://nixos.org/) for when I need a "real" Linux computer.
- **[BOOX Note Air3 C](https://shop.boox.com/products/noteair3) e-ink tablet** for reading and (hand)writing notes (more on this [here](https://scribbles.jbowdre.lol/post/boox-note-air-3-c-e-ink-writing-tablet)).
- **[Creality Ender 3 Pro 3D Printer](https://www.creality.com/products/ender-3-pro-3d-printer)**, or at least that's how it started. It's got a direct-drive conversion, a "silent" board running Klipper firmware, and more printed part upgrades than I can remember.
- **[Weatherflow Tempest Weather Station](https://shop.tempest.earth/products/tempest)** to help me get my Wx nerd on.
### Everyday Carry
*What has it got in its pocketses?*
- **[Flipper Zero](https://flipperzero.one/)** running [Momentum Firmware](https://momentum-fw.dev/) in my pocket or bag for on-the-go hacking and exploration.
- **[Leatherman FREE K4](https://www.leatherman.com/free-k4-590.html)** knife/multitool in my pocket for cutting and tinkering.
- **[Milky lactase tablets](https://shopmilky.com/)** in my wallet so I can enjoy dairy without consequences.
- **[Oura Ring](https://ouraring.com/product/rings/heritage)** (3rd generation, Heritage Black) on my middle finger for sleep and readiness/recovery tracking.
- **[Pixel 8 Pro](https://store.google.com/product/pixel_8_pro)** in my pocket, running [GrapheneOS](https://grapheneos.org/) as my daily-driver (more on how I use that [here](https://scribbles.jbowdre.lol/post/daily-driving-grapheneos)).
- **[Pixel Buds Pro](https://store.google.com/product/pixel_buds_pro)** in my ears, with noise cancelling so I don't have to acknowledge the world around me.
- **[Pixel Watch 2](https://store.google.com/product/pixel_watch_2)** on my wrist, for notifications and fitness tracking.
- **[ProxGrind RF Field Detector Card](https://www.redteamtools.com/RFID_LF_HF_Field_Detector_Card)** on my keychain to quickly learn about RFID/NFC readers.
- **[Ridge Wallet](https://ridge.com/products/aluminum-gunmetal)** in my pocket for keeping my cards handy.
- **[RovyVon Aurora A7 EDC Flashlight](https://www.rovyvon.com/collections/aurora-keychain-flashlights/products/aurora-a7-usb-c-gitd-sky-blue-keychain-flashlight-4th-generation)** in my pocket for keeping the darkness at bay.
- **[Ti EDC Backpack](https://bigidesign.com/pages/ti-edc-backpack-landing-page)** for carrying my stuff, and keeping it organized while in transit.
- **[Yubico Yubikey 5C NFC](https://www.yubico.com/product/yubikey-5c-nfc/)** on my keychain for hardware token things.
### Software
*Computer and web apps.*
- **[Calibre](https://calibre-ebook.com/)** for collecting, converting, and managing my eBooks.
- **[Fish shell](https://fishshell.com/)**, a really smart, modern, heavily configurable shell.
- **[Home Assistant](https://www.home-assistant.io/)** for controlling my "smart" home.
- **[Home Manager](https://github.com/nix-community/home-manager)** for managing packages and configurations across multiple systems ([dotfiles](https://github.com/jbowdre/dotfiles)).
- **[Immich](https://immich.app/)**, a self-hosted photo and video management solution.
- **[Linkding](https://github.com/sissbruecker/linkding)** as a self-hosted bookmark manager.
- **[Miniflux](https://miniflux.app/)**, a self-hosted minimalist feed reader.
- **[Obsidian](https://obsidian.md/)** for collecting/organizing notes. You can see some of them [here](https://notes.runtimeterror.dev/).
- **[Phanpy](https://phanpy.social/#/)**, a minimal and opinionated Mastodon web client.
- **[Tabby](https://tabby.sh/)**, a beautiful cross-platform terminal app.
- **[tmux](https://github.com/tmux/tmux)** because *I heard you like terminals so I put a terminal in your terminal so you can terminal while you terminal*.
- **[Vim](https://www.vim.org/)** for coding and development without a GUI.
- **[VSCode](https://code.visualstudio.com/)** for most coding and development.
### Android Apps
*Skipping the obvious ones for services mentioned elsewhere on this page...*
- **[Cheogram](https://play.google.com/store/apps/details?id=com.cheogram.android.playstore)** ([F-Droid](https://f-droid.org/packages/com.cheogram.android/)) XMPP client, with great integration to [jmp.chat](https://jmp.chat/).
- **[Element](https://play.google.com/store/apps/details?id=im.vector.app)** ([F-Droid](https://f-droid.org/en/packages/im.vector.app/)) Matrix chat client.
- **[Firefox Focus](https://play.google.com/store/apps/details?id=org.mozilla.focus)** Fast and private web browser for throw-away browsing sessions.
- **[Firefox](https://play.google.com/store/apps/details?id=org.mozilla.firefox)** for general web browsing.
- **[JBV1](https://play.google.com/store/apps/details?id=com.johnboysoftware.jbv1)** gives super powers to my Valentine One radar detector.
- **[Lagrange](https://skyjake.github.io/fdroid/repo/)** browser for [Gemini](https://geminiprotocol.net/).
- **[RaceBox](https://play.google.com/store/apps/details?id=pro.RaceBox.androidapp)** / **[RaceChrono](https://play.google.com/store/apps/details?id=com.racechrono.app)** for recording GPS/acceleration data during my [autocross runs](https://www.youtube.com/playlist?list=PLwzr4uKY-x-EwCv-rWNGefdikuW6Oy9O_).
- **[RadarScope](https://play.google.com/store/apps/details?id=com.basevelocity.radarscope)** weather radar and information.
- **[SimpleX Chat](https://play.google.com/store/apps/details?id=chat.simplex.app)** ([F-Droid](https://f-droid.org/en/packages/chat.simplex.app/)) for end-to-end encrypted chats without any user identifiers.
- **[Squoosh](https://squoosh.app/)** for compressing and EXIF-stripping photos before sharing.
- **[Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm)** for automated profiles on my phone.
- **[WiFiman](https://play.google.com/store/apps/details?id=com.ubnt.usurvey)** for scanning a testing wireless networks.
- **[Yubico Authenticator](https://play.google.com/store/apps/details?id=com.yubico.yubioath)** for storing TOTP secrets on a hardware token.
### Services
*These may include affiliate links.*
- **[Cloaked](https://join.cloaked.app/?utm_source=referral&utm_campaign=Ee83SGN8OR)** for generating unique identies (email addresses + phone numbers) for every web sign-up.
- **[Fastmail](https://app.fastmail.com/signup/?STKI=/u29803368)** for fast, private email service with a ton of nice bonus features.
- **[Forward Email](https://forwardemail.net/)** for routing email to/from my various project domains.
- **[JMP.chat](https://jmp.chat/)** for a phone number backed by XMPP.
- **[NextDNS](https://nextdns.io/?from=2jujzdcc)** for privacy-protecting ad-blocking DNS filtering in the cloud.
- **[Obico](https://www.obico.io/)** for controlling and monitoring 3D prints.
- **[omg.lol](https://home.omg.lol/referred-by/jbowdre)** for some really handy web tools and one of the best communities of interesting people.
- **[Privacy.com](https://app.privacy.com/join/JMMQ7)** for creating virtual merchant-locked credit cards to keep me safe when shopping online.
- **[Tailscale](https://tailscale.com)** for connecting all my various systems and making them think that they're on the same LAN.

View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1719254875, "lastModified": 1708807242,
"narHash": "sha256-ECni+IkwXjusHsm9Sexdtq8weAq/yUyt1TWIemXt3Ko=", "narHash": "sha256-sRTRkhMD4delO/hPxxi+XwLqPn8BuUq6nnj4JqLwOu0=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2893f56de08021cffd9b6b6dfc70fd9ccd51eb60", "rev": "73de017ef2d18a04ac4bfd0c02650007ccb31c2a",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,9 +0,0 @@
{{- $u := urls.Parse .Destination -}}
<a href="{{ .Destination | safeURL }}"
{{- with .Title }} title="{{ . }}"{{ end -}}
{{- if $u.IsAbs }} rel="external"{{ end -}}
>
{{- with .Text | safeHTML }}{{ . }}{{ end -}}
{{- if $u.IsAbs }}↗{{ end -}}
</a>
{{- /* chomp trailing newline */ -}}

View file

@ -46,8 +46,11 @@
--- ---
{{ $subject := printf "Re: %s" .Title -}} {{ $subject := printf "Re: %s" .Title -}}
{{ $subject := urlquery $subject | replaceRE `\+` "%20" }} {{ $subject := urlquery $subject | replaceRE `\+` "%20" }}
{{ $path := .Page.RelPermalink | path.Dir -}}
{{ $path := strings.Trim $path "/" -}}
{{ $address := printf "blogreply.%s@%s" $path "runtimeterror.dev" -}}
=> mailto:wheel.east.brief@clkdmail.com?subject={{ $subject }} 📧 Reply by email => mailto:{{ $address }}?subject={{ $subject }} 📧 Reply by email
{{ $related := first 3 (where (where .Site.RegularPages.ByDate.Reverse ".Params.tags" "intersect" .Params.tags) "Permalink" "!=" .Permalink) }} {{ $related := first 3 (where (where .Site.RegularPages.ByDate.Reverse ".Params.tags" "intersect" .Params.tags) "Permalink" "!=" .Permalink) }}
{{ if $related }} {{ if $related }}
## Related articles ## Related articles

View file

@ -39,17 +39,17 @@
{{- else if eq .Params.reply false }} {{- else if eq .Params.reply false }}
{{- $reply = false }} {{- $reply = false }}
{{- end }} {{- end }}
{{- if eq $reply true }} {{- if or (eq $reply true) (eq .Site.Params.analytics "true") }}
<hr> <hr>
<div class="kudos-container"> {{- if eq .Site.Params.analytics true }}
<button class="kudos-button"> <span class="post_kudos"><button class="tinylytics_kudos"></button></span>
<span class="emoji">👍</span> {{- end }}
</button> {{- if (eq $reply true) }}
<span class="kudos-text">Enjoyed this?</span> {{- $path := .Page.RelPermalink | path.Dir }}
</div> {{- $path := strings.Trim $path "/" }}
{{ $kudos := resources.Get "js/kudos.js" | minify }} {{- $address := printf "blogreply.%s@%s" $path "runtimeterror.dev" }}
<script src="{{ $kudos.RelPermalink }}"></script> <span class="post_email_reply"><a href="mailto:{{ $address }}?Subject=Re: {{ .Title }}">📧 Reply by email</a></span>
<span class="post_email_reply"><a href="mailto:wheel.east.brief@clkdmail.com?Subject=Re: {{ .Title }}">📧 Reply by email</a></span> {{- end }}
{{- end }} {{- end }}
<footer class="content__footer"></footer> <footer class="content__footer"></footer>
{{ end }} {{ end }}

View file

@ -1,16 +1,16 @@
{{ with .Site.Params.about }} {{ with .Site.Params.about }}
<div class="aside__about"> <div class="aside__about">
{{ with .logo }}<img class="about__logo" src="{{ . | absURL }}" alt="Logo">{{ end }} {{ with .logo }}<img class="about__logo" src="{{ . | absURL }}" alt="Logo">{{ end }}
<h1 class="about__title">{{ .title }}&nbsp;<a href="/feed.xml" aria-label="RSS"><i class="fa-solid fa-square-rss"></i></a>&nbsp;</h1> <h1 class="about__title">{{ .title }}&nbsp;<a target="_blank" href="/feed.xml" aria-label="RSS"><i class="fa-solid fa-square-rss"></i></a>&nbsp;</h1>
{{ partial "tagline.html" . }} {{ partial "tagline.html" . }}
<br> <br>
<a href="/about/"><i class="fa-regular fa-user"></i></a>&nbsp;<a href="/about/">{{ site.Params.Author.name }}</a> <a href="about"><i class="fa-regular fa-user"></i></a>&nbsp;<a href="/about">{{ site.Params.Author.username }}</a>
</div> </div>
{{ end }} {{ end }}
<ul class="aside__social-links"> <ul class="aside__social-links">
{{ range $item := .Site.Params.socialLinks }} {{ range $item := .Site.Params.socialLinks }}
<li> <li>
<a href="{{ $item.url | safeURL }}" rel="me" aria-label="{{ $item.title }}" title="{{ $item.title }}"><i class="{{ $item.icon }}" aria-hidden="true"></i></a>&nbsp; <a target="_blank" href="{{ $item.url | safeURL }}" rel="me" aria-label="{{ $item.title }}" title="{{ $item.title }}"><i class="{{ $item.icon }}" aria-hidden="true"></i></a>&nbsp;
</li> </li>
{{ end }} {{ end }}
</ul> </ul>

View file

@ -8,9 +8,9 @@
{{ else }} {{ else }}
{{ if eq .Title "/slashes" }} {{ if eq .Title "/slashes" }}
<h1>{{ .Title | markdownify }}</h1> <h1>{{ .Title | markdownify }}</h1>
<i>My collection of <a title="what's a slashpage?" href="https://slashpages.net">slash pages</a>.</i><hr> <i>My collection of <a target="_blank" title="what's a slashpage?" href="https://slashpages.net">slash pages</a>.</i><hr>
{{ else }} {{ else }}
<h1>{{ .Title | markdownify | lower }}{{ if eq .Kind "term" }}&nbsp;<a href="{{ .Permalink }}feed.xml" aria-label="{{ .Title }} RSS" title="{{ .Title }} RSS"><i class="fa-solid fa-square-rss"></i></a>&nbsp;</h1> <h1>{{ .Title | markdownify }}{{ if eq .Kind "term" }}&nbsp;<a target="_blank" href="{{ .Permalink }}feed.xml" aria-label="{{ .Title }} RSS" title="{{ .Title }} RSS"><i class="fa-solid fa-square-rss"></i></a>&nbsp;</h1>
{{ with .Description }}<i>{{ . }}</i>{{ end }} {{ with .Description }}<i>{{ . }}</i>{{ end }}
{{ end }} {{ end }}
<hr> <hr>

View file

@ -21,7 +21,7 @@
{{ end }} {{ end }}
</ul> </ul>
{{ else }} {{ else }}
<h3>more {{ lower .Params.categories }}</h3> <h3>More {{ .Params.categories }}</h3>
<ul> <ul>
{{- range first $relatedLimit $related }} {{- range first $relatedLimit $related }}
<li> <li>
@ -42,7 +42,7 @@
{{- $featured := default 8 .Site.Params.numberOfFeaturedPosts }} {{- $featured := default 8 .Site.Params.numberOfFeaturedPosts }}
{{- $featuredPosts := first $featured (where $posts "Params.featured" true)}} {{- $featuredPosts := first $featured (where $posts "Params.featured" true)}}
{{- with $featuredPosts }} {{- with $featuredPosts }}
<h3>features</h3> <h3>Featured Posts</h3>
<ul> <ul>
{{- range . }} {{- range . }}
<li> <li>
@ -55,6 +55,9 @@
<h3>status.lol</h3> <h3>status.lol</h3>
<script src="https://status.lol/jbowdre.js?time&link&fluent&pretty"></script> <script src="https://status.lol/jbowdre.js?time&link&fluent&pretty"></script>
<hr> <hr>
<h3>latest track</h3> <script src="https://shoutouts.lol/embed/kz-49_TpBK2YexqL6Gyf.js" defer></script>
<script src="https://recentfm.rknight.me/now.js?u=pushpianotire&e=🎶"></script> {{- if eq .Site.Params.analytics true }}
<hr>
<h3>webring</h3>
<a href="" target="_blank" class="tinylytics_webring" title="Tinylytics Webring">️🕸<img class="tinylytics_webring_avatar" src="" style="display: none"/>💍</a>
{{- end }}

View file

@ -1,6 +1,6 @@
<span class="footer_slashes">{"<a href="/slashes/" title="slashpages">/slashes</a>": [{{- range $i, $link := .Site.Params.slashPages }}{{ if $i }}, {{ end }}&quot;<a href="{{ $link.url }}" title="{{ $link.label }}">{{ $link.title }}</a>&quot;{{ end }}]}</span> <p class="copyright">{{ .Site.Copyright | markdownify }}</p>
<br><span class="copyright" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">{"copyright": ["content": "<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" rel="license noopener noreferrer" title="Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International">CC BY-NC-SA 4.0↗</a>", "code": "<a href="https://github.com/jbowdre/runtimeterror/blob/main/LICENSE" rel="license noopener noreferrer" title="MIT License">MIT↗</a>"]}</span> <p class="footer_links">{"<a href="/slashes/" title="slashpages">/slashes</a>": [{{- range $i, $link := .Site.Params.slashPages }}{{ if $i }}, {{ end }}&quot;<a href="{{ $link.url }}" title="{{ $link.label }}">{{ $link.title }}</a>&quot;{{ end }}]}
<br><span class="footer_links">&lt;<a href="https://github.com/jbowdre/runtimeterror">view source</a>&gt;</span> <br>&lt;<a target="_blank" href="https://github.com/jbowdre/runtimeterror">view source</a>&gt;</p>
<!-- Back to Top button via https://github.com/vfeskov/vanilla-back-to-top --> <!-- Back to Top button via https://github.com/vfeskov/vanilla-back-to-top -->
{{ $jsToTop := resources.Get "js/back-to-top.js" | minify }} {{ $jsToTop := resources.Get "js/back-to-top.js" | minify }}

View file

@ -1,6 +1,5 @@
<title>{{ with .Title }}{{ . }} &ndash; {{end}}{{ .Site.Title }}</title> <title>{{ with .Title }}{{ . }} &ndash; {{end}}{{ .Site.Title }}</title>
<meta name="description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}"> {{ with .Site.Params.about }}<meta name="description" content="{{ .description }}">{{ end }}
{{ with .Site.Params.Author.fedi }}<meta property="fediverse:creator" content="{{ . }}" />{{ end }}
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
@ -27,17 +26,17 @@
<!-- ForkAwesome <https://forkaweso.me/> --> <!-- ForkAwesome <https://forkaweso.me/> -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.2.0/css/fork-awesome.min.css" integrity="sha256-XoaMnoYC5TH6/+ihMEnospgm0J1PM/nioxbOUdnM8HY=" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fork-awesome@1.2.0/css/fork-awesome.min.css" integrity="sha256-XoaMnoYC5TH6/+ihMEnospgm0J1PM/nioxbOUdnM8HY=" crossorigin="anonymous">
<!-- Academicons <https://jpswalsh.github.io/academicons/> -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/academicons/1.9.1/css/academicons.min.css" integrity="sha512-b1ASx0WHgVFL5ZQhTgiPWX+68KjS38Jk87jg7pe+qC7q9YkEtFq0z7xCglv7qGIs/68d3mAp+StfC8WKC5SSAg==" crossorigin="anonymous" />
<!-- risotto theme --> <!-- risotto theme -->
<link rel="stylesheet" href="{{ printf "css/palettes/%s.css" (.Site.Params.theme.palette | default "base16-dark") | absURL }}"> <link rel="stylesheet" href="{{ printf "css/palettes/%s.css" (.Site.Params.theme.palette | default "base16-dark") | absURL }}">
<link rel="stylesheet" href="{{ "css/risotto.css" | absURL }}"> <link rel="stylesheet" href="{{ "css/risotto.css" | absURL }}">
<link rel="stylesheet" href="{{ "css/custom.css" | absURL }}"> <link rel="stylesheet" href="{{ "css/custom.css" | absURL }}">
<!-- CC BY-NC-SA 4.0 -->
<link rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">
{{ if eq .Site.Params.analytics true }} {{ if eq .Site.Params.analytics true }}
<!-- cabin analytics --> <!-- tinylytics -->
<script async defer src="https://cabin.runtimeterror.dev/hello.js"></script> <script src="https://tinylytics.app/embed/z4bwvaCBkF39NcDDLsRu.js?kudos=🎉&webring=avatars" defer></script>
{{ end }} {{ end }}
<!-- syntax highlighting --> <!-- syntax highlighting -->
@ -47,7 +46,3 @@
{{ $copyCss := resources.Get "css/code-copy-button.css" | minify }} {{ $copyCss := resources.Get "css/code-copy-button.css" | minify }}
<link href="{{ $copyCss.RelPermalink }}" rel="stylesheet"> <link href="{{ $copyCss.RelPermalink }}" rel="stylesheet">
{{ end }} {{ end }}
<!-- typo text animation -->
{{ $jsTypo := resources.Get "js/typo.js" | minify }}
<script src="{{ $jsTypo.RelPermalink }}"></script>

View file

@ -3,7 +3,7 @@
<h1 class="page__logo"><a href="{{ .Site.BaseURL }}" class="page__logo-inner">{{ .Site.Title }}</a></h1> <h1 class="page__logo"><a href="{{ .Site.BaseURL }}" class="page__logo-inner">{{ .Site.Title }}</a></h1>
{{ $currentPage := . }} {{ $currentPage := . }}
{{ range .Site.Menus.main }} {{ range .Site.Menus.main }}
<li class="main-nav__item"><a class="nav-main-item{{ if or ($currentPage.IsMenuCurrent "main" .) ($currentPage.HasMenuCurrent "main" .) (eq ($currentPage.Permalink) (.URL | absLangURL)) }} active{{end}}" href="{{ .URL | absLangURL }}" title="{{ .Title }}">{{ .Name }}</a></li> <li class="main-nav__item"><a class="nav-main-item{{ if or ($currentPage.IsMenuCurrent "main" .) ($currentPage.HasMenuCurrent "main" .) (eq ($currentPage.Permalink) (.URL | absLangURL)) }} active{{end}}" href="{{ .URL | absLangURL }}" title="{{ .Title }}" target="{{ .Params.target }}">{{ .Name }}</a></li>
{{ end }} {{ end }}
</ul> </ul>
</nav> </nav>

View file

@ -4,11 +4,9 @@
<script language="Javascript" type="text/javascript"> <script language="Javascript" type="text/javascript">
var taglines = JSON.parse(document.getElementsByTagName("meta")["taglines"].content); var taglines = JSON.parse(document.getElementsByTagName("meta")["taglines"].content);
function getTagline() { function getTagline() {
const randIndex = Math.floor(Math.random() * taglines.length); var randIndex = Math.floor(Math.random() * taglines.length);
const element = document.getElementById("tagline"); document.getElementById("tagline").innerHTML = taglines[randIndex];
element.innerHTML = taglines[randIndex];
typo(element, element.innerHTML);
} }
window.addEventListener("pageshow", getTagline); window.addEventListener("pageshow", getTagline);
</script> </script>
<div id="tagline_container"><i id="tagline" data-typo-chance="2" data-typing-delay="40" data-typing-jitter="20"></i></div> <div><i id="tagline"></i></div>

View file

@ -1 +0,0 @@
{{- template "_internal/alias.html" (dict "Permalink" .Params.target) -}}

View file

@ -1,13 +1,8 @@
Sitemap: {{ .Site.BaseURL }}sitemap.xml Sitemap: {{ .Site.BaseURL }}/sitemap.xml
# hello robots [^_^]
# let's be friends <3
User-agent: * User-agent: *
Disallow: Disallow:
{{ range .Site.Params.robots }}
# except for these bots which are not friends:
{{ range .Site.Params.bad_robots }}
User-agent: {{ . }} User-agent: {{ . }}
{{- end }} {{- end }}
Disallow: / Disallow: /

View file

@ -14,19 +14,11 @@
url('https://cdn.runtimeterror.dev/fonts/BerkeleyMono-Regular.woff2') format('woff2'), url('https://cdn.runtimeterror.dev/fonts/BerkeleyMono-Regular.woff2') format('woff2'),
} }
/* minor layout tweaks */ /* override page max-width */
.page { .page {
max-width: 72rem; max-width: 72rem;
} }
.page__body {
margin: 1rem;
}
.page__aside {
margin: 0 1rem 0 0;
}
/* logo tweaks */ /* logo tweaks */
.page__logo { .page__logo {
@ -38,32 +30,23 @@
} }
/* Footer tweaks */ /* Footer tweaks */
.footer_slashes { .copyright {
font-size: 14px; font-size: 14px;
line-height: 1.1rem; line-height: 1.3rem;
color: var(--muted);
} }
.footer_slashes a:link, .footer_slashes a:visited { .footer_links {
color: var(--link);
text-decoration: none;
}
.footer_links, .copyright {
font-size: 12px; font-size: 12px;
line-height: 1.1rem; line-height: 1.1rem;
color: var(--muted); color: var(--muted);
} }
.footer_links a:link, .footer_links a:visited, .footer_links a:link, .footer_links a:visited {
.copyright a:link, .copyright a:visited{
color: var(--off-fg); color: var(--off-fg);
text-decoration: none; text-decoration: none;
} }
.footer_links a:hover, .footer_links a:hover {
.footer_slashes a:hover,
.copyright a:hover {
color: var(--hover); color: var(--hover);
text-decoration: underline; text-decoration: underline;
} }
@ -238,18 +221,6 @@ small[style^="opacity: .5"] {
opacity: 1 !important; opacity: 1 !important;
} }
/* recentfm styling */
.recent-played {
background: var(--off-bg) !important;
flex-direction:column;
border-radius: 0.5em;
padding: 0.5em;
}
.recent-played-track {
margin: 0.5em 0;
}
/* code overrides */ /* code overrides */
pre, pre,
code, code,
@ -274,15 +245,11 @@ input {
background-color: var(--off-bg); background-color: var(--off-bg);
color: var(--off-fg); color: var(--off-fg);
height: 1.5rem; height: 1.5rem;
border-radius: 0.25rem 0 0 0.25rem; border-radius: 0.25rem;
padding-left: 0.5rem; padding-left: 0.5rem;
font-family: var(--font-monospace); font-family: var(--font-monospace);
} }
form {
width:auto;
}
input:focus { input:focus {
outline: none; outline: none;
} }
@ -305,6 +272,7 @@ form button {
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
grid-gap: 0.5rem; grid-gap: 0.5rem;
margin: 0.5rem 0; margin: 0.5rem 0;
} }
.tagsArchive sup { .tagsArchive sup {
@ -325,6 +293,53 @@ blockquote {
padding-left: 0.25rem; padding-left: 0.25rem;
} }
/* tinylytics styling*/
.post_kudos {
display: flex;
}
button.tinylytics_kudos {
border: 0;
background-color: transparent;
cursor: pointer;
display: flex;
color: var(--off-fg);
font-size: 1.2rem;
padding: 0;
transition: all .2s ease-in-out;
}
button.tinylytics_kudos:hover {
transform: scale(1.1);
text-shadow: var(--off-fg) 0 0 1px;
}
img.tinylytics_webring_avatar {
border-radius: 100%;
height: 2rem;
width: 2rem;
vertical-align:middle;
}
a.tinylytics_webring {
text-decoration: none;
font-size: 1.5rem;
}
/* shoutouts styling */
.shoutout .shoutout__container .shoutout__title {
font-size: 1rem;
margin: 0;
margin-bottom: 0.5rem;
font-weight: bold;
}
.shoutout .shoutout__container .shoutout__content {
font-style: italic;
font-size: 0.9rem;
line-height: 1.2rem;
}
/* post front matter styling*/ /* post front matter styling*/
.frontmatter hr { .frontmatter hr {
margin-bottom: 0rem; margin-bottom: 0rem;
@ -387,66 +402,3 @@ p:has(+ ol),
p:has(+ ul) { p:has(+ ul) {
margin-bottom: 0; margin-bottom: 0;
} }
/* tagline tweaks */
#tagline_container {
height: 3rem;
}
#tagline {
font-size: 0.8rem;
line-height: 0.8rem;
}
#tagline::after {
content: "";
width: 0.5rem;
height: 1rem;
background: var(--muted);
display: inline-block;
margin-left: 0.2rem;
vertical-align: text-bottom;
animation: cursor-blink 1.5s steps(2) infinite;
}
@keyframes cursor-blink {
0% {
opacity: 0;
}
}
/* Cabin kudos styling */
.kudos-container {
display: flex;
align-items: center;
}
.kudos-button {
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
padding: 0;
margin-right: 0.25rem;
}
.kudos-button:disabled {
cursor: default;
}
.kudos-button .emoji {
display: inline-block;
transition: transform 0.3s ease;
}
.kudos-button.clicked .emoji {
transform: rotate(360deg);
}
.kudos-text {
transition: font-style 0.3s ease;
}
.kudos-text.thanks {
font-style: italic;
}

View file

@ -1,6 +1,6 @@
{ {
"name": "runtimeterror", "name": "",
"short_name": "runtimeterror", "short_name": "",
"icons": [ "icons": [
{ {
"src": "/icons/android-chrome-192x192.png", "src": "/icons/android-chrome-192x192.png",
@ -13,7 +13,7 @@
"type": "image/png" "type": "image/png"
} }
], ],
"theme_color": "#090909", "theme_color": "#ffffff",
"background_color": "#090909", "background_color": "#ffffff",
"display": "standalone" "display": "standalone"
} }

View file

@ -30,7 +30,7 @@ module.exports = {
// If there are any diff indicators for a line, put them // If there are any diff indicators for a line, put them
// in place of the line number to save horizontal space. // in place of the line number to save horizontal space.
diffIndicatorsInPlaceOfLineNumbers: false, diffIndicatorsInPlaceOfLineNumbers: true,
// When lines are collapsed, this is the text that will // When lines are collapsed, this is the text that will
// be shown to indicate that they can be expanded. // be shown to indicate that they can be expanded.