Embedding a pkgdown package site within a Quarto website

Note

This report was generated using AI under general human direction. At the time of generation, the contents have not been comprehensively reviewed by a human analyst.

Overview

Like the book, an R package’s pkgdown reference site is its own beast with its own build tooling - pkgdown::build_site(), not quarto render. There’s no way to make Quarto natively aware of a package’s documentation, so the approach here is the same one used for the book: treat the package as an independent project living in a subdirectory (pkg/practicepkg/), build its pkgdown site separately, and stitch the rendered docs/ output into the website’s output directory as a post-render step.

At a high level:

flowchart TD
    A["quarto render (site root)"] --> B["Renders *.qmd into _site/"]
    A --> C["post-render.sh runs"]
    C --> D["quarto render book (book/_quarto.yml)"]
    D --> E["book/_book/ output"]
    C --> F["copy book/_book/ -> _site/book/"]
    C --> G["pkgdown::build_site(pkg/practicepkg)"]
    G --> H["pkg/practicepkg/docs/ output"]
    C --> I["copy pkg/practicepkg/docs/ -> _site/pkg/"]
    B --> J["Combined _site/ with site pages + /book/ + /pkg/"]
    F --> J
    I --> J

Step-by-step setup

This section walks through the concrete commands and file edits used to add a pkgdown-built package site to an existing Quarto website that already has the book-embedding pattern in place. If starting from scratch (no book yet), the same ideas apply - just skip anything book-specific.

1. Create the package as a subdirectory project

From the root of the website project, scaffold a new package inside a subdirectory (here, pkg/practicepkg/):

usethis::create_package(
  path = "pkg/practicepkg",
  open = FALSE,
  rstudio = FALSE
)
NoteNested-project warning

Because pkg/practicepkg/ sits inside the website’s own project directory, usethis::create_package() will refuse to proceed without confirmation - it flags nested projects as “rarely a good idea” since tools like here::here() can get confused about which project root is active. This is exactly the setup we want here, though, so the warning can be bypassed with:

withr::with_options(
  list(usethis.allow_nested_project = TRUE),
  usethis::create_package(path = "pkg/practicepkg", open = FALSE, rstudio = FALSE)
)

From there, add at least one function (with roxygen comments) under pkg/practicepkg/R/, then generate documentation:

devtools::document()

2. Set up pkgdown for the package

Still scoped to the package directory, initialize pkgdown’s configuration:

usethis::use_pkgdown()

This writes pkg/practicepkg/_pkgdown.yml and adds docs to the package’s own .gitignore and .Rbuildignore (pkgdown’s default build output is docs/, which conveniently keeps it out of the package’s R CMD build tarball). Edit _pkgdown.yml to fill in the url: field - pkgdown’s sitrep will complain about “URLs not ok” until this points at wherever the site is ultimately served, e.g.:

url: https://your-site.example.com/pkg/

The same URL should also be added to the package’s own DESCRIPTION under a URL: field, since pkgdown cross-checks the two.

3. Exclude the package from the website’s own render

Open the website’s top-level _quarto.yml and add pkg/ to the excluded paths under project.render, alongside the book’s existing exclusion:

project:
  type: website
  render:
    - "*.qmd"
    - "!book/"
    - "!pkg/"

Without this, quarto render would try to treat any .qmd or .Rmd files it finds under pkg/ as ordinary website pages - not a concern for a typical package (which has no .qmd files), but worth excluding defensively in case vignettes or similar get added later.

4. Extend the post-render script

The website’s existing post-render.sh already renders the book and copies its output in; add a second block that builds the pkgdown site and copies its output in too:

#!/bin/sh
set -e

quarto render book

