Compare commits

...

43 Commits

Author SHA1 Message Date
c8f72b28f4 Adds the GF rules so I can link them for Eric 2026-06-09 13:51:23 -07:00
dcf36ce8e4 Adds some favorite albums 2026-06-09 13:49:37 -07:00
8aaa4b146b Adds note on running untrusted code 2026-05-31 11:11:17 -07:00
bcc2d85560 Adds table of contents. Removes now from the top level of the sidebar. 2026-05-26 17:10:01 -07:00
5c90096a10 Fixes syntax highlighting 2026-05-21 16:14:40 -07:00
42cf585dca Adds TODO doc 2026-05-21 15:10:26 -07:00
25da58b4ed [Notes] Adds note about NixOS cloud hosting. 2026-05-21 15:07:41 -07:00
9ea91ff983 Updates now page 2026-05-18 14:18:51 -07:00
c96adeb025 Fixes social links. Adds back rel='me' 2026-05-18 09:40:56 -07:00
3a77bef362 Fleshes out Now 2026-05-15 17:33:34 -07:00
83f3bbdd1e Fix another bug with the notes URLs. 2026-05-15 16:50:59 -07:00
8f299c9631 Move now to the notes, add redirect. Fix bug with note links and capitalization 2026-05-15 16:49:36 -07:00
0a98a4c6ed Adds a Now section to about 2026-05-15 16:21:48 -07:00
3c87527dac Publishes Done With Crypto 2026-05-11 14:41:05 -07:00
8066b04b36 Capitalize categories in hierarchy 2026-05-07 17:01:39 -07:00
774c713110 Some updates to the scrub post 2026-05-07 16:51:41 -07:00
98430c26bc Fix figure formatting and add a little more to the ruins note 2026-05-01 21:02:33 -07:00
8d737fe280 Reduce header size 2026-05-01 20:51:40 -07:00
f8e1ce6b70 Unify social links component. Style tweaks for responsiveness 2026-05-01 20:48:01 -07:00
40f38301ea More menu layout fixes 2026-05-01 19:02:38 -07:00
5535a30873 Adds notes to mobile menu 2026-05-01 18:26:19 -07:00
9af10f938e Adds a note about 2-in ruins 2026-05-01 17:08:55 -07:00
65cb7bb0f8 Attempts to fix the code scrolling 2026-04-21 16:42:32 -07:00
609ac3e9c3 2026-04-21 Post about adding notes hierarchy 2026-04-21 16:30:45 -07:00
692430f959 Fixes blockquote styling 2026-04-21 15:47:51 -07:00
4a3f6774cc Small cleanups to values 2026-04-21 15:44:40 -07:00
81c912e3ff Adds notes. 2026-04-21 15:39:16 -07:00
1e5d1f13e8 Adds AGENT.md 2026-04-17 14:38:02 -07:00
bebca2ee17 Removes tailwind 2026-04-17 14:37:51 -07:00
d2d612e513 Adds nix result to gitignore 2026-04-17 14:32:22 -07:00
8ce2e32f54 Clean up and fix build bug 2026-04-13 16:19:34 -07:00
e92657b90d Adds IndieAuth pointing to indieauth.com 2026-04-13 16:08:15 -07:00
2df49ec83a Creates Motivation is an Institutional Problem 2026-04-07 15:56:42 -07:00
0e4c438869 Adds per-tag RSS link to each tag page. 2026-03-16 11:35:49 -07:00
8f6de2cced Indieweb draft, creates per-tag RSS feeds. 2026-03-16 11:27:45 -07:00
eb067b6a39 Adds info about Blaze Star to the about page. 2026-03-16 10:47:37 -07:00
6b5fbca9bb Adds h-entry data to blog posts 2026-03-13 17:54:24 -07:00
b5f2d9af0b Adds h-card 2026-03-13 17:30:09 -07:00
59c6c6db3e Draft for Joining the IndieWeb 2026-03-13 17:06:34 -07:00
bb373f0f37 Setup rel=me links 2026-03-13 16:18:51 -07:00
3a33325d3a Adds post tags 2026-03-11 18:29:14 -07:00
0a9f425591 Fixes social link in the sidebar 2026-03-11 18:29:14 -07:00
7445778ecd Adds draft about being a scrub 2026-03-11 18:29:14 -07:00
54 changed files with 2906 additions and 1406 deletions

4
.gitignore vendored
View File

@@ -135,3 +135,7 @@ dist
# Direnv
.direnv/
# Nix stuff
result

57
AGENT.md Normal file
View File

@@ -0,0 +1,57 @@
# AGENT.md
This file provides guidance to AI agents like Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Blazestar.net is a personal blog/homepage built with **Astro v5** (static site generator). It uses React for interactive components, CSS v4 for styling, and MDX for markdown with JSX support.
## Commands
```bash
npm run dev # Start dev server at localhost:4321
npm run build # Build production site to ./dist/
npm run preview # Preview production build locally
```
Linting and formatting tools (ESLint, Prettier) are available via the Nix dev environment (`nix develop` or direnv). No npm scripts are configured for them.
## Architecture
### Content Collections
Defined in `src/content.config.ts`. Two collections:
- **blog** — Markdown/MDX posts in `src/content/blog/`. Drafts live in `src/content/blog/drafts/` and are excluded from production builds.
- **page** — Static pages in `src/content/page/` (about, values).
Blog post frontmatter: `title` (required), `description` (required), `pubDate`, `updatedDate`, `heroImage`, `tags[]`.
### Routing
- `/` — Home (`src/pages/index.astro`)
- `/blog/` — Post listing (`src/pages/blog/index.astro`)
- `/blog/[slug]/` — Individual posts (`src/pages/blog/[...slug].astro`)
- `/blog/tag/[tag]/` — Tag listing pages (`src/pages/blog/tag/[...tag].astro`)
- `/blog/tag/[tag].rss.xml` — Per-tag RSS feeds
- `/rss.xml` — Main RSS feed
### Key Utilities
- `src/lib/blog.ts` — Functions for fetching/sorting posts and extracting tag data
- `src/consts.ts` — Global constants (`SITE_TITLE`, `SITE_DESCRIPTION`)
### Layouts & Components
- `src/layouts/RootLayout.astro` — Root HTML shell; all pages use this
- `src/layouts/BlogPost.astro` — Layout for individual blog posts (h-entry microformat)
- `src/layouts/BlogList.astro` — Layout for post listing pages
- `src/components/` — Shared components (Header, Footer, Sidebar, etc.)
### Styling
Tailwind CSS v4 via Vite plugin (`@tailwindcss/vite`). Global CSS and custom properties in `src/styles/global.css`. Custom fonts in `public/fonts/`.
### IndieWeb
The site supports IndieWeb standards: h-card and h-entry microformats in layouts, IndieAuth endpoint pointing to indieauth.com.

15
TODO.md Normal file
View File

@@ -0,0 +1,15 @@
## UI
- [ ] Outline mode on documents
- [ ] Improve table rendering
- [ ] More image-flow options
- [ ] Fix capitalization in notes values/headers. Only the first letter is capitalized, but it should follow the capitalization of the folder. E.g. "NixOS" not "Nixos"
- [x] Code Highlighting
## Interaction
- [ ] Web Mentions
- [ ] Comments
- [ ] Guest Book
## Content

View File

@@ -13,4 +13,8 @@ export default defineConfig({
vite: {
plugins: [],
},
redirects: {
"/now": "/notes/now",
},
});

2536
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,13 +13,12 @@
"@astrojs/react": "^4.3.0",
"@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.4.1",
"@tailwindcss/vite": "^4.1.8",
"@types/react": "^19.1.7",
"@types/react-dom": "^19.1.6",
"astro": "^5.9.2",
"astro-loader-obsidian": "^0.10.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"sharp": "^0.34.2",
"tailwindcss": "^4.1.8"
"sharp": "^0.34.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -13,6 +13,13 @@ interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const blogTagMatch = Astro.url.pathname.match(/blog\/tag\/(\w+)/);
const rssUrl =
blogTagMatch
? new URL(`blog/tag/${blogTagMatch[1]}.rss.xml`, Astro.site)
: new URL(`rss.xml`, Astro.site);
const { title, description, image } = Astro.props;
---
@@ -25,7 +32,7 @@ const { title, description, image } = Astro.props;
rel="alternate"
type="application/rss+xml"
title={SITE_TITLE}
href={new URL('rss.xml', Astro.site)}
href={rssUrl}
/>
<meta name="generator" content={Astro.generator} />
@@ -33,6 +40,9 @@ const { title, description, image } = Astro.props;
<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin />
<!-- IndieAuth -->
<link rel="authorization_endpoint" href="https://indieauth.com/auth">
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />

View File

