SBSharp Virtual Pages


01/26/2025

Static site generation is overall a simple process which relies on three main pillars:
  • Defining content (literally the text/assets you will render),

  • Defining views (how the raw text is converted to user facing HTML),

  • (optional but very useful) Defining extensions (how some part of the website are rendered in a specific manner - OpenAPI/SwaggerUI, etc...).

However, there is one particular kind of rendered views which can be challenging depending the kind of generator you use: the virtual pages.

Virtual Pages?

Virtual pages are virtual on the source side, i.e. there is no .md or .adoc but you end up with a .html in the output.

The common workaround for these pages are to create an empty source file just referencing a custom view.

While this workaround is great for simple pages like tags related pages for example, there are two challenges which can not work:

  • These pages often need all pages (with actual source file) metadata. For example to create a list of tags page you need to have all tags of all pages, if your generator is not able to collect it upfront and provide it to you it can be hard to do without a preprocessing step,

  • These pages can need to be paginated - list of posts per author or category, for example, in a blog.

SBSharp and Virtual Pages

SBSharp , the static site generator behind this blog, uses virtual pages.

For example all the paginated pages are virtual pages (for example the list of posts I wrote - rmannibucau/1.html ).

What is specific to SBSharp is that any page has the list of all pages (with source) of the site so no need to collect anything while rendering pages with a physical source (.adoc for SBSharp) or reprocess all pages.

Thanks to this model, SBSharp enables to create virtual pages 100% by configuration.

The (pseudo) model passed to a view in SBSharp is the following one:

class Document {
  IElement Title;
  IElement Body;
  IDictionary<string,string> Options;
}
class Page {
    string View; // view name
    Document Page; // asciidoc model
    string Slug;
    Func<string> Body; // body renderer - lazy thanks Func
    IEnumerable Pages; // all pages
}

Here is how it looks like in sbsharp.json to generate the index of post per author:

{
    "sbsharp": {
        "Input": {
            // ...
            "VirtualPages": [
                {
                    "Slug": "blog/author/{Value}/{Page}", (1)
                    "Title": "Posts of {Value}, page {Page}", (2)
                    "View": "post.index", (3)
                    "Paginated": true, (4)
                    "PerValue": true, (5)
                    "CriteriaAttribute": "author", (6)
                    "OrderByAttribute": "published-on", (7)
                    "ReverseOrderBy": true (8)
                },
                // ...
            ]
        }
    }
}
  1. The slug represents the url of the page without its .html extension, for example, the first generated page will be blog/author/rmannibucau/1.html with this configuration,
  2. The title is literally the page title, simulating a h1 in HTML or = xxx in asciidoc,
  3. The view is the name of the view to use for the rendering which is where the logic will sit to handle specifically the model virtually created which is equivalent to an empty .adoc document with a title BUT with only the chunk of selected pages as ̀Pages` property,
  4. paginated enables to know if the list of pages will be paginated, it works with the other PageSize parameter - implicit in this snippet - which enables to control the number of items to keep per page. It will also inject specific attributes to the virtual page like paginationTotalPages and paginationCurrentPage which enable to have the pagination context computed by the generator and paginationAttributeValue which enables to know which attribute was used - if configured,
  5. perValue enables to create a pagination suite per criteria attribute value (per author here),
  6. criteriaAttribute enables to filter and select pages based on a document attribute (here the author name),
  7. orderByAttribute enables to sort the resulting pages based on a document attribute,
  8. and reverseOrderBy enables to reverse the natural order (you want latest posts first here).

With this configuration, it is easy to write a custom view (post.index there) which will create the page.

This page can inherit a global theme/layout:

@{
    Layout = "_Layout";
}

It can extract the virtual page title

<p>@Model.Document.Header.Titlep>

Extract the criteria attribute value (if relevant):

Model.Document.Header.Attributes.TryGetValue("paginationAttributeValue", out string val)

Check if there is any selected page (which means you can show a message "No post found" for example):

@if (Model.Context.Pages.Count == 0)
{
    <div>No post found.div>
}

And, of course, generate the index based on the selected pages metadata:

@foreach (var post in Model.Context.Pages)
{
  <article>
    <h5>
      <a href="/@(post.Slug).html">@post.Document.Header.Titlea>
    h5>
    <div>@post.PublishedOndiv>
  article>
}

Finally, since it has the pagination metadata it can generate the pagination navigation bar as needed:

@{
    int totalPages = int.Parse(
      Model.Document.Header.Attributes
        .TryGetValue("paginationTotalPages", out string tp) ?
        tp : "0");
    int currentPage = int.Parse(
      Model.Document.Header.Attributes
        .TryGetValue("paginationCurrentPage", out string cp) ?
        cp : "0");
}
@if (totalPages > 0)
{
    <Pagination count={@paginationTotalPages} defaultPage={@currentPage} boundaryCount={2} />
}

Conclusion

There are ton of static site generators but SBSharp role is literally just to load properly Asciidoc models and associate them to Razor(Light) views which makes it very powerful and natural for any .NET developer.

Thanks to its generic model, it can rely on virtual pages making it easier to create boilerplate pages without an increased maintenance and multiple steps.

Do not hesitate to have a look to SBSharp on Github and submit a pull request if you need some enhancement.


rmannibucau
Tech Lead/Software Architect, Apache Software committer, Java/Js/.NET guy

LinkedIn GitHub