Merge branch 'main' into preview

This commit is contained in:
John Bowdre 2024-06-06 17:56:09 -05:00
commit 7014e8d7bb
56 changed files with 1382 additions and 163 deletions

3
.gitmodules vendored
View file

@ -2,3 +2,6 @@
path = themes/risotto path = themes/risotto
url = https://github.com/joeroe/risotto.git url = https://github.com/joeroe/risotto.git
[submodule "themes/hugo-cloak-email"]
path = themes/hugo-cloak-email
url = https://github.com/martignoni/hugo-cloak-email.git

View file

@ -1 +1 @@
[![Deployment Status](https://github.com/jbowdre/runtimeterror/actions/workflows/deploy-to-prod.yml/badge.svg)](https://github.com/jbowdre/runtimeterror/actions/workflows/deploy-to-prod.yml) [![Deployment Status](https://github.com/jbowdre/runtimeterror/actions/workflows/deploy-prod.yml/badge.svg)](https://github.com/jbowdre/runtimeterror/actions/workflows/deploy-prod.yml)

View file

@ -6,7 +6,7 @@ draft: true
description: "This is a new post about..." description: "This is a new post about..."
featured: false featured: false
toc: true toc: true
comments: true reply: true
categories: Tips # Backstage, ChromeOS, Code, Self-Hosting, VMware categories: Tips # Backstage, ChromeOS, Code, Self-Hosting, VMware
tags: tags:
- android - android

View file

@ -65,7 +65,7 @@ enableRobotsTXT = true
disableInlineCSS = true disableInlineCSS = true
[services.rss] [services.rss]
limit = 20 limit = 10
[services.twitter] [services.twitter]
disableInlineCSS = true disableInlineCSS = true

View file

@ -9,21 +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 = "virtuallypotato" identifier = "backstage"
name = "whereis virtuallypotato" name = "backstage"
url = "/virtuallypotato-runtimeterror/" url = "/categories/backstage/"
weight = 1
[[main.params]]
target = "_self"
[[main]]
identifier = "slashes"
name = "slashes"
url = "/slashes/"
weight = 10
[[main.params]]
target = "_self"
[[main]]
identifier = "notes"
name = "notes"
url = "https://notes.runtimeterror.dev"
weight = 100 weight = 100
[[main.params]]
target = "_blank"

View file

@ -37,23 +37,8 @@ robots = [
] ]
# Comments # Comments
comments = true
giscusCategory = "Announcements"
giscusCategoryId = "DIC_kwDOKKEGD84CcG89"
giscusCrossOrigin = "anonymous"
giscusEmitMetadata = "0"
giscusInputPosition = "bottom"
giscusLang = "en"
giscusLoading = "lazy"
giscusMapping = "og:title"
giscusReactions = "0"
giscusRepo = "jbowdre/site-comments"
giscusRepoId = "R_kgDOKKEGDw"
giscusStrict = "0"
giscusTheme = "noborder_gray"
analytics = true analytics = true
kudos = true reply = true
[author] [author]
name = "John Bowdre" name = "John Bowdre"
@ -181,8 +166,8 @@ url = "https://scribbles.jbowdre.lol"
[[socialLinks]] [[socialLinks]]
icon = "fa-solid fa-satellite" icon = "fa-solid fa-satellite"
title = "Gemlog" title = "Gemini Capsule"
url = "https://capsule.jbowdre.lol/gemlog/" url = "gemini://capsule.jbowdre.lol"
[[socialLinks]] [[socialLinks]]
icon = "fa-solid fa-circle-user" icon = "fa-solid fa-circle-user"
@ -199,25 +184,35 @@ icon = "fa-solid fa-envelope"
title = "Email" title = "Email"
url = "mailto:jbowdre@omg.lol" url = "mailto:jbowdre@omg.lol"
[[powerLinks]] [[slashPages]]
title = "hugo" title = "/about"
url = "https://gohugo.io" url = "/about"
label = "about this site"
[[powerLinks]] [[slashPages]]
title = "neocities" title = "/changelog"
url = "https://neocities.org/about" url = "/changelog"
label = "recent changes to the site"
[[powerLinks]] [[slashPages]]
title = "risotto" title = "/colophon"
url = "https://github.com/joeroe/risotto" url = "/colophon"
label = "how this site works"
[[powerLinks]] [[slashPages]]
title = "torchlight" title = "/homelab"
url = "https://torchlight.dev" url = "/homelab"
label = "my homelab setup"
[[powerLinks]] [[slashPages]]
title = "tinylytics" title = "/save"
url = "https://tinylytics.app/home" url = "/save"
label = "referral links"
[[slashPages]]
title = "/uses"
url = "/uses"
label = "stuff i use"
[[verifyLinks]] [[verifyLinks]]
title = "omg.lol" title = "omg.lol"

View file

@ -1,2 +1 @@
comments = false
analytics = false analytics = false

View file

@ -1,2 +1,2 @@
comments = false reply = false
analytics = false analytics = false

View file

@ -3,12 +3,11 @@ title = "404'd!"
noindex = true noindex = true
timeless = true timeless = true
comments = true comments = true
kudos = false
+++ +++
We're not sure what you were looking for but it's not here. We're not sure what you were looking for but it's not here.
![Animated GIF from the movie "The Naked Gun". A man in the foreground proclaims "Please disperse. Nothing to see here." while a building explodes in the background.](/images/nothing-to-see-here.gif) ![Animated GIF from the movie "The Naked Gun". A man in the foreground proclaims "Please disperse. Nothing to see here." while a building explodes in the background.](https://cdn.runtimeterror.dev/images/nothing-to-see-here.gif)
Maybe head back [home](/)? Maybe head back [home](/)?

View file

@ -1,10 +1,14 @@
+++ ---
title = "Hi, I'm John." title: "/about"
description = "A brief introduction to me, this blog, and what you're likely to see here." date: "2024-05-26T21:19:08Z"
timeless = true lastmod: "2024-05-29"
comments = false description: "A brief introduction to me, this blog, and what you're likely to see here."
aliases = ["tldr", "bio"] timeless: true
+++ toc: false
categories: slashes
---
**Hi, I'm John.**
![Me, +/- a few decades](/images/john.jpg) ![Me, +/- a few decades](/images/john.jpg)
You've (somehow) managed to stumble upon my dark corner of the internet[^1]. You've (somehow) managed to stumble upon my dark corner of the internet[^1].

View file

@ -0,0 +1,8 @@
---
title: /slashes
url: /slashes
aliases:
- categories/slashes
description: >
My collection of slash pages.
---

31
content/changelog.md Normal file
View file

@ -0,0 +1,31 @@
---
title: "/changelog"
date: "2024-05-26T21:19:08Z"
lastmod: "2024-05-30"
description: "Maybe I should keep a log of all my site-related tinkering?"
featured: false
toc: false
timeless: true
categories: slashes
---
*High-level list of config/layout changes to the site.*
**2024-05-30:**
- Fix broken styling for taxonomy (categories/tags) feeds
- Open "notes" header link in new tab since it's an external link
- Misc improvements for handling /slashes
**2024-05-29:**
- Display post descriptions (if set) on archive pages; otherwise fall back to summaries
- Add /slashes archive page
- Add /slashes to top menu, add /about
**2024-05-27:**
- Replace "powered by" links with slashpages
**2024-05-26:**
- Begin changelog
- Simplify logic for displaying kudos and post reply buttons
- Reduce gap for paragraphs followed by lists
The full changelog is of course [on GitHub](https://github.com/jbowdre/runtimeterror/commits/main/).

24
content/colophon.md Normal file
View file

@ -0,0 +1,24 @@
---
title: "/colophon"
date: "2024-05-26T22:30:58Z"
lastmod: "2024-05-28"
description: "There's a lot that goes into this site. Let me tell you how it works."
featured: false
toc: true
timeless: true
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`.*
### This site...
- 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/)).
- 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/).
- leverages [tinylytics](https://tinylytics.app/) for privacy-friendly analytics and cute kudos buttons.
- uses [bunny.net](https://bunny.net) for DNS and CDN services.
- is published to / hosted by [Neocities](https://neocities.org) with a GitHub Actions workflow ([details](/deploy-hugo-neocities-github-actions/)).
- 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).
Look behind the scenes at [github.com/jbowdre/runtimeterror](https://github.com/jbowdre/runtimeterror).

83
content/homelab.md Normal file
View file

@ -0,0 +1,83 @@
---
title: "/homelab"
date: "2024-05-26T21:30:51Z"
lastmod: "2024-05-28"
aliases:
- playground
description: "The systems I use for fun and enrichment."
featured: false
toc: true
timeless: true
categories: slashes
---
*I enjoy tinkering with small technology projects, and I learn a ton from these experiments. I also self-host a number of apps/services from my home as well as various cloud environments. This page describes some of my technical playground.*
Everything is connected to my [Tailscale](https://tailscale.com) tailnet, with a GitOps-managed ACL to allow access as needed. This lets me access and manage systems without really caring if they're local or remote. [Tailscale is magic](/secure-networking-made-simple-with-tailscale/).
### On Premise
**Proxmox VE 8 Cluster**
- 1x [Intel NUC 9 Extreme (NUC9i9QNX)](https://www.amazon.com/Intel-Extreme-NUC9i9QNX-Single-Model/dp/B0851JV4R8)
- 9th Gen Intel® Core™ i9-9980HK (8 cores @ 2.40GHz)
- 64GB RAM
- 1x 512GB NVMe system drive
- 2x 1TB NVMe drives (ZFS)
- 2x [HP Elite Mini 800 G9](https://www.hp.com/us-en/shop/pdp/hp-elite-mini-800-g9-desktop-pc-p-88u16ua-aba-1)
- 12th Gen Intel® Core™ i7-12700 (8 cores @ 2.10GHz, 4 cores @ 1.60GHz)
- 96GB RAM
- 1x 512GB NVMe system drive
- 1x 2TB NVMe drive (ZFS)
- [Unifi USW Flex XG 10GbE Switch](https://store.ui.com/us/en/collections/unifi-switching-utility-10-gbps-ethernet/products/unifi-flex-xg)
The Proxmox cluster hosts a number of VMs and LXC containers:
- `doc`: Ubuntu 22.04 Docker host for various on-prem container workloads, served via [Tailscale Serve](/tailscale-ssh-serve-funnel/#tailscale-serve) / [Cloudflare Tunnel](/publish-services-cloudflare-tunnel/):
- [Calibre Web](https://github.com/janeczku/calibre-web) for managing my ebooks
- [Crowdsec](https://www.crowdsec.net/) log processor
- [Cyberchef](https://github.com/gchq/CyberChef), the Cyber Swiss Army Knife
- [Hashicorp Vault](https://www.vaultproject.io/) for secrets management
- [Miniflux](https://miniflux.app/) feed reader
- [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)
- `hassos`: [Home Assistant OS](https://www.home-assistant.io/installation/), manages all my "smart home" stuff ([post](/automating-camera-notifications-home-assistant-ntfy/))
- `immich`: Ubuntu 22.04 [Immich](https://immich.app/) server
- `ipam`: Ubuntu 20.04 [phpIPAM](https://phpipam.net/) server ([post](/integrating-phpipam-with-vrealize-automation-8/#step-0-phpipam-installation-and-base-configuration))
- `salt`: Ubuntu 20.04 [Salt](https://saltproject.io/) Master server for configuration management
- `unifi`: UniFi Network Application. Manages the Unifi switch.
**Hashicorp Nomad Cluster (WIP)**
- 3x [Zima Blade 7700](https://shop.zimaboard.com/products/zimablade-single-board-server-for-cyber-native)
- Intel® Celeron® N3450 (4 cores @ 1.10GHz)
- 16GB RAM
- 1x 32GB eMMC
- 1x 1TB SATA SSD
- [TP-Link TL-SG108E 1GbE Switch](https://www.tp-link.com/us/home-networking/8-port-switch/tl-sg108e/)
This triad of cute little single-board computers will *eventually* be a combination Nomad + Consul + Vault cluster, fully managed with Salt.
**[PiAware](https://www.flightaware.com/adsb/piaware/build) ADS-B/MLAT Receiver**
- Raspberry Pi 2 Model B
- 2x [RTL-SDR Blog V3 R860 RTL2832U 1PPM TCXO SMA Dongle](https://www.amazon.com/gp/product/B0129EBDS2)
- [SIGNALPLUS 1090MHz 12dBi 1.1m ADS-B Antenna](https://www.amazon.com/gp/product/B08XYRMG3V/)
I like to know what's flying overhead, and I'm also feeding flight data to [flightaware.com](https://flightaware.com) and [adsb.fi](https://adsb.fi).
### Cloud
**[Oracle Cloud Infrastructure](https://www.oracle.com/cloud/free/)**
- `git`: Ubuntu 22.04 [Forgejo](https://forgejo.org/) server for [git.bowdre.net](https://git.bowdre.net/explore/repos)
- `smp2`: Ubuntu 22.04 [SimpleX](/simplex/) server
**[Google Cloud Platform](https://cloud.google.com/free/docs/free-cloud-features)**
- `smp`: Ubuntu 22.04 [SimpleX](/simplex/) server
- `smp1`: Ubuntu 22.04 [SimpleX](/simplex/) server
**[Vultr](https://www.vultr.com)**
- `volly`: Ubuntu 22.04 Docker host for various workloads, served either through [Caddy](https://caddyserver.com/) or [Cloudflare Tunnel](/publish-services-cloudflare-tunnel/):
- [Agate](https://github.com/mbrubeck/agate) Gemini server ([post](/gemini-capsule-gempost-github-actions/))
- [Crowdsec](https://www.crowdsec.net) security engine
- [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)
- [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://scribbles.jbowdre.lol/post/self-hosting-a-search-engine-iyjdlk6y))
- [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

View file

@ -6,7 +6,6 @@ description: "Using the power of Home Assistant automations and Ntfy push notifi
featured: true featured: true
alias: automating-security-camera-notifications-with-home-assistant-and-ntfy alias: automating-security-camera-notifications-with-home-assistant-and-ntfy
toc: true toc: true
comments: true
thumbnail: thumbnail.png thumbnail: thumbnail.png
categories: Self-Hosting categories: Self-Hosting
tags: tags:

View file

@ -5,7 +5,6 @@ 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
comments: true
categories: Backstage categories: Backstage
tags: tags:
- cloud - cloud

View file

@ -5,7 +5,6 @@ date: 2024-01-21
description: "Using GitHub Actions to automatically deploy a Hugo website to Neocities." description: "Using GitHub Actions to automatically deploy a Hugo website to Neocities."
featured: false featured: false
toc: true toc: true
comments: true
categories: Backstage categories: Backstage
tags: tags:
- cicd - cicd

View file

@ -6,7 +6,6 @@ description: "Using a GitHub Actions workflow to retrieve data from an authentic
featured: false featured: false
thumbnail: "finished-product.png" thumbnail: "finished-product.png"
toc: true toc: true
comments: true
categories: Backstage categories: Backstage
tags: tags:
- api - api

View file

@ -4,7 +4,6 @@ date: 2023-11-24
description: "I moved my homelab from VMware vSphere to Proxmox VE, and my only regret is that I didn't make this change sooner." description: "I moved my homelab from VMware vSphere to Proxmox VE, and my only regret is that I didn't make this change sooner."
featured: false featured: false
toc: true toc: true
comments: true
categories: Tips # Projects, Code categories: Tips # Projects, Code
tags: tags:
- homelab - homelab

View file

@ -5,7 +5,6 @@ date: "2024-02-19T04:12:27Z"
description: "Using Hugo built-in functions to dynamically generate OpenGraph share images for every post." description: "Using Hugo built-in functions to dynamically generate OpenGraph share images for every post."
featured: false featured: false
toc: true toc: true
comments: true
thumbnail: hugo-logo-wide.png thumbnail: hugo-logo-wide.png
categories: Backstage categories: Backstage
tags: tags:

View file

@ -5,7 +5,6 @@ lastmod: 2023-12-22
description: "Deploying and configuring a self-hosted pub-sub notification handler, getting another server to send a notifcation when it boots, and integrating the notification handler into Home Assistant." description: "Deploying and configuring a self-hosted pub-sub notification handler, getting another server to send a notifcation when it boots, and integrating the notification handler into Home Assistant."
featured: false featured: false
toc: true toc: true
comments: true
categories: Self-Hosting categories: Self-Hosting
tags: tags:
- android - android

View file

@ -4,7 +4,6 @@ date: 2024-01-19
# lastmod: 2024-01-19 # lastmod: 2024-01-19
description: "Never in my life have I seen enabling FIPS *fix* a problem - until now." description: "Never in my life have I seen enabling FIPS *fix* a problem - until now."
featured: false featured: false
comments: true
categories: VMware categories: VMware
tags: tags:
- vmware - vmware

View file

@ -5,7 +5,6 @@ lastmod: "2024-04-05T21:07:38Z"
description: "Deploying a Gemini capsule, powered by Agate, gempost, kineto, Tailscale, and GitHub Actions" description: "Deploying a Gemini capsule, powered by Agate, gempost, kineto, Tailscale, and GitHub Actions"
featured: false featured: false
toc: true toc: true
comments: true
categories: Self-Hosting categories: Self-Hosting
tags: tags:
- caddy - caddy

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View file

@ -0,0 +1,230 @@
---
title: "Prettify Hugo RSS Feeds with XSLT"
date: 2024-04-30
lastmod: "2024-06-05"
description: "Making my Hugo-generated RSS XML look as good to human visitors as it does to feed readers."
featured: false
thumbnail: pretty-feed.png
toc: true
categories: Backstage
tags:
- hugo
- meta
---
I put in some work several months back making my sure my site's RSS would work well in a feed reader. This meant making a *lot* of modifications to the [default Hugo RSS template](https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/_default/rss.xml). I made it load the full article text rather than just the summary, present correctly-formatted code blocks with no loss of important whitespace, include inline images, and even pass online validation checks:
[![Validate my RSS feed](valid-rss-rogers.png)](http://validator.w3.org/feed/check.cgi?url=https%3A//runtimeterror.dev/feed.xml)
But while the feed looks great when rendered by a reader, the browser presentation left some to be desired...
![Ugly RSS rendered without styling](ugly-rss.png)
It feels like there should be a friendlier way to present a feed "landing page" to help users new to RSS figure out what they need to do in order to follow a blog - and there absolutely is. In much the same way that you can prettify plain HTML with the inclusion of a CSS stylesheet, you can also style boring XML using [eXtensible Stylesheet Language Transformations (XSLT)](https://www.w3schools.com/xml/xsl_intro.asp).
This post will quickly cover how I used XSLT to style my blog's RSS feed and made it look like this:
![Much more attractive RSS feed with styling to fit the site's theme](pretty-feed.png)
### Starting Point
The [RSS Templates](https://gohugo.io/templates/rss/) page from the Hugo documentation site provides some basic information about how to generate (and customize) an RSS feed for a Hugo-powered site. The basic steps are to [enable the RSS output in `hugo.toml`](https://github.com/jbowdre/runtimeterror/blob/871be9794234177c1bfa0b1c470873bde8f046be/config/_default/hugo.toml#L19-L30), include a link to the generated feed inside the `<head>` element of the site template (I added it to [`layouts/partials/head.html`](https://github.com/jbowdre/runtimeterror/blob/871be9794234177c1bfa0b1c470873bde8f046be/layouts/partials/head.html#L8-L11)), and (optionally) include a customized RSS template to influence how the output gets rendered.
Here's the content of my `layouts/_default/rss.xml`, before adding the XSLT styling:
```xml
# torchlight! {"lineNumbers": true}
{{- $pctx := . -}}
{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
{{- $pages := slice -}}
{{- if or $.IsHome $.IsSection -}}
{{- $pages = (where $pctx.RegularPages "Type" "in" site.Params.mainSections) -}}
{{- else -}}
{{- $pages = (where $pctx.Pages "Type" "in" site.Params.mainSections) -}}
{{- end -}}
{{- $limit := .Site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
<rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>
<link>{{ .Permalink }}</link>
<description>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}</description>
<generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}
<language>{{.}}</language>{{end}}{{ with .Site.Copyright }}
<copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}
<lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
{{- with .OutputFormats.Get "RSS" -}}
{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
{{- end -}}
<image>
<url>{{ .Site.Params.fallBackOgImage | absURL }}</url>
<title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>
<link>{{ .Permalink }}</link>
</image>
{{ range $pages }}
<item>
<title>{{ .Title | plainify }}</title>
<link>{{ .Permalink }}</link>
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
{{ with .Site.Params.Author.name }}<dc:creator>{{.}}</dc:creator>{{ end }}
{{ with .Params.series }}<category>{{ . | lower }}</category>{{ end }}
{{ range (.GetTerms "tags") }}
<category>{{ .LinkTitle }}</category>{{ end }}
<guid>{{ .Permalink }}</guid>
{{- $content := replaceRE "a href=\"(#.*?)\"" (printf "%s%s%s" "a href=\"" .Permalink "$1\"") .Content -}}
{{- $content = replaceRE "img src=\"(.*?)\"" (printf "%s%s%s" "img src=\"" .Permalink "$1\"") $content -}}
{{- $content = replaceRE "<svg.*</svg>" "" $content -}}
{{- $content = replaceRE `-moz-tab-size:\d;-o-tab-size:\d;tab-size:\d;?` "" $content -}}
<description>{{ $content | html }}</description>
</item>
{{ end }}
</channel>
</rss>
```
There's a lot going on here, but much of it is conditional logic so that Hugo can use the same template to render feeds for individual tags, categories, or the entire site. It then loops through the `range` of pages of that type to generate the data for each post. It also uses the [Hugo `strings.ReplaceRE` function](https://gohugo.io/functions/strings/replacere/) to replace relative image and anchor links with the full paths so those references will work correctly in readers, and to clean up some potentially-problematic HTML markup that was causing validation failures.
All I really need to do to get this XML ready to be styled is just link in a style sheet, and I'll do that by inserting a `<?xml-stylesheet />` element directly below the top-level `<?xml />` one:
```xml
# torchlight! {"lineNumbers": true, "lineNumbersStart": 10}
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
{{ printf "<?xml-stylesheet type=\"text/xsl\" href=\"/xml/feed.xsl\" media=\"all\"?>" | safeHTML }} <!-- [tl! ++ ] -->
<rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/">
```
I'll put the stylesheet in `static/xml/feed.xsl` so Hugo will make it available at the appropriate path.
### Creating the Style
While trying to figure out how I could dress up my RSS XML, I came across the [default XSL file](https://github.com/getnikola/nikola/blob/master/nikola/data/themes/base/assets/xml/rss.xsl) provided with the [Nikola SSG](https://getnikola.com/), and I thought it looked like a pretty good starting point.
Here's Nikola's default XSL:
```xml
# torchlight! {"lineNumbers": true}
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="1.0">
<xsl:output method="xml"/>
<xsl:template match="/">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width"/>
<title><xsl:value-of select="rss/channel/title"/> (RSS)</title>
<style><![CDATA[html{margin:0;padding:0;}body{color:hsl(180,1%,31%);font-family:Helvetica,Arial,sans-serif;font-size:17px;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:20rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}ol{list-style-type:disc;padding-left:1rem;}h2{font-size:22px;font-weight:inherit;}]]></style>
</head>
<body>
<h1><xsl:value-of select="rss/channel/title"/> (RSS)</h1>
<p>This is an <abbr title="Really Simple Syndication">RSS</abbr> feed. To subscribe to it, copy its address and paste it when your feed reader asks for it. It will be updated periodically in your reader. New to feeds? <a href="https://duckduckgo.com/?q=how+to+get+started+with+rss+feeds" title="Search on the web to learn more">Learn more</a>.</p>
<p>
<label for="address">RSS address:</label>
<input><xsl:attribute name="type">url</xsl:attribute><xsl:attribute name="id">address</xsl:attribute><xsl:attribute name="spellcheck">false</xsl:attribute><xsl:attribute name="value"><xsl:value-of select="rss/channel/atom:link[@rel='self']/@href"/></xsl:attribute></input>
</p>
<p>Preview of the feeds current headlines:</p>
<ol>
<xsl:for-each select="rss/channel/item">
<li><h2><a><xsl:attribute name="href"><xsl:value-of select="link"/></xsl:attribute><xsl:value-of select="title"/></a></h2></li>
</xsl:for-each>
</ol>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
```
If I just plug that in at `static/xml/feed.xml`, I do successfully get a styled (though *very* white) RSS page:
![A very bright white (but styled) RSS page](very-white-feed.png)
I'd like this to inherit the same styling as the rest of the site so that it looks like it belongs. I can go a long way toward that by bringing in the CSS stylesheets that are used on every page, and I'll also tweak the existing `<style />` element to remove some conflicts with my preferred styling:
```xml
# torchlight! {"lineNumbers": true, "lineNumbersStart": 10}
<title><xsl:value-of select="rss/channel/title"/> (RSS)</title>
<style><![CDATA[html{margin:0;padding:0;}body{color:hsl(180,1%,31%);font-family:Helvetica,Arial,sans-serif;font-size:17px;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:20rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}ol{list-style-type:disc;padding-left:1rem;}h2{font-size:22px;font-weight:inherit;}]]></style><title><xsl:value-of select="rss/channel/title"/> (RSS)</title> <!-- [tl! remove ] -->
<style><![CDATA[html{margin:0;padding:0;}body{color:font-size:1.1rem;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:20rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}h2{font-size:22px;font-weight:inherit;}h2:before{content:"" !important;}]]></style> <!-- [tl! ++:3 reindex(-1) ] -->
<link rel="stylesheet" href="/css/palettes/runtimeterror.css" />
<link rel="stylesheet" href="/css/risotto.css" />
<link rel="stylesheet" href="/css/custom.css" />
</head>
```
While I'm at it, I'll also go on and add in some favicons:
```xml
# torchlight! {"lineNumbers": true, "lineNumbersStart": 10}
<title><xsl:value-of select="rss/channel/title"/> (RSS)</title>
<style><![CDATA[html{margin:0;padding:0;}body{color:font-size:1.1rem;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:20rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}h2{font-size:22px;font-weight:inherit;}h2:before{content:"" !important;}]]></style>
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" /> <!-- [tl! ++:5] -->
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
<link rel="manifest" href="/icons/site.webmanifest" />
<link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#5bbad5" />
<link rel="shortcut icon" href="/icons/favicon.ico" />
<link rel="stylesheet" href="/css/palettes/runtimeterror.css" />
<link rel="stylesheet" href="/css/risotto.css" />
<link rel="stylesheet" href="/css/custom.css" />
```
That's getting there:
![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://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:
```xml
# torchlight! {"lineNumbers": true}
<?xml version="1.0" encoding="UTF-8"?>
<!-- adapted from https://github.com/getnikola/nikola/blob/master/nikola/data/themes/base/assets/xml/rss.xsl -->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="1.0">
<xsl:output method="xml"/>
<xsl:template match="/">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width"/>
<title><xsl:value-of select="rss/channel/title"/> (RSS)</title>
<style><![CDATA[html{margin:0;padding:0;}body{color:font-size:1.1rem;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:20rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}h2{font-size:22px;font-weight:inherit;}h3:before{content:"" !important;}]]></style>
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
<link rel="manifest" href="/icons/site.webmanifest" />
<link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#5bbad5" />
<link rel="shortcut icon" href="/icons/favicon.ico" />
<link rel="stylesheet" href="/css/palettes/runtimeterror.css" />
<link rel="stylesheet" href="/css/risotto.css" />
<link rel="stylesheet" href="/css/custom.css" />
</head>
<body>
<h1><xsl:value-of select="rss/channel/title"/> (RSS)</h1>
<p>This is an <abbr title="Really Simple Syndication">RSS</abbr> feed. To subscribe to it, copy its address and paste it when your feed reader asks for it. It will be updated periodically in your reader.</p>
<p>New to feeds? <a href="https://www.mojeek.com/search?q=how+to+get+started+with+rss+feeds" title="Search on the web to learn more">Learn more</a>.</p>
<p>
<label for="address">RSS address:</label>
<input><xsl:attribute name="type">url</xsl:attribute><xsl:attribute name="id">address</xsl:attribute><xsl:attribute name="spellcheck">false</xsl:attribute><xsl:attribute name="value"><xsl:value-of select="rss/channel/atom:link[@rel='self']/@href"/></xsl:attribute></input>
</p>
<p><h2>Recent posts:</h2></p>
<ul>
<xsl:for-each select="rss/channel/item">
<li><h3><a><xsl:attribute name="href"><xsl:value-of select="link"/></xsl:attribute><xsl:value-of select="title"/></a></h3></li>
</xsl:for-each>
</ul>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
```
I'm pretty pleased with [that result](/feed.xml)!

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View file

@ -5,7 +5,6 @@ date: 2024-01-15
description: "Exploring Cloudflare Tunnel as an alternative to Tailscale Funnel for secure public access to internal resources." description: "Exploring Cloudflare Tunnel as an alternative to Tailscale Funnel for secure public access to internal resources."
featured: false featured: false
toc: true toc: true
comments: true
categories: Self-Hosting categories: Self-Hosting
tags: tags:
- cloudflare - cloudflare

View file

@ -5,7 +5,6 @@ lastmod: 2024-02-03
description: "A hasty Salt state to deploy netdata monitoring and publish it internally on my tailnet with Tailscale Serve" description: "A hasty Salt state to deploy netdata monitoring and publish it internally on my tailnet with Tailscale Serve"
featured: false featured: false
toc: true toc: true
comments: true
categories: Code categories: Code
tags: tags:
- homelab - homelab

View file

@ -5,7 +5,6 @@ lastmod: 2023-11-13
description: "Syntax highlighting powered by the Torchlight.dev API makes it easier to dress up code blocks. Here's an overview of what I did to replace this blog's built-in Hugo highlighter (Chroma) with Torchlight." description: "Syntax highlighting powered by the Torchlight.dev API makes it easier to dress up code blocks. Here's an overview of what I did to replace this blog's built-in Hugo highlighter (Chroma) with Torchlight."
featured: false featured: false
toc: true toc: true
comments: true
categories: Backstage categories: Backstage
tags: tags:
- javascript - javascript

View file

@ -5,7 +5,6 @@ date: 2023-10-15
description: "Quick notes on using `systemctl edit` to override a systemd service to delay its startup." description: "Quick notes on using `systemctl edit` to override a systemd service to delay its startup."
featured: false featured: false
toc: false toc: false
comments: true
categories: Tips # Projects, Code categories: Tips # Projects, Code
tags: tags:
- crostini - crostini

View file

@ -5,7 +5,6 @@ lastmod: 2024-02-07
description: "Using Docker Compose to deploy containerized applications and make them available via Tailscale Serve and Tailscale Funnel" description: "Using Docker Compose to deploy containerized applications and make them available via Tailscale Serve and Tailscale Funnel"
featured: false featured: false
toc: true toc: true
comments: true
categories: Self-Hosting categories: Self-Hosting
tags: tags:
- containers - containers

View file

@ -5,7 +5,6 @@ date: 2023-12-20
description: "Exploring some of my favorite Tailscale addon features: SSH, Serve, and Funnel." description: "Exploring some of my favorite Tailscale addon features: SSH, Serve, and Funnel."
featured: false featured: false
toc: true toc: true
comments: true
categories: Tips # Projects, Code categories: Tips # Projects, Code
tags: tags:
- homelab - homelab

View file

@ -0,0 +1,321 @@
---
title: "The Slash Page Scoop"
date: 2024-06-02
# 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."
featured: false
toc: true
reply: true
categories: Backstage
tags:
- hugo
- meta
---
Inspired by [Robb Knight](https://rknight.me/)'s recent [slash pages](https://slashpages.net/) site, I spent some time over the past week or two drafting some slash pages of my own.
> Slash pages are common pages you can add to your website, usually with a standard, root-level slug like `/now`, `/about`, or `/uses`. They tend to describe the individual behind the site and are distinguishing characteristics of the IndieWeb.
On a blog that is otherwise organized in a fairly chronological manner, slash pages provide a way share information out-of-band. I think they're great for more static content (like an about page that says who I am) as well as for content that may be regularly updated (like a changelog).
The pages that I've implemented (so far) include:
- [/about](/about) tells a bit about me and my background
- [/changelog](/changelog) is just *starting* to record some of visual/functional changes I make here
- [/colophon](/colophon) describes the technology and services used in producing/hosting this site
- [/homelab](/homelab) isn't a canonical slash page but it provides a lot of details about my homelab setup
- [/save](/save) shamelessly hosts referral links for things I love and think you'll love too
- [/uses](/uses) shares the stuff I use on a regular basis
And, of course, these are collected in one place at [/slashes](/slashes).
Feel free to stop here if you just want to check out the slash pages, or keep on reading for some nerd stuff about how I implemented them on my Hugo site.
---
### Implementation
All of my typical blog posts get created within the site's Hugo directory under `content/posts/`, like this one at `content/posts/the-slash-page-scoop/index.md`. They get indexed, automatically added to the list of posts on the home page, and show up in the RSS feed. I don't want my slash pages to get that treatment so I made them directly inside the `content` directory:
```
content
├── categories
├── posts
├── search
├── 404.md
├── _index.md
├── about.md [tl! ~~]
├── changelog.md [tl! ~~]
├── colophon.md [tl! ~~]
├── homelab.md [tl! ~~]
├── save.md [tl! ~~]
├── simplex.md
└── uses.md [tl! ~~]
```
Easy enough, but I didn't then want to have to worry about manually updating a list of slash pages so I used [Hugo's Taxonomies](https://gohugo.io/content-management/taxonomies/) feature for that. I simply tagged each page with a new `slashes` category by adding it to the post's front matter:
```yaml
# torchlight! {"lineNumbers":true}
---
title: "/changelog"
date: "2024-05-26"
lastmod: "2024-05-30"
description: "Maybe I should keep a log of all my site-related tinkering?"
categories: slashes # [tl! ~~]
---
```
{{% notice note "Category Names" %}}
I really wanted to name the category `/slashes`, but that seems to trip up Hugo a bit when it comes to creating an archive of category posts. So I settled for `slashes` and came up with some workarounds to make it present the way I wanted.
{{% /notice %}}
Hugo will automatically generate an archive page for a given taxonomy term (so a post tagged with the category `slashes` would be listed at `$BASE_URL/category/slashes/`), but I like to have a bit of control over how those archive pages are actually presented. So I create a new file at `content/categories/slashes/_index.md` and drop in this front matter:
```yaml
# torchlight! {"lineNumbers":true}
---
title: /slashes
url: /slashes
aliases:
- /categories/slashes
description: >
My collection of slash pages.
---
```
The `slashes` in the file path tells Hugo which taxonomy it belongs to and so it can match the appropriately-categorized posts.
Just like with normal posts, the `title` field defines the title (duh) of the post; this way I can label the archive page as `/slashes` instead of just `slashes`.
The `url` field lets me override where the page will be served, and I added `/categories/slashes` as an alias so that anyone who hits that canonical URL will be automatically redirected.
Setting a `description` lets me choose what introductory text will be displayed at the top of the index page, as well as when it's shown at the next higher level archive (like `/categories/`).
Of course, I'd like to include a link to [slashpages.net](https://slashpages.net) to provide a bit more info about what these pages are, and I can't add hyperlinks to the description text. What I *can* do is edit the template which is used for rendering the archive page. In my case, that's at `layouts/partials/archive.html`, and it starts out like this:
```jinja-html
# torchlight! {"lineNumbers":true}
{{ $pages := .Pages }}
{{ if .IsHome }}
{{ $pages = where site.RegularPages "Type" "in" site.Params.mainSections }}
{{ end }}
<header class="content__header">
{{ if .IsHome }}
<h1>{{ site.Params.indexTitle | markdownify }}</h1>
{{ else }}
<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 }}
{{ end }}{{ end }}
{{ .Content }}
</header>
```
Line 9 is where I had already modified the template to conditionally add an RSS link for category archive pages. I'm going to tweak the setup a bit to conditionally render designated text when the page `.Title` matches `/slashes`:
```jinja-html
# torchlight! {"lineNumbers":true}
{{ $pages := .Pages }}
{{ if .IsHome }}
{{ $pages = where site.RegularPages "Type" "in" site.Params.mainSections }}
{{ end }}
<header class="content__header">
{{ if .IsHome }}
<h1>{{ site.Params.indexTitle | markdownify }}</h1>
{{ else }}
{{ if eq .Title "/slashes" }} <!-- [tl! **:3 ++:3 ] -->
<h1>{{ .Title | markdownify }}</h1>
<i>My collection of <a target="_blank" title="what's a slashpage?" href="https://slashpages.net">slash pages</a>.</i><hr>
{{ else }}
<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 }}
{{ end }} <!-- [tl! ** ++ ] -->
{{ end }}{{ end }}
{{ .Content }}
</header>
```
So instead of rendering the `description` I defined in the front matter the archive page will show:
> *My collection of [slash pages](https://slashpages.net).*
While I'm at it, I'd like for the slash pages themselves to be listed in alphabetical order rather than sorted by date (like everything else on the site). The remainder of my `layouts/partials/archive.html` already handles a few different ways of displaying lists of content:
```jinja-html
# torchlight! {"lineNumbers":true}
{{- if and (eq .Kind "taxonomy") (eq .Title "Tags") }} <!-- [tl! reindex(15)] -->
{{/* /tags/ */}}
<div class="tagsArchive">
{{- range $key, $value := .Site.Taxonomies }}
{{- $slicedTags := ($value.ByCount) }}
{{- range $slicedTags }}
{{- if eq $key "tags"}}
<div><a href='/{{ $key }}/{{ (replace .Name "#" "%23") | urlize }}/' title="{{ .Name }}">{{ .Name }}</a><sup>{{ .Count }}</sup></div>
{{- end }}
{{- end }}
{{- end }}
</div>
{{- else if eq .Kind "taxonomy" }}
{{/* /categories/ */}}
{{- $sorted := sort $pages "Title" }}
{{- range $sorted }}
{{- $postDate := .Date.Format "2006-01-02" }}
{{- $updateDate := .Lastmod.Format "2006-01-02" }}
<article class="post">
<header class="post__header">
<h1><a href="{{ .Permalink }}">{{ .Title | markdownify }}</a></h1>
<p class="post__meta">
<span class="date">["{{ with $updateDate }}{{ . }}{{ else }}{{ $postDate }}{{ end }}"]</span>
</p>
</header>
<section class="post__summary">
{{ .Description }}
</section>
<hr>
</article>
{{ end }}
{{- else }}
{{/* regular posts archive */}}
{{- range (.Paginate $pages).Pages }}
{{- $postDate := .Date.Format "2006-01-02" }}
{{- $updateDate := .Lastmod.Format "2006-01-02" }}
<article class="post">
<header class="post__header">
<h1><a href="{{ .Permalink }}">{{ .Title | markdownify }}</a></h1>
<p class="post__meta">
<span class="date">["{{- $postDate }}"{{- if ne $postDate $updateDate }}, "{{ $updateDate }}"{{ end }}]</span>
</p>
</header>
<section class="post__summary">
{{if .Description }}{{ .Description }}{{ else }}{{ .Summary }}{{ end }}
</section>
<hr>
</article>
{{- end }}
{{- template "_internal/pagination.html" . }}
{{- end }}
```
1. The [/tags/](/tags/) archive uses a condensed display format which simply shows the tag name and the number of posts with that tag.
2. Other taxonomy archives (like [/categories](/categories)) are sorted by title, displayed with a brief description, and the date that a post in the categories was published or updated.
3. Archives of posts are sorted by date (most recent first) and include the post description (or summary if it doesn't have one), and both the publish and updated dates.
I'll just tweak the second condition there to check for either a taxonomy archive or a page with the title `/slashes`:
```jinja-html
# torchlight! {"lineNumbers":true}
{{- if and (eq .Kind "taxonomy") (eq .Title "Tags") }} <!-- [tl! collapse:start reindex(20)] -->
{{/* /tags/ */}}
<div class="tagsArchive">
{{- range $key, $value := .Site.Taxonomies }}
{{- $slicedTags := ($value.ByCount) }}
{{- range $slicedTags }}
{{- if eq $key "tags"}}
<div><a href='/{{ $key }}/{{ (replace .Name "#" "%23") | urlize }}/' title="{{ .Name }}">{{ .Name }}</a><sup>{{ .Count }}</sup></div>
{{- end }}
{{- end }}
{{- end }} <!-- [tl! collapse:end] -->
</div>
{{- else if eq .Kind "taxonomy" }} <!-- [tl! **:3 --] -->
{{- else if or (eq .Kind "taxonomy") (eq .Title "/slashes") }} <!-- [tl! ++ reindex(-1)] -->
{{/* /categories/ */}} <!-- [tl! --] -->
{{/* /categories/ or /slashes/ */}} <!-- [tl! ++ reindex(-1)] -->
{{- $sorted := sort $pages "Title" }}
{{- range $sorted }}
{{- $postDate := .Date.Format "2006-01-02" }}
{{- $updateDate := .Lastmod.Format "2006-01-02" }}
<article class="post">
<header class="post__header">
<h1><a href="{{ .Permalink }}">{{ .Title | markdownify }}</a></h1>
<p class="post__meta">
<span class="date">["{{ with $updateDate }}{{ . }}{{ else }}{{ $postDate }}{{ end }}"]</span>
</p>
</header>
<section class="post__summary">
{{ .Description }}
</section>
<hr>
</article>
{{ end }}
{{- else }} <!-- [tl! collapse:start] -->
{{/* regular posts archive */}}
{{- range (.Paginate $pages).Pages }}
{{- $postDate := .Date.Format "2006-01-02" }}
{{- $updateDate := .Lastmod.Format "2006-01-02" }}
<article class="post">
<header class="post__header">
<h1><a href="{{ .Permalink }}">{{ .Title | markdownify }}</a></h1>
<p class="post__meta">
<span class="date">["{{- $postDate }}"{{- if ne $postDate $updateDate }}, "{{ $updateDate }}"{{ end }}]</span>
</p>
</header>
<section class="post__summary">
{{if .Description }}{{ .Description }}{{ else }}{{ .Summary }}{{ end }}
</section>
<hr>
</article>
{{- end }}
{{- template "_internal/pagination.html" . }}
{{- end }} <!-- [tl! collapse:end] -->
```
So that's got the [/slashes](/slashes/) page looking the way I want it to. The last tweak will be to the template I use for displaying related (ie, in the same category) posts in the sidebar. The magic for that happens in `layouts/partials/aside.html`:
```jinja-html
# torchlight! {"lineNumbers":true}
{{ if .Params.description }}<p>{{ .Params.description }}</p><hr>{{ end }} <!-- [tl! collapse:start] -->
{{ if and (gt .WordCount 400 ) (gt (len .TableOfContents) 180) }}
<p>
<h3>On this page</h3>
{{ .TableOfContents }}
<hr>
</p>
{{ end }} <!-- [tl! collapse:end] -->
{{ if isset .Params "categories" }} <!--[tl! **:start] -->
{{$related := where .Site.RegularPages ".Params.categories" "eq" .Params.categories }}
{{- $relatedLimit := default 8 .Site.Params.numberOfRelatedPosts }}
{{ if eq .Params.categories "slashes" }} <!-- [tl! ++:10] -->
<h3>More /slashes</h3>
{{ $sortedPosts := sort $related "Title" }}
<ul>
{{- range $sortedPosts }}
<li>
<a href="{{ .Permalink }}" title="{{ .Title }}">{{ .Title | markdownify }}</a>
</li>
{{ end }}
</ul>
{{ else }}
<h3>More {{ .Params.categories }}</h3>
<ul>
{{- range first $relatedLimit $related }}
<li>
<a href="{{ .Permalink }}" title="{{ .Title }}">{{ .Title | markdownify }}</a>
</li>
{{ end }}
{{ if gt (len $related) $relatedLimit }}
<li>
<a href="/categories/{{ lower .Params.categories }}/"><i>See all {{ .Params.categories }}</i></a>
</li>
{{ end }}
</ul>
{{ end }} <!-- [tl! ++ **:end] -->
<hr>
{{ end }}
{{- $posts := where .Site.RegularPages "Type" "in" .Site.Params.mainSections }} <!-- [tl! collase:12] -->
{{- $featured := default 8 .Site.Params.numberOfFeaturedPosts }}
{{- $featuredPosts := first $featured (where $posts "Params.featured" true)}}
{{- with $featuredPosts }}
<h3>Featured Posts</h3>
<ul>
{{- range . }}
<li>
<a href="{{ .Permalink }}" title="{{ .Title }}">{{ .Title | markdownify }}</a>
</li>
{{- end }}
</ul>
{{- end }}
```
So now if you visit any of my slash pages (like, say, [/colophon](/colophon/)) you'll see the alphabetized list of other slash pages in the side bar.
### Closing
I'll probably keep tweaking these slash pages in the coming days, but for now I'm really glad to finally have them posted. I've only thinking about doing this for the past six months.

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View file

@ -0,0 +1,368 @@
---
title: "Using a Custom Font with Hugo"
date: 2024-04-28
lastmod: "2024-05-01T13:29:30Z"
description: "Installing a custom font on a Hugo site, and taking steps to protect the paid font files from unauthorized distribution. Plus a brief exploration of a pair of storage CDNs, and using Tailscale in a GitHub Actions workflow."
featured: false
toc: true
categories: Backstage
tags:
- bunny
- cloudflare
- hugo
- meta
- 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://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).
### Web Font
This site's styling is based on the [risotto theme for Hugo](https://github.com/joeroe/risotto/tree/main). Risotto uses the CSS variable `--font-monospace` in `themes/risotto/static/css/typography.css` to define the font face, and then that variable is inserted wherever the font may need to be set:
```css
/* torchlight! {"lineNumbers":true} */
/* Fonts */
:root {
--font-monospace: "Fira Mono", monospace; /* [tl! **] */
}
body {
font-family: var(--font-monospace); /* [tl! **] */
font-size: 16px;
line-height: 1.5rem;
}
```
This makes it easy to override the theme's font by inserting my preferred font in `static/custom.css`:
```css
/* font overrides */
:root {
--font-monospace: 'Berkeley Mono', 'Fira Mono', monospace; /* [tl! **] */
}
```
And that would be the end of things if I could expect that everyone who visited my site already had the Berkeley Mono font installed; if they don't, though, the site will fallback to either the Fira Mono font or whatever generic monospace font is on the system. So maybe I'll add a few other monospace fonts just for good measure:
```css
/* font overrides */
:root {
--font-monospace: 'Berkeley Mono', 'IBM Plex Mono', 'Cascadia Mono', 'Roboto Mono', 'Source Code Pro', 'Fira Mono', 'Courier New', monospace; /* [tl! **] */
}
```
That provides a few more options to fall back to if the preferred font isn't available. But let's see about making that font available.
#### Hosted Locally
I can use a `@font-face` rule to tell the browser how to find the `.woff2` file for my preferred web font, and I could just set the `src: url` parameter to point to a local path in my Hugo environment:
```css
/* load preferred font */
@font-face {
font-family: 'Berkeley Mono';
font-style: normal;
font-weight: 400;
/* use the installed font with this name if it's there... */
src: local('Berkeley Mono'),
/* otherwise look at these paths */
url('/fonts/BerkeleyMono.woff2') format('woff2'),
}
```
{{% notice note "WOFF2 vs WOFF(1)" %}}
A previous version of this post also included the `.woff` file in addition to `.woff2`. A kind reader let me know that [basically everything](https://caniuse.com/?search=woff2) supports `.woff2`, and since `.woff2` offers much better compression than first-generation `.woff` there *really* isn't any reason to offer a font in `.woff` format in this modern age. I can just offer `.woff2` on its own.
I've updated this post, my CSS, and the contents of my CDN storage accordingly.
{{% /notice %}}
And that would work just fine... but it *would* require storing those web font files in the (public) [GitHub repo](https://github.com/jbowdre/runtimeterror) which powers my site, and I'd rather not store any paid font files there.
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" %}}
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 %}}
#### Cloudflare R2
Getting started with R2 was really easy; I just [created a new R2 bucket](https://developers.cloudflare.com/r2/buckets/create-buckets/) called `runtimeterror` and [connected it to the custom domain](https://developers.cloudflare.com/r2/buckets/public-buckets/#connect-a-bucket-to-a-custom-domain) `cdn.runtimeterror.dev`. I put the two web font files in a folder titled `fonts` and uploaded them to the bucket so that they can be accessed under `https://cdn.runtimeterror.dev/fonts/`.
I could then employ a [Cross-Origin Resource Sharing (CORS)](https://developers.cloudflare.com/r2/buckets/cors/) policy to ensure the fonts hosted on my fledgling CDN can only be loaded on my site. I configured the policy to also allow access from my `localhost` Hugo build environment as well as a preview Neocities environment I use for testing such major changes:
```json
[
{
"AllowedOrigins": [
"http://localhost:1313",
"https://secret--runtimeterror--preview.neocities.org",
"https://runtimeterror.dev"
],
"AllowedMethods": [
"GET"
]
}
]
```
Then I just needed to update the `@font-face` rule accordingly:
```css
/* load preferred font */
@font-face {
font-family: 'Berkeley Mono';
font-style: normal;
font-weight: 400;
font-display: fallback; /* [tl! ++] */
src: local('Berkeley Mono'),
url('/fonts/BerkeleyMono.woff2') format('woff2'), /* [tl! --] */
url('https://cdn.runtimeterror.dev/fonts/BerkeleyMono.woff2') format('woff2'), /* [tl! ++] */
}
```
I added in the `font-display: fallback;` descriptor to address the fact that the site will now be loading a remote font. Rather than blocking and not rendering text until the preferred font is loaded, it will show text in one of the available fallback fonts. If the preferred font loads quickly enough, it will be swapped in; otherwise, it will just show up on the next page load. I figured this was a good middle-ground between wanting the site to load quickly while also looking the way I want it to.
To test my work, I ran `hugo server` to build and serve the site locally on `http://localhost:1313`... and promptly encountered a cascade of CORS-related errors. I kept tweaking the policy and trying to learn more about what I'm doing (reminder: I'm bad at this), but just couldn't figure out what was preventing the font from being loaded.
I *eventually* discovered that sometimes you need to clear Cloudflare's cache so that new policy changes will take immediate effect. Once I [purged everything](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-everything/), the errors went away and the font loaded successfully.
### Bunny Storage
After migrating my domain to Bunny.net, the CDN font setup was pretty similar - but also different enough that it's worth mentioning. I started by creating a new Storage Zone named `runtimeterror-storage`, and selecting an appropriate-seeming set of replication regions. I then uploaded the same `fonts/` folder as before.
To be able to access the files in Bunny Storage, I connected a new Pull Zone (called `runtimeterror-pull`) and linked that Pull Zone with the `cdn.runtimeterror.dev` hostname. I also made sure to enable the option to automatically generate a certificate for this host.
Rather than needing me to understand CORS and craft a viable policy file, Bunny provides a clean UI with easy-to-understand options for configuring the pull zone security. I enabled the options to block root path access, block `POST` requests, block direct file access, and also added the same trusted referrers as before:
![Bunny CDN security configuration](bunny-cdn-security.png)
I made sure to use the same paths as I had on Cloudflare so I didn't need to update the Hugo config at all even after changing CDNs. That same CSS from before still works:
```css
/* load preferred font */
@font-face {
font-family: 'Berkeley Mono';
font-style: normal;
font-weight: 400;
font-display: fallback;
src: local('Berkeley Mono'),
url('https://cdn.runtimeterror.dev/fonts/BerkeleyMono.woff2') format('woff2'),
}
```
I again tested locally with `hugo server` and confirmed that the font loaded from Bunny CDN without any CORS or other errors.
So that's the web font for the web site sorted (twice); now let's tackle the font in the OpenGraph share images.
### Image Filter Text
My [setup for generating the share images](/dynamic-opengraph-images-with-hugo/) leverages the Hugo [images.Text](https://gohugo.io/functions/images/text/) function to overlay text onto a background image, and it needs a TrueType font in order to work. I was previously just storing the required font directly in my GitHub repo so that it would be available during the site build, but I definitely don't want to do that with a paid TrueType font file. So I needed to come up with some way to provide the TTF file to the GitHub runner without making it publicly available.
I recently figured out how I could [use a GitHub Action to easily connect the runner to my Tailscale environment](/gemini-capsule-gempost-github-actions/#publish-github-actions:~:text=name%3A%20Connect%20to%20Tailscale), and I figured I could re-use that idea here - only instead of pushing something to my tailnet, I'll be pulling something out.
#### Tailscale Setup
So I SSH'd to the cloud server I'm already using for hosting my Gemini capsule, created a folder to hold the font file (`/opt/fonts/`), and copied the TTF file into there. And then I used [Tailscale Serve](/tailscale-ssh-serve-funnel/#tailscale-serve) to publish that folder internally to my tailnet:
```shell
sudo tailscale serve --bg --set-path /fonts /opt/fonts/ # [tl! .cmd]
# [tl! .nocopy:4]
Available within your tailnet:
https://node.tailnet-name.ts.net/fonts/
|-- path /opt/fonts
```
The `--bg` flag will run the share in the background and automatically start it with the system (like a daemon-mode setup).
When I set up Tailscale for the Gemini capsule workflow, I configured the Tailscale ACL so that the GitHub runner (`tag:gh-bld`) could talk to my server (`tag:gh-srv`) over SSH:
```json
"acls": [
{
// github runner can talk to the deployment target
"action": "accept",
"users": ["tag:gh-bld"],
"ports": [
"tag:gh-srv:22"
],
}
],
```
I needed to update that ACL to allow communication over HTTPS as well:
```json
"acls": [
{
// github runner can talk to the deployment target
"action": "accept",
"users": ["tag:gh-bld"],
"ports": [
"tag:gh-srv:22",
"tag:gh-srv:443" // [tl! ++]
],
}
],
```
I then logged into the Tailscale admin panel to follow the same steps as last time to generate a unique [OAuth client](https://tailscale.com/kb/1215/oauth-clients) tied to the `tag:gh-bld` tag. I stored the ID, secret, and tags as repository secrets named `TS_API_CLIENT_ID`, `TS_API_CLIENT_SECRET`, and `TS_TAG`.
I also created a `REMOTE_FONT_PATH` secret which will be used to tell Hugo where to find the required TTF file (`https://node.tailnet-name.ts.net/fonts/BerkeleyMono.ttf`).
#### Hugo Setup
Here's the image-related code that I was previously using in `layouts/partials/opengraph` to create the OpenGraph images:
```jinja-html
{{ $img := resources.Get "og_base.png" }}
{{ $font := resources.Get "/FiraMono-Regular.ttf" }}
{{ $text := "" }}
{{- if .IsHome }}
{{ $text = .Site.Params.Description }}
{{- end }}
{{- if .IsPage }}
{{ $text = .Page.Title }}
{{ end }}
{{- with .Params.thumbnail }}
{{ $thumbnail := $.Resources.Get . }}
{{ with $thumbnail }}
{{ $img = $img.Filter (images.Overlay (.Process "fit 300x250") 875 38 )}}
{{ end }}
{{ end }}
{{ $img = $img.Filter (images.Text $text (dict
"color" "#d8d8d8"
"size" 64
"linespacing" 2
"x" 40
"y" 300
"font" $font
))}}
{{ $img = resources.Copy (path.Join $.Page.RelPermalink "og.png") $img }}
```
All I need to do is get it to pull the font resource from a web address rather than the local file system, and I'll do that by loading an environment variable instead of hardcoding the path here.
{{% notice note "Hugo Environent Variable Access" %}}
By default, Hugo's `os.Getenv` function only has access to environment variables which start with `HUGO_`. You can [adjust the security configuration](https://gohugo.io/functions/os/getenv/#security) to alter this restriction if needed, but I figured I could work just fine within the provided constraints.
{{% /notice %}}
```jinja-html
{{ $img := resources.Get "og_base.png" }}
{{ $font := resources.Get "/FiraMono-Regular.ttf" }} <!-- [tl! -- ] -->
{{ $text := "" }}
{{ $font := "" }} <!-- [tl! ++:10 **:10 ]>
{{ $path := os.Getenv "HUGO_REMOTE_FONT_PATH" }}
{{ with resources.GetRemote $path }}
{{ with .Err }}
{{ errorf "%s" . }}
{{ else }}
{{ $font = . }}
{{ end }}
{{ else }}
{{ errorf "Unable to get resource %q" $path }}
{{ end }}
{{- if .IsHome }}
{{ $text = .Site.Params.Description }}
{{- end }}
<!-- [tl! collapse:start ] -->
{{- if .IsPage }}
{{ $text = .Page.Title }}
{{ end }}
{{- with .Params.thumbnail }}
{{ $thumbnail := $.Resources.Get . }}
{{ with $thumbnail }}
{{ $img = $img.Filter (images.Overlay (.Process "fit 300x250") 875 38 )}}
{{ end }}
{{ end }}
{{ $img = $img.Filter (images.Text $text (dict
"color" "#d8d8d8"
"size" 64
"linespacing" 2
"x" 40
"y" 300
"font" $font
))}}
{{ $img = resources.Copy (path.Join $.Page.RelPermalink "og.png") $img }} <!-- [tl! collapse:end ] -->
```
I can test that this works by running a build locally from a system with access to my tailnet. I'm not going to start a web server with this build; I'll just review the contents of the `public/` folder once it's complete to see if the OpenGraph images got rendered correctly.
```shell
HUGO_REMOTE_FONT_PATH=https://node.tailnet-name.ts.net/fonts/BerkeleyMono.ttf hugo
```
Neat, it worked!
![OpenGraph share image for this post](og-sample.png)
#### GitHub Action
All that's left is to update the GitHub Actions workflow I use for [building and deploying my site to Neocities](/deploy-hugo-neocities-github-actions/) to automate things:
```yaml
# torchlight! {"lineNumbers": true}
# .github/workflows/deploy-to-neocities.yml
name: Deploy to Neocities
# [tl! collapse:start]
on:
schedule:
- cron: 0 13 * * *
push:
branches:
- main
concurrency:
group: deploy-to-neocities
cancel-in-progress: true
defaults:
run:
shell: bash
# [tl! collapse:end]
jobs:
deploy:
name: Build and deploy Hugo site
runs-on: ubuntu-latest
steps:
- name: Hugo setup
uses: peaceiris/actions-hugo@v2.6.0
with:
hugo-version: '0.121.1'
extended: true
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Connect to Tailscale # [tl! ++:10 **:10]
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.TS_API_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_API_CLIENT_SECRET }}
tags: ${{ secrets.TS_TAG }}
- name: Build with Hugo
run: hugo --minify # [tl! -- **]
run: HUGO_REMOTE_FONT_PATH=${{ secrets.REMOTE_FONT_PATH }} hugo --minify # [tl! ++ ** reindex(-1) ]
- name: Insert 404 page
run: |
cp public/404/index.html public/not_found.html
- name: Highlight with Torchlight
run: |
npm i @torchlight-api/torchlight-cli
npx torchlight
- name: Deploy to Neocities
uses: bcomnes/deploy-to-neocities@v1
with:
api_token: ${{ secrets.NEOCITIES_API_TOKEN }}
cleanup: true
dist_dir: public
```
This uses the [Tailscale GitHub Action](https://github.com/tailscale/github-action) to connect the runner to my tailnet using the credentials I created earlier, and passes the `REMOTE_FONT_PATH` secret as an environment variable to the Hugo command line. Hugo will then be able to retrieve and use the TTF font during the build process.
### Conclusion
Configuring and using a custom font in my Hugo-generated site wasn't hard to do, but I had to figure some things out on my own to get started in the right direction. I learned a lot about how fonts are managed in CSS along the way, and I love the way the new font looks on this site!
This little project also gave me an excuse to play with first Cloudflare R2 and then Bunny Storage, and I came away seriously impressed by Bunny (and have since moved more of my domains to bunny.net). Expect me to write more about cool Bunny stuff in the future.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -6,7 +6,6 @@ timeless: true
draft: false draft: false
description: "This blog has migrated from virtuallypotato.com to runtimeterror.dev." description: "This blog has migrated from virtuallypotato.com to runtimeterror.dev."
toc: false toc: false
comments: true
categories: Backstage categories: Backstage
tags: tags:
- meta - meta

22
content/save.md Normal file
View file

@ -0,0 +1,22 @@
---
title: "/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,5 +1,5 @@
+++ +++
comments = false reply = false
toc = false toc = false
usePageBundles = false usePageBundles = false
showDate = false showDate = false

3
content/tags/_index.md Normal file
View file

@ -0,0 +1,3 @@
---
title: Tags
---

View file

@ -1,19 +1,77 @@
--- ---
title: "Stuff I Use" title: "/uses"
date: "2024-01-19T04:15:31Z" date: "2024-05-29"
# lastmod: {{ .Date | time.Format "2006-01-02" }} lastmod: "2024-06-02"
description: "The hardware, software, and services which keep me going." description: "The hardware, software, services, and gear which I use (almost) daily."
toc: true toc: true
draft: true
comments: true
timeless: true timeless: true
categories: slashes
--- ---
Here's the stuff I use and how I use it. *Here's some of the stuff I use and how I use it.*
### Hardware ### Hardware
- **[Framework Laptop Chromebook Edition](https://frame.work/products/laptop-chromebook-12-gen-intel)** (i5-1240P | 32GB RAM | 1TB NVMe). This is my primary personal computing device. Yep, it's an overpowered Chromebook, and I make full use of the [Linux Development Environment](https://www.chromium.org/chromium-os/developer-library/guides/containers/containers-and-vms/) to Do Things. I love it. *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 ### 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 ### 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

@ -17,6 +17,9 @@
hugo hugo
nodePackages.npm nodePackages.npm
]; ];
shellHook = ''
source .env
'';
}; };
}; };
} }