@@ -1,9 +1,15 @@
---
import SocialLinks from "./SocialLinks.astro";
const today = new Date();
---
<footer>
&copy; {today.getFullYear()} Periodic. All rights reserved.
<div class="social-links">
<SocialLinks />
</div>
<div>
&copy; {today.getFullYear()} Periodic. All rights reserved.
</div>
<script>
window.goatcounter = {
path: (path) => `${location.host}${path}`,
@@ -17,7 +23,15 @@ const today = new Date();
<style>
footer {
padding: 2em 1em 6em 1em;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
color: var(--color-gray);
}
@media (min-width: 1000px) {
.social-links {
display: none;
}
}
</style>

View File

@@ -18,6 +18,5 @@ const { date } = Astro.props;
<style>
time {
font-family: "Fira Code", monospace;
font-size: var(--font-size-sm);
}
</style>

View File

@@ -1,6 +1,9 @@
---
import HeaderLink from './HeaderLink.astro';
import Blazestar from './Blazestar.astro';
import NoteHierarchy from './NoteHierarchy.astro';
import NotchedBox from './NotchedBox.astro';
import SocialLinks from './SocialLinks.astro';
---
<header>
@@ -10,33 +13,29 @@ import Blazestar from './Blazestar.astro';
<HeaderLink href="/">Home</HeaderLink>
<HeaderLink href="/blog">Blog</HeaderLink>
<HeaderLink href="/about">About</HeaderLink>
</div>
<div class="social-links">
<a href="https://m.webtoo.ls/@astro" target="_blank">
<span class="sr-only">Follow Periodic on Mastodon</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
><path
fill="currentColor"
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
></path></svg
>
</a>
<a href="https://github.com/periodic" target="_blank">
<span class="sr-only">Go to Periodic's GitHub repo</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
><path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
></path></svg
>
</a>
<div class="notes-menu">
<label for="notes-menu-checkbox" id="notes-menu-button">
Notes
</label>
<input type="checkbox" id="notes-menu-checkbox" name="notes-menu-checkbox"/>
<div class="notes-menu-tree" id="notes-menu-tree">
<NotchedBox fillNotches="left">
<NoteHierarchy prefix="/notes" />
</NotchedBox>
</div>
</div>
</div>
</nav>
<div class="h-card">
<img class="u-photo" src="/photos/Bingley Business.jpg" />
<a class="p-name u-url" href="https://www.blazestar.net">Drew Haven</a>
a.k.a. <span class="p-nickname">Periodic</span>
</div>
</header>
<style>
header {
color: var(--color-light-text);
margin: 0;
margin: 0 1em;
padding: 0;
}
@@ -53,11 +52,11 @@ import Blazestar from './Blazestar.astro';
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
nav a {
padding: 1em 0.5em;
color: var(--color-light-text);
border-bottom: 4px solid transparent;
text-decoration: none;
&:hover {
@@ -72,14 +71,61 @@ import Blazestar from './Blazestar.astro';
.social-links a {
display: flex;
}
@media (max-width: 720px) {
.social-links {
display: none;
}
}
@media (max-width: 520px) {
nav {
flex-direction: column;
.internal-links {
display: flex;
flex-direction: row;
gap: 4px;
font-size: 120%;
}
#notes-menu-checkbox {
display: none;
&:checked ~ #notes-menu-tree {
display: flex;
}
}
&:checked ~ #notes-menu-button {
color: var(--color-accent);
}
}
.notes-menu {
position: relative;
margin: 1em 0.5em; /* Matches links above */
.notes-menu-tree {
position: absolute;
top: 100%;
z-index: 10;
right: 0;
}
}
.notes-menu-tree {
display: none;
overflow: scroll;
flex-direction: column;
width: 15em;
background-color: rgba(0, 0, 0, 0.2);
padding: 8px;
a {
color: var(--color-light-text);
text-decoration: none;
&:hover {
color: var(--color-accent);
}
&.active {
text-decoration: none;
}
}
}
.h-card {
display: none;
}
</style>

View File

@@ -1,13 +1,14 @@
---
interface Props {
fillNotches: "left" | "right" | "both" | "none";
color?: string;
}
const { fillNotches = "none" } = Astro.props;
const { fillNotches = "none", color = "gold" } = Astro.props;
---
<div class={`container notch-${fillNotches}`}>
<div class={`container notch-${fillNotches}`} style={`background-color: var(--color-${color})`}>
<div class="content">
<slot />
</div>
@@ -16,7 +17,7 @@ const { fillNotches = "none" } = Astro.props;
<style>
.container {
height: 100%;
width: 100%;
width: calc(100% - 2px);
padding: 1px;
background-color: var(--color-gold);
}

View File

@@ -0,0 +1,71 @@
---
import { type Hierarchy } from "../lib/hierarchy.ts";
import { makeHierarchy } from '../lib/hierarchy';
import { getCollection } from 'astro:content';
interface Props {
prefix: string;
hierarchy?: Hierarchy;
}
const { prefix, hierarchy: maybeHierarchy} = Astro.props;
const notes = await getCollection('notes');
const hierarchy = maybeHierarchy ?? makeHierarchy(notes.map(note => [note.id, note.data.title]));
const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, '/');
---
{
Object.entries(hierarchy).map(([leader, node]) => {
const notePath = `${prefix}/${leader}`;
const isActive = pathname === notePath;
const linkClasses = isActive ? "active" : "";
if (node.children) {
return (
<details open={pathname.startsWith(notePath) && "true"}>
<summary>
{ node.id
? <a href={notePath} class={linkClasses}>{node.title || leader}</a>
: <span class="header-only">{node.title || leader}</span>
}
</summary>
<div class="child-notes">
{ <Astro.self prefix={notePath} hierarchy={node.children} /> }
</div>
</details>
);
}
return (
<div>
<a href={`/notes/${node.id}`} class={linkClasses}>
{node.title || leader}
</a>
</div>
);
})
}
<style>
.active {
font-weight: bold;
}
.child-notes {
margin-left: 1em;
}
summary {
.header-only {
color: var(--color-gray)
}
&:hover {
color: var(--color-gold);
}
}
</style>

View File

@@ -0,0 +1,23 @@
---
import NotchedBox from './NotchedBox.astro';
import NoteHierarchy from './NoteHierarchy.astro';
const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, '');
const isActive = pathname.startsWith("notes");
---
<NotchedBox fillNotches={isActive ? 'left' : 'none'}>
<div class="notes-header">Notes</div>
<div class="note-links">
<NoteHierarchy prefix="/notes" />
</div>
</NotchedBox>
<style>
.notes-header {
color: var(--color-light-text);
font-size: var(--font-size-lg);
}
.note-links {
margin-left: 8px;
}
</style>

View File

@@ -1,6 +1,8 @@
---
import SidebarLink from './SidebarLink.astro';
import Blazestar from './Blazestar.astro';
import NotesLinks from './NotesLinks.astro';
import SocialLinks from './SocialLinks.astro';
---
<header>
@@ -10,26 +12,10 @@ import Blazestar from './Blazestar.astro';
<SidebarLink href="/">Home</SidebarLink>
<SidebarLink href="/blog">Blog</SidebarLink>
<SidebarLink href="/about">About</SidebarLink>
<NotesLinks />
</div>
<div class="social-links">
<a href="https://m.webtoo.ls/@astro" target="_blank">
<span class="sr-only">Follow Periodic on Mastodon</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
><path
fill="currentColor"
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
></path></svg
>
</a>
<a href="https://github.com/periodic" target="_blank">
<span class="sr-only">Go to Periodic's GitHub repo</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
><path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
></path></svg
>
</a>
<SocialLinks />
</div>
</nav>
</header>
@@ -57,13 +43,12 @@ import Blazestar from './Blazestar.astro';
gap: 16px;
a {
color: var(--color-light-text);
border-bottom: 4px solid transparent;
text-decoration: none;
&:hover {
color: var(--color-accent);
border-bottom: 4px solid transparent;
text-decoration: none;
}
&.active {
text-decoration: none;
}
@@ -74,15 +59,4 @@ import Blazestar from './Blazestar.astro';
flex-direction: column;
gap: 16px;
}
.social-links,
.social-links a {
display: flex;
gap: 16px;
justify-content: center;
}
@media (max-width: 720px) {
.social-links {
display: none;
}
}
</style>

View File

@@ -0,0 +1,45 @@
<div class="social-links">
<a href="https://mastodon.social/@periodic" target="_blank" rel="me">
<span class="sr-only">Follow Periodic on Mastodon</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
><path
fill="currentColor"
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
></path></svg>
</a>
<a href="https://github.com/periodic" target="_blank" rel="me">
<span class="sr-only">Go to Periodic's GitHub repo</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
><path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
></path></svg
>
</a>
<a href="/rss.xml">
<span class="sr-only">Subscribe to Periodic's RSS feed</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" width="32" height="32">
<!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com
License - CC BY 4.0 - https://fontawesome.com/license/free
Copyright 2026 Fonticons, Inc.-->
<path
fill="currentColor"
d="M96 128C96 110.3 110.3 96 128 96C357.8 96 544 282.2 544 512C544 529.7 529.7 544 512 544C494.3 544 480 529.7 480 512C480 317.6 322.4 160 128 160C110.3 160 96 145.7 96 128zM96 480C96 444.7 124.7 416 160 416C195.3 416 224 444.7 224 480C224 515.3 195.3 544 160 544C124.7 544 96 515.3 96 480zM128 224C287.1 224 416 352.9 416 512C416 529.7 401.7 544 384 544C366.3 544 352 529.7 352 512C352 388.3 251.7 288 128 288C110.3 288 96 273.7 96 256C96 238.3 110.3 224 128 224z"/>
cial
</svg>
</a>
</div>
<style>
.social-links {
display: flex;
flex-direction: row;
gap: 16px;
justify-content: center;
a {
color: var(--color-light-text);
text-decoration: none;
}
}
</style>

View File

@@ -0,0 +1,31 @@
---
import TableOfContentsTree from "./TableOfContentsTree.astro";
import type { MarkdownHeading } from "astro";
import { makeHeadingsTree, removeEmptyLevels } from "../lib/headings";
type Props = {
headings: Array<MarkdownHeading>;
};
const {headings} = Astro.props;
const headingsTree = removeEmptyLevels(makeHeadingsTree(headings));
---
<div class="table-of-contents">
<TableOfContentsTree headingsTree={headingsTree}></TableOfContentsTree>
</div>
<style>
.table-of-contents {
border: 1px solid var(--color-space-blue-light);
ul {
list-style: none;
}
& > ul {
padding: 0;
margin: 8px 16px;
}
}
</style>

View File

@@ -0,0 +1,15 @@
---
type Props = {
headingsTree: HeadingsTree;
};
const {headingsTree} = Astro.props;
---
<ul>
{headingsTree.map(node =>
Array.isArray(node)
? <Astro.self headingsTree={node} />
: <li><a href={`#${node.slug}`}>{node.text}</a></li>
)}
</ul>

View File

@@ -2,4 +2,4 @@
// You can import this data from anywhere in your site by using the `import` keyword.
export const SITE_TITLE = "Blazestar.net";
export const SITE_DESCRIPTION = "Welcome to Blazestar.net";
export const SITE_DESCRIPTION = "Periodic musings about systems";

View File

@@ -1,3 +1,7 @@
import {
ObsidianDocumentSchema,
ObsidianMdLoader,
} from "astro-loader-obsidian";
import { glob } from "astro/loaders";
import { defineCollection, z } from "astro:content";
@@ -14,6 +18,7 @@ const blog = defineCollection({
pubDate: z.coerce.date().optional(),
updatedDate: z.coerce.date().optional(),
heroImage: image().optional(),
tags: z.array(z.string()).optional(),
}),
});
@@ -25,4 +30,15 @@ const page = defineCollection({
}),
});
export const collections = { blog, page };
const notes = defineCollection({
loader: ObsidianMdLoader({
base: "src/content/notes",
url: "notes",
}),
schema: ({ image }) =>
ObsidianDocumentSchema.extend({
image: image().optional(),
}),
});
export const collections = { blog, page, notes };

View File

