Embedding a pkgdown package site within a Quarto website
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
)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 renderThen 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: Packagerender: [..., "!pkg/"]keeps the website project from trying to render anything underpkg/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: 5Nothing 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/pkgdown::build_site()builds the package’s reference site independently, producingpkg/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.- The script ensures
_site/pkg/exists and is emptied of stale content. - 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 renderThis triggers, in order:
- Quarto renders the website project’s own
.qmdfiles into_site/. - Quarto’s
post-renderhook runspost-render.sh. - The script renders
book/and copies its output into_site/book/. - The script builds
pkg/practicepkg’s pkgdown site and copies its output into_site/pkg/.
A few practical implications, mirroring the book’s:
quarto previewlimitations: as with the book, Quarto’s live-preview server does not re-runpost-renderhooks on every save, so package docs may appear stale or missing duringquarto preview. Runpkgdown::build_site("pkg/practicepkg")directly in R (or do a fullquarto 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 withpkgdown::build_reference()orpkgdown::build_home()(which rebuild only part of the site) and reserve the fullbuild_site()call for the post-render step.
Adding a link back to the website from the package site
pkgdown supports a navbar component list, similar in spirit to the book’s sidebar tools. Adding a component that links back to the site root gives every package page a way back to the main site:
navbar:
components:
home:
icon: fa-home
href: ../index.html
aria-label: Back to askurzThe href is ../index.html rather than index.html for the same reason as the book: post-render.sh copies the flat contents of pkg/practicepkg/docs/ into _site/pkg/, so every package page (_site/pkg/index.html, _site/pkg/reference/hello.html, etc.) sits at or below the site root, and the relative path needs to account for that.
As with the book, this relative link only resolves correctly once the package docs have been stitched into _site/pkg/. Previewing the package on its own with pkgdown::build_site() run from pkg/practicepkg/ will treat ../index.html as one level above the package directory - not the real site - so double check the link after a full quarto render from the site root, not in isolation.
Gotchas and recommendations
- Theming consistency: pkgdown ships its own Bootstrap 5 theme independent of the website’s
cosmo+brandtheme. Unlike Quarto’sformat.html.theme, pkgdown’s theming knobs live undertemplate:in_pkgdown.yml(e.g.,template: bootstrap: 5,template: bslib: ...) and don’t automatically pick upstyles.cssfrom 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 asbook/_book/. - CI/deployment: any deployment pipeline must run
quarto renderfrom the site root, exactly as with the book - renderingpkg/practicepkgin 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
pkgdownand the package’s own dependencies installed. If deploying via CI, make sure the R setup step happens beforequarto renderruns, 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.