<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>ostat</title>
    <link>https://ostat.vetr0s.dev/</link>
    <description>A static site generator written in Odin. Layouts are procedures, not templates.</description>
    <generator>ostat</generator>
    <language>en-us</language>
    <lastBuildDate>Sat, 01 Aug 2026 08:45:00 +0000</lastBuildDate>
    <atom:link href="https://ostat.vetr0s.dev/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>0.5.0</title>
      <link>https://ostat.vetr0s.dev/blog/0-5-0/</link>
      <pubDate>Sat, 01 Aug 2026 08:45:00 +0000</pubDate>
      <guid>https://ostat.vetr0s.dev/blog/0-5-0/</guid>
      <description>&lt;p&gt;Every page on this site sat under a breadcrumb that said &lt;code&gt;blog&lt;/code&gt;. The section is
called Release Notes. It has been called Release Notes since it existed, in the
&lt;code&gt;title&lt;/code&gt; of its &lt;code&gt;_index.md&lt;/code&gt;, which is what the section heading has always shown.&lt;/p&gt;
&lt;p&gt;The trail printed &lt;code&gt;p.section&lt;/code&gt;, which is the directory name. That is a URL
segment. It is only the section’s name when the two happen to be the same word,
and the coincidence held for long enough to look deliberate: every directory
anyone had made was already lowercase, so the trail looked like a considered
lowercase style rather than a path being printed where a name belonged.&lt;/p&gt;
&lt;p&gt;The crumb is the section’s title now, lowercased the way a crumb at the content
root already lowercased its page title. The link is unchanged: the &lt;code&gt;href&lt;/code&gt; is
still the directory, because that is what a URL is made of.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;a href=&amp;quot;/blog/&amp;quot;&amp;gt;release notes&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Upgrading&lt;/h2&gt;
&lt;p&gt;A site whose section directories match their titles sees no change. A site
where they differ sees the title, which is the point. If a section should be
called what its directory is called, that is the &lt;code&gt;title&lt;/code&gt; in its &lt;code&gt;_index.md&lt;/code&gt;.&lt;/p&gt;
</description>
    </item>
    <item>
      <title>0.4.0</title>
      <link>https://ostat.vetr0s.dev/blog/0-4-0/</link>
      <pubDate>Sat, 01 Aug 2026 08:30:00 +0000</pubDate>
      <guid>https://ostat.vetr0s.dev/blog/0-4-0/</guid>
      <description>&lt;p&gt;Three of the release notes below this one were published on the same day, and
came out in the order 0.2.0, 0.3.0, 0.2.1. That is not their order, or their
file order, or any order at all.&lt;/p&gt;
&lt;p&gt;Two things were wrong, and the second is the one that matters.&lt;/p&gt;
&lt;p&gt;A date is a day, so three posts on 2026-08-01 were equal, and no comparison of
their dates could separate them. And &lt;code&gt;slice.sort_by&lt;/code&gt; is not stable, so equal
posts did not merely come out in an arbitrary order: they came out in an order
that could change when an unrelated post was added.&lt;/p&gt;
&lt;h2&gt;A date may carry a time&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;quot;date&amp;quot;: &amp;quot;2026-01-30&amp;quot;
&amp;quot;date&amp;quot;: &amp;quot;2026-01-30T14:30&amp;quot;
&amp;quot;date&amp;quot;: &amp;quot;2026-01-30T14:30:05&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Optional, and midnight when absent, so every stamp written before this means
what it meant. Written large unit first a stamp sorts chronologically as a
string, which is what the sort already relied on; a bare date sorts at the
start of its day because &lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt; is less than &lt;code&gt;&amp;quot;T&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The slug now breaks a tie between identical stamps, so the comparison is total
and an unstable sort cannot reorder anything.&lt;/p&gt;
&lt;h2&gt;What else it fixed&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;pubDate&amp;gt;&lt;/code&gt; said &lt;code&gt;00:00:00&lt;/code&gt; for every post ever published, because the day was
all the front matter could say. It now carries the real time.&lt;/p&gt;
&lt;p&gt;A listing shows the day and puts the whole stamp in the &lt;code&gt;&amp;lt;time datetime&amp;gt;&lt;/code&gt;
attribute, which is what that attribute is for. &lt;code&gt;&amp;lt;lastmod&amp;gt;&lt;/code&gt; in the sitemap
stays a date: a W3C datetime needs a timezone once it carries a time, and
ostat does not know one.&lt;/p&gt;
&lt;h2&gt;Upgrading&lt;/h2&gt;
&lt;p&gt;Nothing to do. A site whose posts are dated to the day builds identically, and
gains an order only if it starts writing times.&lt;/p&gt;
</description>
    </item>
    <item>
      <title>0.3.0</title>
      <link>https://ostat.vetr0s.dev/blog/0-3-0/</link>
      <pubDate>Sat, 01 Aug 2026 01:08:23 +0000</pubDate>
      <guid>https://ostat.vetr0s.dev/blog/0-3-0/</guid>
      <description>&lt;p&gt;A section listing was a link and, for the blog, a date. That is enough for
