<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Foundry</title>
    <link>https://foundry.sodalabs.io/</link>
    <description>Foundry is where Lachlan O'Neill writes up engineering and research projects: what was built, why, and what was learned along the way.</description>
    <language>en-au</language>
    <lastBuildDate>Tue, 06 Oct 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://foundry.sodalabs.io/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>Hello, Foundry</title>
      <link>https://foundry.sodalabs.io/posts/hello-foundry/</link>
      <guid isPermaLink="true">https://foundry.sodalabs.io/posts/hello-foundry/</guid>
      <pubDate>Tue, 06 Oct 2026 00:00:00 GMT</pubDate>
      <description>A new home for project write-ups, and a tour of the bespoke static-site pipeline that builds it from a folder of Markdown.</description>
      <category>meta</category>
      <category>tooling</category>
      <content:encoded><![CDATA[<p>This is the first post on Foundry, so it doubles as a tour of the machinery behind it. Everything you see here is produced by a short TypeScript build script that reads a folder of Markdown files and writes static HTML. No framework, no runtime, nothing to log in to.</p>
<h2 id="why-build-it-rather-than-pick-one" tabindex="-1">Why build it rather than pick one <a class="heading-anchor" href="#why-build-it-rather-than-pick-one" aria-hidden="true">#</a></h2>
<p>There are excellent static-site generators. I didn’t use one because the thing I actually want is tiny: a loop over Markdown files, a template, and a deploy. Owning that loop means every behaviour on this site is a few lines I can read in one sitting, and there is nothing to upgrade when the ecosystem moves on. The genuinely hard parts (parsing Markdown, highlighting code, typesetting maths) are delegated to boring, stable libraries.</p>
<figure><img src="https://foundry.sodalabs.io/posts/hello-foundry/pipeline.svg" alt="Content goes in as Markdown, comes out as a static site, and GitHub Actions ships it to Pages." loading="lazy" decoding="async"><figcaption>Content goes in as Markdown, comes out as a static site, and GitHub Actions ships it to Pages.</figcaption></figure>
<h2 id="what-a-post-can-contain" tabindex="-1">What a post can contain <a class="heading-anchor" href="#what-a-post-can-contain" aria-hidden="true">#</a></h2>
<p>Each post is a folder with an <code>index.md</code> and whatever assets it needs beside it. Images, data files and demos are copied through verbatim, so relative links just work.</p>
<h3 id="code" tabindex="-1">Code <a class="heading-anchor" href="#code" aria-hidden="true">#</a></h3>
<p>Fenced blocks are highlighted at build time with <a href="https://shiki.style" rel="noopener">shiki</a>, using the same grammars as VS Code. Both light and dark colours are emitted, and CSS picks one based on the system theme, so there is no flash and no JavaScript. An optional label after the language names the file:</p>
<div class="codeblock" data-lang="ts"><div class="codeblock-label">build/build.ts</div><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> post</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> of</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> posts) {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> rendered</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> await</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> renderer.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">render</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(post.markdown);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> writeFile</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'posts'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, post.slug, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'index.html'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">), </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">postPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(post, rendered, tagSlugs));</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  await</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> copyDir</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(post.sourceDir, path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(paths.dist, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'posts'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, post.slug), (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">src</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    const</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> name</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">basename</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(src);</span></span>
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> name </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">!==</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'index.md'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> &#x26;&#x26;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">name.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">startsWith</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'.'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">  });</span></span>
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre></div>
<p>Shell, Python, Rust, YAML and a couple of hundred other languages are available; anything unrecognised falls back to plain text rather than failing the build.</p>
<div class="codeblock" data-lang="sh"><pre class="shiki shiki-themes github-light github-dark" style="--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> run</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> new</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> --</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> "Title of the next post"</span></span>
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> run</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> dev</span></span></code></pre></div>
<h3 id="maths" tabindex="-1">Maths <a class="heading-anchor" href="#maths" aria-hidden="true">#</a></h3>
<p>Inline maths like <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="script">O</mi><mo stretchy="false">(</mo><mi>n</mi><mi>log</mi><mo>⁡</mo><mi>n</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\mathcal{O}(n \log n)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathcal" style="margin-right:0.0278em;">O</span><span class="mopen">(</span><span class="mord mathnormal">n</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop">lo<span style="margin-right:0.0139em;">g</span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">n</span><span class="mclose">)</span></span></span></span> and display maths are rendered to HTML with KaTeX at build time, so the page needs only a stylesheet:</p>
<p class="katex-block"><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>H</mi><mo stretchy="false">(</mo><mi>X</mi><mo stretchy="false">)</mo><mo>=</mo><mo>−</mo><munderover><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>n</mi></munderover><mi>p</mi><mo stretchy="false">(</mo><msub><mi>x</mi><mi>i</mi></msub><mo stretchy="false">)</mo><msub><mrow><mi>log</mi><mo>⁡</mo></mrow><mn>2</mn></msub><mi>p</mi><mo stretchy="false">(</mo><msub><mi>x</mi><mi>i</mi></msub><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">H(X) = -\sum_{i=1}^{n} p(x_i) \log_2 p(x_i)
</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal" style="margin-right:0.0813em;">H</span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.0785em;">X</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.9291em;vertical-align:-1.2777em;"></span><span class="mord">−</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.6514em;"><span style="top:-1.8723em;margin-left:0em;"><span class="pstrut" style="height:3.05em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">i</span><span class="mrel mtight">=</span><span class="mord mtight">1</span></span></span></span><span style="top:-3.05em;"><span class="pstrut" style="height:3.05em;"></span><span><span class="mop op-symbol large-op">∑</span></span></span><span style="top:-4.3em;margin-left:0em;"><span class="pstrut" style="height:3.05em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight">n</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:1.2777em;"><span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">p</span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mop"><span class="mop">lo<span style="margin-right:0.0139em;">g</span></span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.207em;"><span style="top:-2.4559em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight">2</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.2441em;"><span></span></span></span></span></span></span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">p</span><span class="mopen">(</span><span class="mord"><span class="mord mathnormal">x</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3117em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">i</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mclose">)</span></span></span></span></span></p>
<p>The KaTeX stylesheet is only included on pages that actually contain maths.</p>
<h3 id="figures-tables-and-asides" tabindex="-1">Figures, tables and asides <a class="heading-anchor" href="#figures-tables-and-asides" aria-hidden="true">#</a></h3>
<p>A paragraph containing nothing but an image becomes a <code>&lt;figure&gt;</code> with the alt text as the caption, which is what happened to the diagram above. Tables are plain GitHub-flavoured Markdown:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Required</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>title</code></td>
<td>yes</td>
<td>Shown everywhere</td>
</tr>
<tr>
<td><code>date</code></td>
<td>yes</td>
<td>Ordering and the feed</td>
</tr>
<tr>
<td><code>summary</code></td>
<td>yes</td>
<td>Post list, feed, link previews</td>
</tr>
<tr>
<td><code>tags</code></td>
<td>no</td>
<td>Grouping; tag pages are generated</td>
</tr>
<tr>
<td><code>draft</code></td>
<td>no</td>
<td>Hidden from production builds</td>
</tr>
<tr>
<td><code>updated</code></td>
<td>no</td>
<td>Shown alongside the publish date</td>
</tr>
<tr>
<td><code>image</code></td>
<td>no</td>
<td>Social card image for link previews</td>
</tr>
</tbody>
</table>
<blockquote>
<p>Blockquotes work for asides and pulled quotes. Footnotes work too, for the things that don’t belong in the main thread.<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p>
</blockquote>
<h3 id="interactive-demos" tabindex="-1">Interactive demos <a class="heading-anchor" href="#interactive-demos" aria-hidden="true">#</a></h3>
<p>Write-ups about simulations or visualisations want something you can poke at. The convention here is deliberately low-tech: a self-contained <code>index.html</code> in the post folder, embedded with an <code>&lt;iframe&gt;</code>. It can use any library it likes and can’t interfere with the rest of the page.</p>
<iframe class="demo" src="https://foundry.sodalabs.io/posts/hello-foundry/demo/index.html" title="Phyllotaxis demo" height="360" loading="lazy"></iframe>
<p>That one is a phyllotaxis spiral: point <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>k</mi></mrow><annotation encoding="application/x-tex">k</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.0315em;">k</span></span></span></span> sits at radius <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msqrt><mi>k</mi></msqrt></mrow><annotation encoding="application/x-tex">\sqrt{k}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.04em;vertical-align:-0.1078em;"></span><span class="mord sqrt"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.9322em;"><span class="svg-align" style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span class="mord" style="padding-left:0.833em;"><span class="mord mathnormal" style="margin-right:0.0315em;">k</span></span></span><span style="top:-2.8922em;"><span class="pstrut" style="height:3em;"></span><span class="hide-tail" style="min-width:0.853em;height:1.08em;"><svg xmlns="http://www.w3.org/2000/svg" width="400em" height="1.08em" viewBox="0 0 400000 1080" preserveAspectRatio="xMinYMin slice"><path d="M95,702
c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14
c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54
c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10
s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429
c69,-144,104.5,-217.7,106.5,-221
l0 -0
c5.3,-9.3,12,-14,20,-14
H400000v40H845.2724
s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7
c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z
M834 80h400000v40h-400000z"/></svg></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.1078em;"><span></span></span></span></span></span></span></span></span> and angle <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>k</mi><mo>⋅</mo><msup><mn>137.5</mn><mo>∘</mo></msup></mrow><annotation encoding="application/x-tex">k \cdot 137.5^\circ</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.0315em;">k</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.6741em;"></span><span class="mord">137.</span><span class="mord"><span class="mord">5</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.6741em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mbin mtight">∘</span></span></span></span></span></span></span></span></span></span></span>. Drag the slider to change the divergence angle and watch the pattern collapse into spokes whenever the angle approaches a rational fraction of a turn.</p>
<h2 id="how-publishing-works" tabindex="-1">How publishing works <a class="heading-anchor" href="#how-publishing-works" aria-hidden="true">#</a></h2>
<p>A post lives in <code>content/posts/&lt;date&gt;-&lt;slug&gt;/index.md</code>. Running <code>npm run dev</code> serves the site locally with drafts visible and reloads the browser on every save. When a post is ready, flip <code>draft: false</code> and push to <code>main</code>; a GitHub Actions workflow builds the site and deploys it to GitHub Pages. There is no other step.</p>
<p>The feed is at <a href="/feed.xml">/feed.xml</a>. Expect posts to be infrequent and long.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Like this. Footnotes are collected at the end of the post with back-links. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded>
    </item>
  </channel>
</rss>
