﻿<feed xmlns="http://www.w3.org/2005/Atom">
  <title type="text" xml:lang="en">Statiq</title>
  <link type="application/atom+xml" href="https://d.moonfire.us/tags/statiq/atom.xml" rel="self" />
  <link type="text/html" href="https://d.moonfire.us/tags/statiq/" rel="alternate" />
  <updated>2026-10-08T17:35:30Z</updated>
  <id>https://d.moonfire.us/tags/statiq/</id>
  <author>
    <name>D. Moonfire</name>
  </author>
  <rights>Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</rights>
  <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>Reflections on Nitride</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2024/04/21/nitride/" />
    <updated>2024-04-21T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2024/04/21/nitride/</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="gatsbyjs" scheme="https://d.moonfire.us/tags/" label="GatsbyJS" />
    <category term="statiq" scheme="https://d.moonfire.us/tags/" label="Statiq" />
    <category term="cobblestonejs" scheme="https://d.moonfire.us/tags/" label="CobblestoneJS" />
    <category term="bevy" scheme="https://d.moonfire.us/tags/" label="Bevy" />
    <summary type="html">I just got mfgames.com migrated over to Nitride, so a few thoughts about the previous three years.
</summary>
    <content type="html">&lt;p&gt;This week's goal was to get &lt;a href="https://mfgames.com"&gt;mfgames.com&lt;/a&gt; migrated over to &lt;a href="/tags/mfgames-nitride/"&gt;MfGames.Nitride&lt;/a&gt;. It was a lofty goal, mainly because I've been making small incremental changes over the last year but never got something pushed over the finish line.&lt;/p&gt;
