﻿<feed xmlns="http://www.w3.org/2005/Atom">
  <title type="text" xml:lang="en">D. Moonfire</title>
  <link type="application/atom+xml" href="https://d.moonfire.us/atom.xml" rel="self" />
  <link type="text/html" href="https://d.moonfire.us/" rel="alternate" />
  <updated>2026-04-16T17:41:23Z</updated>
  <id>https://d.moonfire.us/</id>
  <author>
    <name>D. Moonfire</name>
  </author>
  <rights>Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</rights>
  <entry>
    <title>Leicmin</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2026/04/04/leicmin/" />
    <updated>2026-04-04T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2026/04/04/leicmin/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="leicmin" scheme="https://d.moonfire.us/tags/" label="Leicmin" />
    <category term="covid" scheme="https://d.moonfire.us/tags/" label="Covid" />
    <category term="fedran" scheme="https://d.moonfire.us/tags/" label="Fedran" />
    <category term="age-verification" scheme="https://d.moonfire.us/tags/" label="Age Verification" />
    <category term="300-weeks" scheme="https://d.moonfire.us/tags/" label="300 Weeks" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="mfgames-writing" scheme="https://d.moonfire.us/tags/" label="MfGames Writing" />
    <category term="patreon" scheme="https://d.moonfire.us/tags/" label="Patreon" />
    <category term="subscribe-star" scheme="https://d.moonfire.us/tags/" label="Subscribe Star" />
    <category term="allegro" scheme="https://d.moonfire.us/tags/" label="Allegro" />
    <category term="large-language-model" scheme="https://d.moonfire.us/tags/" label="Large Language Model" />
    <summary type="html">I spent six months working on a flight or fight response and created a self-hosted web application to calm it down.
</summary>
    <content type="html">&lt;p&gt;I haven't posted in three months. That isn't to nothing has been happening, only that I was so focused on the &amp;ldquo;now&amp;rdquo; that I didn't really have a chance for any retrospection (which is effectively what a lot of my posts are) or idle thoughts (which cover most of the rest). Part of that is how my thoughts work, I withdraw from everything when I'm struggling with problems.&lt;/p&gt;
