44 Commits
Author SHA1 Message Date
jfmonty2 8799733474 change light mode to line up better with dark mode 2026-06-22 12:11:04 -04:00
jfmonty2 6385ef9a86 get rid of BookPreview for now 2026-06-21 07:26:05 -04:00
jfmonty2 9161a8c739 start working on LLMs post 2026-06-21 07:15:18 -04:00
jfmonty2 108f812914 add supporting components for sidenotes post 2026-04-07 06:22:43 -04:00
jfmonty2 1091285d01 convert remaining posts to mdx 2026-04-07 06:12:56 -04:00
jfmonty2 591a3fa17d add remaining posts (still .svx for now) 2026-04-06 21:18:11 -04:00
jfmonty2 6ba91ab8fe fix heading color on posts list 2026-04-06 12:08:08 -04:00
jfmonty2 67779ecd40 fix draft notice in dark mode 2026-04-05 06:41:37 -04:00
jfmonty2 eda7d3aa00 extract post-listing logic 2026-04-05 06:40:18 -04:00
jfmonty2 e0af57e5b1 add draft notice to post list and fix draft-hiding logic 2026-04-05 06:32:27 -04:00
jfmonty2 675ef1003d add custom headings with deep link 2026-04-04 14:58:59 -04:00
jfmonty2 68bb25357b finish footer links 2026-03-31 08:30:12 -04:00
jfmonty2 3f24f42c1f add about page 2026-03-30 20:44:04 -04:00
jfmonty2 bfdaac8095 minor cleanups 2026-03-30 20:36:23 -04:00
jfmonty2 e02af4c552 get rid of animations on theme switcher 2026-03-30 20:35:46 -04:00
jfmonty2 fd8ed38572 all-posts page, page titles, index page 2026-03-30 05:27:41 -04:00
jfmonty2 5ffb51d5ce add remark plugin for summarization 2026-03-28 06:50:16 -04:00
jfmonty2 485e3a0a91 style links a little more 2026-03-16 21:25:29 -04:00
jfmonty2 d2770b9a5a pick a code theme and setup styles for inline code 2026-03-16 21:16:17 -04:00
jfmonty2 3aa7ad1368 add another couple posts and start working on code styling 2026-03-14 10:07:25 -04:00
jfmonty2 4da303fceb finish sidenotes 2026-03-13 15:24:41 -04:00
jfmonty2 2ac002d798 keep working on sidenotes 2026-03-11 05:19:18 -04:00
jfmonty2 6e6351f5cf keep working on sidenotes, copy latest real post over to test 2026-03-09 21:50:51 -04:00
jfmonty2 e2049c2e29 tweak header bar styling 2026-03-09 07:58:28 -04:00
jfmonty2 173b5ba9f4 theme switcher 2026-03-09 07:48:39 -04:00
jfmonty2 0070ed1c19 settle on a dark theme and implement with override 2026-03-08 10:55:41 -04:00
jfmonty2 0f5dadbf6f add obsidian/mocha/deep-ocean themes (Gemini) 2026-03-07 13:40:33 -05:00
jfmonty2 8fc267e6df add mise config which was missing somehow 2026-03-07 13:38:43 -05:00
jfmonty2 827a4406bd remove extraneous themes 2026-03-07 13:25:03 -05:00
jfmonty2 657ad09a20 all the themes together + switcher 2026-03-07 13:18:56 -05:00
jfmonty2 6bf81e0b20 claude dark v9 "northern lights" 2026-03-07 11:09:28 -05:00
jfmonty2 6b8c47cbb4 claude dark v8 "copper & slate" 2026-03-07 11:08:08 -05:00
jfmonty2 1142003e40 claude dark v7 "stormfront" 2026-03-07 11:06:36 -05:00
jfmonty2 365da1f285 claude dark v6 "midnight garden" 2026-03-07 09:27:30 -05:00
jfmonty2 e01cf188e2 claude dark v5, salmon accent 2026-03-07 09:21:34 -05:00
jfmonty2 6747faeb7a claude dark v4, blue/gold 2026-03-07 09:18:19 -05:00
jfmonty2 7acf1f2c9f start work on sidenotes 2026-03-05 21:39:22 -05:00
jfmonty2 13d0ac8de7 make dropcap switchable between ascender/descender 2026-03-05 08:45:49 -05:00
jfmonty2 b291e93e75 move to Baskervville for headings, with proper smallcaps variant 2026-03-05 08:15:24 -05:00
jfmonty2 b0e6576b33 add SmallCaps component and use for title 2026-03-03 20:31:46 -05:00
jfmonty2 6b0a985ee1 finish TOC component 2026-03-01 18:34:48 -05:00
jfmonty2 dfdf6c6e66 continue working on post layout, add typography styles 2026-02-28 15:21:36 -05:00
jfmonty2 c28f340333 start working on posts with placeholder content 2026-02-28 09:26:10 -05:00
jfmonty2 95b58b5615 start work on Astro port 2026-02-27 09:10:25 -05:00
107 changed files with 2553 additions and 5315 deletions
+2 -11
View File
@@ -1,12 +1,3 @@
.DS_Store .astro/
dist/
node_modules node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
**/_test.*
/scratch
-2
View File
@@ -1,2 +0,0 @@
engine-strict=true
resolution-mode=highest
-38
View File
@@ -1,38 +0,0 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
+21
View File
@@ -0,0 +1,21 @@
import { defineConfig } from "astro/config";
import mdx from '@astrojs/mdx';
import vue from '@astrojs/vue';
import { remarkDescription } from './src/plugins.ts';
export default defineConfig({
integrations: [
mdx(),
vue(),
],
prefetch: true,
markdown: {
remarkPlugins: [remarkDescription],
shikiConfig: {
themes: {
light: 'dracula',
dark: 'dark-plus',
}
},
},
});
+1116 -137
View File
File diff suppressed because it is too large Load Diff
-3210
View File
File diff suppressed because it is too large Load Diff
+22 -23
View File
@@ -1,25 +1,24 @@
{ {
"name": "blog.jfmonty2.com", "dependencies": {
"version": "0.0.1", "@astrojs/check": "^0.9.6",
"private": true, "@astrojs/mdx": "^4.3.13",
"scripts": { "@astrojs/rss": "^4.0.15",
"dev": "vite dev", "@astrojs/sitemap": "^3.7.0",
"build": "vite build", "@astrojs/vue": "^5.1.4",
"preview": "vite preview" "@fontsource-variable/baskervville": "^5.2.3",
}, "@fontsource-variable/baskervville-sc": "^5.2.3",
"devDependencies": { "@fontsource-variable/figtree": "^5.2.10",
"@sveltejs/adapter-auto": "^2.0.0", "@fontsource-variable/fira-code": "^5.2.7",
"@sveltejs/adapter-static": "^2.0.3", "astro": "^5.18.0",
"@sveltejs/kit": "^1.20.4", "sharp": "^0.34.5"
"hast-util-to-html": "^9.0.0", },
"hast-util-to-text": "^4.0.0", "name": "astro",
"mdast-util-to-string": "^4.0.0", "version": "0.0.1",
"mdsvex": "^0.11.0", "scripts": {
"sass": "^1.69.5", "dev": "astro dev",
"svelte": "^4.0.5", "build": "astro build",
"unist-util-find": "^3.0.0", "preview": "astro preview",
"unist-util-visit": "^5.0.0", "astro": "astro"
"vite": "^4.4.2" },
}, "type": "module"
"type": "module"
} }
@@ -3,7 +3,7 @@ title: 'Advent of Languages 2024, Day 1: C'
date: 2024-12-02 date: 2024-12-02
--- ---
<script>import Sidenote from '$lib/Sidenote.svelte';</script> import Sidenote from '@components/Sidenote.astro';
As time goes on, it's becoming increasingly clear to me that I'm a bit of a programming-language dilletante. I'm always finding weird niche languages like [Pony](https://www.ponylang.io/) or [Roc](https://www.roc-lang.org), going "wow that looks cool," spending a bunch of time reading the documentation, and then never actually using it and forgetting all about it for the next three years. As time goes on, it's becoming increasingly clear to me that I'm a bit of a programming-language dilletante. I'm always finding weird niche languages like [Pony](https://www.ponylang.io/) or [Roc](https://www.roc-lang.org), going "wow that looks cool," spending a bunch of time reading the documentation, and then never actually using it and forgetting all about it for the next three years.
@@ -3,7 +3,7 @@ title: 'Advent of Languages 2024, Day 2: C++'
date: 2024-12-03 date: 2024-12-03
--- ---
<script>import Sidenote from '$lib/Sidenote.svelte';</script> import Sidenote from '@components/Sidenote.astro';
Well, [Day 1](/advent-of-languages-2024-01) went swimmingly, more or less, so let's push on to Day 2: C++! C++, of course, is famous for being what happens when you take C and answer "Yes" to every question that starts "Can I have" and ends with a language feature. Yes, you can have classes and inheritance. Yes, even multiple inheritance. Yes, you can have constructors and destructors. Yes, you can have iterators (sorta).<Sidenote>More on that later.</Sidenote> Yes, you can have metaprogramming. Yes, you can have move semantics. Yes, you can also have raw pointers, why not? Well, [Day 1](/advent-of-languages-2024-01) went swimmingly, more or less, so let's push on to Day 2: C++! C++, of course, is famous for being what happens when you take C and answer "Yes" to every question that starts "Can I have" and ends with a language feature. Yes, you can have classes and inheritance. Yes, even multiple inheritance. Yes, you can have constructors and destructors. Yes, you can have iterators (sorta).<Sidenote>More on that later.</Sidenote> Yes, you can have metaprogramming. Yes, you can have move semantics. Yes, you can also have raw pointers, why not?
@@ -3,7 +3,7 @@ title: 'Advent of Languages 2024, Day 3: Forth'
date: 2024-12-07 date: 2024-12-07
--- ---
<script>import Sidenote from '$lib/Sidenote.svelte';</script> import Sidenote from '@components/Sidenote.astro';
My original plan was to stick with the "systems language" theme for Day 3 and go with Zig, but the more I thought about it the more I started to think, you know, Zig is nice and clean and modern. It hasn't had time to get all warty and ugly with bolted-on afterthoughts and contentious features that divide the community into warring tribes, and it has things like common datastructures in its standard library. I should probably save it for one of the later days, when I anticipate spending more time fighting _the problem_ and less time fighting _the language_. Also I looked at Day 3 and it (the first part at least) looked very simple, which makes me even less inclined to use a big honkin' heavy-duty language like Zig. Instead, today I'm going to take a look at Forth!<Sidenote>I know, I know, I would have been able to make all kinds of terrible jokes had I just waited for the _forth_ day of the AoC, but hey, we can't all get what we want.</Sidenote> My original plan was to stick with the "systems language" theme for Day 3 and go with Zig, but the more I thought about it the more I started to think, you know, Zig is nice and clean and modern. It hasn't had time to get all warty and ugly with bolted-on afterthoughts and contentious features that divide the community into warring tribes, and it has things like common datastructures in its standard library. I should probably save it for one of the later days, when I anticipate spending more time fighting _the problem_ and less time fighting _the language_. Also I looked at Day 3 and it (the first part at least) looked very simple, which makes me even less inclined to use a big honkin' heavy-duty language like Zig. Instead, today I'm going to take a look at Forth!<Sidenote>I know, I know, I would have been able to make all kinds of terrible jokes had I just waited for the _forth_ day of the AoC, but hey, we can't all get what we want.</Sidenote>
@@ -3,7 +3,7 @@ title: 'Advent of Languages 2024, Day 4: Fortran'
date: 2024-12-10 date: 2024-12-10
--- ---
<script>import Sidenote from '$lib/Sidenote.svelte';</script> import Sidenote from '@components/Sidenote.astro'
Oh, you thought we were done going back in time? Well I've got news for you, Doc Brown, you'd better not mothball the ol' time machine just yet, because we're going back even further. That's right, for Day 4 I've decided to use Fortran!<Sidenote>Apparently it's officially called `Fortran` now and not `FORTRAN` like it was in days of yore, and has been ever since the 1990s. That's right, when most languages I've used were just getting their start, Fortran was going through its mid-life identity crisis.</Sidenote><Sidenote>When I told my wife that I was going to be using a language that came out in the 1950s, she wanted to know if the next one would be expressed in Egyptian hieroglyphs.</Sidenote> Oh, you thought we were done going back in time? Well I've got news for you, Doc Brown, you'd better not mothball the ol' time machine just yet, because we're going back even further. That's right, for Day 4 I've decided to use Fortran!<Sidenote>Apparently it's officially called `Fortran` now and not `FORTRAN` like it was in days of yore, and has been ever since the 1990s. That's right, when most languages I've used were just getting their start, Fortran was going through its mid-life identity crisis.</Sidenote><Sidenote>When I told my wife that I was going to be using a language that came out in the 1950s, she wanted to know if the next one would be expressed in Egyptian hieroglyphs.</Sidenote>
@@ -49,7 +49,7 @@ My word-search grid appears to be 140 characters by 140, so I'm just going to ha
Not gonna lie here, this part took me _way_ longer than I expected it to. See, the standard way to read a file in Fortran is with the `read()` statement. (It looks like a function call, but it's not.) You use it something like this: Not gonna lie here, this part took me _way_ longer than I expected it to. See, the standard way to read a file in Fortran is with the `read()` statement. (It looks like a function call, but it's not.) You use it something like this:
```fortran ```f90
read(file_handle, *) somevar, anothervar, anothervar2 read(file_handle, *) somevar, anothervar, anothervar2
``` ```
@@ -57,7 +57,7 @@ Or at least, that's one way of using it. But here's the problem: by default, For
Initially, I thought I might be able to do this: Initially, I thought I might be able to do this:
```fortran ```f90
character, dimension(140, 140) :: grid character, dimension(140, 140) :: grid
! ...later ! ...later
@@ -70,7 +70,7 @@ But sadly, this kept spitting out errors about how it had encountered the end of
My next try looked something like this: My next try looked something like this:
```fortran ```f90
do row = 1, 100 do row = 1, 100
read(file_handle, *) grid(row, :) read(file_handle, *) grid(row, :)
end do end do
@@ -82,7 +82,7 @@ I'm pretty sure the proper way to do this would be to figure out how to set the
So instead, I decided to just make it dumber. So instead, I decided to just make it dumber.
```fortran ```f90
program advent04 program advent04
implicit none implicit none
@@ -139,7 +139,7 @@ S A M X M A S
S . . S . . S S . . S . . S
``` ```
Which has all 8 possible orientationS of the word `XMAS` starting from the central X. Then, we can just take a sliding "window" of the same size into our puzzle grid and compare it to the test grid. This is a native operation in Fortran--comparing two arrays of the same size results in a third array whose elements are the result of each individual comparison from the original arrays. Then we can just call `count()` on the resulting array to get the number of true values, and we know how many characters matched up. Subtract 1 for the central X we already knew about, then divide by 3 since there are 3 letters remaining in each occurrence of `XMAS`, and Bob's your uncle, right? Which has all 8 possible orientations of the word `XMAS` starting from the central X. Then, we can just take a sliding "window" of the same size into our puzzle grid and compare it to the test grid. This is a native operation in Fortran--comparing two arrays of the same size results in a third array whose elements are the result of each individual comparison from the original arrays. Then we can just call `count()` on the resulting array to get the number of true values, and we know how many characters matched up. Subtract 1 for the central X we already knew about, then divide by 3 since there are 3 letters remaining in each occurrence of `XMAS`, and Bob's your uncle, right?
...Wait, no. That won't work because it doesn't account for partial matches. Say we had a "window" that looked like this (I'm only showing the bottom-right quadrant of the window for simplicity): ...Wait, no. That won't work because it doesn't account for partial matches. Say we had a "window" that looked like this (I'm only showing the bottom-right quadrant of the window for simplicity):
@@ -164,7 +164,7 @@ Will this work? Is it even marginally more efficient than the stupidly obvious w
Ok, first things first. Let's adjust the data-loading code to pad the grid with 3 bogus values on each edge, so that we can still generate our window correctly when we're looking at a point near the edge of the grid. Ok, first things first. Let's adjust the data-loading code to pad the grid with 3 bogus values on each edge, so that we can still generate our window correctly when we're looking at a point near the edge of the grid.
```fortran ```f90
grid = '.' ! probably wouldn't matter if we skipped this, uninitialized memory just makes me nervous grid = '.' ! probably wouldn't matter if we skipped this, uninitialized memory just makes me nervous
open(newunit=handle, file="data/04.txt", status="old", action="read") open(newunit=handle, file="data/04.txt", status="old", action="read")
@@ -196,7 +196,7 @@ Oh. Oh, _noooo_.
It's okay, I mean, uh, it's not _that_ much higher. Only two orders of magnitude, and what are the odds of all eight versions of `XMAS` appearing in the same window, anyway? Something like 1/4<sup>25</sup>? Maybe we can still make this work. It's okay, I mean, uh, it's not _that_ much higher. Only two orders of magnitude, and what are the odds of all eight versions of `XMAS` appearing in the same window, anyway? Something like 1/4<sup>25</sup>? Maybe we can still make this work.
```fortran ```f90
integer function count_xmas(row, col) result(count) integer function count_xmas(row, col) result(count)
implicit none implicit none
@@ -277,7 +277,7 @@ Those `&`s are line-continuation characters, by the way. Apparently you can't ha
Now we just have to put it all together: Now we just have to put it all together:
```fortran ```f90
total = 0 total = 0
do col = 4, 143 do col = 4, 143
do row = 4, 143 do row = 4, 143
@@ -314,7 +314,7 @@ Hmm, I wonder if there's a way to take a single starting test grid and manipulat
Turns out, yes! Yes there is. We can use a combination of slicing with a negative step, and transposing, which switches rows with columns, effectively rotating and flipping the array. So setting up our test grids looks like this: Turns out, yes! Yes there is. We can use a combination of slicing with a negative step, and transposing, which switches rows with columns, effectively rotating and flipping the array. So setting up our test grids looks like this:
```fortran ```f90
character, dimension(3, 3) :: window, t1, t2, t3, t4 character, dimension(3, 3) :: window, t1, t2, t3, t4
t1 = reshape( & t1 = reshape( &
@@ -332,7 +332,7 @@ t4 = t3(:, 3:1:-1) ! flip t3 left-to-right
Then we can just compare the window to each test grid: Then we can just compare the window to each test grid:
```fortran ```f90
window = grid(row - 1:row + 1, col - 1:col + 1) window = grid(row - 1:row + 1, col - 1:col + 1)
if ( & if ( &
count_matches(window, t1) == 5 & count_matches(window, t1) == 5 &
@@ -5,7 +5,7 @@ draft: true
toc: false toc: false
--- ---
<script>import Sidenote from '$lib/Sidenote.svelte';</script> import Sidenote from '@components/Sidenote.astro';
I know I said at the beginning that I was most likely not going to get through much of Advent of Code before running out of steam and giving up on the whole different-language-every-day thing, but man, even I wasn't expecting to only make it through 4 days before giving up. That's a new low, even for me. I know I said at the beginning that I was most likely not going to get through much of Advent of Code before running out of steam and giving up on the whole different-language-every-day thing, but man, even I wasn't expecting to only make it through 4 days before giving up. That's a new low, even for me.
@@ -4,10 +4,7 @@ date: 2023-12-26
draft: true draft: true
--- ---
<script> import Sidenote from '@components/Sidenote.astro';
import Sidenote from '$lib/Sidenote.svelte';
import BookPreview from '$projects/fantasy/BookPreview.svelte';
</script>
For a while now, I've had a private taxonomy of fantasy books, based on the distinction (or lack thereof) between the fantasy world and our own. It goes something like this: For a while now, I've had a private taxonomy of fantasy books, based on the distinction (or lack thereof) between the fantasy world and our own. It goes something like this:
@@ -40,7 +37,7 @@ Notable subregions include:
No overlap at all. I think most fantasy that's written tends to fall here. At least, it's what most people think of when you say "fantasy book," and the Wikipedia definition of "Fantasy" specifies that it's "typically set in a fictional universe," so I think it's fair to say that this is the "standard" position for a fantasy story to occupy on this axis. No overlap at all. I think most fantasy that's written tends to fall here. At least, it's what most people think of when you say "fantasy book," and the Wikipedia definition of "Fantasy" specifies that it's "typically set in a fictional universe," so I think it's fair to say that this is the "standard" position for a fantasy story to occupy on this axis.
Examples: <BookPreview ref="lotr">_The Lord of the Rings_</BookPreview>, <BookPreview ref="earthsea">_Earthsea_</BookPreview>, _The Prydain Chronicles_, _Wheel of Time_, _Belgariad_, _A Song of Ice and Fire_, etc. Pick up a book from the fantasy section of a bookstore and there's at least a 50% chance it will fall into this category. Examples: _The Lord of the Rings_, _Earthsea_, _The Prydain Chronicles_, _Wheel of Time_, _Belgariad_, _A Song of Ice and Fire_, etc. Pick up a book from the fantasy section of a bookstore and there's at least a 50% chance it will fall into this category.
### Mythopoeic Fantasy ### Mythopoeic Fantasy
@@ -75,7 +72,7 @@ Fantasy that's set in our world, but with magic.<Sidenote>Or other fantastical e
In the end, though, I decided that the point of this axis is to classify fantasy according to how much the fantasy world overlaps with our own, and alternate history involves _quite a lot of overlap_, even though the end result is a world that's not _quite_ identical with the real world. In the end, though, I decided that the point of this axis is to classify fantasy according to how much the fantasy world overlaps with our own, and alternate history involves _quite a lot of overlap_, even though the end result is a world that's not _quite_ identical with the real world.
Broadly speaking there are two variants of alternate history: 1) Either the fantastic has always been a part of life, or 2) it was suddenly introduced into the world by some (usually fairly cataclysmic) event. Broadly speaking there are two variants of alternate history: 1) Either the fantastic has always been a part of life, or 2) it was suddenly introduced into the world by some (usually fairly cataclysmic) event.
Examples of the first variant include _Cecelia and Kate_, the _Temeraire_ books, _Jonathan Strange and Mr Norrell_, etc. An interesting quirk of this variant is that it's almost always set significantly in the past, but for some reason not _quite_ as far back as the quasi-Medieval era that is the bread and butter of most "standard" fantasy. The Napoleonic/Regency era is popular, as is the Victorian era. Modern-day alternate-history stories of this type seem fairly uncommon--_Bartimaeus_ is the only example I can think of off the top of my head. Examples of the first variant include _Cecelia and Kate_, the _Temeraire_ books, _Jonathan Strange and Mr Norrell_, etc. An interesting quirk of this variant is that it's almost always set significantly in the past, but for some reason not _quite_ as far back as the quasi-Medieval era that is the bread and butter of most "standard" fantasy. The Napoleonic/Regency era is popular, as is the Victorian era. Modern-day alternate-history stories of this type seem fairly uncommon--_Bartimaeus_ is the only example I can think of off the top of my head.
@@ -3,9 +3,8 @@ title: Exposing Docker Containers to your LAN
description: If, for some strange reason, you should want to do such a thing. description: If, for some strange reason, you should want to do such a thing.
date: 2022-03-21 date: 2022-03-21
--- ---
<script>
import Sidenote from '$lib/Sidenote.svelte'; import Sidenote from '@components/Sidenote.astro';
</script>
A while back I had occasion to make a number of docker containers directly accessible on the LAN, i.e. without all the usual ceremony of port-forwardism that Docker requires. In retrospect I made it a lot more complicated than it had to be, but I wanted to document the process anyway because you never know when that sort of thing might come in handy. A while back I had occasion to make a number of docker containers directly accessible on the LAN, i.e. without all the usual ceremony of port-forwardism that Docker requires. In retrospect I made it a lot more complicated than it had to be, but I wanted to document the process anyway because you never know when that sort of thing might come in handy.
@@ -3,9 +3,8 @@ title: The Hitchiker's Guide to Mesh VPNs
description: The golden age of VPNery is upon us. description: The golden age of VPNery is upon us.
date: 2022-03-17 date: 2022-03-17
--- ---
<script>
import Sidenote from '$lib/Sidenote.svelte'; import Sidenote from '@components/Sidenote.astro';
</script>
Recently at work we've been moving to a new VPN, and naturally as part of that process we done a bunch of research into the available options before settling on one. Mostly I want to document that for my own future reference, so that if this question comes up again I don't have to go redo it all, but if it ends up being helpful to someone else someday then that's great too. (If I ever get this blog site launched, that is. Currently it's not looking too good.) Recently at work we've been moving to a new VPN, and naturally as part of that process we done a bunch of research into the available options before settling on one. Mostly I want to document that for my own future reference, so that if this question comes up again I don't have to go redo it all, but if it ends up being helpful to someone else someday then that's great too. (If I ever get this blog site launched, that is. Currently it's not looking too good.)
@@ -3,11 +3,10 @@ title: Imagining A Passwordless Future
description: Can we replace passwords with something more user-friendly? description: Can we replace passwords with something more user-friendly?
date: 2021-04-30 date: 2021-04-30
draft: true draft: true
dropcap: false dropcap: 'ascender'
--- ---
<script>
import Sidenote from '$lib/Sidenote.svelte'; import Sidenote from '@components/Sidenote.astro';
</script>
Passwords are the *worst*. Passwords are the *worst*.
@@ -33,4 +32,4 @@ and I doubt they'll be going anywhere any time soon.
I really hate passwords. I really hate passwords.
I use them, of course, because I can't not. And I use a password manager, because to my mind that's the current best compromise between being secure and absolutely losing your mind, but it still isn't great. Sometimes my password manager bugs out and refuses to auto-fill the password box, so I have to go hunt it down and copy-paste it in.<Sidenote>If I'm lucky. If I'm unlucky, the site will have disabled pasting into password inputs because "security," and I'm stuck having to type in a 16-character string of gibberish on a mobile phone, because that's how life is.</Sidenote> Other times I'll create a password, the password manager will happily file it away, and then I'll discover that it didn't meet the site's requirements,<Sidenote>Another test</Sidenote> because my auto-generated gibberish string didn't include the *right* special characters, and now I have the wrong password saved. I use them, of course, because I can't not. And I use a password manager, because to my mind that's the current best compromise between being secure and absolutely losing your mind, but it still isn't great. Sometimes my password manager bugs out and refuses to auto-fill the password box, so I have to go hunt it down and copy-paste it in.<Sidenote>If I'm lucky. If I'm unlucky, the site will have disabled pasting into password inputs because "security," and I'm stuck having to type in a 16-character string of gibberish on a mobile phone, because that's how life is.</Sidenote> Other times I'll create a password, the password manager will happily file it away, and then I'll discover that it didn't meet the site's requirements,<Sidenote>Another test</Sidenote> because my auto-generated gibberish string didn't include the *right* special characters, and now I have the wrong password saved.
@@ -4,9 +4,7 @@ date: 2023-10-01
draft: true draft: true
--- ---
<script> import Sidenote from '@components/Sidenote.astro';
import Sidenote from '$lib/Sidenote.svelte';
</script>
I use Kubernetes on my personal server, largely because I wanted to get some experience working with it. It's certainly been helpful in that regard, but after a couple of years I think I can pretty confidently say that it's not the ideal tool for my use-case. Duh, I guess? But I think it's worth talking about _why_ that's the case, and what exactly _would_ be the ieal tool. I use Kubernetes on my personal server, largely because I wanted to get some experience working with it. It's certainly been helpful in that regard, but after a couple of years I think I can pretty confidently say that it's not the ideal tool for my use-case. Duh, I guess? But I think it's worth talking about _why_ that's the case, and what exactly _would_ be the ieal tool.
@@ -4,9 +4,7 @@ date: 2026-01-01
draft: true draft: true
--- ---
WORK IN PROGRESS import Sidenote from '@components/Sidenote.astro';
<script>import Sidenote from '$lib/Sidenote.svelte';</script>
The other day I came across [this post](https://nesbitt.io/2025/12/26/how-uv-got-so-fast.html), and it kind of got my feathers ruffled a little bit. The other day I came across [this post](https://nesbitt.io/2025/12/26/how-uv-got-so-fast.html), and it kind of got my feathers ruffled a little bit.
@@ -5,7 +5,7 @@ date: 2022-08-19
draft: true draft: true
--- ---
<script>import Sidenote from '$lib/Sidenote.svelte';</script> import Sidenote from '@components/Sidenote.astro';
Here's a fun way to start an argument: Go ask on a programming forum whether C should be considered a "high-level" or "low-level" language. The majority of responses will be along the lines of "low-level, obviously, you have to call `malloc()` all the time", but every once in a while you'll find some crusty old wizard who earned his sripes on MULTICS grousing about how kids these days don't understand what a _real_ low-level language looks like, and back in my day we had to flip individual bits with our teeth, and so on. Here's a fun way to start an argument: Go ask on a programming forum whether C should be considered a "high-level" or "low-level" language. The majority of responses will be along the lines of "low-level, obviously, you have to call `malloc()` all the time", but every once in a while you'll find some crusty old wizard who earned his sripes on MULTICS grousing about how kids these days don't understand what a _real_ low-level language looks like, and back in my day we had to flip individual bits with our teeth, and so on.
@@ -21,4 +21,4 @@ You can probably throw little language-specific details back and forth until the
* **Level 4** languages give you control over things like intermediate variables and so on - there are probably a lot of sub-levels here, like having to instantiate loop variables vs. having them handled for you,<Sidenote>Or both! Hi Javascript!</Sidenote> but I think this is the core of it. Most dynamic/interpreted languages probably fall here: Python, JS, Ruby, PHP, Perl, etc. * **Level 4** languages give you control over things like intermediate variables and so on - there are probably a lot of sub-levels here, like having to instantiate loop variables vs. having them handled for you,<Sidenote>Or both! Hi Javascript!</Sidenote> but I think this is the core of it. Most dynamic/interpreted languages probably fall here: Python, JS, Ruby, PHP, Perl, etc.
* **Level 5** languages don't give you control over any of those. The only one I can think of off the top of my head is SQL, but there are probably others. I have a hunch that at this point you tend to see a lot more domain-specific stuff, since it's easier to abstract away details when you know what the use-case will look like. * **Level 5** languages don't give you control over any of those. The only one I can think of off the top of my head is SQL, but there are probably others. I have a hunch that at this point you tend to see a lot more domain-specific stuff, since it's easier to abstract away details when you know what the use-case will look like.
I can also imagine a Level 0 which gives you control over things that aren't currently exposed by the underlying hardware. For instance, you could have a language construct that allows you to "pin" memory regions to a certain cache level, guaranteeing that you can always access it quickly. Or you could attach a "hint" to a branching codepath that allows you to override the CPU's branch predictor, if you know that one case will be overwhelmingly more common than the other. I wonder whether we'll start to see this sort of thing in the future, as Moore's Law continues to slow down and people start looking for more and more ways to squeeze maximum performance out of their hardware. I can also imagine a Level 0 which gives you control over things that aren't currently exposed by the underlying hardware. For instance, you could have a language construct that allows you to "pin" memory regions to a certain cache level, guaranteeing that you can always access it quickly. Or you could attach a "hint" to a branching codepath that allows you to override the CPU's branch predictor, if you know that one case will be overwhelmingly more common than the other. I wonder whether we'll start to see this sort of thing in the future, as Moore's Law continues to slow down and people start looking for more and more ways to squeeze maximum performance out of their hardware.
@@ -3,7 +3,7 @@ title: 'Mixing GUIs and CLIs on Windows: A Cautionary Tale'
date: 2024-06-17 date: 2024-06-17
--- ---
<script>import Sidenote from '$lib/Sidenote.svelte';</script> import Sidenote from '@components/Sidenote.astro';
If you've used desktop Linux, then I'm sorry for you.<Sidenote>I also use desktop Linux. I'm sorry for me, too.</Sidenote> You will, however, most likely be familiar with the practice of using the same app from either the CLI or the GUI, depending on how you invoke it and what you want to do with it. In some cases, the CLI merely replicates the functionality of the GUI, but (due to being, you know, a CLI) is much easier to incorporate in scripts and such. In other cases ([Wezterm](https://wezfurlong.org/wezterm/) is a good example) the GUI app acts as a "server" with which the CLI communicates to cause it to do various things while it runs. If you've used desktop Linux, then I'm sorry for you.<Sidenote>I also use desktop Linux. I'm sorry for me, too.</Sidenote> You will, however, most likely be familiar with the practice of using the same app from either the CLI or the GUI, depending on how you invoke it and what you want to do with it. In some cases, the CLI merely replicates the functionality of the GUI, but (due to being, you know, a CLI) is much easier to incorporate in scripts and such. In other cases ([Wezterm](https://wezfurlong.org/wezterm/) is a good example) the GUI app acts as a "server" with which the CLI communicates to cause it to do various things while it runs.
@@ -3,9 +3,8 @@ title: Password Strength, Hackers, and You
date: 2023-10-21 date: 2023-10-21
draft: true draft: true
--- ---
<script>
import Sidenote from '$lib/Sidenote.svelte'; import Sidenote from '@components/Sidenote.astro';
</script>
Every once in a while, as my friends and family can attest, I go off on a random screed about passwords, password strength, password cracking, logins, etc. To which they listen with polite-if-increasingly-glassy-eyed expressions, followed by an equally polite change of conversational topic. To avoid falling into this conversational tarpit _quite_ so often, I've decided to write it all up here, so that instead of spewing it into an unsuspecting interlocutor's face I can simply link them here.<Sidenote>Maybe I can get business cards printed, or something.</Sidenote> Whereupon they can say "Thanks, that sounds interesting," and proceed to forget that it ever existed. So it's a win-win: I get to feel like I've Made A Difference, and they don't have to listen to a half-hour of only-marginally-interesting infosec jargon. Every once in a while, as my friends and family can attest, I go off on a random screed about passwords, password strength, password cracking, logins, etc. To which they listen with polite-if-increasingly-glassy-eyed expressions, followed by an equally polite change of conversational topic. To avoid falling into this conversational tarpit _quite_ so often, I've decided to write it all up here, so that instead of spewing it into an unsuspecting interlocutor's face I can simply link them here.<Sidenote>Maybe I can get business cards printed, or something.</Sidenote> Whereupon they can say "Thanks, that sounds interesting," and proceed to forget that it ever existed. So it's a win-win: I get to feel like I've Made A Difference, and they don't have to listen to a half-hour of only-marginally-interesting infosec jargon.
@@ -4,64 +4,10 @@ description: An entirely-too-detailed dive into how I implemented sidenotes for
date: 2023-08-14 date: 2023-08-14
draft: true draft: true
--- ---
<script>
import Sidenote from '$lib/Sidenote.svelte';
import UnstyledSidenote from '$lib/UnstyledSidenote.svelte';
</script>
<style> import Sidenote from '@components/Sidenote.astro';
.counter { import UnstyledSidenote from '@projects/sidenotes/UnstyledSidenote.astro';
counter-increment: sidenote; import '@projects/sidenotes/sidenote.css';
}
.counter::before {
content: counter(sidenote);
color: var(--accent-color);
font-size: 0.75rem;
position: relative;
bottom: 0.2rem;
margin-left: 0.1rem;
}
.sidenote-absolute {
position: absolute;
left: calc(50% + min(100%, var(--content-width)) / 2 + 1rem);
max-width: 12rem;
font-size: 0.75rem;
}
.sidenote-absolute::before {
content: counter(sidenote);
color: var(--accent-color);
font-size: 0.75rem;
position: relative;
bottom: 0.2rem;
margin-right: 0.1rem;
}
:global(.sn-float) {
float: right;
}
:global(.sn-clear) {
float: right;
clear: right;
}
:global(.sn-gutter) {
float: right;
width: 14rem;
margin-right: -14rem;
}
:global(.sn-gap) {
float: right;
width: 14rem;
margin-right: -16rem;
}
:global(.sn-var-width) {
float: right;
--width: min(14rem, calc(50vw - var(--content-width) / 2) - 2rem);
width: var(--width);
margin-right: calc(0rem - var(--width) - 2rem);
}
</style>
One of my major goals when building this blog was to have sidenotes. I've always been a fan of sidenotes on the web, because the most comfortable reading width for a column of text is <em>far</em> less than the absurd amounts of screen width we tend to have available, and what else are we going to use it for?<Sidenote>Some sites use it for ads, of course, which is yet another example of how advertising ruins everything.</Sidenote> One of my major goals when building this blog was to have sidenotes. I've always been a fan of sidenotes on the web, because the most comfortable reading width for a column of text is <em>far</em> less than the absurd amounts of screen width we tend to have available, and what else are we going to use it for?<Sidenote>Some sites use it for ads, of course, which is yet another example of how advertising ruins everything.</Sidenote>
@@ -81,7 +27,7 @@ My first approach was something like this:
.sidenote { .sidenote {
position: absolute; position: absolute;
/* 50% takes us to the midpoint of the page, /* 50% takes us to the midpoint of the page,
half of content-width gets out out to the gutter, half of content-width gets out out to the gutter,
and the extra 1rem gives us some breathing room. */ and the extra 1rem gives us some breathing room. */
left: calc(50% + var(--content-width) / 2 + 1rem); left: calc(50% + var(--content-width) / 2 + 1rem);
max-width: 12rem; max-width: 12rem;
@@ -89,7 +35,7 @@ My first approach was something like this:
} }
``` ```
And it worked! Sort of. Here's an example.<span class="counter"></span><span class="sidenote-absolute">My initial take on sidenotes. Seems to be working, right?</span> Unfortunately it has a major flaw: Absolute positioning removes an element from the document flow _entirely_, while I wanted sidenotes to still flow with _each other_, That doesn't happen with this solution--if you have multiple sidenotes too close together, they will overlap because absolute positioning Just Doesn't Care.<span class="counter"></span><span class="sidenote-absolute">Like this one.</span><span class="counter"><span class="sidenote-absolute" style="transform: translateY(0.2rem)">And this one, which I've moved down just a smidge to make the overlap more apparent.</span> And it worked! Sort of. Here's an example.<span class="counter"></span><span class="sidenote-absolute">My initial take on sidenotes. Seems to be working, right?</span> Unfortunately it has a major flaw: Absolute positioning removes an element from the document flow _entirely_, while I wanted sidenotes to still flow with _each other_, That doesn't happen with this solution--if you have multiple sidenotes too close together, they will overlap because absolute positioning Just Doesn't Care.<span class="counter"></span><span class="sidenote-absolute">Like this one.</span><span class="counter"></span><span class="sidenote-absolute" style="transform: translateY(0.2rem)">And this one, which I've moved down just a smidge to make the overlap more apparent.</span>
Obviously, it isn't that hard to just scan through the page looking for sidenotes, detect when they overlap, and then (since they're already absolutely positioned) adjust their `top` values appropriately to get rid of the overlap. But I didn't want to do this for a variety of reasons. Obviously, it isn't that hard to just scan through the page looking for sidenotes, detect when they overlap, and then (since they're already absolutely positioned) adjust their `top` values appropriately to get rid of the overlap. But I didn't want to do this for a variety of reasons.
@@ -140,7 +86,7 @@ body {
content: counter(unstyled-sidenote); content: counter(unstyled-sidenote);
font-size: 0.75rem; font-size: 0.75rem;
color: var(--accent-color); color: var(--accent-color);
/* Since the sidenote is floated it counts as a positioned element, /* Since the sidenote is floated it counts as a positioned element,
so this would make the counter overlap the start of the text... */ so this would make the counter overlap the start of the text... */
position: absolute; position: absolute;
/* ...except that we move it out to the left and up a bit, so /* ...except that we move it out to the left and up a bit, so
@@ -165,7 +111,7 @@ This is easiest to show with an example, so let's do that. Here are two sidenote
This is fixed by `clear` because it changes the behavior of floats. Here are the same two sidenotes as above, but with `clear: right` applied to the second.<UnstyledSidenote class="sn-float">One.</UnstyledSidenote><UnstyledSidenote class="sn-clear">Two.</UnstyledSidenote> The `clear` property causes the second sidenote to drop below the first, which happens to be exactly the behavior that we want. All that's left is to apply the negative margin like so<UnstyledSidenote class="sn-clear sn-gutter">Three.</UnstyledSidenote><UnstyledSidenote class="sn-clear sn-gutter">Four.</UnstyledSidenote>and the whole stack will slide right over into the gutter. This is fixed by `clear` because it changes the behavior of floats. Here are the same two sidenotes as above, but with `clear: right` applied to the second.<UnstyledSidenote class="sn-float">One.</UnstyledSidenote><UnstyledSidenote class="sn-clear">Two.</UnstyledSidenote> The `clear` property causes the second sidenote to drop below the first, which happens to be exactly the behavior that we want. All that's left is to apply the negative margin like so<UnstyledSidenote class="sn-clear sn-gutter">Three.</UnstyledSidenote><UnstyledSidenote class="sn-clear sn-gutter">Four.</UnstyledSidenote>and the whole stack will slide right over into the gutter.
It's smack up against the body text, though. In fact, since the floating counter hangs off to the left, it actually overlaps with the body text.(Depending on line wrapping, this may not be immediately apparent from the above.) It's smack up against the body text, though. In fact, since the floating counter hangs off to the left, it actually overlaps with the body text.(Depending on line wrapping, this may not be immediately apparent from the above.)
We can fix that in one of two ways. 1) We can increase the negative margin so that it's _greater_ than the width of the sidenote, or 2) We can just stick in some padding.<UnstyledSidenote class="sn-gap">Voila! Collision avoided.</UnstyledSidenote> I like the first option better, because it better reflects what we're actually doing here--margin is for creating caps _outside_ and _between_ elements, while padding is for gaps _inside_. We can fix that in one of two ways. 1) We can increase the negative margin so that it's _greater_ than the width of the sidenote, or 2) We can just stick in some padding.<UnstyledSidenote class="sn-gap">Voila! Collision avoided.</UnstyledSidenote> I like the first option better, because it better reflects what we're actually doing here--margin is for creating caps _outside_ and _between_ elements, while padding is for gaps _inside_.
@@ -179,7 +125,7 @@ Here's what we have so far:
} }
``` ```
We still have a bit of a problem, though. Because we've assigned the sidenote a fixed width, it doesn't automatically shrink when the window gets too small for it. Obviously, of course, at _some_ point we're going to switch to the mobile version, which displays at the bottom of the screen and can be toggled on or off. But there are plenty of widths where sidenotes would still work perfectly well, just with a slightly narrower width than our initial `14rem`. We still have a bit of a problem, though. Because we've assigned the sidenote a fixed width, it doesn't automatically shrink when the window gets too small for it. Obviously, of course, at _some_ point we're going to switch to the mobile version, which displays at the bottom of the screen and can be toggled on or off. But there are plenty of widths where sidenotes would still work perfectly well, just with a slightly narrower width than our initial `14rem`.
Fortunately, CSS `calc()` is widely supported and does exactly what we need.<UnstyledSidenote class="sn-var-width">Here we are! You may need to resize your window to get full effect.</UnstyledSidenote> Let's take a look: Fortunately, CSS `calc()` is widely supported and does exactly what we need.<UnstyledSidenote class="sn-var-width">Here we are! You may need to resize your window to get full effect.</UnstyledSidenote> Let's take a look:
+55
View File
@@ -0,0 +1,55 @@
---
title: Slam-dunk No-brainer Uses For LLMs, Today
date: 2026-04-09
draft: true
---
import Sidenote from '@components/Sidenote.astro';
There's a lot of debate these days on how far and how fast AI will go, will it take over all human endeavor or just some, will it gain sentience<Sidenote>According to some, it already has.</Sidenote> and decide to exterminate humanity, etc. I have lots of thoughts about those questions, but that's not what I'm here to talk about today. Today, I would like to talk about what you can already use LLMs for, productively, with few-to-no downsides. Most of my own experience using LLMs has been for coding, so that's what I'm going to focus on here.
None of what I'm saying here is particularly novel, or even very interesting if you're very experienced with using LLMs for coding. Really I'm writing as much to organize my own thoughts as anything else, and to hopefully cement in my mind that "these are the sorts of tasks for which I should use an LLM" so that I don't waste time in the future doing something by hand that really _would_ be better done by an LLM.
## Debugging
Now, I need to preface this by saying that LLMs are not a magic bullet for debugging, just like for most othe things. But here's the thing about debugging. _A lot_ of the time that I spend debugging is really just spent staring at code and waiting for inspiration to strike. If I don't immediately have a theory for what's wrong upon seeing the bug - or if I do, but my theory turns out to be wrong - I often just get stuck. And I will sit there, poking at random things, bouncing around between the same three pieces of code or whatever, waiting for some unconscious process to click into place and provide me with a new angle of inquiry.<Sidenote>It really is an unconscious process, which is where the advice to "sleep on it" comes from. This often really works! Taking time away from a problem and coming back to it can often provide fresh insight, even though - maybe even _because_ - that time isn't spent consciously thinking about it.</Sidenote>
But a lot of the time, the root cause of a bug is really glaringly obvious once you finally figure it out. A coworker and I have a private aphorism that we always quote to each other: "The more baffling and incomprehensible the problem, the more obvious the solution." This isn't _strictly_ true, of course; some bugs really _are_ baffling and incomprehensible, but it's true the vast majority of the time.
But what if you could flip that around? Instead of trying to work back from the problem to a possible solution, what if you could be presented with a possible solution and then determine whether it matches your problem? If the solution really _is_ obvious as soon as you see it, then it would be much easier if you could just crank out a dozen possible solutions, then check them each to see if they trigger that "oh, duh" flash of insight.
So all you need is something that can barf out a bunch of _plausible_ solutions to your problem, without necessarily having to be right every time. And LLMs are absolutely _great_ at that. They don't even need a very good hit rate. If even 20% of their suggestions are worth pursuing (and in my experience it's usually much better than that), then you're still coming out ahead, because it can come up with 5 suggestions far faster than it takes you to come up with even one.
### Especially syntax errors
But LLM debugging _really_ gets supercharged when it comes to syntax errors. I don't know if this is a common problem or if it's just me, but I can easily spend upwards of half an hour staring at just a few lines of code, looking for the syntax error that my tooling is complaining about but that I _just can't see_.<Sidenote>SQL is the worst for this. For some reason, databases seem stuck in the stone ages when it comes to reporting synatx errors. If you're _lucky_ they'll give you a character index, but most of the time it's just `Invalid syntax, near AND` or something. Great, thanks. there are 20 ANDs in this query, that's real helpful.</Sidenote>
But when I throw these situations at an LLM, it will usually find the syntax error on the first try. I have very rarely observed an LLM failing in this paticular situation. I think it might have to do with the way LLMs process text in tokens: while humans process text visually, so it's easy for us to miss things that are visually less prominent (like dots and commas), LLMs see text as a series of tokens, whose similarity is governed by their _meaning_<Sidenote>Kind of; my understanding is that LLMs ultimately reduce "meaning" to something like "this word's statistical relationship to a bunch of other words, based on analysis of a massive amount of text."</Sidenote>. Moreover, LLMs are fundamentally "next-token predictors", <Sidenote>Not trying to minimize their capabilities here, it's truly incredible that we've gotten so far with such a conceptually simple mechanism.</Sidenote> so something like a missing or extra comma is _glaringly_ obvious to an LLM in a way that it wouldn't be to a human.
And thank goodness for that, because tracking down a non-obvious syntax error is one of my least favorite ways to spend time.
### But they can still fall flat
Not that, again, LLMs are a magic bullet for debugging. In my experience, most bugs fall into one of three categories:
1. Obvious bugs, where you know what's wrong as soon as the bug manifests,
2. Subtle-at-first-but-ultimately-obvious bugs, where the root cause is hard to track down but obvious once you see it, and
3. _Actually_ subtle bugs, the kind that you write up on your corporate blog after solving to reap those sweet sweet upvotes.
## UI Mockups
## Brainstorming
You can't uncritically accept everything it comes up with, of course, but if you're stuck an LLM can really help to get you unstuck sometimes.
## Boilerplate
e.g. persistence wrappers
## Migrating
E.g. Vue options -> composition API. This has to be used with some care, though, since porting something to a sufficiently different paradigm can introduce weirdness. (E.g. Python that looks like it was written by a Java programmer)
## Naming Things
No joke! One of the two Hard Problems in Computer Science, but they're great for it.
@@ -3,9 +3,7 @@ title: 'Converting ssh keys from old formats'
date: 2024-07-06 date: 2024-07-06
--- ---
<script> import Sidenote from '@components/Sidenote.astro';
import Sidenote from '$lib/Sidenote.svelte';
</script>
Like a lot of people, my main experience with private keys has come from using them for SSH. I'm familiar with the theory, of course - I know generally what asymmetric encryption does,<Sidenote>Although exactly _how_ it does so is still a complete mystery to me. I've looked up descriptions of RSA several times, and even tried to work my way through a toy example, but it's never helped. And I couldn't even _begin_ to explain elliptic curve cryptography beyond "black math magic".</Sidenote> and I know that it means a compromised server can't reveal your private key, which is nice although if you only ever use a given private key to SSH into your server and the server is already compromised, is that really so helpful?<Sidenote>Yes, yes, I know that it means you can use the same private key for _multiple_ things without having to worry, but in practice a lot of people seem to use separate private keys for separate things, and even though I'm not entirely sure why I feel uncomfortable doing otherwise.</Sidenote> Like a lot of people, my main experience with private keys has come from using them for SSH. I'm familiar with the theory, of course - I know generally what asymmetric encryption does,<Sidenote>Although exactly _how_ it does so is still a complete mystery to me. I've looked up descriptions of RSA several times, and even tried to work my way through a toy example, but it's never helped. And I couldn't even _begin_ to explain elliptic curve cryptography beyond "black math magic".</Sidenote> and I know that it means a compromised server can't reveal your private key, which is nice although if you only ever use a given private key to SSH into your server and the server is already compromised, is that really so helpful?<Sidenote>Yes, yes, I know that it means you can use the same private key for _multiple_ things without having to worry, but in practice a lot of people seem to use separate private keys for separate things, and even though I'm not entirely sure why I feel uncomfortable doing otherwise.</Sidenote>
@@ -46,7 +44,7 @@ It also dates back to a time when manpages were the primary way of communicating
``` ```
-i This option will read an unencrypted private (or public) key file in the format specified by the -m option and print an -i This option will read an unencrypted private (or public) key file in the format specified by the -m option and print an
OpenSSH compatible private (or public) key to stdout. This option allows importing keys from other software, including OpenSSH compatible private (or public) key to stdout. This option allows importing keys from other software, including
several commercial SSH implementations. The default import format is RFC4716. several commercial SSH implementations. The default import format is "RFC4716".
``` ```
Sounds great, right? Import private keys from other formats, i.e. convert them to our format, right? But it's _lying_. The `-i` mode doesn't accept private keys _at all_, that I've been able to tell, whatever their format. Giving it one will first prompt you for the passphrase, if any (so it's lying about needing an unencrypted input, although that's not a big deal) and then tell you bluntly that the your file is not in a valid format. The specific error message varies slightly with the particular format - attempting to give it a PEM file (with the appropriate option) returns `<file> is not a recognized public key format`, PKCS8 gets `unrecognised raw private key format`, and speicfy OpenSSH format just says `parse key: invalid format`. So really, the only thing this mode is useful for is reading a _public_ key in some PEM-ish format, and spitting out the line that you can used in `authorized_keys` - the one that starts with `ssh-rsa`, `ssh-ed25519`, etc. Sounds great, right? Import private keys from other formats, i.e. convert them to our format, right? But it's _lying_. The `-i` mode doesn't accept private keys _at all_, that I've been able to tell, whatever their format. Giving it one will first prompt you for the passphrase, if any (so it's lying about needing an unencrypted input, although that's not a big deal) and then tell you bluntly that the your file is not in a valid format. The specific error message varies slightly with the particular format - attempting to give it a PEM file (with the appropriate option) returns `<file> is not a recognized public key format`, PKCS8 gets `unrecognised raw private key format`, and speicfy OpenSSH format just says `parse key: invalid format`. So really, the only thing this mode is useful for is reading a _public_ key in some PEM-ish format, and spitting out the line that you can used in `authorized_keys` - the one that starts with `ssh-rsa`, `ssh-ed25519`, etc.
@@ -59,9 +57,9 @@ But wait! The `-i` option mentions that the formats accepted are specified by th
-m key_format -m key_format
Specify a key format for key generation, the -i (import), -e (export) conversion options, and the -p change passphrase Specify a key format for key generation, the -i (import), -e (export) conversion options, and the -p change passphrase
operation. The latter may be used to convert between OpenSSH private key and PEM private key formats. The supported operation. The latter may be used to convert between OpenSSH private key and PEM private key formats. The supported
key formats are: RFC4716 (RFC 4716/SSH2 public or private key), PKCS8 (PKCS8 public or private key) or PEM (PEM key formats are: "RFC4716" (RFC 4716/SSH2 public or private key), "PKCS8" (PKCS8 public or private key) or "PEM" (PEM
public key). By default OpenSSH will write newly-generated private keys in its own format, but when converting public public key). By default OpenSSH will write newly-generated private keys in its own format, but when converting public
keys for export the default format is RFC4716. Setting a format of PEM when generating or updating a supported pri keys for export the default format is "RFC4716". Setting a format of "PEM" when generating or updating a supported pri
vate key type will cause the key to be stored in the legacy PEM private key format. vate key type will cause the key to be stored in the legacy PEM private key format.
``` ```
@@ -4,9 +4,8 @@ description: I see what Arthur C. Clarke was getting at, but I don't think I agr
date: 2022-05-14 date: 2022-05-14
draft: true draft: true
--- ---
<script>
import Sidenote from '$lib/Sidenote.svelte'; import Sidenote from '@components/Sidenote.astro';
</script>
Arthur C. Clarke is famous for many things, among them being his dictum that "Sufficiently advanced technology is indistinguishable from magic."<Sidenote>Which I had always thought of as "Clarke's Law", I looked it up when I went to write this and discovered that it's actually Clarke's _Third_ Law. Apparently he has two others. I don't know that I've ever run into them, though. Maybe he should have led with this one?</Sidenote> I'm often happy to take issue with Clarke's opinions, so today I want to put this one on the chopping block. Arthur C. Clarke is famous for many things, among them being his dictum that "Sufficiently advanced technology is indistinguishable from magic."<Sidenote>Which I had always thought of as "Clarke's Law", I looked it up when I went to write this and discovered that it's actually Clarke's _Third_ Law. Apparently he has two others. I don't know that I've ever run into them, though. Maybe he should have led with this one?</Sidenote> I'm often happy to take issue with Clarke's opinions, so today I want to put this one on the chopping block.
@@ -3,9 +3,8 @@ title: The Enduring Shell
date: 2023-11-26 date: 2023-11-26
draft: true draft: true
--- ---
<script>
import Sidenote from '$lib/Sidenote.svelte'; import Sidenote from '@components/Sidenote.astro';
</script>
Over twenty years ago, Neal Stephenson wrote an essay/pamphlet/novella/whatever-you-want-to-call-it titled [_In the beginning was the Command Line_](http://project.cyberpunk.ru/lib/in_the_beginning_was_the_command_line/). It's worth reading, and you should definitely do that at some point, but you should finish this first because it's quite long and Neal Stephenson is a much better writer than I am, so I worry you wouldn't come back.<Sidenote>I should probably also mention that it's Stephenson at his, ah, least restrained, so it's rather meandering. Don't get me wrong, it's _well-written_ meandering, but I don't think you can argue that an essay about command lines isn't meandering when it includes a thousand-word segment about Disney World.</Sidenote> As you might expect, Stephenson spends a lot of that material talking about the CLI versus the GUI, as though they were opposite poles of some abstract computational magnet. It's been a while since I read it, but I distinctly remember him describing the advent of the GUI as a sort of impending inevitability, an unfortunate but unstoppable end to which all things in time will eventually come. It's a little like watching [_Valkyrie_](https://www.imdb.com/title/tt0985699/), actually--you know whe whole time how it's going to turn out, but you can't keep yourself from watching it anyway. Over twenty years ago, Neal Stephenson wrote an essay/pamphlet/novella/whatever-you-want-to-call-it titled [_In the beginning was the Command Line_](http://project.cyberpunk.ru/lib/in_the_beginning_was_the_command_line/). It's worth reading, and you should definitely do that at some point, but you should finish this first because it's quite long and Neal Stephenson is a much better writer than I am, so I worry you wouldn't come back.<Sidenote>I should probably also mention that it's Stephenson at his, ah, least restrained, so it's rather meandering. Don't get me wrong, it's _well-written_ meandering, but I don't think you can argue that an essay about command lines isn't meandering when it includes a thousand-word segment about Disney World.</Sidenote> As you might expect, Stephenson spends a lot of that material talking about the CLI versus the GUI, as though they were opposite poles of some abstract computational magnet. It's been a while since I read it, but I distinctly remember him describing the advent of the GUI as a sort of impending inevitability, an unfortunate but unstoppable end to which all things in time will eventually come. It's a little like watching [_Valkyrie_](https://www.imdb.com/title/tt0985699/), actually--you know whe whole time how it's going to turn out, but you can't keep yourself from watching it anyway.
@@ -3,9 +3,8 @@ title: Thoughts on Vue vs Svelte
description: They're more similar than they are different, but they say the most bitter enemies are those who have the fewest differences. description: They're more similar than they are different, but they say the most bitter enemies are those who have the fewest differences.
date: 2023-06-29 date: 2023-06-29
--- ---
<script>
import Sidenote from '$lib/Sidenote.svelte'; import Sidenote from '@components/Sidenote.astro';
</script>
Recently I've had a chance to get to know Vue a bit. Since my frontend framework of choice has previously been Svelte (this blog is built in Svelte, for instance) I was naturally interested in how they compared. Recently I've had a chance to get to know Vue a bit. Since my frontend framework of choice has previously been Svelte (this blog is built in Svelte, for instance) I was naturally interested in how they compared.
@@ -3,7 +3,7 @@ title: Why the Internet is Terrible
date: 2024-11-16 date: 2024-11-16
--- ---
<script>import Sidenote from '$lib/Sidenote.svelte';</script> import Sidenote from '@components/Sidenote.astro';
I just got done deleting ~30 bogus user accounts from my [personal Gitea insteance](https://git.jfmonty2.com). They all had reasonable-ish-sounding names, one empty repository, and profiles that looked like [this](/bogus_user_profile.jpg). Note the exceedingly spammy link to a real site (still up as of writing) and the ad-copy bio. I just got done deleting ~30 bogus user accounts from my [personal Gitea insteance](https://git.jfmonty2.com). They all had reasonable-ish-sounding names, one empty repository, and profiles that looked like [this](/bogus_user_profile.jpg). Note the exceedingly spammy link to a real site (still up as of writing) and the ad-copy bio.
-13
View File
@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="preload" href="/Tajawal-Regular.woff2" as="font" type="font/woff2" />
<link rel="alternate" type="application/atom+xml" href="/feed">
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+27
View File
@@ -0,0 +1,27 @@
---
export interface Props {
name: string,
width?: string,
height?: string,
display?: string,
};
const { name, width, height, display } = Astro.props;
const icons = import.meta.glob<{string: string}>('@components/icons/*.svg', { query: '?raw', import: 'default' });
const path = `/src/components/icons/${name}.svg`;
if (icons[path] === undefined) {
throw new Error(`Icon ${name} does not exist.`);
}
const icon = await icons[path]();
---
<span class="icon" set:html={icon} />
<style define:vars={{ width, height, display }}>
.icon :global(svg) {
display: var(--display, block);
width: var(--width, 100%);
height: var(--height, 100%);
}
</style>
+164
View File
@@ -0,0 +1,164 @@
---
import '@fontsource-variable/baskervville-sc';
import '@styles/code.css';
import type { CollectionEntry } from 'astro:content';
import { render } from 'astro:content';
import Toc from '@components/Toc.vue';
import { formatDate } from '@lib/datefmt';
import Icon from '@components/Icon.astro';
import { headingElements } from '@components/headings';
export interface Props {
entry: CollectionEntry<'posts'>,
prevSlug?: string | null,
nextSlug?: string | null,
};
const { entry, prevSlug, nextSlug } = Astro.props;
const { Content, headings } = await render(entry);
---
<article class="prose" data-dropcap-style={entry.data.dropcap}>
<header class="title">
<h1>
<!-- <SmallCaps text={entry.data.title} upperWeight={500} lowerWeight={800} /> -->
{ entry.data.title }
</h1>
<p class="subtitle">{ formatDate(entry.data.date) }</p>
</header>
<div id="left-gutter">
<Toc client:load {headings} />
</div>
<section class="post">
<Content components={headingElements} />
</section>
<div id="right-gutter" />
<footer>
{prevSlug && (
<div class="footer-link left">
<Icon name="arrow-left" height="1em" />
<a href={`/${prevSlug}`} data-astro-prefetch>Older</a>
</div>
)}
{nextSlug && (
<div class="footer-link right">
<a href={`/${nextSlug}`} data-astro-prefetch>Newer</a>
<Icon name="arrow-right" height="1em" />
</div>
)}
</footer>
</article>
<style>
/* 3-column grid: left gutter, center content, and right gutter */
article {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, var(--content-width)) minmax(0, 1fr);
/* a bit of breathing room for narrow screens */
padding: 0 var(--content-padding);
}
h1 {
font-family: 'Baskervville SC Variable';
}
#left-gutter {
grid-column: 1 / 2;
justify-self: end;
}
#right-gutter {
grid-column: 3 / 4;
justify-self: start;
}
.title {
grid-column: 2 / 3;
}
.subtitle {
font-size: 0.85em;
font-style: italic;
margin-top: -0.25rem;
}
.post {
grid-column: 2 / 3;
}
footer {
grid-column: 2 / 3;
margin-bottom: 2.5rem;
display: flex;
color: var(--content-color-faded);
margin-top: 1.5rem;
padding-top: 2.25rem;
border-top: 1px solid var(--content-color-faded);
& a {
font-size: 1.25rem;
color: var(--content-color-faded);
text-decoration: underline;
text-underline-offset: 0.25em;
text-decoration-color: transparent;
transition: 150ms;
&:hover {
text-decoration-color: currentColor;
text-decoration: underline;
}
}
}
/* basic styles */
.footer-link {
display: flex;
gap: 0.25rem;
align-items: center;
&.right {
margin-left: auto;
}
}
/* animate on hover */
.footer-link {
& :global(svg) {
transition: transform 150ms;
}
&.right:hover :global(svg) {
transform: translateX(50%);
}
&.left:hover :global(svg) {
transform: translateX(-50%);
}
}
article {
& :global(section.post::first-letter) {
font-family: 'Baskervville';
color: var(--accent-color);
}
&[data-dropcap-style="descender"] :global(section.post::first-letter) {
initial-letter: 2;
margin-right: 0.5rem;
}
&[data-dropcap-style="ascender"] :global(section.post::first-letter) {
font-size: 2em;
line-height: 1;
}
}
</style>
+159
View File
@@ -0,0 +1,159 @@
---
import Icon from '@components/Icon.astro';
const id = crypto.randomUUID();
SIDENOTE_COUNT += 1
---
<label for={id} class="counter anchor">{ SIDENOTE_COUNT }</label>
<input {id} type="checkbox" class="sidenote-toggle" />
<!-- we have to use spans for everything, otherwise Astro "helpfully" inserts
ending </p> tags before every sidenote because you technically can't have
another block-level element inside a <p> -->
<span class="sidenote">
<span class="content">
<span class="counter floating">{ SIDENOTE_COUNT }</span>
<slot />
</span>
<button class="dismiss">
<label for={id}>
<Icon name="chevron-down" />
</label>
</button>
</span>
<!-- make sure that only one sidenote is active at a time -->
<script>
let activeSidenote = null;
document.querySelectorAll('.sidenote-toggle').forEach(el => {
el.addEventListener('change', evt => {
if (evt.target.checked) {
if (activeSidenote) {
activeSidenote.checked = false;
}
activeSidenote = evt.target;
}
else if (activeSidenote.id === evt.target.id) {
activeSidenote = null;
}
});
});
</script>
<style>
.sidenote {
display: block;
position: relative;
font-size: var(--content-size-sm);
hyphens: auto;
/* note: our minimum desirable sidenote width is 15rem, and the gutters are symmetrical, so our
breakpoint between desktop/mobile will be content-width + 2(gap) + 2(15rem) + (scollbar buffer) */
@media(min-width: 89rem) {
--gap: 2.5rem;
--gutter-width: calc(50vw - var(--content-width) / 2);
--scrollbar-buffer: 1.5rem;
--sidenote-width: min(
24rem,
calc(var(--gutter-width) - var(--gap) - var(--scrollbar-buffer))
);
width: var(--sidenote-width);
float: right;
clear: right;
margin-right: calc(-1 * var(--sidenote-width) - var(--gap));
margin-bottom: 0.75rem;
}
@media(max-width: 89rem) {
position: fixed;
left: 0;
right: 0;
bottom: 0;
/* horizontal buffer for the counter and dismiss button */
--padding-x: calc(var(--content-padding) + 1.5rem);
padding: 1rem var(--padding-x);
background-color: var(--bg-color);
/* show the sidenote only when the corresponding checkbox is checked */
transform: translateY(calc(100% + 2rem));
transition: transform 0.125s;
/* when moving from shown -> hidden, ease-in */
transition-timing-function: ease-in;
.sidenote-toggle:checked + & {
border-top: 2px solid var(--accent-color);
transform: translateY(0);
/* when moving hidden -> shown, ease-out */
transition-timing-function: ease-out;
/* the active sidenote should be on top of any other sidenotes as well
(this isn't critical unless you have JS disabled, but it's still annoying) */
z-index: 20;
}
}
}
.content {
display: block;
max-width: var(--content-width);
margin: 0 auto;
}
.counter.anchor {
color: var(--accent-color);
font-size: 0.75em;
margin-left: 0.065rem;
position: relative;
bottom: 0.375rem;
@media(max-width: 89rem) {
&::before {
content: '[';
}
&::after {
content: ']';
}
}
}
.counter.floating {
position: absolute;
/* move it out to the left by its own width + a fixed gap */
transform: translateX(calc(-100% - 0.4em));
color: var(--accent-color);
}
.dismiss {
display: block;
@media(min-width: 89rem) {
display: none;
}
width: 2rem;
margin: 0.5rem auto 0;
color: var(--neutral-gray);
border-radius: 100%;
background: transparent;
border: 1px solid var(--neutral-gray);
padding: 0.25rem;
&:hover, &:active {
color: var(--accent-color);
border-color: var(--accent-color);
}
cursor: pointer;
& label {
cursor: pointer;
}
}
/* this is just to track the state of the mobile sidenote, it doesn't need to be seen */
.sidenote-toggle {
display: none;
}
</style>
+62
View File
@@ -0,0 +1,62 @@
---
import Icon from '@components/Icon.astro';
---
<div class="theme-switcher">
<button id="switch-to-dark">
<Icon name="sun" />
</button>
<button id="switch-to-light">
<Icon name="moon" />
</button>
</div>
<style>
.theme-switcher {
position: relative;
isolation: isolate;
width: 1.5rem;
height: 1.5rem;
transform: translateY(0.1rem);
}
button {
position: absolute;
inset: 0;
background-color: transparent;
padding: 0;
color: var(--nav-link-color);
border: none;
&:hover {
cursor: pointer;
color: var(--navbar-accent-color);
}
/* hide by default, i.e. if JS isn't enabled and the data-theme attribute didn't get set, */
display: none;
}
:global(html[data-theme="light"]) button#switch-to-dark {
display: block;
}
:global(html[data-theme="dark"]) button#switch-to-light {
display: block;
}
</style>
<script>
document.getElementById('switch-to-dark')?.addEventListener('click', () => {
localStorage.setItem('theme-preference', 'dark');
document.documentElement.dataset.theme = 'dark';
});
document.getElementById('switch-to-light')?.addEventListener('click', () => {
localStorage.setItem('theme-preference', 'light');
document.documentElement.dataset.theme = 'light';
})
</script>
+163
View File
@@ -0,0 +1,163 @@
<script setup lang="ts">
import type { MarkdownHeading } from 'astro';
import { onBeforeUnmount, onMounted, ref } from 'vue';
const props = defineProps<{ headings: MarkdownHeading[] }>();
// headings deeper than h3 don't display well because they are too deeply indented
const headings = props.headings.filter(h => h.depth <= 3);
// for each heading slug, track whether the corresponding heading is above the cutoff point
// (the cutoff point being a hypothetical line 2/3 of the way up the viewport)
let headingStatuses = Object.fromEntries(headings.map(h => ([h.slug, false])));
// we need to store a reference to the observer so we can dispose of it on resize/unmount
let headingObserver: IntersectionObserver | null = null;
// the final slug that should be highlighted as "current" in the TOC
let currentSlug = ref('');
function handleIntersectionUpdate(entries: IntersectionObserverEntry[], headingElems: HTMLElement[]) {
for (const entry of entries) {
const slug = entry.target.id;
const status = entry.isIntersecting;
headingStatuses[slug] = status;
}
// headings are in DOM order, so this gives us the last heading that's still above the cutoff point
for (const elem of headingElems) {
if (headingStatuses[elem.id]) {
currentSlug.value = elem.id;
}
else {
break;
}
}
}
function setupObserver() {
// if there was already an observer, turn it off
if (headingObserver) {
headingObserver.disconnect();
}
const headingElems = headings.map(h => document.getElementById(h.slug)!);
const obs = new IntersectionObserver(
entries => handleIntersectionUpdate(entries, headingElems),
// top margin equal to body height means that the intersection zone extends up beyond
// the top of the document, i.e. elements can only enter/leave the zone at the bottom
{ rootMargin: `${document.body.clientHeight}px 0px -66% 0px` },
);
for (const elem of headingElems) {
obs.observe(elem);
}
headingObserver = obs;
}
onMounted(() => {
// create the observer once on component startup
setupObserver();
// any time the window resizes, the document height could change, so we need to recreate it
window.addEventListener('resize', setupObserver);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', setupObserver);
headingObserver?.disconnect();
});
</script>
<template>
<div id="toc">
<h5>C<span class="lower">ontents</span></h5>
<ul id="toc-list">
<li
v-for="heading in headings"
:data-current="heading.slug == currentSlug"
:style="`--depth: ${heading.depth}`"
>
<span v-show="heading.slug == currentSlug" class="marker"></span>
<a :href="`#${heading.slug}`">
{{ heading.text }}
</a>
</li>
</ul>
</div>
</template>
<style scoped>
#toc {
position: sticky;
top: 1.5rem;
margin-left: 1rem;
margin-right: 4rem;
max-width: 18rem;
font-size: var(--content-size-sm);
color: var(--content-color-faded);
/*
minimum desirable TOC width is 8rem
add 4rem for margins, giving total gutter width of 12.5rem
multiply by 2 since there are two equally-sized gutters, then add content-width (52.5rem)
*/
@media(max-width: 77.5rem) {
display: none;
}
}
h5 {
font-variant: petite-caps;
font-weight: 350;
font-size: var(--content-size);
font-family: 'Figtree Variable';
color: var(--content-color-faded);
max-width: fit-content;
margin-top: 0;
margin-bottom: 0.35em;
/*padding-bottom: 0.25em;*/
border-bottom: 1px solid currentcolor;
/* make the border stretch beyond the text just a bit, because I like the effect */
padding-right: 1.5rem;
/* SmallCaps is an Astro component so we can't use it here, but we can fake it */
& .lower {
font-weight: 500;
}
}
li {
position: relative;
margin-top: 0.45em;
margin-left: calc(0.75em * (var(--depth) - 2));
font-size: var(--content-size-sm);
/* make sure that one item wrapped across multiple lines doesn't just look like multiple items */
line-height: 1.15;
&[data-current="true"], &:hover {
color: var(--content-color);
}
}
.marker {
position: absolute;
left: -0.6rem;
top: 0.05em;
bottom: 0.2em;
width: 0.125rem;
background-color: var(--accent-color);
}
a {
color: inherit;
text-decoration: none;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
</style>
+7
View File
@@ -0,0 +1,7 @@
---
import Heading from './Heading.astro';
---
<Heading tag="h1" {...Astro.props}>
<slot />
</Heading>
+7
View File
@@ -0,0 +1,7 @@
---
import Heading from './Heading.astro';
---
<Heading tag="h2" {...Astro.props}>
<slot />
</Heading>
+7
View File
@@ -0,0 +1,7 @@
---
import Heading from './Heading.astro';
---
<Heading tag="h3" {...Astro.props}>
<slot />
</Heading>
+7
View File
@@ -0,0 +1,7 @@
---
import Heading from './Heading.astro';
---
<Heading tag="h4" {...Astro.props}>
<slot />
</Heading>
+7
View File
@@ -0,0 +1,7 @@
---
import Heading from './Heading.astro';
---
<Heading tag="h5" {...Astro.props}>
<slot />
</Heading>
+7
View File
@@ -0,0 +1,7 @@
---
import Heading from './Heading.astro';
---
<Heading tag="h6" {...Astro.props}>
<slot />
</Heading>
+45
View File
@@ -0,0 +1,45 @@
---
import Icon from '@components/Icon.astro';
export interface Props extends astroHTML.JSX.HTMLAttributes {
tag: string,
}
const { tag: Tag, ...rest } = Astro.props;
---
<Tag {...rest}>
<slot />
<a href={`#${Astro.props.id}`} class="deep-link" >
<Icon name="link" height="0.9em" width="0.9em" display="inline" />
</a>
</Tag>
<style>
h1, h2, h3, h4, h5, h6 {
position: relative;
&:hover :global(svg) {
color: var(--accent-color);
}
}
a {
& :global(svg) {
color: var(--content-color-faded);
padding-bottom: 0.15em;
}
&:hover :global(svg), &:active :global(svg) {
color: var(--accent-color);
}
@media(min-width: 60rem) {
position: absolute;
top: 0.1em;
left: -1.25em;
padding-right: 0.5em;
}
}
</style>
+15
View File
@@ -0,0 +1,15 @@
import H1 from './H1.astro';
import H2 from './H2.astro';
import H3 from './H3.astro';
import H4 from './H4.astro';
import H5 from './H5.astro';
import H6 from './H6.astro';
export const headingElements = {
h1: H1,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
h6: H6,
};
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>

After

Width:  |  Height:  |  Size: 219 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
</svg>

After

Width:  |  Height:  |  Size: 219 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>

After

Width:  |  Height:  |  Size: 210 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
</svg>

After

Width:  |  Height:  |  Size: 353 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
</svg>

After

Width:  |  Height:  |  Size: 364 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>

After

Width:  |  Height:  |  Size: 204 B

+19
View File
@@ -0,0 +1,19 @@
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
const posts = defineCollection({
loader: glob({ pattern: '*.mdx', base: './posts' }),
schema: z.object({
title: z.string(),
date: z.date(),
draft: z.boolean().default(false),
dropcap: z.enum(['ascender', 'descender']).default('descender'),
toc: z.boolean().default(true),
})
});
export const collections = { posts };
+1
View File
@@ -0,0 +1 @@
declare var SIDENOTE_COUNT: number;
+90
View File
@@ -0,0 +1,90 @@
---
import '@styles/main.css';
import '@fontsource-variable/baskervville-sc';
import ThemeSwitcher from '@components/ThemeSwitcher.astro';
export interface Props {
pageTitle: string,
}
const { pageTitle } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{pageTitle}</title>
<!-- avoid FOUC by setting the color schme here in the header -->
<script>
const explicitPref = localStorage.getItem('theme-preference');
if (explicitPref) {
document.documentElement.dataset.theme = explicitPref;
} else {
const isLight = window.matchMedia('(prefers-color-scheme: light)').matches;
document.documentElement.dataset.theme = isLight ? 'light' : 'dark';
}
</script>
{/* Note: The styles are inside the document here because otherwise it breaks Astro's parsing */}
<style>
header {
background-color: var(--navbar-color);
padding: 0.5rem var(--content-padding);
}
nav {
max-width: var(--content-width);
margin: 0 auto;
display: flex;
gap: 1.5rem;
align-items: baseline;
& a {
font-family: 'Figtree Variable';
font-weight: 500;
font-size: 1.3rem;
color: var(--nav-link-color);
text-decoration: underline;
text-underline-offset: 0.5rem;
text-decoration-color: transparent;
transition: text-decoration-color 0.2s ease, opacity 0.2s ease;
&.home {
font-family: 'Baskervville SC Variable';
font-size: 2rem;
text-decoration-thickness: 0.125rem;
margin-right: auto;
}
&:hover, &:active {
text-decoration-color: var(--navbar-accent-color);
}
}
}
.switcher-container {
align-self: center;
}
</style>
</head>
<body>
<header>
<nav>
<a href="/" class="home" data-astro-prefetch>Joe's Blog</a>
<div class="switcher-container">
<ThemeSwitcher />
</div>
<a href="/posts" data-astro-prefetch>Posts</a>
<a href="/about" data-astro-prefetch>About</a>
</nav>
</header>
<main>
<slot />
</main>
</body>
</html>
-9
View File
@@ -1,9 +0,0 @@
<script>
let classes = '';
export {classes as class};
</script>
<p>Hello world!</p>
<pre class={classes}>
<slot></slot>
</pre>
-52
View File
@@ -1,52 +0,0 @@
<script>
// Usage: <Dropcap word="Lorem">ipsum dolor sit amet...</Dropcap>
export let word;
const initial = word.slice(0, 1).toUpperCase();
const remainder = word.slice(1);
// a few letters are narrower at the top, so we need to shift the remainder to compensate
const shiftValues = {
A: '-0.45em',
L: '-0.3em',
R: '-0.25em',
};
const shift = shiftValues[initial] || '0em';
</script>
<style>
@font-face {
font-family: 'Baskerville';
font-style: normal;
font-weight: 400;
src: url(/Baskerville-Regular.woff2) format('woff2');
font-display: block;
}
.drop-cap {
display: block;
float: left;
margin-right: 0.1em;
color: var(--accent-color);
font-size: calc(var(--content-size) * 1.5 * 2);
line-height: 0.8;
font-family: 'Baskerville';
text-transform: uppercase;
}
.first-word {
margin-left: var(--shift);
font-variant: petite-caps;
}
</style>
<svelte:head>
<link rel="preload" href="/Baskerville-Regular.woff2" as="font" type="font/woff2">
</svelte:head>
<span class="drop-cap">{initial}</span>
{#if remainder.length}
<span class="first-word" style:--shift={shift}>{remainder}</span>
{/if}
-68
View File
@@ -1,68 +0,0 @@
<script>
export let level;
export let id = '';
const tag = `h${level}`;
</script>
<style lang="scss">
.h {
position: relative;
}
// shift the anchor link to hang off the left side of the content when there's room
.anchor-wrapper {
// slightly overlap the span with the heading so that it doesn't
// lose its hover state as the cursor moves between them
position: absolute;
padding-right: 0.5em;
left: -1.25em;
@media(max-width: 58rem) {
position: revert;
}
}
a {
// works better to set the size here for line-height reasons
font-size: 0.9em;
// give the anchor link a faded appearance by default
color: hsl(0deg, 0%, 29%);
opacity: 40%;
transition: opacity 150ms, color 150ms;
&:hover {
border-bottom: 0.05em solid currentcolor;
}
}
// emphasize anchor link when heading is hovered or when clicked (the latter for mobile)
.h:hover a, .anchor-wrapper:hover a, .h a:active {
color: var(--accent-color);
opacity: 100%;
}
svg {
// undo the reset that makes images block
display: inline;
width: 1em;
// tiny tweak for optical alignment
transform: translateY(2px);
}
</style>
<svelte:element this={tag} {id} class="h">
<span>
<slot></slot>
</span>
<!-- Icon from https://heroicons.com/ -->
<span class="anchor-wrapper">
<a href="#{id}" >
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>
</a>
</span>
</svelte:element>
-44
View File
@@ -1,44 +0,0 @@
<script context="module">
import { page } from '$app/stores';
function host(url) {
try {
let u = new URL(url);
return u.host;
}
catch {
return null;
}
}
function ext(url) {
}
</script>
<script>
export let href;
export let rel = null;
let url = null;
try {
url = new URL(href);
}
catch {}
let isLocal = false;
if (href.startsWith('/') || url?.host === $page.url.host) {
isLocal = true;
}
// if href is not a valid url, assume that it's a relative link
const path = url?.pathname || href;
// set rel="external" on links to static files (i.e. local links with a dot in them)
if (isLocal && path.search(/\.\w+$/) > -1) {
rel = 'external';
}
</script>
<a data-sveltekit-preload-data={isLocal ? 'hover' : null} {href} {rel}>
<slot></slot>
</a>
-150
View File
@@ -1,150 +0,0 @@
<script context="module">
import '$styles/prose.scss';
import '$styles/code.scss';
import { onMount } from 'svelte';
import { formatDate } from './datefmt.js';
import { makeSlug } from '$lib/utils.js';
import Toc from './Toc.svelte';
import Link from './Link.svelte';
export { Link as a };
</script>
<script>
export let title, date;
export let description = '';
export const draft = false;
export let toc = null;
export let slug;
export let prev = null;
export let next = null;
</script>
<style lang="scss">
.page {
/* 3-column grid: left gutter, center content, and right gutter */
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, var(--content-width)) minmax(0, 1fr);
/* a bit of breathing room for narrow screens */
padding: 0 var(--content-padding);
}
/* container for the table of contents */
.left-gutter {
grid-column: 1 / 2;
justify-self: end;
}
.title {
grid-column: 2 / 3;
}
.subtitle {
font-size: 0.9em;
font-style: italic;
margin-top: -0.75rem;
}
.post {
grid-column: 2 / 3;
}
hr {
grid-column: 2 / 3;
width: 100%;
border-top: 1px solid hsl(0 0% 75%);
border-bottom: none;
margin: 2.5rem 0;
}
.footer {
grid-column: 2 / 3;
margin-bottom: 2.5rem;
display: flex;
& a {
display: flex;
align-items: center;
gap: 0.45em;
font-size: 1.25rem;
color: var(--content-color-faded);
text-decoration: underline;
text-underline-offset: 0.25em;
text-decoration-color: transparent;
transition: 150ms;
&:hover {
text-decoration-color: currentColor;
text-decoration: underline;
}
}
& svg {
width: 1em;
transition: 150ms;
}
& .prev:hover svg {
transform: translateX(-50%);
}
& .next:hover svg {
transform: translateX(50%);
}
}
</style>
<svelte:head>
<title>{title} | Joe's Blog</title>
<meta property="og:title" content="{title} | Joe's Blog">
<meta property="og:type" content="article">
<meta property="og:url" content="https://blog.jfmonty2.com/{slug}">
<meta property="og:description" content={description}>
<meta property="og:site_name" content="Joe's Blog">
<!-- Put this here for now, until I can get custom components working for codeblocks -->
<link rel="preload" href="/Hack-Regular.woff2" as="font" type="font/woff2">
</svelte:head>
<div class="page prose">
<div class="title">
<h1 id="{makeSlug(title)}">{title}</h1>
<p class="subtitle">{formatDate(date)}</p>
</div>
<div class="left-gutter">
{#if toc && toc.length !== 0}
<Toc items={toc} />
{/if}
</div>
<div class="post">
<slot></slot>
</div>
<hr>
<div class="footer">
{#if prev}
<a href="/{prev}" class="prev" data-sveltekit-preload-data="hover">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>
Previous
</a>
{/if}
{#if next}
<!-- we use margin-left rather than justify-content so it works regardless of whether the "previous" link exists -->
<a href="/{next}" class="next" style="margin-left: auto;" data-sveltekit-preload-data="hover">
<span style:vertical-align={'center'}>Next</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
</svg>
</a>
{/if}
</div>
</div>
-219
View File
@@ -1,219 +0,0 @@
<style lang="scss">
// minimum desirable sidenote width is 15rem, so breakpoint is
// content-width + 2(gap) + 2(15rem) + 2(scrollbar buffer)
$sidenote-breakpoint: 89.5rem;
// this has to be global because otherwise we can't target the body
:global(body) {
counter-reset: sidenote;
}
.counter.anchor {
color: #444;
margin-left: 0.065rem;
font-size: 0.75em;
position: relative;
bottom: 0.375rem;
color: var(--accent-color);
@media(max-width: $sidenote-breakpoint) {
&:hover {
color: var(--content-color);
cursor: pointer;
}
// only top-level anchors get brackets
&:not(.nested)::before {
content: '[';
}
&:not(.nested)::after {
content: ']';
}
}
}
.counter.floating {
position: absolute;
transform: translateX(calc(-100% - 0.4em));
color: var(--accent-color);
}
// hidden checkbox that tracks the state of the mobile sidenote
.sidenote-toggle {
display: none;
}
.sidenote {
// anchor the counter, which is absolutely positioned
position: relative;
color: #555;
font-size: var(--content-size-sm);
line-height: 1.25;
hyphens: auto;
// desktop display, this can't coexist with mobile styling
@media(min-width: $sidenote-breakpoint) {
// max sidenote width is 20rem, if the window is too small then it's
// the width of the gutter, minus the gap between sidenote and gutter,
// minus an extra 1.5rem to account for the scrollbar on the right
--gap: 2.5rem;
--gutter-width: calc(50vw - var(--content-width) / 2);
--sidenote-width: min(
24rem,
calc(var(--gutter-width) - var(--gap) - 1.5rem)
);
width: var(--sidenote-width);
float: right;
clear: right;
margin-right: calc(-1 * var(--sidenote-width) - var(--gap));
margin-bottom: 0.75rem;
}
@media(max-width: $sidenote-breakpoint) {
position: fixed;
left: 0;
right: 0;
bottom: 0;
// since headings have relative position, any that come after
// the current sidenote in the DOM get stacked on top by default
z-index: 10;
// give us a horizontal buffer for the counter and dismiss button
--padding-x: calc(var(--content-padding) + 1.5rem);
padding: 1rem var(--padding-x);
background-color: white;
box-shadow: 0 -2px 4px -1px rgba(0, 0, 0, 0.06), 0 -2px 12px -2px rgba(0, 0, 0, 0.1);
// show the sidenote only when the corresponding checkbox is checked
transform: translateY(calc(100% + 2rem));
transition: transform 125ms;
// when moving from shown -> hidden, ease-in
transition-timing-function: ease-in;
.sidenote-toggle:checked + & {
transform: translateY(0);
// when moving hidden -> shown, ease-out
transition-timing-function: ease-out;
// the active sidenote should be on top of any other sidenotes as well
// (this isn't critical unless you have JS disabled, but it's still annoying)
z-index: 20;
}
}
}
.sidenote-content {
max-width: var(--content-width);
margin: 0 auto;
&.nested {
margin-right: 0;
margin-top: 0.75rem;
margin-bottom: 0;
}
}
.dismiss {
display: block;
width: max-content;
margin: 0.5rem auto 0;
border-radius: 100%;
background: white;
border: 1px solid hsl(0deg, 0%, 75%);
box-shadow: 1px 1px 4px -1px rgba(0, 0, 0, 0.1);
padding: 0.25rem;
color: hsl(0deg, 0%, 50%);
&:hover, &:active {
color: var(--accent-color);
border: 1px solid var(--accent-color);
}
display: none;
@media(max-width: $sidenote-breakpoint) {
display: block;
}
cursor: pointer;
& label {
cursor: pointer;
}
& svg {
height: 1.5rem;
}
}
// nesting still needs work
/* @media(min-width: $sidenote-breakpoint) {
.nested.sidenote {
margin-right: 0;
margin-top: 0.7rem;
margin-bottom: 0;
}
} */
</style>
<script context="module">
import { writable } from 'svelte/store';
let activeSidenote = writable(null);
</script>
<script>
import { onMount } from 'svelte';
export let count;
let noteBody;
let nested = false;
onMount(() => {
// check to see if the parent node is also a sidenote, if so move this one to the end
let parentContent = noteBody.parentElement.closest('div.sidenote-content');
if (parentContent) {
// extract just the content of the nested note, ditch the rest (i.e. the button)
const noteContent = noteBody.firstChild;
noteBody.remove();
parentContent.appendChild(noteContent);
nested = true;
}
});
let toggle;
activeSidenote.subscribe(activeCount => {
// if we were the active toggle, but are no longer, hide
if (toggle?.checked && activeCount !== count) {
toggle.checked = false;
}
})
function toggleState() {
// if we are the active sidenote, deactivate us (upating the store will trigger subscription)
if ($activeSidenote === count) {
$activeSidenote = null;
}
// otherwise, we are becoming active
else {
$activeSidenote = count;
}
}
</script>
<label for={count} class="counter anchor" class:nested>{count}</label>
<input id={count} bind:this={toggle} on:click={toggleState} type="checkbox" class="sidenote-toggle" />
<!-- outer element so that on mobile it can extend the whole width of the viewport -->
<div class="sidenote" bind:this={noteBody}>
<!-- inner element so that content can be centered -->
<div class="sidenote-content" class:nested>
<span class="counter floating">{count}</span>
<slot></slot>
</div>
<button class="dismiss">
<label for={count}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</label>
</button>
</div>
-166
View File
@@ -1,166 +0,0 @@
<script>
import { onMount } from 'svelte';
import { makeSlug } from '$lib/utils.js';
export let items;
items.forEach(i => i.slug = makeSlug(i.text));
let headings = [];
let currentHeadingSlug = null;
let currentSubheadingSlug = null;
function setCurrentHeading() {
for (const h of headings) {
const yPos = h.getBoundingClientRect().y;
if (yPos > (window.innerHeight / 3)) {
break;
}
if (h.tagName === 'H2') {
currentHeadingSlug = h.id;
currentSubheadingSlug = null;
}
if (h.tagName === 'H3') {
currentSubheadingSlug = h.id
}
}
}
function ellipsize(text) {
return text;
// not sure about this, decide on it later
// if (text.length > 40) {
// text = text.slice(0, 40);
// // looks weird when we have an ellipsis following a space
// if (text.slice(-1) === ' ') {
// text = text.slice(0, -1);
// }
// return text + '…';
// }
// return text;
}
onMount (() => {
// These shouldn't change over the life of the page, so we can cache them
headings = Array.from(document.querySelectorAll('h2[id], h3[id]'));
setCurrentHeading();
});
</script>
<svelte:window on:scroll={setCurrentHeading} />
<style lang="scss">
#toc {
position: sticky;
top: 1.5rem;
margin-left: 1rem;
margin-right: 4rem;
max-width: 18rem;
color: var(--content-color-faded);
// minimum desirable TOC width is 8rem
// add 4rem for margins, giving total gutter width of 12.5rem
// multiply by 2 since there are two equally-sized gutters, then add content-width (52.5rem)
@media(max-width: 77.5rem) {
display: none;
}
}
// margin-left is to match the padding on the top-level list items,
// but here it needs to be margin so that the border is also shifted
h5 {
font-variant: petite-caps;
font-weight: 500;
max-width: fit-content;
margin-top: 0;
margin-bottom: 0.25em;
padding-bottom: 0.25em;
border-bottom: 1px solid currentcolor;
// make the border stretch beyond the text just a bit, because I like the effect
padding-right: 1.5rem;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
position: relative;
margin-top: 0.45em;
font-size: var(--content-size-sm);
// make sure that one item wrapped across multiple lines doesn't just looke like multiple items
line-height: 1.1;
&.depth-2 {
align-items: stretch;
margin-bottom: 0.2rem;
}
&.depth-3 {
align-items: center;
margin-bottom: 0.05rem;
}
&.current, &:hover {
color: var(--content-color);
}
}
.marker {
position: absolute;
left: -0.6rem;
.current &, li:hover & {
background-color: var(--accent-color);
}
&.bar {
width: 0.125rem;
height: 100%;
}
&.dot {
width: 0.2rem;
height: 0.2rem;
border-radius: 50%;
// vertically center within its containing block
top: 0;
bottom: 0;
margin: auto 0;
}
}
// default link styling messes everything up again
a {
color: inherit;
text-decoration: none;
}
</style>
<div id="toc">
<h5>
<span class="heading">Contents</span>
</h5>
<ul>
{#each items as item}
{#if item.depth === 2}
<li class="depth-2" class:current={item.slug === currentHeadingSlug} style:align-items="stretch">
<span class="marker bar"></span>
<a href="#{item.slug}">{ellipsize(item.text)}</a>
</li>
{:else if item.depth === 3}
<li class="depth-3" class:current={item.slug === currentSubheadingSlug} style:align-items="center" style:margin-left="0.75em">
<span class="marker dot"></span>
<a href="#{item.slug}">{ellipsize(item.text)}</a>
</li>
{/if}
{/each}
</ul>
</div>
+10
View File
@@ -0,0 +1,10 @@
import { getCollection } from 'astro:content';
export async function listPosts() {
// all posts in dev, exlucde draft posts in prod
let entries = await getCollection('posts', ({ data }) => import.meta.env.DEV || !data.draft);
// sort by date descending
entries.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
return entries;
}
+7 -11
View File
@@ -17,15 +17,11 @@ const weekdays = [
]; ];
export function formatDate(timestr) { export function formatDate(date: Date) {
const datestr = timestr.slice(0, 10); const year = date.getFullYear();
const [year, month, monthday] = datestr.split('-').map(n => parseInt(n)); const month = months[date.getMonth() - 1];
// for some reason the Date constructor expects the month index instead of ordinal const monthday = ordinals[date.getDate() - 1];
const weekdayIdx = new Date(year, month - 1, monthday).getDay(); const weekday = weekdays[date.getDay() - 1];
const names = {
month: months[month - 1], return `${weekday}, the ${monthday} of ${month}, A.D. ${year}`;
monthday: ordinals[monthday - 1],
weekday: weekdays[weekdayIdx],
}
return `${names.weekday}, the ${names.monthday} of ${names.month}, A.D. ${year}`;
} }
-8
View File
@@ -1,8 +0,0 @@
const nonAlphaNum = /[^A-Za-z0-9\-]/g;
const space = /\s+/g;
export function makeSlug(text) {
return text
.toLowerCase()
.replace(space, '-')
.replace(nonAlphaNum, '');
}
-94
View File
@@ -1,94 +0,0 @@
// const Node = {
// addChild(child) {
// this.children.push(child);
// return child;
// }
// }
export function tag(name, attrs, children) {
return {
type: 'tag',
tag: name,
attrs: attrs || {},
children: children || [],
addTag(name, attrs, children) {
const child = tag(name, attrs, children);
this.children.push(child);
return child;
},
};
}
export function text(content) {
return {
type: 'text',
text: content,
};
}
export function serialize(node, depth) {
if (!depth) {
depth = 0;
}
const indent = ' '.repeat(depth * 4);
let fragments = [];
// version tag, if this is the top level
if (depth === 0) {
fragments.push('<?xml version="1.0" encoding="UTF-8"?>\n')
}
fragments.push(`${indent}<${node.tag}`);
// this happens if there are multiple text nodes within the same parent
if (node.type === 'text') {
return `${indent}${escape(node.text)}`;
}
if (node.children === undefined) {
console.log(node);
}
// opening tag <element attr="value">
for (const attr in node.attrs) {
fragments.push(` ${attr}="${node.attrs[attr]}"`);
}
if (node.children.length === 0) {
fragments.push(' />');
return fragments.join('');
}
fragments.push('>');
// if the only child is a single text node, skip recursion and just dump contents directly
if (node.children.length === 1 && node.children[0].type === 'text') {
const text = escape(node.children[0].text);
fragments.push(text);
}
// otherwise, start a new line for each child node, then recurse
else {
for (const child of node.children) {
fragments.push('\n');
fragments.push(serialize(child, depth + 1));
}
// no need to verify that there were children, we already did that
fragments.push(`\n${indent}`);
}
fragments.push(`</${node.tag}>`);
return fragments.join('');
}
function escape(text) {
// we aren't going to bother with escaping attributes, so we won't worry about quotes
return text
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
}
+7
View File
@@ -0,0 +1,7 @@
import { defineMiddleware } from 'astro:middleware';
// set SIDENOTE_COUNT to 0 at the start of every request so that as sidenotes are rendered, it only counts them on the current page
export const onRequest = defineMiddleware((_context, next) => {
globalThis.SIDENOTE_COUNT = 0;
return next();
})
+28
View File
@@ -0,0 +1,28 @@
---
import { listPosts } from '@lib/content.ts';
import BaseLayout from '@layouts/BaseLayout.astro';
import Post from '@components/Post.astro';
export async function getStaticPaths() {
const entries = await listPosts();
// for each route, the page gets passed the entry itself, plus the previous and next slugs
// (if any), so that it can render links to them
return entries.map((entry, idx) => {
// entries are sorted in by date descending, so prev has a higher index and next has lower
const prevSlug = entries[idx + 1]?.id || null;
const nextSlug = entries[idx - 1]?.id || null;
return {
params: { slug: entry.id },
props: { entry, prevSlug, nextSlug },
}
});
}
const { entry, prevSlug, nextSlug } = Astro.props;
---
<BaseLayout pageTitle={entry.data.title}>
<Post {entry} {prevSlug} {nextSlug} />
</BaseLayout>
+24
View File
@@ -0,0 +1,24 @@
---
import BaseLayout from '@layouts/BaseLayout.astro';
---
<BaseLayout pageTitle="Joe's Blog - About">
<article class="prose">
<h1>About Me</h1>
<p>(Joe's wife wrote this because Joe feels weird writing about himself.)</p>
<p>Joe is a quirky, techy Tolkienite with a beautiful singing voice, an uncanny ability to do mental math, a bony, un-cuddleable frame, and a big mushy heart. He enjoys bike riding, computers, watching TV, reading about computers, playing Breath of the Wild, building computers, talking about something called "programming languages", and spending time with his family (which often involves fixing their computers). He graduated with a Liberal Arts degree from Thomas Aquinas College, the school of his forebears. He often remarks that he has greatly benefitted from the critical thinking skills he acquired at his alma mater in his current line of work.</p>
<p>He has spent, at the current time, about 2 years working on this blog. Most of his posts are about all of the work it took and everything he learned making this blog. Unlike most "bloggers", he has started with many blog posts and no blog, rather than a blog without posts. "Someday", he says, "I will actually get that blog up". I always nod encouragingly.</p>
<p>If you are reading this, then that day has arrived. We hope you enjoy it, and maybe even learn something along the way.</p>
</article>
</BaseLayout>
<style>
article {
max-width: var(--content-width);
margin: 0 auto;
}
</style>
+14
View File
@@ -0,0 +1,14 @@
---
import { listPosts } from '@lib/content.ts';
import BaseLayout from '@layouts/BaseLayout.astro';
import Post from '@components/Post.astro';
const entries = await listPosts();
// there will always be at leaste one entry
const entry = entries[0]!;
const prevSlug = entries[1] ? entries[1]?.id : null;
---
<BaseLayout pageTitle={entry.data.title}>
<Post {entry} {prevSlug} />
</BaseLayout>
+80
View File
@@ -0,0 +1,80 @@
---
import '@styles/prose.css';
import { render } from 'astro:content';
import { listPosts } from '@lib/content.ts';
import BaseLayout from '@layouts/BaseLayout.astro';
// return all posts in dev, only non-draft in prod
const entries = await listPosts();
const posts = await Promise.all(entries.map(async entry => {
const { remarkPluginFrontmatter } = await render(entry);
return { ...entry, remarkPluginFrontmatter };
}));
---
<BaseLayout pageTitle="Posts">
<div class="prose">
<h1>All Posts</h1>
<ul>
{posts.map( (p, idx) =>
<li>
{idx > 0 && <hr></hr>}
<h2>
<a href={`/${p.id}`} data-astro-prefetch>
{p.data.title}
</a>
{p.data.draft && <span class="draft-notice">Draft</span>}
</h2>
<p>
{p.remarkPluginFrontmatter.description}
</p>
</li>
)}
</ul>
</div>
</BaseLayout>
<style>
div.prose {
max-width: var(--content-width);
padding: var(--content-padding);
margin: 0 auto;
& h2 a {
color: inherit;
}
}
h1 {
text-align: center;
margin-bottom: 2rem;
}
ul {
list-style: none;
margin-left: 0;
}
h2 {
display: flex;
gap: 01rem;
align-items: center;
}
.draft-notice {
font-size: 0.5em;
font-family: 'Figtree Variable', sans-serif;
color: var(--nav-link-color);
background-color: var(--accent-color);
padding: 0.05em 0.5em;
border-radius: 20% / 50%;
}
hr {
border: none;
height: 1px;
margin: 2.5rem 0;
background-color: var(--content-color);
}
</style>
+33
View File
@@ -0,0 +1,33 @@
import type { Root, Paragraph, PhrasingContent } from 'mdast';
import type { VFile } from 'vfile';
import { visit } from 'unist-util-visit';
import { toString } from 'mdast-util-to-string';
export function remarkDescription() {
return (tree: Root, vfile: VFile) => {
let description: string | null = null;
visit(tree, 'paragraph', (node: Paragraph) => {
if (description !== null) return;
description = summarize(node);
});
// Astro exposes this as `remarkPluginFrontmatter` from render()
const astro = (vfile.data.astro ??= { frontmatter: {} }) as { frontmatter: Record<string, unknown> };
astro.frontmatter.description = description;
};
}
/**
* Convert a paragraph node to a plain-text string, stripping any
* MDX JSX elements (e.g. <Sidenote>) so their content doesn't
* leak into the summary.
*/
function summarize(par: Paragraph): string {
const filtered = par.children.filter(
(child: PhrasingContent) => !(child.type as string).startsWith('mdxJsx')
);
return toString({ type: 'paragraph', children: filtered });
}
-116
View File
@@ -1,116 +0,0 @@
import { visit, CONTINUE, EXIT, SKIP, } from 'unist-util-visit';
import { find } from 'unist-util-find';
import { toText } from 'hast-util-to-text';
import { makeSlug } from '../lib/utils.js';
import {writeFileSync} from 'node:fs';
import {toHtml} from 'hast-util-to-html';
export function localRehype() {
return (tree, vfile) => {
const needsDropcap = vfile.data.fm.dropcap !== false
let dropcapAdded = false;
let sidenotesCount = 0;
let moduleScript;
let imports = new Set();
if (needsDropcap) {
imports.add("import Dropcap from '$lib/Dropcap.svelte';");
}
visit(tree, node => {
// add slugs to headings
if (isHeading(node)) {
processHeading(node);
imports.add("import Heading from '$lib/Heading.svelte';");
return SKIP;
}
// mdsvex adds a <script context="module"> so we just hijack that for our own purposes
if (isModuleScript(node)) {
moduleScript = node;
}
// convert first letter/word of first paragraph to <Dropcap word="{whatever}">
if (needsDropcap && !dropcapAdded && isParagraph(node)) {
addDropcap(node);
dropcapAdded = true;
}
// add `count` prop to each <Sidenote> component
if (isSidenote(node)) {
// increment the counter first so that the count starts at 1
sidenotesCount += 1;
addSidenoteCount(node, sidenotesCount);
}
});
// insert our imports at the top of the `<script context="module">` tag
if (imports.size > 0) {
const script = moduleScript.value;
// split the script where the opening tag ends
const i = script.indexOf('>');
const openingTag = script.slice(0, i + 1);
const remainder = script.slice(i + 1);
// mdvsex uses tabs so we will as well
const importScript = Array.from(imports).join('\n\t');
moduleScript.value = `${openingTag}\n\t${importScript}${remainder}`;
}
// const name = vfile.filename.split('/').findLast(() => true);
// writeFileSync(`scratch/${name}.json`, JSON.stringify(tree, undefined, 4));
}
}
function processHeading(node) {
const level = node.tagName.slice(1);
node.tagName = 'Heading';
node.properties.level = level;
node.properties.id = makeSlug(toText(node));
}
function addDropcap(par) {
let txtNode = find(par, {type: 'text'});
const i = txtNode.value.search(/\s/);
const firstWord = txtNode.value.slice(0, i);
const remainder = txtNode.value.slice(i);
par.children.unshift({
type: 'raw',
value: `<Dropcap word="${firstWord}" />`,
});
txtNode.value = remainder;
}
function addSidenoteCount(node, count) {
// get the index of the closing >
const i = node.value.search(/>\s*$/);
if (i < 0) {
throw new Error('Failed to add counter to element, closing angle bracket not found.');
}
// splice in the count prop
node.value = `${node.value.slice(0, i)} count={${count}}>`;
}
function isHeading(node) {
return node.type === 'element' && node.tagName.match(/h[1-6]/);
}
function isModuleScript(node) {
return node.type === 'raw' && node.value.match(/^<script context="module">/);
}
function isParagraph(node) {
return node.type === 'element' && node.tagName === 'p';
}
function isSidenote(node) {
return node.type === 'raw' && node.value.match(/<\s*Sidenote/);
}
-50
View File
@@ -1,50 +0,0 @@
import { visit } from 'unist-util-visit';
import { toString } from 'mdast-util-to-string';
import fs from 'node:fs';
// build table of contents and inject into frontmatter
export function localRemark() {
return (tree, vfile) => {
let toc = [];
let description = null;
visit(tree, ['heading', 'paragraph'], node => {
// build table of contents and inject into frontmatter
if (node.type === 'heading' && vfile.data.fm.toc !== false) {
toc.push({
text: toString(node),
depth: node.depth,
});
}
// inject description (first 25 words of the first paragraph)
if (node.type === 'paragraph' && description === null) {
description = summarize(node);
}
});
if (vfile.data.fm.toc !== false) {
vfile.data.fm.toc = toc;
}
vfile.data.fm.description = description;
}
}
// convert paragraph to single string after stripping everything between html tags
function summarize(par) {
let newChildren = [];
let push = true;
for (const child of par.children) {
if (child.type === 'html') {
push = !push;
continue;
}
if (push) {
newChildren.push(child);
}
}
return toString({type: 'paragraph', children: newChildren});
}
-169
View File
@@ -1,169 +0,0 @@
<script context="module">
import { writable } from 'svelte/store';
let activePreview = writable(null);
</script>
<script>
import { tick } from 'svelte';
import data from './books.json';
const images = import.meta.glob('./images/*.jpg', {eager: true});
export let ref;
const {type, title, author, description, url} = data[ref];
const imageUrl = images[`./images/${ref}.jpg`].default;
$: visible = $activePreview === ref;
let mousePresent = false;
let offset, popover;
async function show() {
$activePreview = ref;
mousePresent = true;
await tick();
const rect = popover.getBoundingClientRect();
// 12px is approximately var(--content-padding)
if (rect.x < 12) {
offset = `${12 - rect.x}px`;
}
}
function hide() {
mousePresent = false;
// mouseenter fires when the mouse moves into the floating div as well,
// so this gives us a "grace period" that applies to either anchor or popover
setTimeout(
() => {
if (!mousePresent && $activePreview === ref) {
$activePreview = null;
}
},
300
);
}
function clickLink(evt) {
// if click happened without hover, then we must be on mobile
if (!visible) {
$activePreview = ref;
evt.preventDefault();
}
// if visible, but mouse is not present, also mobile
else if (visible && !mousePresent) {
$activePreview = null;
evt.preventDefault();
}
}
let detailsLink;
function blurLink(evt) {
// do this in the next task, in case the click was inside the popover
setTimeout(
() => {
// check this here in case it got changed by a different event handler
if ($activePreview == ref) {
$activePreview = null;
}
},
0
)
// if ($activePreview == ref) {
// setTimeout(() => $activePreview = null, 0);
// }
}
</script>
<style lang="scss">
.base {
position: relative;
// on mobile, we want the popover's position to be calculated
// relative to the whole document, not the link text
@media(max-width: 27rem) {
position: static;
}
}
.popover {
position: absolute;
// popover should float above the link text by a bit
bottom: calc(100% + 0.5rem);
// and be centered relative to the link, unless that would put it off screen
left: 50%;
transform: translateX(
calc(-50% + var(--offset, 0px))
);
@media(max-width: 27rem) {
// bounding box is now the whole document
// we want to start from its initial vertical position
bottom: unset;
// center it horizontally, with some space on the sides
width: unset;
left: 1rem;
right: 1rem;
margin-left: auto;
margin-right: auto;
// and move it back up so it's above the text again
transform: translateY(
calc(-100% - 1.5em - 0.5rem)
);
}
// visibility is controlled by the .visible class
display: none;
&.visible {
display: flex;
}
// two-column layout, one for image and one for text
gap: 1rem;
width: 25rem;
height: 192px;
overflow-y: auto;
padding: 0.35rem;
background: white;
box-shadow: 1px 2px 6px rgba(0, 0, 0, 0.1);
border: 1px solid var(--content-color);
z-index: 1;
font-size: var(--content-size-sm);
}
img {
height: 100%;
// sticky position ensures that the image stays visible when we scroll the text
position: sticky;
top: 0;
}
a.details {
color: var(--primary-color);
&:visited {
color: var(--accent-color);
}
}
a:active {
color: var(--accent-color);
}
</style>
<span class="base" on:mouseenter={show} on:mouseleave={hide}><!-- get rid of whitespace
--><a href={url} target="_blank" on:click={clickLink} on:blur={blurLink}>
<slot></slot><!--
--></a><!--
--><div class="popover" bind:this={popover} class:visible style:--offset={offset}>
<img src={imageUrl}>
<div>
<h4>{title}</h4>
<p>
{description}
<a class="details" href={url} target="_blank" bind:this={detailsLink}>More</a>
</p>
</div>
</div><!--
--></span>
-23
View File
@@ -1,23 +0,0 @@
{
"lotr": {
"type": "trilogy",
"title": "The Lord of the Rings",
"author": "J. R. R. Tolkien",
"description": "Epic fantasy trilogy written by Oxford professor and linguist J. R. R. Tolkien. Considered by many to be the major trend-setter for the modern fantasy genre.",
"url": "https://www.goodreads.com/series/66175-the-lord-of-the-rings"
},
"neverwhere": {
"type": "book",
"title": "Neverwhere",
"author": "Neil Gaiman",
"description": "Under the streets of London there's a world most people could never dream of. A city of monsters and saints, murderers and angels, knights in armour and pale girls in black velvet. \"Neverwhere\" is the London of the people who have fallen between the cracks.",
"url": "https://www.goodreads.com/book/show/14497.Neverwhere"
},
"earthsea": {
"type": "series",
"title": "Earthsea Cycle",
"author": "Ursula K. Le Guin",
"description": "Series of high fantasy stories set in an archipelago world where names are power and dragons roam the skies.",
"url": "https://www.goodreads.com/series/40909-earthsea-cycle"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

@@ -1,8 +1,16 @@
<script> ---
export let floatingCounter = true; export interface Props {
export let classes = ''; floatingCounter: boolean,
export {classes as class}; class: string,
</script> };
const { class: classList, floatingCounter} = Astro.props;
---
<span class="counter"></span>
<span class={`sidenote ${classList} ${floatingCounter ? 'floatingCounter' : ''}`}>
<slot></slot>
</span>
<style> <style>
:global(body) { :global(body) {
@@ -29,7 +37,7 @@
content: counter(unstyled-sidenote); content: counter(unstyled-sidenote);
font-size: 0.75rem; font-size: 0.75rem;
color: #0083c4; color: #0083c4;
/* Since the sidenote is floated it counts as a positioned element, /* Since the sidenote is floated it counts as a positioned element,
so this would make the counter overlap the start of the text... */ so this would make the counter overlap the start of the text... */
position: absolute; position: absolute;
/* ...except that we move it out to the left and up a bit, so /* ...except that we move it out to the left and up a bit, so
@@ -41,8 +49,3 @@
); );
} }
</style> </style>
<span class="counter"></span>
<span class="sidenote {classes}" class:floatingCounter={floatingCounter}>
<slot></slot>
</span>
+51
View File
@@ -0,0 +1,51 @@
.counter {
counter-increment: sidenote;
}
.counter::before {
content: counter(sidenote);
color: var(--accent-color);
font-size: 0.75rem;
position: relative;
bottom: 0.2rem;
margin-left: 0.1rem;
}
.sidenote-absolute {
position: absolute;
left: calc(50% + min(100%, var(--content-width)) / 2 + 1rem);
max-width: 12rem;
font-size: 0.75rem;
}
.sidenote-absolute::before {
content: counter(sidenote);
color: var(--accent-color);
font-size: 0.75rem;
position: relative;
bottom: 0.2rem;
margin-right: 0.1rem;
}
.sn-float {
float: right;
}
.sn-clear {
float: right;
clear: right;
}
.sn-gutter {
float: right;
width: 14rem;
margin-right: -14rem;
}
.sn-gap {
float: right;
width: 14rem;
margin-right: -16rem;
}
.sn-var-width {
float: right;
--width: min(14rem, calc(50vw - var(--content-width) / 2) - 2rem);
width: var(--width);
margin-right: calc(0rem - var(--width) - 2rem);
}
-2
View File
@@ -1,2 +0,0 @@
import '../styles/main.scss';
export const prerender = true;
-40
View File
@@ -1,40 +0,0 @@
<style lang="scss">
.header {
background: var(--primary-color-faded);
}
nav {
max-width: 30rem;
margin: 0 auto;
display: flex;
justify-content: space-between;
& a {
flex: 1;
max-width: 8rem;
padding: 0.25rem 1rem;
font-size: 1.75rem;
color: white;
text-decoration: none;
text-align: center;
&:hover {
background: hsl(0deg 0% 0% / 10%);
}
}
}
</style>
<div class="header">
<nav>
<a data-sveltekit-preload-data="hover" href="/">Home</a>
<a data-sveltekit-preload-data="hover" href="/posts">Posts</a>
<a data-sveltekit-preload-data="hover" href="/about">About</a>
</nav>
</div>
<main>
<slot></slot>
</main>
-8
View File
@@ -1,8 +0,0 @@
export async function load({ data }) {
let post = await import(`./_posts/${data.slug}.svx`);
post.metadata.slug = data.slug;
post.metadata.next = data.next;
return {
post: post.default,
}
}
-11
View File
@@ -1,11 +0,0 @@
import { postData, siblingPosts } from './_posts/all.js';
// this is in a "servserside" loader so that we don't end up embedding the metadata
// for every post into the final page
export function load() {
return {
slug: postData[0].slug,
next: postData[1].slug,
};
}
-5
View File
@@ -1,5 +0,0 @@
<script>
export let data;
</script>
<svelte:component this={data.post} />
-14
View File
@@ -1,14 +0,0 @@
<style>
h1 {
margin-top: 6rem;
}
h1, p {
text-align: center;
}
</style>
<h1>404</h1>
<p>That page doesn't exist. Sorry!</p>
-24
View File
@@ -1,24 +0,0 @@
import { error } from '@sveltejs/kit';
export async function load({ url, params, data }) {
let post;
try {
post = await import(`../_posts/${params.slug}.svx`);
}
catch (err) {
if (err.message.match(/Unknown variable dynamic import/)) {
throw error(404, `Not found: ${url.pathname}`);
}
else {
throw err;
}
}
post.metadata.slug = params.slug;
post.metadata.prev = data.prev;
post.metadata.next = data.next;
return {
post: post.default,
}
}
-10
View File
@@ -1,10 +0,0 @@
import { postData } from '../_posts/all.js';
export function load({ params }) {
const i = postData.findIndex(p => p.slug === params.slug);
return {
prev: i < postData.length - 1 ? postData[i + 1].slug : null,
next: i > 0 ? postData[i - 1].slug : null,
};
}
-5
View File
@@ -1,5 +0,0 @@
<script>
export let data;
</script>
<svelte:component this={data.post} />
-26
View File
@@ -1,26 +0,0 @@
import { dev } from '$app/environment';
const posts = import.meta.globEager('./*.svx');
let postData = [];
for (const path in posts) {
// skip draft posts in production mode
if (!dev && posts[path].metadata.draft) {
continue;
}
// slice off the ./ and the .svx
const slug = path.slice(2, -4);
posts[path].metadata.slug = slug;
postData.push(posts[path].metadata);
}
postData.sort((a, b) => {
// sorting in reverse, so we flip the intuitive order
if (a.date > b.date) return -1;
if (a.date < b.date) return 1;
return 0;
});
export { postData };
-27
View File
@@ -1,27 +0,0 @@
<script>
import '$styles/prose.scss';
</script>
<style>
.content {
max-width: var(--content-width);
margin: 0 auto;
}
</style>
<svelte:head>
<title>About Me | Joe's Blog</title>
</svelte:head>
<div class="prose content">
<h1>About Me</h1>
<p>(Joe's wife wrote this because Joe feels weird writing about himself.)</p>
<p>Joe is a quirky, techy Tolkienite with a beautiful singing voice, an uncanny ability to do mental math, a bony, un-cuddleable frame, and a big mushy heart. He enjoys bike riding, computers, watching TV, reading about computers, playing Breath of the Wild, building computers, talking about something called "programming languages", and spending time with his family (which often involves fixing their computers). He graduated with a Liberal Arts degree from Thomas Aquinas College, the school of his forebears. He often remarks that he has greatly benefitted from the critical thinking skills he acquired at his alma mater in his current line of work.</p>
<p>He has spent, at the current time, about 2 years working on this blog. Most of his posts are about all of the work it took and everything he learned making this blog. Unlike most "bloggers", he has started with many blog posts and no blog, rather than a blog without posts. "Someday", he says, "I will actually get that blog up". I always nod encouragingly.</p>
<p>If you are reading this, then that day has arrived. We hope you enjoy it, and maybe even learn something along the way.</p>
</div>
-53
View File
@@ -1,53 +0,0 @@
import { tag, text, serialize } from '$lib/xml.js';
import { postData } from '../_posts/all.js';
export const prerender = true;
export function GET() {
return new Response(renderFeed(), {
headers: {'Content-Type': 'application/atom+xml'}
});
}
function renderFeed() {
const feed = tag('feed', {xmlns: 'http://www.w3.org/2005/Atom'});
feed.addTag('id', {}, [text('https://blog.jfmonty2.com/')])
feed.addTag('title', {}, [text("Joe's Blog")]);
feed.addTag('link', {rel: 'alternate', href: 'https://blog.jfmonty2.com/'});
feed.addTag('link', {rel: 'self', href: 'https://blog.jfmonty2.com/feed/'});
const lastUpdate = iso(postData[0].updated || postData[0].date);
feed.addTag('updated', {}, [text(lastUpdate)]);
const author = feed.addTag('author');
author.addTag('name', {}, [text('Joseph Montanaro')]);
for (const post of postData) {
const url = `https://blog.jfmonty2.com/${post.slug}`
const entry = feed.addTag('entry');
entry.addTag('title', {}, [text(post.title)]);
entry.addTag('link', {rel: 'alternate', href: url});
entry.addTag('id', {}, [text(url)]);
const publishedDate = iso(post.date);
entry.addTag('published', {}, [text(publishedDate)])
const updatedDate = iso(post.updated || post.date);
entry.addTag('updated', {}, [text(updatedDate)]);
entry.addTag('content', {type: 'html'}, [text(renderDescription(post))]);
}
return serialize(feed);
}
function renderDescription(post) {
return `<p>${post.description} <a href="https://blog.jfmonty2.com/${post.slug}">Read more</a></p>`;
}
function iso(datetimeStr) {
return new Date(datetimeStr).toISOString();
}
-86
View File
@@ -1,86 +0,0 @@
<script>
import '$styles/prose.scss';
import { formatDate } from '$lib/datefmt.js';
import { postData } from '../_posts/all.js';
</script>
<style lang="scss">
.wrapper {
padding: 0 var(--content-padding);
}
.posts {
max-width: var(--content-width);
margin: 0 auto;
}
hr {
margin: 2.5rem 0;
border-color: #eee;
}
.post-date {
color: var(--content-color-faded);
}
.draft-notice {
vertical-align: middle;
font-size: 0.75rem;
padding: 0 0.3rem;
color: #e00;
background-color: #ffd9d9;
border: 1px solid red;
border-radius: 20% / 50%;
}
.post-link {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
h2 {
display: flex;
align-items: center;
gap: 0.75rem;
margin-top: 0.5rem;
margin-bottom: 0.75rem;
font-size: 1.5rem;
& a {
color: currentcolor;
}
}
</style>
<svelte:head>
<title>Posts</title>
</svelte:head>
<div class="wrapper">
<div class="posts prose">
<h1 style:text-align="center">All Posts</h1>
{#each postData as post, idx}
<div class="post">
<div class="post-date">{new Date(post.date).toISOString().split('T')[0]}</div>
<h2 class="prose">
<a data-sveltekit-preload-data="hover" class="post-link" href="/{post.slug}">
{post.title}
</a>
{#if post.draft}
<span class="draft-notice">Draft</span>
{/if}
</h2>
<p>{post.description}</p>
</div>
{#if idx < postData.length - 1}
<hr>
{/if}
{/each}
</div>
</div>
+41
View File
@@ -0,0 +1,41 @@
@import '@fontsource-variable/fira-code';
:not(pre) > code {
font-family: 'Fira Code Variable';
font-size: var(--content-size-sm);
padding: 0.05rem 0.2rem 0.1rem;
border-radius: 0.2rem;
color: var(--inline-code-color);
background-color: var(--inline-code-bg);
}
pre.astro-code {
padding: 1em;
margin: 1em 0;
border-radius: 0.3em;
font-size: var(--content-size-sm);
--shiki-dark-font-weight: 500;
}
:root[data-theme="dark"] .astro-code,
:root[data-theme="dark"] .astro-code span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
/* Optional, if you also want font styles */
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
@media(prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .astro-code,
:root:not([data-theme="light"]) .astro-code span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
/* Optional, if you also want font styles */
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
}
-27
View File
@@ -1,27 +0,0 @@
@import 'prism-dracula';
@font-face {
font-family: 'Hack';
font-style: normal;
font-weight: 400;
src: url(/Hack-Regular.woff2) format('woff2');
font-display: block;
}
code {
padding: 0.05rem 0.2rem 0.1rem;
background: #eee;
border-radius: 0.2rem;
font-size: 0.75em;
font-family: 'Hack', monospace;
}
pre[class*="language-"] {
line-height: 1.25;
}
pre > code[class*="language-"] {
font-size: 0.75em;
font-family: 'Hack', monospace;
}
+24
View File
@@ -0,0 +1,24 @@
@import '@fontsource-variable/figtree';
@import 'reset.css';
@import 'vars.css';
@import 'prose.css';
body {
font-family: 'Figtree Variable', sans-serif;
font-weight: 350;
font-size: var(--content-size);
line-height: var(--content-line-height);
color: var(--content-color);
background-color: var(--bg-color);
}
a {
color: var(--link-color);
text-decoration: none;
&:hover {
text-decoration: underline;
}
&:visited {
color: var(--link-color-visited);
}
}
-39
View File
@@ -1,39 +0,0 @@
@import 'reset';
@font-face {
font-family: 'Tajawal';
font-style: normal;
font-weight: 400;
src: url(/Tajawal-Regular.woff2) format('woff2');
font-display: block;
}
:root {
--content-size: 1.25rem;
--content-size-sm: 1rem;
--content-line-height: 1.4;
--content-width: 52.5rem;
--content-padding: 0.65rem;
--content-color: #1e1e1e;
--content-color-faded: #555;
--primary-color: hsl(202deg 72% 28%);
--primary-color-faded: hsl(202deg 14% 36%);
--accent-color: hsl(0deg, 92%, 29%);
--accent-color-faded: hsl(0deg, 25%, 55%);
@media(max-width: 640px) {
--content-line-height: 1.25;
--content-size: 1.15rem;
--content-size-sm: 0.9rem;
}
}
body {
font-family: 'Tajawal', sans-serif;
font-size: var(--content-size);
line-height: var(--content-line-height);
letter-spacing: -0.005em;
color: var(--content-color);
}
-121
View File
@@ -1,121 +0,0 @@
/**
* Dracula Theme originally by Zeno Rocha [@zenorocha]
* https://draculatheme.com/
*
* Ported for PrismJS by Albert Vallverdu [@byverdu]
*/
code[class*="language-"],
pre[class*="language-"] {
color: #f8f8f2;
background: none;
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: 1em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #282a36;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #6272a4;
}
.token.punctuation {
color: #f8f8f2;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #ff79c6;
}
.token.boolean,
.token.number {
color: #bd93f9;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #50fa7b;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #f8f8f2;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.class-name {
color: #f1fa8c;
}
.token.keyword {
color: #8be9fd;
}
.token.regex,
.token.important {
color: #ffb86c;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
+15 -10
View File
@@ -1,30 +1,35 @@
@import '@fontsource-variable/baskervville';
.prose { .prose {
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, Arial, sans-serif; font-family: 'Baskervville Variable', serif;
font-weight: 600; font-weight: 650;
color: #464646; margin-bottom: 0.25rem;
color: var(--heading-color);
letter-spacing: 0.015em;
line-height: 1.25
} }
h1 { h1 {
margin-top: 0.5em; margin-top: 0.5em;
font-size: 2em; font-size: 2.25em;
font-variant: petite-caps;
} }
h2 { h2 {
font-size: 1.5em; font-size: 1.75em;
} }
h3 { h3 {
font-size: 1.2em; font-size: 1.4em;
} }
h4 { h4 {
font-size: 1.1em; font-size: 1.2em;
} }
h1, h2, h3, h4 { h5, h6 {
margin-bottom: 0.5em; font-size: 1em;
font-weight: 700;
} }
p, ul, ol { p, ul, ol {
+22
View File
@@ -0,0 +1,22 @@
/* This reset lifted largely from Josh Comeau's "CSS for JS Devs" course */
/* Use a more-intuitive box-sizing model. */
*, *::before, *::after {
box-sizing: border-box;
}
/* Remove default margin */
* {
margin: 0;
}
/* Allow percentage-based heights in the application */
html, body {
min-height: 100%;
}
/* Improve media defaults */
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
-22
View File
@@ -1,22 +0,0 @@
// This reset lifted largely from Josh Comeau's "CSS for JS Devs" course
// Use a more-intuitive box-sizing model.
*, *::before, *::after {
box-sizing: border-box;
}
// Remove default margin
* {
margin: 0;
}
// Allow percentage-based heights in the application
html, body {
min-height: 100%;
}
// Improve media defaults
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
+93
View File
@@ -0,0 +1,93 @@
:root {
--content-size: 1.25rem;
--content-size-sm: 1rem;
--content-line-height: 1.5;
--content-width: 52.5rem;
--content-padding: 0.65rem;
/* squish things down a little on mobile so more text fits on the screen */
@media(max-width: 640px) {
--content-line-height: 1.25;
--content-size: 1.15rem;
--content-size-sm: 0.9rem;
}
/* light-mode colors (warm "paper" theme, harmonized with dark mode) */
--bg-color: hsl(40deg 30% 97%);
/* text */
--content-color: hsl(30deg 12% 22%);
--content-color-faded: hsl(30deg 8% 38%);
/* links */
--primary-color: hsl(202deg 55% 32%);
/* indicators, hover effects, etc */
--accent-color: hsl(18deg 75% 45%);
--accent-color-faded: hsl(14deg 35% 50%);
/* misc */
--navbar-color: hsl(220deg 12% 34%);
--navbar-accent-color: hsl(18deg 80% 55%);
--heading-color: hsl(30deg 18% 25%);
--link-color: var(--primary-color);
--link-color-visited: var(--accent-color-faded);
--nav-link-color: white;
--neutral-gray: hsl(30deg 6% 32%);
--inline-code-bg: hsl(40deg 22% 91%);
--inline-code-color: var(--content-color);
/* dark-mode colors (defined here so that we only have to update them in one place) */
--dark-bg-color: hsl(220deg 10% 13%);
--dark-content-color: hsl(30deg 10% 75%);
--dark-content-color-faded: hsl(25deg 6% 50%);
--dark-primary-color: hsl(220deg 15% 40%);
--dark-accent-color: hsl(18deg 70% 55%);
--dark-accent-color-faded: hsl(18deg 30% 45%);
--dark-navbar-color: hsl(220deg 12% 18%);
/* dark mode navbar is much close to the page background,
so it doesn't need a separate accent color like light mode */
--dark-navbar-accent-color: var(--dark-accent-color);
--dark-heading-color: hsl(35deg 25% 88%);
--dark-link-color: hsl(202deg 50% 50%);
--dark-link-color-visited: hsl(270deg 33% 55%);
--dark-nav-link-color: var(--dark-heading-color);
--dark-neutral-gray: hsl(220deg 10% 45%);
--dark-inline-code-bg: hsl(220deg 7.5% 21%);
--dark-inline-code-color: hsl(18deg 10% 90%);
&[data-theme="dark"] {
--bg-color: var(--dark-bg-color);
--content-color: var(--dark-content-color);
--content-color-faded: var(--dark-content-color-faded);
--primary-color: var(--dark-primary-color);
--accent-color: var(--dark-accent-color);
--accent-color-faded: var(--dark-accent-color-faded);
--navbar-color: var(--dark-navbar-color);
--navbar-accent-color: var(--dark-navbar-accent-color);
--heading-color: var(--dark-heading-color);
--link-color: var(--dark-link-color);
--link-color-visited: var(--dark-link-color-visited);
--nav-link-color: var(--dark-nav-link-color);
--neutral-gray: var(--dark-neutral-gray);
--inline-code-bg: var(--dark-inline-code-bg);
--inline-code-color: var(--dark-inline-code-color);
}
&:not([data-theme="light"]) {
@media(prefers-color-scheme: dark) {
color-scheme: dark;
--bg-color: var(--dark-bg-color);
--content-color: var(--dark-content-color);
--content-color-faded: var(--dark-content-color-faded);
--primary-color: var(--dark-primary-color);
--navbar-color: var(--dark-navbar-color);
--navbar-accent-color: var(--dark-navbar-accent-color);
--accent-color: var(--dark-accent-color);
--accent-color-faded: var(--dark-accent-color-faded);
--heading-color: var(--dark-heading-color);
--link-color: var(--dark-link-color);
--link-color-visited: var(--dark-link-color-visited);
--nav-link-color: var(--dark-nav-link-color);
--neutral-gray: var(--dark-neutral-gray);
--inline-code-bg: var(--dark-inline-code-bg);
--inline-code-color: var(--dark-inline-code-color);
}
}
}

Some files were not shown because too many files have changed in this diff Show More