R package development with GitHub Pages and pkgdown

R/Medicine 2025

Melissa Van Bussel

Senior Data Scientist, Statistics Canada

Housekeeping 🧹

Where to access today’s materials

How to get the most out of today’s session

  • Please participate! This is a 3-hour workshop, which is a long time to stay focused. The more you participate, the more fun it will be for everyone. ☺️
  • Ask questions❓if you have them
  • This will help me figure readjust pacing and topics in real-time.

By the end of this workshop, you will have…

  • 🌎 Your package available on GitHub for others to install
  • 📄 A polished pkgdown website for your package, deployed via GitHub Pages
  • 🎨 An understanding of how to customize the trickier components of pkgdown websites

The final product we will create

Background knowledge

  • Participants should have prior experience with Git, but don’t need prior experience with pkgdown or GitHub Pages.
  • Participants should also have some basic familiarity with the structure of R packages (e.g., the R/ folder and the man/ folder).

Schedule for today

Part 1: Getting started

  • Setup steps (installations + configurations)
  • Basic package creation
  • Getting things onto GitHub Pages

break

Part 2: Making your site yours

  • Styling
  • Customizing content (navbar, footer, reference page)
  • Adding JavaScript

break

Part 3: Mastering GitHub Pages

  • How GitHub Actions + GitHub Pages work
  • Quarto vignettes
  • Further customization: logo in navbar, sidebar content

break

BONUS topics (if there’s time)

  • Adding a custom domain to your GitHub Pages
  • Adding Quarto pages inside your pkgdown site

the end

Part 1: Getting started

Pre-workshop setup steps

Software installations

Please ensure you have all of the following software installed (and updated to the most recent versions):

Git and GitHub setup

  • We will be using GitHub (and GitHub Pages) for this workshop. Please ensure you have a GitHub account that you’re able to log into and access during the workshop.
  • Before the workshop, please ensure that Git is configured properly on your machine.
  • In RStudio, you can run the git_sitrep() function from the usethis package to verify your configuration.

What you should see from running git_sitrep()

  • You should see your name and email under the “Git global (user)” section. If not:
usethis::use_git_config(
 user.name = "Melissa Van Bussel",
 user.email = "melissalisevb@gmail.com"
)

What you should see from running git_sitrep()

  • You should see Vaccinated: TRUE. If not:
usethis::git_vaccinate()

What you should see from running git_sitrep()

  • You should see the username and email associated with your GitHub account, and you should see the word “discovered” beside the line that says “Personal access token”. If not:
usethis::create_github_token()
gitcreds::gitcreds_set()

Basic package creation

Creating a new project

  • Make sure the devtools package is installed
  • File > New project > New Directory > R package using devtools
  • Name of the package: rmedicine
library(devtools)
library(pkgdown)

The DESCRIPTION file

Run

use_mit_license()

then update the Authors@R field

Authors@R: 
    person("Melissa", "Van Bussel", , "melissalisevb@gmail.com", role = c("aut", "cre"))

and change the version number to something a little less long.

Version: 0.0.9

Creating a README.md file


use_readme_md()

Making our first R function


use_r("print_hello_world")
#' Print hello world
#'
#' This function just prints hello world.
#'
#' @param x This argument does nothing.
#'  If it did do something, it might require more words to explain.
#'
#' @return Prints `"Hello, world!"` to the console
#'
#' @examples
#' print_hello_world(x = 1)
#'
#' @export
print_hello_world <- function(x) {
  print("Hello, world!")
}

Let’s add a second function


use_r("print_hello_rmedicine")
#' Print hello R/Medicine
#'
#' This function just prints hello R/Medicine.
#'
#' @param x This argument does nothing.
#'  If it did do something, it might require more words to explain.
#'
#' @return Prints `"Hello, R/Medicine"` to the console
#'
#' @examples
#' print_hello_rmedicine(x = 1)
#'
#' @export
print_hello_rmedicine <- function(x) {
  print("Hello, R/Medicine!")
}

We should probably add some tests, too


# install.packages("testthat")
library(testthat)
use_test("print_hello_world")
test_that("print_hello_world prints hello world", {
  expect_equal(
    print_hello_world(x = "test"),
    "Hello, world!"
  )
})

Repeat for the print_hello_rmedicine function


