Skip to content

wilsoncg/wilsoncg.github.io

Repository files navigation

Jekyll blog converted to WebAssembly, written in F# & hosted on github pages.

DeployToGitHubPages

Repository for wilsoncg.net.

Uses Bolero - F# Tools for Blazor, see Bolero website and repository.

To build & Run

with .NET Core SDK 3.1.300 or newer

dotnet build -c Debug; dotnet run -p .\src\BoleroGitHub.Server\ -c Debug

BoleroGitHub.Server project uses dotnet Kestrel web server to assist development (template hot reloading). The static site served is contained in BoleroGitHub.Client project.

To run as staging environment:

dotnet publish -c Release;dotnet run -p .\src\BoleroGitHub.Server\ -c Release --launch-profile "KestrelStaging"

You've heard of JAMstack, now it's time to meet BAMstack

JAMstack is a cloud-native web development architecture based on client-side JavaScript code, BAMstack is a step forward using WebAssembly.

What is BAMstack?

“A modern web development architecture based on client-side Bolero, web Assembly, and prebuilt Markup”

Craig Wilson (.Net Developer)

Benefits

  • Cross platform development & delivery with .NET Core & WebAssembly with Bolero
  • Elmish Model-View-Update functional approach to building a reactive UI
  • Access the full suite of community created Blazor resources
  • Utilise MarkDig tool which converts markdown to html

Motivation

There were two motivating factors which lead to the decision to convert this blog from jekyll to .Net & WebAssembly:

  1. Randomizing the header splash image on page reload. After some investigation it's a surpisingly non-trivial task. This involes fiddling with liquid syntax & loops in order to fetch the list of image files in a folder, creating a customized page template and finally combining some handcrafted javascript to instruct the browser to randomly fetch an image file.
  2. Bypassing the jekyll engine for specified sub directories in order to serve up some WebAssembly.

Other benefits

✅ No more Gem file security alerts

Image Link

✅ Thousands of lines of un-necessary YAML/HTML/JavaScript/Ruby removed

Image Link

✅ Forget node.js & node_modules bloat

Say goodbye to maintenance of 300MB node_modules directories & potential javascript security issues.

✅ Much simpler splash header randomization

let splashImages = [ "splash-1.jpg"; "splash-2.png"; "splash-3.jpg" ]
  let r = Random().Next(splashImages.Length)
  Main
    .Home()
    .SplashImage(splashImages.[r])
    .Elt()

✅ Simplified search button toggle

type Toggle = On | Off
type Model = { searchToggle: Toggle }
type Message =
    | SearchToggle of Toggle

let initModel = { searchToggle = Off }
let update message model =
    match message with
    | SearchToggle toggle ->
        { model with searchToggle = if toggle = On then Off else On }, Cmd.none

type Main = Template<"wwwroot/templateMainMinimal.html">
let view model dispatch =
    Main()
    .SearchToggle(fun _ -> dispatch (SearchToggle model.searchToggle))
    .ContentIsVisible(if model.searchToggle = On then "is--hidden" else "")
    .SearchIsVisible(if model.searchToggle = On then "is--visible" else "")
    .Elt()

And some definitions in the html template:

<nav id="site-nav">
    <button class="search__toggle" type="button" onClick="${SearchToggle}">
        <span>Toggle search</span>
    </button>
</nav>

<div class="initial-content ${ContentIsVisible}">
    ...
</div>

<div class="search-content ${SearchIsVisible}">
    ...
</div>

Conversion from Jekyll to Bolero

❕ highlight.js requires javascript Blazor interop

First we have to include highlight.js, along with a function for enabling the highlight:

<head>
 <script src="/js/highlight-10-1-2.pack.js"></script>
 <script type="text/javascript">
      window.syntaxHighlight = () => 
        document.querySelectorAll('pre code').forEach((block) => {
          hljs.highlightBlock(block);
        });
 </script>
</head>

We need a mechanism to trigger the highlighting, as the usual DOM loaded event is not triggered. Fortunately Bolero has an ElmishComponent we can utilize. With the injected IJSRuntime, we can have Blazor asynchronously call the syntaxHighlight() javascript function after it has rendered the component.

type PostBodyComponentModel = { RawHtml: string }
type PostBodyComponent() =
    inherit ElmishComponent<PostBodyComponentModel, PostPageMsg>()    

    [<Inject>]
    member val JSRuntime = Unchecked.defaultof<IJSRuntime> with get, set

    override this.ShouldRender() = true

    override this.View model dispatch =
        RawHtml model.RawHtml

    override this.OnAfterRenderAsync firstRender =
        match firstRender with
        | true -> 
            this.JSRuntime.InvokeVoidAsync("syntaxHighlight").AsTask()
        | false -> ValueTask().AsTask()

