- This is thought of as a local-first static blog/site generator.
- It will generate a full static blog/site in HTML.
- It will generate HTML based on Markdown.
- HTML is rendered from the use of Razor views.
- Blog posts (essentially markdown) are not hosted within this generator.
- Blog posts are downloaded at runtime, from a github repo.
- Blog posts use markdown comments as blog post properties (slug, categories, publish date etc.).
- Generated HTML is not published anywhere outside of the generator.
- Generated HTML is saved at the execution path of the executable (on your disk).
I have written some thoughts about how I got here and why I ended up creating a new small static site generator. Thinking about really, it I hardly puts my reflective choice of building this in the category of "not invented here". I had an itch. I scratched it.
{
"markdownContentsUrl": "https://api.github.com/repos/Danielovich/markdownposts/contents/?ref=main"
}
[//]: # "title: When I invented the wheel"
[//]: # "slug: i-circled-around"
[//]: # "pubDate: 14/12/1995 12:01"
[//]: # "lastModified: 14/05/2023 13:07"
[//]: # "excerpt: This is some really interesting stuff."
[//]: # "categories: engineering, wheels"
[//]: # "isPublished: true"
### Let me enlighten you!
This is some content I wrote and it's in a markdown file
And I found it easy enough...
> It will be transformed into HTML by a dependency and will look good if you drizzle a few rules of css on it.
(it could also look like this: https://github.com/Danielovich/markdownposts/blob/main/et-liv-i-programmering.md)
git clone https://github.com/Danielovich/RubinStatic.git
cd RubinStatic
.\build.ps1
cd RubinStatic\src\Rubin.Static.Console\bin\Release\net8.0
.\Rubin.Static.Console.exe generate
cd RubinStatic\src\Rubin.Static.Console\bin\Release\net8.0\Views\Output
.\index.html
The generated HTML can be structured how you wish it to be, really. The HTML is rendered by executing a Razor view, by default each view has a model. Index.cshtml looks like this.
@using System.Web
@using Microsoft.AspNetCore.Html
@using Rubin.Static
@using Rubin.Static.ViewModels
@using Rubin.Static.Extensions
@model IndexPageViewModel
@{
Layout = "_Layout";
ViewData["Title"] = "Frontpage";
}
@foreach (var post in Model.Posts)
{
var htmlContent = new HtmlString(post.HtmlContent.Content);
<div class="content-container">
<div class="main-content">
<h3 class="post-title"><a href="@post.Slug.ToUri()">@post.Title.PostTitle</a></h3>
<p class="published">@post.PublishedDate.ToLongDateString()</p>
@htmlContent
</div>
</div>
}
And the Layout (a shared view), which "wraps" around the given view looks like this.
@using Rubin.Static
@using Rubin.Static.Infrastructure
@using Rubin.Static.Extensions
@using Rubin.Static.Rendering
@{
var categories = await SharedViewViewModel.Instance.GetCategoryPosts();
}
<html>
<head>
<title>Blog name - @ViewData["Title"]</title>
<link rel="stylesheet" href="Assets/styles.css" />
</head>
<body>
<div class="menu">
<span class="frontpage-identifier"><a href="index.html">Frontpage</a> | <a href="all.html">All posts</a></span>
</div>
@RenderBody()
<div class="menu">
<span class="frontpage-identifier"><a href="index.html">Frontpage</a> | <a href="all.html">All posts</a> | </span>
@{
foreach (var item in categories)
{
<span><a href="@item.Key.Slug.ToUri()">@item.Key.Title (@item.Value.Count())</a> | </span>
}
}
</div>
</body>
</html>
There are a few views by default.
- index.cshtml will render a number of posts .
- all.cshtml will render all posts.
- post.cshtml will be rendered as the slug of a post, hence it will be a post.
- category.cshtml will be rendered as a category value/name and render links to posts within each category.
You can adjust this to your liking of course.
- .NET 8
- Markdig
- AutoFixture
- Xunit
- MarkdownFile (Rubin.Markdown) - earliest artifact portraying a blog post
- MarkdownPost (Rubin.Markdown) - loosely typed MarkdownFile
- Post (Rubin.Static) - strongly typed MarkdownPost
- RenderedPage (Rubin.Static) - Strongly typed HTML representation of a Post
It is said that very few developers actually reads code they are depended on, so based on that sentiment I will share some pointers with a brief technical documentation.
The solution is based on several projects. Only the Rubin.Static.Console has multiple dependencies to inter-solution projects. Rubin.Static.Console relies on both Rubin.Static and Rubin.Markdown.
The API of Rubin.Markdown is basically where we communicate with the markdown git repository and parses that markdown:
- downloading markdown files from a repo of your choice, which basically represent blog posts. They utilize markdown comments as properties.
- parsing those markdown files from a string to strongly typed model.
- you MUST change the constants. Specifically the "MarkdownContentsUrl" should be added inside a appsettings.json file from where you use the API (see how the Rubin.Static.Console does it). The Github client for downloading the markdown files will throw an exception if not set.
You can use the API as a "stand-alone" API if you wish to utilize it from somewhere else than this solution. There is an extenstion method you can use for this.
The API of Rubin.Static gives you the possibility to generate HTML files by utilizing the Razor syntax.
- The API has its own Models which makes it independent from models in other projects, e.g: Rubin.Markdown.
- Views are cshtml files which can hold HTML, Razor and .NET code. Views can be located in Views and Views/Shared.
- Rendering a razor view outside a web application is not exactly a non-trival challange, there is a internal dependency on a HostingEnvironment and I haven't found a way to discard the ServiceProvider as being used for service location.
- The AddRazorTemplating and AddStatic are extension methods which registers these dependencies.
- The presumable easist approach to understanding how the actual views are generated is to follow along here.
- Markdig dependency is only used here.
- You can use this API from at least a console application and there is no dependencies to other internal projects.
Rubin.Static.Console is where the pieces are connected and a blog/site is generated. I have tried to make it as slim as possible, and so...
- there is a type implementing the IGeneratePages called PageGenerator.
- you can control where the static HTML pages should be saved, default is Output dir which is located in bin\Release\net8.0\Views\Output at runtime.
- the SharedViewViewModel exists because of its responsibilty to the Shared View (_Layout) files. Layout views cannot have a strongly typed model so to overcome this it utilizes the SharedViewViewModel as a singleton that one can use if one wishes to output content which should be shared across all Views that use a Layout view.
- the GenerateCommand is a Command in the sense of System.CommandLine. I have nothing against this library but the dependency is quite large, so I will keep it until I find a more suitable solution. The command is calling into a IGeneratePages type.
- posts are converted from a MarkdownPost model to a Post model, and I have tried to keep the sharing of the two models as far away as I found possible.
- the program is executed with the command "generate".