Serverless interactive reports

An introduction to Quarto and Shinylive
Rapports interactifs sans serveur : une introduction à Quarto et Shinylive

October 20, 2026

1 Welcome

1.1 Welcome

Interactive reports without a server

A hands-on introduction to Quarto and Shinylive, from reproducible documents to fully interactive reports served from a plain static web page.

1.2 Who is this for?

You already know. R is your daily tool, and R Markdown is second nature.

You will consolidate. Quarto: the modern, RMarkdown-compatible successor, with a lighter learning curve than you might fear.

You will discover. Shiny’s reactive model, then Shinylive: the same Shiny apps running entirely in the browser, with no R server at all.

1.3 Today’s goals

By the end of this workshop you will walk away able to:

Quarto. Map everything you know from R Markdown onto Quarto, the modern successor.

Shiny. Build a small interactive app and grasp its reactive core.

Shinylive. Export that app to a standalone static page, and embed Shiny in a Quarto report hosted on GitHub Pages, with no server.

Choose. Weigh the trade-offs and pick the right engine for each job.

1.4 Housekeeping

Everything today runs locally: a browser and your IDE, with no server to set up or maintain.

Nothing new to install. R plus Quarto’s render, in RStudio or Positron, exactly as you already work.
No remote service until the end. We only deploy a static page in the very last step.
Falling behind? The finished examples live in example-app/. Copy the answer and keep going.
Ask any time. The Shiny section is paced deliberately slower, so questions are welcome throughout.

1.5 A gentle introduction to Positron

Positron is an open source data science IDE from Posit. If you use RStudio, everything today works identically.

  • A fork of VS Code: first-class editor, fast console, R and Python side by side.
  • Quarto built in: render from the toolbar, one-click slide preview.
  • Same shortcuts as RStudio (Ctrl/Cmd+Enter).
  • Download from https://positron.posit.co, open it like RStudio.
  • Pick your R interpreter on first launch (it auto-detects).
  • The labs are IDE-neutral: stay in RStudio or drop in.

Note

Not a sales pitch. A modern, free editor with strong Quarto support.

2 Quarto

Four blocks, one destination: serverless.

Quarto

The modern, RMarkdown-compatible successor: projects and one source, many outputs.

Shiny

Reactives, inputs and outputs, and how Shiny embeds in a Quarto document.

Shinylive

WASM and webR: export an app, or embed a report. Both run with no server.

Wrap-up

Trade-offs, deployment automation, and a take-home project to try on your own.

Quarto · shift in mindset Shiny · build an app Shinylive · ship it static Wrap-up · choose & deploy

2.1 Up next: Quarto.

Up next: Quarto.

Quarto

The modern, RMarkdown-compatible successor: projects and one source, many outputs.

Shiny

Shinylive

Wrap-up

Quarto · now Shiny · next Shinylive · later Wrap-up · soon

2.2 From R Markdown to Quarto

Quarto is a multi-language scientific and technical publishing system built by Posit. If you know R Markdown, you already know 80% of Quarto.

Same ideas. Markdown + code chunks + YAML front matter, rendered to many output formats.