mkdir -p _site/book
rm -rf _site/book/*
cp -R book/_book/. _site/book/

Rscript -e 'pkgdown::build_site("pkg/practicepkg", preview = FALSE)'

mkdir -p _site/pkg
rm -rf _site/pkg/*
cp -R pkg/practicepkg/docs/. _site/pkg/

pkgdown::build_site() is called via Rscript -e, since pkgdown site-building is an R function, not a shell command - there’s no pkgdown CLI to shell out to directly.

6. Exclude build artifacts from version control

Add the pkgdown output directory to .gitignore, alongside the site’s and book’s own build outputs:

/_site/
/book/_book/
/pkg/practicepkg/docs/

7. Render and verify

From the project root:

quarto render

Then confirm the package docs were stitched in correctly:

ls _site/pkg/                          # should contain index.html, reference/, etc.
open _site/pkg/reference/index.html    # spot-check in a browser (macOS)

If _site/pkg/ is missing or stale, check that pkgdown::build_site("pkg/practicepkg") succeeds on its own (run it directly in R to see package-specific errors in isolation - most commonly a missing url: in _pkgdown.yml, or roxygen docs that haven’t been regenerated after editing a function).

Project structure

The repository now contains three independent projects glued together at the file-system level:

askurz/
|-- _quarto.yml         # website project
|-- post-render.sh      # stitches the book and package docs into _site/
|-- index.qmd, about.qmd, styles.css, ...
|-- book/
|   |-- _quarto.yml     # book project (independent)
|   `-- index.qmd, intro.qmd, summary.qmd, references.qmd
`-- pkg/
    `-- practicepkg/
        |-- DESCRIPTION, NAMESPACE
        |-- _pkgdown.yml    # pkgdown config (independent)
        |-- R/hello.R
        `-- docs/           # pkgdown build output (git-ignored)

pkg/practicepkg/ is a completely ordinary R package - it has no idea it’s nested inside a Quarto website, and could be pulled out into its own repository without any changes.

Website configuration

The parent _quarto.yml needs the same three ingredients used for the book, extended to cover the package:

project:
  type: website
  render:
    - "*.qmd"
    - "!book/"
    - "!pkg/"
  post-render:
    - ./post-render.sh

website:
  navbar:
    left:
      - href: book/index.html
        text: Book
      - href: pkg/index.html
        text: Package
  • render: [..., "!pkg/"] keeps the website project from trying to render anything under pkg/ itself.
  • post-render: [./post-render.sh] is unchanged - it’s the same hook already used for the book, just doing more work now.
  • The navbar link points to pkg/index.html, which only exists once the post-render step has copied pkgdown’s build output into _site/pkg/.

Package configuration

pkg/practicepkg/_pkgdown.yml is a normal, standalone pkgdown config:

url: https://your-site.example.com/pkg/
template:
  bootstrap: 5

Nothing here references the parent website. Running pkgdown::build_site("pkg/practicepkg") produces exactly the same docs/ output it would if pkg/practicepkg/ were checked out and built on its own.

The stitching step

The extended post-render.sh does for the package what it already did for the book:

Rscript -e 'pkgdown::build_site("pkg/practicepkg", preview = FALSE)'

mkdir -p _site/pkg
rm -rf _site/pkg/*
cp -R pkg/practicepkg/docs/. _site/pkg/
  1. pkgdown::build_site() builds the package’s reference site independently, producing pkg/practicepkg/docs/. Note this also involves installing the package into a temporary library, since pkgdown needs to actually load it to render function examples and generate the reference index.
  2. The script ensures _site/pkg/ exists and is emptied of stale content.
  3. The pkgdown output - reference pages, articles, search index, and so on - is copied into _site/pkg/, so the final site serves the package docs at /pkg/.

Because the whole script runs under set -e, a failed pkgdown build (e.g., a roxygen syntax error) fails the entire render loudly, rather than quietly leaving _site/pkg/ stale or missing.

Rendering workflow

To produce the combined site - website pages, book, and package docs - from a clean checkout:

quarto render

This triggers, in order:

  1. Quarto renders the website project’s own .qmd files into _site/.
  2. Quarto’s post-render hook runs post-render.sh.
  3. The script renders book/ and copies its output into _site/book/.
  4. The script builds pkg/practicepkg’s pkgdown site and copies its output into _site/pkg/.

A few practical implications, mirroring the book’s:

  • quarto preview limitations: as with the book, Quarto’s live-preview server does not re-run post-render hooks on every save, so package docs may appear stale or missing during quarto preview. Run pkgdown::build_site("pkg/practicepkg") directly in R (or do a full quarto render) when iterating on package documentation.
  • Rebuild cost: pkgdown::build_site() reinstalls the package into a temporary library on every call, which is noticeably slower than a plain Quarto re-render. For a package under active development, it’s often faster to iterate with pkgdown::build_reference() or pkgdown::build_home() (which rebuild only part of the site) and reserve the full build_site() call for the post-render step.

Gotchas and recommendations

  • Theming consistency: pkgdown ships its own Bootstrap 5 theme independent of the website’s cosmo + brand theme. Unlike Quarto’s format.html.theme, pkgdown’s theming knobs live under template: in _pkgdown.yml (e.g., template: bootstrap: 5, template: bslib: ...) and don’t automatically pick up styles.css from the website - if visual consistency matters, either duplicate the relevant CSS or reference a shared stylesheet from both configs.
  • Git-ignoring build artifacts: pkg/practicepkg/docs/ (pkgdown output) and the top-level _site/ are both build outputs and belong in .gitignore, same as book/_book/.
  • CI/deployment: any deployment pipeline must run quarto render from the site root, exactly as with the book - rendering pkg/practicepkg in isolation won’t produce a complete _site/.
  • R dependency at build time: unlike the book (which only needs Quarto), building the package docs requires a working R installation with pkgdown and the package’s own dependencies installed. If deploying via CI, make sure the R setup step happens before quarto render runs, not after.
  • Cross-linking: links from website pages into specific package reference topics (or vice versa) need plain relative URLs (e.g., pkg/reference/hello.html), same caveat as book cross-links - Quarto and pkgdown are independent projects and neither can resolve the other’s cross-reference syntax.
  • Search: pkgdown’s built-in search indexes only the package site; it doesn’t see the website’s or book’s content, so there’s no unified search across all three without extra tooling.