Skip to content

Releases: vetr0s/ostat

0.5.0

Choose a tag to compare

@vetr0s vetr0s released this 01 Aug 08:31

A breadcrumb names a section by its title rather than by its directory.

Every page under blog/ sat under a breadcrumb that said blog. The section is called Release Notes, and has been since it existed, in the title of its _index.md — which is what the section heading already showed.

The trail printed p.section, the directory name. That is a URL segment. It is the section's name only when the two happen to be the same word, and the coincidence held long enough to look deliberate: every directory anyone had made was lowercase already, so the trail read as a considered lowercase style rather than as a path printed where a name belonged.

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 href is still the directory, because that is what a URL is made of.

<a href="/blog/">release notes</a>

Upgrading

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 title in its _index.md.

0.4.0

Choose a tag to compare

@vetr0s vetr0s released this 01 Aug 08:26

A date may carry a time, so two posts published on one day have an order.

Three release notes were published on the same day and came out ordered 0.2.0, 0.3.0, 0.2.1 — not the order they were written, or their file order, or any order at all.

Two causes, and the second is the one that matters. A date is a day, so three posts on one date compared equal and nothing could separate them. And slice.sort_by is not stable, so equal posts did not merely come out arbitrarily: they came out in an order that could change when an unrelated post was added.

A date may carry a time

"date": "2026-01-30"
"date": "2026-01-30T14:30"
"date": "2026-01-30T14:30:05"

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 "" is less than "T".

The slug now breaks a tie between identical stamps, so the comparison is total and nothing depends on the sort being stable.

What else it fixed

<pubDate> said 00:00:00 for every post ever published, because a day was all front matter could say. It carries the real time now.

A listing shows the day and puts the whole stamp in the <time datetime> attribute, which is what that attribute is for. <lastmod> in the sitemap stays a date: a W3C datetime needs a timezone once it carries a time, and ostat does not know one.

Upgrading

Nothing to do. A site whose posts are dated to the day builds identically, and gains an order only if it starts writing times.

0.3.0

Choose a tag to compare

@vetr0s vetr0s released this 01 Aug 08:08

A section index says what each page is, not just what it is called.

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.

Section listings now carry the page's description under its title, in a <p class="entry-summary">.

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.

The summary is the front matter's description and nothing else. page_description 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.

Upgrading

Nothing breaks. A site with no rule for .entry-summary renders it as an ordinary paragraph, which is legible if not styled. Style it to taste; ostat's own stylesheet sets it muted and slightly smaller, the same treatment <time> already gets in a listing.

0.2.1

Choose a tag to compare

@vetr0s vetr0s released this 01 Aug 07:58

One fix, found by using the generator rather than reading it.

write_post_entries wrote a <time> for every entry in a listing, so a page without a date got <time datetime=""></time>. An empty datetime is not a valid date string, and a <time> carrying no date is not a time.

Only the blog is dated — section_pages says so in a comment two procedures below the bug. 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.

write_single has guarded this since it was written; this is the same guard in the other place that writes a time.

No site that built on 0.2.0 builds differently on this, unless it had an undated page in a non-blog section, in which case that page's listing entry loses an empty element.

0.2.0

Choose a tag to compare

@vetr0s vetr0s released this 01 Aug 07:16

The home page becomes a document, and seven silent failures stop being silent.

Ten issues, closed together. Most 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.

Breaking

The home page is no longer described in site.json. portrait, contact, elsewhere and home 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 html/home.html and gets the whole page, with <!--ostat:recent--> marking where the recent posts go.

html/ also holds header-01.html and header-02.html, the two halves of every page's <head>, and not-found-404.html. Each is optional and falls back to a built-in default, so a bare content directory still builds.

Three content trees that used to build now fail, on purpose:

  • A directory inside a section directory (#3). Section membership was an exact string comparison, so a page under blog/2024/ was built and crawlable and reachable from nowhere on the site.
  • A section directory with no _index.md (#5). Every page inside it linked a breadcrumb to a page that was never written.
  • A root content/_index.md (#1). It was loaded, written, then overwritten by the synthesised home page, and the sitemap advertised the front page twice.

An _index.md marked draft removes its section, so that now fails too.

Migrating

Move the four site.json keys into html/home.html as ordinary markup, and put <!--ostat:recent--> where the post list belongs. A site with no html/ still builds, on a placeholder that says so.

Fixed

  • Two pages resolving to one output path both stayed in every listing, the feed and the sitemap, while the later write took the path (#2)
  • A file in static/ silently replaced the generated page at the same path, including the home page, the feed and the sitemap (#4)
  • -o, -base-url, -today and ostat new -s took a following flag as their value, so ostat build . -o -drafts created a directory named -drafts and reported success (#7)
  • truncate_words measured its limit in bytes and cut on a byte offset, putting invalid UTF-8 in the meta description of any page whose summary had no ASCII space in it (#6)
  • Feed items kept root-relative URLs in single-quoted attributes, srcset, poster, cite and CSS url(), where they resolve against the reader's host (#9)
  • A bare & made strip_tags search the rest of the document for a ; and swallow everything between as one entity (#9)
  • base_url and locale reached six URLs and attributes unescaped, and -base-url replaced the value after it was checked (#8)

Also

ostat writes a 404.html, which it did not have at all.

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.