posts, where a title and a date are the conventional index. It is thin for a
section whose pages carry no date, where the link is the entire entry.&lt;/p&gt;
&lt;p&gt;Section listings now carry the page’s description under its title, in a
&lt;code&gt;&amp;lt;p class=&amp;quot;entry-summary&amp;quot;&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The home page’s recent list does not. It is five entries sitting under two
other sections, and its job is to say what is new rather than what each thing
is. A section index has the room; the front page does not.&lt;/p&gt;
&lt;p&gt;The summary is the front matter’s &lt;code&gt;description&lt;/code&gt; and nothing else.
&lt;code&gt;page_description&lt;/code&gt; would fall back to the opening paragraph, which is right for
a meta tag nobody reads in place and wrong here: an author who wrote no
description gets no line rather than an arbitrary one.&lt;/p&gt;
&lt;h2&gt;Upgrading&lt;/h2&gt;
&lt;p&gt;Nothing breaks. A site with no rule for &lt;code&gt;.entry-summary&lt;/code&gt; renders it as an
ordinary paragraph, which is legible if not styled. &lt;code&gt;site/static/css/style.css&lt;/code&gt;
sets it muted and slightly smaller, the same treatment &lt;code&gt;&amp;lt;time&amp;gt;&lt;/code&gt; already gets in
a listing.&lt;/p&gt;
</description>
    </item>
    <item>
      <title>0.2.1</title>
      <link>https://ostat.vetr0s.dev/blog/0-2-1/</link>
      <pubDate>Sat, 01 Aug 2026 00:58:08 +0000</pubDate>
      <guid>https://ostat.vetr0s.dev/blog/0-2-1/</guid>
      <description>&lt;p&gt;One fix, found by using the generator rather than reading it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;write_post_entries&lt;/code&gt; wrote a &lt;code&gt;&amp;lt;time&amp;gt;&lt;/code&gt; for every entry in a listing, so a page
without a date got &lt;code&gt;&amp;lt;time datetime=&amp;quot;&amp;quot;&amp;gt;&amp;lt;/time&amp;gt;&lt;/code&gt;. An empty &lt;code&gt;datetime&lt;/code&gt; is not a
valid date string, and a &lt;code&gt;&amp;lt;time&amp;gt;&lt;/code&gt; carrying no date is not a time.&lt;/p&gt;
&lt;p&gt;Only the blog is dated&lt;sup class=&quot;fn-ref&quot; id=&quot;fnref-1&quot;&gt;&lt;a href=&quot;https://ostat.vetr0s.dev/blog/0-2-1/#fn-1&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. The blog was also the only section anyone had
put pages in, and a post is required to carry a date, so the broken shape had
never been rendered. It appeared the first time a second section got a page.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;write_single&lt;/code&gt; has guarded this since it was written; this is the same guard in
the other place that writes a time. The fixture site gains a &lt;code&gt;notes/&lt;/code&gt; section
holding one undated page, because the golden tree only knows the shapes the
fixture contains and this was not one of them.&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
&lt;ol&gt;
&lt;li id=&quot;fn-1&quot;&gt;&lt;code&gt;section_pages&lt;/code&gt; says so in a comment two procedures below the bug. &lt;a href=&quot;https://ostat.vetr0s.dev/blog/0-2-1/#fnref-1&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    <item>
      <title>0.2.0</title>
      <link>https://ostat.vetr0s.dev/blog/0-2-0/</link>
      <pubDate>Sat, 01 Aug 2026 00:15:41 +0000</pubDate>
      <guid>https://ostat.vetr0s.dev/blog/0-2-0/</guid>
      <description>&lt;p&gt;Ten issues, closed together. Most of them were the same bug wearing different