Same engine. R code still runs through knitr; only the chunk-option syntax changes ({r, echo=} → #| echo: lines).

Same workflow. Write a .qmd, click Render / quarto render, get HTML, PDF or slides.

2.3 Similarities with R Markdown

Most of what you already know carries straight over. With a few renames:

  • Chunk syntax. Same three backticks and {r}: ```{r} starts a chunk, unchanged.
  • Chunk options. Now written one per line, #| key: value. Or keep the old {r, echo=TRUE} header form.
  • YAML front matter. Same fields; only output: becomes format:.
  • Inline R in prose. `r 2 + 2` still evaluates at render.
  • One source, many formats. HTML, PDF, docx, revealjs and more.

2.4 A minimal Quarto example

A minimal Quarto document is barely more than YAML + one code chunk.

hello.qmd

---
title: "My first Quarto doc"
format: html
---

```{r}
#| label: fig-mpg
#| fig-cap: "Distribution of mpg"
hist(mtcars$mpg)
```

Render is one click. #| label: names the figure for a cross-reference.

2.5 Differences that matter

The same skeleton, but the moving parts have names and defaults:

Table 1: R Markdown vs Quarto at a glance
Aspect R Markdown (.Rmd) Quarto (.qmd)
Languages R, via knitr Same R, plus Python, Julia, OJS
Output output: in YAML format: in YAML
Chunk options {r, echo=FALSE} header #| echo: false lines (old form still works)
Projects _site.yml for websites _quarto.yml, first-class for any project
Slides xaringan, slidy, ioslides Native format: revealjs

Note

The useful habit: treat Quarto as RMarkdown that grew up. Nothing you know is wasted; most of it just got a new name.

2.6 Quarto projects

A folder with a _quarto.yml becomes a project: one file configuring the whole folder. Four built-in types – website, book, manuscript, or default.

_quarto.yml

project:
  type: default        # default | website | book | manuscript
output-dir: _output
execute:
  freeze: true         # cache results, rerun only when inputs change

format:
  html:
    toc: true
    theme: cosmo

quarto preview rebuilds + live-reloads project as you edit; quarto render builds final output; quarto publish deploys it. See the project reference.

2.7 One brand, everywhere

A _brand.yml at the project root carries colors, fonts and logo in one place; one change re-skins every report, deck and app.

_brand.yml

color:
  palette:
    teal: "#2E7D6B"
    amber: "#E8A33D"
  primary: teal
  secondary: amber
typography:
  base: Inter
  fonts:
    - family: Inter

Apply it via the brand key, or let Quarto/Shiny auto-discover _brand.yml. See the brand reference.

2.8 Render targets

Quarto renders many formats from the same source. The key commands:

Table 2: Same source, many targets
Command Output
Render button The format in YAML format:
quarto render f.qmd --to html Force a specific format
quarto preview Live-reload preview
quarto render --to revealjs A slide deck from a folder

Quick check. Open example-qmd/ and render the hello.qmd example to HTML. Confirm the output file appears next to the source.

2.9 From R Markdown to Quarto in practice

Converting existing analysis is mechanical. Same code, few renamed keys:

analysis.Rmd

---
output: html_document
runtime: shiny   # Rmd: live Shiny in a document
---

```{r, echo = FALSE}
hist(mtcars$mpg)
```

analysis.qmd

---
format: html
server: shiny   # Qmd: live Shiny in a document
---

```{r}
#| echo: false
hist(mtcars$mpg)
```
  • output: becomes nested format:.
  • {r, opt=} headers become #| opt: lines.
  • Live Shiny: Rmd’s runtime: shiny is renamed to server: shiny in Qmd.

2.10 Cross-references and citations

Cite anything by label, not by a shifting number. As the two figures auto-number below.

report.qmd (editor)

---
bibliography: references.bib
---

See @fig-mpg, @fig-rank and @wickham2016.

```{r}
#| label: fig-mpg
#| fig-cap: "Distribution of mpg"
hist(mtcars$mpg)
```

```{r}
#| label: fig-rank
#| fig-cap: "Ranks of mpg values"
plot(rank(mtcars$mpg))
```

2.11 Callouts

Quarto can emphasise a note, a tip, a warning with a callout (fenced div with a callout-* class). Five built-ins: note, tip, important, warning, caution.

::: {.callout-tip title="Tip"}
Your note, tip, warning or caution goes here.
:::

Tip

Your note, tip, warning or caution goes here.

::: {.callout-caution title="Caution"}
Your note, tip, warning or caution goes here.
:::

Caution

Your note, tip, warning or caution goes here.

::: {.callout-warning title="Warning"}
Your note, tip, warning or caution goes here.
:::

Warning

Your note, tip, warning or caution goes here.

::: {.callout-important title="Important"}
Your note, tip, warning or caution goes here.
:::

Important

Your note, tip, warning or caution goes here.

2.12 Why Quarto for our purpose

For this workshop, Quarto matters for three reasons that build toward the final goal:

Structured reports. Text, code and figures interleaved in one reproducible document.

Embedded interactivity. Either live Shiny (server: shiny in Qmd) on a server, or Shinylive blocks (or Observable JS) that run in the browser.

Static output. A rendered Quarto HTML page is plain files, ready for any static host.

2.13 Hands-on bandwidth

Now open labs/lab-01-quarto.qmd and work through it. It covers:

  1. Writing your first .qmd and choosing an output format.
  2. Structure, headings, lists, code chunks and inline code.
  3. A comparison table of .Rmd vs .qmd chunk options.
  4. Adding a caption and cross-referencing a figure.

2.14 Summary so far

  • Quarto = RMarkdown, generalised: Python, Julia and Observable join R.
  • Projects (_quarto.yml) centralise config; one source, many formats.
  • Rendered output is plain files → a perfect fit for static hosting later.

Why Quarto V2

  • Instant start. A single self-contained binary; launches and renders faster.
  • Native PDF, no LaTeX. Embedded Typst engine: polished PDFs from plain text.
  • Live authoring. Hot reload and fast incremental renders while you edit.
  • Collaborative. Live-preview session links for real-time team co-editing.
  • Backwards compatible. Quarto 1 projects and extensions keep working.

3 Shiny

Up next: Shiny.

Quarto

Shiny

The reactive model: inputs, outputs and renderers, and how Shiny embeds in a Quarto document.

Shinylive

Wrap-up

Quarto · done Shiny · now Shinylive · next Wrap-up · soon

3.1 What Shiny is

Shiny is a framework for interactive web applications: you write an app in R, and Shiny serves it as a web page that reacts to the reader’s actions. It is not a purely R idea – later we note the Python flavour – but we go through R throughout today.

UI. The layout: sliders, buttons, tables, plots, text.

Server. The logic: what happens when the user changes an input.

Reactive wiring. What Shiny calls the “reactive graph”: inputs flow to outputs.

3.2 A minimal Shiny app

Every Shiny app has two pieces. The smallest possible app:

app.R

library(shiny)

ui <- fluidPage(
  sliderInput(inputId = "n", label = "Number of points",
              min = 10, max = 200, value = 50),
  textOutput(outputId = "msg")
)

server <- function(input, output, session) {
  output$msg <- renderText({
    paste("You chose", input$n, "points")
  })
}

shinyApp(ui = ui, server = server)

Run it with Run App (or shiny::runApp("app.R")).

3.3 The reactive model, visually

Shiny tracks dependencies so only what changed re-renders.

flowchart LR
  I["input$n (slider)"] --> R["reactive expression<br/>computed once, cached"]
  R --> M["output$msg<br/>renderText"]
  R --> P["output$plot<br/>renderPlot"]

Note

Reactive flow:

  • input change invalidates expressions that read it, which in turn invalidate outputs that depend on them
  • Shiny recomputes only that minimal set.

Reactive expression: compute a value once + cache it \(\rightarrow\) dependent outputs reuse a single computation.

3.4 Inputs you will use today

Shiny ships a rich widget set. The four you will rely on for the labs:

Table 3: The four input widgets you will rely on
Widget Function Returns
Slider sliderInput("id", "N", 0, 100, 30) one number
Select selectInput("id", "Var", choices) one (or more) values
Checkbox checkboxInput("id", "Label") TRUE / FALSE
File upload fileInput("id", "CSV") a file handle, read with read.csv(input$id$datapath)

Need a different widget? See the full list of UI inputs.

3.5 Output functions and renderers

Table 4: Output functions and their paired renderers
Output function Paired renderer Example
textOutput() renderText() paste0(...)
plotOutput() renderPlot({}) a ggplot
tableOutput() renderTable() / renderDT() data frame
uiOutput() renderUI() dynamically built UI

A rule of thumb: every render*() function turns an R value into the HTML its paired output function displays.

More UI outputs and render functions.

3.6 A reactive app with a plot

app.R

library(shiny); library(ggplot2); library(dplyr)
ui <- fluidPage(
  titlePanel("Palmer penguins explorer"),
  fileInput("data", "Upload your data (CSV)", accept = ".csv"),
  sliderInput("bins", "Number of bins", 5, 40, 20),
  plotOutput("hist")
)
server <- function(input, output, session) {
  peng <- reactive({ req(input$data); readr::read_csv(input$data$datapath) })
  output$hist <- renderPlot({
    peng() |> ggplot(aes(body_mass_g)) +
      geom_histogram(bins = input$bins, fill = "#2E7D6B")
  })
}
shinyApp(ui, server)

reactive({}) reads the file once, then re-runs only when the upload changes. Run it at example-app/app.R.

3.7 Reactive details that trip people up

Reactives are lazy and cached: a reactive({}) recomputes only when its inputs change.

  • req() guards: req(input$x) halts until input$x is non-empty, avoiding errors on first render.
  • eventReactive(): for click-driven actions (button → go), when you do not want recomputation on every keystroke.
  • Side-effects live in observers or render*(): a reactive() only computes a value. To do something when an input changes – a showNotification(), refreshed updateSelectInput() choices, a revealed panel – use an observer (observe()) or an output.

3.8 Shiny inside a Quarto document

“Shiny in Quarto” covers two very different setups; keep them apart. The classic, server-backed route first:

server: shiny in the YAML serves a live app from an R process, so it needs a server (Old R Markdown runtime: shiny became server: shiny in Shiny).

---
title: "My live app"
format: html
server: shiny
---

App code goes in regular {r} chunks; the reactive logic sits in an {r} chunk (plain for UI elements, with #| context: server option for server).

The YAML loads the shinylive filter, so no server is involved.

---
title: "My interactive report"
format: html
filters:
  - shinylive
---

App code goes in {shinylive-r} chunks. We write our first one in the next block.

3.9 Hands-on bandwidth

Work through labs/lab-02-shiny.qmd. It builds your first Shiny app step by step:

  1. The two-piece skeleton (UI + server), run it with Run App.
  2. Add an input and a text output and turn it into a histogram.
  3. Add a second input (a selectInput) and a reactive expression.
  4. (Stretch) Update UI elements dynamically.
  5. (Stretch) Handle an uploaded CSV with fileInput and readr::read_csv.

3.10 Summary and breakthrough

  • Shiny = UI + server wired through a reactive graph.
  • Inputs change → reactives recompute → outputs re-render, minimally.
  • req() and reactive() keep apps robust and lean.
  • In Quarto, server: shiny needs a server; our goal is the static route.

Shiny is not just R

A Python flavour of Shiny exists too, with the same ideas in another language.

So the question: what if we shipped the app itself to the reader’s browser instead of hosting it on an R process? That is Shinylive (WebAssembly, webR, and a web asset library), which is next up.

4 Shinylive

Up next: Shinylive.

Quarto

Shiny

Shinylive

WASM and webR: export a standalone app, or embed a report. Both run entirely in the browser, no server.

Wrap-up

Quarto · done Shiny · done Shinylive · now Wrap-up · next

4.1 Why host without a server?

Shiny traditionally needs a server program running around the clock, which takes time, expertise and money to keep up.

Shinylive is the alternative: the whole app becomes plain files that any static host can serve, so the browser does the computing.

  • A server program computes and streams results; any package, secret keys; costs money and maintenance.
  • The host just serves files; the reader’s browser runs the app. Free and fast; but WASM-only, no secret keys.

4.2 The three tools behind Shinylive

Three tools, one idea: run R in the browser with no server.

WebAssembly. A portable, sandboxed binary format. Sandboxed: it runs isolated from your computer and the page. Binary: it is compiled machine code in a .wasm file, not human-readable source. Browsers run it at near-native speed.

webR. The R interpreter compiled to WebAssembly, so R runs inside the browser.

Web assets library. A bundle of static JS/CSS/files a page downloads and runs; Shinylive itself is one.

4.3 What is WebAssembly?

WebAssembly (WASM) is a low-level bytecode: the machine-readable instructions a browser executes, run inside a secure sandbox. Think of it as a “portable CPU” that every browser already includes.

  • Languages like C, C++, Rust, R are compiled to .wasm files.
  • The browser runs them at near-native speed, isolated from the page.
  • It powers heavy in-browser apps: softs and web apps (Google Earth, AutoCAD, TensorFlow), games (Unity 3D engine, Doom, GameBoy), smartphone apps, and now statistical computing.
  • R is normally an installed binary. WASM lets the same R run wherever a browser runs: a phone, a colleague’s computer, GitHub Pages.
  • No server is needed to interpret the code; the browser is the runtime.

4.4 What is webR?

webR is the R interpreter recompiled to WebAssembly, so R runs entirely in the browser. The recipe builds R for the browser in one line:

\[\mathrm{webR} = \mathrm{R} + \mathrm{WASM} + \mathrm{Node.js} + \mathrm{Emscripten}\]

WASM. The new “hardware”: the portable binary every browser executes.

Node.js. The build kitchen: the JS toolchain that assembles R for the web.

Emscripten. The compiler bridge: pure R ports easily, but its C/C++ core needs turning into WASM, which is where Emscripten comes in.

4.5 webR in action

Warning

A package must already be compiled to WASM to run in the browser. Only packages listed in the webR repository (https://repo.r-wasm.org) work out of the box.

Note

Sweet spot. webR means “the same R you use at your desk, inside the reader’s tab”. The data never leaves their computer, and the code they see is the code you wrote.

4.6 What is a web assets library?

A web assets library is a set of static files (JavaScript, CSS, worker scripts, WASM binaries) a page downloads and runs. Shinylive is exactly one: it bundles the machinery to run Shiny in the browser via webR, as files you can drop on any static host.

shinylive::export() turns a local Shiny app into a self-contained static folder:

export.R

shinylive::export("app", "_site")  # app folder -> static site

quarto add quarto-ext/shinylive runs Shiny inside a report via a filter:

_quarto.yml

filters:
  - shinylive

Copy-paste a ready-made app to see one running in your browser:

4.7 What the browser downloads

On first open, the browser pulls down a bundle once:

webR / WASM binaries. The R interpreter run in the browser.

Shiny + Shinylive assets. The framework and glue.

Your app’s code & packages. Your UI/server, plus WASM packages.

Warning

This bundle is the main cost of Shinylive: a “cold start” may take a few seconds on first load; later visits are cached by the browser.

4.8 Route 1: a Shiny app, hosted static

Our first serverless goal: take a standalone Shiny app and host it on a static site. The route, in one picture:

flowchart LR
  A[app.R] --> B[shinylive::export]
  C[R packages → WASM] --> B
  D[Shinylive web assets] --> B
  B --> E[static site folder]
  E --> F[any static host]

4.9 Export, in practice

Take the penguins app from the Shiny block. Exporting it is two lines:

export.R

shinylive::export("example-app", "_site")
httpuv::runStaticServer("_site")
  • shinylive::export(appdir, destdir) walks your app folder;
  • resolves the R packages it needs,
  • downloads the Shinylive web assets; and,
  • writes a self-contained static site into destdir.

The result is plain files: drop them on GitHub Pages, Netlify, or any static host.

4.10 Try it now

In labs/lab-03-shinylive.qmd:

  1. Run shinylive::export("example-app", "_site").
  2. Serve locally with httpuv::runStaticServer("_site") and open the URL. Confirm app runs in your browser, with no R server involved.
  3. Inspect exported _site/ folder: notice the payload files (WASM, JS).
  4. Optional: upload your own CSV and confirm no network request carries it (check the Network tab: the file never leaves).

4.11 Route 2: Shiny inside a Quarto report

Our second serverless goal: turn a Quarto report into an interactive page where the charts themselves are Shiny apps, all within one static document.

flowchart LR
  A[report.qmd] --> B[quarto-ext/shinylive]
  C[shinylive-r blocks] --> B
  B --> E[static report.html]
  E --> F[any static host]

Install the extension, then enable its filter:

quarto add quarto-ext/shinylive
format: html

filters:
  - shinylive

4.12 Anatomy of an interactive block

Wrap a complete Shiny app in a {shinylive-r} code block: the rest of the page is static, the block becomes a live self-contained app.

---
title: "Interactive report"
format: html
filters:
  - shinylive
---
```{shinylive-r}
#| standalone: true
#| components: [editor]
library(shiny)
ui <- fluidPage(
  sliderInput("n", "Samples", 10, 500, 100),
  plotOutput("hist")
)
server <- function(input, output, session) {
  output$hist <- renderPlot(hist(rnorm(input$n), col = "#2E7D6B"))
}
shinyApp(ui, server)
```

4.13 A full interactive report pattern

A realistic report mixes prose, static figures, tables, and one or more {shinylive-r} widgets:

  • Narrative sections with static text and figures.
  • A {shinylive-r} block for each interactive tool.
  • TOC, captions, cross-references as usual.
  • #| standalone: true, required: the block is a complete app.
  • #| components: [editor]: show/hide the code editor and viewer.
  • #| viewerHeight: 420: panel height in px.
  • #| layout: vertical: stack editor above viewer.
  • A live gallery of every option: https://www.royfrancis.com/quarto-exp/documents/shinylive/

App logic in a separate .R file? Paste it into the block with a Quarto shortcode, rather than source()-ing it: copy-paste plays far better with Shinylive’s in-browser file system.

```{shinylive-r}
#| standalone: true
{{< include app.R >}}
```

4.14 Deployment: static hosts

A rendered report is plain files, so any static web host works. Quarto has native commands:

terminal

quarto publish gh-pages      # GitHub Pages
quarto publish quarto-pub    # free Posit cloud service
quarto publish netlify       # custom domains

Warning

Before sharing, treat the service worker as a project resource, or collaborators may see a blank page:

format:
  html:
    resources:
      - shinylive-sw.js

4.15 Try it now

In labs/lab-04-shinylive-quarto.qmd:

  1. Create report.qmd with the extension enabled and one {shinylive-r} block (the histogram above).
  2. quarto render → open the HTML. Confirm the widget runs in the browser.
  3. Add a second interactive block and cross-reference a static figure.
  4. (Stretch) Deploy the folder holding report.html to GitHub Pages or Netlify and share the URL.

4.16 A word on privacy and data

Because everything runs in the reader’s browser, uploaded data stays local, true of a standalone app and of a {shinylive-r} block in a report alike:

  • Files are read into browser memory; they are never transmitted to the static host.
  • Reader A’s uploaded data is invisible to reader B; closing the tab discards it.
  • This makes Shinylive an excellent fit for sensitive or GDPR-relevant data.

Caution

The flip side: your code is public (it is downloaded to run). Never ship API keys, passwords, or proprietary algorithms in a Shinylive app or report.

4.17 Two routes, one goal

Both routes end in plain static files; the reader’s browser does the computing.

Route 1: standalone app

flowchart TB
  A[app.R] --> B[shinylive::export]
  B --> C[static site]
  C --> D[any static host]

Route 2: embedded report

flowchart TB
  E[report.qmd] --> F[shinylive-r blocks]
  F --> G[static report.html]
  G --> H[any static host]

5 Wrap-up

Up next: wrap-up.

Quarto

Shiny

Shinylive

Wrap-up

Trade-offs, deployment automation, and a take-home project to cement the workflow.

Quarto · done Shiny · done Shinylive · done Wrap-up · now

5.1 Choosing an interactivity engine

There is no single right answer. Match the engine to the job:

Table 5: Choosing an interactivity engine
Engine Hosting Best for Drawback
Quarto + Shinylive Static Small data, open work Cold start, package limits
Observable JS (OJS) Static Dashboards, JS plots You write JS
Traditional Shiny Live server Big data, all packages Server cost and ops

Note

Roughly:

  • static (Shinylive/OJS) = free, private by architecture, but browser-bound.
  • Server Shiny = full power, more cost and ops. Most productions mix both.

5.2 Limitations to remember

Be honest about the trade-offs before committing to Shinylive:

  • Load time: first visit downloads webR + packages; expect a multi-second start.
  • Package availability: only WASM-built packages work; no system libs, no raw sockets.
  • Network: no direct databases; HTTP calls are subject to the host’s cross-origin resource-sharing (CORS) rules.
  • Data size: ship data to the browser, so keep payloads small (a few MB max).
  • Secrecy: the code your reader can run is code they can read.

5.3 Automating deployment with GitHub Actions

Manual publishing is fine for a first pass. For reproducibility, automate it.

Two idiomatic options:

A reusable workflow (posit-dev/r-shinylive, deploy-app.yaml) runs on push:

  • installs R + packages (renv), runs shinylive::export(".", "site"),
  • uploads the site/ artifact to GitHub Pages with proper pages / id-token write permissions.
  • quarto publish gh-pages from a workflow;

  • Or:

    • quarto add quarto-ext/shinylive
    • quarto render then a Pages deploy action.

5.4 The workflow in skeleton

name: Deploy app
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - name: Export Shinylive site
        run: Rscript -e 'renv::restore(); shinylive::export(".", "site")'
      - uses: actions/upload-pages-artifact@v5
        with: { path: "site" }
  deploy:
    needs: build
    permissions: { pages: write, id-token: write }
    environment: { name: github-pages, url: ${{ steps.deployment.outputs.page_url }} }
    runs-on: ubuntu-latest
    steps:
      - id: deployment
        uses: actions/deploy-pages@v5
name: Deploy report
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: quarto-dev/quarto-actions/setup@v2
      - name: Render report
        run: quarto render report.qmd
      - uses: actions/upload-pages-artifact@v5
        with: { path: "_output" }
  deploy:
    needs: build
    permissions: { pages: write, id-token: write }
    environment: { name: github-pages }
    runs-on: ubuntu-latest
    steps:
      - uses: actions/deploy-pages@v5

5.5 Ecosystem and further reading

5.6 Take-home project

Tonight: take one of your own analyses, small and non-sensitive:

  1. Render it as a Quarto report with one embedded {shinylive-r} widget.
  2. Host it on GitHub Pages (or Netlify).
  3. Share the URL. Watch relatives click a slider and see a chart update, with no server anywhere.

5.7 The three tools

Quarto, Shiny, Shinylive.

5.8 Three tools, one goal

Three tools, one goal.

Quarto

A publishing system that grew up from R Markdown: the reproducible foundation of the report.

Shiny

A reactive UI + server model that turns analysis into interactive apps.

Shinylive

The same apps and reports running in the browser, no server needed.

5.9 The focus of today

Shinylive: serverless by design.

Shinylive

A web assets library built on webR + WASM. Export a standalone app with shinylive::export(), or embed a {shinylive-r} block in a Quarto report. Both run entirely in the reader’s browser and deploy on any static host, with no R server at all.

5.10 Quarto + Shiny + Shinylive

Quarto + Shiny + Shinylive.