&lt;p&gt;There is always the usual family crises (we lost a family member last week and there is a good chance we're going to lose a close one &amp;ldquo;soon&amp;rdquo;), work pressure, and the difficulties of being a parent.&lt;/p&gt;
&lt;h2&gt;Age Verification&lt;/h2&gt;
&lt;p&gt;On top of that, the various news about &lt;a href="https://action.freespeechcoalition.com/age-verification-bills/"&gt;age verification laws&lt;/a&gt;, the casual tossing around of felonies and crippling monetary fines, and the rise of privacy-busting legislation were hitting my communities. Too many legislation were salivating at the openings that the Texas got with the Supreme Court ruling, the idea of forcing all cases to be dragged into the state with the most restrictions, and generally finding Machiavellian ways to force creators from closing up shop without explicitly saying that porn was illegal (because it isn't).&lt;/p&gt;
&lt;p&gt;I know &lt;a href="https://fedran.com"&gt;Fedran&lt;/a&gt; is &amp;ldquo;mostly&amp;rdquo; excluded from most of those age verification laws. There are a million words on the site and less than 10% of them are what I would consider &amp;ldquo;adult&amp;rdquo; or graphic. I could easily say it isn't pornographic, but then Tennessee's &lt;a href="https://wapp.capitol.tn.gov/apps/BillInfo/Default?BillNumber=HB1614&amp;amp;GA=113"&gt;Protect Tennessee Minors Act&lt;/a&gt; got put into effect and it appears to &lt;a href="https://ondato.com/blog/tennessee-age-verification/"&gt;apply to my fiction&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It was that law that triggered a &amp;ldquo;fight or flight&amp;rdquo; response. I seriously considered just closing down the shop and giving up writing. I was already in the middle of the burnout from my &lt;a href="/blog/2021/03/17/three-hundred-weeks/"&gt;300 consecutive week&lt;/a&gt; challenge and my struggles after Covid. It felt like an easy escape, just cut off that part of my life.&lt;/p&gt;
&lt;p&gt;But then I didn't. I'm terrible at giving up, I mean that was pretty much the major thread throughout &lt;a href="/tags/sand-and-blood/"&gt;Sand and Blood&lt;/a&gt;, &lt;a href="/tags/sand-and-ash/"&gt;Sand and Ash&lt;/a&gt;, and &lt;a href="/tags/sand-and-bone/"&gt;Sand and Bone&lt;/a&gt;. That part is part of me. I might take a while to get there, but I don't give up easily. I also saw other creators who were also struggling with the same problems, but they were clearly in the arenas the laws were targeting. Creators who were writing things that were more erotic (it's never about violence, which I universally consider more disgusting than sexuality).&lt;/p&gt;
&lt;p&gt;So the flight (giving up) turned into fight (doing something). I already knew that I wasn't going to be great about anything I wrote, but it was something I could do. I have to be honest, I didn't come out of Covid unscathed and I'm painfully aware of how much I struggle with things that I found pleasurable less than ten years ago.&lt;/p&gt;
&lt;p&gt;Also, quite a few years before all this, I had an idea of creating a self-hosted site that could coordinate various subscription/patron services. Basically, taking the supporter badges from forums, the ability to have &lt;a href="/tags/patreon/"&gt;Patreon&lt;/a&gt; or &lt;span class="missing-link" data-path="/tags/ko-fi/"&gt;Ko-Fi&lt;/span&gt; supporters (or for a while, cryptocurrency), and be able to control access to sites. Ideally, in a manner that would not require exposing personal information (because no one needs to know those details). And no gamification or social interactions, just something between a creator and someone who likes their work.&lt;/p&gt;
&lt;p&gt;That also would give me the ability to integrate age verification systems or even geo-blocking or VPN-blocking (there were a few bills that required periodic verification of age or the need to ban all VPN usage) in a central place.&lt;/p&gt;
&lt;p&gt;Would it be helpful? Maybe. But if anything, I could implement something to protect &lt;em&gt;my&lt;/em&gt; ass and maybe it would help others.&lt;/p&gt;
&lt;h2&gt;Leicmin&lt;/h2&gt;
&lt;p&gt;And that ended up being &lt;a href="/tags/leicmin/"&gt;Leicmin&lt;/a&gt; (more detail on the &lt;a href="https://mfgames.com/leicmin/"&gt;development site&lt;/a&gt; and the &lt;a href="https://mfgames.com/leicmin/"&gt;forge&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Well, multiple iterations of Leicmin. I started with trying to implement a C# version since I thought I was really good at the language and it would be &amp;ldquo;easy&amp;rdquo; to bang up something.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I didn't do it because it was easy. I did it because I thought it was easy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After a few months, I realized that I had too many things rattling in my head, and the ideas that I toyed with for years thinking they would make a great system (event sourcing, automated auditing, API-first) ended up crumbling once I started to do significant things. I spent more time fighting libraries and tools than I was moving forward.&lt;/p&gt;
&lt;p&gt;Somewhere in December, I realized it wasn't going to work out so I decided to burn it down and start from scratch. This time, I jettisoned most of the fancy ideas and went with something simple: a website that required no Javascript, had no API, and was about as plain and simple as I could make it. I also decided to go with Rust, but avoided the fancier front ends and went with straight, old-school templating.&lt;/p&gt;
&lt;p&gt;There were some things that I wasn't going to get rid of: there are &lt;a href="https://github.com/jetify-com/typeid"&gt;type-safe identifiers&lt;/a&gt; everywhere, almost everything is CLI first, it is based on Postgres, and there are almost no &lt;a href="https://xkcd.com/974/"&gt;general solutions&lt;/a&gt; anywhere.&lt;/p&gt;
&lt;p&gt;And I got progress. I could see things moving forward and I didn't feel like I was writing myself into a corner. True, the C# attempt taught me a lot, but it was really nice seeing slow but steady progress to something that works. I felt bad that it wasn't &amp;ldquo;fancy&amp;rdquo; or more capable but I was happy that I was creating something I thought could benefits others.&lt;/p&gt;
&lt;p&gt;My goal was the end of February, but it ended up being the end of March, that I got something with a very simple functionality.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Folks could log in and change passwords&lt;/li&gt;
&lt;li&gt;They could connect Patreon and Subscribe Star (a feature requested online)&lt;/li&gt;
&lt;li&gt;They could set up a password that could be used basic authentication&lt;/li&gt;
&lt;li&gt;The creator could export a &lt;code&gt;.htpasswd&lt;/code&gt; file, hook it up to a static site, and be able to authenticate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Through that, I could get a &amp;ldquo;basic&amp;rdquo; age verification since Patreon and Subscribe Star both do age verification on their sides. That means I could mark my patreon as &amp;ldquo;mature&amp;rdquo; (I haven't done that yet), wire up my site in &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt; to use it, and have what I need to quell that &amp;ldquo;fight&amp;rdquo; response in the back of my head.&lt;/p&gt;
&lt;p&gt;And someone (the person who asked for Subscribe Star) is already using it. And reporting bugs and defects as they onboard their subscribers onto it. Which is exciting, because I rarely see someone actually using my tools (Nitride and &lt;a href="/tags/mfgames-writing/"&gt;MfGames Writing&lt;/a&gt; mostly).&lt;/p&gt;
&lt;h2&gt;Going Slow&lt;/h2&gt;
&lt;p&gt;Some of the painful parts is that progress was slow. I've been getting a lot of pressure from work to embrace &lt;a href="/tags/large-language-model/"&gt;large language models&lt;/a&gt; but I didn't want to do that with Leicmin. I want to lay down the code, to come up with the ideas, and work through them because I cherish those little insights when I find some pattern I never thought about before.&lt;/p&gt;
&lt;h2&gt;Protecting Minors&lt;/h2&gt;
&lt;p&gt;I'm not against the idea of limiting minors from accessing content. What I don't like is how law makers was going about it, since little of what they are producing is about protecting children, but very clearly attacking something they consider immoral.&lt;/p&gt;
&lt;p&gt;(I also think it should be something parents should decide on, not law makers making decisions for everyone.)&lt;/p&gt;
&lt;p&gt;Even listening to them talk about it, it was just another attack on people they don't like, they just use minors as their shield to defend their own biases.&lt;/p&gt;
&lt;p&gt;That also means as I have the bandwidth, I'll find alternatives to handing over personal information to for-profit companies. Or find a way not to need a subscription service. I read about and bookmarked a couple promising alternatives, but they aren't quite there yet.&lt;/p&gt;
&lt;h2&gt;Going Forward&lt;/h2&gt;
&lt;p&gt;There are a ton of things that still need to be done before I consider it &amp;ldquo;done&amp;rdquo; in terms of user experience and usability.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Light theme was the first thing I was asked about&lt;/li&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;li&gt;Better authentication options&lt;/li&gt;
&lt;li&gt;Emails&lt;/li&gt;
&lt;li&gt;More age verification options&lt;/li&gt;
&lt;li&gt;More geo blocking options&lt;/li&gt;
&lt;li&gt;Auditing compliance (okay, that one is me)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://src.mfgames.com/leicmin/leicmin/issues"&gt;Many more&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On my personal side:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set up an instance of my own&lt;/li&gt;
&lt;li&gt;Go through my Fedran stories and mark the &amp;ldquo;adult&amp;rdquo; ones&lt;/li&gt;
&lt;li&gt;Set up the instance to put a password on the adult stories&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And, keep slowly moving forward. It was almost immediately after I was able to lean back and say &amp;ldquo;I got the MVP&amp;rdquo; that I was able to relax. Even not setting it up myself (which I will do), the knowledge that I had a clear path forward to handle that fight or flight was a major relief.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Just keep swimming.&lt;/p&gt;
&lt;p&gt;&amp;mdash;Dory, &lt;em&gt;Finding Nemo&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Allegro&lt;/h2&gt;
&lt;p&gt;Speaking of writing, I'm slowly submitting &lt;a href="/tags/allegro/"&gt;Allegro&lt;/a&gt; through the writing group. Of the alpha readers, about half of them came back with some good feedback and half of them never finished. It probably means that it will end up as well as &lt;a href="/tags/flight-of-the-scions/"&gt;Flight of the Scions&lt;/a&gt;, but I'm still going to try publishing the best book I can.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Other Smarter People</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2026/01/04/other-smarter-people/" />
    <updated>2026-01-04T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2026/01/04/other-smarter-people/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="uuidv7" scheme="https://d.moonfire.us/tags/" label="UUIDv7" />
    <category term="identifiers" scheme="https://d.moonfire.us/tags/" label="Identifiers" />
    <category term="typeid" scheme="https://d.moonfire.us/tags/" label="TypeID" />
    <category term="purl" scheme="https://d.moonfire.us/tags/" label="PURL" />
    <category term="bakfu" scheme="https://d.moonfire.us/tags/" label="Bakfu" />
    <summary type="html">I found another specification for representing identifiers that is better supported, has more attention, and covers most of the basics I need to.
</summary>
    <content type="html">&lt;p&gt;I don't like reinventing the wheel. At the same time, I'll look for something and not find it, go through the effort to create something of my own, only to find out that my search skills weren't high enough and someone has already figured that out.&lt;/p&gt;
&lt;p&gt;In a matter of twenty-four hours, I found that two such situations had happened. Which means, I can close the topic on identifiers and simply point to other folk's work and build the little quirk I want on to of those.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2024/05/30/identifiers/"&gt;2024-05-30 Generating and Using Identifiers&lt;/a&gt; - A little discussion on unique identifiers, including formatting and presentation. A small database lesson on page splitting.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2024/06/29/identifiers-update/"&gt;2024-06-29 Generating and Using Identifiers (Part 2)&lt;/a&gt; - A few expanded ideas about identifiers after discussing it with others.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2026/01/04/other-smarter-people/"&gt;2026-01-04 Other Smarter People&lt;/a&gt; - I found another specification for representing identifiers that is better supported, has more attention, and covers most of the basics I need to.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Identifiers&lt;/h2&gt;
&lt;p&gt;Let's start with the last six months (it's been that last since the last of this series&amp;hellip; actually, of most of my posts): I actually used my ideas to see if they were viable in practice as they were in my head.&lt;/p&gt;
&lt;p&gt;I found that when I elided the identifiers, which means only showing the last group of the identifier along with the prefix, it didn't give the benefit I thought it would. For example, &lt;code&gt;group__01_hz613s22_k8nr6w6g_rv1y5c52&lt;/code&gt; being elided to &lt;code&gt;group__rv1y5c52&lt;/code&gt;. When seen on screen, the &lt;code&gt;group__&lt;/code&gt; was still the bulk of the screen estate and the content was sufficient to make it unimportant.&lt;/p&gt;
&lt;p&gt;Then while I was working on &lt;a href="/tags/leicmin/"&gt;Leicmin&lt;/a&gt;, I stumbled onto a series of Rust libraries that implemented a standard that was almost completely the same as mine, including using Crockford32 encoding: &lt;a href="https://github.com/jetify-com/typeid"&gt;TypeID&lt;/a&gt; (or my &lt;a href="/tags/typeid/"&gt;local tag&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;TypeID uses the underscore separator, so double-click and selection works. It defaults to UUIDv7 but can also be used with other UUIDs. It only has one between the prefix and identifier block and no grouping. Which means the above example would be &lt;code&gt;group_01hz613s22k8nr6w6grv1y5c52&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I lose the grouping and there no rules for eliding, but those are easy to handle. Just take the left &lt;code&gt;X&lt;/code&gt; characters and not worry about anything else. So, in C#, eliding would simply be &lt;code&gt;identifier.ToString()[^8..]&lt;/code&gt; and getting &lt;code&gt;rv1y5c52&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Good enough.&lt;/p&gt;
&lt;p&gt;Fortunately, there are a lot of implementations of TypeID. None of them work the way I want them too, or they are &lt;em&gt;quite&lt;/em&gt; there, but it's a much better start and works with a standard that someone else has already established.&lt;/p&gt;
&lt;h2&gt;Package URLs&lt;/h2&gt;
&lt;p&gt;Speaking of finding standard, when I was giving my thoughts on &lt;a href="/tags/bakfu/"&gt;Bakfu&lt;/a&gt;, I was trying to come up with a useful approach for referencing packages across different ecosystems.&lt;/p&gt;
&lt;p&gt;While learning about TypeID, I noticed that the &lt;a href="https://crates.io/crates/mti"&gt;crates.io packages&lt;/a&gt; has a &lt;a href="/tags/purl/"&gt;PURL&lt;/a&gt; link on the sidebar. I had never seen that before, but it basically lead me down to learning about a &lt;a href="https://github.com/package-url/purl-spec"&gt;&amp;ldquo;mostly universal&amp;rdquo;&lt;/a&gt; URL format for packages so&amp;hellip; other people were trying to solve the same thing and they had some interest behind their own (as opposed to none behind mine).&lt;/p&gt;
&lt;p&gt;Some examples (from their website):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pkg:deb/debian/curl@7.50.3-1?arch=i386&amp;amp;distro=jessie&lt;/li&gt;
&lt;li&gt;pkg:docker/cassandra@sha256:244fd47e07d1004f0aed9c&lt;/li&gt;
&lt;li&gt;pkg:gem/jruby-launcher@1.1.2?platform=java&lt;/li&gt;
&lt;li&gt;pkg:golang/google.golang.org/genproto#googleapis/api/annotations&lt;/li&gt;
&lt;li&gt;pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?repository_url=repo.spring.io%2Frelease&amp;amp;packaging=sources&lt;/li&gt;
&lt;li&gt;pkg:npm/%40angular/animation@12.3.1&lt;/li&gt;
&lt;li&gt;pkg:nuget/EnterpriseLibrary.Common@6.0.1304&lt;/li&gt;
&lt;li&gt;pkg:pypi/django@1.11.1&lt;/li&gt;
&lt;li&gt;pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&amp;amp;distro=fedora-25&lt;/li&gt;
&lt;li&gt;pkg:rpm/opensuse/curl@7.56.1-1.1?arch=i386&amp;amp;distro=opensuse-tumbleweed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Well, that covers pretty much the same scope I was trying to implement, so another thing I can move to an accepted standard and stop working on my own.&lt;/p&gt;
&lt;h2&gt;Thoughts&lt;/h2&gt;
&lt;p&gt;A lot of my personal work is in isolation. I've always struggled to join communities and I have a relatively small &amp;ldquo;footprint&amp;rdquo; when it comes to social networks. And, because of my fragmented nature, I'm rarely in touch of people who are in the &amp;ldquo;know&amp;rdquo; who can tell me that someone else is already working on, or has solved, a problem I'm working on.&lt;/p&gt;
&lt;p&gt;That all said, I'm glad I still went down the rabbit hole of trying to solve it. More so when my solution is only trivially different from the more popular one. It tells me I'm in the right direction.&lt;/p&gt;
&lt;p&gt;I also think as a thought exercise, it gives me more appreciation for the work that goes behind these efforts and frequently makes me a fan or even advocate because I'm emotional behind the reasons.&lt;/p&gt;
&lt;p&gt;So, time not wasted but also relief that someone else figured it out instead.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Git For Authors (v0.0.10)</title>
    <link rel="alternate" href="https://d.moonfire.us/garden/git-for-authors/" />
    <updated>2025-08-08T23:41:54Z</updated>
    <id>https://d.moonfire.us/garden/git-for-authors/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="writing" scheme="https://d.moonfire.us/categories/" label="Writing" />
    <summary type="html">An evolution of how I use Git for managing writing projects.</summary>
    <content type="html">&lt;blockquote&gt;
&lt;p&gt;🛈 This isn't done. Not even close. As you can see from the front page, a lot of the bullet items aren't leading to links, which I full intend to flesh out into more detailed sections. Plus I'm still in the process of dumping information and the organization isn't there. If this was the 90s, I would have a &amp;ldquo;Under Construction&amp;rdquo; GIF.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I enjoy writing as much as I like coding. While I used entirely separate tools to do either, over the years, those two processes have converge to my writing projects look like my coding projects. They use pipelines to generate the final output, source control, automated versioning, and the same suite of tools. In many cases, these tools are designed to automate tedious parts of publishing or coordinate work with others, something that can be difficult with existing tools.&lt;/p&gt;
&lt;p&gt;The inspiration of this is &lt;a href="//d.moonfire.us/tags/fast-trip/"&gt;Fast Trip&lt;/a&gt;, a story by &lt;a href="https://www.sectorgeneral.com/shortstories/fasttrip.html"&gt;James White&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Organization&lt;/h2&gt;
&lt;p&gt;I don't like just saying &amp;ldquo;this is how I do things&amp;rdquo; because this isn't, nor will it ever be, the One True Way™ of writing with Git. Instead, I've attempted to organize the topics from high-level ones, to global concepts, and then into advanced topics.&lt;/p&gt;
&lt;p&gt;There is a lot of cross-linking of topics within the individual pages for relevance, but you can easily choose to ignore those links since if you follow this index page, you'll get to them in &amp;ldquo;discovery&amp;rdquo; order.&lt;/p&gt;
&lt;h2&gt;Meta Topics&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./audience/"&gt;Target Audience&lt;/a&gt; - Who this plot is aimed for and what skills are useful to take advantage of it&lt;/li&gt;
&lt;li&gt;&lt;a href="./conventions/"&gt;Conventions&lt;/a&gt; describes the notations used&lt;/li&gt;
&lt;li&gt;&lt;a href="./model-view-controller/"&gt;Model-View-Controller&lt;/a&gt; is the high-level concepts of separating the content (model) from its appearance (view)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;File Formats&lt;/h2&gt;
&lt;p&gt;While the goal of this plot is to use a program&amp;mdash;Git in this case&amp;mdash;to manage writing, the first part is talking about &amp;ldquo;what&amp;rdquo; is being managed. In this case, it is the words you want to put into a novel or story. There are other things that you might want tracked, such as character notes, research, various pin boards and themes. These are all going to be &amp;ldquo;tracked&amp;rdquo; as part of the project.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./text-files/"&gt;Text Files&lt;/a&gt; is a discussion of why writing should be done in plain text files&lt;/li&gt;
&lt;li&gt;&lt;a href="./top-matter/"&gt;Top Matter&lt;/a&gt; are the details about a given chapter or story&lt;/li&gt;
&lt;li&gt;&lt;a href="./binary-files/"&gt;Binary Files&lt;/a&gt; talks about why Git doesn't play well with binary files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because of limitations in text files for doing extra formatting, I use markup to tell when to make something italic, bold, or write an epigraph.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./markup/"&gt;Markup&lt;/a&gt; is how to decorate a text file with some of the formatting rules that writers need&lt;/li&gt;
&lt;li&gt;&lt;a href="./markdown/"&gt;Markdown&lt;/a&gt; is the most popular form of markup&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Organization&lt;/h2&gt;
&lt;p&gt;Organization also becomes important with bigger projects. While you can throw everything into the project directory in a glorious mess, I have established some conventions that work well for me.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./layout/"&gt;Project Layout&lt;/a&gt; is how I organize my files and directories for writing projects.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Source Control&lt;/h2&gt;
&lt;p&gt;The entire reason for this plot is to use a source control program to manage writing. These programs keep a historical record as you write, allow to review changes, but also avoid accidentally overwriting changes. Programs like Git also allow you to handle feedback from editors and reviewers even when they come in over a period of time and after you've already made changes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./source-control/"&gt;Source Control&lt;/a&gt; discusses the systems in general and my history with them&lt;/li&gt;
&lt;li&gt;&lt;a href="./git/"&gt;Git&lt;/a&gt; is the source control system I mainly use while writing&lt;/li&gt;
&lt;li&gt;&lt;a href="./repositories/"&gt;Repositories&lt;/a&gt; are copies of the writing project&lt;/li&gt;
&lt;li&gt;&lt;a href="./commits/"&gt;Commits&lt;/a&gt; are how changes are saved to a repository (and a branch)&lt;/li&gt;
&lt;li&gt;&lt;a href="./tags/"&gt;Tags&lt;/a&gt; can be used to identify points in time where changes are made&lt;/li&gt;
&lt;li&gt;&lt;a href="./branches/"&gt;Branches&lt;/a&gt; are used to coordinate with readers and editor, or to redo chapters&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Text Editors&lt;/h2&gt;
&lt;p&gt;One advantage of using text files is that almost any editor or IDE can be used to write. They have different capabilities or plugins that help.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./visual-studio-code/"&gt;Visual Studio Code&lt;/a&gt; is a lightweight and flexible text editor&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Transforming&lt;/h2&gt;
&lt;p&gt;A critical component of using text formats is how to transform them into other formats that editors and readers need.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./transforming/"&gt;Transforming&lt;/a&gt; talks about transforming source files into the desired output&lt;/li&gt;
&lt;li&gt;&lt;a href="./mfgames-writing/"&gt;MfGames Writing&lt;/a&gt; is a publishing tool for making EPUB and PDF files&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Source Forges&lt;/h2&gt;
&lt;p&gt;Building on top of source control, Git in specific, are code forges which give a web view of your projects, limit others from seeing them, but also gathering feedback and edits.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./forges/"&gt;Code Forges&lt;/a&gt; is about online systems for managing Git repositories, pipelines, and issues&lt;/li&gt;
&lt;li&gt;&lt;a href="./pipelines/"&gt;Pipelines&lt;/a&gt; are automated tasks that run&lt;/li&gt;
&lt;li&gt;&lt;a href="./issues/"&gt;Issues&lt;/a&gt; are a task tracking for a project&lt;/li&gt;
&lt;li&gt;&lt;a href="./pull-requests/"&gt;Pull Requests&lt;/a&gt; are for getting changes from others&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each forge is different from the others in almost every way. That includes setting them up, how to run the pipelines, or even how issues are tracked.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Forgejo&lt;/li&gt;
&lt;li&gt;SourceHut&lt;/li&gt;
&lt;li&gt;GitLab&lt;/li&gt;
&lt;li&gt;GitHub&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Versioning&lt;/h2&gt;
&lt;p&gt;One way of dealing with the &amp;ldquo;final23&amp;rdquo; files&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./versioning/"&gt;Versioning&lt;/a&gt; describes while creating a version for a story is important and how to use semantic versioning&lt;/li&gt;
&lt;li&gt;&lt;a href="./conventional-commits/"&gt;Conventional Commits&lt;/a&gt; describes a common formatting for Git commit messages to automate versioning&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Other Tools&lt;/h2&gt;
&lt;p&gt;Beyond the format of the files, how a project is organized is important to keeping everything separate and also to make some of the tools more effective.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./querying/"&gt;Querying&lt;/a&gt; talks about using tools to work with the source files&lt;/li&gt;
&lt;li&gt;&lt;a href="./markdowny/"&gt;Markdowny&lt;/a&gt; is a tool used for querying the top matter&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Setting Up the Environment&lt;/h2&gt;
&lt;p&gt;I've been writing novels and stories for many decades. While most of the time, I just write it and then produce the resulting PDF or EPUB, occasionally I want to go back only to find that the libraries have &amp;ldquo;bit rotted&amp;rdquo; or I based on them on a global package I forgot to migrate to my new machine or that I had modified to handle a newer book that broke the old one.&lt;/p&gt;
&lt;p&gt;Inevitability, this means days or even weeks of bringing the project up to date just to get a PDF out of it.&lt;/p&gt;
&lt;p&gt;Along the way, I got frustrated with that and have come up with various tools or frameworks for producing a consistent environment years or even decades later.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="./nixos/"&gt;Using NixOS&lt;/a&gt; describes setting up NixOS to make it easier to write&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;About This&lt;/h2&gt;
&lt;p&gt;Finally, this is a living document. Writing it as a digital plot and versioning it will hopefully allow me to evolve as we go.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://src.mfgames.com/dmoonfire-garden/git-for-authors"&gt;MfGames Forge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="./contributing/"&gt;Contributing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;License&lt;/h3&gt;
&lt;p&gt;This book is distributed under a Creative Commons Attribution-ShareAlike 4.0 International license. More info can be found at &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/"&gt;https://creativecommons.org/licenses/by-sa/4.0/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The preferred attribution for this novel is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Git For Authors&amp;rdquo; by D. Moonfire is licensed under CC BY-SA 4.0&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the above attribution, use the following links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Git For Authors: &lt;a href="https://d.moonfire.us/garden/git-for-authors/"&gt;https://d.moonfire.us/garden/project-layout/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;D. Moonfire: &lt;a href="https://d.moonfire.us/"&gt;https://d.moonfire.us/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CC BY-SA 4.0: &lt;a href="https://creativecommons.org/licenses/by-sa/4.0/"&gt;https://creativecommons.org/licenses/by-sa/4.0/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Source&lt;/h3&gt;
&lt;p&gt;The source of this project can be found on the &lt;a href="https://src.mfgames.com/dmoonfire-garden/git-for-authors"&gt;Moonfire Games forge&lt;/a&gt;. Feel free to report any issues, requests for expansion or clarifications. Alternately, you can &lt;a href="//d.moonfire.us/contact/"&gt;contact me&lt;/a&gt; directly.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Using Nitride - Front Matter</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/06/12/using-nitride-front-matter/" />
    <updated>2025-06-12T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/06/12/using-nitride-front-matter/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="mfgames-gallium" scheme="https://d.moonfire.us/tags/" label="MfGames.Gallium" />
    <category term="yaml" scheme="https://d.moonfire.us/tags/" label="YAML" />
    <category term="opengraph" scheme="https://d.moonfire.us/tags/" label="OpenGraph" />
    <category term="handlebars" scheme="https://d.moonfire.us/tags/" label="Handlebars" />
    <summary type="html">How to use Nitride to take the front matter from pages and add them as a component into the Entity class.
</summary>
    <content type="html">&lt;p&gt;Being able to associated metadata for a page is a very useful thing. It can be used to group pages into categories, control how it is styled, or to simply provide internal notes. To do that, we use something called a YAML front matter to describe the details.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;---
title: Name of the Page
summary: Summary of page
image: /path/to/image.png
---

This is the front page.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a major functionality of the Markdown + YAML pages that I use in my technical, fantasy, and even blog posts. I use the categories and tags heavily on this page, not to mention giving a summary details for links. It can also be used to provide &lt;a href="/tags/opengraph/"&gt;OpenGraph&lt;/a&gt; schemas.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Defining a Model&lt;/h2&gt;
&lt;p&gt;Because C# is a static language, we want to take advantage of a schema so we can have type-safe. Historically, I've called this &lt;code&gt;PageModel&lt;/code&gt; because I wrap that in a &lt;code&gt;TemplateModel&lt;/code&gt; when I get to the styling.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Models/PageModel.cs
namespace Generator.Models;

public class PageModel
{
    /// &amp;lt;summary&amp;gt;
    ///     Gets or sets the optional list of categories associated with the page.
    /// &amp;lt;/summary&amp;gt;
    public List&amp;lt;string&amp;gt;? Categories { get; set; }

    /// &amp;lt;summary&amp;gt;
    ///     Gets or sets the URL of the image associated with the page.
    /// &amp;lt;/summary&amp;gt;
    public string? Image { get; set; }

    /// &amp;lt;summary&amp;gt;
    ///     Gets or sets the summary for the page.
    /// &amp;lt;/summary&amp;gt;
    public string? Summary { get; set; }

    /// &amp;lt;summary&amp;gt;
    ///     Gets or sets the title of the page.
    /// &amp;lt;/summary&amp;gt;
    public string? Title { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Naturally, this can and will get a lot more complicated. Because we are using YAML, we can have nested objects, tags, and references that are appropriate for our page. In the above example, we are defining an optional title, summary, an image, and a list of categories.&lt;/p&gt;
&lt;p&gt;It may come to a surprise to you, but this will eventually become a component in the page &lt;code&gt;Entity&lt;/code&gt; class that is passed through the pipelines.&lt;/p&gt;
&lt;h2&gt;Including the YAML module.&lt;/h2&gt;
&lt;p&gt;Like the others &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt; modules, we have to add it as a NuGet reference and tell the system to use it.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;cd src/dotnet
dotnet add package MfGames.Nitride.Yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding it to the system is just a matter of modifying &lt;code&gt;Program.cs&lt;/code&gt; to include it.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Program.cs
var builder = new NitrideBuilder(args)
    .UseIO(rootDirectory)
    .UseMarkdown()
    .UseHtml()
    .UseYaml()
    .UseModule&amp;lt;WebsiteModule&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Parsing Front Matter&lt;/h2&gt;
&lt;p&gt;Parsing the front matter uses an operation, &lt;code&gt;MfGames.Nitride.Yaml.ParseYamlHeader&lt;/code&gt; to parse the text content, pull off the front matter, wrap it in the model call, and then replace &lt;code&gt;ITextContent&lt;/code&gt; with the page without the YAML header.&lt;/p&gt;
&lt;p&gt;In effect, this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;---
title: Name of the Page
summary: Summary of page
image: /path/to/image.png
---

This is the front page.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;... becomes &amp;ldquo;This is the front page.&amp;rdquo; in the text content with a &lt;code&gt;PageModel&lt;/code&gt; component.&lt;/p&gt;
&lt;p&gt;Adding the operation is relatively simple but this operation uses a generic parameter to identify the model to parse the YAML as.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Pipelines/Inputs/PagesModel.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles,
    IdentifyMarkdownFromPath identifyMarkdownFromPath,
    MoveToIndexPath moveToIndexPath,
    ParseYamlHeader&amp;lt;PageModel&amp;gt; parseYamlHeader)
{
    _logger = logger;
    _identifyMarkdownFromPath = identifyMarkdownFromPath;
    _moveToIndexPath = moveToIndexPath;
    _parseYamlHeader = parseYamlHeader;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And, like the others, it's just a matter of adding the operation into the pipeline.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// In //src/dotnet/Pipelines/Inputs/PagesModel.cs
var list = _readFiles
    .Run(cancellationToken)
    .Run(_identifyMarkdownFromPath)
    .Run(_parseYamlHeader)
    .Run(_moveToIndexPath)
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we run it, we get the following output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[01:44:05 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /contact/index.md, Components [&amp;quot;Zio.UPath&amp;quot;,&amp;quot;Generator.Models.PageModel&amp;quot;,&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;]
[01:44:05 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;Zio.UPath&amp;quot;,&amp;quot;Generator.Models.PageModel&amp;quot;,&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we look at the HTML output, you'll notice it has a simplified version.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ cat build/typewriter/html/index.html 
&amp;lt;h1&amp;gt;Typewriter Press&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Retrieving the component is code just requires the &lt;code&gt;Get&amp;lt;PageModel&amp;gt;()&lt;/code&gt; method or to use the various LINQ-based calls from &lt;a href="/tags/mfgames-gallium/"&gt;MfGames.Gallium&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var page = entity.Get&amp;lt;PageModel&amp;gt;();

var list = entities
    .SelectEntity&amp;lt;PageModel&amp;gt;(OnlyRunOnEntitiesWithPageModel)
    .ToList();

public Entity OnlyRunOnEntitiesWithPageModel(Entity entity, PageModel page)
{
    return entity.Set(somePageSpecificVariable);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Gallium Methods&lt;/h2&gt;
&lt;p&gt;In the above case, the &lt;code&gt;SelectEntity&lt;/code&gt; call will skip calling the lambda for entities that don't have a &lt;code&gt;PageModel&lt;/code&gt;, but will still return it into the list. This is the &amp;ldquo;systems&amp;rdquo; part of the ECS system. The following will strip out the entities that don't have the appropriate models.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;.SelectEntity&amp;lt;PageModel&amp;gt;((entity, page) =&amp;gt; entity, includeEntitiesWithoutComponents: false)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So far, Gallium is set up to allow up to four components. I haven't had a need to do more, but those are easy to add. This makes it useful when doing some model processing on files that have a future date.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;.WhereEntity&amp;lt;PageModel, UPage, Instant&amp;gt;(FilterOutFuturePages)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;SelectEntity&lt;/code&gt; is rather powerful because it takes an &lt;code&gt;Entity&lt;/code&gt; and returns one. It can be the same entity, or it can be one that has zero or more components added or changed inside it. I found this really useful when I'm parsing out categories or I'm trying to build up a list of some specialized functionality that the styling will need.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public Entity OnSelectEntity(
    Entity entity,
    PageModel page,
    UPath path,
    ITextContent textContent)
{
    return entity
        .Set(nextPreviousModel)
        .SetAll(parentPage, parentUrl)
        .Set(modifiedPageModel);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What's Next&lt;/h2&gt;
&lt;p&gt;We have the minimum number of components and systems needed to setup styling. Next time, I'll use &lt;a href="/tags/handlebars/"&gt;Handlebars&lt;/a&gt; to style the page and put a little chrome around the output.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Using Nitride - Markdown</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/06/10/using-nitride-markdown/" />
    <updated>2025-06-10T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/06/10/using-nitride-markdown/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="markdig" scheme="https://d.moonfire.us/tags/" label="MarkDig" />
    <category term="gemini" scheme="https://d.moonfire.us/tags/" label="Gemini" />
    <category term="autofac" scheme="https://d.moonfire.us/tags/" label="Autofac" />
    <category term="zio" scheme="https://d.moonfire.us/tags/" label="Zio" />
    <category term="statiq" scheme="https://d.moonfire.us/tags/" label="Statiq" />
    <summary type="html">Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.
</summary>
    <content type="html">&lt;p&gt;Yesterday, I created an over-engineered program to copy a single file from one directory to another. Now, time to make it less overkill by transforming that Markdown file into simple HTML.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MarkDig&lt;/h2&gt;
&lt;p&gt;I don't like reinventing the wheel. I mean, I seem to keep doing it but I don't enjoy it. That was one reason why I tried another static site generators before working on &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt;. However, when it comes to redoing a Markdown parser, even I'm not that foolish when there is already &lt;a href="/tags/markdig/"&gt;MarkDig&lt;/a&gt;, an excellent library for turning Markdown into HTML and extendable enough that I could also turn Markdown into &lt;a href="/tags/gemini/"&gt;Gemini&lt;/a&gt; (a later post).&lt;/p&gt;
&lt;p&gt;In these cases, we need to tell Nitride how to do anything with Markdown since I didn't make it part of the core library. To do that, we need to pull in the NuGet package. While we're at it, we're also going to add the HTML processing library.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ cd src/dotnet
$ dotnet add package MfGames.Nitride.Markdown
$ dotnet add package MfGames.Nitride.Html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we have the packages installed, we need to add those modules into the system. This is where &lt;a href="/tags/autofac/"&gt;Autofac&lt;/a&gt; came in helpful since I just have to add a module for the package it will handle the registration of any operations, components, and systems that we need to use.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Program.cs
var builder = new NitrideBuilder(args)
    .UseIO(rootDirectory)
    .UseMarkdown()
    .UseHtml()
    .UseModule&amp;lt;WebsiteModule&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, I'm trying to follow the generic host pattern for the setup.&lt;/p&gt;
&lt;h2&gt;Identifying Markdown&lt;/h2&gt;
&lt;p&gt;While it may be obvious to convert any entity class that ends in &lt;code&gt;.md&lt;/code&gt; into &lt;code&gt;.html&lt;/code&gt;, we break this apart into separate steps. First, is that we identify a file as a Markdown file. This does two things, it adds the &lt;code&gt;MfGames.Nitride.Markdown.IsMarkdown&lt;/code&gt; as a component, and then treats the contents as text instead of binary.&lt;/p&gt;
&lt;p&gt;If you remember previously, we had this output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[00:41:57 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[00:41:57 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[00:41:57 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 1 entities
[00:41:57 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 1 entities
[00:41:57 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 1 files
[00:41:57 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we're going to use a new operation, &lt;code&gt;MfGames.Nitride.Markdown.IdentifyMarkdownFromPath&lt;/code&gt;. This could be put in a central place, such as &lt;code&gt;SimplifiedMarkdownPipeline&lt;/code&gt;, but I found it is better to do this earlier than later so I usually put the identify process in the input methods. In this case, &lt;code&gt;PagesPipeline&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Pipelines/Inputs/PagesPipeline.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles,
    IdentifyMarkdownFromPath identifyMarkdownFromPath)
{
    _logger = logger;
    _identifyMarkdownFromPath = identifyMarkdownFromPath;

    _readFiles = readFiles
        .WithPattern(&amp;quot;/src/pages/typewriter/**/*.md&amp;quot;)
        .WithRemovePathPrefix(&amp;quot;/src/pages/typewriter&amp;quot;);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = _readFiles
        .Run(cancellationToken)
        .Run(_identifyMarkdownFromPath)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This operation doesn't take any parameters because it attempts to &amp;ldquo;do the right thing&amp;rdquo; with a minimal amount of effort. Running the code now produces this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[00:46:43 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;,&amp;quot;Zio.UPath&amp;quot;,&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The big things is that we have a new component, &lt;code&gt;IsMarkdown&lt;/code&gt;, and the &lt;code&gt;IBinaryContent&lt;/code&gt; changed to &lt;code&gt;ITextComponent&lt;/code&gt; which gives us some additional extension methods but also indicates that the file is a text file instead of treating it as a simple binary. It still hasn't loaded the file into memory, it just switched how it is handled.&lt;/p&gt;
&lt;p&gt;This is a separate step is because sometimes I want to keep a specific file as Markdown and not convert it to HTML. Also, there are times when I construct an &lt;code&gt;Entity&lt;/code&gt; directly without having a valid path and I just have to add the &lt;code&gt;IsHtml&lt;/code&gt; and &lt;code&gt;ITextContent&lt;/code&gt; and it then acts like every other file without having to have any special rules.&lt;/p&gt;
&lt;h2&gt;Content&lt;/h2&gt;
&lt;p&gt;Entities with &lt;code&gt;ITextComponent&lt;/code&gt; have a number of useful extension methods associated with them. (Technically, you can use these methods against any &lt;code&gt;Entity&lt;/code&gt; class and it will try to convert a binary to text if there is a &lt;code&gt;IBinaryContent&lt;/code&gt; and we want a &lt;code&gt;ITextContent&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;bool hasText = entity.HasTextContent();
string text = entity.GetTextContent();

entity.SetTextContent(stringValue);
entity.SetTextContent(stringBufferValue);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are also stored as a &lt;code&gt;ITextContent&lt;/code&gt; instead of &lt;code&gt;string&lt;/code&gt; or &lt;code&gt;StringBuffer&lt;/code&gt;. This is because the default interface is to leave the content on the disk and use &lt;a href="/tags/zio/"&gt;Zio&lt;/a&gt; to retrieve it. However, as soon as &lt;code&gt;SetTextContent&lt;/code&gt; is used, then it keeps that value in memory for the rest of the execution. This is the point where memory pressure begins to increase.&lt;/p&gt;
&lt;p&gt;In the future, we could easily create a &lt;code&gt;ITextContent&lt;/code&gt; implementation that writes large text files to the disk to get them out of memory. However, even my largest chapter of twenty-five thousand words doesn't create too much of a problem so I haven't bothered trying to implement that at this point (but if I did, it would go into &lt;code&gt;//.cache&lt;/code&gt; in some manner).&lt;/p&gt;
&lt;h2&gt;Converting Markdown to HTML&lt;/h2&gt;
&lt;p&gt;Just identifying a file as Markdown doesn't do anything in itself. To convert it, we use another operation, &lt;code&gt;MfGames.Nitride.Markdown.ConvertMarkdownToHtml&lt;/code&gt;. This easily goes into the &lt;code&gt;StyleHtmlPipeline&lt;/code&gt; to handle the conversion and styling. It allows any MarkDig extension or plugin to be called as part of the setup, allow one to customize exactly how the output is generated.&lt;/p&gt;
&lt;p&gt;We also want to change the extension from &lt;code&gt;.md&lt;/code&gt; to &lt;code&gt;.html&lt;/code&gt;. I realize I should have baked that logic into the &lt;code&gt;ConvertMarkdownToHtml&lt;/code&gt; since it is a common operation, which I will do, but for now, we also need to use the &lt;code&gt;MfGames.Nitride.IO.Paths.ChangePathExtension&lt;/code&gt; to do that.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public StyleHtmlPipeline(
    ILogger&amp;lt;StyleHtmlPipeline&amp;gt; logger,
    SimplifiedMarkdownPipeline simplifiedMarkdownPipeline,
    ConvertMarkdownToHtml convertMarkdownToHtml,
    ChangePathExtension changePathExtension)
{
    _logger = logger;
    _changePathExtension = changePathExtension.WithExtension(&amp;quot;.html&amp;quot;);

    _convertMarkdownToHtml = convertMarkdownToHtml
        .WithConfigureMarkdown(builder =&amp;gt;
        {
            SmartyPantOptions smartyPantOptions = new();
            SmartyPantsExtension smartyPants = new(smartyPantOptions);

            builder
                .Use&amp;lt;GenericAttributesExtension&amp;gt;()
                .Use(smartyPants);
        });

    AddDependency(simplifiedMarkdownPipeline);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = entities
        .Run(_convertMarkdownToHtml, cancellationToken)
        .Run(_changePathExtension, cancellationToken)
        .ToList();

    _logger.LogInformation(&amp;quot;Reading {Count:N0} entities&amp;quot;, list.Count);

    return list.ToAsyncEnumerable();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running this gets us:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build
[01:00:43 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[01:00:43 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;,&amp;quot;Zio.UPath&amp;quot;,&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;]
[01:00:43 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 1 entities
[01:00:44 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 1 entities
[01:00:44 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 1 files
[01:00:44 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/index.html, Components [&amp;quot;MfGames.Nitride.Html.IsHtml&amp;quot;,&amp;quot;Zio.UPath&amp;quot;,&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;]
$ find build -type f
build/typewriter/html/index.html
$ cat src/pages/typewriter/index.md 
# Typewriter Press
$ cat build/typewriter/html/index.html
cat build/typewriter/html/index.html
&amp;lt;h1&amp;gt;Typewriter Press&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And now our over-engineered copy method has duplicated &lt;code&gt;markdown2html&lt;/code&gt; for a single file.&lt;/p&gt;
&lt;h2&gt;Components&lt;/h2&gt;
&lt;p&gt;You may notice that &lt;code&gt;IsMarkdown&lt;/code&gt; has been removed and &lt;code&gt;IsHtml&lt;/code&gt; was added. This is part of where I struggled with &lt;a href="/tags/statiq/"&gt;Statiq&lt;/a&gt; and lead me down the path of using components. I don't have to pre-define the different data types, purposes, or even formats of a file. Enums are great, but they don't allow easy extension but with an ECS, it's just a matter of adding and removing components based on the use.&lt;/p&gt;
&lt;p&gt;I've used components for a lot of things including identifying pages that should be in the blog archives, special notices, or pages that I want to ignore because they are aliases. I also embed indexes and lists into the pages to allow things like the &amp;ldquo;next&amp;rdquo; or &amp;ldquo;previous&amp;rdquo; links. If I was doing a web comic, I could have a per-character next/previous system easily implemented via those components.&lt;/p&gt;
&lt;p&gt;My other static site generators didn't even have the content type tagging Statiq did, which was a novel concept for me and one that I'm glad I had a chance to use. It simplified a lot of my logic and lead nicely into where I am today.&lt;/p&gt;
&lt;h2&gt;Planning Ahead&lt;/h2&gt;
&lt;p&gt;As I'm planning ahead, I'm going to do the following change:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rename &lt;code&gt;SimplifiedMarkdownPipeline&lt;/code&gt; to &lt;code&gt;ContentPipeline&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Move the logic I just added into a new pipeline called &lt;code&gt;BareHtmlPipeline&lt;/code&gt; and insert it into between &lt;code&gt;ContentPipeline&lt;/code&gt; and &lt;code&gt;StyleHtmlPipeline&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reason for this is because RSS/Atom feeds use bare HTML to generate their content, so it makes sense to have that bare pipeline feed both of them while having the &lt;code&gt;ContentPipeline&lt;/code&gt; handle a lot of the linking and references that we'll need.&lt;/p&gt;
&lt;h2&gt;Directory Paths&lt;/h2&gt;
&lt;p&gt;One last thing for this post: I prefer paths that end in directory slashes instead of files. So, if create a contact page at &lt;code&gt;//src/pages/content.md&lt;/code&gt;, we want the HTML to be at &lt;code&gt;https://typewriter.press/content/&lt;/code&gt;. This is, creatively enough, another operation: &lt;code&gt;MfGames.Nitride.IO.Paths.MoveToIndexPath&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Pipelines/Inputs/PagesPipeline.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles,
    IdentifyMarkdownFromPath identifyMarkdownFromPath,
    MoveToIndexPath moveToIndexPath)
{
    _logger = logger;
    _identifyMarkdownFromPath = identifyMarkdownFromPath;
    _moveToIndexPath = moveToIndexPath;

    _readFiles = readFiles
        .WithPattern(&amp;quot;/src/pages/typewriter/**/*.md&amp;quot;)
        .WithRemovePathPrefix(&amp;quot;/src/pages/typewriter&amp;quot;);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = _readFiles
        .Run(cancellationToken)
        .Run(_identifyMarkdownFromPath)
        .Run(_moveToIndexPath)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we add a &lt;code&gt;contact.md&lt;/code&gt; page which a run gives us this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build
[01:12:26 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /contact/index.md, Components [&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[01:12:26 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Markdown.IsMarkdown&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[01:12:26 INF] &amp;lt;ContentPipeline&amp;gt; Reading 2 entities
[01:12:26 INF] &amp;lt;BareHtmlPipeline&amp;gt; Reading 2 entities
[01:12:26 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 2 entities
[01:12:26 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 2 files
[01:12:26 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/contact/index.html, Components [&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Html.IsHtml&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[01:12:26 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/index.html, Components [&amp;quot;MfGames.Nitride.Contents.ITextContent&amp;quot;,&amp;quot;MfGames.Nitride.Html.IsHtml&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
$ cat src/pages/typewriter/contact.md 
# Contact Us

Our emails is [contact@typewriter.press](mailto:contact@typewriter.press).
$ cat build/typewriter/html/contact/index.html 
&amp;lt;h1&amp;gt;Contact Us&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;Our emails is &amp;lt;a href=&amp;quot;mailto:contact@typewriter.press&amp;quot;&amp;gt;contact@typewriter.press&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, the input is &lt;code&gt;//src/pages/contact.md&lt;/code&gt;, but through the pipeline, it is written out as &lt;code&gt;//build/typewriter/html/contact/index.html&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;What's Next&lt;/h2&gt;
&lt;p&gt;Next up, handling front matter. We need that for many reasons, but one of the biggest reason is to provide metadata to generate styled HTML output.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Using Nitride - Entities</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/06/09/using-nitride-entities/" />
    <updated>2025-06-09T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/06/09/using-nitride-entities/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="mfgames-gallium" scheme="https://d.moonfire.us/tags/" label="MfGames.Gallium" />
    <category term="statiq" scheme="https://d.moonfire.us/tags/" label="Statiq" />
    <category term="cobblestonejs" scheme="https://d.moonfire.us/tags/" label="CobblestoneJS" />
    <category term="gatsbyjs" scheme="https://d.moonfire.us/tags/" label="GatsbyJS" />
    <category term="autofac" scheme="https://d.moonfire.us/tags/" label="Autofac" />
    <category term="zio" scheme="https://d.moonfire.us/tags/" label="Zio" />
    <category term="serilog" scheme="https://d.moonfire.us/tags/" label="Serilog" />
    <summary type="html">The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.
</summary>
    <content type="html">&lt;p&gt;From yesterday's post, there was an dangling topic, the &lt;code&gt;Entity&lt;/code&gt; class. Unlike most of the static site generators I've worked with, &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt; uses an ECS (&lt;a href="https://en.wikipedia.org/wiki/Entity_component_system"&gt;Entity Component System&lt;/a&gt;) to generate its files.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;History&lt;/h2&gt;
&lt;p&gt;I've worked on a number of static site generators over the years, including more than a few adhoc ones until I finally got something stable with &lt;a href="/tags/cobblestonejs/"&gt;CobblestoneJS&lt;/a&gt; which carried me for almost a decade of heavy use. A lot of the patterns that Nitride was based on were on how I found I had to build my pages in Cobblestone, including having a separate &amp;ldquo;gather&amp;rdquo; step which wrote out temporary files and then the &amp;ldquo;process&amp;rdquo; step which formatted them and made everything pretty. I had to do that because I wanted to have cross-linking and references work across multiple Git projects.&lt;/p&gt;
&lt;p&gt;A good example is the &lt;a href="//fedran.com/"&gt;Fedran&lt;/a&gt; website. At the bottom of a &lt;a href="//fedran.com/sand-and-blood/chapter-001/"&gt;chapter&lt;/a&gt;, I have a list of characters and locations in the chapter. The links are built from data from the wiki project but the chapter information comes from the story Git repository. The gather step let me combine both of those together to generate those links.&lt;/p&gt;
&lt;p&gt;That also meant that I needed to have some supplementary information stored in the gather step (which was written to a &lt;code&gt;//build&lt;/code&gt; folder) to help with those linking.&lt;/p&gt;
&lt;p&gt;Near the end, once Cobblestone began to break under its abstraction and complexity, I wanted to get away from writing my own and switched to &lt;a href="/tags/gatsbyjs/"&gt;GatsbyJS&lt;/a&gt; which made a very pretty website, but it was painful to debug and develop. It only lasted a year or so before I wanted to move (though the Gatsby version of Fedran was probably the prettiest I had ever made). If anything, Gatsby ended up being an ecosystem that needed me to pay attention to update things and my cycle of coming around to update a project was too far for it so I spent days trying to get it working again to build it.&lt;/p&gt;
&lt;p&gt;So I tried out &lt;a href="/tags/statiq/"&gt;Statiq&lt;/a&gt;. I never successfully made a website out of it, but it introduced me to some really interesting concepts including pulling in data from other sources (Gatsby also did this, just never really used it). But with the C#, it had some really interesting problems. However, I quickly found that I was trying to do something that was outside of the planned usage, which involved me trying to extend an enumeration which involved basically rewriting large hunks of the system just to handle it. Plus, I really like &lt;a href="/tags/autofac/"&gt;Autofac&lt;/a&gt; and I could never get it to play well with that.&lt;/p&gt;
&lt;p&gt;Which lead me into creating Nitride.&lt;/p&gt;
&lt;p&gt;Now, I want to say, both Gatsby and Statiq are both good systems. They just aren't good systems &lt;em&gt;for me&lt;/em&gt;. These don't do what I want them to do and they are designed around patterns that I'm not comfortable with. Even with the desire not to write my own, it wasn't enough.&lt;/p&gt;
&lt;h2&gt;Entity Component Systems&lt;/h2&gt;
&lt;p&gt;Nitride is build on &lt;a href="/tags/mfgames-gallium/"&gt;MfGames.Gallium&lt;/a&gt; which basically means the formal name of the system &lt;a href="https://en.wikipedia.org/wiki/Gallium_nitride"&gt;Gallium Nitride&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Gallium nitride (GaN) is a binary III/V direct bandgap semiconductor commonly used in blue light-emitting diodes since the 1990s. The compound is a very hard material that has a Wurtzite crystal structure. Its wide band gap of 3.4 eV affords it special properties for applications in optoelectronics, high-power and high-frequency devices. For example, GaN is the substrate that makes violet (405 nm) laser diodes possible, without requiring nonlinear optical frequency doubling.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Gallium is a small ECS library. It doesn't intend to be fast or efficient, but it was written to match the idiomatic patterns of &lt;code&gt;System.Linq&lt;/code&gt;. I wanted something that wasn't going to reinvent the wheel, which also meant not making custom collection classes or lookup tables. Instead, I wanted to work with BCL primitives which is why much of the system is based on &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There is some high-level documentation for Gallium on &lt;a href="//mfgames.com/mfgames-cil/docs/gallium/"&gt;its project page&lt;/a&gt; and its &lt;a href="https://src.mfgames.com/mfgames-cil/mfgames-cil/src/branch/main/tests/MfGames.Gallium.Tests"&gt;test&lt;/a&gt;, but I'll give the basics here.&lt;/p&gt;
&lt;p&gt;The core object is &lt;code&gt;MfGames.Gallium.Entity&lt;/code&gt;. This is a &lt;a href="https://src.mfgames.com/mfgames-cil/mfgames-cil/src/branch/main/src/MfGames.Gallium/Entity.cs"&gt;small class&lt;/a&gt; that has an integer identifier and basically a &lt;code&gt;Dictionary&amp;lt;Type, object&amp;gt;&lt;/code&gt; called &lt;code&gt;Components&lt;/code&gt;. It has some methods to make it easier, but basically you can set or get components from it:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// This creates an empty entity.
Entity entity1 = new();

// This creates an entity with four components:
//   &amp;quot;System.Int32&amp;quot;: 123
//   &amp;quot;System.String&amp;quot;: &amp;quot;bob&amp;quot;
//   &amp;quot;System.IO.FileInfo&amp;quot;: ...
//   &amp;quot;System.IO.DirectoryInfo&amp;quot;: ...
Entity entity2 = new()
    .Set(123)
    .Set(&amp;quot;bob&amp;quot;)
    .SetAll(
        new FileInfo(&amp;quot;/tmp/bob.txt&amp;quot;),
        new DirectoryInfo(&amp;quot;/tmp&amp;quot;)
    );

Assert.True(entity2.HasComponent&amp;lt;FileInfo&amp;gt;);
Assert.False(entity2.HasComponent&amp;lt;decimal&amp;gt;);

Assert.True(123, entity2.Get&amp;lt;int&amp;gt;());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In effect, the class of the component is the key. There are a number of other functions, the &lt;a href="https://src.mfgames.com/mfgames-cil/mfgames-cil/src/branch/main/tests/MfGames.Gallium.Tests/EntityTests.cs"&gt;test class&lt;/a&gt; is probably the best documentation at this point.&lt;/p&gt;
&lt;p&gt;The systems part of the system is the LINQ-inspired collection classes. Most of them work off &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt; with the &lt;a href="https://src.mfgames.com/mfgames-cil/mfgames-cil/src/branch/main/tests/MfGames.Gallium.Tests/EnumerableEntityTests.cs"&gt;test class&lt;/a&gt; being the bulk of the documentation:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// Set up a list of entities.
List&amp;lt;Entity&amp;gt; list = ...;

// This gets all the entities that have a FileInfo.
var fileOnlyList = list.WhereEntityHas&amp;lt;FileInfo&amp;gt;().ToList();

// This gets all entities that have both a FileInfo and an int.
var fileAndIntList = list.WhereEntityHasAll&amp;lt;FileInfo, int&amp;gt;().ToList();

// Get a modified list based on components. This looks for all
// entities that have a FileInfo and then adds a marker component,
// IsHtml, into it.
IsHtml isHtml = IsHtml.Instance;

var modifiedList = list
    .SelectEntity&amp;lt;FileInfo&amp;gt;((entity, file) =&amp;gt; entity.Set(isHtml))
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As a note, &lt;code&gt;Entity&lt;/code&gt; is &amp;ldquo;mostly&amp;rdquo; immutable. The &lt;code&gt;Set&lt;/code&gt; command will return a new &lt;code&gt;Entity&lt;/code&gt; instance with the same ID but with a different set of components. The &amp;ldquo;mostly&amp;rdquo; bit comes that the objects that it points to can be mutated.&lt;/p&gt;
&lt;p&gt;🛈 The lack of formal documentation for Gallium and Nitride is one of the reasons this is still alpha or beta level code.&lt;/p&gt;
&lt;h2&gt;Operations&lt;/h2&gt;
&lt;p&gt;The next concept needed for today's post is an operation which extends &lt;code&gt;MfGames.Nitride.IOperation&lt;/code&gt;. These are just easy ways of creating some functionality that works on an &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt; that can be combined together.&lt;/p&gt;
&lt;p&gt;Naturally, there are some extension methods that make it easier to work with operations with an &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;IEnumerable&amp;lt;Entity&amp;gt; entityList = ...;

IEnumerable&amp;lt;Entity&amp;gt; otherList = someOperation1.Run(entityList);

IEnumerable&amp;lt;Entity&amp;gt; resultList = otherList
    .Run(someOperation2)
    .Run(someOperation3)
    .Run(someOperation4);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes it relatively easy to create easy processing through composition of one or more operations. Alternatively, we could easily just work directly off the enumerable.&lt;/p&gt;
&lt;h2&gt;Zio&lt;/h2&gt;
&lt;p&gt;There is no question, I'm fond of the &lt;a href="/tags/zio/"&gt;Zio&lt;/a&gt; library. In this case, Nitride uses two big features with it: to create an abstract view of the project that is independent of the working directory and the &lt;code&gt;UPath&lt;/code&gt; class which normalizes paths.&lt;/p&gt;
&lt;p&gt;If you remember from the previous posts, we set a root directory in the &lt;code&gt;Program.cs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public static async Task&amp;lt;int&amp;gt; Main(string[] args)
{
    var rootDirectory = new DirectoryInfo(Environment.CurrentDirectory);

    var builder = new NitrideBuilder(args)
        .UseIO(rootDirectory)
        .UseModule&amp;lt;WebsiteModule&amp;gt;();

    return await builder.RunAsync();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This caused an &lt;code&gt;Zio.IFileSystem&lt;/code&gt; to be injected in the system which treats that directory as the &lt;code&gt;/&lt;/code&gt;. So, in our case where we had our index file in &lt;code&gt;//src/pages/index.md&lt;/code&gt;, it has a &lt;code&gt;UPath&lt;/code&gt; of &lt;code&gt;/src/pages/index.md&lt;/code&gt;. It doesn't seem like a lot, but it means that all paths in the system are always relative to the root of the project and there is no need to pass a root directory throughout the system which means path operations are considerably simplified.&lt;/p&gt;
&lt;p&gt;And the &lt;code&gt;UPath&lt;/code&gt; ensures a consistent pattern for accessing so this works on Windows, Mac, and Linux without a problem. Not to mention, any issues with &lt;code&gt;..&lt;/code&gt; are handled automatically.&lt;/p&gt;
&lt;p&gt;It also simplifies writing tests.&lt;/p&gt;
&lt;h2&gt;Copy File in Six Files&lt;/h2&gt;
&lt;p&gt;Put all that together, we can use two operations to create a massively over-engineered copy file operation using Nitride. Admittedly, the complexity will make sense later, but at the moment, we're going to read the file in the &lt;code&gt;PagesPipeline&lt;/code&gt;, through &lt;code&gt;SimplifiedMarkdownPipeline&lt;/code&gt;, through &lt;code&gt;StyleHtmlPipeline&lt;/code&gt;, and finally write it out in &lt;code&gt;OutputHtmlPipeline&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Making Some Noise&lt;/h3&gt;
&lt;p&gt;To start with, let's put some logging to show how everything flows from pipeline to pipeline. To do that, we're going to inject an &lt;code&gt;ILogger&amp;lt;&amp;gt;&lt;/code&gt; into the constructor into all four pipelines and then put a logging message into the last three (the &lt;code&gt;PagesPipeline&lt;/code&gt; is going to do something different).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Pipelines/Content/SimplifiedMarkdownPipeline.cs
public class SimplifiedMarkdownPipeline : PipelineBase
{
    private readonly ILogger _logger;

    public SimplifiedMarkdownPipeline(
        ILogger&amp;lt;SimplifiedMarkdownPipeline&amp;gt; logger,
        PagesPipeline pagesPipeline)
    {
        _logger = logger;

        AddDependency(pagesPipeline);
    }

    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        var list = entities.ToList();

        _logger.LogInformation(&amp;quot;Reading {Count:N0} entities&amp;quot;, list.Count);

        return list.ToAsyncEnumerable();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Internally, Nitride uses &lt;a href="/tags/serilog/"&gt;Serilog&lt;/a&gt;, so using &lt;code&gt;{Count:N0}&lt;/code&gt; does the right thing even thought we are using a logging abstraction for the actual call. When we run this, we get the following output (I'm not going to show the &lt;code&gt;just build&lt;/code&gt; call for these today):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[14:00:46 DBG] &amp;lt;DirectoryService&amp;gt; Setting root directory and filesystem to /home/dmoonfire/src/typewriter-press/typewriter-press-website
[14:00:46 DBG] &amp;lt;BuildCommand&amp;gt; Processing 0 pipeline options
[14:00:46 INF] &amp;lt;BuildCommand&amp;gt; Running pipelines
[14:00:46 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 0 entities
[14:00:46 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 0 entities
[14:00:46 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Reading 0 entities
[14:00:46 INF] &amp;lt;PipelineManager&amp;gt; Completed in 00:00:00.1079413
[14:00:46 INF] &amp;lt;BuildCommand&amp;gt; Command took 00:00:00.1097142
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🛈 I moved &lt;code&gt;NuGet.config&lt;/code&gt; to the root level because there is where the &lt;code&gt;Website.sln&lt;/code&gt; file was located and it felt right.&lt;/p&gt;
&lt;h3&gt;Reading Files&lt;/h3&gt;
&lt;p&gt;Now, to read a file, we are going to use the &lt;code&gt;MfGames.Nitride.IO.ReadFiles&lt;/code&gt; operation which reads file from the Zio file system and passes them through. We've already included that NuGet package in a previous step, so nothing to do there.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Inputs/PagesPipeline.cs
using MfGames.Gallium;
using MfGames.Nitride.IO.Contents;
using MfGames.Nitride.Pipelines;
using Microsoft.Extensions.Logging;

namespace Generator.Pipelines.Inputs;

public class PagesPipeline : PipelineBase
{
    private readonly ILogger _logger;
    private readonly ReadFiles _readFiles;

    public PagesPipeline(
        ILogger&amp;lt;PagesPipeline&amp;gt; logger,
        ReadFiles readFiles)
    {
        _logger = logger;
        _readFiles = readFiles.WithPattern(&amp;quot;/src/pages/**/*.md&amp;quot;);
    }

    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        var list = _readFiles
            .Run(cancellationToken)
            .ToList();

        _logger.LogInformation(
            &amp;quot;Read in {Count:N0} files from /src/pages&amp;quot;,
            list.Count);

        foreach (var entity in list)
        {
            _logger.LogInformation(&amp;quot;Entity: Path {Path}, Components {ComponentList}&amp;quot;,
                entity.Get&amp;lt;UPath&amp;gt;(),
                entity.GetComponentTypes().Select(x =&amp;gt; x.FullName));
        }

        return list.ToAsyncEnumerable();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This requires a bit of explanation. We are using dependency injection to insert the &lt;code&gt;ReadFiles&lt;/code&gt; operation into the constructor. This is not a singleton, so you can have multiple &lt;code&gt;ReadFiles&lt;/code&gt; if you need to read from multiple locations (or use a globbing operator to do it in one).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;_readFiles = readFiles.WithPattern(&amp;quot;/src/pages/**/*.md&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the constructor, I configure it. I could do this in the &lt;code&gt;RunAsync&lt;/code&gt; function but I found it made it easier to parse the files when I configure in the constructor. Like most things in Nitride, this uses a fluent coding style where each object sets the properties and returns itself to make a nice chained list of calls.&lt;/p&gt;
&lt;p&gt;There are two properties being set, &lt;code&gt;Pattern&lt;/code&gt; and &lt;code&gt;RemovePathPrefix&lt;/code&gt;. I use a source generator to automatically create the &lt;code&gt;With...&lt;/code&gt; pattern for fluent coding. The first one tells which files to read, relative to the root directory. This happen to use a globbing that allows me to grab all Markdown files in the source directory.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;var list = _readFiles
    .Run(cancellationToken)
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This run the _readFiles operation and gets a list of the files it read in. (I'm also going to put this in the &lt;code&gt;OutputHtmlPipeline&lt;/code&gt; to show how everything writes out.)&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;foreach (var entity in list)
{
    _logger.LogInformation(
        &amp;quot;Entity: Path {Path}, Components {ComponentList}&amp;quot;,
        entity.Get&amp;lt;UPath&amp;gt;(),
        entity.GetComponentTypes().Select(x =&amp;gt; x.FullName));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is just some debugging to show how things are working and how Gallium works. In this case, the read files sets two components: the &lt;code&gt;UPath&lt;/code&gt; for the path of the file and a &lt;code&gt;IBinaryContent&lt;/code&gt; which is an abstraction to the binary content. Since we haven't done anything to the file, this just points to the file system itself without loading it into memory. That way, we can easily deal with gigabyte-sized files without overloading the system.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;return list.ToAsyncEnumerable();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this, we return the list of files that we just read in. With this change, running the build gets us this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[14:20:03 DBG] &amp;lt;DirectoryService&amp;gt; Setting root directory and filesystem to /home/dmoonfire/src/typewriter-press/typewriter-press-website
[14:20:03 DBG] &amp;lt;BuildCommand&amp;gt; Processing 0 pipeline options
[14:20:03 INF] &amp;lt;BuildCommand&amp;gt; Running pipelines
[14:20:04 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[14:20:04 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /src/pages/typewriter/index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[14:20:04 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 1 entities
[14:20:04 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 1 entities
[14:20:04 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 1 files
[14:20:04 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /src/pages/typewriter/index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[14:20:04 INF] &amp;lt;PipelineManager&amp;gt; Completed in 00:00:00.9543639
[14:20:04 INF] &amp;lt;BuildCommand&amp;gt; Command took 00:00:00.9559424
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, that single file read in is then passed through the other pipelines because each of them return their input list and then the final pipeline gets those same entities and paths.&lt;/p&gt;
&lt;h3&gt;Changing Paths&lt;/h3&gt;
&lt;p&gt;Now the problem with the above example is that we are writing our output to the same location as we are reading. That isn't optional. What we want to do is strip off the &lt;code&gt;/src/pages/typewriter&lt;/code&gt; from the beginning of the read in path and add &lt;code&gt;/build/typewriter/html&lt;/code&gt; in front of it. This is easily done with two additional operators, &lt;code&gt;RemovePathPrefix&lt;/code&gt; and &lt;code&gt;AddPathPrefix&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To start with, let's remove the prefix from the read methods to normalize the paths.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Inputs/PagesPipeline.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles,
    RemovePathPrefix removePathPrefix)
{
    _logger = logger;
    _removePathPrefix = removePathPrefix.WithPathPrefix(&amp;quot;/src/pages/typewriter&amp;quot;);
    _readFiles = readFiles.WithPattern(&amp;quot;/src/pages/**/*.md&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then we chain the operation using an extension method on operations:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Inputs/PagesPipeline.cs
public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = _readFiles
        .Run(cancellationToken)
        .Run(_removePathPrefix)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since each operation also takes an &lt;code&gt;IEnumerable&amp;lt;Entity&amp;gt;&lt;/code&gt; and removes the same type, they can flow from one operation to another. When we run the build, the output line looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[14:23:35 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[14:23:35 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding the output prefix is done on the output step:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Html/OutputHtmlPipeline.cs
public OutputHtmlPipeline(
    ILogger&amp;lt;OutputHtmlPipeline&amp;gt; logger,
    AddPathPrefix addPathPrefix,
    StyleHtmlPipeline styleHtmlPipeline)
{
    _logger = logger;
    _addPathPrefix = addPathPrefix.WithPathPrefix(&amp;quot;/build/typewriter/html&amp;quot;);

    AddDependency(styleHtmlPipeline);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = entities
        .Run(_addPathPrefix)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because we already have a list of entities, we can just use the extension method to pipe it through the operation and produce the following output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[14:28:35 INF] &amp;lt;PagesPipeline&amp;gt; Read in 1 files from /src/pages
[14:28:35 INF] &amp;lt;PagesPipeline&amp;gt; Entity: Path /index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
[14:28:35 INF] &amp;lt;SimplifiedMarkdownPipeline&amp;gt; Reading 1 entities
[14:28:35 INF] &amp;lt;StyleHtmlPipeline&amp;gt; Reading 1 entities
[14:28:35 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Writing out 1 files
[14:28:35 INF] &amp;lt;OutputHtmlPipeline&amp;gt; Entity: Path /build/typewriter/html/index.md, Components [&amp;quot;MfGames.Nitride.Contents.IBinaryContent&amp;quot;,&amp;quot;Zio.UPath&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reason &lt;code&gt;PagesPipeline&lt;/code&gt; doesn't use &lt;code&gt;entities.Run(...)&lt;/code&gt; is because that is the first pipeline and the &lt;code&gt;entities&lt;/code&gt; is going to be an empty list. Instead, we need to make entities, which is why we call &lt;code&gt;_readFiles.Run()&lt;/code&gt; instead.&lt;/p&gt;
&lt;h3&gt;Writing Files&lt;/h3&gt;
&lt;p&gt;And the last bit for today's post is to write out files. This, creatively enough, uses the &lt;code&gt;WriteFiles&lt;/code&gt; operation.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Html/OutputHtmlPipeline.cs
public OutputHtmlPipeline(
    ILogger&amp;lt;OutputHtmlPipeline&amp;gt; logger,
    AddPathPrefix addPathPrefix,
    WriteFiles writeFiles,
    StyleHtmlPipeline styleHtmlPipeline)
{
    _logger = logger;
    _writeFiles = writeFiles;
    _addPathPrefix = addPathPrefix.WithPathPrefix(&amp;quot;/build/typewriter/html&amp;quot;);

    AddDependency(styleHtmlPipeline);
}

public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    var list = entities
        .Run(_addPathPrefix)
        .Run(_writeFiles)
        .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, we've already done all the fancy operations to adjust the path so it writes out the files in the proper location:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ find build -type f
build/typewriter/html/index.md
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Convenience Methods&lt;/h3&gt;
&lt;p&gt;Now, adding and removing path prefixes is pretty common, so both the &lt;code&gt;ReadFiles&lt;/code&gt; and &lt;code&gt;WriteFiles&lt;/code&gt; have methods to do those in a single call.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Inputs/PagesPipeline.cs
public PagesPipeline(
    ILogger&amp;lt;PagesPipeline&amp;gt; logger,
    ReadFiles readFiles)
{
    _logger = logger;
    _readFiles = readFiles
        .WithPattern(&amp;quot;/src/pages/typewriter/**/*.md&amp;quot;)
        .WithRemovePathPrefix(&amp;quot;/src/pages/typewriter&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// In //src/dotnet/Html/OutputHtmlPipeline.cs
public OutputHtmlPipeline(
    ILogger&amp;lt;OutputHtmlPipeline&amp;gt; logger,
    WriteFiles writeFiles,
    StyleHtmlPipeline styleHtmlPipeline)
{
    _logger = logger;
    _writeFiles = writeFiles.WithAddPathPrefix(&amp;quot;/build/typewriter/html&amp;quot;);

    AddDependency(styleHtmlPipeline);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What's Next&lt;/h2&gt;
&lt;p&gt;And there you go, an over-engineered system for copying a single file from one directory to another. In the next post of this series, we'll turn that Markdown file into an HTML page.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Using Nitride - Pipelines</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/06/08/using-nitride-pipelines/" />
    <updated>2025-06-08T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/06/08/using-nitride-pipelines/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="just" scheme="https://d.moonfire.us/tags/" label="Just" />
    <category term="statiq" scheme="https://d.moonfire.us/tags/" label="Statiq" />
    <category term="cobblestonejs" scheme="https://d.moonfire.us/tags/" label="CobblestoneJS" />
    <category term="zio" scheme="https://d.moonfire.us/tags/" label="Zio" />
    <summary type="html">The first major concept of Nitride is the idea of a "pipeline" which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.
</summary>
    <content type="html">&lt;p&gt;I found a good way of working on a project is being able to see the end product. That means forging a minimal path from the start to end, and then going back to flesh it out. Interestingly, this is the opposite of how I write novels because, in those, the journey is the important part. Not so much with development projects.&lt;/p&gt;
&lt;p&gt;In this case, the goal is to get a website generated and the ability to see it locally.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Project Setup&lt;/h2&gt;
&lt;p&gt;Since this mainly a .NET project, we start by creating a new project and solution. This is a fairly simple generator and I don't plan on needing more than one assembly, so I'm going to throw everything into &lt;code&gt;//src/dotnet/&lt;/code&gt; for the website.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ dotnet new console --name Generator --output src/dotnet
$ dotnet new solution --name Website
$ dotnet sln add src/dotnet/Generator.csproj
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And since we want to have the project run, we also update the &lt;code&gt;Justfile&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-just"&gt;# //Justfile

# Builds the typewriter.press website
build-typewriter:
    dotnet run --project src/dotnet/Generator.csproj --
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that gives us an easy command to run that works anywhere underneath the Git repository (thanks to &lt;a href="/tags/just/"&gt;Just&lt;/a&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build-typewriter 
dotnet run --project src/dotnet/Generator.csproj --
Hello, World!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Setting up NuGet&lt;/h2&gt;
&lt;p&gt;At the moment, the &lt;code&gt;MfGames.*&lt;/code&gt; assemblies are not on nuget.org. There are a couple of reasons, mostly self-doubt that anyone would find this useful and the dread of setting up a SSL certificate which appeared to be required. My current intent is to do it once I have a number of sites using Nitride or I get a request to do so. Until then, I use &lt;code&gt;NuGet.config&lt;/code&gt; file to hook up my libraries to the website.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;!-- //src/dotnet/NuGet.config &amp;gt;
&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
    &amp;lt;packageSources&amp;gt;
    &amp;lt;clear /&amp;gt;
    &amp;lt;add key=&amp;quot;nuget.org&amp;quot; value=&amp;quot;https://api.nuget.org/v3/index.json&amp;quot; protocolVersion=&amp;quot;3&amp;quot; /&amp;gt;
    &amp;lt;add key=&amp;quot;mfgames.com&amp;quot; value=&amp;quot;https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json&amp;quot; protocolVersion=&amp;quot;3&amp;quot; /&amp;gt;
  &amp;lt;/packageSources&amp;gt;
  &amp;lt;packageSourceMapping&amp;gt;
    &amp;lt;packageSource key=&amp;quot;nuget.org&amp;quot;&amp;gt;
      &amp;lt;package pattern=&amp;quot;*&amp;quot; /&amp;gt;
    &amp;lt;/packageSource&amp;gt;
    &amp;lt;packageSource key=&amp;quot;mfgames.com&amp;quot;&amp;gt;
      &amp;lt;package pattern=&amp;quot;MfGames.*&amp;quot; /&amp;gt;
    &amp;lt;/packageSource&amp;gt;
  &amp;lt;/packageSourceMapping&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Setting up the Generator&lt;/h2&gt;
&lt;p&gt;While &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt; could use configuration files to do the bulk of the setup, I went with a configuration-as-code approach to it since I almost always end up writing custom code. That way, everything is done in the same manner. This also uses a model roughly based on the generic host for command line.&lt;/p&gt;
&lt;p&gt;🛈 I'm in the process of reworking the initial setup to use the generic host more completely. The current implementation was my best approach to write a dependency-injected console application that is configured via the CLI instead of JSON files.&lt;/p&gt;
&lt;p&gt;The first step is to include the required package. Nitride is organized in a lot of small, focused packages. This is it avoid pulling in too much for a given project but also to help keep everything organized. For the initial work, we can add the first two required assemblies.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ cd src/dotnet
$ dotnet add package MfGames.Nitride
$ dotnet add package MfGames.Nitride.IO
$ dotnet add package Autofac
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nitride using the excellent &lt;a href="/tags/zio/"&gt;Zio&lt;/a&gt; library for abstracting file system access, plus making it easier to write test to make sure everything work. We also use &lt;a href="https://autofac.org/"&gt;Autofac&lt;/a&gt; for a lot of the module registration and processing.&lt;/p&gt;
&lt;p&gt;🛈 I'm working on removing the Autofac dependency and using Microsoft.Extensions.DependencyInjection instead but it will take a while. There are a lot of useful features in Autofac that the extensions don't have so it will take some time.&lt;/p&gt;
&lt;p&gt;First, we create the website module with a very generic, naive implementation (unless otherwise mentioned, all of these files are in &lt;code&gt;//src/dotnet/&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// WebsiteModule.cs
using Autofac;

namespace Generator;

public class WebsiteModule : Module
{
    /// &amp;lt;inheritdoc /&amp;gt;
    protected override void Load(ContainerBuilder builder)
    {
        builder
            .RegisterAssemblyTypes(GetType().Assembly)
            .AsSelf()
            .AsImplementedInterfaces()
            .SingleInstance();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that, we change the initial program to use Nitride:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// Program.cs
using MfGames.Nitride;
using MfGames.Nitride.IO.Setup;

namespace Generator;

public static class Program
{
    public static async Task&amp;lt;int&amp;gt; Main(string[] args)
    {
        var rootDirectory = new DirectoryInfo(Environment.CurrentDirectory);

        var builder = new NitrideBuilder(args)
            .UseIO(rootDirectory)
            .UseModule&amp;lt;WebsiteModule&amp;gt;();

        return await builder.RunAsync();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we run this, we'll get an error:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build-typewriter 
dotnet run --project src/dotnet/Generator.csproj --
Required command was not provided.

Description:

Usage:
  MfGames.Nitride [command] [options]

Options:
  --log-level &amp;lt;log-level&amp;gt;                    Controls the verbosity of the output, not case-sensitive and prefixes allowed: 
                                             Verbose, Debug, Information, Warning, Error, Fatal [default: Warning]
  --log-context-format &amp;lt;log-context-format&amp;gt;  Controls the format of the source context for log items, not case-sensitive and 
                                             prefixes allowed: None, Class, Full [default: Class]
  --log-time-format &amp;lt;log-time-format&amp;gt;        Controls the format of the time in the log messages, such as 'HH:mm:ss' (default) or 
                                             'yyyy-MM-ddTHH:mm:ss'. Blank means exclude entirely. [default: HH:mm:ss]
  --log-level-override &amp;lt;log-level-override&amp;gt;  Overrides log levels for certain contexts in the format of either 'Context' or 
                                             'Context=Level' (repeat for multiple) [default: Microsoft=Warning]
  --version                                  Show version information
  -?, -h, --help                             Show help and usage information

Commands:
  build  Generate the website
  watch  Generates the site and then watches for changes

error: Recipe `build-typewriter` failed on line 15 with exit code 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;watch&lt;/code&gt; command doesn't work very well right now, but we want &lt;code&gt;build&lt;/code&gt;, so we'll update our Justfile:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# //Justfile
build-typewriter:
    dotnet run --project src/dotnet/Generator.csproj -- build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then try again:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build-typewriter
dotnet run --project src/dotnet/Generator.csproj -- build
[12:27:17 ERR] &amp;lt;PipelineManager&amp;gt; There are no registered pipelines run, use ConfigureContainer to include IPipeline instances
error: Recipe `build-typewriter` failed on line 15 with exit code 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pipelines&lt;/h2&gt;
&lt;p&gt;Now we get to the first big concept of Nitride: pipelines. Some of the ideas are built up from &lt;a href="/tags/statiq/"&gt; Statiq&lt;/a&gt; and also the patterns I've worked out with &lt;a href="/tags/cobblestonejs/"&gt;CobblestoneJS&lt;/a&gt;. They basically are larger &amp;ldquo;units&amp;rdquo; of processing for the site that are intended to gather a bunch of files and then feed them into another pipeline which then splits out into different ones for output.&lt;/p&gt;
&lt;p&gt;It also allows a group of inputs to be handled differently, such as linking the &amp;ldquo;next&amp;rdquo; and &amp;ldquo;previous&amp;rdquo; to posts which doesn't make for pages. Another case where I've used this heavily is to copy binary assets directly to the styled output, generate CSS through &lt;code&gt;webpack&lt;/code&gt; or &lt;code&gt;esbuild&lt;/code&gt;, or generate project-specific pages from the input.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-goat"&gt;        +-------+ +-------+
        | Pages | | Posts |
        +-------+ +-------+
            |         |
            +----+----+
                 | 
                 v
          +-------------+
          |  Simplified |
          |   Markdown  |
          +-------------+
                 |
     +-----------+-----------+
     |           |           |
     v           v           v
+---------+ +---------+ +---------+
|  Style  | |  Atom   | | Style   |
|  HTML   | |  Feeds  | | Gemtext |
+---------+ +---------+ +---------+
     |                       |
     v                       v
+---------+             +---------+
|  Output |             | Output  |
|  HTML   |             | Gemtext |
+---------+             +---------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pipelines attempt to run in parallel to reduce the amount of time on the developer's machine. They are also asynchronous at their core since a pipeline could be used to query another system, require long-running processes, or perform some other action that would require using that functionality.&lt;/p&gt;
&lt;p&gt;Obviously, you could use a single pipeline. I use to start that way, but the pattern of gathering the information, combining them together to generate tags and categories, and then styling fits better as separate steps for me. Since this is my website, we're going to create a minimum from the pages, generate simplified Markdown, style it, and then write it out.&lt;/p&gt;
&lt;p&gt;Since we use dependency injection throughout Nitride, we pass in the dependencies into constructor of a pipeline and then add it to that project. Internally, Nitride runs every pipeline in a separate task when it is building the site.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// ./Pipelines/Inputs/PagesPipeline.cs
using MfGames.Gallium;
using MfGames.Nitride.Pipelines;

namespace Generator.Pipelines.Inputs;

public class PagesPipeline : PipelineBase
{
    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// ./Pipelines/Content/SimplifiedMarkdownPipeline.cs
using Generator.Pipelines.Inputs;
using MfGames.Gallium;
using MfGames.Nitride.Pipelines;

namespace Generator.Pipelines.Content;

public class SimplifiedMarkdownPipeline : PipelineBase
{
    public SimplifiedMarkdownPipeline(
        PagesPipeline pagesPipeline)
    {
        AddDependency(pagesPipeline);
    }

    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// ./Pipelines/Html/StyleHtmlPipeline.cs
using Generator.Pipelines.Content;
using MfGames.Gallium;
using MfGames.Nitride.Pipelines;

namespace Generator.Pipelines.Html;

public class StyleHtmlPipeline : PipelineBase
{
    public StyleHtmlPipeline(
        SimplifiedMarkdownPipeline simplifiedMarkdownPipeline)
    {
        AddDependency(simplifiedMarkdownPipeline);
    }

    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;// ./Pipelines/Html/OutputHtmlPipeline.cs
using MfGames.Gallium;
using MfGames.Nitride.Pipelines;

namespace Generator.Pipelines.Html;

public class OutputHtmlPipeline : PipelineBase
{
    public OutputHtmlPipeline(
        StyleHtmlPipeline styleHtmlPipeline)
    {
        AddDependency(styleHtmlPipeline);
    }

    public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
        IEnumerable&amp;lt;Entity&amp;gt; entities,
        CancellationToken cancellationToken = default)
    {
        throw new NotImplementedException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And since we want to avoid exceptions, we'll ignore the entire &lt;code&gt;Entity&lt;/code&gt; bit and do a couple of changes that will be explained in the next post.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;PagesPipeline.cs&lt;/code&gt;, we want to return an empty list:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    return Array.Empty&amp;lt;Entity&amp;gt;().ToAsyncEnumerable();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For all the others, we're just going to pass whatever we got in back out:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public override IAsyncEnumerable&amp;lt;Entity&amp;gt; RunAsync(
    IEnumerable&amp;lt;Entity&amp;gt; entities,
    CancellationToken cancellationToken = default)
{
    return entities.ToAsyncEnumerable();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'll admit, I stumbled into the usage of &lt;code&gt;IAsyncEnumerable&lt;/code&gt;. It lets me use &lt;code&gt;await&lt;/code&gt; calls inside a function without needing to load an entire list into memory just to copy it out again into another list to make the pipelines to work. Given that one of my baseline websites was to process my entire &lt;a href="//fedran.com/"&gt;fiction website&lt;/a&gt; without killing my machine, I want to reduce the number of copies in memory and reduce the pressure there.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;IAsyncEnumerable&lt;/code&gt; is cumbersome though, so might I might create a convenience method in &lt;code&gt;PipelineBase&lt;/code&gt; that allows for a &lt;code&gt;Task&amp;lt;IEnumerable&amp;lt;Entity&amp;gt;&amp;gt;&lt;/code&gt; and change it an &lt;code&gt;IAsyncEnumerable&lt;/code&gt; but that is a future idea to work out.&lt;/p&gt;
&lt;p&gt;When we run this, we get this (after adding ``&amp;ndash;log-level debug&lt;code&gt;into the&lt;/code&gt;//Justfile`):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ just build-typewriter 
dotnet run --project src/dotnet/Generator.csproj -- build --log-level debug
[13:22:48 DBG] &amp;lt;BuildCommand&amp;gt; Processing 0 pipeline options
[13:22:48 INF] &amp;lt;BuildCommand&amp;gt; Running pipelines
[13:22:48 INF] &amp;lt;PipelineManager&amp;gt; Completed in 00:00:00.1051617
[13:22:48 INF] &amp;lt;BuildCommand&amp;gt; Command took 00:00:00.1075694
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It doesn't really do anything in the 0.1 seconds that it takes to run, but it basically gets the pipeline chain working so things will be generating with the next in this series.&lt;/p&gt;
&lt;h2&gt;Observations&lt;/h2&gt;
&lt;p&gt;I'm also aware that this appears to be overkill for most websites. If I was doing an HTML-only website, I would have started with only a single pipeline but I already know that I'm going to generate Atom feeds out of that and they work better with simple HTML instead of fancy chrome. Being able to style the Atom's output separately gives me some flexibility to do different things there. Also there are going to be multiple inputs eventually so experience tells me that this is the &amp;ldquo;best&amp;rdquo; approach for generating output given the circumstances of this specific website.&lt;/p&gt;
&lt;p&gt;You'll also notice that I'm not doing a site-specific pipelines. I've tried a couple of approached and my current one is to run the entire generation process once per website using some configuration settings, which I'll get after I finish the main website.&lt;/p&gt;
&lt;h2&gt;Future Plans&lt;/h2&gt;
&lt;p&gt;The pipelines also are the units that are triggered on the &lt;code&gt;watch&lt;/code&gt; command (which doesn't really work). If a pipeline identifies that it needs to be run, it will run but then trigger a re-run of every pipeline that depends on that.&lt;/p&gt;
&lt;p&gt;The advantage of that if I'm working on styling CSS, I can have a pipeline feeding into the &lt;code&gt;StyleHtmlPipeline&lt;/code&gt; that generates that. When the hypothetical &lt;code&gt;CssHtmlPipeline&lt;/code&gt; triggers a rebuild, it runs itself, &lt;code&gt;StyleHtmlPipeline&lt;/code&gt;, and &lt;code&gt;OutputHtmlPipeline&lt;/code&gt; only because of those dependencies. But changing a page would trigger everything &lt;em&gt;but&lt;/em&gt; the &lt;code&gt;CssHtmlPipeline&lt;/code&gt; which should keep watches fairly performant.&lt;/p&gt;
&lt;h2&gt;What's Next&lt;/h2&gt;
&lt;p&gt;In the next post, I'll explain what &lt;code&gt;Entity&lt;/code&gt; is and start make this actually generate something useful.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Using Nitride - Introduction</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/06/07/using-nitride-introduction/" />
    <updated>2025-06-07T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/06/07/using-nitride-introduction/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="bitrot" scheme="https://d.moonfire.us/tags/" label="Bitrot" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="direnv" scheme="https://d.moonfire.us/tags/" label="direnv" />
    <category term="lefthook" scheme="https://d.moonfire.us/tags/" label="Lefthook" />
    <category term="editorconfig" scheme="https://d.moonfire.us/tags/" label="EditorConfig" />
    <category term="conventional-commits" scheme="https://d.moonfire.us/tags/" label="Conventional Commits" />
    <category term="gemini" scheme="https://d.moonfire.us/tags/" label="Gemini" />
    <summary type="html">I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.
</summary>
    <content type="html">&lt;p&gt;One drawback of having a lot of irons in the fire is occasionally projects that should not have been forgotten are and they &lt;a href="/tags/bitrot/"&gt;bitrow&lt;/a&gt;. A good example is the &lt;a href="https://typewriter.press"&gt;Typewriter Press&lt;/a&gt; and it's associated reflected websites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://sassy.typewriter.press"&gt;Sassy&lt;/a&gt; - Romance novels&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dusty.typewriter.press"&gt;Dusty&lt;/a&gt; - Historical fiction&lt;/li&gt;
&lt;li&gt;&lt;a href="https://broken.typewriter.press"&gt;Broken&lt;/a&gt; - Fantasy including contemporary&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Eventually, when I start publishing my sci-fi stories, they would go under &amp;ldquo;Electric&amp;rdquo;. I call them &amp;ldquo;reflected&amp;rdquo; sites because they are just filtered versions of the main Typewriter site with some slightly different branding. That way, if I wrote more romance, I could put them there and folks could go to the romance-only site or jump over to the main one to see that I have no single genre (much like I don't have a single genre for my writing).&lt;/p&gt;
&lt;p&gt;However, it's been years since I've updated those sites and they are beginning to creak underneath their age. Not to mention, I haven't included the last two books I published on them and that is humiliating for me.&lt;/p&gt;
&lt;p&gt;So, I decided to switch the sites over to &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt; and document the process.&lt;/p&gt;
&lt;h2&gt;Series&lt;/h2&gt;
&lt;p&gt;This is going to be broken into multiple posts, mostly because I know I'm going to get distracted around the fifteenth of the month when I switch to writing, but also to keep these topic-focused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conventions&lt;/h2&gt;
&lt;p&gt;My usual conventions when giving file names is to use &lt;code&gt;//&lt;/code&gt; for the Git repository root. So, &lt;code&gt;//flake.nix&lt;/code&gt; means the &lt;code&gt;flake.nix&lt;/code&gt; in the root level of the project. Also, I'll add a trailing &lt;code&gt;/&lt;/code&gt; when I'm talking specifically about a directory, such as &lt;code&gt;//node_modules/&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;NixOS Flakes&lt;/h2&gt;
&lt;p&gt;Since I'm going to be creating a new site instead of converting the existing, I can easily start from the beginning. That usually means starting with a NixOS flake which is my preferred way of writing.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-nix"&gt;# //flake.nix
{
  inputs = {
    nixpkgs.url = &amp;quot;nixpkgs/nixos-25.05&amp;quot;;
    flake-utils.url = &amp;quot;github:numtide/flake-utils&amp;quot;;
    mfgames-project-setup.url = &amp;quot;git+https://src.mfgames.com/nixos-contrib/mfgames-project-setup-flake.git&amp;quot;;
  };

  outputs = inputs @ { self, nixpkgs, flake-utils, mfgames-project-setup }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
        project-config = mfgames-project-setup.lib.mkConfig {
          inherit system;
          pkgs = nixpkgs.legacyPackages.${system};
          dotnet.enable = true;
          prettier.proseWrap = &amp;quot;never&amp;quot;;
        };
      in
      {
        devShell = pkgs.mkShell {
          packages = [
            # Development
            pkgs.dotnet-sdk
            pkgs.nodejs_20

            # Local Serving
            pkgs.miniserve
            pkgs.agate
          ]
          ++ project-config.packages;

          shellHook = project-config.shellHook;
        };
      });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There really isn't much to this other than my use of &lt;a href="https://mfgames.com/mfgames-project-setup-flake/"&gt;mfgames-project-setup-flake&lt;/a&gt;. This is a flake that sets up a lot of the common elements I use: a consistent &lt;code&gt;.editorconfig&lt;/code&gt;, &lt;code&gt;treefmt&lt;/code&gt; for formatting various parts of the system, &lt;code&gt;lefthook&lt;/code&gt; for setting up &lt;a href="/tags/conventional-commits/"&gt;conventional commits&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And naturally, I have to set up &lt;a href="/tags/direnv/"&gt;direnv&lt;/a&gt; because I want to set up my environment as soon as I change into the directory.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# //.envrc
use flake || use nix

dotenv_if_exists .env

PATH_add $PWD/node_modules/.bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'm adding the &lt;code&gt;node_modules&lt;/code&gt; path because I know I'm going to be using &lt;code&gt;webpack&lt;/code&gt; as part of this site.&lt;/p&gt;
&lt;p&gt;Once those two files are in, then I can get my basic setup.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ git add flake.nix .envrc
$ direnv allow
direnv: nix-direnv: Renewed cache
nixago: updating repository files
nixago: '.conform.yaml' link updated
nixago: '/.conform.yaml' added to .gitignore
nixago: '.editorconfig' copy created
nixago: 'lefthook.yml' link updated
Error: unknown shorthand flag: 'a' in -a
Error: unknown shorthand flag: 'a' in -a
nixago: '/lefthook.yml' added to .gitignore
nixago: '.prettierrc.json' link updated
nixago: '/.prettierrc.json' added to .gitignore
nixago: 'treefmt.toml' link updated
nixago: '/treefmt.toml' added to .gitignore
mfgames-project-setup: '/.direnv/' added to .gitignore
sync hooks: ✔️ (commit-msg, pre-commit)
direnv: export +AR +AS +AWS_ACCESS_KEY_ID +AWS_BUCKET +AWS_ENDPOINT +AWS_REGION +AWS_SECRET_ACCESS_KEY +CC +CONFIG_SHELL +CXX +DOTNET_CLI_TELEMETRY_OPTOUT +DOTNET_NOLOGO +DOTNET_SKIP_FIRST_TIME_EXPERIENCE +DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK +HOST_PATH +IN_NIX_SHELL +LD +MSBUILDALWAYSOVERWRITEREADONLYFILES +MSBUILDTERMINALLOGGER +NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_BUILD_CORES +NIX_CC +NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_CFLAGS_COMPILE +NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE +NIX_LDFLAGS +NIX_STORE +NM +NODE_PATH +OBJCOPY +OBJDUMP +RANLIB +READELF +SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +__structuredAttrs +buildInputs +buildPhase +builder +cmakeFlags +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +dontAddDisableDepTrack +mesonFlags +name +nativeBuildInputs +out +outputs +patches +phases +preferLocalBuild +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system ~PATH ~XDG_DATA_DIRS
$ l
total 64K
drwxr-xr-x  5 dmoonfire users 4.0K Jun  7 11:10 .
drwxrwxr-x 18 dmoonfire users 4.0K Dec 26 08:08 ..
lrwxrwxrwx  1 dmoonfire users   56 Jun  7 11:10 .conform.yaml -&amp;gt; /nix/store/pckycri07q2ah90swk6hf21ilsi59a4s-conform.yaml
drwxr-xr-x  4 dmoonfire users 4.0K Jun  7 11:10 .direnv
-rw-r--r--  1 dmoonfire users 5.2K Jun  7 11:10 .editorconfig
-rw-------  1 dmoonfire users  259 Jun  7 10:40 .env
-rw-r--r--  1 dmoonfire users  13K Jun  7 11:10 flake.lock
-rw-r--r--  1 dmoonfire users  983 Jun  7 11:03 flake.nix
drwxrwxr-x  8 dmoonfire users 4.0K Jun  7 11:10 .git
-rw-r--r--  1 dmoonfire users  139 Jun  7 11:10 .gitignore
lrwxrwxrwx  1 dmoonfire users   56 Jun  7 11:10 lefthook.yml -&amp;gt; /nix/store/ihdb9vnc6jp535m43ndas79s4p9viyjn-lefthook.yml
lrwxrwxrwx  1 dmoonfire users   59 Jun  7 11:10 .prettierrc.json -&amp;gt; /nix/store/mjdm21r27r4n43aq92qkvmdcvy0l2qrc-prettierrc.json
-rw-r--r--  1 dmoonfire users 1.2K Jun  7 10:40 README.md
lrwxrwxrwx  1 dmoonfire users   56 Jun  7 11:10 treefmt.toml -&amp;gt; /nix/store/4lk6rmb2khs5l3yn91rah9y4c6zmxybf-treefmt.toml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Probably one of my favorite things is that it also sets up the initial &lt;code&gt;.gitignore&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ cat .gitignore
# nixago: ignore-linked-files
/treefmt.toml
/.prettierrc.json
/lefthook.yml
/.conform.yaml
# mfgames-project-setup: ignore-files
/.direnv/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Overall, using that just cuts out that first hour of starting up a new project. And it lets me evolve my changes to style and different tools by simply running &lt;code&gt;nix flake update&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Directory Layout&lt;/h2&gt;
&lt;p&gt;Related to using a flake to setup a lot of the boilerplate, I have &lt;a href="//d.moonfire.us/garden/project-layout/"&gt;written my thoughts&lt;/a&gt; on setting up polyglot projects such as this in a consistent manner. I think it is a relatively short digital garden plot, but there are a couple applicable things.&lt;/p&gt;
&lt;p&gt;Source code goes into &lt;code&gt;//src/&lt;/code&gt; and the output is going into &lt;code&gt;//build&lt;/code&gt; (which has an entry in &lt;code&gt;.gitignore&lt;/code&gt;). One of the artifacts of my &lt;a href="/tags/wordpress/"&gt;WordPress&lt;/a&gt; days is that I have static pages, which I put into &lt;code&gt;//src/pages/&lt;/code&gt;, and blog posts which go into &lt;code&gt;//src/posts/&lt;/code&gt;. There is some magic that goes on with posts, but that's a different post.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;$ find src
src
src/pages
src/pages/typewriter
src/pages/typewriter/index.md
src/posts
src/posts/2025-07-01-website-redesign.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I put an extra layer of directories under &lt;code&gt;//src/pages/&lt;/code&gt; for a site &amp;ldquo;slug&amp;rdquo; to identify which site the page will go on. In this case, I'm going to have &lt;code&gt;typewriter&lt;/code&gt; for the main site, &lt;code&gt;sassy&lt;/code&gt;, &lt;code&gt;dusty&lt;/code&gt;, &lt;code&gt;broken&lt;/code&gt;, and &lt;code&gt;electric&lt;/code&gt;. There will also be a &lt;code&gt;common&lt;/code&gt; for pages that are shared across all pages such as the contact page.&lt;/p&gt;
&lt;p&gt;For output, those same slugs are going to be used. I'm intending to have this output both HTML and Gemtext pages for HTTPS and &lt;a href="/tags/gemini/"&gt;Gemini&lt;/a&gt; respectively. I know not a lot of people use Gemini these days, but I still think it is useful and there is a certain minimalism to it that appeals to me.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sh"&gt;find build
build
build/typewriter
build/typewriter/html
build/typewriter/gemtext
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Just&lt;/h2&gt;
&lt;p&gt;Another part of my standard project layout is using &lt;a href="/tags/just/"&gt;Just&lt;/a&gt; to handle the build system. There are a number of reasons, but I'm going to be dealing with Node programs, such as &lt;code&gt;webpack&lt;/code&gt;, and also .NET projects for the build. There is also CLI executables for hosting locally (&lt;code&gt;miniserv&lt;/code&gt; and &lt;code&gt;agate&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;I made a conscious decision not to write wrappers for Node and the other CLIs into Nitride. There is the ability to execute, but I don't want to tightly tie the site generator to a specific tool. Not to mention, most of the time, I want to set them up in different tabs to watch them so I want a more &amp;ldquo;generic&amp;rdquo; task runner than the ones built into Node or .NET which have some biases I've struggled with.&lt;/p&gt;
&lt;p&gt;Just is something that is self-contained, doesn't make a lot of assumptions, and doesn't drag an entire ecosystem in with it.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-just"&gt;# //Justfile
set dotenv-load

_default:
    just --choose

# Cleans up the project without removing the local .env
clean:
    git clean -xfd --exclude .env

# Builds all the websites
build: build-typewriter

# Builds the typewriter.press website
build-typewriter:
    echo I did something

# Serves the Typewriter website
serve-typewriter-html:
    miniserve --index index.html build/typewriter/html

# Serves the Typewriter Gemini pod
serve-typewriter-gemtext:
    mkdir -p .cache/agate
    agate --content build/typewriter/gemtext --certs .cache/agate --hostname localhost
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Naturally, &lt;code&gt;.cache/&lt;/code&gt; has to be added to the &lt;code&gt;.gitignore&lt;/code&gt; file. I use &lt;code&gt;.cache&lt;/code&gt; from the project layout plot.&lt;/p&gt;
&lt;h2&gt;What's Next&lt;/h2&gt;
&lt;p&gt;Now that we have the basic boilerplate for a new project, time to start adding .NET into the mix.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/07/using-nitride-introduction/"&gt;2025-06-07 Using Nitride - Introduction&lt;/a&gt; - I'm going to start a long, erratic journey to update my publisher website. Along the way, I'm going to document how to create a MfGames.Nitride website from the ground up, along with a lot of little asides about reasoning or purpose.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/08/using-nitride-pipelines/"&gt;2025-06-08 Using Nitride - Pipelines&lt;/a&gt; - The first major concept of Nitride is the idea of a &amp;quot;pipeline&amp;quot; which are the largest units of working in the system. This shows a relatively complex setup of pipelines that will be used to generate the website.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/09/using-nitride-entities/"&gt;2025-06-09 Using Nitride - Entities&lt;/a&gt; - The continued adventures of creating the Typewriter website using MfGames.Nitride, a C# static site generator. The topic of today is the Entity class and how Nitride uses an ECS (Entity-Component-System) to generate pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/10/using-nitride-markdown/"&gt;2025-06-10 Using Nitride - Markdown&lt;/a&gt; - Examples and explanations of converting Markdown to HTML using MfGames.Nitride and MarkDig.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="/blog/2025/06/12/using-nitride-front-matter/"&gt;2025-06-12 Using Nitride - Front Matter&lt;/a&gt; - How to use Nitride to take the front matter from pages and add them as a component into the Entity class.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Not Quite a New Leg</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/05/12/changes/" />
    <updated>2025-05-12T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/05/12/changes/</id>
    <category term="household" scheme="https://d.moonfire.us/categories/" label="Household" />
    <category term="2023-spine-injury" scheme="https://d.moonfire.us/tags/" label="2023 Spine Injury" />
    <category term="2014-broken-leg" scheme="https://d.moonfire.us/tags/" label="2014 Broken Leg" />
    <summary type="html">A few weeks ago, I had spine surgery.
</summary>
    <content type="html">&lt;p&gt;It's been a while since I posted. That isn't too much of a surprise, blogging is sometimes difficult for me when I'm under stress. But, there are times like now when things have relaxed a little and I figured it would be good to make a post. If I'm lucky, I'll do even a few more on what is going on my writing and coding aspects.&lt;/p&gt;
&lt;p&gt;Some years ago, I &lt;a href="/tags/2023-spine-injury/"&gt;fell and smashed my knee&lt;/a&gt; into driveway. It was a nasty injury and took me almost a month before I could kneel again, but there was a lingering pain that continued to get worse. First slowly with a tingling in my toes and a constant pressure but steadily becoming a constant, unrelenting agony.&lt;/p&gt;
&lt;p&gt;I managed to hold off for about eighteen months before I was struggling to stand for more than a few seconds and I couldn't really walk in the mornings beyond a hobble from room to room. When I brushed my teeth, I had to half-crouch and lean against the sink just to remain standing. Even a grocery store run knocked me out.&lt;/p&gt;
&lt;p&gt;I went to physical therapy where they stretched and prodded to stretch the sciatica. When it ended, with the feeling that it wasn't going to get much better, I was still failing the &amp;ldquo;slump test&amp;rdquo; where I couldn't look at my toes if I was sitting down. They tried a lot of things, but they only lasted a few hours or a day at most before everything hurt again.&lt;/p&gt;
&lt;p&gt;I walked as much as I could but I could feel the distance contracting. Near the end of last year, I wasn't able to sleep or move or really do anything. My entire right leg, from hip to toes was constantly tingling or feeling like it was ice. It also felt like I was a few seconds away from having a charlie horse in my right leg.&lt;/p&gt;
&lt;p&gt;It was finally too much and I went to the doctor again, asking for something else. This time, she sent me to get a MRI which indicated I had slipped a disk in my lumbar region and it was digging into my spinal cord; well, that wasn't optimal but it explained the pain.&lt;/p&gt;
&lt;p&gt;The doctor said there was a risk because of my weight and suggested I lost about twenty pounds. But, if I couldn't and would accept the risk, he could do it. Until then, he got me a Cortisone shot.&lt;/p&gt;
&lt;p&gt;That was a &amp;ldquo;fun&amp;rdquo; experience. For three days, I got to enjoy walking around the house without pain.&lt;/p&gt;
&lt;p&gt;Before a week was over, it was back to the way it was. Which is ironic since they kept saying &amp;ldquo;it could last for months or even years&amp;rdquo; when I honestly think there was no way it would have been any lasting pain. Instead, it was a stalling gesture or something to placate insurance.&lt;/p&gt;
&lt;p&gt;So I tried to lose the weight.&lt;/p&gt;
&lt;p&gt;I really did.&lt;/p&gt;
&lt;p&gt;I tried when three steps on the elliptical caused my leg to shake and sharp pains to radiate up my leg. I tried when even walking the length of the basement was agony. The walls in the house had scuff marks from my shoulder because I couldn't walk straight.&lt;/p&gt;
&lt;p&gt;For three months I tried and I failed.&lt;/p&gt;
&lt;p&gt;I called the doctor back. I was willing to take the risk, but I couldn't lose any more weight.&lt;/p&gt;
&lt;p&gt;The nurse called back. The doctor refused to do anything unless I lost the weight.&lt;/p&gt;
&lt;p&gt;I can't really describe how much that gutted me. I was too fat to help and I was in a situation where I couldn't swim anymore, I couldn't walk, I couldn't do anything unless I was going to cripple myself. I didn't have the strength or willpower to grind through the pain and that&amp;hellip; person just dismissed me as no longer worth the effort.&lt;/p&gt;
&lt;p&gt;(As a side note, I got the same behavior from that office when I &lt;a href="/tags/2014-broken-leg/"&gt;broke my leg in 2014&lt;/a&gt;. Different doctor, same &amp;ldquo;compassionate&amp;rdquo; office. Needless to say, I'm not planning on going back.)&lt;/p&gt;
&lt;p&gt;Thankfully, things worked out. As I bitched about the doctor (to whoever looked like they wanted to know), random people in my life started talking about the same doctor: Dr Chad Abernathy. I heard about husbands, family members, or even themselves that he had helped. It wasn't 100% and sometimes they had to go back, but he did something.&lt;/p&gt;
&lt;p&gt;I needed something.&lt;/p&gt;
&lt;p&gt;I called. I got an appointment.&lt;/p&gt;
&lt;p&gt;It was only a twenty minute conversation where he ensured that I wasn't too fat, he was very good at the surgery, and he could take me the next week. I even heard a medical version of &amp;ldquo;bless his heart&amp;rdquo; in the conversation.&lt;/p&gt;
&lt;p&gt;A week later, I was walking without pain. No limping in the morning, no hobbling. I'm still limited in what I can do: no bending more than thirty degrees or lifting more than thirty pounds, but I'm not in pain anymore. My leg isn't tingling and I'm not waking up thinking I'm drowning in ice. I can get out of bed and I can stand.&lt;/p&gt;
&lt;p&gt;I can go to the grocery store and walk around the block. It has been &lt;em&gt;years&lt;/em&gt; since I could do that without pain. Hopefully, I'll be able to take the dog on a walk and be able to lift things again.&lt;/p&gt;
&lt;p&gt;Throughout this, Child.0 has done an amazing job of stepping up and helping me. Even when I drop something on the floor and announce &amp;ldquo;well, that was lost forever&amp;rdquo;. Last weekend, they dragged two couches onto the sun porch to decorate it without asking for help. I'm really lucky that I have a good kid to help me.&lt;/p&gt;
&lt;p&gt;I'm back in physical therapy, but this time, things are getting better. I don't feel like I was hitting a plateau of recovery yet, only slow and steady progress where each day is getting better. I still have another month or so before I'm reevaluated.&lt;/p&gt;
&lt;p&gt;I also have two months before I'm going for my followup for my diabetes. Part of this inability to move has done some noticeable damage because I couldn't work out. I'm scared about this next one because of that, but I have a small reprieve so I'm going to do what I tried: get my strength back and hopefully lose some weight.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Writing Mannerisms</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/03/08/writing-mannerisms/" />
    <updated>2025-03-08T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/03/08/writing-mannerisms/</id>
    <category term="writing" scheme="https://d.moonfire.us/categories/" label="Writing" />
    <summary type="html">Just a little commentary on my quirks of writing, the origin of some of them, and commentaries on the thoughts that rattle around in my head.
</summary>
    <content type="html">&lt;p&gt;This post is just a fun little writing game to point out some of the quirks of my writing, some intentional, some of them I thought about, and others that just keep showing up no matter what I do.&lt;/p&gt;
&lt;p&gt;This was inspired by &lt;a href="https://joelchrono.xyz/blog/writing-mannerisms"&gt;Joel's post of the same name&lt;/a&gt; which was inspired by &lt;a href="https://shellsharks.com/writing-mannerisms"&gt;shellshark's post&lt;/a&gt;. Mostly I see it as a little fun, not unlike the hash games that show up on the fediverse. Also an opportunity to say why I do some things and some that haunt me.&lt;/p&gt;
&lt;h2&gt;Tokenization&lt;/h2&gt;
&lt;p&gt;Let's start with the most annoying: I frequently write the opposite word of what I mean. I also skip words when I'm writing. Because I also type it correctly, that means I don't notice it when I'm checking for spelling or even scanning the page. This has been one of my most frustrating little bugs I've never really handled.&lt;/p&gt;
&lt;p&gt;I also miss the plurals of words or skip a couple key letters (but still spell some random word right).&lt;/p&gt;
&lt;p&gt;I also start paragraphs frequently with &amp;ldquo;So,...&amp;rdquo; or &amp;quot;Because&amp;hellip;&amp;quot;.&lt;/p&gt;
&lt;p&gt;I also say &amp;ldquo;does that make sense&amp;rdquo; way too often in general.&lt;/p&gt;
&lt;p&gt;I do not like complex sentences. I loved learning German, but hated having to wait until the end of the sentence to know what was going on. And, while I have a relatively large vocabulary, I don't use when I write. I like simple sentences; according to a lot grammar programs, I write at a &amp;ldquo;third grade level.&amp;rdquo; I'm not sure what that means since I was reading David Eddings and Andre Norton in third grade.&lt;/p&gt;
&lt;p&gt;I also love &lt;a href="https://en.wikipedia.org/wiki/Interrobang"&gt;interrobangs&lt;/a&gt; but I use them for one specific purpose: when someone is screaming out a question. Honestly, if I had the nerve, I would typeset the books with &amp;ldquo;‽&amp;rdquo; instead of &amp;ldquo;!?&amp;rdquo; (and never &amp;quot;?!&amp;quot;). You never know, I might some day.&lt;/p&gt;
&lt;p&gt;Likewise, I have rules for using ellipses (only for trailing sentences or thoughts, eliding, and leading examples) and em-dashes (parathenitcal asides and interrupted sentences and thoughts) along with specific formatting. Outside of those, I almost never use them.&lt;/p&gt;
&lt;p&gt;Outside of ellipses, I don't use repeated punctuation. You will never see a &amp;ldquo;!!&amp;rdquo; or a &amp;ldquo;??&amp;rdquo; out of me.&lt;/p&gt;
&lt;p&gt;Likewise, you will almost never see a casual use of italics or bolds in my writing and rarely in my conversations. Italics are for names of books and plays, ships, and whatever else the Chicago Manual of Style says they are for. One of my early lessons was &amp;ldquo;the words you write should give the intensity or purpose&amp;rdquo; right after they told me I should never use an interrobang again. Still going to use those.&lt;/p&gt;
&lt;h2&gt;Details&lt;/h2&gt;
&lt;p&gt;I'm terrible at details. I mean, I usually scatter in a few here and there, but I don't need to know someone's height or how big their bust is. Most of the time, it's a light brush of hair color, eye color, and specific build.&lt;/p&gt;
&lt;p&gt;But, that depends on the character. Since I write single point-of-view stories almost exclusively, the personality and interests of the main character also dictates the details. The seamstress notices stitches and the fit of clothing. A warrior will have obvious weapons right up there before they notice hair color. The teenage boy is going to be staring at someone's ass or breasts. My OCD mage has a stunning amount of detail that I have to dial back because it can be overwhelming (I don't write Mudd much anymore because of that).&lt;/p&gt;
&lt;p&gt;Now, the part of me that shows up in every character is how I show emotions. Remember the scene in &lt;em&gt;The Accountant&lt;/em&gt; when they are in prison and Ben Affleck's character says &amp;ldquo;you are angry.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;That's kind of me.&lt;/p&gt;
&lt;p&gt;I don't always know when I'm feeling an emotional unless I look at myself objectively. (&amp;quot;I appear to be speaking loudly and typing faster, I think I'm upset.&amp;quot;) I'm not so great at reading other people's emotions at an unconscious level, so I'm usually looking for someone's body language, how their language structure changes, how they fidget or what goes on in their faces. That shows up in my writing because I don't really have a concept of &amp;ldquo;they are angry&amp;rdquo; in my head. I have to show it, because that is how I perceive emotions.&lt;/p&gt;
&lt;p&gt;Sadly, this happens even for my characters who can understand emotions and don't have my struggles.&lt;/p&gt;
&lt;h2&gt;Structure&lt;/h2&gt;
&lt;p&gt;My paragraph structure is formulaic. Some of this comes from my favorite writing book, &lt;em&gt;Techniques of a Selling Writer&lt;/em&gt; by &lt;a href="https://en.wikipedia.org/wiki/Dwight_V._Swain"&gt;Dwight V. Swain&lt;/a&gt; but others come from patterns. I'm fairly strict about one paragraph per character. I used to have one person saying something, then have the next character in the same paragraph but then I found it confusing. So, the actions and dialog are kept in the same paragraph to make it easier to know who is speaking.&lt;/p&gt;
&lt;p&gt;I also use the back and forth with these paragraphs, the motivation-reaction unit (MRU) that Swain talks about. Those two together tie together into the beat of moving the story faster but also help me picture the scene since one thing leads to another in a logical sequence.&lt;/p&gt;
&lt;p&gt;It also helps prevent something Mickey Zucker Reichert told me: there is no such thing as simultaneous action on the page.&lt;/p&gt;
&lt;p&gt;Swain and Jim Butcher's also create another reoccurring pattern in my writing. I like to take a chapter to smell the roses. The so called &amp;ldquo;sequel&amp;rdquo; scenes where the characters have a chance to process what happened in the previous one, come up with a new plan that will eventually fail, and then go into it. That helps break up the constant build-up of scenes, which I usually reverse for the end of books, and instead keeps things moving steadily.&lt;/p&gt;
&lt;p&gt;And the last is: I don't do scene breaks. I mean, I used to but then I realized I don't like the &amp;ldquo;hours later&amp;rdquo; starts to paragraphs, so I break apart the chapters. New location? Chapter. Hour later? Chapter. Character takes a twenty minute nap? Chapter. Sex scene? Depends on the book, but fade to black is always a chapter.&lt;/p&gt;
&lt;h2&gt;Exposition&lt;/h2&gt;
&lt;p&gt;I hate exposition. I'm not talking building a scene and setting it. I'm mostly talking about the &amp;ldquo;as everyone in the entire room already knows, the current president of France is&amp;hellip;.&amp;rdquo; If everyone knows, then I want the scene to be natural.&lt;/p&gt;
&lt;p&gt;That is why I have epigraphs for when I really need to tell everyone a rod is sixteen and a half feet, or a chain is sixty-six feet. When I need to let readers know who the royalty is in the country or how a certain aspect of magic works.&lt;/p&gt;
&lt;p&gt;This does mean I don't resolve every plot. If a character doesn't have the opportunity to know why something happened, they will never know. Someone else might, but not them. I try to resolve the &amp;ldquo;important&amp;rdquo; plots, but I'll leave some dangling ones here and there to be picked up later.&lt;/p&gt;
&lt;h2&gt;Style Guide&lt;/h2&gt;
&lt;p&gt;Now, given all those, you probably aren't surprise to know that I've formalized some of the intentional aspects of my writing and posted it as a &lt;a href="//fedran.com/style-guide/"&gt;style guide&lt;/a&gt; for my editors. That way, they won't call out things I've done on purpose or I do because of my tools.&lt;/p&gt;
&lt;h2&gt;Conversations&lt;/h2&gt;
&lt;p&gt;And the last section, how I talk informally. The biggest one is when I have a problem, I give a lot of details to explain it. Way too much in some cases, but I don't like wasting people's time, have a relatively high typing speed, and can read at a rather impressive rate. So, to avoid someone going through the basic diagnostic problems (&amp;ldquo;is your computer turned on&amp;rdquo;, &amp;ldquo;have you rebooted&amp;rdquo;), I will try to detail what I have tried so we can skip the easy steps.&lt;/p&gt;
&lt;p&gt;This leads into first &amp;ldquo;wall of text&amp;rdquo; responses I give. The other is when I'm upset. When I am upset, I type faster. Usually, I'm trying to explain why I did something and my reasoning behind it because someone implied I was stupid or didn't understand the problem. This leads to the other wall of text.&lt;/p&gt;
&lt;p&gt;Probably the most impressive one was when I was going through college. I was trying to get out of a networking class because I had been doing it for years. The person who reviewed it came back with a &amp;ldquo;nothing in this person's resume indicates they have any idea how networking goes.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;My response started with asking what level of the OCI networking did they feel I didn't have enough experience. And then started detailing my experience in all seven layers. I waxed poetically about running cat5 in the drop ceilings of my work or the hours I sat in front of a nine-inch monitor writing a real-time stock trading application. It took me an hour to write thirty pages of a response, which I sent back before I realized I should have sat on it or maybe edited it.&lt;/p&gt;
&lt;p&gt;About two hours later, I got the &amp;ldquo;yeah&amp;hellip; I'm not going to read this, good enough&amp;rdquo; response and I got out of the class.&lt;/p&gt;
&lt;p&gt;The last thing is probably related to whatever value of the autism spectrum I'm on: I tag a lot of my sentences with an emotional state at the end. Usually in terms of &amp;ldquo;old school&amp;rdquo; emoji (&lt;code&gt;:)&lt;/code&gt;, &lt;code&gt;:(&lt;/code&gt;). I know that text is a lossy form of communication, and it is highly likely the people will misunderstand you, so I try to give hints of whatever emotional state I think I'm trying to convey.&lt;/p&gt;
&lt;p&gt;I don't like graphical emoji though, mainly because I can never figure out the keys and I don't like the packaged emojis on most systems. But, if I find ones I like, such the fruits on Microsoft Teams, I'll use them fairly heavily.&lt;/p&gt;
&lt;p&gt;Now, emojis at the beginning of a sentence is a thread indicator. That means I'm having multiple conversations going on at once and need to group them. Kind of like playing D&amp;amp;D and having an in-character chat, and out-of-character chat, and a &amp;ldquo;anyone see the cheetos&amp;rdquo; chat.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;So, those are some of my quirks of writing and the origin of a few of them. Hopefully, this will interest someone but it was also a chance to self-reflect on myself.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>So Close, Yet So Far</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2025/01/28/home-repairs/" />
    <updated>2025-01-28T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2025/01/28/home-repairs/</id>
    <category term="derecho-2020" scheme="https://d.moonfire.us/tags/" label="Derecho 2020" />
    <summary type="html">The "big stuff" is finally (mostly) repaired.
</summary>
    <content type="html">&lt;p&gt;As much as I hoped to be done with the major repairs from the &lt;a href="/tags/derecho-2020/"&gt;derecho&lt;/a&gt;, we hit a few snags along the way that got in the way. Overall, they aren't major ones and my anxiety has dropped significantly with what they had done.&lt;/p&gt;
&lt;p&gt;The contractors finished with one room but it was too cold to finish the repairs on the porch (the derecho blew out windows, tore up wood, and soaked everything in rain). Repairing that is going to need a soffit and we hit some nasty cold weather, so they are going to wait until March to finish up.&lt;/p&gt;
&lt;p&gt;The repairs they did were&amp;hellip; okay? I mean, after the amazing work they did on the basement, the upstairs felt more like a 80% job in terms of polish and quality. Some of it is that I'm really sensitive to the floor and I can feel it flexing, but in the end, I decided it was good enough for now.&lt;/p&gt;
&lt;p&gt;I mean, the entire reason we're getting contractors for this bit was because I didn't have the proper tools for everything and I'm struggling with kneeling and getting up. The bulk of the repairs involved tearing out carpet and putting down planks, so not being able to knee was kind of a big deal. In that regard, I'm as happy as I'm going to be so I'm just telling myself it is okay.&lt;/p&gt;
&lt;p&gt;That also means the big stuff is finally done. At least the things I have to get anxious about. All that is left are little things: bringing back tables, hanging pictures, and the like. In March, they'll repair the porch but that should involve me getting some couches off the porch and letting them do their thing.&lt;/p&gt;
&lt;p&gt;Of course, &amp;ldquo;little things&amp;rdquo; will probably take me another year to finish. It's easier to put &amp;ldquo;replace an electrical outlet&amp;rdquo; to the side when it involves shutting down my home lab, or buying new cold air return plates. But I'm okay with that.&lt;/p&gt;
&lt;p&gt;That isn't to say that there aren't other big things. Besides the porch, I'm also hoping to get my mother's library that I inherited into the house. That is going to be a trial in itself. If I'm lucky, I'll build some bookcases along the way and get a nice, dense arrangement that doesn't try to take over too much of the house.&lt;/p&gt;
&lt;p&gt;I also need to work on my health and see if I can get my dislocated disks fixed.&lt;/p&gt;
&lt;p&gt;Despite &lt;a href="/blog/2024/12/31/end-of-2024/"&gt;last post&lt;/a&gt; being rather negative, I feel a lot better.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>The End of 2024</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/12/31/end-of-2024/" />
    <updated>2024-12-31T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/12/31/end-of-2024/</id>
    <category term="derecho-2020" scheme="https://d.moonfire.us/tags/" label="Derecho 2020" />
    <category term="nor-curse-be-found" scheme="https://d.moonfire.us/tags/" label="Nor Curse Be Found" />
    <category term="second-hand-dresses" scheme="https://d.moonfire.us/tags/" label="Second-Hand Dresses" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <summary type="html">The final thoughts of 2024.
</summary>
    <content type="html">&lt;p&gt;In a few hours from now, 2024 ends. I don't have a lot of emotional attachment to the new year, mainly because it is just an abstract point of time for a calendar we've created over the centuries. But, it isn't a bad time to look back and see what I have and haven't done.&lt;/p&gt;
&lt;p&gt;Overall, 2024 is probably my worst year ever. There was a lot going on, but I'm not going to go into the details of those because it doesn't really help anyone. Needless to say, it was rough for me, mentally and physically, professionally and personally. And a whole bunch of other &amp;ldquo;-ly&amp;rdquo; I can't really think about.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Warning: This isn't a happy or positive post.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Mental Health&lt;/h2&gt;
&lt;p&gt;2024 was the first time I've ever had a panic attack.&lt;/p&gt;
&lt;p&gt;That was scary and I had to look at why it was happening. It was just &amp;ldquo;more of the same&amp;rdquo; as it were but I'm still trying to figure out what got me there. I think a lot of it is this feeling that I can't solve things anymore (Long Covid maybe) and everything is ten times harder than it used to be.&lt;/p&gt;
&lt;p&gt;More importantly, I haven't been writing. I want to write, I want to write so badly. Writing makes me feel good but I've had this writer's block for a few years now and its beginning to dig into my thoughts. I have ideas in my head, but not the focus that isn't interrupted by obligations and deadlines.&lt;/p&gt;
&lt;p&gt;This is also the year I've been pushed into the rubicon of being a individual contributor at work and a manager. That means 4-6 hours of Zoom meetings almost every single day. It starts at 08:30 with a 1:1 with my manager, goes into an ninety minute meeting with the developers, an hour of stand up, and then the &amp;ldquo;when we're done, I need Dylan&amp;rdquo; that follows. In most cases, I have less than a minute between meetings or I'm five minutes late for the next, so I don't get any break for four hours every day.&lt;/p&gt;
&lt;p&gt;Like commuting, that meeting train is grinding me down. I'm struggling to be excited about programming because I'm just stagger from that train to try getting a half hour nap before going into the next set.&lt;/p&gt;
&lt;p&gt;Something is going to break in 2025, I only hope it isn't me. More likely, I'm going to transition completely off programming and will no longer have the joy of refactoring code or getting it solved. It isn't going to help that politics are going to be digging into me, watching friends being afraid and hurt, and struggling through&amp;hellip; everything.&lt;/p&gt;
&lt;h2&gt;Physical Health&lt;/h2&gt;
&lt;p&gt;2024 was when I found out that I had a dislocated disk which was digging into my spinal cord, which is why I've been in pain since March of 2023. The surgeon said it could be fixed, but I need to lose weight to get there. A lot of the tail end was just trying to dig through my backlog of obligations and needs so I could get the home exercise equipment working again. I'm still not there. I'm still trying to do that, the Cortisone shot only gave me a few days of relief, but then a slow slide back to where I want.&lt;/p&gt;
&lt;p&gt;The main part is, I can barely pick up anything and have had to leverage Child.0 into picking things up. The good thing, I know what is causing the pain so I can stop. The bad part is that there is no exercise, no stretch, nothing that can prevent it other than losing weight, getting surgery, and hoping it gets better.&lt;/p&gt;
&lt;p&gt;And not thinking about the catastrophic failure of my &amp;ldquo;routine&amp;rdquo; hernia surgery. Or the severe problems I had from my vasectomy.&lt;/p&gt;
&lt;h2&gt;Derecho&lt;/h2&gt;
&lt;p&gt;The &lt;a href="/tags/derecho-2020/"&gt;2020 derecho&lt;/a&gt; is still in my life, but we're &lt;em&gt;so&lt;/em&gt; close to finally closing that chapter of our life. In January, we should be getting the &amp;ldquo;last&amp;rdquo; of the repairs done on the house.&lt;/p&gt;
&lt;p&gt;Related, I got a letter this week from my insurance company about replacing my roof and needing deposits to register it. How can I provide that went the guy who did it stole twelve thousand dollars from me and walked away? It isn't like I can say &amp;ldquo;yeah, you know how you owe me a tremendous amount of money? Could I get a receipt? By the way, fuck you.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I'm hoping between that, getting the final room done, and organizing my workshop (to integrate my late father's shop) will finally let me set everything aside. Thought, at this point, it feels like it has been burrowing in my skull for so long that I don't know how to live without it.&lt;/p&gt;
&lt;p&gt;I'm hoping to find out.&lt;/p&gt;
&lt;h2&gt;Writing&lt;/h2&gt;
&lt;p&gt;The urge to write is coming back. I think it's finally time to focus on getting the culture library finished, figure out &lt;a href="/tags/second-hand-dresses/"&gt;Second-Hand Dresses&lt;/a&gt;, and maybe design some covers. Or even finish &lt;a href="/tags/nor-curse-be-found/"&gt;Nor Curse Be Found&lt;/a&gt;, which has been hanging around for a while.&lt;/p&gt;
&lt;p&gt;If anything, I want 2025 to be a year of being creative again. I want to write, I want to work on libraries, I want to make things and have that feeling of being satisfied with what comes out.&lt;/p&gt;
&lt;p&gt;Plus handle my monthly obligations that never go away.&lt;/p&gt;
&lt;h2&gt;Coding&lt;/h2&gt;
&lt;p&gt;I want to polish up some of my libraries, including &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt; and maybe put it out into the open. I have other libraries that have been hanging out (&lt;a href="/tags/mfgames-culture/"&gt;MfGames.Culture&lt;/a&gt; being one of them) that I need to buckle down and finish up.&lt;/p&gt;
&lt;h2&gt;Play&lt;/h2&gt;
&lt;p&gt;I'm hoping to play around more in 2025. Not just games with the children, but drawing, writing, and maybe building a kitchen cabinet.&lt;/p&gt;
&lt;h2&gt;Goals&lt;/h2&gt;
&lt;p&gt;I can't really say I have goals for the year though. Instead, I just have hopes that 2025 is going to be better than 2024. Beyond that, I'm not really planning or expecting anything, just a vague hope that it works out in the end and I find my smiles again.&lt;/p&gt;
&lt;p&gt;There is only one way to find out.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Just keep swimming.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dory, &lt;em&gt;Finding Nemo&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
</content>
  </entry>
  <entry>
    <title>Why SeaweedFS?</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/11/08/why-seaweedfs/" />
    <updated>2024-11-08T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/11/08/why-seaweedfs/</id>
    <category term="development" scheme="https://d.moonfire.us/categories/" label="Development" />
    <category term="ceph" scheme="https://d.moonfire.us/tags/" label="Ceph" />
    <category term="seaweedfs" scheme="https://d.moonfire.us/tags/" label="SeaweedFS" />
    <category term="nixos" scheme="https://d.moonfire.us/tags/" label="NixOS" />
    <category term="sand-and-bone" scheme="https://d.moonfire.us/tags/" label="Sand and Bone" />
    <summary type="html">Thoughts on why I take the effort to use a distributed network drive for my home.
</summary>
    <content type="html">&lt;p&gt;I got an email last week asking if I could explain why I set up a Seaweed server. It's been a while since I talked about it, mostly in hints in &lt;a href="/blog/2022/12/10/ceph-and-nixos/"&gt;this post from 2022&lt;/a&gt; and &lt;a href="/blog/2024/03/21/switching-ceph-to-seaweedfs/"&gt;this post from 2024&lt;/a&gt;. The request gives me an opportunity to expand on it, and to remind myself why I do it.&lt;/p&gt;
&lt;h2&gt;Stories&lt;/h2&gt;
&lt;p&gt;In some regards, there is a bit of trauma in me when it comes to losing out on things. This includes hearing stories that my father told me and realizing that when he died, they were gone forever. These same thoughts came up in my novel &lt;a href="/tags/sand-and-bone/"&gt;Sand and Bone&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The little girl's confessions echoed in Rutejìmo's head in a quiet symphony as he wrote in his book. The hand-bound collections of pages creaked under his hand, the leather thong strained to hold the almost fifty pages of tightly-spaced writing. Over the years, he had added a dozen pages to the collection. It wouldn't be too long before the binding couldn't handle the additional pages, but he thought he had a few more years left before that happened.&lt;/p&gt;
&lt;p&gt;Even with his additional pages, he didn't have room to write down all of the stories he had heard over the years. He wanted to detail the joys of the little girl's death, such as the choked story about how she had stolen her brother's toy when he wasn't looking. He had also wanted to write the horrors, like one man's confession for killing his sister. Each one was precious and important. Time would erase their stories and a part of Rutejìmo died every time he forgot one.&lt;/p&gt;
&lt;p&gt;&amp;mdash; &lt;a href="//fedran.com/sand-and-bone/chapter-008/"&gt;Sand and Bone 8: Alone&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This fear is more than just one person, losing stories of things that happened decades ago as told by an old man. This includes my own stories, which will also be lost when I die. There is little chance I will be &amp;ldquo;done&amp;rdquo; with &lt;a href="/tags/fedran/"&gt;Fedran&lt;/a&gt;. There is little chance I will get all of the stories out of my head and onto paper. There isn't enough time, I don't have the self-esteem to do it, and I don't have the drive. When I'm gone, so are those stories. But, even going beyond that, there are so many stories of horror and joy that I feel we are losing with the passage of time. Happy stories of people falling in love, horror stories of &lt;a href="https://www.auschwitz.org/en/"&gt;Auschwitz&lt;/a&gt;, and everything in between. It is that breadth of stories, lessons, and happiness that we lose.&lt;/p&gt;
&lt;p&gt;But the big question is, will anyone care? Does anyone want to know the first time I got caught shoplifting. Or the first time I finally said &amp;ldquo;I love you&amp;rdquo; to Partner?&lt;/p&gt;
&lt;p&gt;Probably not.&lt;/p&gt;
&lt;p&gt;But, you never know.&lt;/p&gt;
&lt;p&gt;I'm never going to record them, but when I was given my father's artwork and decades of effort, I don't want to lose them yet. I don't want to just chuck it aside. So, I need to keep them.&lt;/p&gt;
&lt;h2&gt;Hoarding&lt;/h2&gt;
&lt;p&gt;I come from a long line of hoarders. I realized that as I look at my DVD collection or the boxes of books I have. My LEGO collection isn't huge, but it does fill five or six boxes. I once got rid of it, and I felt guilty for years for doing that; which is why I built it up again. Sometimes, I just play.&lt;/p&gt;
&lt;p&gt;My mother did it. She had a massive display case of dragons and cats. She has dozens of sets of china in the basement. Entire room with the bones of a bankrupt lumber mill. A massive library (that I inherited).&lt;/p&gt;
&lt;p&gt;My father did it. I spent days going through pieces of paper where he kept every calendar, shred of note, and personal letter he sent to his children. I saw research when I asked for help to get through college, mainly to prove that I was going the wrong path and I really should just drive two hours one way to go the college he thought was a better choice for me. I ended up saying &amp;ldquo;screw you&amp;rdquo; and went into debt for a couple decades instead.&lt;/p&gt;
&lt;p&gt;My father has terabytes of images that he drew. Decades of him struggling to be a &amp;ldquo;good&amp;rdquo; artist, self-doubt and pain. But I'm so happy to watch him to do it. Also, he did the artwork for the nuclear reactor project he was on, and the particle accelerator. And birthday cards for all of his children and grandchildren.&lt;/p&gt;
&lt;p&gt;For me, there is a good size of digital hoarding going on. I mean, a couple thousand ebooks is one things, but I have PDFs, for game systems that go back years. The original PDFs of HERO System 5 and 6. Every scan of the &lt;em&gt;Dragon&lt;/em&gt; magazine. GURPS. Legal documents, funny stories, and the like. And then there are my DVDs. Back when I didn't have children, I was buying 3-5 a week. When Suncoast Video went out of business, I had a huge refund from the IRS. Walked into the store and said &amp;ldquo;I'm gunna buy everything I can.&amp;rdquo; Now, there are DVDs I can't buy anymore, I can't find. But they are slowly rotting in my file cabinet (did you know they only have a 20-30 year shelf life?). I don't want to lose them, even if I don't use them every day.&lt;/p&gt;
&lt;p&gt;And then there is Partner's photography business. They do large photo shoots but they also need to keep them around for years &amp;ldquo;just in case&amp;rdquo; someone loses their wedding photos. She doesn't &amp;ldquo;have to&amp;rdquo;, but I don't want to ever have to say &amp;ldquo;sorry, I don't have them&amp;rdquo; for a senior photos or someone's puppies. Or family members are lost.&lt;/p&gt;
&lt;p&gt;So I want to keep them.&lt;/p&gt;
&lt;h2&gt;Storage&lt;/h2&gt;
&lt;p&gt;All this takes space. The nice thing about digital space is that I can make backups. I can take them with me with a few kilograms of hard drives or upload them into the cloud. I can make copies to make it harder to lose (like when I was hit with a ransomware that took out my media server). The goal is &lt;a href="https://www.backblaze.com/blog/the-3-2-1-backup-strategy/"&gt;3-2-1 Backup Strategy&lt;/a&gt;: three copies, two locations, and at least one offline copy.&lt;/p&gt;
&lt;p&gt;Now, dealing with storage is something I've done for quite a long time. I remember being pulled out of sixth grade school for a day to help my mother recover her RAID 5 box. At the time, drives were in the low 100s of megabytes, but I had to learn a lot about how RAID worked to figure it out. Then watching the slow recovery that took an entire day&amp;hellip; then having another drive fail about a day after we recovered the first time and doing it all again. I used and watched hardware and software RAID controllers fail.&lt;/p&gt;
&lt;p&gt;At the time, the data wasn't static. We were processing millions of records to do analysis. The drives would get corrupted by poor power, heavy usage, and the eventual failure of mechanical drives. We had customers lose data, watched our data centers blow a disk. Like Partner's photos, we had to keep our analysis for years after because of re-evaluation or the occasional lawsuit.&lt;/p&gt;
&lt;p&gt;Over time, it became apparent that there was a maximum size where RAID was useful. After a while, it wasn't a matter &amp;ldquo;if&amp;rdquo; a drive goes back but &amp;ldquo;when&amp;rdquo; a drive goes bad. Backblaze (where I keep my backups, I recommend them) had a &lt;a href="https://www.backblaze.com/blog/backblaze-drive-stats-for-q2-2024/"&gt;quarterly blog post&lt;/a&gt; about drive stats. They track with my own experiences. Yeah, 1.7% failure rate doesn't seem like much but when I'm dealing with stuff that needs to be kept for years, it hits a lot faster than you'd think it would. And offline backups fail too.&lt;/p&gt;
&lt;p&gt;When I left my mother's company, I went back a little and just threw some drives into a big machine. Didn't bother with RAID because I knew it would fail since I was looking at a decade of hard drive use and started with a manual mirroring between a couple Windows partitions. But as the DVDs got ripped, music got purchased from Amazon, and photos kept being taken, I started to run out. Soon, it wasn't just &lt;code&gt;Q:\&lt;/code&gt;, &lt;code&gt;R:\&lt;/code&gt;, and &lt;code&gt;S:\&lt;/code&gt; being copies but each one having a portion of everything.&lt;/p&gt;
&lt;p&gt;When I got hit with the ransomware attack, I lost my DVD collection (years of ripping) but not most of Partner's photos and the other things. I started to recover them but one of the drives didn't make it.&lt;/p&gt;
&lt;p&gt;Then I lost another drive a few months later.&lt;/p&gt;
&lt;p&gt;Then we didn't have money to handle the failure indicators, so I was just watching the SMART notices popping up knowing that there was nothing to do but watch the slow-moving train accident.&lt;/p&gt;
&lt;p&gt;Then the media server decided to go down for a week.&lt;/p&gt;
&lt;p&gt;Not having access to anything was a stark reminder that I had to do &amp;ldquo;something&amp;rdquo; if I didn't want to lose everything. And it wasn't just tossing in another drive every once in a while. I had hit the threshold where I needed to spread it out across more than just drives, I needed to spread it across servers.&lt;/p&gt;
&lt;h3&gt;Ceph&lt;/h3&gt;
&lt;p&gt;Enter &lt;a href="/tags/ceph/"&gt;Ceph&lt;/a&gt;. I've read about Ceph for many years before that point and it always appealed to me. It seemed to solve a lot of the problems I've experienced over the years. And it didn't have to require same-sized disks to pull of a RAID. And I could add machines if I needed to add more storage.&lt;/p&gt;
&lt;p&gt;The biggest thing with Ceph is that it balanced across multiple servers to reduce failure, but also let you mark files as needing one, two, or more copies. Partner's photo library? Two copies. Videos? One is probably good enough. The servers didn't have to be that powerful either, so I could use my older machines to keep the disk when I had to upgrade to a newer one. In theory, I could use a Raspberry Pi to act as a cluster.&lt;/p&gt;
&lt;p&gt;The incident when this happened was when the media server died again. I had saved some money from my commissions (earmarked to get a book edited) and used it to buy fresh hard drives. If anything, replacing the 10+ year old drives with something new and bigger would give me some. I also took the opportunity to switch to &lt;a href="/tags/nixos/"&gt;NixOS&lt;/a&gt; instead of Windows, which I dislike anyways. I mean, NixOS had options for Ceph, how hard could it be?&lt;/p&gt;
&lt;p&gt;Apparently, it really wasn't ready for prime time. Eventually the wiki popped up to say that.&lt;/p&gt;
&lt;p&gt;Took me a week to figure it out. There was a lot missing (and still is) from the core, but I managed to write up some notes for myself on how to make it work. But, after days of trial and errors, of &amp;ldquo;almost got it&amp;rdquo; joys only to watch it crash, I finally got something working. And it was glorious. At least until I realized was swapping like mad and Ceph really needs 1 GB per TB of storage. A few frantic purchases later, and I had a couple more cheap Dell machines (one of the ones that actually died this week) and I ended up with a fairly balanced Ceph cluster.&lt;/p&gt;
&lt;h3&gt;SeaweedFS&lt;/h3&gt;
&lt;p&gt;I liked Ceph, but I'm not on it anymore. Partially it was selfish reasons, I want to fix the struggles I had bringing on a new drive into the cluster but the community decided to go a different way. But there was also no one who was able to to do that different way while I was down. To test a fix myself, I had to build for twelve hours just to see if something worked (I also work with 10+ year old computers a lot). I was on unstable, so I went weeks being unable to build because I couldn't downgrade to stable. I also learned that &lt;code&gt;nixpkgs&lt;/code&gt; had some patterns that were really hard to get into, like redefining &lt;code&gt;lua&lt;/code&gt; at the package level to mean a specific version of &lt;code&gt;lua&lt;/code&gt; and no one on the Matrix or Discourse forum was able to tell me that until I happened to find one person who explained it and said I should have just &amp;ldquo;known&amp;rdquo; about that mapping.&lt;/p&gt;
&lt;p&gt;And then, the same day I realized my efforts to get Ceph working were linked on the &lt;a href="https://nixos.wiki/wiki/Ceph"&gt;NixOS wiki for Ceph&lt;/a&gt;, I also saw a little line:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Another distributed filesystem alternative you may evaluate is SeaweedFS.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I vaguely remembered looking at SeaweedFS before, but I was focused on getting Ceph working. And in that moment, when Ceph was not building and I didn't have the resources to fix the problem myself, I decided to try it out.&lt;/p&gt;
&lt;p&gt;SeaweedFS does 80% of Ceph. It didn't have all the fancy features, but it had the features I wanted:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Distributed across multiple machines&lt;/li&gt;
&lt;li&gt;Ability to add or remove storage on the fly&lt;/li&gt;
&lt;li&gt;Variable replication copies&lt;/li&gt;
&lt;li&gt;Currently built and could be run&lt;/li&gt;
&lt;li&gt;Can be mounted on Linux&lt;/li&gt;
&lt;li&gt;It uses 30 GB volumes on standard &lt;code&gt;ext4&lt;/code&gt; partitions instead of a custom one I cannot debug&lt;/li&gt;
&lt;li&gt;Single Go executable (don't code Go but it only takes twenty minutes build, not twelve hours)&lt;/li&gt;
&lt;li&gt;Could create S3/cloud tier backup (Ceph couldn't do that)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Yeah, it didn't have NixOS options but Google and GitHub gave me a starting point for me to puzzle it out on my own. I learned a new library and started with a little partition to see if it worked. Like Ceph, I had a lots of fits and starts, trials and puzzling through it, but eventually got it working.&lt;/p&gt;
&lt;p&gt;And it was more scattered in terms of information but it worked.&lt;/p&gt;
&lt;p&gt;It didn't do Ceph's &amp;ldquo;deep cleaning&amp;rdquo; to detect bit rot on failing drives.&lt;/p&gt;
&lt;p&gt;It blew up when you tried to create too many encoding shards without the space.&lt;/p&gt;
&lt;p&gt;It blew up when you tried to replicate without having enough nodes.&lt;/p&gt;
&lt;p&gt;Around that time, Ceph started building on NixOS again but I was already enamored by SeaweedFS. It was &amp;ldquo;good enough&amp;rdquo; for me. It took me about a week to migrate hunks of the Ceph data to the Seaweed, decommission a Ceph drive and make it a SeaweedFS drive. Then repeat until everything was moved over.&lt;/p&gt;
&lt;p&gt;A few weeks ago, I tried to consolidate my dad's hard drives into the cluster and ran out of space. I ended up buying a new minicomputer and throwing 11 TB worth of drives into it to give me room (two copies of everything, even the media files). This week, I lost one of those old Dells so I had to shuffle around the volumes with a few commands and it &amp;ldquo;just worked&amp;rdquo;. I'm going to replace the dead computer and I'm confident that it will &amp;ldquo;just work&amp;rdquo; then too. And, more importantly, I don't have to spend a week to figure out the commands to make it happen since I can just copy/paste a bunch of Nix code and redeploy my servers.&lt;/p&gt;
&lt;h2&gt;Thoughts&lt;/h2&gt;
&lt;p&gt;I could hoard less. It takes time and energy to keep the computers running but it makes sure Partner has their &lt;em&gt;Golden Girls&lt;/em&gt;, the kids have their videos, and I have my dad's artwork. Also, there is something peaceful about looking at a 29.9 TB partition and having it working smoothly.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+---------------------------------------------------------------------------------------------------------------+
| 1 fuse device                                                                                                 |
+-------------+-------+-------+-------+-------------------------------+----------------+------------------------+
| MOUNTED ON  |  SIZE |  USED | AVAIL |              USE%             | TYPE           | FILESYSTEM             |
+-------------+-------+-------+-------+-------------------------------+----------------+------------------------+
| /mnt/home   | 29.9T | 20.9T |  9.0T | [#############.......]  69.8% | fuse.seaweedfs | fs.home:8888:/         |
+-------------+-------+-------+-------+-------------------------------+----------------+------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Eventually this is all going to go away. When I die, my family isn't going to be able to keep it going. Like Rutejìmo's stories and my dad's artwork, all this will fade. But I'm going to keep it going as long as I can. And try to find more pages for my book.&lt;/p&gt;
</content>
  </entry>
</feed>