clothes: a build that exited 0 and wrote a site that was wrong in a way nothing
on the site revealed.&lt;/p&gt;
&lt;h2&gt;Breaking&lt;/h2&gt;
&lt;p&gt;The home page is no longer described in &lt;code&gt;site.json&lt;/code&gt;. &lt;code&gt;portrait&lt;/code&gt;, &lt;code&gt;contact&lt;/code&gt;,
&lt;code&gt;elsewhere&lt;/code&gt; and &lt;code&gt;home&lt;/code&gt; are gone, along with the three structs behind them and
the procedure that turned them back into the markup they started as. A site now
writes &lt;code&gt;html/home.html&lt;/code&gt; and gets the whole page, with &lt;code&gt;&amp;lt;!--ostat:recent--&amp;gt;&lt;/code&gt;
marking where the recent posts go&lt;sup class=&quot;fn-ref&quot; id=&quot;fnref-1&quot;&gt;&lt;a href=&quot;https://ostat.vetr0s.dev/blog/0-2-0/#fn-1&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;html/&lt;/code&gt; also holds &lt;code&gt;header-01.html&lt;/code&gt; and &lt;code&gt;header-02.html&lt;/code&gt;, the two halves of
every page’s &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, and &lt;code&gt;not-found-404.html&lt;/code&gt;. Each is optional and falls
back to a built-in default, so a bare content directory still builds. This is
the arrangement gingerBill’s generator uses, read at runtime rather than
compiled in, because compiling it in is what would undo multi-site support.&lt;/p&gt;
&lt;p&gt;Three content trees that used to build now fail, on purpose:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A directory inside a section directory. Section membership was an exact
string comparison, so a page under &lt;code&gt;blog/2024/&lt;/code&gt; was built and crawlable and
reachable from nowhere on the site.&lt;/li&gt;
&lt;li&gt;A section directory with no &lt;code&gt;_index.md&lt;/code&gt;. Every page inside it linked a
breadcrumb to a page that was never written.&lt;/li&gt;
&lt;li&gt;A root &lt;code&gt;content/_index.md&lt;/code&gt;. It was loaded, written, and then overwritten by
the synthesised home page, and the sitemap advertised the front page twice.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An &lt;code&gt;_index.md&lt;/code&gt; marked &lt;code&gt;draft&lt;/code&gt; removes its section, so that now fails too.&lt;/p&gt;
&lt;h2&gt;Fixed&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Two pages resolving to one output path both stayed in every listing, the feed
and the sitemap, while the later write took the path&lt;/li&gt;
&lt;li&gt;A file in &lt;code&gt;static/&lt;/code&gt; silently replaced the generated page at the same path,
including the home page, the feed and the sitemap&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o&lt;/code&gt;, &lt;code&gt;-base-url&lt;/code&gt;, &lt;code&gt;-today&lt;/code&gt; and &lt;code&gt;ostat new -s&lt;/code&gt; took a following flag as their
value, so &lt;code&gt;ostat build . -o -drafts&lt;/code&gt; created a directory named &lt;code&gt;-drafts&lt;/code&gt; and
reported success&lt;/li&gt;
&lt;li&gt;&lt;code&gt;truncate_words&lt;/code&gt; measured its limit in bytes and cut on a byte offset, which
landed mid-codepoint and put invalid UTF-8 in the meta description of any
page whose summary had no ASCII space in it&lt;/li&gt;
&lt;li&gt;Feed items kept root-relative URLs in single-quoted attributes, &lt;code&gt;srcset&lt;/code&gt;,
&lt;code&gt;poster&lt;/code&gt;, &lt;code&gt;cite&lt;/code&gt; and CSS &lt;code&gt;url()&lt;/code&gt;, where they resolve against the reader’s host&lt;/li&gt;
&lt;li&gt;A bare &lt;code&gt;&amp;amp;&lt;/code&gt; made &lt;code&gt;strip_tags&lt;/code&gt; search the rest of the document for a &lt;code&gt;;&lt;/code&gt; and
swallow everything between it as one entity&lt;/li&gt;
&lt;li&gt;&lt;code&gt;base_url&lt;/code&gt; and &lt;code&gt;locale&lt;/code&gt; reached six URLs and attributes unescaped, and
&lt;code&gt;-base-url&lt;/code&gt; replaced the value after it was checked&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Also&lt;/h2&gt;
&lt;p&gt;ostat writes a &lt;code&gt;404.html&lt;/code&gt;, which it did not have at all.&lt;/p&gt;
&lt;p&gt;The test suite went from 82 to 98. The golden tree is unchanged except for the
new 404 page, which is what says the home page migration was faithful rather
than merely finished.&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
&lt;ol&gt;
&lt;li id=&quot;fn-1&quot;&gt;The data model had to grow a field for every idea a home page might have, and the generator a branch to draw it. A fourth section meant editing &lt;code&gt;write_home&lt;/code&gt; and recompiling; a paragraph or a table had no representation at all. A document is stored as a document now. &lt;a href=&quot;https://ostat.vetr0s.dev/blog/0-2-0/#fnref-1&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    <item>
      <title>0.1.1</title>
      <link>https://ostat.vetr0s.dev/blog/0-1-1/</link>
      <pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://ostat.vetr0s.dev/blog/0-1-1/</guid>
      <description>&lt;p&gt;A whole-project audit, then the work it turned up. The headline was that the