We then use the Bolero html helper ecomp, which creates an html fragment from our Blazor component. Then the view funcion places that component into the Body hole defined in the html template.

let showPost post title dispatch =
    let rendered = post    
    let postBody =
        ecomp<PostBodyComponent,_,_> [] { RawHtml = rendered.Body } dispatch
    
    Main
        .Body(postBody)
        .Elt()

❕ Hooking into window resize event requires calling into .NET from JS

First we define some Javascript, notice the callback will be provided to the JS environment. We see that the .NET framework will create the machinery for us.

window.generalFunctions = {
    env: {
      hamburgerVisible: false
    },
    getSize: function(){
      var size = { "height": window.innerHeight, "width" : window.innerWidth };
      return size;
    },
    initResizeCallback: function(onResize) {
      window.addEventListener('resize', (ev) => {         
        this.resizeCallbackJS(onResize);
      });
    },
    resizeCallbackJS: function(callback) {
      var size = this.getSize();
      if(size.width < 450 && !this.env.hamburgerVisible)
      {
        this.env.hamburgerVisible = true;
        callback.invokeMethodAsync('Invoke', size.height, size.width);
      }
      if(size.width > 450 && this.env.hamburgerVisible)
      {
        this.env.hamburgerVisible = false;
        callback.invokeMethodAsync('Invoke', size.height, size.width);
      }
    }
  };

This should be loaded after the blazor WASM framework initialization.

<script src="_framework/blazor.webassembly.js"></script>
<script src="/js/windowResize.js"></script>

We then use DotNetObjectReference.Create() to a DotNet JS interop object which is passed into the Javascript defined above. We can define a helper Callback type which will be decorated with JSInvokable, this allows the blazor framework to correctly identify & call the instance method. We create a subscription message during initialization, where the Javascript is instructed to call the Invoke() method on the .NET object. With this mechanism we have achieved JS interop, where a WindowResize message will be dispatched within Bolero on each window.resize DOM event.

type Size(h:int, w:int) =
    member this.Height with get() = h
    member this.Width with get() = w
    new() = Size(0,0)

type Callback =
    static member OfSize(f) =
        DotNetObjectReference.Create(SizeCallback(f))

and SizeCallback(f: Size -> unit) =
    [<JSInvokable>]
    member this.Invoke(arg1, arg2) =
        f (Size(arg1, arg2))

type Message =
    | Initialize
    | WindowResize of Size

let update (jsRuntime:IJSRuntime) message model =
    let setupJSCallback = 
        Cmd.ofSub (fun dispatch -> 
            // given a size, dispatch a message
            let onResize = dispatch << WindowResize
            jsRuntime.InvokeVoidAsync("generalFunctions.initResizeCallback", Callback.OfSize onResize).AsTask() |> ignore
        )
    
    match message with
    | Initialize -> model, setupJSCallback
    | WindowResize size ->
        // handle window resize message
        model, Cmd.none

❕ Parsing markdown & YAML front matter from files

As WebAssembly runs inside the browser sandbox, if we attempt to load a markdown file using System.File.IO.ReadLAllText() we will receive an error from the mono wasm runtime. To overcome this we first need to hook into msbuild with a custom build target which generates an index of markdown files.

<Target Name="GenerateIndexJsonForPostsFolder">
 <ItemGroup>
  <_MarkdownPosts 
    Include="$(_BlazorCurrentProjectWWWroot)\posts\**\*.md" />
  <_MarkdownPostsRelative 
    Include="@(_MarkdownPosts->'posts/%(Filename)%(Extension)')" />
 </ItemGroup>
 <WriteLinesToFile
    File="$(_BlazorCurrentProjectWWWroot)\posts\index.txt"
    Lines="@(_MarkdownPostsRelative)"
    Overwrite="true"
    Encoding="Unicode"/>
</Target>
<PropertyGroup>
 <_BlazorCopyFilesToOutputDirectoryDependsOn>
 $(_BlazorCopyFilesToOutputDirectoryDependsOn);
 GenerateIndexJsonForPostsFolder
 </_BlazorCopyFilesToOutputDirectoryDependsOn>
</PropertyGroup>

We can then asynchronously fetch the /posts/index.txt file from the server, then for each markdown file listed in index.txt, we can retrieve and parse the markdown. You could argue that this is redundant as Bolero already has an inbuilt html templating mechanism, but I wanted to see if it's possible to keep the existing jekyll markdown files and combine the two rendering systems. The full implementation details can be found in PostPage.fs & Markdown.fs.

Useful links:

  • FBlazorShop - An F# implementation of Steve Sanderson's pizza store blazor app workshop
  • TryFSharpOnWasm - F# compiler running in WebAssembly with Bolero. A useful working reference application showing JS interop & other Bolero/Blazor features.

Releases

No releases published

Packages

No packages published