Merge branch 'main' into preview
3
.gitmodules
vendored
|
@ -2,3 +2,6 @@
|
|||
path = themes/risotto
|
||||
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
|
||||
|
|
|
@ -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)
|
|
@ -6,7 +6,7 @@ draft: true
|
|||
description: "This is a new post about..."
|
||||
featured: false
|
||||
toc: true
|
||||
comments: true
|
||||
reply: true
|
||||
categories: Tips # Backstage, ChromeOS, Code, Self-Hosting, VMware
|
||||
tags:
|
||||
- android
|
||||
|
|
|
@ -65,7 +65,7 @@ enableRobotsTXT = true
|
|||
disableInlineCSS = true
|
||||
|
||||
[services.rss]
|
||||
limit = 20
|
||||
limit = 10
|
||||
|
||||
[services.twitter]
|
||||
disableInlineCSS = true
|
||||
|
|
|
@ -9,21 +9,45 @@
|
|||
name = "self-hosting"
|
||||
url = "/categories/self-hosting/"
|
||||
weight = 1
|
||||
[[main.params]]
|
||||
target = "_self"
|
||||
|
||||
[[main]]
|
||||
identifier = "tips"
|
||||
name = "tips"
|
||||
url = "/categories/tips/"
|
||||
weight = 1
|
||||
[[main.params]]
|
||||
target = "_self"
|
||||
|
||||
[[main]]
|
||||
identifier = "code"
|
||||
name = "code"
|
||||
url = "/categories/code/"
|
||||
weight = 1
|
||||
[[main.params]]
|
||||
target = "_self"
|
||||
|
||||
[[main]]
|
||||
identifier = "virtuallypotato"
|
||||
name = "whereis virtuallypotato"
|
||||
url = "/virtuallypotato-runtimeterror/"
|
||||
identifier = "backstage"
|
||||
name = "backstage"
|
||||
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
|
||||
[[main.params]]
|
||||
target = "_blank"
|
||||
|
|
|
@ -37,23 +37,8 @@ robots = [
|
|||
]
|
||||
|
||||
# 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
|
||||
kudos = true
|
||||
reply = true
|
||||
|
||||
[author]
|
||||
name = "John Bowdre"
|
||||
|
@ -181,8 +166,8 @@ url = "https://scribbles.jbowdre.lol"
|
|||
|
||||
[[socialLinks]]
|
||||
icon = "fa-solid fa-satellite"
|
||||
title = "Gemlog"
|
||||
url = "https://capsule.jbowdre.lol/gemlog/"
|
||||
title = "Gemini Capsule"
|
||||
url = "gemini://capsule.jbowdre.lol"
|
||||
|
||||
[[socialLinks]]
|
||||
icon = "fa-solid fa-circle-user"
|
||||
|
@ -199,25 +184,35 @@ icon = "fa-solid fa-envelope"
|
|||
title = "Email"
|
||||
url = "mailto:jbowdre@omg.lol"
|
||||
|
||||
[[powerLinks]]
|
||||
title = "hugo"
|
||||
url = "https://gohugo.io"
|
||||
[[slashPages]]
|
||||
title = "/about"
|
||||
url = "/about"
|
||||
label = "about this site"
|
||||
|
||||
[[powerLinks]]
|
||||
title = "neocities"
|
||||
url = "https://neocities.org/about"
|
||||
[[slashPages]]
|
||||
title = "/changelog"
|
||||
url = "/changelog"
|
||||
label = "recent changes to the site"
|
||||
|
||||
[[powerLinks]]
|
||||
title = "risotto"
|
||||
url = "https://github.com/joeroe/risotto"
|
||||
[[slashPages]]
|
||||
title = "/colophon"
|
||||
url = "/colophon"
|
||||
label = "how this site works"
|
||||
|
||||
[[powerLinks]]
|
||||
title = "torchlight"
|
||||
url = "https://torchlight.dev"
|
||||
[[slashPages]]
|
||||
title = "/homelab"
|
||||
url = "/homelab"
|
||||
label = "my homelab setup"
|
||||
|
||||
[[powerLinks]]
|
||||
title = "tinylytics"
|
||||
url = "https://tinylytics.app/home"
|
||||
[[slashPages]]
|
||||
title = "/save"
|
||||
url = "/save"
|
||||
label = "referral links"
|
||||
|
||||
[[slashPages]]
|
||||
title = "/uses"
|
||||
url = "/uses"
|
||||
label = "stuff i use"
|
||||
|
||||
[[verifyLinks]]
|
||||
title = "omg.lol"
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
comments = false
|
||||
analytics = false
|
|
@ -1,2 +1,2 @@
|
|||
comments = false
|
||||
reply = false
|
||||
analytics = false
|
|
@ -3,12 +3,11 @@ title = "404'd!"
|
|||
noindex = true
|
||||
timeless = true
|
||||
comments = true
|
||||
kudos = false
|
||||
+++
|
||||
|
||||
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](/)?
|
||||
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
+++
|
||||
title = "Hi, I'm John."
|
||||
description = "A brief introduction to me, this blog, and what you're likely to see here."
|
||||
timeless = true
|
||||
comments = false
|
||||
aliases = ["tldr", "bio"]
|
||||
+++
|
||||
---
|
||||
title: "/about"
|
||||
date: "2024-05-26T21:19:08Z"
|
||||
lastmod: "2024-05-29"
|
||||
description: "A brief introduction to me, this blog, and what you're likely to see here."
|
||||
timeless: true
|
||||
toc: false
|
||||
categories: slashes
|
||||
---
|
||||
**Hi, I'm John.**
|
||||
|
||||
![Me, +/- a few decades](/images/john.jpg)
|
||||
|
||||
You've (somehow) managed to stumble upon my dark corner of the internet[^1].
|
||||
|
|
8
content/categories/slashes/_index.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
title: /slashes
|
||||
url: /slashes
|
||||
aliases:
|
||||
- categories/slashes
|
||||
description: >
|
||||
My collection of slash pages.
|
||||
---
|
31
content/changelog.md
Normal 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
|
@ -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
|
@ -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
|
|
@ -6,7 +6,6 @@ description: "Using the power of Home Assistant automations and Ntfy push notifi
|
|||
featured: true
|
||||
alias: automating-security-camera-notifications-with-home-assistant-and-ntfy
|
||||
toc: true
|
||||
comments: true
|
||||
thumbnail: thumbnail.png
|
||||
categories: Self-Hosting
|
||||
tags:
|
||||
|
|
|
@ -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."
|
||||
featured: false
|
||||
toc: true
|
||||
comments: true
|
||||
categories: Backstage
|
||||
tags:
|
||||
- cloud
|
||||
|
|
|
@ -5,7 +5,6 @@ date: 2024-01-21
|
|||
description: "Using GitHub Actions to automatically deploy a Hugo website to Neocities."
|
||||
featured: false
|
||||
toc: true
|
||||
comments: true
|
||||
categories: Backstage
|
||||
tags:
|
||||
- cicd
|
||||
|
|
|
@ -6,7 +6,6 @@ description: "Using a GitHub Actions workflow to retrieve data from an authentic
|
|||
featured: false
|
||||
thumbnail: "finished-product.png"
|
||||
toc: true
|
||||
comments: true
|
||||
categories: Backstage
|
||||
tags:
|
||||
- api
|
||||
|
|
|
@ -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."
|
||||
featured: false
|
||||
toc: true
|
||||
comments: true
|
||||
categories: Tips # Projects, Code
|
||||
tags:
|
||||
- homelab
|
||||
|
|
|
@ -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."
|
||||
featured: false
|
||||
toc: true
|
||||
comments: true
|
||||
thumbnail: hugo-logo-wide.png
|
||||
categories: Backstage
|
||||
tags:
|
||||
|
|
|
@ -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."
|
||||
featured: false
|
||||
toc: true
|
||||
comments: true
|
||||
categories: Self-Hosting
|
||||
tags:
|
||||
- android
|
||||
|
|
|
@ -4,7 +4,6 @@ date: 2024-01-19
|
|||
# lastmod: 2024-01-19
|
||||
description: "Never in my life have I seen enabling FIPS *fix* a problem - until now."
|
||||
featured: false
|
||||
comments: true
|
||||
categories: VMware
|
||||
tags:
|
||||
- vmware
|
||||
|
|
|
@ -5,7 +5,6 @@ lastmod: "2024-04-05T21:07:38Z"
|
|||
description: "Deploying a Gemini capsule, powered by Agate, gempost, kineto, Tailscale, and GitHub Actions"
|
||||
featured: false
|
||||
toc: true
|
||||
comments: true
|
||||
categories: Self-Hosting
|
||||
tags:
|
||||
- caddy
|
||||
|
|
BIN
content/posts/prettify-hugo-rss-feed-xslt/getting-there-feed.png
Normal file
After Width: | Height: | Size: 86 KiB |
230
content/posts/prettify-hugo-rss-feed-xslt/index.md
Normal 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 feed’s 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)!
|
BIN
content/posts/prettify-hugo-rss-feed-xslt/pretty-feed.png
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
content/posts/prettify-hugo-rss-feed-xslt/ugly-rss.png
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
content/posts/prettify-hugo-rss-feed-xslt/valid-rss-rogers.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
content/posts/prettify-hugo-rss-feed-xslt/very-white-feed.png
Normal file
After Width: | Height: | Size: 109 KiB |
|
@ -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."
|
||||
featured: false
|
||||
toc: true
|
||||
comments: true
|
||||
categories: Self-Hosting
|
||||
tags:
|
||||
- cloudflare
|
||||
|
|
|
@ -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"
|
||||
featured: false
|
||||
toc: true
|
||||
comments: true
|
||||
categories: Code
|
||||
tags:
|
||||
- homelab
|
||||
|
|
|
@ -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."
|
||||
featured: false
|
||||
toc: true
|
||||
comments: true
|
||||
categories: Backstage
|
||||
tags:
|
||||
- javascript
|
||||
|
|
|
@ -5,7 +5,6 @@ date: 2023-10-15
|
|||
description: "Quick notes on using `systemctl edit` to override a systemd service to delay its startup."
|
||||
featured: false
|
||||
toc: false
|
||||
comments: true
|
||||
categories: Tips # Projects, Code
|
||||
tags:
|
||||
- crostini
|
||||
|
|
|
@ -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"
|
||||
featured: false
|
||||
toc: true
|
||||
comments: true
|
||||
categories: Self-Hosting
|
||||
tags:
|
||||
- containers
|
||||
|
|
|
@ -5,7 +5,6 @@ date: 2023-12-20
|
|||
description: "Exploring some of my favorite Tailscale addon features: SSH, Serve, and Funnel."
|
||||
featured: false
|
||||
toc: true
|
||||
comments: true
|
||||
categories: Tips # Projects, Code
|
||||
tags:
|
||||
- homelab
|
||||
|
|
321
content/posts/the-slash-page-scoop/index.md
Normal 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" }} <a target="_blank" href="{{ .Permalink }}feed.xml" aria-label="Category RSS"><i class="fa-solid fa-square-rss"></i></a> </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" }} <a target="_blank" href="{{ .Permalink }}feed.xml" aria-label="Category RSS"><i class="fa-solid fa-square-rss"></i></a> </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.
|
BIN
content/posts/using-custom-font-hugo/bunny-cdn-security.png
Normal file
After Width: | Height: | Size: 264 KiB |
368
content/posts/using-custom-font-hugo/index.md
Normal 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.
|
BIN
content/posts/using-custom-font-hugo/og-sample.png
Normal file
After Width: | Height: | Size: 27 KiB |
|
@ -6,7 +6,6 @@ timeless: true
|
|||
draft: false
|
||||
description: "This blog has migrated from virtuallypotato.com to runtimeterror.dev."
|
||||
toc: false
|
||||
comments: true
|
||||
categories: Backstage
|
||||
tags:
|
||||
- meta
|
||||
|
|
22
content/save.md
Normal 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
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
+++
|
||||
comments = false
|
||||
reply = false
|
||||
toc = false
|
||||
usePageBundles = false
|
||||
showDate = false
|
||||
|
|
3
content/tags/_index.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
title: Tags
|
||||
---
|
|
@ -1,19 +1,77 @@
|
|||
---
|
||||
title: "Stuff I Use"
|
||||
date: "2024-01-19T04:15:31Z"
|
||||
# lastmod: {{ .Date | time.Format "2006-01-02" }}
|
||||
description: "The hardware, software, and services which keep me going."
|
||||
title: "/uses"
|
||||
date: "2024-05-29"
|
||||
lastmod: "2024-06-02"
|
||||
description: "The hardware, software, services, and gear which I use (almost) daily."
|
||||
toc: true
|
||||
draft: true
|
||||
comments: 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
|
||||
- **[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
|
||||
*Computer and web apps.*
|
||||
- **[Calibre](https://calibre-ebook.com/)** for collecting, converting, and managing my eBooks.
|
||||
- **[Fish shell](https://fishshell.com/)**, a really smart, modern, heavily configurable shell.
|
||||
- **[Home Assistant](https://www.home-assistant.io/)** for controlling my "smart" home.
|
||||
- **[Home Manager](https://github.com/nix-community/home-manager)** for managing packages and configurations across multiple systems ([dotfiles](https://github.com/jbowdre/dotfiles)).
|
||||
- **[Immich](https://immich.app/)**, a self-hosted photo and video management solution.
|
||||
- **[Linkding](https://github.com/sissbruecker/linkding)** as a self-hosted bookmark manager.
|
||||
- **[Miniflux](https://miniflux.app/)**, a self-hosted minimalist feed reader.
|
||||
- **[Obsidian](https://obsidian.md/)** for collecting/organizing notes. You can see some of them [here](https://notes.runtimeterror.dev/).
|
||||
- **[Phanpy](https://phanpy.social/#/)**, a minimal and opinionated Mastodon web client.
|
||||
- **[Tabby](https://tabby.sh/)**, a beautiful cross-platform terminal app.
|
||||
- **[tmux](https://github.com/tmux/tmux)** because *I heard you like terminals so I put a terminal in your terminal so you can terminal while you terminal*.
|
||||
- **[Vim](https://www.vim.org/)** for coding and development without a GUI.
|
||||
- **[VSCode](https://code.visualstudio.com/)** for most coding and development.
|
||||
|
||||
### Android Apps
|
||||
*Skipping the obvious ones for services mentioned elsewhere on this page...*
|
||||
- **[Cheogram](https://play.google.com/store/apps/details?id=com.cheogram.android.playstore)** ([F-Droid](https://f-droid.org/packages/com.cheogram.android/)) XMPP client, with great integration to [jmp.chat](https://jmp.chat/).
|
||||
- **[Element](https://play.google.com/store/apps/details?id=im.vector.app)** ([F-Droid](https://f-droid.org/en/packages/im.vector.app/)) Matrix chat client.
|
||||
- **[Firefox Focus](https://play.google.com/store/apps/details?id=org.mozilla.focus)** Fast and private web browser for throw-away browsing sessions.
|
||||
- **[Firefox](https://play.google.com/store/apps/details?id=org.mozilla.firefox)** for general web browsing.
|
||||
- **[JBV1](https://play.google.com/store/apps/details?id=com.johnboysoftware.jbv1)** gives super powers to my Valentine One radar detector.
|
||||
- **[Lagrange](https://skyjake.github.io/fdroid/repo/)** browser for [Gemini](https://geminiprotocol.net/).
|
||||
- **[RaceBox](https://play.google.com/store/apps/details?id=pro.RaceBox.androidapp)** / **[RaceChrono](https://play.google.com/store/apps/details?id=com.racechrono.app)** for recording GPS/acceleration data during my [autocross runs](https://www.youtube.com/playlist?list=PLwzr4uKY-x-EwCv-rWNGefdikuW6Oy9O_).
|
||||
- **[RadarScope](https://play.google.com/store/apps/details?id=com.basevelocity.radarscope)** weather radar and information.
|
||||
- **[SimpleX Chat](https://play.google.com/store/apps/details?id=chat.simplex.app)** ([F-Droid](https://f-droid.org/en/packages/chat.simplex.app/)) for end-to-end encrypted chats without any user identifiers.
|
||||
- **[Squoosh](https://squoosh.app/)** for compressing and EXIF-stripping photos before sharing.
|
||||
- **[Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm)** for automated profiles on my phone.
|
||||
- **[WiFiman](https://play.google.com/store/apps/details?id=com.ubnt.usurvey)** for scanning a testing wireless networks.
|
||||
- **[Yubico Authenticator](https://play.google.com/store/apps/details?id=com.yubico.yubioath)** for storing TOTP secrets on a hardware token.
|
||||
|
||||
### Services
|
||||
*These may include affiliate links.*
|
||||
- **[Cloaked](https://join.cloaked.app/?utm_source=referral&utm_campaign=Ee83SGN8OR)** for generating unique identies (email addresses + phone numbers) for every web sign-up.
|
||||
- **[Fastmail](https://app.fastmail.com/signup/?STKI=/u29803368)** for fast, private email service with a ton of nice bonus features.
|
||||
- **[Forward Email](https://forwardemail.net/)** for routing email to/from my various project domains.
|
||||
- **[JMP.chat](https://jmp.chat/)** for a phone number backed by XMPP.
|
||||
- **[NextDNS](https://nextdns.io/?from=2jujzdcc)** for privacy-protecting ad-blocking DNS filtering in the cloud.
|
||||
- **[Obico](https://www.obico.io/)** for controlling and monitoring 3D prints.
|
||||
- **[omg.lol](https://home.omg.lol/referred-by/jbowdre)** for some really handy web tools and one of the best communities of interesting people.
|
||||
- **[Privacy.com](https://app.privacy.com/join/JMMQ7)** for creating virtual merchant-locked credit cards to keep me safe when shopping online.
|
||||
- **[Tailscale](https://tailscale.com)** for connecting all my various systems and making them think that they're on the same LAN.
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
hugo
|
||||
nodePackages.npm
|
||||
];
|
||||
shellHook = ''
|
||||
source .env
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
|
@ -10,7 +10,8 @@
|
|||
{{- if ge $limit 1 -}}
|
||||
{{- $pages = $pages | first $limit -}}
|
||||
{{- 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"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
{{- $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 `\{\{%\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 `(?m:^\[!\[(.*)\]\(.*\)\]\((.*)\)$)` "=> $2 $1" -}}{{/* remove images from uptime links */}}
|
||||
{{- $content := $content | replaceRE `(?m:^\s*(?:(?:\*|\-)\s+)?\[(.*)\]\((.*)\)$)` "=> $2 $1" -}}{{/* convert links already on own line */}}
|
||||
|
@ -45,7 +45,12 @@
|
|||
|
||||
---
|
||||
{{ $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) }}
|
||||
{{ if $related }}
|
||||
## Related articles
|
||||
|
|
|
@ -33,25 +33,23 @@
|
|||
<div class="content__body">
|
||||
{{ .Content }}
|
||||
</div>
|
||||
|
||||
{{- $showComments := true }}
|
||||
{{- if eq .Site.Params.comments false }}
|
||||
{{- $showComments = false }}
|
||||
{{- else if eq .Params.comments false }}
|
||||
{{- $showComments = false }}
|
||||
{{- $reply := true }}
|
||||
{{- if eq .Site.Params.reply false }}
|
||||
{{- $reply = false }}
|
||||
{{- else if eq .Params.reply false }}
|
||||
{{- $reply = false }}
|
||||
{{- end }}
|
||||
{{- if ne $showComments false }}
|
||||
{{- if or (eq $reply true) (eq .Site.Params.analytics "true") }}
|
||||
<hr>
|
||||
{{- $showKudos := true }}
|
||||
{{- if eq .Site.Params.kudos false }}
|
||||
{{- $showKudos = false }}
|
||||
{{- else if eq .Params.kudos false }}
|
||||
{{- $showKudos = false }}
|
||||
{{- if eq .Site.Params.analytics true }}
|
||||
<span class="post_kudos"><button class="tinylytics_kudos"></button></span>
|
||||
{{- end }}
|
||||
{{- if and (eq .Site.Params.analytics true) (ne $showKudos false) }}
|
||||
<span class="post_kudos">Celebrate this post: <button class="tinylytics_kudos"></button></span>
|
||||
{{- if (eq $reply true) }}
|
||||
{{- $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 }}
|
||||
{{- partial "comments" . }}
|
||||
{{- end }}
|
||||
<footer class="content__footer"></footer>
|
||||
{{ end }}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<ul class="aside__social-links">
|
||||
{{ range $item := .Site.Params.socialLinks }}
|
||||
<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>
|
||||
<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>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
|
|
@ -6,14 +6,19 @@
|
|||
{{ if .IsHome }}
|
||||
<h1>{{ site.Params.indexTitle | markdownify }}</h1>
|
||||
{{ else }}
|
||||
<h1>{{ .Title | markdownify }}{{ if eq .Kind "term" }} <a target="_blank" href="feed.xml" aria-label="Category RSS"><i class="fa-solid fa-square-rss"></i></a> </h1>
|
||||
{{ with .Description }}<i>{{ . }}</i><hr>{{ else }}<br>{{ end }}
|
||||
{{ if eq .Title "/slashes" }}
|
||||
<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" }} <a target="_blank" href="{{ .Permalink }}feed.xml" aria-label="{{ .Title }} RSS" title="{{ .Title }} RSS"><i class="fa-solid fa-square-rss"></i></a> </h1>
|
||||
{{ with .Description }}<i>{{ . }}</i>{{ end }}
|
||||
{{ end }}
|
||||
<hr>
|
||||
{{ end }}{{ end }}
|
||||
{{ .Content }}
|
||||
</header>
|
||||
|
||||
{{- if eq .Kind "taxonomy" }}
|
||||
{{- if eq .Title "Tags" }}
|
||||
{{- if and (eq .Kind "taxonomy") (eq .Title "Tags") }}
|
||||
{{/* /tags/ */}}
|
||||
<div class="tagsArchive">
|
||||
{{- range $key, $value := .Site.Taxonomies }}
|
||||
{{- $slicedTags := ($value.ByCount) }}
|
||||
|
@ -24,15 +29,17 @@
|
|||
{{- end }}
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- else }}
|
||||
{{- range .Pages.ByDate.Reverse }}
|
||||
{{- 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">["{{- $postDate }}"{{- if ne $postDate $updateDate }}, "{{ $updateDate }}"{{ end }}]</span>
|
||||
<span class="date">["{{ with $updateDate }}{{ . }}{{ else }}{{ $postDate }}{{ end }}"]</span>
|
||||
</p>
|
||||
</header>
|
||||
<section class="post__summary">
|
||||
|
@ -41,8 +48,8 @@
|
|||
<hr>
|
||||
</article>
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
{{/* regular posts archive */}}
|
||||
{{- range (.Paginate $pages).Pages }}
|
||||
{{- $postDate := .Date.Format "2006-01-02" }}
|
||||
{{- $updateDate := .Lastmod.Format "2006-01-02" }}
|
||||
|
@ -54,7 +61,7 @@
|
|||
</p>
|
||||
</header>
|
||||
<section class="post__summary">
|
||||
{{ .Summary }}
|
||||
{{if .Description }}{{ .Description }}{{ else }}{{ .Summary }}{{ end }}
|
||||
</section>
|
||||
<hr>
|
||||
</article>
|
||||
|
|
|
@ -10,6 +10,17 @@
|
|||
{{ if isset .Params "categories" }}
|
||||
{{$related := where .Site.RegularPages ".Params.categories" "eq" .Params.categories }}
|
||||
{{- $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>
|
||||
<ul>
|
||||
{{- range first $relatedLimit $related }}
|
||||
|
@ -21,8 +32,9 @@
|
|||
<li>
|
||||
<a href="/categories/{{ lower .Params.categories }}/"><i>See all {{ .Params.categories }}</i></a>
|
||||
</li>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
<hr>
|
||||
{{ end }}
|
||||
|
||||
|
|
|
@ -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 }}
|
|
@ -1,6 +1,5 @@
|
|||
{{- partial "lang.html" . -}}
|
||||
<p class="copyright">{{ .Site.Copyright | markdownify }}</p>
|
||||
<p class="powered_by">{"powered_by": [{{- range $i, $link := .Site.Params.powerLinks }}{{ if $i }}, {{ end }}"<a target="_blank" href="{{ $link.url }}">{{ $link.title }}</a>"{{ end }}]}
|
||||
<p class="footer_links">{"<a href="/slashes/" title="slashpages">/slashes</a>": [{{- range $i, $link := .Site.Params.slashPages }}{{ if $i }}, {{ end }}"<a href="{{ $link.url }}" title="{{ $link.label }}">{{ $link.title }}</a>"{{ end }}]}
|
||||
<br><<a target="_blank" href="https://github.com/jbowdre/runtimeterror">view source</a>></p>
|
||||
|
||||
<!-- Back to Top button via https://github.com/vfeskov/vanilla-back-to-top -->
|
||||
|
|
10
layouts/partials/header.html
Normal 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>
|
||||
|
|
@ -12,7 +12,6 @@
|
|||
font-display: fallback;
|
||||
src: local('Berkeley Mono'),
|
||||
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 */
|
||||
|
@ -36,18 +35,18 @@
|
|||
line-height: 1.3rem;
|
||||
}
|
||||
|
||||
.powered_by {
|
||||
.footer_links {
|
||||
font-size: 12px;
|
||||
line-height: 1.1rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.powered_by a:link, .powered_by a:visited {
|
||||
.footer_links a:link, .footer_links a:visited {
|
||||
color: var(--off-fg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.powered_by a:hover {
|
||||
.footer_links a:hover {
|
||||
color: var(--hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
@ -392,3 +391,14 @@ a.tinylytics_webring {
|
|||
hr {
|
||||
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
|
@ -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>
|