miso

Platform

HTML & prerendering

miso's View type doubles as an HTML serialiser via the ToHtml class in Miso.Html.Render. Build a View with the normal DSL and render it to a lazy ByteString on the server — or, as this website does, at build time.

class ToHtml a where
  toHtml :: a -> L.ByteString

pageHtml :: L.ByteString
pageHtml = toHtml $ H.div_ [ HP.id_ "root" ] [ "Hello, world!" ]

Instances are provided for View and [View]. Servant users can serve them directly with servant-miso-html, which provides an HTML content type for View and Component values:

import Servant.Miso.Html (HTML)

type Home    = "home"    :> Get '[HTML] (Component context props model action)
type About   = "about"   :> Get '[HTML] (View context model action)
type Contact = "contact" :> Get '[HTML] [View context model action]

Prerendering

Prerendering is delivering HTML from a web server (or a static host) before the client loads and draws anything. It comes in two flavours: static prerendering assumes no model state needs to be shared between server and client; dynamic prerendering uses hydrateModel to share it.

Static prerendering

miso provides prerender and miso for static prerendering. Any page can be generated from a View with toHtml; on the client, pass the matching component to miso (instead of startApp) so it hydrates the markup rather than redrawing:

main :: IO ()
main = prerender defaultEvents $
  (component () noop $ \_ _ () -> "hello world") { logLevel = DebugPrerender }

With the payload and HTML delivered together, the console shows:

[DEBUG_HYDRATE] Successfully prerendered page

Dynamic prerendering

Dynamic prerendering shares model state so the client hydrates from a meaningful initial state rather than a blank model. The -fssr flag must be enabled when compiling the server. The hydrateModel field is Maybe (IO model): when set, the action runs once at hydration time to produce the initial model; a typical pattern embeds the model as JSON in the response and reads it back through the JS DSL:

myComp :: App Model Action
myComp = (component defaultModel updateModel viewModel)
  { hydrateModel = Just $ do
      val <- jsg "window" ! "__initialModel__"
      fromJSValUnchecked val
  }

-- On the server, populate window.__initialModel__ alongside the rendered HTML:
serverView :: context -> props -> Model -> View context Model Action
serverView _ _ m =
  H.div_ []
    [ H.script_ [] ("window.__initialModel__ = " <> encode m)
    , appView m
    ]

When hydrateModel is Nothing the static model field is used instead — equivalent to static prerendering.

How this site does it

haskell-miso.org has no server. A small prerender executable, compiled with vanilla GHC and -fssr, walks every route, calls toHtml on the root component and writes index.html files into public/. The WASM bundle then hydrates whichever page was loaded with misoWithContext and takes over navigation with Miso.Router.