Skip to contents

[!NOTE] This package is under active development and its functionality may change over time.

freshwater provides server-side rendering utilities for plumber2 backends:

  • composable HTML templates (slots, parameters, and fragments)
  • template caching
  • weak ETag caching
  • shiny tag serialisation1
  • CSRF protection

For autoreloading support, consider hotwater.

Installation

You can install the development version of freshwater from GitHub with:

# install.packages("pak")
pak::pak("ElianHugh/freshwater")

Reusable templates

library(freshwater)

details <- template(name, age, {
  div(
    p(sprintf("Name: %s", name)),
    p(sprintf("Age: %s", age))
  )
})

details("Jim", 30)
<div>
    <p>Name: Jim</p>
    <p>Age: 30</p>
</div>

Fragments

card <- template(title, {
  div(
    h2(title),
    fragment(
        div("Card body"),
        name = "body"
    ),
    fragment(
        div("Footer"),
        name = "footer"
    )
  )
})

card("Hello")
<div>
  <h2>Hello</h2>
  <div>Card body</div>
  <div>Footer</div>
</div>
card("Hello", fragment = "body")
<div>Card body</div>

Layouts & content injection

layout <- template({
    div(
    head(title("App")),
    body(...)
  )
})

layout(
  htmltools::div(
    htmltools::h1("Dashboard"),
    htmltools::p("Welcome back")
  )
)
<div>
  <body>
    <div>
      <h1>Dashboard</h1>
      <p>Welcome back</p>
    </div>
  </body>
</div>

Attribute name normalisation

Attribute names with non-leading underscores are rewritten to hyphenated HTML attributes Double underscores (__) act as an escape hatch for literal underscores

template({
  div(
    hx_get = "/items",
    hx_target = "#main",
    data_user_id = 42,
    `data__raw__name` = "keep_underscore",
    "Load"
  )
})()
<div
    hx-get="/items"
    hx-target="#main"
    data-user-id="42"
    data_raw_name="keep_underscore"
>
    Load
</div>

Cached partials

Cached partials are keyed by template, fragment, cache name, and vary.

nav <- template(user, {
  ul(
    cache(
      name = "nav",
      vary = user$id,
      li("Home"),
      li("Profile"),
      if (user$is_admin) li("Admin")
    )
  )
})

nav(list(id = 1, is_admin = TRUE))
<ul><li>Home</li>
<li>Profile</li>
<li>Admin</li></ul>

Nested Caches

dashboard <- template(page, stats, {
  cache(
    "page",
    vary = page$updated_at,
    div(
      h1("Dashboard"),
      cache(
        "stats",
        vary = stats$updated_at,
        p(stats$count)
      )
    )
  )
})

dashboard(
  page  = list(updated_at = 1),
  stats = list(updated_at = 2, count = 42)
)

dashboard(
  page  = list(updated_at = 1),
  stats = list(updated_at = 2, count = 42)
)
<div>
  <h1>Dashboard</h1>
  <p>42</p>
</div>

[cached partial]
<div>
  <h1>Dashboard</h1>
  <p>42</p>
</div>

Conditional GETs

If the client’s If-None-Match header matches the current ETag, freshwater returns 304 Not Modified and skips rendering.

#* @get /dashboard
#* @etag \() as.integer(Sys.Date())
function() {
  page_main()
}

CSRF Protection

Prevent hijacking unsafe HTTP methods via the double-submit cookie pattern

function(api) {
  api |>
    api_csrf(secure = TRUE)
}