Static site + Halogen demo

On Mon, 25 Apr 2022, by @lucasdicioccio, 1041 words, 2 code snippets, 10 links, 2images.

This article is considered archived.

If you’ve followed closely my blog, you know that I’ve recently written my own blogging engine. This article demonstrates how I plan to use some PureScript (and, in particular, Halogen) when I need some client-based interactivity or dynamic-content.

Context

My blog engine is a static-site generator that reads a lot of different files. You may want to catch-up reading the dedicated article with a high-level overview.

state of affairs

A short summary would be that my static-blog generator has layouts in Haskell and in CommonMark. Two steps are required to generate my blog: a compilation of the Haskell code with ghc into a binary that can then compiles CommonMark via a series of read/assemble/production steps.

Pictorially, the following pipeline shows this two-binary process.

blog phases

In addition to HTML files, the binary produces a number of other targets, generally in JSON format but somewhat-arbitrary file generation is also supported. Among these extra files, the blog engine generates a sibling metadata file for each article, a listing of all targets, and a graph of how articles, topics, and images are linked with each other.

For reasons, I want to consume and use of these extra-files on the blog files you are reading (e.g., on the home I want to display a graphical view of how articles relate). To achieve these interactions, I need to use some JavaScript to run in browsers. Alas, JavaScript is not my cup of tea. For simple interactions such as performing a fetch and add an ECharts graph I find JavaScript tolerable. However, as I generate more-and-more complex objects on the Haskell-side, I need to make sense of these objects on the frontend-side.

For translating API-types into in-memory values, I fint that the usual monadic-parsing approaches we find in Haskell-like languages is much more pragmatic than the sloppy “trust this opaque JSON blob has this JS shape” approach pervasive in the JavaScript ecosystem. We even can get generic-deriving in Haskell-like languages. On the frontend-side, the two most popular Haskell-like languages are PureScript and Elm. I’ve used and enjoyed both, however I find Elm more focused on full-page application. Whereas I find that for this blog where each post will have different scripts maps more-naturally to the PureScript approach.

next step: PureScript

I settled on generating some .js input file from spago (the build tool I prefer to run the PureScript compiler).

blog phases

Given that spago has a “watch-mode”, I can still benefit from live-reloading the rendered page, even after changing some PureScript source code. Indeed, spago watches the source PureScript, then bundles a .js file in the blog-source files, which re-loads my browser. Overall, even if this file-watch sequences feels like a brittle Rube-Goldberg machine, it works well enough for my needs.

An alternative option would have been to drive spago or the PureScript compiler as a production rule in my blog-engine. This approach would be a bit cleaner and be more direct than the implementation I’ve picked so far. A key benefit, would be that a closer-embedding would result in faster reload times. However, this #alternative option would give me less-direct access to compilation error messages.

Later I’ll probably support both modes. The implmentation I’ve settled on required no changes to the blog-engine and is likely to work as long as the auto-reload works (i.e., forever as auto-reload was one key reason for writing my own blog-engine).

the demo

The goal of the demo is to somewhat dynamically fetch the list of targets in the blog and render this list in some form of filterable list. Such a demo would prove that I can write some PureScript that is inserted as a small JavaScript in the page, and which interacts with a structure inspecting the data from the blog engine. There is nothing especially surprising or hard in this demo, so the demonstration is more useful as an assessment of whether the friction is low-enough to my taste or not.

For this assessment, I need to write some Halogen code in PureScript, and some Commonmark file for the blog with enough to insert the JavaScript.

Halogen code

You can look directly at the Halogen source code. In this source code you’ll find some incantation to attach the QuerySelector "#halogen-routes".

The code itself is pretty standard Halogen code that uses some libraries for fetching data using an Ajax request and then renders the data to HTML. The filtered-list is not really a component because there is not Output nor Query to extract information out of the component. However, the filtered-list has a State that contains a modifyiable filter string, the whole set of routes is passed as Input and stored in the state but never modifier. The rendering function applies the filtering logic (i.e., the trimmed list is transient). The rendering function is typed by the Action that bound DOM-handlers can generate, which is limited to updating the filter.

I’ve put everything in a single file for the purpose of the demo (see below: I use some trick for you to see the source of the PureScript file).

Blog-source in Commonmark

You can also look at the CommonMark file with sections source code.

The actual CommonMark content has two purposes. On one hand, the file contains the meaty-content, that is, the text you are reading right now. On the other hand we need to prepare the HTML content to have some DOM Node ready to accept the Halogen application. The latter is feasible as follows in CommonMark:

::: {#halogen-routes}
:::

<script type="text/javascript" src="/js/halogen-demo.js" async></script>

The above CommonMark will generate some div with the id tag that the Halogen expects to find (the #halogen-routes from the QuerySelector in previous sectoin). Whereas the script tag is a normal HTML tag that gets integrated as raw HTML.

The CSS is then specialized to the page as a content-section of the .cmark source file, as follows.

=base:main-css.css
@import "/css/main.css";

#halogen-routes ul {
  list-style: none;
  border: 1px solid #eeeeee;
  border-radius: 5px;
  padding: 0.2em;
  max-height: 50vh;
  overflow: scroll;
}

// ... more stuff

And that’s about it. You’ll also notice some =generator:cmd.json sections at the bottom of the file, which contain commands to cat the source code (and hence this is how I make available the sources for the blog-article and for the Halogen script).

live-running Halogen code

Conclusion

I appreciate the feeling I got when editing this demo article.

The combination of strong types that do not get in the way on the blog-engine as well as on the PureScript sides is pleasing and pragmatic. I only need to edit a few files and I can easily adapt the “size” of the PureScript logic from simple decorations to moderately-large web applications with it. I think this combination achieves pretty well my desire of having a “fluid” experience between a static-site and a single-page-app.

The code I wrote for the demo could serve in dev-mode, and could also be the basis for some menu-navigation.