# install.packages("testthat")
library(testthat)
use_test("print_hello_rmedicine")
test_that("print_hello_rmedicine prints hello rmedicine", {
  expect_equal(
    print_hello_rmedicine(x = "test"),
    "Hello, R/Medicine!"
  )
})

Getting things up onto GitHub Pages

Always run check() before pushing to GitHub

We should see no errors, warnings, or notes!

check()

Initialize the git repository

This will create the .git folder, and populate the .gitignore file.

use_git()

Creating the repository on GitHub

The usethis package makes this step very simple for us!

use_github()

Publishing the pkgdown site using GitHub Pages

Again, the usethis package makes this very easy to do!

use_pkgdown_github_pages()

Making changes from here

  • Whenever we push to the main branch, our website will be automatically updated for us!
  • Remember: always pull before you push

To preview changes locally:

build_site()

BREAK

Part 2: Making your site yours

Let’s make things a little prettier

Adding the logo to your package

You can run the use_logo function, which will automatically place the file in the right location within your package.

use_logo("your/path/here.png")

Creating favicons

Without loading the pkgdown package, run

pkgdown::build_favicons()

Adding a bootswatch theme

In the _pkgdown.yaml file, add a bootswatch theme:

url: https://melissavanbussel.github.io/rmedicine/
template:
  bootstrap: 5
  bslib:
    bootswatch: zephyr

Tip

There are a couple of bootswatch themes that are so new, they haven’t been implemented in Quarto. Most of them should work, though!

Customizing appearance using bslib variables

  • Consult this page to see a full list of options that you can change
  • To change the “primary colour” of your theme, for example:
url: https://melissavanbussel.github.io/rmedicine/
template:
  bootstrap: 5
  bslib:
    bootswatch: zephyr
    primary: "#c70000"

Changing the font using bslib

  • There are a few ways you can change the fonts, but using Google fonts with bslib is the easiest!
url: https://melissavanbussel.github.io/rmedicine/
template:
  bootstrap: 5
  bslib:
    bootswatch: zephyr
    primary: "#c70000"
    base_font: {google: "Outfit"}
    code_font: {google: "Fira Code"}

Adding a custom.scss file

  • If you want to customize things further, create a file called extra.scss in the pkgdown folder
  • It will automatically be copied to the correct location by GitHub Actions
└── 📁 rmedicine/
    └── 📁 pkgdown/
        └── 📄 extra.scss

Minimal content for extra.scss:

/*-- scss:defaults --*/
/*-- scss:rules --*/

Right clicking and inspecting

Let’s change the colour of the search bar!

  • In Google Chrome: Right click > Inspect
  • The class is called .form-control
.form-control {
  background-color: #f4cccc;
}

Adding a lightswitch

  • We can enable a dark mode by setting light-switch to true in our _pkgdown.yml file:
url: https://melissavanbussel.github.io/rmedicine/
template:
  bootstrap: 5
  bslib:
    bootswatch: zephyr
    primary: "#c70000"
    base_font: {google: "Outfit"}
    code_font: {google: "Fira Code"}
  light-switch: true

…but what if we want different styles for light mode and dark mode?

In our extra.scss file:

[data-bs-theme="light"] .form-control {
  background-color: #f4cccc;
}

[data-bs-theme="dark"] .form-control {
  background-color: #40292C;
}

Syntax highlighting theme

We can set a syntax highlighting theme for light mode and dark mode.

url: https://melissavanbussel.github.io/rmedicine/
template:
  bootstrap: 5
  bslib:
    bootswatch: zephyr
    primary: "#c70000"
    base_font: {google: "Outfit"}
    code_font: {google: "Fira Code"}
  light-switch: true
  theme: atom-one-light
  theme-dark: atom-one-dark

Customizing our content

The default “reference” page

  • The “Reference” page is the page that lists all of the functions (and datasets) that are part of your package
  • By default, it puts the functions in alphabetical order
  • We can change that, though! Take a look at the pkgdown documentation for an example.

Reordering the contents of the reference page

In the _pkgdown.yml file:

  • Create sections using title
  • Create descriptions using desc
  • List the pages using contents
reference:
  - title: Functions that print things
    desc: These functions will all print things to your console.
    contents:
      - print_hello_world
      - print_hello_rmedicine

