Working with files

If your static site is generated depending on local files on disk, the general flow of things is as follows:

runEma render $ \model -> do
  -- Load everything on launch
  initialModel <- loadFilesAndBuildModel
  LVar.set model initialModel
  -- Continue to monitor and update the model
  observeFileSystem $ \action -> 
    LVar.modify model $ applyAction action

For monitoring local files on disk you would typically use something like fsnotify in place of observeFileSystem. What is the point of doing this? To support hot reload on data change. Imagine that your static site is generated based on Markdown files as well as HTML templates on disk. If either the Markdown file, or a HTML template file is modified, we want the web browser to hot reload the updated HTML instantly. This is enabled by storing both these kinds of files in the application model and using LVar to update it over time.

For filesystem changes, Ema provides a helper based on fsnotify in the Ema.Helper.FileSystem module. You can use it as follows

import qualified Ema.Helper.FileSystem as FileSystem

type Model = Map FilePath Text

Ema.runEma render $ \model -> do
  LVar.set model =<< do
    mdFiles <- FileSystem.filesMatching "." ["**/*.md"]
    forM mdFiles readFileText
      <&> Map.fromList 
  FileSystem.onChange "." $ \fp -> \case
    FileSystem.Update ->
      when (takeExtension fp == ".md") $ do
        log $ "Update: " <> fp 
        s <- readFileText fp
        LVar.modify model $ Map.insert fp s
    FileSystem.Delete ->
      whenJust (takeExtension fp == ".md") $ do
        log $ "Delete: " <> fp
        LVar.modify model $ Map.delete fp

In most cases, however, you probably want to use the higher level function mountOnLVar. It โ€œmountsโ€ the files you specify onto the model LVar such that any changes to them are automatically reflected in your model value.

Ema.runEma render $ \model -> do
  FileSystem.mountOnLVar "." ["**/*.md"] [] model def $ \() fp -> \case
    FileSystem.Update () -> do
      s <- readFileText fp
      pure $ Map.insert fp s
    FileSystem.Delete ->
      pure $ Map.delete fp

Full example here.

This functionality is mostly provided by the unionmount library.

Links to this page
  • Using Markdown

    Note that with Ema you can get hot reload support for your Markdown files using filesystem notifications.

  • Rendering HTML

    Of course when using Ema nothing prevents you from choosing to use traditional HTML templates, and you can get hot reload on them too with a little bit of plumbing.

  • Hot Reload

    Finally, hot reload on code changes are supported via ghcid. The template repo's `bin/run` script uses ghcid underneath. Any HTML DSL (like blaze-html โ€“ as used by the Blaze helper) or CSS DSL automatically gets supported for hot-reload. If you choose to use a file-based HTML template language, you can enable hot-reload on template change using the FileSystem helper.

    For anything outside of the Haskell code, your code becomes responsible for monitoring and updating the model LVar. The filesystem helper already provides utilities to facilitate this for monitoring changes to files and directories.

  • Helpers
    Working with files โ€“ Ema provides a helper to support hot-reload on files
  • Defining your model

    In this contrived example (full code here), we are using UTCTime as the model. We set the initial value using LVar.set, and then continually update the current time every second. Every time the model gets updated, the web browser will hot reload to display the up to date value. For the BlogPosts model, you would typically use fsnotify to monitor changes to the underlying Markdown files, but note that Ema provides a helper for that.