&lt;p&gt;I succeeded.&lt;/p&gt;
&lt;h2&gt;Migration&lt;/h2&gt;
&lt;p&gt;This is my third &amp;ldquo;big&amp;rdquo; site for Nitride, one that follows the common pattern of pulling in multiple Git repositories from my &lt;a href="https://src.mfgames.com"&gt;forge&lt;/a&gt;, integrating the data, and then creating a website out of it. In other words, one of the main reasons I created Nitride.&lt;/p&gt;
&lt;p&gt;It isn't exactly a fancy site, but I find that I'm not really creating flashy sites anymore. Part of this is because I still enjoy &lt;a href="/tags/gemini/"&gt;Gemini&lt;/a&gt;, which is a text-only format, but also because the chrome is distracting to me. I want something simple but not the black on gray of Netscape Navigator.&lt;/p&gt;
&lt;p&gt;Overall, I'm pretty happy with the results. It creates landing pages for projects, pulls in the &lt;code&gt;./docs/&lt;/code&gt; folder for each project and formats them (for example, &lt;a href="//mfgames.com/mfgames-cil/docs/gallium/"&gt;MfGames.Gallium&lt;/a&gt;), and merges news posts from the projects (while decorating them) and creates a single unified feed.&lt;/p&gt;
&lt;p&gt;There are more things I want to do, such as links to issue tracking, licenses, and other stuff. I also ended up losing some features like the culture sandbox (which I needed to rewrite anyways), the slippery maps, and a few other minor things.&lt;/p&gt;
&lt;p&gt;Still happy with it.&lt;/p&gt;
&lt;h2&gt;News&lt;/h2&gt;
&lt;p&gt;I was working on debugging creating news posts in individual project repositories anews posts from individual projects into mfgames.com as my &amp;ldquo;little thing&amp;rdquo; for today and I came up with the exact period of time I decided to write my latest static site generator (SSG):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ git log --pretty=&amp;quot;%ai %s&amp;quot;
...
2021-05-04 02:27:28 -0500 feat: implementing my own
2021-04-07 22:13:20 -0500 chore: updating Statiq libraries
...
2014-05-07 14:41:27 -0700 Initial commit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It all started a year before, when I migrated from CobblestoneJS (my SSG from the early 2010s) to &lt;a href="https://www.gatsbyjs.com/"&gt;GatsbyJS&lt;/a&gt;. Which was pretty and gorgeous and so difficult to debug. It was also fragile for me because I don't update my libraries to follow the curve and every time I tried to add a new feature to the site, I spent a few days just trying to figure out the node packages to get everything building again first.&lt;/p&gt;
&lt;p&gt;When I got into Gemini for the first time, I also realized that GatsbyJS wasn't going to work there because that shiny site was not text friendly. It was also tied into my frustrating that going to a page the first time downloaded the content in one manner, but navigation was done via SPA and pulled it from another; and the cache was killing me there.&lt;/p&gt;
&lt;p&gt;(But it looked so pretty.)&lt;/p&gt;
&lt;p&gt;So I decided to look for a new SSG. I didn't want to write my own because&amp;hellip; I really don't like writing utility programs. But I wanted to do something Gatsby didn't do, which is generate a site that didn't require Javascript.&lt;/p&gt;
&lt;p&gt;I figured I was good at C# (this was before Rust, but I'm still best at C#), so I wanted to find a C# static site generator in hopes that the NuGet package management wasn't as excruciating as Node's.&lt;/p&gt;
&lt;p&gt;So I found &lt;a href="https://www.statiq.dev/"&gt;Statiq&lt;/a&gt; which seemed like a perfect match. But going from GatsbyJS to Statiq quickly hit a point where I realized I was, once again, forcing a program to do things it wasn't supposed to be doing. The patterns in Statiq were nice but they were too rigid for the complexity of my way of creating sites:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The fragility of pulling in 80+ Git repositories into a CI pipeline&lt;/li&gt;
&lt;li&gt;The use of multiple dynamic lists on a single page&lt;/li&gt;
&lt;li&gt;The desire to make a Gemini-friendly site&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After that month of thinking, I started to write my own. This came when I was looking at Statiq's document type realizing that my desire to create a new type basically meant I had to rebuild the entire framework to support a slightly different enumeration.&lt;/p&gt;
&lt;h2&gt;Gallium&lt;/h2&gt;
&lt;p&gt;Enter Entity Component Systems (ECS) which I learned about while pondering writing a game using &lt;a href="https://bevyengine.org/"&gt;Bevy&lt;/a&gt;. I had never heard of an ECS before that, but the idea was great. Why not write a static site generator that didn't need a single enum to drive processing, where I could add arbitrary components to pages such as nested lists or metadata, and then easily be able to use them.&lt;/p&gt;
&lt;p&gt;Of course, then I had to look at existing ECS systems in C#, but I eventually decided to be more idiomatic to C# (another frustration of Statiq) and focus on something that let me use LINQ-like method calls (such as &lt;code&gt;.Select()&lt;/code&gt; and &lt;code&gt;.Where()&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;List&amp;lt;Entity&amp;gt; newsList = fileList
    .WhereEntity&amp;lt;UPath&amp;gt;(this.OnlyNewsFiles)
    .Run(this.setInstantFromPath, cancellationToken)
    .SelectEntity&amp;lt;UPath, Instant&amp;gt;(this.pathService.SetPathFromInstant)
    .Select(entity =&amp;gt; entity.Set(IsPost.Instance))
    .Select(entity =&amp;gt; entity.Set(IncludeInFeed.Instance))
    .Where(entity =&amp;gt; entity.Has&amp;lt;IsMarkdown&amp;gt;())
    .OrderBy(entity =&amp;gt; entity.Get&amp;lt;PageModel&amp;gt;().Title)
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'm specifically not saying &lt;a href="/tags/gallium/"&gt;Gallium&lt;/a&gt; is a &amp;ldquo;high performance&amp;rdquo; because it isn't. It is an ECS for relatively small systems that builds on existing libraries for patterns. I don't need high performance for sites with only a few thousand pages, I want something easy to understand.&lt;/p&gt;
&lt;h2&gt;Next Steps&lt;/h2&gt;
&lt;p&gt;There are quite a few steps going forward. I still want to make Nitride easier to use, Gallium to track LINQ patterns, and mfgames.com to present more data. But this is a big step and I'm really happy that I'm proud of (I'm sure there will be tweaks). More importantly, I have something that hasn't given me any major trouble updating since I started three years ago.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Gallium Nitride and Gemini</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2021/07/10/gallium-nitride-and-gemini/" />
    <updated>2021-07-10T05:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2021/07/10/gallium-nitride-and-gemini/</id>
    <category term="programming" scheme="https://d.moonfire.us/categories/" label="Programming" />
    <category term="statiq" scheme="https://d.moonfire.us/tags/" label="Statiq" />
    <category term="gallium" scheme="https://d.moonfire.us/tags/" label="Gallium" />
    <category term="mfgames-nitride" scheme="https://d.moonfire.us/tags/" label="MfGames.Nitride" />
    <category term="gitlab" scheme="https://d.moonfire.us/tags/" label="Gitlab" />
    <category term="gemini" scheme="https://d.moonfire.us/tags/" label="Gemini" />
    <summary type="html">Last November, I switched my static site generator from CobblestoneJS (my homebrew Gulp-based one) to Statiq. There were a number of reasons, all of them still good but mainly to support what I want to do with fedran.com and my other sites. This weekend, I changed it to a new static site generator that I have written and hosted the site also on Gemini.
</summary>
    <content type="html">&lt;p&gt;Last &lt;a href="/blog/2020/11/20/website-update/"&gt;November&lt;/a&gt;, I switched my static site generator from CobblestoneJS (my homebrew Gulp-based one) to &lt;a href="https://statiq.dev/"&gt;Statiq&lt;/a&gt;. There were a number of reasons, all of them still good but mainly to support what I want to do with &lt;a href="https://fedran.com/"&gt;fedran.com&lt;/a&gt; and my other sites.&lt;/p&gt;
&lt;p&gt;This holiday weekend, I ended up ripping out all of the Statiq and wrote another static site generator, this time in C# and using an &lt;a href="https://en.m.wikipedia.org/wiki/Entity_component_system"&gt;Entity-Component-System&lt;/a&gt; (ECS). I also migrated my site over to &lt;a href="gemini://d.moonfire.us/"&gt;Gemini&lt;/a&gt; as part of the effort.&lt;/p&gt;
&lt;h1&gt;Why?&lt;/h1&gt;
&lt;p&gt;I liked many of the ideas that Statiq provided:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Treating the website generation as code.&lt;/li&gt;
&lt;li&gt;Having pipelines with dependencies for generation.&lt;/li&gt;
&lt;li&gt;Immutable objects on the pipeline.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, I found myself struggling with concepts. It just didn't sit well with me and I would spend two weeks trying to implement a feature and getting stuck. When I realized I had spent a month not fixing something that was bothering me because I didn't want to delve into the code, I knew it was time to change.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;That isn't to say Statiq isn't bad. It just isn't for me. That's it.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;About once a year, I get 4-7 days of &amp;ldquo;alone time&amp;rdquo; to do what I want. This year, I decided to work on a new static site generator that did work the way I work today and that I hoped would carry me over for the next five years or so.&lt;/p&gt;
&lt;h1&gt;Entity-Component-Systems (ECS)&lt;/h1&gt;
&lt;p&gt;One thing that Statiq did (but differently) was implemented the system as an ECS. Basically, you have a lightweight object (the &amp;ldquo;entity&amp;rdquo;) and add various components into it. Those components are what provide the features: the text content, flags to say if it is HTML or Markdown, or the path.&lt;/p&gt;
&lt;p&gt;While Statiq had a number of these elements built-into the &lt;code&gt;Document&lt;/code&gt; call (basically their entity), there were a lot of assumptions that didn't always fit. Likewise, &lt;code&gt;Statiq.Web&lt;/code&gt; had some nice opinionated ways of handling it, including a document type (binary, text, etc), but I couldn't find an easy way to extend it.&lt;/p&gt;
&lt;h1&gt;Gallium&lt;/h1&gt;
&lt;p&gt;With my ECS, &lt;code&gt;Entity&lt;/code&gt; is only a collection plus an integer identifier. Components can be added, removed, and replaced easily using generics to determine the type. Methods are chained together but not pseudo-English fluent (which I'm also not fond of). Entities are immutable, so all the operations return a clone of that entity with the new components added, removed, or otherwise changed. (Thanks to functional programming for some of those ideas.)&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;Entity entity = new();
Entity entityWithTwoComponents = entity
  .Set(new Uri(&amp;quot;https://d.moonfire.us&amp;quot;))
  .Set&amp;lt;FileSystemInfo&amp;gt;(new FileInfo(&amp;quot;/bob.txt&amp;quot;));
Entity entityWithReplacedUri = entityWithTwoComponents
  .Set(new Uri(&amp;quot;http://path-to-replace-d.moonfire.us/&amp;quot;));
Entity entityWithOnlyUri = entityWithReplacedUri
  .Remove&amp;lt;FileSystemInfo&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also really like chained operations, so most of the processing looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;IEnumerable&amp;lt;Entity&amp;gt; input;
var onlyHtmlOutputs = input
  .OrderBy(x =&amp;gt; x.Get&amp;lt;FileInfo&amp;gt;().FullPath)
  .ForComponents&amp;lt;Uri&amp;gt;((entity, uri) =&amp;gt; this.Uri(entity, uri))
  .WhereComponents&amp;lt;IsHtml&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The whole idea is &lt;code&gt;ForComponents&amp;lt;T1, T2, T3&amp;gt;&lt;/code&gt; will go through the list and for all entities that have &lt;code&gt;T1&lt;/code&gt;, &lt;code&gt;T2&lt;/code&gt;, and &lt;code&gt;T3&lt;/code&gt;, it will do the callback, otherwise it will passs it on. Likewise &lt;code&gt;WhereComponents&amp;lt;T1&amp;gt;&lt;/code&gt; is basically a &lt;code&gt;Where&lt;/code&gt; that says only the entities with the given components.&lt;/p&gt;
&lt;p&gt;Those ideas really simplified a lot of the difficulties I had with CobblestoneJS. Overall, most of the logic &amp;ldquo;felt&amp;rdquo; right for me, so I'm really happy with the results. Plus, it is based on a far more stable package ecosystem (NuGet) and in a language I enjoy greatly (C#).&lt;/p&gt;
&lt;p&gt;Also, the language uses &lt;a href="https://autofac.org/"&gt;Autofac&lt;/a&gt; as my preferred dependency injection of choice. I really like the library plus &lt;a href="https://nodatime.org/"&gt;NodaTime&lt;/a&gt; when coding, so I went with these. It's a bit opinionated, but&amp;hellip; only a few people ever used Cobblestone, so I'm going to assume very few are going to use this.&lt;/p&gt;
&lt;p&gt;Once I get it cleaned up, I'll probably call the ECS &amp;ldquo;Gallium&amp;rdquo; because my original name was &amp;ldquo;Gallium Nitride&amp;rdquo; (GaN, because it's a cool name and I like what the molecule does). The static site generator would be named Nitride.&lt;/p&gt;
&lt;h1&gt;Nitride&lt;/h1&gt;
&lt;p&gt;Nitride is just a multi-threaded pipeline static generator. It uses pipelines much like Statiq but based on C#'s thread control (&lt;code&gt;ManualReset&lt;/code&gt;, &lt;code&gt;ReaderWriterLockSlim&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The rough code looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;List&amp;lt;Entity&amp;gt; list = entities
    .Run(new IdentifyMarkdown())
    .Run(new ParseYamlHeader&amp;lt;PageModel&amp;gt;())
    .ForComponents&amp;lt;PageModel&amp;gt;(this.SetHandlebarsFromPageModel)
    .Run(this.setInstantFromComponent)
    .Run(this.filterOutFutureInstants)
    .Run(this.createCategoryIndexes)
    .Run(this.createTagIndexes)
    .ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again, DI or direct instantiate of modules, it all works the same and really ties into using Linq and C# generics. All of the pipelines are &lt;code&gt;async&lt;/code&gt; but most of the operations (&lt;code&gt;createTagIndexes&lt;/code&gt;, &lt;code&gt;ParseYamlHeader&lt;/code&gt;) are not. But since the pipelines are, it is easy to make something &lt;code&gt;await&lt;/code&gt; without changing signatures.&lt;/p&gt;
&lt;p&gt;I really like the pipelines. For my site, I have the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PostsPipeline&lt;/code&gt;: Load posts and create blog archives&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PagesPipeline&lt;/code&gt;: Load static pages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ContentPipeline&lt;/code&gt;: Combines &lt;code&gt;PostsPipeline&lt;/code&gt; and &lt;code&gt;PagesPipeline&lt;/code&gt;, creates categories/tag indexes, handle filtering out future pages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BareHtmlPipeline&lt;/code&gt;: Takes &lt;code&gt;ContentPipeline&lt;/code&gt; and converts into bare, unstyled HTML. Also creates the RSS and Atom feeds from the bare HTML.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WebpackPipeline&lt;/code&gt;: Runs Webpack.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StyledHtmlPipeline&lt;/code&gt;: Takes &lt;code&gt;BareHtmlPipeline&lt;/code&gt; and &lt;code&gt;WebpackPipeline&lt;/code&gt; and makes it styled using handlebars.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GeminiPipeline&lt;/code&gt;: Takes &lt;code&gt;ContentPipeline&lt;/code&gt; and turns the Markdown into Gemini, applies some simple styling, and generates Gemtext pages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That's it, but I'm happy with the result because I've taken lessons learned from my previous attempts to created something that will handle Fedran's massive cross-linking and project pages, MfGames's pulling in of separate Git repositories, and also some of the more complex formatting of my new sci-fi fiction website.&lt;/p&gt;
&lt;h1&gt;Gemini&lt;/h1&gt;
&lt;p&gt;I like the idea of &lt;a href="//gemini.circumlunar.space/"&gt;Gemini&lt;/a&gt;. It is a low-overhead protocol that has almost no extra features, no cookies, and basically focused on presenting content. In my case, I really want to see all of my sites on Gemini because I think it has some significant merits, more so as I want to get away from heavily styled content written by people who like tiny fonts or don't have my color contrast issues.&lt;/p&gt;
&lt;p&gt;To do that, I ended up taking inspiration from &lt;a href="https://pypi.org/project/md2gemini/"&gt;md2gemini&lt;/a&gt; and wrote a C# library that converts Markdown into Gemtext (Gemini's markup format inspired by Markdown). The end result is &lt;a href="gemini://d.moonfire.us/"&gt;pretty nice&lt;/a&gt;, I think and I'm really happy with the results.&lt;/p&gt;
&lt;p&gt;Of course, it meant I had to get a virtual machine to host a Gemini server next to a HTTP one, but that was going to happen sooner or later anyways.&lt;/p&gt;
&lt;h1&gt;Next Steps&lt;/h1&gt;
&lt;p&gt;I write a lot libraries that I think are interesting but very few people worry about. They rise up, either I stick with them or I trail off, but they always scratch my itches. On that front, the following things are left to do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Split out Gallium more formally into &lt;code&gt;MfGames.Gallium&lt;/code&gt; or &lt;code&gt;Gallium&lt;/code&gt;, not sure.&lt;/li&gt;
&lt;li&gt;Clean up the API for &lt;code&gt;Nitride&lt;/code&gt; and make sure everything is consistent. Some of the names are a bit&amp;hellip; off, but the functionality is good.&lt;/li&gt;
&lt;li&gt;Break all the modules into separate Gitlab projects and set up CI for releasing.&lt;/li&gt;
&lt;li&gt;Document everything.&lt;/li&gt;
&lt;li&gt;Convert five other websites sites to use it to figure out what is missing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I haven't found the money to get a developer's signing certificate, so I'll probably just put everything up on my public &lt;a href="https://www.myget.org/"&gt;MyGet repository&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Website Update</title>
    <link rel="alternate" href="https://d.moonfire.us/blog/2020/11/20/website-update/" />
    <updated>2020-11-20T06:00:00Z</updated>
    <id>https://d.moonfire.us/blog/2020/11/20/website-update/</id>
    <category term="technology" scheme="https://d.moonfire.us/categories/" label="Technology" />
    <category term="programming" scheme="https://d.moonfire.us/categories/" label="Programming" />
    <category term="statiq" scheme="https://d.moonfire.us/tags/" label="Statiq" />
    <category term="cobblestone" scheme="https://d.moonfire.us/tags/" label="Cobblestone" />
    <category term="gatsby" scheme="https://d.moonfire.us/tags/" label="Gatsby" />
    <category term="gemini" scheme="https://d.moonfire.us/tags/" label="Gemini" />
    <summary type="html">I've switched the generator used to create this website from Cobblestone to Statiq, a much different type of static site generator. With this change came a revamp of the site's appearance, one that is far less graphical than before, but it will allow for some future plans.
</summary>
    <content type="html">&lt;p&gt;I haven't been posting here lately. There are a number of reasons, but I'm going to have to go with the simple one: 2020. Lots of things all piling up, adding to to stress, and making just getting things difficult. Along the way, occasionally things slip to make sure other things continue.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://fedran.com/"&gt;Fedran&lt;/a&gt; was one of those things. I've been working on maintaining my weekly chapters but it seems like I haven't been doing other than heads-down writing.&lt;/p&gt;
&lt;p&gt;One of my favorite methods for managing stress is obsessing about a little change that quickly blossoms into a much larger epic worthy of sailing across the ocean and trying to debate some sirens. Usually this ends up with me redoing one of my websites, rewriting my accounting system, or completely revamping one of my writing tools.&lt;/p&gt;
&lt;p&gt;If you can't get, I have a &amp;ldquo;type&amp;rdquo; when it comes to reducing stress.&lt;/p&gt;
&lt;p&gt;In this case, it was stumbling onto &lt;a href="https://gemini.circumlunar.space/"&gt;Gemini&lt;/a&gt;. This is a stripped-down but still fairly capable protocol based on both Gopher and web browsing. It focused more on information but without stressing about the &amp;ldquo;pretty&amp;rdquo; part of the web. There are no themes, font choices, or a ton of links. Well, there can be links but they are one per line and clearly identified as such.&lt;/p&gt;
&lt;p&gt;Gemini appeals to me. Almost everything I create is already in written form, this seems like an ideal place to hang up a pole and see what happens. Not to mention, it's just fun to find new places to explore.&lt;/p&gt;
&lt;p&gt;This also happened at the time when I had butted up against limitations in &lt;a href="https://www.gatsbyjs.com/"&gt;Gatsby&lt;/a&gt;, which I had just spent three months rewriting Fedran. I had switched to Gatsby because of flaws I encountered in Cobblestone, my own static site generator.&lt;/p&gt;
&lt;p&gt;That isn't to say, Gatsby doesn't make a beautiful sites. The trick they use to load the next page quickly was great. But it also is very heavy on the Javascript side of things which was pretty much the opposite of trying out Gemini which has no Javascript.&lt;/p&gt;
&lt;p&gt;Since I rarely do things halfway, I want to migrate this site over to Gemini. This means either I stick with Cobblestone (which was already failing on me), convert it to Gatsby &lt;em&gt;and&lt;/em&gt; to Gemini, or find something new.&lt;/p&gt;
&lt;p&gt;Enter &lt;a href="https://statiq.dev/"&gt;Statiq&lt;/a&gt;. It's another static site generator that is one of my preferred languages (C#, Typescript, and apparently Rust) but it also allows for pipelines (what I liked about the Gulp-based Cobblestone) that would let me diverge the site between HTML and Gemini on the final steps while still allowing me the massive cross-referencing I use on Fedran.&lt;/p&gt;
&lt;p&gt;I did consider switching Fedran, but after years of spinning wheels on redoing projects, I limit myself to one website rewrite a year. Otherwise, I would just endlessly redo my website and not actually create anything.&lt;/p&gt;
&lt;p&gt;If you are reading this, then I've managed to switch over. Fortunately, &lt;a href="http://d.moonfire.us/"&gt;http://d.moonfire.us/&lt;/a&gt; is one of the more basic websites I have. That means I can build on it as I work toward the final boss, Fedran.&lt;/p&gt;
</content>
  </entry>
</feed>