Shortcut: “Topic matching”

You can use “topic matching” as a shortcut:

reference:
  - title: Functions that print things
    desc: These functions will all print things to your console.
    contents:
      - starts_with("print_")

Getting fancier with the reference page

You can also use emojis, and create subsections!

reference:
  - title: ":pencil: Functions that print things"
    desc: These functions will all print things to your console.
  - subtitle: ":one: First subsection"
    contents:
      - print_hello_world
  - subtitle: ":two: Second subsection"
    contents:
      - print_hello_rmedicine

Customizing navbar contents

  • There are some built-in options for the navbar, such as search, github and lightswitch
navbar:
  structure:
    right: [search, github, lightswitch]

Components

  • You can create custom buttons in the navbar using components
  • Use fontawesome icons to customize the button appearance
navbar:
  structure:
    right: [search, github, my_linkedin, lightswitch]
  components:
    my_linkedin:
      icon: fa-linkedin
      href: https://www.linkedin.com/in/melissavanbussel/
      aria-label: LinkedIn

Adding JavaScript

  • Earlier, we saw the extra.scss file
  • pkgdown also automatically recognizes the pkgdown/extra.js file
└── 📁 rmedicine/
    └── 📁 pkgdown/
        └── 📄 extra.scss
        └── 📄 extra.js

Let’s see it in action

  • We’ll add an additional component to our navbar called #js_alert# – a pop-up message will appear when button is clicked
navbar:
  structure:
    right: [search, github, my_linkedin, lightswitch]
  components:
    my_linkedin:
      icon: fa-linkedin
      href: https://www.linkedin.com/in/melissavanbussel/
      aria-label: LinkedIn
    js_alert:
      icon: fa-code
      href: "#js_alert#"
      aria-label: JavaScript
document.addEventListener("DOMContentLoaded", function() {
  let javascriptAlert = document.querySelectorAll(".nav-link[href*='#js_alert#']");
  javascriptAlert.forEach(function (jsAlert) {
    jsAlert.addEventListener("click", function(event) {
      event.preventDefault();
      alert("JavaScript is working!");
    });
  });
});

Can you add more files?

Yes! You can insert additional HTML etc.

For example, we can add additional javascript using after_body in the _pkgdown.yml file:

template:
  includes:
    after_body: |
      <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
      <script>
        document.addEventListener("DOMContentLoaded", function () {
          document.addEventListener("click", function (e) {
            if (typeof confetti === "function") {
              const x = e.clientX / window.innerWidth;
              const y = e.clientY / window.innerHeight;

              confetti({
                origin: { x: x, y: y }
              });
            }
          });
        });
      </script>

BREAK

Part 3: Mastering GitHub Pages

How GitHub Actions + GitHub Pages work

How does GitHub Pages actually work*?

(* = in our current, very specific scenario)

  • When you push to main, the terminal commands in the .github/workflows/pkgdown.yaml file are run on a virtual machine
  • This file defines the “workflow” (pipeline)

What happens in a “workflow”?

  • The commands tell GitHub to:
    • Install stuff: R, pandoc, our R packages…
    • Build the site (pkgdown::build_site_github_pages())
    • Place the resulting HTML files in the correct place (/docs) 😊

…Why do I need to know this?

  • The pkgdown and usethis packages take care of the hard work for us by writing this file for us
  • If we want to do more complicated things, though, we need to be able to read, understand, and modify this file.

GitHub Actions

  • If you plan on re-running the same terminal commands in many different workflows/pipelines/projects, you probably don’t want to retype them out all the time
  • Example: You’ll probably always want to install R for every R project
  • You can create a GitHub Action and run a series of terminal commands with just 2 lines, instead of many (e.g., 50+)!
  • Other people can use your Actions, too! 🤝

Example: “Install Quarto” action

It’s pretty complicated:

GitHub Actions in the .github/workflows/pkgdown.yaml file

…The Quarto installation is just one small part of this action

      - uses: r-lib/actions/setup-r-dependencies@v2

What’s my point?

  • Point 1: It’s complicated, and it’s great that most of the hard work is done for us.
  • Point 2: It’s ultimately just terminal commands, though. You can probably do 99% of what you want to do with just a few commands.

What are those commands?