@@ -0,0 +1,161 @@
---
title: "Adding notes to Blazestar.net"
description: "I wanted to add some free-form notes to my site so that I could put up more permanent knowledge than chronological blog posts. I wanted to be able to use a system much like my markdown notes I keep for myself that had a nice menu like EmaNote. Here's how I made Astro do it for me."
pubDate: "Apr 21 2026"
tags:
- Coding
---
There are two influences that came together and prompted me to add some notes to this site. I've often wanted a quick way to add a page that holds some information or knowledge in a more structured way than just a blog, but I didn't want to have to manually build up the structure with each note. I wanted something that will take simple markdown files that I use for content and build a menu of notes that I can link to and readers can browse.
The first is my notes system. A few years ago I wanted a good plain-text notes system to replace what I had been doing in [Notion](https://www.notion.com/). I started out by trying [Obsidian](https://obsidian.md/), but I didn't like the client and preferred to edit directly in [NeoVim](https://neovim.io/). I kept the overall layout and format. I do all my syncing with [SyncThing](https://syncthing.net/).
I was inspired by [EmaNote](https://emanote.srid.ca/). I stumbled over it one time when I was researching a technical topic and was impressed by the layout and linking. It supports lots of ways to connect notes such as backlinks and "folgetzettel" links. I wanted something like that to organize my information.
I experimented with migrating to EmaNote. I found the templates a little awkward to use, the documentation a little lacking and the migration was non-trivial. I really wanted to support the Haskell ecosystem, but I also want something that will work and get things done for me. I don't particularly like Astro as a project or a company, but it has been working for me, so I decided to stick with it.
## Content Loading
The first thing I needed was a content loader that would get all the notes and handle things like links between them. Fortunately, I found [astro-loader-obsidian](https://github.com/aitorllj93/astro-loader-obsidian) through the list of Astro plugins. I was a little worried at first that it didn't list anything about supporting folders or structure, which is something I really wanted.
I installed it, made a dummy notes folder, configured it and then did good old `JSON.stringify` in my sidebar template to see what I got. Thankfully, it did support the full path to each note! Score!
## Rebuilding the Note Structure
So now that I had these notes, I needed to rebuild the structure so I could render a hierarchy. This was going to be a relatively simple matter of breaking of the paths and grouping them. I already know how to split paths and group things into a `Record`. This should be easy.
I created a type like the following. Note that TypeScript does not support self-referential recursive types! It does allow self-referential interfaces though. Hence the odd syntax.
```typescript
interface Hierarchy extends Record<string, Hierarchy> {}
```
However, after messing around with it for a while I realized I needed a little more information in my notes. I wanted the full ID/path and title of the note stored on each node. The ID would be an indicator of whether a given node in the tree has a note or not. That way I can support folder notes, such as having `foo.md` be the folder note where I also have `foo/bar.md`. The title lets me render the title without having to worry about making the file name be well capitalized or punctuated.
This lead me to create the following data structure. Some nodes would have children, others not, and they may or may not have an ID or title. (I could write both of these as `type` declarations because they are _mutually recursive_ types. TypeScript is weird.)
```typescript
interface HierarchyNode {
id: string | null;
title: string | null;
children: Hierarchy | null;
}
type Hierarchy = Record<string, HierarchyNode>;
```
I started with algorithm of splitting all the paths into their parts, then going through and recursively grouping. However, I found this would be much simpler if I just did this as a fold/reduce so I could just focus on adding one path at a time. This gave me the following simple algorithm. Please ignore the repetition, it didn't seem worth cleaning up at the time.
```typescript
function addToHierarchy(
tree: Hierarchy,
[noteId, title]: [string, string],
): Hierarchy {
const path = noteId.split("/");
if (path.length === 0 || path[0] === "") {
return tree;
}
if (!tree[path[0]]) {
tree[path[0]] = { id: null, title: null, children: null };
}
let curr = tree[path[0]];
for (const node of path.slice(1)) {
curr.children ||= {};
if (!curr.children[node]) {
curr.children[node] = { id: null, title: null, children: null };
}
curr = curr.children[node];
}
curr.id = noteId;
curr.title = title;
return tree;
}
export function makeHierarchy(notes: Array<[string, string]>): Hierarchy {
return notes.reduce(addToHierarchy, {});
}
```
## Rendering Links
So now I had to render the links. Astro thankfully _does_ support recursive components through `Astro.self`, so I was able to write a single component to render the hierarchy.
```astro
---
import { type Hierarchy } from "../lib/hierarchy.ts";
interface Props {
prefix: string;
hierarchy: Hierarchy;
}
const { prefix, hierarchy } = Astro.props;
const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, '/');
---
{
Object.entries(hierarchy).map(([leader, node]) => {
const notePath = `${prefix}/${leader}`;
const isActive = pathname === notePath;
const linkClasses = isActive ? "active" : "";
if (node.children) {
return (
<details open={pathname.startsWith(notePath) && "true"}>
<summary>
{ node.id
? <a href={notePath} class={linkClasses}>{node.title || leader}</a>
: <span class="header-only">{node.title || leader}</span>
}
</summary>
<div class="child-notes">
{ <Astro.self prefix={notePath} hierarchy={node.children} /> }
</div>
</details>
);
}
return (
<div>
<a href={notePath} class={linkClasses}>
{node.title || leader}
</a>
</div>
);
})
}
<style>
.active {
font-weight: bold;
}
.child-notes {
margin-left: 1em;
}
summary {
.header-only {
color: var(--color-gray)
}
&:hover {
color: var(--color-gold);
}
}
</style>
```
And there we go, a list of links for my notes that mirrors the folder structure.
## In Closing
Overall it was a pleasant little project. I'm glad I now have a place to collect information online that I think might be useful to others as easily as importing a markdown file. I can even publish as simply as copying things directly from my main notes folder into the site with minimal changes.
Please let me know if you have any thoughts or if it helped you. The best way to reach me is to ping me on Mastodon!
Happy hacking!

View File

@@ -0,0 +1,62 @@
---
title: "I'm done watching the crypto scene"
description: "I've found the crypto world fascinating since it's inception. The hype, the riches, the lies, the scams! It was a fascinating case study on what people could convince themselves of and what markets could be if we ignored the last few centuries of wisdom. It was ridiculous and I couldn't believe it kept going. I didn't realize it was just a preview of what was to come."
pubDate: 2026-05-11
tags:
- Crypto
- Tech
---
I've been fascinated by the crypto world for years. It started way back when I first heard of Bitcoin around 2009. It was a really interesting idea. A distributed append-only ledger that anyone could write to. I hadn't heard of anything quite like it.
I couldn't really figure out what it was good for. It was slow, expensive, and hard to correct mistakes. I heard lots of arguments for it, but none of them were convincing to me.
- You can instantly send money anywhere in the world!
- But you still have to deal with exchanging it for local currency.
- And the instantaneous nature means there isn't a way to reverse transactions so it's going to become a magnet for fraud.
- You can have privacy and anonymity! Governments won't be able to stop you!
- Except that every transaction is public so anonymity only lasts as long as you can avoid having _any identifying information_ tied to your wallet address.
- And you'll be identified as soon as you try to convert to fiat currency.
- It's immune to government censorship!
- Except we mostly have that censorship for good crime-prevention reasons!
- There's still the issue of turning it into real currency.
- It'll become a way to give everyone a path to global currency exchange!
- Except it relies heavily on electronics and internet access which can be censored.
So I watched as it slowly grew. I was amazed by how many people continued to think that the revolution was just around the corner. All I saw was a lot of speculation and fraud. Stories of people having their fortunes stolen with no recourse. Scammers using it to extort money from victims. Even major exchanges being hacked and drained belying the claim that it was all decentralized.
But it kept growing. We had loud proclamations that Web3 was here with very little definition of what it was. We had the NFT craze. Everyone was jumping on the bandwagon yet no one could really describe where they were hoping the wagon would go. Remember when **Long Island Ice Tea Corp.** changed their name to [**Long Blockchain Corp.**](https://en.wikipedia.org/wiki/Long_Blockchain_Corp.) and saw their stock jump 380%? Blockchain was going to be the solution to everything!
I started to follow Molly White's [Web3 is Going Just Great](https://www.web3isgoinggreat.com/) and later her [Citation Needed](https://www.citationneeded.news/) newsletter. There was the excellent essay/documentary [Line Goes Up](https://www.youtube.com/watch?v=YQ_xWvX1n9g) by Dan Olson. It was pretty clear that this was all crazy, but it was fascinating. How did people get sucked into this cult?
There was one phrase that seemed to encapsulate the whole movement: Have Fun Staying Poor. It was directed at non-believers. It was repeated so often it was often abbreviated HFSP. It made it clear that the real driver was greed. This wasn't about making the world better. It was about getting yours and sticking it to everyone else.
There was a moment when it seemed to recede. It was refreshing when FTX folded and Sam Bankman-Fried went to jail. There was a semblance of sanity and there were mechanisms to rein in the worst of the excess. I had hope that the craze would pass, the volatility would die down, people could safely start saving for retirement again.
## It's too real now
I was so wrong. Over the last few months the entire economy seems to have been taken over by HFSP mentality. Leaders in business and finance are happy to get theirs to the detriment of the rest of us. If you don't embrace it, have fun staying poor.
- Sports gambling and prediction markets are taking off.
- Sports broadcasts are now plastered with ads for gambling platforms and odds.
- Even though about [43% of adults see it as a bad thing for society](https://www.pewresearch.org/short-reads/2025/10/02/americans-increasingly-see-legal-sports-betting-as-a-bad-thing-for-society-and-sports/).
- In response to an American soldier making over $400k on Polymarket we had the president of the USA say, "the whole world, unfortunately, has become somewhat of a casino."
- AI is the ultimate in FOMO
- AllBirds, the shoes and clothing company announced a pivot to AI and [saw a 582% jump in their stock price](https://www.cnbc.com/2026/04/15/allbirds-bird-stock-shoes-ai.html) in just one day.
- There has been [very little demonstrated value](https://garymarcus.substack.com/p/agents-and-roi) from most AI roll-outs.
- There's a rush to invest and spend [without any concrete plan on how to recoup that capital](https://www.wheresyoured.at/am-i-meant-to-be-impressed/).
- Companies are taking advantage of reduced regulation
- The [EPA recently reversed the endangerment finding](https://www.npr.org/2025/07/24/nx-s1-5302162/climate-change-trump-epa), meaning that companies no longer have to consider the health costs of air pollution.
- The FCC seems content to only [go after enemies of the president](https://www.npr.org/2026/04/28/nx-s1-5802997/fcc-abc-license-renewal-melania-trump-jimmy-kimmel) instead of [reviewing mergers](https://arstechnica.com/tech-policy/2025/05/fcc-chair-brendan-carr-is-letting-isps-merge-as-long-as-they-end-dei-programs/) that will reduce consumer choice and power.
All this in the broader economy means crypto news just doesn't hit the same way anymore. I used to laugh, I now just sigh. I see the [grift](https://en.wikipedia.org/wiki/$Trump), the [political dealing](https://www.wired.com/story/trumpcoin-dinner-ticket-bidding/), the [dropped indictments](https://abcnews.com/US/sec-drops-case-crypto-firm-ties-trump-ceo/story?id=119963257), the [pardons](https://www.politico.com/news/2025/10/23/trump-pardons-crypto-billionaire-changpeng-zhao-00620175).
I was right that crypto would not grow to take over the economy. I was wrong in thinking that the greedy mindset would stay contained. Now it seems like naked greed is the order of the day. We live in a grift economy. Everything is about taking advantage of others and there are few mechanisms to prevent it.
## So what's next
Crypto used to be a fascinating corner of the economy. I found it puzzling and amazing that some people could make so much money while so many people lost theirs. I couldn't see the value in it.
The whole economy has become the same grift that crypto was. The mindset is everywhere, particularly around AI. Every week we get a breathless announcement about a new AI model or $100B round of circular financing.
I'm done with it. I have better things to do with my time, energy and emotions. I am going to continue to focus on building real connections with real people in a way that adds real value to their lives. I don't need another distraction or the negative emotions. It's time to focus on what I can do differently.

View File

@@ -0,0 +1,66 @@
---
title: "What is a scrub?"
description: "Devin Sirlin applied the term scrub to a specific type of player he saw in the fighting-game circuit. This person stops improving and fails to win because they create self-imposed rules that get in their way. This concept goes far beyond getting good at video games or winning competitions. It applies to everything in life. How are you creating self-imposed rules that conflict with your goals?"
---
Let me just start this by saying I have a deep fear of failure. I spend a lot of time and energy on preparation and analysis in order to avoid it. Many times the failure is only in my own eyes and that's something that I endeavor to let go of.
This had lead me to That can often ironically lead to failure! I've come to realize many subtle ways in which I can take responsibility for my own success. Much of this is around re-framing success and re-framing what I can do.
One author that had a big impact on me is [David Sirlin](https://en.wikipedia.org/wiki/David_Sirlin). He wrote a great mini-book from his blog, [sirlin.net](https://sirlin.net), called [Playing to Win](https://sirlin.squarespace.com/ptw). It's a treatise about how to learn how to win in competitive pursuits. He made his gaming career playing [Street Fighter](https://en.wikipedia.org/wiki/Street_Fighter), but also designing and balancing competitive video and table-top games, so that's the context he's coming from.
I dabble in competitive games. They are something I really enjoy, but I don't play them at a particularly competitive level. However, the concepts really stuck with me.
## What is a scrub?
One of his key terms is the participant he calls [the scrub](https://sirlin.squarespace.com/ptw-book/introducingthe-scrub). It's a very specific definition and a useful one, even if I think the condescension implied by the name is unhelpful. I'll let him define it with a few excerpts.
> A scrub is a player who is handicapped by self-imposed rules that the game knows nothing about. A scrub does not play to win.
>
> Now, everyone begins as a poor player—it takes time to learn a game to get to a point where you know what youre doing. There is the mistaken notion, though, that by merely continuing to play or “learn” the game, one can become a top player. In reality, the “scrub” has many more mental obstacles to overcome than anything actually going on during the game. The scrub has lost the game even before it starts. Hes lost the game even before deciding which game to play. His problem? He does not play to win.
>
> [...]
>
> The first step in becoming a top player is the realization that playing to win means doing whatever most increases your chances of winning. That is true by definition of playing to win. The game knows no rules of “honor” or of “cheapness.” The game only knows winning and losing.
>
> A common call of the scrub is to cry that the kind of play in which one tries to win at all costs is “boring” or “not fun.” Who knows what objective the scrub has, but we know his objective is not truly to win. Yours is. Your objective is good and right and true, and let no one tell you otherwise. You have the power to dispatch those who would tell you otherwise, anyway. Simply beat them.
That's a pretty harsh description of a type of person we have likely known, been, are and will-be. I think it's bit too harsh, but there's something very useful here.
I think if you took this literally you might focus entirely on winning. However, I think that's actually scrub-behavior for most people! Most people who focus solely on winning have created a narrow mindset that actually limits their ability to thrive in their chosen sport, hobby, vocation and life.
## There's more than just winning
Sirlin examines the scrub from a very narrow context: winning the game. This is itself a self-imposed rule. It is deciding that the only thing that matters in the game is your final score and your tournament standings.
This is a very narrow way to play a game. What is it you might want out of a game? There are so many other things to optimize for: teaching, growing the community, making friends, personal fulfillment. It's not uncommon that people will drive themselves to succeed only to incur great physical, emotional and interpersonal costs. How many athletes damage their bodies in their youth? How many entrepreneurs sacrifice their relationships for their business?
You have to ask yourself, what are you working for? Why do you want to win? Do you think it will bring you happiness or is that just an assumption that creates a mental obstacle and prevents you from seeing the happiness all around you?
## Growth and Goals
There are two key aspects of a scrub that make them stand out as undesirable models of behavior: they don't learn or improve and they don't reach their goals. Any non-trivial goal requires that we grow and learn to be able to reach our goals, otherwise you could trivially achieve them and then they aren't goals anymore. Anything worth doing and talking about requires change.
Growth can be the goal in itself, but generally it's in service of a goal. Growth without a goal is really just play. It's exploring and trying new things for their own sake. Children do this really well before they learn to think ahead and compare themselves to others. Adults do it very poorly because we tend to think ahead compare ourselves to others or abstract ideas of what we should be.
Turning your play into work is a surefire way to make it unpleasant. The only thing that keeps us going through the unpleasant times is hope for some reward at the end.
## Understanding Failure
> If you never lose, you are never truly tested, and never forced to grow. A loss is an opportunity to learn.
## The danger of always playing to win
TODO
If you focus only on winning you may miss out on goals that actually fulfill your needs.
## How to set good goals
TODO
What makes a good goal? What are you optimizing for?
## Other Quotes
> I once played a scrub who was actually quite good. That is, he knew the rules of the game well, he knew the character matchups well, and he knew what to do in most situations. But his web of mental rules kept him from truly playing to win. He cried cheap as I beat him with “no skill moves” while he performed many difficult dragon punches. He cried cheap when I threw him five times in a row asking, “Is that all you know how to do? Throw?” I gave him the best advice he could ever hear. I told him, “Play to win, not to do difficult moves.’” This was a big moment in that scrubs life. He could either ignore his losses and continue living in his mental prison or analyze why he lost, shed his rules, and reach the next level of play.

View File

@@ -0,0 +1,49 @@
---
title: "Joining the Indie Web"
description: "I've decided to make an effort to join the Indie Web. This is mostly a philosophical distinction, but there are also some technical details."
---
I decided to give this site some more attention. That leads me to an important question: what is it for? There's an easy answer that it's where I can publish some of my ideas. I can host files and link to them. However, that's not the real power of the web. Let me explain.
I already have a folder of Markdown files that I keep for informational purposes. They have internal links, formatting, structure, etc. I was originally inspired by [Obsidian](https://obsidian.md/), but decided I simply love [NeoVim](https://neovim.io/) with [LazyVim](https://www.lazyvim.org/) too much and have since transitioned to using that as my editor of choice. I keep the documents in sync across four devices—including a server and a phone—using [SyncThing](https://syncthing.net/). I some day want to get more of this information online so that I can share it, but that's still effectively the same thing. That's just publishing.
The web enables something far more interesting: connection. These days we have a mess of siloed accounts across 100 different services. Since the '00s companies have been shoving social features into anything they can. I just checked my password vault and I have over 350 entries private to me and another 200 shared with my spouse. I know a bunch of these are sites with their own profiles, but I'm certainly not going to go through and count. How many is it, dozens? All these are fragmented. All of them want my attention. How do I connect these accounts when each one wants me to spend all my time with them alone?
This brings me to my recent discovery: the [Indie Web](https://indieweb.org/). It bills itself as the more personal alternative to the corporate web. It's centered around the idea of a personal website, like this one. It's a space that I own, I control. It makes it a place that is more human. And that enables so much more connection.
The web has turned into a group of five websites, each consisting of screenshots of text from the other four[^1]. We spend our time in various silos: TikTok, YouTube, Facebook, Reddit, Discord. It's all very disconnected and you ultimately have no control.
I'm old enough to have grown up in an age where personal websites were common, though many were hosted through sites like the long-gone [GeoCities](https://en.wikipedia.org/wiki/GeoCities) and [AngelFire](https://en.wikipedia.org/wiki/Angelfire). The web was a lot of little sites where people shared their passion or just a little bit about themselves. Search engines used to be one of the major tools for finding these sites, along with directories and [web rings](https://en.wikipedia.org/wiki/Webring). I spent many a teenage year browsing around sites looking for more information on Magic: the Gathering, Dungeons & Dragons, or learning how to build my own sites and wrangle with that new-fangled CSS thing.
## What is the Indie Web
The Indie Web seems to have come out of Bruce Sterling's IndieWeb Camp way back in 2013. There he outlined a few major principles, which are [largely unchanged today](https://indieweb.org/principles).
I think it can largely be summed up by two major themes:
- Build things for people. You are the first customer so design for yourself and what you love. Don't agonize about making things interoperable for machines.
- Build things that last. You should have control over the data and site. It should only loosely depend on APIs and platform
So how do we go about joining the Indie Web?
## Step 1: Have a website
This is off to a great start!
I picked up this domain about a year ago. I was looking for some places to host some of my own things. There's a domain I've had for over a decade but never really liked. I wanted something memorable, a bit obscure, but easy to describe. I came across Blaze Star when there was talk of the star [T Coronae Borealis](https://en.wikipedia.org/wiki/T_Coronae_Borealis) flaring. It is theorized to have a roughly 80-year cycle, though this latest one has yet to materialize. It has the official proper name "Blaze Star". I thought it particularly fitting since I've been using the handle Periodic for over a decade now.
The site itself is running on a home server. I should transfer it to a VPS, but that is pretty low on my priority list.
The site is built with [Astro](https://astro.build/), which is _fine_. I think it tries to be a bit too magical in a way that makes the code hard to follow, but it works well enough. I'll likely replace it soon with something a little more dynamic.
## Step 2: Create an Identity
This is where the interesting part starts to happen. We can start to turn this website into an identity by linking it to other profiles and starting to connect things together.
First, I can start to link to my profiles on other sites using [`rel=me`](https://indieweb.org/rel-me) links. This is a common attribute of the `<a>` tags that is short for "relationship". It's meant to tell the browser what the relationship of the linked item is, for example, whether it links to an alternate representation, the next document in a series. It also encodes a bunch of technical things like `nofollow` and `prefetch` which are more about actions than relationships. That's what decades of standards development will do!
Anyway, I can link to things like my Mastodon and GitHub profiles to indicate that they are mine. I should also make sure that those profiles _link back_ to this page to complete the link and show that I have control of both profiles. There's nothing to stop me from linking to any old profile and calling it mine. The link-back shows that there's at least a mutual relationship.
Next up is [h-card](https://microformats.org/wiki/h-card)
[^1]: Credit to Tom Eastman on Twitter on 12/3/18. I feel dirty just [linking to that site](https://twitter.com/tveastman/status/1069674780826071040).

View File

@@ -2,6 +2,9 @@
title: "The Forge of God by Greg Bear"
description: "A short review"
pubDate: "Jul 18 2025"
tags:
- Sci-Fi
- Book Review
---
[The Forge of God](https://en.wikipedia.org/wiki/The_Forge_of_God) by [Greg Bear](https://en.wikipedia.org/wiki/Greg_Bear) is a bit of an old book to be reviewing now. It was first published in 1987 and it's 2025 now. Did I pick it up without realizing this? Was a stricken with an acute case of nostalgia? Was I trapped in an abandoned house with nothing else to read? Dear reader, I assure you that nothing so exciting happened. I simply saw it on my shelf and decided to read it. I don't need any other reason that that.

View File

@@ -2,6 +2,10 @@
title: "I Forgot to Use AI On My Latest Project"
description: "I forgot to use an LLM to assist on my latest coding project. I probably set the progress of humanity back many hours. But hey, I learned something along the way. What did I learn? That I'd rather not use an LLM on this project."
pubDate: "Jun 26 2025"
tags:
- AI
- Tech
- Coding
---
I forgot to use AI on my last project. All the tech bros are laughing at me right now. I'm sure in the time I wasted I could have launched a cloud-based passive-income app, but now I'll just have fun staying poor. I'll cry myself to sleep right after I get the satisfaction of learning something new and building something with my own mind.

View File

@@ -0,0 +1,92 @@
---
title: "Motivation is an institutional problem"
description: "It's not that no one wants to work anymore. People do want to work. They want to be inspired to work hard. However, the institutions that employ them have broken the contract under which that was done. No one is motivated to work _for them_."
pubDate: "Apr 07 2026"
---
This morning Anil Dash posted [Actually, people love to work hard](https://www.anildash.com/2026/04/06/people-love-to-work-hard/). He argues that most people are energized by the opportunity to work hard with other people, if conditions are right. The rise of "nobody wants to work anymore" is just a misunderstanding of motivation.
I've worked as a manager, team lead or staff-level engineer for about a decade and I have seen companies regularly sabotage their own employees by demotivating them. I was fortunate to work at Asana in the late 2010s which put a lot of emphasis on giving the employees an environment to thrive and do their best work. However, since the pandemic, it has become clear that most companies have an adversarial relationship with their employees and have taken many steps that demotivate and alienate their employees. Now we get to listen to business leaders complain about the lack of a motivated work force, showing just how little they deserve our time and energy.
I'm going to be talking mostly from the standpoint of people in the tech industry and heavily biased towards my personal experience.
## What motivates people
Dash lays out four factors that he sees as key for motivating groups of people:
> - A clearly understood goal
> - A common set of values in pursuit of that goal
> - Permission to follow their own ideas to achieve their goal
> - Trust and responsibility to be accountable to one another
>
> If people have these things, and believe in what theyre doing together, they will joyfully work their asses off.
>
> It is genuinely one of the best feelings in life to be completely exhausted while sitting next to someone whos been right beside you, shoulder to shoulder, fighting to accomplish the same goal. Ive known that to be true whether we were launching a new company into the world, campaigning to get a candidate we believed in elected, organizing to rally people around an issue, raising funds for an important cause, or even just trying to get people together for a big event or party.
These four roughly map to a few other frameworks that I've found helpful for exploring motivation, that I've used with my reports, teammates and in my own life.
The first work I read on understanding motivation was [Drive by Daniel H. Pink](https://en.wikipedia.org/wiki/Drive:_The_Surprising_Truth_About_What_Motivates_Us). It's in the vein of popular psychology books that uses research as anecdotes to make an argument. It's a relatively good book and likely worth a read despite being a little out of date since its publication in 2009.
He came up with three factors that motivate people.
- Autonomy: self-direction and clear impact from your work
- Mastery: self-improvement over time
- Purpose: connection to something meaningful or important
This was my baseline framework for about a year. They are very simple and easy to understand. However, I think they miss some additional nuances. For example, I noticed that I often found I lacked motivation when the goals or the steps to get there were unclear.
Later in my career, I had to endure some mandatory management training given by LifeLabs Learning. They had a larger set of five core psychological needs that people want met. This became my go-to framework for evaluating motivation and engagement.
They used the somewhat arbitrary acronym [CAMPS](https://www.lifelabslearning.com/blog/whats-your-camps-score), which is a shame when SCAMP is also a possibility.
- Certainty: having clear expectations and priorities
- Autonomy: Self-direction and ownership
- Meaning: A connection to larger goals and values
- Progress: A sense of progress
- Social Inclusion: A sense of connection and belonging
I can see a lot of similarities in these three frameworks. Each of them encourages autonomy. They all ask that measurable progress be made in some way. All require some sense of progress or improvement which implies some form of measurement.
There are a few differences around the edges. Humans are notoriously messy and hard to quantify. Each of these is trying to reduce that unquantifiable humanity down to a few elements. They each come from slightly different perspectives, but they seem to be describing the same underlying concept.
Some people may resonate more with one framework or another. Some people may find social inclusion to be critical while others may prefer a less social work place. Try them out, see if they resonate with you or your team.
## Why no one wants to work anymore
Anyone who actually empathizes with their workers can see why motivation might be declining these days. The last decade has seen a steady shift in power from workers to corporations. Corporations have gotten larger and more monopolistic. Platforms have been enshitifying everything they can get their hands on, including work. Worker wages have been stagnant while corporate profits, valuations and inflation have all increased.
A great example is return-to-work policies after the pandemic. Employees generally enjoyed remote work flexibility and [productivity generally increased in most industries](https://www.bls.gov/opub/btn/volume-13/remote-work-productivity.htm). Those employees didn't have any trouble being motivated to work! However, companies started to implement restrictive return-to-office policies that took away the autonomy that employees enjoyed. Many employees quit, many returned begrudgingly. The company damaged worker motivation.
Over the last few years I've personally struggled to be motivated to work for most companies. I see virtually every tech company out there trying to monopolize and extract as much value from customers as possible with very little regard for the long-term effects on our society. I've never wanted to work for Facebook because I could see the damage they were doing to information and privacy. I soured on Google after working there from 2012 through when they removed "Don't be evil" from the code of conduct in 2015.
I thought that start-ups would be a good alternative. I see now that they may state a noble mission but their ultimate mission is just to make their founders and investors rich. They exist to widen the wealth gap, not close it. Now every start-up seems to be obsessed with riding the AI hype wave and trying to find new ways to put people out of work. It's gotten so bad that tech workers are darkly joking about it creating a [perpetual underclass](https://www.newyorker.com/culture/infinite-scroll/will-ai-trap-you-in-the-permanent-underclass).
## There are alternatives
Neo-liberal capitalist simps will tell you, "there is no alternative"[^tina]
They are wrong. I believe that there are a few ways that we can get our motivation back and get back to work. And we can't just wait for companies to start caring about workers. Corporations are profit-maximizing AI. They don't actually care about anyone. They are strongly incentivized to do the minimum possible to maximize their profit. The incentives of the system are designed to move towards more profit and find ways to externalize their costs. More on that another time.
The first thing that needs to happen is a realignment of goals and values. This establishes the mission and meaning. We need companies that don't just feel like vehicles for the ultra-rich to extract money from the rest of us. Some people are motivated by making as much money as possible and screwing over everyone else, but I believe those people are in the minority and should really seek therapy.
This can start with something like employee unions, which are at their lowest levels in a century[^union-membership]. These give employees a way to have a stake in the company and make sure that they get a voice in decision making and a share of the profits.
We also need anti-trust and market regulation with some teeth. The smaller companies that are integrated with their communities are being bought up by private equity or displaced by corporate behemoths who care less about the well-being of you or your community.
I think that a real alternative is worker-owned co-ops. Why struggle through aligning workers and owners when we can have a single class of worker-owners? Co-ops have their own complications, such as how decisions are made, but it's near impossible to not feel involved. Many co-ops follow a variant of the [Rochdale Principles](https://en.wikipedia.org/wiki/Rochdale_Principles) which codify all the motivating factors above.
This is the critical first step. We must align the values of a company with the values of the owners. All these other values and crises of motivation will be solvable if companies can be made to care.
## What can we do now?
You can start to think about your motivation right now. Values can vary widely between people and situations. Maybe you value establishing a career or supporting a family and that keeps you focused. Maybe you really care about justice and equity and find that any job without it burns you out.
The most important thing is to realize this before you burn out and need to take a year off to reorient your life. Otherwise you'll be bound for a mid-life crisis.
If you are a leader, think about what motivates your employees. Some of my best teams have come from focusing on making my team as great a place to be as possible. Investing in motivating your employees is the best way to get work done. Any of the frameworks above is a great place to start.
People want to work. I want to work. You probably do too if you made it this far. Humans generally enjoy accomplishing hard things together, from barn-raisings to IPOs. If you think people don't want to work, you probably aren't giving them any reason to work for you.
[^tina]: [TINA](https://en.wikipedia.org/wiki/There_is_no_alternative) is common enough to get its own Wikipedia page! If anyone is trying to tell you not to consider other options, they are probably scared of the other options.
[^union-membership]: [Historical Union membership](https://data.epi.org/unions/union_members_historical/line/year/national/percent_union_members_historical/overall?timeStart=1917-01-01&timeEnd=2024-01-01&dateString=2024-01-01&highlightedLines=overall)

View File

@@ -0,0 +1,31 @@
---
title: Favorite Albums
---
I'm an album listener. I really prefer to listen to a coherent set of songs that are narratively linked and have a cohesive them and tone. The algorithmic feeds never seem to quite leave me happy with the results. Though there is the one exception of 90s rock because that's what I grew up listening to on the radio.
As I remember them and listen to them, I'll add more albums here under various genres.
## Metal
### Unleash the Archers
I've been following this Canadian band since their third album, Time Stands Still. It has some catchy tracks like Test Your Metal.
All their following albums are built around a narrative, which progresses throughout the album. There are a few recurring characters and themes between them such as The Matriarch. I listened to Apex many times, but didn't find myself drawn to Abyss as much initially. However, later I came back to the album and listened to it a dozen times on repeat.
Their latest, Phantoma, didn't have the strength of execution and I've struggled to like it as much as many of the others, even though I enjoy the story. The sand out track for me is Ghost in the Mist, but sometimes I just want to listen to Buried in Code when I'm coding.
### Elevation by We are the Catalyst
This Swedish alt-metal band got my attention with the track Askja. I was fed it by some algorithm or list and it's stuck with me. I've enjoyed that album, Elevation, but never really picked up their others.Their lead singer, Cat Fey, infuses a lot of emotion into her vocals. Their music is melodic and relatively heavy. The singing never quite loses it's clarity, but has the force of screaming.
## Electronic / Synthwave
### Unicorn by GUNSHIP
I've been really liking this album lately. It seems like a collection of concept pieces because GUNSHIP got various guest artists to join for single tracks. The overall album is more on hopeful and nostalgic side of retro-futurism. Bonus points for the Blood for the Blood God reference.
### Scandroid by Scandroid
I'm a sucker for robots. This was actually one of the first albums I encountered that started to embrace the retro-futuristic aesthetic, with a little bit of cyberpunk mixed it. It introduced me to two terms I hope will someday become common: "probots" vs "robophobes".

View File

@@ -0,0 +1,91 @@
---
title: Deploying NixOS on the cloud
tags:
- NixOS
- cloud
---
There are two major things to consider when deploying NixOS systems into the cloud. The first is where to host, the other is how to set it up.
As always, if you have any input such as suggestions, corrections or more data, please [ping me on Mastodon](https://mastodon.social/@periodic) and I'd love to hear from you.
## Providers
There is a list of [NixOS friendly hosters](https://wiki.nixos.org/wiki/NixOS_friendly_hosters) on the wiki, but I have no idea how up-to-date it is.
My criteria are all skewed by the fact that I am an English speaker based on the west coast of the United States. That means additional latency to Europe and I'm subject to a lot of US laws.
- Location
- Local laws and juristictions
- Latency
- Ease of installation
- How easy it is it to install NixOS on their systems? Is it a native option, require a custom image, or a more specialized option?
- Speed
- How much latency is in the network?
- Price
- How much does a box cost? VPS instances are fairly commodified at this point, so prices should be fairly comparable. My benchmark is 2 vCPU with 4 GB of RAM because that will serve most small sites well with overhead for other tasks.
- Support
- How easy is it to contact support?
- Do they speak my language?
- Other offerings
- What other services do they offer if I should choose to expand or require more specialized services? Database hosting, CDN, file hosting, etc.
- Company Values
- I want a company that matches my values and isn't just an autonomous extractive entity.
### Providers I've investigated
| Provider | Location | NixOS Support | Notes |
| ----------------------------------- | -------------- | ------------- | --------------------------------------------------------------------------------------------- |
| [Gandi.net](https://www.gandi.net/) | France | Native | Had some billing, latency and packet loss issues. |
| Linode/Akamai | US/Global | Custom ISO | Still setting up |
| Hetzner | Germany/US | Custom ISO | I got blocked here because they do not accept credit-card payment without contacting support. |
| [vpsFree.cz](https://vpsfree.org/) | Czech Republic | Native | They operate as a "community" more than a company. |
| [Crocuda.com](https://crocuda.com/) | France | Native | Only accept payment in crypto. IPv6 only. They pride themselves on having no KYC. |
#### Gandi.net
Last tried: May 2026
I was drawn to them initially because they are based in France and seem relatively professional. I've also heard their name come up positively in discussions of domain registrars, so I thought I'd give them a try.
I was able to set up an account fairly quickly, though initially my billing didn't go through and my account got stuck in a state of pending a charge to fund a pre-paid account. I tried initiating payment for that order, but that just resulted in a new order being created and paid, leaving my account hanging and pending billing verification. Eventually I figured out that I could cancel that first order, putting my account back into the initial state and allowing me to set up billing again which worked.
Setting up a NixOS system was trivial. It is an option in the provisioning flow. I was able to have the system up and running within minutes. They provide a very minimal Nix config whihc is mostly just hardware settings. It was a simple matter to integrate that into a new host configuration, checking that out on the host, building and reboot.
The major issue that I had with this provider was latency. I had a minimum latency of about 150ms, which is just enough to create a noticeable lag when typing. There were occasional spikes in latency and dropped packets which were far more annoying. These would happen for about 1% of pings that I measured and could result in dropped packets for a few seconds at a time.
#### Linode
TODO
#### Hetzner
TODO
#### Crocuda
They have a pretty slick terminal UI that you can access over SSH. It labels itself as being alpha status.
However, only accepting crypto for payments is a bit of a non-started for me. It's a bit odd since their pricing is all listed in dollars. They'll give you about $1 in credits, which is enough for about 5 days of usage.
They also pride themselves on no KYC. That's maybe nice from a privacy standpoint, but they can still access everything on the systems, so your data isn't private. This makes me think my neighbors on the boxes will not be the most savory folks. I would be wary of my data getting wrapped up in some criminal activity or having my capacity impacted by the actions of my neighbors. A company like this may attract clients who are prone to activities that would get them in trouble with other providers. That could include using exploits to break out of their VMs and affect my data or the stability of the system.
They seem to only support IPv6, which makes it hard to use as a web server.
Prices seem very competitive, at about $10/month for my target system.
Latency is around 180ms.
## Installation
If a provider doesn't support native NixOS then there are a few options. If they support a custom ISO then I can create a NixOS image and load that. Another option is to use one of the tools that allow you to convert an existing system into NixOS, such as [nixos-anywhere](https://github.com/nix-community/nixos-anywhere/tree/main) or [nixos-infect](https://github.com/elitak/nixos-infect).
TODO
## Deployments and management
TODO
- [morph](https://github.com/DBCDK/morph) - Morph is a tool for managing existing NixOS hosts - basically a fancy wrapper around nix-build, nix copy, nix-env, /nix/store/.../bin/switch-to-configuration, scp and more. Morph supports updating multiple hosts in a row, and with support for health checks makes it fairly safe to do so.
- [colmena](https://github.com/zhaofengli/colmena) - Colmena is a simple, stateless NixOS deployment tool modeled after NixOps and morph, written in Rust. It's a thin wrapper over Nix commands like nix-instantiate and nix-copy-closure, and supports parallel deployment.
- [NixOps](https://github.com/NixOS/nixops) - NixOps is a tool for deploying to NixOS machines in a network or the cloud.

View File

@@ -0,0 +1,40 @@
---
title: Running Untrusted Code
---
What is untrusted code? It's any code for which you cannot vouch for the entire chain of custody. Any code you don't trust should be in a sandbox with as little privilege as possible.
This includes a lot of scripts that you might get from the internet. Some scripts are small enough to review yourself, but honestly, who wants to do that every time? There are tons of small scripts and utilities I might want to run that I don't trust.
Another case is LLM output. Who knows what the LLM might output? They hallucinate and make many mistakes. No one wants to accidentally run `rm -rf /` or anything like it.
There's also the case of supply chain attacks. We trust a lot of code that we download implicitly. A lot of code dependencies are downloaded and executed as part of programming. Sometimes these have malicious code slipped in.
However, you have to draw the line somewhere. For example, it's hard to trust every package on your OS. If someone slipped malicious code into my window manager I would be pretty fucked.
## Using Docker
My favorite trick for running untrusted code is to run it in a small one-time container.
Here's an example for running a python script.
```bash
docker run --rm \
-v ~/src/repo:/repo:ro \ # Mount this RO unless it needs to do something like NPM builds or VENV installs
-v ~/data:/data:ro \ # Mount RO to prevent modification.
--network none \ # No network
--cap-drop ALL \ # Drop other capabilities
--security-opt no-new-privileges \ # Make sure it gets no new ones
python:3.12-slim python3 /repo/untrusted_script.py
```
Some additional optional arguments if you are concerned:
```bash
--read-only \ # Make the container RO
--tmpfs /tmp \ # Give it some dedicated scratch space
--memory 512m \ # Limit memory
--cpus 1 \ # Limit CPU
--pids-limit 100 \ # Prevent fork bombing
--user 1000:1000 # Run as a non-root user to further limit capabilities and prevent root access to whatever you do give it
```

View File

@@ -0,0 +1,57 @@
---
title: On 2" Ruins
tags:
- Warhammer
- 40k
- rules
---
# On 2" Ruins
I've seen an idea floating around that the 2" ruins on GW layouts have a special rule that vehicles are never allowed to set-up or end a movement on them. I cannot find anywhere that this is supported by the rules so I consider a house-rule. Here is some information about how these ruins work.
## What do the blue ruins mean?
The Chapter Approved tournament guide has a section on terrain layouts. First, they describe their lyouts as purely guidelines. It's up to each event to interpret rules however they wish. Here's how they describe it.
> The following layouts primarily use the Ruins terrain feature. This efficiently achieves a good amount of line-of-sight blockage and cover appropriate for balanced games, thanks to the natural abstraction of line of sight within the rules for Ruins. Remember that a variety of terrain heights not only adds to the immersive nature of the battlefield, but is also important for line of sight and rules such as Plunging Fire. For organisers and players with a more robust terrain collection (especially elements that block true line of sight), incorporating features such as Woods, Barricades and Hills into your chosen layouts is perfectly acceptable.
Then they go on to explain what the ruins mean. Gray areas are "More than 4in" and the blue areas are "2in or less".
> For model mobility purposes, we have shaded the area terrain outlines in the above colours to show our recommendations for how tall the terrain should be in each section.
>
> [...]
>
> These height and Ruin placement guidelines help provide a balanced tournament experience; as organisers you are free to adjust this to suit your terrain collection.
That's it. Nothing special about the blue ruins except that they should be 2" or less in height. Basically that hits the cut-off that those terrain features do not have to be accounted for during movement.
There are various other parts of the rules that cover ruins, but those are for ruins in general and don't say anything about differing heights of ruins. You can find it in the core rules section `Terrain Features -> Ruins`. The Rules Commentary has some more detailed explanation of how line of sight works through, in and out of ruins. The word "ruin" doesn't even occur in the official FAQ. I don't see any information about this in the WTC FAQ. I have been unable to find any large-scale ITC FAQ since that was taken over by GW and is therefore presumed replaced by the GW FAQ.
In short, blue ruins are just ruins like anything else. The ground floor acts just like the ground floor of other ruins. The terrain acts just like terrain in other ruins so models can overlap the terrain and the footprint as long as they wouldn't intersect with the physical terrain.
## Tournament Exceptions
A common exception is to treat all walls in ruins as if they were infinitely tall. This means that a model cannot be set up such that it would overhang any wall. These tournaments often do not place any walls in the 2" ruin areas because those are meant to never be over 2" tall.
## Some examples
So there is nothing that prevents vehicles from being inside those ruins. Let's find some examples from prominent streams with very experienced players!
### LVO 2025
These games were cast on Wargames Live. In addition to the ever-present Joe, they have a guest commentator in the form of Jack Harpster from Art of War. The games were also (usually?) observed by a judge. These people have seen a _lot_ of events.
[Here's an example from Round 1](https://youtu.be/HgshKDfWYyU?t=5284) on Layout 1 where John Kilcommons charges a Sky Ray Gunship into a small 2" ruin to engage Michael Hoopeer's Demon Spawn on the other side. It's actually a very interesting case because John is able to engage the Spawn through the ruin by being within 1" engagement range. He rolled low enough that he does not have to go around and fully base-to-base which would have opened him up to a heroic intervention.
![[images/2026-05-01_LVO_round_1.png]]
[Here's another example from Round 3](https://youtu.be/HgshKDfWYyU?t=31597) on Layout 1 where Durante Bozzini moves his Ghostkeel (a walker vehicle) into the 2" ruins on the side in order to get better angles.
![[images/2026-05-01_LVO_round_3.png]]
[Here in Round 4](https://youtu.be/9FTPZxTcbic?t=4283) on Layout 4, Forrest Phanton moves his Land Raider into a blue ruin in corner before disembarking, with a judge watching over his shoulder. If you watch until 10:33:00 Joe makes a comment about it.
> They just talked about it, Andrew. So vehicles can fit on the footprint, they just can't stand on top of the wall. It's a pretty standard interpretation on the GW layouts, I think. Sometimes they're crates though, so there's not a lot of room to stand on those things.
![[images/2026-05-01_LVO_round_4.png]]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 KiB

50
src/content/notes/now.md Normal file
View File

@@ -0,0 +1,50 @@
---
title: Now
---
This is my [Now Page](https://nownownow.com/about). It hopes to answer the question, **"what are you doing these days?"**
## Pets
| Rosie | Bingley | Clover |
| --------------------- | ----------------------------------- | --------------------------------- |
| ![[images/rosie.jpg]] | ![[images/bingley.jpg]] | ![[images/clover.jpg]] |
| 8 yeas old | 8 years old | 3 years old |
| GSD / Husky / AmStaff | Husky / AmStaff | Terrier / Chihuahua / many others |
| Loves sun, her ball | Loves food, wrestling, being a lump | Loves barking, chasing |
## Projects
- Creating and improving my personal site, Blazestar.net
- Comments
- [Webmentions](https://webmention.io/)
- More content
- Exploring ways to use Haskell for various projects, particularly the web
- Maintaining our forest of timber bamboo
- Logging observations of all the [Messier Objects](https://en.wikipedia.org/wiki/Messier_object)
## Hobbies
- Playing miniature war games, mostly Warhammer 40k
- Weekly games and monthly tournaments at [Great Escape Games](https://greatescapegames.com/)
- T'au Empire: Printed and painted around 3,000 points of [FishMech](https://pipermakes.art/collections/fishmech) proxies
- Blood Angels: Printed and painted 2,000 points of various proxies
- Enjoying World of Warcraft: Midnight
- [Main Tank](https://raider.io/characters/us/tichondrius/Ironsoul) for &lt;Mitsakes Were Made&gt;
- Hiking and camping in the High Sierras
- Star Gazing with the [Sacramento Valley Astronomical Society](https://www.svas.org/)
- Current equipment:
- Meade 2160 254mm SCT
- Stellarview ST102 102mm refractor
- iOptron AX Mount Pro
## Media
- Re-reading Lord of the Rings
- Listening to [Marketplace](https://www.marketplace.org/) for economic news
- Reading many blogs I'll get around to listing some time
## Personal
- Developing my physical health and mental health
- Finding my place in swiftly changing culture, economy and technology

View File

@@ -26,8 +26,7 @@ they are living and growing in line with their ideals.
- Take responsibility for your own actions.
- Hold others accountable and responsible for their actions.
- Give honest feedback.
- Take the time to do good work.
- Be proud of your work.
- Take the time to do good work and be proud of it.
- Be your authentic self.
### Sustainability and long-term thinking
@@ -47,8 +46,7 @@ they are living and growing in line with their ideals.
### Self-improvement
- Be curious and motivated to learn and understand.
- Be open to feedback and learning.
- Learn new things.
- Be open to feedback.
- Seek to understand.
- Embrace failure as an opportunity to learn.
- Take the time to review and retrospect.

View File

@@ -7,3 +7,11 @@ Hi! I'm Drew, but I also go by `Periodic` in most online spaces. I am a human be
I do full stack web-development: A little front-end, a little back-end, database optimization, infrastructure automation, the whole deal.
I have been a software developer from a young age and have spent two decades in the tech industry. I've done freelance, start-ups (Asana, Fossa) and some big corps (Google, Visa).
See my [now page](/now) for more about what I'm focused on right now.
## What is "Blaze Star"?
I picked up this domain about a year ago. I was looking for some places to host some of my own things. There's a domain I've had for over a decade but never really liked. I wanted something memorable, a bit obscure, but easy to describe.
I came across Blaze Star when there was talk of the star [T Coronae Borealis](https://en.wikipedia.org/wiki/T_Coronae_Borealis) flaring in 2025. It is theorized to have a roughly 80-year cycle, though this latest one has yet to materialize. It has the official proper name "Blaze Star". I thought it particularly fitting since I've been using the handle `Periodic` for over a decade now.

123
src/layouts/BlogList.astro Normal file
View File

@@ -0,0 +1,123 @@
---
import NotchedBox from '../components/NotchedBox.astro';
import RootLayout from '../layouts/RootLayout.astro';
import FormattedDate from '../components/FormattedDate.astro';
import { Image } from 'astro:assets';
import { getPublishedPosts, getAllTags } from '../lib/blog';
export interface Params {
selectedTag?: string | undefined;
}
const { selectedTag } = Astro.props;
const tags = await getAllTags();
const posts = await getPublishedPosts( selectedTag );
---
<RootLayout>
<style>
ul.tags {
list-style: none;
margin: 0;
margin-bottom: 1em;
padding: 0;
display: flex;
flex-direction: row;
gap: 16px;
align-items: baseline;
li a {
color: var(--color-gray);
text-decoration: none;
}
li.selected-tag a {
color: var(--color-gold);
}
}
ul.posts {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 16px;
align-items: stretch;
li {
/* Required since flex won't render list-elements correctly. */
display: block;
}
a {
text-decoration: none;
.title, .description {
color: var(--color-light-text);
}
.date {
color: var(--color-gray);
font-size: var(--font-size-sm);
}
.entry {
padding: 0.5em;
}
p {
margin: 0;
}
}
a:hover .title {
color: var(--color-accent);
}
}
</style>
<section>
<ul class="tags">
{
tags.map(tag =>
<li class={ tag === selectedTag ? "selected-tag" : "" }>
<a href={`/blog/tag/${tag}`}>
<NotchedBox color={ tag === selectedTag ? "gold" : "gray" } fillNotches={ tag === selectedTag ? "right" : "none"}>
{tag}
</NotchedBox>
</a>
</li>
)
}
</ul>
</section>
<section>
<ul class="posts">
{
posts.map((post) => (
<li>
<a href={`/blog/${post.id}/`}>
<NotchedBox fillNotches="left">
<div class="entry">
{post.data.heroImage && (
<Image width={720} height={360} src={post.data.heroImage} alt="" />
)}
<h4 class="title">{post.data.title}</h4>
<p class="date">
<FormattedDate date={post.data.pubDate} />
</p>
{ post.data.description &&
<p class="description">
{post.data.description}
</p>
}
</div>
</NotchedBox>
</a>
</li>
))
}
</ul>
</section>
</RootLayout>

View File

@@ -1,40 +1,89 @@
---
import type { MarkdownHeading } from "astro";
import type { CollectionEntry } from 'astro:content';
import RootLayout from '../layouts/RootLayout.astro';
import FormattedDate from '../components/FormattedDate.astro';
import NotchedBox from '../components/NotchedBox.astro';
import TableOfContents from '../components/TableOfContents.astro';
import { Image } from 'astro:assets';
type Props = CollectionEntry<'blog'>['data'];
type Props = CollectionEntry<'blog'>['data'] & { slug: string; headings: MarkdownHeading[] };
const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
const { slug, title, description, pubDate, updatedDate, heroImage, tags, headings } = Astro.props;
---
<style>
.byline {
color: var(--color-gray);
}
.title {
margin-top: 1em;
h1 {
margin: 0;
}
}
.permalink {
color: var(--color-gray);
font-size: 80%;
}
.dt-published {
font-size: var(--font-size-sm);
}
.tags {
display: flex;
flex-direction: row;
gap: 16px;
a {
color: var(--color-gray);
}
}
</style>
<RootLayout title={title} description={description}>
<main>
<NotchedBox fillNotches="left">
<article>
<div class="hero-image">
{heroImage && <Image width={1020} height={510} src={heroImage} alt="" />}
</div>
<div class="prose">
<div class="title">
<div class="date">
{ pubDate && <FormattedDate date={pubDate} /> }
{
updatedDate && (
<div class="last-updated-on">
Last updated on <FormattedDate date={updatedDate} />
</div>
)
}
</div>
<h1>{title}</h1>
<hr />
</div>
<slot />
</div>
</article>
</NotchedBox>
</main>
<NotchedBox fillNotches="left">
<article class="h-entry">
<div class="hero-image">
{heroImage && <Image width={1020} height={510} src={heroImage} alt="" />}
</div>
<div class="prose">
<div class="byline">
<div>
<time class="dt-published">{ pubDate && <FormattedDate date={pubDate} /> }</time>
by
<a class="p-author h-card u-url" href="https://www.blazestar.net">Periodic</a>
</div>
<div>
<a class="permalink u-url" href={`https://blazestar.net/blog/${slug}`}>Permalink</a>
</div>
{
updatedDate && (
<div class="last-updated-on dt-updated">
Last updated on <FormattedDate date={updatedDate} />
</div>
)
}
</div>
<div class="title">
<h1 class="p-name">{title}</h1>
</div>
{ tags &&
<div class="tags">
{
tags.map(tag =>
<a href={`/blog/tag/${tag}`} >#{tag}</a>
)
}
</div>
}
<hr />
<TableOfContents headings={headings} />
<div class="e-content">
<slot />
</div>
</div>
</article>
</NotchedBox>
</RootLayout>

View File

@@ -33,7 +33,7 @@ const { title = SITE_TITLE, description = SITE_DESCRIPTION } = Astro.props;
</div>
</body>
<style>
@media (max-width: 800px) {
@media (max-width: 1000px) {
body {
grid-template-areas:
"header"
@@ -42,6 +42,9 @@ const { title = SITE_TITLE, description = SITE_DESCRIPTION } = Astro.props;
grid-template-columns: 1fr;
margin: 1rem 0.5rem;
}
main {
padding: 1em;
}
.header {
display: block;
}
@@ -49,7 +52,8 @@ const { title = SITE_TITLE, description = SITE_DESCRIPTION } = Astro.props;
display: none;
}
}
@media (min-width: 800px) {
@media (min-width: 1000px) {
body {
grid-template-areas:
"header header"
@@ -66,6 +70,13 @@ const { title = SITE_TITLE, description = SITE_DESCRIPTION } = Astro.props;
display: block;
}
}
@media (max-width: 500px) {
body {
font-size: 14px;
}
}
body {
display: grid;
}

26
src/lib/blog.ts Normal file
View File

@@ -0,0 +1,26 @@
import { type CollectionEntry, getCollection } from "astro:content";
function publishedOnly(
p: CollectionEntry<"blog">,
): p is CollectionEntry<"blog"> & { data: { pubDate: Date } } {
return p.data.pubDate !== undefined;
}
export async function getPublishedPosts(
tag?: string,
): Promise<CollectionEntry<"blog">[]> {
const allPosts = (await getCollection("blog", publishedOnly)).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
return tag ? allPosts.filter((p) => p.data.tags?.includes(tag)) : allPosts;
}
export async function getAllTags(): Promise<string[]> {
const posts = await getCollection("blog", publishedOnly);
return Object.keys(
Object.fromEntries(
posts.flatMap((p) => p.data.tags || []).map((t) => [t, 1]),
),
);
}

49
src/lib/headings.ts Normal file
View File

@@ -0,0 +1,49 @@
import type { MarkdownHeading } from "astro";
export type HeadingsTree = Array<MarkdownHeading | HeadingsTree>;
// could take a list like [5, 1, 2, 2, 3, 5, 1] and should return
// [ [ [ [ [ 5 ] ] ] ], 1, [ 2, 2, [3, [[5]]]], 1]
export function makeHeadingsTree(headings: MarkdownHeading[]): HeadingsTree {
let index = 0;
let currDepth = 1;
const treeStack = [[]] as Array<HeadingsTree>;
while (index < headings.length) {
const currHeading = headings[index];
// If the next heading is at the current depth, push it into the tree
if (currHeading.depth == currDepth) {
treeStack[treeStack.length - 1].push(currHeading);
index += 1;
}
// If the next heading is deeper, go deeper
if (currHeading.depth > currDepth) {
const nextLevel = [] as HeadingsTree;
treeStack[treeStack.length - 1].push(nextLevel);
treeStack.push(nextLevel);
currDepth += 1;
}
// If the next heading is shallower, back out
if (currHeading.depth < currDepth) {
treeStack.pop();
currDepth -= 1;
}
}
return treeStack[0];
}
export function removeEmptyLevels(tree: HeadingsTree): HeadingsTree {
const newLevel = tree.map((node) =>
Array.isArray(node) ? removeEmptyLevels(node) : node,
);
if (tree.every(Array.isArray)) {
return tree.flatMap((node) => node);
}
return tree;
}

38
src/lib/hierarchy.ts Normal file
View File

@@ -0,0 +1,38 @@
export interface HierarchyNode {
id: string | null;
title: string | null;
children: Hierarchy | null;
}
export type Hierarchy = Record<string, HierarchyNode>;
function addToHierarchy(
tree: Hierarchy,
[noteId, title]: [string, string],
): Hierarchy {
const path = noteId.split("/");
if (path.length === 0 || path[0] === "") {
return tree;
}
// Create a dummy node to start the loop since the Hierarchy root is not a node.
let curr: HierarchyNode = { id: null, title: null, children: tree };
for (const node of path) {
// Capitalize first letter
const nodeName = node.charAt(0).toUpperCase() + node.slice(1);
curr.children ||= {};
if (!curr.children[nodeName]) {
curr.children[nodeName] = { id: null, title: null, children: null };
}
curr = curr.children[nodeName];
}
curr.id = noteId;
curr.title = title;
return tree;
}
export function makeHierarchy(notes: Array<[string, string]>): Hierarchy {
return notes.reduce(addToHierarchy, {});
}

View File

@@ -15,6 +15,6 @@ const { Content } = await render(about);
<RootLayout>
<NotchedBox fillNotches="left">
<h1>{about.data.title}</h1>
<Content />
<Content />
</NotchedBox>
</RootLayout>

View File

@@ -13,9 +13,10 @@ export async function getStaticPaths() {
type Props = CollectionEntry<'blog'>;
const post = Astro.props;
const { Content } = await render(post);
const { slug } = Astro.params;
const { Content, headings } = await render(post);
---
<BlogPost {...post.data}>
<BlogPost slug={slug} headings={headings} {...post.data} >
<Content />
</BlogPost>

View File

@@ -1,85 +1,5 @@
---
import NotchedBox from '../../components/NotchedBox.astro';
import RootLayout from '../../layouts/RootLayout.astro';
import { getCollection, type CollectionEntry } from 'astro:content';
import FormattedDate from '../../components/FormattedDate.astro';
import { Image } from 'astro:assets';
function publishedOnly(p: CollectionEntry<'blog'>): p is (CollectionEntry<'blog'> & { data: { pubDate: Date }}) {
return p.data.pubDate !== undefined;
}
const posts = (await getCollection('blog', publishedOnly))
.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
import BlogList from '../../layouts/BlogList.astro';
---
<RootLayout>
<style>
ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 16px;
align-items: stretch;
li {
/* Required since flex won't render list-elements correctly. */
display: block;
}
a {
text-decoration: none;
.title, .description {
color: var(--color-light-text);
}
.date {
color: var(--color-gray);
}
.entry {
padding: 0.5em;
}
p {
margin: 0;
}
}
a:hover .title {
color: var(--color-accent);
}
}
</style>
<section>
<ul>
{
posts.map((post) => (
<li>
<a href={`/blog/${post.id}/`}>
<NotchedBox fillNotches="left">
<div class="entry">
{post.data.heroImage && (
<Image width={720} height={360} src={post.data.heroImage} alt="" />
)}
<h4 class="title">{post.data.title}</h4>
<p class="date">
<FormattedDate date={post.data.pubDate} />
</p>
{ post.data.description &&
<p class="description">
{post.data.description}
</p>
}
</div>
</NotchedBox>
</a>
</li>
))
}
</ul>
</section>
</RootLayout>
<BlogList/>

View File

@@ -0,0 +1,20 @@
---
import { type CollectionEntry, getCollection } from 'astro:content';
import BlogList from '../../../layouts/BlogList.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog');
const tags = posts.flatMap(p => p.data.tags);
return tags.map((tag) => ({
params: { tag: tag },
props: { tag: tag },
}));
}
type Props = { tag: string }
const tag = Astro.props.tag;
---
<BlogList selectedTag={tag} />

View File

@@ -0,0 +1,37 @@
import { type CollectionEntry, getCollection } from "astro:content";
import type { AstroUserConfig } from "astro";
import { SITE_TITLE, SITE_DESCRIPTION } from "../../../consts";
import rss from "@astrojs/rss";
import { getPublishedPosts } from "../../../lib/blog";
function publishedOnly(
p: CollectionEntry<"blog">,
): p is CollectionEntry<"blog"> & { data: { pubDate: Date } } {
return p.data.pubDate !== undefined;
}
export async function GET(context: AstroUserConfig) {
const { tag: selectedTag } = context.params;
const posts = await getPublishedPosts(selectedTag);
return rss({
title: `${SITE_TITLE} - ${selectedTag}`,
description: SITE_DESCRIPTION,
site: context.site as string,
items: posts.map((post) => ({
...post.data,
link: `/blog/${post.id}/`,
})),
});
}
export async function getStaticPaths() {
const posts = await getCollection("blog", publishedOnly);
const tags = posts.flatMap((p) => p.data.tags);
return tags.map((tag) => ({
params: { tag: tag },
props: { tag: tag },
}));
}

View File

@@ -0,0 +1,38 @@
---
import RootLayout from '../../layouts/RootLayout.astro';
import NotchedBox from '../../components/NotchedBox.astro';
import FormattedDate from '../../components/FormattedDate.astro';
import TableOfContents from '../../components/TableOfContents.astro';
import { type CollectionEntry, getCollection } from 'astro:content';
import { render } from 'astro:content';
export async function getStaticPaths() {
const notes = await getCollection('notes');
return notes.map((note) => ({
params: { slug: note.id },
props: note,
}));
}
type Props = CollectionEntry<'notes'>;
const note = Astro.props;
const { Content, headings } = await render(note);
---
<RootLayout>
<NotchedBox fillNotches="left">
<h1>{note.data.title}</h1>
<div class="last-updated">
Last updated <FormattedDate date={note.data.updated} />
</div>
<TableOfContents headings={headings}/>
<Content />
</NotchedBox>
</RootLayout>
<style>
.last-updated {
font-size: var(--font-size-sm);
color: var(--color-gray);
}
</style>

View File

@@ -17,12 +17,15 @@
--background-light: var(--color-space-blue-light);
--font-size-sm: calc(var(--font-size-md) / 1.2);
--font-size-md: 20px;
--font-size-md: 18px;
--font-size-lg: calc(var(--font-size-md) * 1.2);
--font-size-xl: calc(var(--font-size-lg) * 1.2);
--font-size-2xl: calc(var(--font-size-xl) * 1.2);
--font-size-3xl: calc(var(--font-size-2xl) * 1.2);
--font-size-4xl: calc(var(--font-size-3xl) * 1.2);
--width-full: 1000px;
--width-narrow: 800px;
--width-mobile: 500px;
}
@font-face {
@@ -107,6 +110,7 @@ body {
font-size: var(--font-size-md);
line-height: 1.7;
background-color: var(--color-space-blue);
min-width: 360px;
}
main {
@@ -123,19 +127,21 @@ h6 {
}
/* Progressive 1.2x scaling from the base */
h1 {
font-size: var(--font-size-4xl);
}
h2 {
font-size: var(--font-size-3xl);
}
h3 {
h2 {
font-size: var(--font-size-2xl);
}
h4 {
h3 {
font-size: var(--font-size-xl);
}
h4 {
font-size: var(--font-size-lg);
font-weight: bold;
}
h5 {
font-size: var(--font-size-lg);
font-style: italic;
}
strong,
b {
@@ -176,27 +182,25 @@ code {
pre {
padding: 1.5em;
border-radius: 8px;
overflow: scroll;
}
pre > code {
all: unset;
}
blockquote {
border-left: 4px solid var(--accent);
padding: 0 0 0 20px;
margin: 0px;
font-size: 1.333em;
border-left: 4px solid var(--color-gray);
padding: 0 8px 0 16px;
margin: 0;
background-color: var(--color-space-blue-light);
}
hr {
border: none;
border-top: 1px solid rgb(var(--gray-light));
}
@media (max-width: 720px) {
body {
font-size: 18px;
}
main {
padding: 1em;
}
figure {
margin: 8px;
}
.sr-only {