View file

@ -10,7 +10,8 @@
{{- if ge $limit 1 -}} {{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}} {{- $pages = $pages | first $limit -}}
{{- end -}} {{- end -}}
<!-- {{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }} --> {{- printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
{{ printf "<?xml-stylesheet type=\"text/xsl\" href=\"/xml/feed.xsl\" media=\"all\"?>" | safeHTML }}
<rss version="2.0" <rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom" xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:content="http://purl.org/rss/1.0/modules/content/"

View file

@ -18,7 +18,7 @@
{{- $content := $content | replaceRE "(?m:^`([^`]*)`$)" "```\n$1\n```\n" -}}{{/* convert single-line inline code to blocks */}} {{- $content := $content | replaceRE "(?m:^`([^`]*)`$)" "```\n$1\n```\n" -}}{{/* convert single-line inline code to blocks */}}
{{- $content := $content | replaceRE `\{\{%\snotice.*%\}\}` "<-- note -->" -}}{{/* convert hugo notices */}} {{- $content := $content | replaceRE `\{\{%\snotice.*%\}\}` "<-- note -->" -}}{{/* convert hugo notices */}}
{{- $content := $content | replaceRE `\{\{%\s/notice.*%\}\}` "<-- /note -->" -}} {{- $content := $content | replaceRE `\{\{%\s/notice.*%\}\}` "<-- /note -->" -}}
{{- $content := $content | replaceRE `((\/\/)|#)\s*torchlight!.*\n` "" -}}{{/* remove torchlight markup */}} {{- $content := $content | replaceRE `(?:(?:<!--)|(?:#)|(?:\/\/))\s*torchlight!.*\n` "" -}}{{/* remove torchlight markup */}}
{{- $content := $content | replaceRE `(?:(?:<!--)|(?:#)|(?:\/\/))*\s*\[tl!.*\].*` "" -}} {{- $content := $content | replaceRE `(?:(?:<!--)|(?:#)|(?:\/\/))*\s*\[tl!.*\].*` "" -}}
{{- $content := $content | replaceRE `(?m:^\[!\[(.*)\]\(.*\)\]\((.*)\)$)` "=> $2 $1" -}}{{/* remove images from uptime links */}} {{- $content := $content | replaceRE `(?m:^\[!\[(.*)\]\(.*\)\]\((.*)\)$)` "=> $2 $1" -}}{{/* remove images from uptime links */}}
{{- $content := $content | replaceRE `(?m:^\s*(?:(?:\*|\-)\s+)?\[(.*)\]\((.*)\)$)` "=> $2 $1" -}}{{/* convert links already on own line */}} {{- $content := $content | replaceRE `(?m:^\s*(?:(?:\*|\-)\s+)?\[(.*)\]\((.*)\)$)` "=> $2 $1" -}}{{/* convert links already on own line */}}
@ -45,7 +45,12 @@
--- ---
{{ $subject := printf "Re: %s" .Title -}} {{ $subject := printf "Re: %s" .Title -}}
=> mailto:blog@runtimeterror.dev?subject={{ urlquery $subject | replaceRE `\+` "%20" }} 📧 Reply via email {{ $subject := urlquery $subject | replaceRE `\+` "%20" }}
{{ $path := .Page.RelPermalink | path.Dir -}}
{{ $path := strings.Trim $path "/" -}}
{{ $address := printf "blogreply.%s@%s" $path "runtimeterror.dev" -}}
=> 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

@ -33,25 +33,23 @@
<div class="content__body"> <div class="content__body">
{{ .Content }} {{ .Content }}
</div> </div>
{{- $reply := true }}
{{- $showComments := true }} {{- if eq .Site.Params.reply false }}
{{- if eq .Site.Params.comments false }} {{- $reply = false }}
{{- $showComments = false }} {{- else if eq .Params.reply false }}
{{- else if eq .Params.comments false }} {{- $reply = false }}
{{- $showComments = false }}
{{- end }} {{- end }}
{{- if ne $showComments false }} {{- if or (eq $reply true) (eq .Site.Params.analytics "true") }}
<hr> <hr>
{{- $showKudos := true }} {{- if eq .Site.Params.analytics true }}
{{- if eq .Site.Params.kudos false }} <span class="post_kudos"><button class="tinylytics_kudos"></button></span>
{{- $showKudos = false }}
{{- else if eq .Params.kudos false }}
{{- $showKudos = false }}
{{- end }} {{- end }}
{{- if and (eq .Site.Params.analytics true) (ne $showKudos false) }} {{- if (eq $reply true) }}
<span class="post_kudos">Celebrate this post:&nbsp;<button class="tinylytics_kudos"></button></span> {{- $path := .Page.RelPermalink | path.Dir }}
{{- $path := strings.Trim $path "/" }}
{{- $address := printf "blogreply.%s@%s" $path "runtimeterror.dev" }}
<span class="post_email_reply"><a href="mailto:{{ $address }}?Subject=Re: {{ .Title }}">📧 Reply by email</a></span>
{{- end }} {{- end }}
{{- partial "comments" . }}
{{- end }} {{- end }}
<footer class="content__footer"></footer> <footer class="content__footer"></footer>
{{ end }} {{ end }}

View file

@ -10,7 +10,7 @@
<ul class="aside__social-links"> <ul class="aside__social-links">
{{ range $item := .Site.Params.socialLinks }} {{ range $item := .Site.Params.socialLinks }}
<li> <li>
<a target="_blank" href="{{ $item.url }}" 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

@ -6,55 +6,62 @@
{{ 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 target="_blank" href="feed.xml" aria-label="Category RSS"><i class="fa-solid fa-square-rss"></i></a>&nbsp;</h1> {{ if eq .Title "/slashes" }}
{{ with .Description }}<i>{{ . }}</i><hr>{{ else }}<br>{{ end }} <h1>{{ .Title | markdownify }}</h1>
<i>My collection of <a target="_blank" title="what's a slashpage?" href="https://slashpages.net">slash pages</a>.</i><hr>
{{ else }}
<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 }}
{{ end }}
<hr>
{{ end }}{{ end }} {{ end }}{{ end }}
{{ .Content }} {{ .Content }}
</header> </header>
{{- if and (eq .Kind "taxonomy") (eq .Title "Tags") }}
{{- if eq .Kind "taxonomy" }} {{/* /tags/ */}}
{{- if eq .Title "Tags" }} <div class="tagsArchive">
<div class="tagsArchive"> {{- range $key, $value := .Site.Taxonomies }}
{{- range $key, $value := .Site.Taxonomies }} {{- $slicedTags := ($value.ByCount) }}
{{- $slicedTags := ($value.ByCount) }} {{- range $slicedTags }}
{{- range $slicedTags }} {{- if eq $key "tags"}}
{{- if eq $key "tags"}} <div><a href='/{{ $key }}/{{ (replace .Name "#" "%23") | urlize }}/' title="{{ .Name }}">{{ .Name }}</a><sup>{{ .Count }}</sup></div>
<div><a href='/{{ $key }}/{{ (replace .Name "#" "%23") | urlize }}/' title="{{ .Name }}">{{ .Name }}</a><sup>{{ .Count }}</sup></div>
{{- end }}
{{- end }} {{- end }}
{{- end }} {{- end }}
</div>
{{- else }}
{{- range .Pages.ByDate.Reverse }}
{{- $postDate := .Date.Format "2006-01-02" }}
{{- $updateDate := .Lastmod.Format "2006-01-02" }}
<article class="post">
<header class="post__header">
<h1><a href="{{ .Permalink }}">{{ .Title | markdownify }}</a></h1>
<p class="post__meta">
<span class="date">["{{- $postDate }}"{{- if ne $postDate $updateDate }}, "{{ $updateDate }}"{{ end }}]</span>
</p>
</header>
<section class="post__summary">
{{ .Description }}
</section>
<hr>
</article>
{{ end }}
{{- end }} {{- end }}
</div>
{{- else if or (eq .Kind "taxonomy") (eq .Title "/slashes") }}
{{/* /categories/ or /slashes/ */}}
{{- $sorted := sort $pages "Title" }}
{{- range $sorted }}
{{- $postDate := .Date.Format "2006-01-02" }}
{{- $updateDate := .Lastmod.Format "2006-01-02" }}
<article class="post">
<header class="post__header">
<h1><a href="{{ .Permalink }}">{{ .Title | markdownify }}</a></h1>
<p class="post__meta">
<span class="date">["{{ with $updateDate }}{{ . }}{{ else }}{{ $postDate }}{{ end }}"]</span>
</p>
</header>
<section class="post__summary">
{{ .Description }}
</section>
<hr>
</article>
{{ end }}
{{- else }} {{- else }}
{{/* regular posts archive */}}
{{- range (.Paginate $pages).Pages }} {{- range (.Paginate $pages).Pages }}
{{- $postDate := .Date.Format "2006-01-02" }} {{- $postDate := .Date.Format "2006-01-02" }}
{{- $updateDate := .Lastmod.Format "2006-01-02" }} {{- $updateDate := .Lastmod.Format "2006-01-02" }}
<article class="post"> <article class="post">
<header class="post__header"> <header class="post__header">
<h1><a href="{{ .Permalink }}">{{ .Title | markdownify }}</a></h1> <h1><a href="{{ .Permalink }}">{{ .Title | markdownify }}</a></h1>
<p class="post__meta"> <p class="post__meta">
<span class="date">["{{- $postDate }}"{{- if ne $postDate $updateDate }}, "{{ $updateDate }}"{{ end }}]</span> <span class="date">["{{- $postDate }}"{{- if ne $postDate $updateDate }}, "{{ $updateDate }}"{{ end }}]</span>
</p> </p>
</header> </header>
<section class="post__summary"> <section class="post__summary">
{{ .Summary }} {{if .Description }}{{ .Description }}{{ else }}{{ .Summary }}{{ end }}
</section> </section>
<hr> <hr>
</article> </article>

View file

@ -10,6 +10,17 @@
{{ if isset .Params "categories" }} {{ if isset .Params "categories" }}
{{$related := where .Site.RegularPages ".Params.categories" "eq" .Params.categories }} {{$related := where .Site.RegularPages ".Params.categories" "eq" .Params.categories }}
{{- $relatedLimit := default 8 .Site.Params.numberOfRelatedPosts }} {{- $relatedLimit := default 8 .Site.Params.numberOfRelatedPosts }}
{{ if eq .Params.categories "slashes" }}
<h3>More /slashes</h3>
{{ $sortedPosts := sort $related "Title" }}
<ul>
{{- range $sortedPosts }}
<li>
<a href="{{ .Permalink }}" title="{{ .Title }}">{{ .Title | markdownify }}</a>
</li>
{{ end }}
</ul>
{{ else }}
<h3>More {{ .Params.categories }}</h3> <h3>More {{ .Params.categories }}</h3>
<ul> <ul>
{{- range first $relatedLimit $related }} {{- range first $relatedLimit $related }}
@ -18,11 +29,12 @@
</li> </li>
{{ end }} {{ end }}
{{ if gt (len $related) $relatedLimit }} {{ if gt (len $related) $relatedLimit }}
<li> <li>
<a href="/categories/{{ lower .Params.categories }}/"><i>See all {{ .Params.categories }}</i></a> <a href="/categories/{{ lower .Params.categories }}/"><i>See all {{ .Params.categories }}</i></a>
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
{{ end }}
<hr> <hr>
{{ end }} {{ end }}

View file

@ -1,21 +0,0 @@
{{ if isset site.Params "giscusrepo" }}
<br>
<div class="post_comments">
<script src="https://giscus.runtimeterror.dev/client.js"
data-repo="{{ .Site.Params.giscusRepo }}"
data-repo-id="{{ .Site.Params.giscusRepoId }}"
data-category="{{ .Site.Params.giscusCategory }}"
data-category-id="{{ .Site.Params.giscusCategoryId }}"
data-mapping="{{ .Site.Params.giscusMapping }}"
data-strict="{{ .Site.Params.giscusStrict }}"
data-reactions-enabled="{{ .Site.Params.giscusReactions }}"
data-emit-metadata="{{ .Site.Params.giscusEmitMetadata }}"
data-input-position="{{ .Site.Params.giscusInputPosition }}"
data-theme="{{ .Site.Params.giscusTheme }}"
data-lang="{{ .Site.Params.giscusLang }}"
data-loading="{{ .Site.Params.giscusLoading }}"
crossorigin="{{ .Site.Params.giscusCrossOrigin }}"
async>
</script>
</div>
{{ end }}

View file

@ -1,6 +1,5 @@
{{- partial "lang.html" . -}}
<p class="copyright">{{ .Site.Copyright | markdownify }}</p> <p class="copyright">{{ .Site.Copyright | markdownify }}</p>
<p class="powered_by">{"powered_by": [{{- range $i, $link := .Site.Params.powerLinks }}{{ if $i }}, {{ end }}&quot;<a target="_blank" href="{{ $link.url }}">{{ $link.title }}</a>&quot;{{ end }}]} <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>&lt;<a target="_blank" href="https://github.com/jbowdre/runtimeterror">view source</a>&gt;</p> <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 -->

View file

@ -0,0 +1,10 @@
<nav class="page__nav main-nav">
<ul>
<h1 class="page__logo"><a href="{{ .Site.BaseURL }}" class="page__logo-inner">{{ .Site.Title }}</a></h1>
{{ $currentPage := . }}
{{ 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 }}" target="{{ .Params.target }}">{{ .Name }}</a></li>
{{ end }}
</ul>
</nav>

View file

@ -12,7 +12,6 @@
font-display: fallback; font-display: fallback;
src: local('Berkeley Mono'), src: local('Berkeley Mono'),
url('https://cdn.runtimeterror.dev/fonts/BerkeleyMono-Regular.woff2') format('woff2'), url('https://cdn.runtimeterror.dev/fonts/BerkeleyMono-Regular.woff2') format('woff2'),
url('https://cdn.runtimeterror.dev/fonts/BerkeleyMono-Regular.woff') format('woff')
} }
/* override page max-width */ /* override page max-width */
@ -36,18 +35,18 @@
line-height: 1.3rem; line-height: 1.3rem;
} }
.powered_by { .footer_links {
font-size: 12px; font-size: 12px;
line-height: 1.1rem; line-height: 1.1rem;
color: var(--muted); color: var(--muted);
} }
.powered_by a:link, .powered_by a:visited { .footer_links a:link, .footer_links a:visited {
color: var(--off-fg); color: var(--off-fg);
text-decoration: none; text-decoration: none;
} }
.powered_by a:hover { .footer_links a:hover {
color: var(--hover); color: var(--hover);
text-decoration: underline; text-decoration: underline;
} }
@ -391,4 +390,15 @@ a.tinylytics_webring {
/* hr override */ /* hr override */
hr { hr {
margin-top: 1.5rem; margin-top: 1.5rem;
} }
/* no extra space for paragraphs with lists */
p:not(:has(+ ol)),
p:not(:has(+ ul)) {
margin-bottom: 1.5em;
}
p:has(+ ol),
p:has(+ ul) {
margin-bottom: 0;
}

39
static/xml/feed.xsl Normal file
View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- adapted from https://github.com/getnikola/nikola/blob/master/nikola/data/themes/base/assets/xml/rss.xsl -->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="1.0">
<xsl:output method="xml"/>
<xsl:template match="/">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width"/>
<title><xsl:value-of select="rss/channel/title"/> (RSS)</title>
<style><![CDATA[html{margin:0;padding:0;}body{color:font-size:1.1rem;line-height:1.4;margin:5%;max-width:35rem;padding:0;}input{min-width:35rem;margin-left:.2rem;padding-left:.2rem;padding-right:.2rem;}h2{font-size:22px;font-weight:inherit;}h3:before{content:"" !important;}]]></style>
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
<link rel="manifest" href="/icons/site.webmanifest" />
<link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#5bbad5" />
<link rel="shortcut icon" href="/icons/favicon.ico" />
<link rel="stylesheet" href="/css/palettes/runtimeterror.css" />
<link rel="stylesheet" href="/css/risotto.css" />
<link rel="stylesheet" href="/css/custom.css" />
</head>
<body>
<h1><xsl:value-of select="rss/channel/title"/> (RSS)</h1>
<p>This is an <abbr title="Really Simple Syndication">RSS</abbr> feed. To subscribe to it, copy its address and paste it when your feed reader asks for it. It will be updated periodically in your reader.</p>
<p>New to feeds? <a href="https://www.mojeek.com/search?q=how+to+get+started+with+rss+feeds" title="Search on the web to learn more">Learn more</a>.</p>
<p>
<label for="address">RSS address:</label>
<input><xsl:attribute name="type">url</xsl:attribute><xsl:attribute name="id">address</xsl:attribute><xsl:attribute name="spellcheck">false</xsl:attribute><xsl:attribute name="value"><xsl:value-of select="rss/channel/atom:link[@rel='self']/@href"/></xsl:attribute></input>
</p>
<p><h2>Recent posts:</h2></p>
<ul>
<xsl:for-each select="rss/channel/item">
<li><h3><a><xsl:attribute name="href"><xsl:value-of select="link"/></xsl:attribute><xsl:value-of select="title"/></a></h3></li>
</xsl:for-each>
</ul>
</body>
</html>
</xsl:template>
</xsl:stylesheet>