Remember: GitHub Pages will host any page that ends up in our /docs folder.

The most important commands for R users are:

Command What it does
mkdir folder_name Make a new folder called folder_name
cd folder_name Change current directory to folder_name (use cd ../ to go up a folder level)
cp old_folder/file_name new_folder/file_name Copy the file_name file from the old_folder to the new_folder
Rscript -e Execute a line of R code
quarto render proj_name --output-dir="docs" Render your Quarto file into the docs folder

Let’s see an example!

We’re going to change the link.svg image

How do we change it?

  • The file being used is located at docs/link.svg
  • The docs folder is in our .gitignore, though, so just changing the file will only work locally
  • If we want it to work on GitHub Pages: In our “workflow” file, we have to make a copy of this file!

Modifying the .github/workflows/pkgdown.yaml file

  • We’ll add our custom image as pkgdown/assets/custom_link.svg (any files in the pkgdown/assets folder will be automatically copied to the docs folder during the workflow).
  • We need to add this after the pkgdown::build_site_github_pages() function is called.
      - name: Override link.svg image with custom_link.svg image
        run: cp docs/custom_link.svg docs/link.svg

Vote: Which topic do you want to cover?

  • A: Quarto vignettes
  • B: Logo image in the navbar
  • C: Adding a dynamic test coverage badge
  • D: Custom domain for your GitHub Pages

Quarto vignettes

Adding a Quarto vignette

use_vignette(name = "tutorial.qmd", title = "Tutorial Title")
  • Build > Install > Clean and install
  • Ensure Quarto is installed (including the quarto package in R)
  • Contents can be pasted from here

Troubleshooting Quarto vignettes

  • If you use any packages that are not in your DESCRIPTION file, you must add it as a dependency in .github/workflows/pkgdown.yaml.
     - uses: r-lib/actions/setup-r-dependencies@v2
        with:
          extra-packages: any::pkgdown, local::., reticulate
          needs: website

Troubleshooting Quarto vignettes

Tip

If there is an error in your Quarto file (i.e., it does not render), the error message will only say system command 'quarto' not found. Quarto is probably installed, and you probably have a syntax error in your Quarto vignette.

Changing your navbar structure

You can type Alt+0160 to add a blank character (&nbsp; will not work).

navbar:
  structure:
    left: [vignettes, rd_files]
    right: [search, github, my_linkedin, js_alert, lightswitch]
  components:
    vignettes:
      text: " Vignettes"
      icon: fa-book
      menu:
        - text: Examples
        - text: "  Tutorial Title"
          href: articles/my_vignette.html
    rd_files:
      text: " Function Documentation"
      icon: fa-file
      menu:
        - text: All functions
          href: reference/index.html
        - text: "  Print things"
        - text: "    Print Hello World"
          href: reference/print_hello_world.html
        - text: "    Print Hello R/Medicine"
          href: reference/print_hello_rmedicine.html

Further customization

Adding a logo in your navbar

In the pkgdown/extra.js file (replace with your package name, URL, and folder depth):

document.addEventListener("DOMContentLoaded", function () {
  const brand = document.querySelector(".navbar-brand");
  if (!brand) return;

  const isFileProtocol = window.location.protocol === "file:";
  const imgSrc = isFileProtocol ? "../".repeat(7) + "logo.png" : "logo.png";

  brand.innerHTML = `
    <img src="${imgSrc}" height="30" style="vertical-align: middle; margin-right: 10px;">
    <span style="vertical-align: middle;">rmedicine</span>
  `;
});

In the pkgdown/extra.scss file (to add a hover effect):

.navbar-brand:hover {
  color: var(--bs-secondary-color);
}

Customizing the sidebar

You can add or remove any of the default components:

home:
  sidebar:
    structure: [links, authors]

Customizing author appearance

You can customize the information for each author, including by adding arbitrary HTML:

authors:
  Melissa Van Bussel:
    href: https://melissavanbussel.com/
    html: "<img src='https://images.squarespace-cdn.com/content/v1/615e5d6733744f26a45a0916/0005b0b1-e327-4965-89c0-3c38a167ac35/Melissa_VanBussel_2025.jpg?format=2500w' width=100 alt=''><br><span style=''>Melissa Van Bussel</span>"

Creating custom sections in your sidebar