preprocessing pass could not see inline code, so a regex in backticks aborted
the build&lt;sup class=&quot;fn-ref&quot; id=&quot;fnref-1&quot;&gt;&lt;a href=&quot;https://ostat.vetr0s.dev/blog/0-1-1/#fn-1&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; with an error naming a note nobody had written.&lt;/p&gt;
&lt;p&gt;Also fixed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tildes inside a code span no longer become &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;A fence closes only on a matching run, so a longer fence holds a shorter one&lt;/li&gt;
&lt;li&gt;A fenced block indented under a list item stays in its item&lt;/li&gt;
&lt;li&gt;Front-matter slugs and note labels are validated before they reach the output&lt;/li&gt;
&lt;li&gt;Repeating one note reference no longer repeats its id&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And the shape of the thing: building a document is separate from writing it,
so the layouts are testable, and there is now a fixture site with a committed
tree of expected output covering the third of the source that no test reached.&lt;/p&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
&lt;ol&gt;
&lt;li id=&quot;fn-1&quot;&gt;&lt;code&gt;[^a-z]&lt;/code&gt; was read as a reference to a note called &lt;code&gt;a-z&lt;/code&gt;. An unresolved marker is prose now, and a mistyped label is caught from the other side instead. &lt;a href=&quot;https://ostat.vetr0s.dev/blog/0-1-1/#fnref-1&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</description>
    </item>
    <item>
      <title>0.1.0</title>
      <link>https://ostat.vetr0s.dev/blog/0-1-0/</link>
      <pubDate>Tue, 28 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://ostat.vetr0s.dev/blog/0-1-0/</guid>
      <description>&lt;p&gt;The first version that generates a whole site.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Content discovery with drafts, future-dating and slug overrides&lt;/li&gt;
&lt;li&gt;Margin notes in the Tufte shape, collapsing to numbered endnotes in the feed&lt;/li&gt;
&lt;li&gt;Tables, strikethrough and syntax highlighting, none of which CommonMark has&lt;/li&gt;
&lt;li&gt;An RSS feed at two addresses, and a sitemap&lt;/li&gt;
&lt;li&gt;Layouts as Odin procedures, with no template language&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Set in &lt;a href=&quot;https://github.com/edwardtufte/et-book&quot;&gt;ET Book&lt;/a&gt;, served from this
domain with &lt;code&gt;font-display: optional&lt;/code&gt;, so the page never trades one typeface for
another while you are reading it.&lt;/p&gt;
</description>
    </item>
  </channel>
</rss>
