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.
You can install the development version of freshwater from GitHub with:
# install.packages("pak")
pak::pak("ElianHugh/freshwater")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>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>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 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 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>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>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()
}Prevent hijacking unsafe HTTP methods via the double-submit cookie pattern
function(api) {
api |>
api_csrf(secure = TRUE)
}Footnotes
-
differs to base plumber2 implementation in that we render the entire tag tree, allowing for emitting head tags amongst others. โฉ