home:
  sidebar:
    structure: [badges, links, authors]
    components:
      badges:
        title: Package badges
        text: |
          Placeholder text

“Simple” badges

Examples where shields.io will do the work for you:

        text: |
          ![](https://img.shields.io/github/r-package/v/melissavanbussel/rmedicine?label=rmedicine)<br>
          ![](https://img.shields.io/github/actions/workflow/status/melissavanbussel/rmedicine/pkgdown.yaml)<br>

Badges that require modifying your workflow file

  • You can create dynamic badges that contain anything you can think of
  • If you use shields.io, you can pull any field from a .json or .yaml file
  • We can modify the workflow to run arbitrary code, then save the results into a .json file or .yaml file
  • As long as we output the file to the docs folder, shields.io will be able to access it!

Code coverage badge

Let’s add a test coverage badge:

We need to:

  • Use covr to check our package coverage
  • Output the results to a .json file in the docs folder
  • Create the badge using shields.io

Calculating the code coverage

In the _pkgdown.yml file:

      - name: Add test coverage badge
        run: Rscript -e "jsonlite::write_json(list(coverage = sprintf('%.1f%%', covr::percent_coverage(covr::package_coverage()))), 'docs/coverage.json', auto_unbox = TRUE)"

We now need to add the jsonlite, testthat, and covr packages to our workflow’s package installations.

In the .github/workflows/pkgdown.yaml file:

      - uses: r-lib/actions/setup-r-dependencies@v2
        with:
          extra-packages: any::pkgdown, local::., reticulate, jsonlite, testthat, covr

Creating the badge

Adding the badge to the sidebar

home:
  sidebar:
    structure: [badges, links, authors]
    components:
      badges:
        title: Package badges
        text: |
          ![](https://img.shields.io/github/r-package/v/melissavanbussel/rmedicine?label=rmedicine)<br>
          ![](https://img.shields.io/github/actions/workflow/status/melissavanbussel/rmedicine/pkgdown.yaml)<br>
          ![](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fmelissavanbussel.github.io%2Frmedicine%2Fcoverage.json&query=%24.coverage&label=test%20coverage)<br>

BREAK

BONUS: Extra fancy stuff (if there’s time)

Using a custom domain for GitHub Pages

  • By default, your website will end with .github.io
  • You can purchase a custom domain from any provider (you do not need to purchase hosting services)
  • For this workshop, I purchased ggnot2.site for a year for \(\approx\) $2 CAD from domain.com

Telling your domain to point to your GitHub Pages

From your domain provider’s DNS settings (full details here):

  • Remove any default A records
  • Add the following 4 IP addresses as A records (use @ rather than www)
185.199.108.153
185.199.109.153
185.199.110.153
185.199.111.153

Telling your domain to point to your GitHub Pages

  • Add a CNAME record (use www rather than @) and point it to your GitHub Pages URL
melissavanbussel.github.io/rmedicine

Tip

If your custom domain has a different depth than your GitHub Pages URL, you may need to adjust some file paths.

Telling GitHub Pages that you have the rights to do this 😊

  • From your GitHub project: Settings > Pages > Custom domain and enter the domain without www or https:// in front of it (e.g., ggnot2.site)
  • Check the “Enforce HTTPS” checkbox
  • It will take 15 minutes to 48 hours for it to work

Including Quarto files inside of your pkgdown site

  • The slides for this workshop are actually rendered as part of my package’s workflow!
  • They are included in the slides/ folder of my package.
  • I don’t necessarily recommend this approach, but it does demonstrate how creative you can get with GitHub workflows.

How to include a Quarto file inside of your pkgdown site

  • Create a slides folder, then a slides.qmd file in this folder
  • Add ^slides$ to your .Rbuildignore
  • Add a quarto render command to your .github/workflows/pkgdown.yaml file, and output the files to the docs folder
      - name: Render the quarto slides
        run: quarto render slides/slides.qmd --output-dir="../docs"

Feedback form

Please use this form to provide anonymous feedback about today’s workshop.

I’ll likely be teaching this again in other contexts, so any feedback you have is greatly appreciated and will be used to improve future iterations 😊.

Thank you!

Any questions?


melissavanbussel.com

@melissavanbussel

@melissavanbussel

@ggnot2

Image credits