A shiny app usually lives at a single url, and everything you show is a matter of hiding and showing pieces of one page. brochure takes the other road: an app serves several endpoints, and each of them is its own page, with its own UI and its own server function.
A page
page() binds an href to a UI and a server function. The
server is optional: a page that only shows static content does not need
one.
home <- function() {
page(
href = "/",
ui = tagList(
h1("Home"),
plotOutput("plot")
),
server = function(input, output, session) {
output$plot <- renderPlot(plot(mtcars))
}
)
}
contact <- function() {
page(
href = "/contact",
ui = tagList(
h1("Contact"),
tags$p("here@example.com")
)
)
}An app
brochureApp() replaces shinyApp(), and
takes the pages:
brochureApp(
home(),
contact()
)Navigating between pages is done with plain links —
tags$a(href = "/contact"). There is no router to configure
on the client: a link is a real navigation, and the browser asks the
server for that url.
Which brings the one thing to keep in mind: every page is a
new Shiny session. Nothing you compute on / is
available on /contact. That is the point rather than a
limitation — it forces the data flowing between pages to be explicit —
but it means state has to live somewhere shared: a cookie, a database, a
disk cache.
Something on every page
Anything passed to brochureApp() that is not a
page() or a redirect() is injected as it is
into every page. That is where a stylesheet, a favicon or a
<script> goes:
brochureApp(
tags$head(
tags$link(rel = "stylesheet", href = "/www/custom.css"),
tags$link(rel = "icon", href = "/www/favicon.ico")
),
home(),
contact()
)Tags, tag lists, html dependencies and strings are accepted — a string too, so a stray value ends up rendered on every page. Anything else is an error naming the element it could not use.
wrapped is the other half of “on every page”: a function
applied to the UI of each page, which is where a common layout goes.
Pass it fluidPage and every page becomes one, Bootstrap and
all — the layout a single page app gets from its own
fluidPage() call:
brochureApp(
home(),
contact(),
wrapped = fluidPage
)Any function taking the ui and returning a ui works, so a navbar shared by the whole app is written once:
with_nav <- function(ui) {
fluidPage(
tags$nav(
tags$a(href = "/", "Home"),
tags$a(href = "/contact", "Contact")
),
ui
)
}
brochureApp(home(), contact(), wrapped = with_nav)How a url finds its page
Matching a request to a page is done by the {routr}
package, and an href is written the way routr writes a path:
| href | matches | does not match |
|---|---|---|
/contact |
/contact, /contact/
|
anything else |
/who/:id |
/who/colin |
/who, /who/colin/edit
|
/pair/:a/:b |
/pair/x/y |
/pair/x |
/files/* |
/files/a/b/c |
/files/ |
Three rules cover it:
-
:namecaptures exactly one segment./who/:idwill not match/who, so if you want that page too, declare it separately. -
*captures whatever is left, however many segments that is. - A trailing slash never makes a difference.
Pages are tried in the order you pass them, and the first match wins. When two hrefs can match the same url, the specific one has to come first:
brochureApp(
page(href = "/who/me", ui = tagList(h1("It's you"))),
page(href = "/who/:id", ui = tagList(h1("Someone else")))
)Swap those two lines and /who/:id catches
/who/me first: the page you wrote for it becomes
unreachable, silently.
When no page matches
A url matching none of your pages gets a 404, whose body is
content_404:
brochureApp(
home(),
contact(),
content_404 = tagList(
h1("This page does not exist"),
tags$a(href = "/", "Back home")
)
)It is served as it is written, and unlike a page it is not rewritten
for basepath. Under a prefix, href = "/"
therefore points at the root of the domain rather than at your home
page: write the prefix in yourself, href = "/myapp/".
Reading the parameters
What :name and * captured is read with
get_keys(): with no argument inside the server, and with
the request the ui receives inside the ui.
brochureApp(
page(
href = "/who/:id",
ui = function(request) {
tagList(
h1(sprintf("Hello %s", get_keys(request)$id)),
verbatimTextOutput("from_server")
)
},
server = function(input, output, session) {
output$from_server <- renderText(get_keys()$id)
}
)
)/who/colin and /who/fay are two sessions of
the same page, each with its own id. A * lands
in a key named *1, so /files/* matched against
/files/a/b/c gives get_keys()[["*1"]] equal to
"a/b/c".
Redirections
brochureApp(
home(),
redirect(from = "/index.html", to = "/")
)redirect() answers at the http level, before any Shiny
code runs. From inside a server,
server_redirect("/contact") sends the browser to another
page.
Middleware
Every page, and the app itself, takes req_handlers and
res_handlers: lists of functions run on the request before
the response is built, and on the response before it is sent. App level
handlers run first, then those of the matched page.
A req_handler returning an httpResponse
short-circuits everything else, which is how you serve something that is
not a page at all:
brochureApp(
home(),
page(
href = "/healthcheck",
ui = tagList(),
req_handlers = list(
~ shiny::httpResponse(200, content = "OK")
)
)
)A res_handler is where you set a cookie:
page(
href = "/login",
ui = tagList(h1("Logged in")),
res_handlers = list(
~ set_cookie(.x, "SESSION", "abc123")
)
)Read it back from the server with get_cookies() and
parse_cookie_string().
Behind a reverse proxy
If the app is not served at the root of its domain, tell it where it
is mounted with basepath:
brochureApp(
home(),
basepath = "myapp"
)The prefix is removed from incoming urls, so it keeps matching your
page() hrefs, and prepended to the urls the app emits. On
Posit Connect the mount is picked up on its own and
basepath is not needed.
What’s next
-
vignette("handlers")— running code before and after a page is built, and answering a request without Shiny at all. -
vignette("cookies")— setting and reading cookies, and carrying a session from one page to the next. -
vignette("deployment")— serving the app under a prefix. -
vignette("design")— what changes when an app is several pages, and where state goes once no session is shared. -
vignette("testing")— testing the routing, the pages and the app. -
vignette("golem")— building a brochure app as a golem package.
