Documentation with Rd, roxygen2 and Vignettes

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. A package is only as usable as its documentation: every exported function, dataset and class needs a help page, R CMD check reports the ones that do not have one, and a long-form guide (a vignette) is what turns a set of functions into something a newcomer can start from. R’s own documentation format is Rd, a LaTeX-like markup language that R converts to plain text, HTML, LaTeX and runnable example code, and that nobody writes by hand any more if they can avoid it: roxygen2 generates the Rd files from comments written next to the code. This page covers the whole chain. It starts with Rd itself and the base-R tools that parse, convert and check it, goes on to roxygen2 (its tags, its markdown, the way it documents data and S3, S4, R6 and S7 classes, how it reuses text), and then to vignettes (knitr and R Markdown, and Quarto), pkgdown websites, the inst/CITATION file and NEWS.md. The tools that ran for real are base R with its tools and utils packages, roxygen2 8.1.1, devtools 2.5.2, usethis 3.2.2, knitr 1.52, rmarkdown 2.32 (with the Pandoc that rmarkdown finds), pkgdown 2.2.1 and withr 3.0.3; S7 0.2.2 and R6 2.6.1 are only documented, not used; the Quarto command-line program 1.10.19 rendered one file; and the default linters of lintr 3.4.0 were used only to check the scripts. The quarto R package (the vignette engine for Quarto) is not installed in the image, so what it adds is described and not run, and the same holds for the doclisting package, the GitHub Actions workflow of a pkgdown site and every command that needs a GitHub account. The image also has no TeX installation, so the conversion to PDF is shown failing and the conversions that work without TeX are shown instead.

Every example is a script that was run with Rscript on R 4.6.1 with warnings printed as they happen (options(warn = 1)), and the lines that start with #> are what it printed. The scripts also pass the default lintr linters. The names books, loans and the book() constructor belong to the running library-shelf scenario described on the section index. The shelf package shown here was created, documented, tested, built and checked for real with these tools, and its files are shown as the tools wrote them: the man/*.Rd files, the NAMESPACE and the DESCRIPTION fields were written by roxygen2 and usethis, and vignettes/shelf.Rmd, inst/CITATION and NEWS.md are the files of the package. Scripts that change the package (devtools::document(), R CMD build, R CMD check) worked on a copy of it. Paths are shortened to ~/shelf (the directory of the package) and ~ (the directory that holds it), the platform lines at the top of an R CMD check log are left out, and so are timings and temporary directories. Quotation marks in messages are the typographic quotes that R prints in a UTF-8 locale. Where a script writes an Rd file or a throwaway package, it writes it to a temporary directory, builds a small package there and prints the generated file: the file text on the page is what roxygen2 wrote.

This page continues the package pages. Creating Packages builds the shelf package and explains DESCRIPTION, NAMESPACE and the R CMD commands, Testing with testthat tests it and Publishing to CRAN submits it. How to read the documentation that this page teaches you to write is in Getting Help and Documentation.

The documentation a package carries

A package has several kinds of documentation, written in different formats, found in different places of the source tree and read with different functions. The table lists them; the sections of this page take them in this order.

Where in the source What it is Written with Read with

man/*.Rd

one help page (topic) per Rd file

roxygen2 comments, or Rd by hand

help(), ?, example(), help(package = "shelf")

DESCRIPTION

Title, Description, Authors@R, URL, BugReports

by hand

packageDescription(), the package page help(package = )

vignettes/ (rendered into inst/doc by R CMD build)

long-form guides

R Markdown with knitr, Quarto, Sweave

vignette(), browseVignettes()

inst/CITATION

how to cite the package

bibentry() calls

citation()

NEWS.md (or inst/NEWS.Rd)

what changed in each version

Markdown by hand

news()

man/figures/, man/macros/

images and user-defined macros used by the Rd files

by hand

the help pages that use them

_pkgdown.yml, README.md

the configuration and the home page of a pkgdown website (not part of the installed package)

by hand

a web browser

The shelf package has most of them. The script lists the files of the source tree that carry documentation. NAMESPACE is in the list because roxygen2 writes it from the same comments as the Rd files (the @export and @importFrom tags), and R/data.R because the datasets are documented in a file of their own.

# The files of the package that carry documentation, relative to its root.
files <- list.files(recursive = TRUE, all.files = TRUE)
pattern <- paste0(
  "^(DESCRIPTION|NAMESPACE|NEWS[.]md|[.]Rbuildignore|",
  "man/|vignettes/|inst/|R/data[.]R|data-raw/)"
)
grep(pattern, files, value = TRUE)
#>  [1] ".Rbuildignore"          "data-raw/books.csv"     "data-raw/books.R"
#>  [4] "data-raw/loans.csv"     "DESCRIPTION"            "inst/CITATION"
#>  [7] "man/book-methods.Rd"    "man/book.Rd"            "man/books.Rd"
#> [10] "man/clean_books.Rd"     "man/loan_summary.Rd"    "man/loans_per_month.Rd"
#> [13] "man/loans.Rd"           "man/normalise_isbn.Rd"  "man/overdue_loans.Rd"
#> [16] "NAMESPACE"              "NEWS.md"                "R/data.R"
#> [19] "vignettes/.gitignore"   "vignettes/shelf.Rmd"

R CMD check looks at all of it. These are the steps of the real check of the shelf tarball that concern documentation, from the log of a check with --as-cran and the remote part of the incoming checks switched off (the check itself is the subject of Publishing to CRAN); each one is explained where its subject comes up below.

* checking Rd files ... OK
* checking Rd metadata ... OK
* checking Rd line widths ... OK
* checking Rd cross-references ... OK
* checking for missing documentation entries ... OK
* checking for code/documentation mismatches ... OK
* checking Rd \usage sections ... OK
* checking Rd contents ... OK
* checking for unstated dependencies in examples ... OK
* checking contents of ‘data’ directory ... OK
* checking data for non-ASCII characters ... OK
* checking LazyData ... OK
* checking data for ASCII and uncompressed saves ... OK
* checking R/sysdata.rda ... OK
* checking installed files from ‘inst/doc’ ... OK
* checking files in ‘vignettes’ ... OK
* checking examples ... OK
* checking for unstated dependencies in vignettes ... OK
* checking package vignettes ... OK
* checking re-building of vignette outputs ... OK

Rd: the documentation format of R

R objects are documented in files written in the R documentation format, Rd. Writing R Extensions describes it as a simple markup language, much of which closely resembles (La)TeX, that can be processed into a variety of formats including LaTeX, HTML and plain text. The R distribution contains more than 1400 such files, one or more per function of the base and recommended packages, and every help page you have read in R was produced from one. When a package is installed, its Rd files are parsed and stored in the library in a binary form (the help directory of the installed package), and help() renders them on demand: as text in a terminal, as HTML in the browser. That design is the reason that the same source gives a page in the console, a page on the CRAN site, a chapter of the PDF reference manual and the examples that example() runs.

The anatomy of a file

An Rd file has three parts. The header is mandatory and gives the name of the file, the topics documented, a title, a short description and the usage of the objects documented. The body (arguments, value, details, examples and so on) gives the rest, and an optional footer carries keywords. The sections are macros that start with a backslash and take their arguments in braces, and they can appear in any order. Unless a section is allowed to repeat, only the first occurrence of a standard section is kept, with a warning. This is the file that roxygen2 wrote for normalise_isbn() and is_valid_isbn13(), which share one page:

% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/isbn.R
\name{normalise_isbn}
\alias{normalise_isbn}
\alias{is_valid_isbn13}
\title{Normalise and validate ISBN-13 strings}
\usage{
normalise_isbn(x)

is_valid_isbn13(x)
}
\arguments{
\item{x}{A character vector of ISBNs; \code{NA} and empty strings are allowed.}
}
\value{
\code{normalise_isbn()} returns a character vector of the same length;
\code{is_valid_isbn13()} a logical vector of the same length, \code{FALSE} for
\code{NA} and for the empty string.
}
\description{
\code{normalise_isbn()} drops everything except digits and \code{X} and upper-cases
the result, so \code{"978-0-00-100010-0"} and \code{"9780001000100"} compare equal.
\code{is_valid_isbn13()} checks that a (normalised) string has 13 digits and a
correct check digit: the digits are multiplied alternately by 1 and 3 and
the sum must be divisible by 10.
}
\examples{
normalise_isbn("978-0-00-100010-0")
is_valid_isbn13(c("978-0-00-100010-0", "9780001000101", "", NA))
}

The first two lines are Rd comments (a percent sign starts a comment that runs to the end of the line) and tell a human not to edit the file. Then come the sections, in the order roxygen2 writes them. The most used ones, with the kind of text each takes, are in the table. Writing R Extensions distinguishes four kinds of input: LaTeX-like text with markup macros, verbatim text where nothing but the comment marker is interpreted, R-like text for code with some embedded macros, and a rare verbatim variant, used in the first argument of \eqn, \deqn, \figure and \newcommand, in which the comment marker need not be escaped.

Section What it holds Input

\name{topic}

the unique name of the Rd object, normally the base name of the file; one per file; no markup, only printable ASCII; it may not contain !, | or @

verbatim

\alias{topic}

a topic that help() can find; one per topic documented; an alias need not be an object’s name (format.book is an alias of the page book-methods)

verbatim

\title{...}

the title; capitalised, no final period, at most about 65 characters; exactly one

LaTeX-like

\description{...}

a short paragraph on what the objects do; mandatory except in package overview pages

LaTeX-like

\usage{...}

the synopsis, which has to match the code exactly so that the checks can compare them; S3 methods are written \method{generic}{class}(args), S4 methods \S4method{generic}{class}(args)

R-like

\arguments{...}

one \item{name}{description} per argument, or several names separated by commas

LaTeX-like

\value{...}

what the function returns; a list can be described with \item{name}{description} entries

LaTeX-like

\details{...}

the detailed description

LaTeX-like

\note{...}, \references{...}, \author{...}, \seealso{...}

notes, literature (with \doi{}, \url{}), the authors, pointers to related topics

LaTeX-like

\examples{...}

code that is shown and run by example() and R CMD check

R-like

\keyword{key}, \concept{term}

index entries; the standard keywords are listed in the KEYWORDS file of R; the keyword internal hides a page from the topic indices

verbatim

\format{...}, \source{...}, \docType{...}

for data sets: the structure, the origin, and the type of the object (data, package, class, methods)

LaTeX-like

\section{Title}{...}, \subsection{Title}{...}

a section of your own, and a subsection inside a section (more than three levels do not format well); extra sections are always printed before \note, \seealso and the examples, whatever their position in the source

LaTeX-like

\encoding{UTF-8}

the encoding, on a line by itself, when the file has non-ASCII characters

verbatim

An Rd file therefore has the same skeleton as every help page you have read in R: the title, Description, Usage, Arguments, Details, Value, Note, References, See Also and Examples. Writing R Extensions also states a rule for the title: capitalised, without a final period, no more than about 65 characters. R 4.4.0 made tools::checkRd() report titles and section names that end in a period; R CMD check ignores that finding unless the environment variable _R_CHECK_RD_CHECKRD_MINLEVEL_ is set to -5 or smaller (the NEWS entry is in the version notes below).

Looking at the parse tree

Rd is parsed by tools::parse_Rd(), which returns a list of class Rd in which every element carries its macro in the attribute Rd_tag. That is the structure that every converter and every check below walks. The script parses man/book.Rd (written by roxygen2 from R/book.R) and looks at the top level: the two comment lines, then \name, three \alias entries and the sections.

# Parse man/book.Rd (written by roxygen2) and look at the parse tree.
rd <- tools::parse_Rd("man/book.Rd")
class(rd)
#> [1] "Rd"
tags <- vapply(rd, \(x) attr(x, "Rd_tag"), character(1))
tags[tags != "TEXT"]
#>  [1] "COMMENT"       "COMMENT"       "\\name"        "\\alias"
#>  [5] "\\alias"       "\\alias"       "\\title"       "\\usage"
#>  [9] "\\arguments"   "\\value"       "\\description" "\\examples"
#> [13] "\\seealso"     "\\concept"
length(rd)
#> [1] 28

# The text of an element: its leaves pasted together.
rd_text <- \(x) paste(vapply(x, as.character, character(1)), collapse = "")

# The \alias entries are what help() searches.
vapply(rd[tags == "\\alias"], rd_text, character(1))
#> [1] "book"          "new_book"      "validate_book"

# The title and the concept index entry as plain text.
rd_text(rd[[which(tags == "\\title")]])
#> [1] "The book class"
rd_text(rd[[which(tags == "\\concept")]])
#> [1] "book class"

# The parts of the usage section, as the parser sees them.
usage <- rd[[which(tags == "\\usage")]]
attr(usage, "Rd_tag")
#> [1] "\\usage"
attr(usage[[2L]], "Rd_tag")
#> [1] "RCODE"

The three aliases of the page book are book, new_book and validate_book: help("new_book") finds this page because of that alias, not because of the name of the file. The same holds for format.book and the page book-methods, which the section on help lookup below shows. The technical description of the parser is the document "Parsing Rd files" on the R developer site (Parsing Rd files).

Rd markup in one file

The macros for marking text, building lists and tables, linking, writing mathematics and running code are the part of Rd that people look up. This is a hand-written Rd file that uses most of them, rendered as text with tools::Rd2txt(). The script writes the Rd text to a temporary file with a raw string, so that the page shows the Rd as it is typed, and then converts it, running the three stages of evaluation of \Sexpr (explained below). The option underline_titles = FALSE only keeps the backspace-based underlining of the section titles, which a terminal shows and a page cannot, out of the output.

# A hand-written Rd file that uses most of the markup macros, rendered as text.
rd_file <- file.path(tempdir(), "loan_rules.Rd")
writeLines(r"---(\newcommand{\isbn}{\code{ISBN-13}}
\name{loan_rules}
\alias{loan_rules}
\title{Loan Rules of the Shelf Library}
\description{
  The loan period is \code{getOption("shelf.loan_days")} days; set it
  with \code{\link{options}}, or with the environment variable
  \env{SHELF_LOAN_DAYS} in \file{.Renviron}. See \pkg{shelf},
  \emph{emphasis}, \strong{strong text} and \sQuote{single} or
  \dQuote{double} quotes; \verb{x[[1]]} is shown verbatim.
  A book is identified by its \isbn{} (see \doi{10.32614/R.manuals}).
}
\details{
  \itemize{
    \item A loan is overdue after \eqn{d > 21} days.
    \item Members may hold \eqn{n \le 5}{n <= 5} loans.
  }
  \enumerate{
    \item Take the book out.
    \item Return it.
  }
  \describe{
    \item{late}{returned after the loan period.}
    \item{out}{not returned yet.}
  }
  \tabular{lrr}{
    genre \tab loans \tab overdue \cr
    Fiction \tab 12 \tab 6 \cr
    Poetry \tab 4 \tab 1
  }
  The rate is \deqn{r = \frac{o}{n}}{r = o / n}
  See \url{https://example.com/shelf} and
  \href{https://example.com/shelf/rules}{the rules}.
  This text is for the console: \ifelse{text}{plain text}{rich text}.
}
\section{Evaluated when the page is built}{
  The page has \Sexpr[stage=build]{length(letters)} letters in its
  alphabet and is rendered with \Sexpr[stage=render]{R.version$language}.
}
\examples{
x <- 1:3
sum(x)
\dontrun{stop("never run")}
\donttest{max(x)}
\dontshow{stopifnot(is.numeric(x))}
}
\keyword{documentation}
)---", rd_file)
tools::Rd2txt(
  rd_file,
  stages = c("build", "install", "render"),
  options = list(underline_titles = FALSE)
)
#> Loan Rules of the Shelf Library
#>
#> Description:
#>
#>      The loan period is ‘getOption("shelf.loan_days")’ days; set it
#>      with ‘options’, or with the environment variable ‘SHELF_LOAN_DAYS’
#>      in ‘.Renviron’. See ‘shelf’, _emphasis_, *strong text* and
#>      ‘single’ or “double” quotes; x[[1]] is shown verbatim.  A book is
#>      identified by its ‘ISBN-13’ (see doi:10.32614/R.manuals
#>      <https://doi.org/10.32614/R.manuals>).
#>
#> Details:
#>
#>         • A loan is overdue after d > 21 days.
#>
#>         • Members may hold n <= 5 loans.
#>
#>        1. Take the book out.
#>
#>        2. Return it.
#>
#>      late
#>           returned after the loan period.
#>
#>      out
#>           not returned yet.
#>
#>        genre    loans  overdue
#>        Fiction     12        6
#>        Poetry       4        1
#>
#>      The rate is
#>
#>                                   r = o / n
#>
#>      See <https://example.com/shelf> and the rules.  This text is for
#>      the console: plain text.
#>
#> Evaluated when the page is built:
#>
#>      The page has 26 letters in its alphabet and is rendered with R.
#>
#> Examples:
#>
#>      x <- 1:3
#>      sum(x)
#>      ## Not run: stop("never run")
#>      max(x)
#>

# The default is stages = "render": a build-stage macro is then left as written.
small <- file.path(tempdir(), "small.Rd")
writeLines(r"---(\name{small}
\alias{small}
\title{Small}
\description{
  Built: \Sexpr[stage=build]{length(letters)};
  rendered: \Sexpr[stage=render]{R.version$language}.
}
)---", small)
tools::Rd2txt(small, options = list(underline_titles = FALSE))
#> Small
#>
#> Description:
#>
#>      Built: \Sexpr[stage=build]{length(letters)}; rendered: R.
#>

Every macro on the page has a purpose, and the plain-text conversion shows what happens to each one. The marking macros are logical, not physical: \code{} says "a piece of R code or the name of an R object" and each output format chooses the typography (typewriter font in HTML and PDF, typographic quotes in text). \emph{} and \strong{} emphasise (the text conversion uses underscores and asterisks), \sQuote{} and \dQuote{} quote without hard-wiring the quotation marks, \pkg{} names a package, \file{} a file, \env{} an environment variable, \option{} a command-line option, \command{} a command, \email{} an address, \var{} a metasyntactic variable, \kbd{} keyboard input, \samp{} a literal sequence of characters, \dfn{} a defining use of a term, \cite{} a reference without a link, \acronym{} and \abbr{} abbreviations. \verb{} and \preformatted{} keep their text exactly, and \url{} and \href{url}{text} write links to the web (the first argument of both is verbatim, with percent signs and braces escaped by a backslash). Two macros are built in for text that cannot be written plainly: \R for the name of the language and \ldots or \dots for an ellipsis; both should be followed by {} unless a space follows.

Lists and tables have three list macros and one table macro. \itemize and \enumerate take \item followed by a space and the text, with no braces; \describe takes \item{label}{body} with two arguments, exactly like the entries of \arguments and \value; \tabular{lrr}{...} takes the alignment of each column (l, r or c) and then lines separated by \cr with fields separated by \tab. There must be the same number of fields on each line as columns, and there is no white space between \tabular and its first argument. Mathematics uses \eqn{latex}{ascii} for inline formulas and \deqn{latex}{ascii} for displayed ones: the first argument is typeset by KaTeX or MathJax in HTML help (from R 4.2.0) and by LaTeX in the PDF manual, the second is what the plain-text help shows, and a single argument serves for both when the LaTeX source is readable as text (the text converter also knows the relations \le, \ge and \ne). In the output above \eqn{d > 21} has the one-argument form and \eqn{n \le 5}{n <= 5} has two.

Three macros decide what an output format sees. \if{format}{text} and \ifelse{format}{text}{alternate} select by output format (example, html, latex or text; TRUE matches all formats); \out{literal} writes its text unescaped and is used inside \if{html}{} to emit raw HTML; and a block between a line #ifdef windows (or #ifdef unix, or #ifndef with either) and a line #endif is included or excluded for one operating system before the page is processed. Platform-specific Rd files can also live in man/unix and man/windows subdirectories. In the console conversion above, \ifelse{text}{plain text}{rich text} produced "plain text".

\link{foo}, usually inside \code{}, links to the help page that has foo as an alias, in this or any installed package; whitespace is stripped when an alias is stored but not when a link target is looked up. There are four forms, and the second matters for S3 and S4 classes, which have no function of their own to link to.

Form Meaning

\link{foo}

link to the topic foo, displayed as foo

\link[=dest]{name}

link to the topic dest, displayed as name; the way to refer to an S3 class (\link[=terms.object]{terms}) or an S4 class (\link[=abc-class]{abc}); since R 4.5.0 the displayed text may contain markup such as \eqn{}

\link[pkg]{foo}, \link[pkg:bar]{foo}

link to the topic foo (or bar) in package pkg; the package must be in Depends, Imports, Suggests or Enhances; since R 4.1.0 this is a link to the topic (before that, to a file foo.html)

\linkS4class{abc}, \linkS4methods{xyz}

shorthand for the S4 class abc and for the methods of the generic xyz; the second is new in R 4.6.0

A link to a topic that does not exist is not an error when the page is converted; it is reported by R CMD check ("checking Rd cross-references"), and roxygen2 reports it while documenting. Both messages appear in the roxygen2 sections below. \concept{term} adds an index term for help.search() and ??, but a \concept cannot be the target of a link: link targets are aliases. Vignettes can be linked from help pages with relative URLs of the form ../doc/file.html, and a help page can link to a vignette of another package with ../../pkg/doc/file.html.

Examples: what is shown, what is run

The \examples section is both documentation and a test: its code is shown on the page, run by example() and run by R CMD check, so an example must be executable on any machine, without Internet access, without interaction and without writing outside a temporary directory. Four macros control that. They appear in the output of the previous script: the code in \dontrun{} is shown (as a comment, ## Not run:) but never run; \donttest{} code is shown and run by example() but, by default, not by R CMD check; \dontshow{} code is run but not shown (it used to be called \testonly, which is still accepted); and \dontdiff{} (new in R 4.4.0) marks code whose output is ignored when the check output is compared with a saved reference output.

R 4.0.0 changed what \donttest means for a CRAN submission: R CMD check --as-cran now runs the \donttest examples too, instead of telling the tester to run them (setting _R_CHECK_DONTTEST_EXAMPLES_ to a false value turns that off during development). So \donttest no longer hides slow or fragile examples from the CRAN checks; it only hides them from a plain R CMD check. The documentation recommends it only for code that may fail in circumstances that are hard to test for, with a comment that explains why. Code that needs a service, a key or a program that may be missing is better guarded by a condition, which is what the roxygen2 tag @examplesIf below writes. Examples that read from stdin, such as scan(), cannot run, because example() uses source(). Data for examples comes from random numbers or from the standard data sets (data()).

Dynamic pages: \Sexpr and \RdOpts

\Sexpr[options]{code} evaluates R code inside an Rd file and inserts the result. The options follow Sweave’s format: eval (default TRUE), echo (default FALSE), keep.source (default TRUE), results (text, the default, applies as.character() to the value; verbatim shows the printed value; rd parses the result as Rd and inserts it, which is how aliases can be computed; hide), strip.white and stage. stage is the important one: stage=build runs when the source tarball is built, stage=install (the default) when the package is installed from source, and stage=render each time the page is displayed. The order is always build, then the #ifdef conditionals, then install, then render, and code runs once in each stage, so a \Sexpr[results=rd] can output a \Sexpr for a later stage but not for the current or an earlier one. \RdOpts{} sets new defaults for the \Sexpr that follow it. The file above has a build-stage and a render-stage \Sexpr in one sentence. The first conversion ran all three stages with stages = c("build", "install", "render") and printed "26 letters" and "R"; the second, at the end of the script, ran a small file with the default stages = "render" and left the build-stage macro exactly as written, because for a package that stage is over by the time the page is rendered. In a package the build stage is the way to compute something once, when the tarball is made (the R 4.6.0 macros for bibliographic citations are built that way), the install stage fits text that depends on the installation, and the render stage text that must be current every time the page is shown. Since R 4.6.0 the error message of a failed \Sexpr includes the call of the original condition, and warnings include the Rd source location.

User-defined macros, encoding and figures

\newcommand and \renewcommand define macros inside an Rd file. They take two verbatim arguments, the name of the new macro with its backslash and its definition, and the definition may use placeholders #1 to #4; the number of arguments is the highest placeholder seen, and the arguments are substituted as text before the result is parsed. \newcommand refuses a name that is already defined, \renewcommand replaces one, built-in macros included. The file above defined \isbn as \code{ISBN-13} in its first line. R ships a set of system macros in the file share/Rd/macros/system.Rd that are available in every Rd file: \doi{} for a digital object identifier, \CRANpkg{} for a package on CRAN, \sspace for a space after a period that does not end a sentence, \packageTitle{} and its relatives (\packageDescription, \packageAuthor, \packageMaintainer, \packageDESCRIPTION, \packageIndices) that read the fields of DESCRIPTION when the package is built, and, new in R 4.6.0, \manual{name}{node} to refer to a section of one of the R manuals. Macros that several pages of a package share go into an Rd file in man/macros, which is installed into help/macros, and a package can use the macros of another by naming it in the RdMacros field of DESCRIPTION.

An Rd file with a character outside ASCII needs an \encoding{} line, unless DESCRIPTION declares Encoding: UTF-8, which shelf does; the file is converted to UTF-8 before parsing and UTF-8 is the preferred encoding. The manual advises avoiding non-ASCII characters in Rd files wherever possible, and also the symbols <, >, $, ^, &, |, @, ~ and * outside verbatim environments, because some fonts used for PDF manuals do not render them; tools::showNonASCIIfile() finds the offending bytes. For one word that must be written differently where an encoding is unavailable, \enc{Jöreskog}{Joreskog} gives the encoded and the ASCII form.

Figures for help pages go into man/figures, and \figure{file}{alternate text} includes one: the file types .jpg, .jpeg, .pdf, .png and .svg in that directory are copied to the help/figures directory of the installed package. The text conversion shows the alternate text instead. PDF figures show in the PDF manual and not in a browser, and SVG the other way round, which is why roxygen2 has the option restrict_image_formats for images added through markdown.

Special characters

Four characters need care in Rd. A percent sign starts a comment in every kind of text and has to be written \%, except in the rare verbatim variant. A backslash is written \\. A brace that is not balanced has to be escaped, and all of %, {, } and \ must be escaped in LaTeX-like text. In R-like text (the usage and examples sections) quoted strings are handled differently: braces inside a string do not need escapes. The mistake that costs most time is the percent sign, because the rest of the line, including a closing brace, disappears into the comment. tools::checkRd() is the tool that finds these mistakes, and the next script feeds it six small broken files and shows what it reports for each. It writes every file to a temporary directory and removes that directory from the messages.

# tools::checkRd() on small broken Rd files; each case shows what R reports.
check_rd <- \(label, lines) {
  f <- file.path(tempdir(), "broken.Rd")
  writeLines(lines, f)
  res <- NULL
  msg <- capture.output(
    res <- tryCatch(tools::checkRd(f), error = \(e) conditionMessage(e)),
    type = "message"
  )
  out <- capture.output(print(res))
  # Keep the temporary directory out of the messages.
  cat(label, ":\n", sep = "")
  cat(sub(paste0(tempdir(), "/"), "", c(msg, out), fixed = TRUE), sep = "\n")
}

check_rd("no title", c("\\name{a}", "\\alias{a}", "\\usage{a()}"))
#> no title:
#> [1] "broken.Rd: Sections \\title, and \\name must exist and be unique in Rd files"

check_rd("unclosed brace", c(
  "\\name{a}", "\\alias{a}", "\\title{T}",
  "\\description{An unclosed {brace.", "}"
))
#> unclosed brace:
#> prepare_Rd: broken.Rd:6: unexpected END_OF_INPUT '
#> '
#> checkRd: (-1) broken.Rd:4-5: Lost braces; missing escapes or markup?
#>      4 | \description{An unclosed {brace.
#>        |                          ^

check_rd("macro without its backslash", c(
  "\\name{a}", "\\alias{a}", "\\title{T}",
  "\\description{Use code{x} here.}"
))
#> macro without its backslash:
#> checkRd: (-1) broken.Rd:4: Lost braces
#>      4 | \description{Use code{x} here.}
#>        |                      ^

check_rd("misspelt macro", c(
  "\\name{a}", "\\alias{a}", "\\title{T}",
  "\\description{Use \\codee{x} here.}"
))
#> misspelt macro:
#> prepare_Rd: broken.Rd:4: unknown macro '\codee'

check_rd("unescaped percent sign", c(
  "\\name{a}", "\\alias{a}", "\\title{T}",
  "\\description{It is 50% done.}", "\\examples{x <- 1}"
))
#> unescaped percent sign:
#> prepare_Rd: broken.Rd:5: unexpected section header '\examples'
#> prepare_Rd: broken.Rd:6: unexpected END_OF_INPUT '
#> '
#> checkRd: (-1) broken.Rd:5: Lost braces; missing escapes or markup?
#>      5 | \examples{x <- 1}
#>        |          ^

check_rd("title ending in a period, item with two arguments", c(
  "\\name{a}", "\\alias{a}", "\\title{A title.}", "\\description{Text.}",
  "\\details{\\itemize{\\item{x}{y}}}"
))
#> title ending in a period, item with two arguments:
#> checkRd: (-5) broken.Rd:3: \title should not end in a period
#> checkRd: (-3) broken.Rd:5: Lost braces
#>      5 | \details{\itemize{\item{x}{y}}}
#>        |                        ^
#> checkRd: (-1) broken.Rd:5: Lost braces in \itemize; meant \describe ?

The messages are worth learning to read. A file without a \title fails the structural check at once. An unclosed brace gives a parser message ("unexpected END_OF_INPUT") followed by "Lost braces" with the offending line and a caret. A macro without its backslash (code{x} for \code{x}) is not an error for the parser, since code{x} is just text followed by a braced group, and is reported as "Lost braces". A misspelt macro is "unknown macro" at parse time. The unescaped percent sign swallowed the closing brace of \description and the \examples section that followed, so the parser reports an "unexpected section header" and then the lost braces. The last file has two further findings: the number in parentheses is the level of the message (-5 is the lowest, for style), and the title ending in a period only appears at that low level; "Lost braces in \itemize; meant \describe ?" is the message for \item{x}{y} used inside \itemize, where \item takes no braces. In a package, R CMD check runs these checks for every file in man in the step "checking Rd files".

Writing Rd by hand with prompt()

prompt() writes an Rd skeleton for a function or data frame that you then edit, with comments (%%) that tell you what to write where. It is the older, manual route: the skeleton is correct for the arguments that exist on the day it is written, and you keep it in step by hand afterwards. promptData(), promptPackage() (and, from the methods package, promptClass() and promptMethods()) do the same for data, packages and S4 classes and methods, and package.skeleton() writes a whole package skeleton with such files, as Creating Packages shows. The script runs prompt() on normalise_isbn() of the installed shelf and prints the start of the file:

# prompt() writes an Rd skeleton for a function, to be edited by hand.
library(shelf)
#> shelf 0.1.0: loan period is 21 days
file <- file.path(tempdir(), "normalise_isbn.Rd")
invisible(capture.output(
  prompt(normalise_isbn, filename = file),
  type = "message"
))
head(readLines(file), 16)
#>  [1] "\\name{normalise_isbn}"
#>  [2] "\\alias{normalise_isbn}"
#>  [3] "%- Also NEED an '\\alias' for EACH other topic documented here."
#>  [4] "\\title{"
#>  [5] "A Capitalized Title (ideally limited to 65 characters)"
#>  [6] "}"
#>  [7] "\\description{"
#>  [8] "%%  ~~ A concise (1-5 lines) description of what the function does. ~~"
#>  [9] "}"
#> [10] "\\usage{"
#> [11] "normalise_isbn(x)"
#> [12] "}"
#> [13] "%- maybe also 'usage' for other objects documented here."
#> [14] "\\arguments{"
#> [15] "  \\item{x}{"
#> [16] "%%     ~~Describe \\code{x} here~~"

Everything between the \description, \usage and \arguments braces is placeholder text. Leaving the placeholders in is a classic CRAN rejection ("A Capitalized Title (ideally limited to 65 characters)" shipped as a title), and the reason most packages that document by hand run R CMD check before every commit.

Converting Rd

The conversions of an Rd file are functions of the tools package, and the R CMD commands wrap them. The input is a file, the result is text, HTML, LaTeX or R code, and none of the conversions needs anything but R except the PDF.

Plain text: Rd2txt

tools::Rd2txt() is what help("book") runs when it shows a page in a terminal. The result of converting man/book.Rd is the page below: the title, the sections in the order of the help system, the markup replaced by typography (\code{} by single typographic quotes) and the usage written in the form the function has. The script is run in a copy of the package, in which man/book.Rd is relative to the package root.

# Render man/book.Rd as plain text, the way help("book") shows it in a terminal.
tools::Rd2txt("man/book.Rd", options = list(underline_titles = FALSE))
#> The 'book' class
#>
#> Description:
#>
#>      ‘book()’ is the user-facing constructor of the ‘book’ class: it
#>      trims the text fields, normalises the ISBN with
#>      ‘normalise_isbn()’, coerces ‘year’ and ‘pages’ to integer and
#>      validates the result. ‘new_book()’ is the low-level constructor
#>      that only checks types, and ‘validate_book()’ checks the values;
#>      both are exported so that other packages can extend the class.
#>
#> Usage:
#>
#>      book(
#>        title,
#>        author = NA_character_,
#>        isbn = NA_character_,
#>        year = NA_integer_,
#>        genre = NA_character_,
#>        pages = NA_integer_
#>      )
#>
#>      new_book(
#>        title = character(),
#>        author = character(),
#>        isbn = character(),
#>        year = integer(),
#>        genre = character(),
#>        pages = integer()
#>      )
#>
#>      validate_book(x)
#>
#> Arguments:
#>
#>    title: Title of the book (a single string).
#>
#>   author: Author (a single string, or ‘NA’).
#>
#>     isbn: ISBN-13, hyphenated or bare (a single string, or ‘NA’).
#>
#>     year: Publication year (a single number, or ‘NA’).
#>
#>    genre: Genre (a single string, or ‘NA’).
#>
#>    pages: Number of pages (a single number, or ‘NA’).
#>
#>        x: A ‘book’ object.
#>
#> Value:
#>
#>      An object of class ‘book’: a list with the six fields above, each
#>      of length one.
#>
#> See Also:
#>
#>      Other book class: ‘book-methods’
#>
#> Examples:
#>
#>      b <- book("  the quiet streets ", "Mara Quill", "978-0-00-100010-0", 2019,
#>                "Fiction ", 214)
#>      b
#>      summary(b)
#>      unclass(b)
#>

HTML, examples and LaTeX

tools::Rd2HTML() writes the HTML that help() shows in the browser and that CRAN and pkgdown show on the web, tools::Rd2ex() writes the examples as an R script and tools::Rd2latex() writes the LaTeX that R CMD Rd2pdf feeds to pdflatex. The script converts the same file with each one into temporary files and shows the headings of the HTML page, the extracted examples, and the first lines of the LaTeX. The header that Rd2ex() writes records the page name, the title and the aliases, which is why running a package’s examples gives traceable output.

# The same Rd file through the other converters, written to temporary files.
html <- tempfile(fileext = ".html")
tools::Rd2HTML("man/book.Rd", out = html, package = "shelf")
lines <- readLines(html)
# The title line of the page and the headings of its sections.
grep("R Documentation|^<h[23]", lines, value = TRUE)
#> [1] "<table style=\"width: 100%;\"><tr><td>book {shelf}</td><td style=\"text-align: right;\">R Documentation</td></tr></table>"
#> [2] "<h2>The <code>book</code> class</h2>"
#> [3] "<h3>Description</h3>"
#> [4] "<h3>Usage</h3>"
#> [5] "<h3>Arguments</h3>"
#> [6] "<h3>Value</h3>"
#> [7] "<h3>See Also</h3>"
#> [8] "<h3>Examples</h3>"

# Rd2ex() extracts the examples, with a header that records the page.
ex <- tempfile(fileext = ".R")
tools::Rd2ex("man/book.Rd", out = ex)
cat(readLines(ex), sep = "\n")
#> ### Name: book
#> ### Title: The 'book' class
#> ### Aliases: book new_book validate_book
#>
#> ### ** Examples
#>
#> b <- book("  the quiet streets ", "Mara Quill", "978-0-00-100010-0", 2019,
#>           "Fiction ", 214)
#> b
#> summary(b)
#> unclass(b)
#>
#>
#>

# Rd2latex() writes the LaTeX that R CMD Rd2pdf feeds to pdflatex.
tex <- tempfile(fileext = ".tex")
tools::Rd2latex("man/book.Rd", out = tex)
head(readLines(tex), 6)
#> [1] "\\HeaderA{book}{The \\code{book} class}{book}"
#> [2] "\\aliasA{new\\_book}{book}{new.Rul.book}"
#> [3] "\\aliasA{validate\\_book}{book}{validate.Rul.book}"
#> [4] "\\keyword{book class}{book}"
#> [5] "%"
#> [6] "\\begin{Description}"

R CMD Rdconv

R CMD Rdconv is the command-line interface to the text, HTML, LaTeX and example conversions of one Rd file. The option -t chooses the type (txt, html, latex or example), and R CMD Rdconv --help lists the rest. The command is run in the directory that holds shelf:

R CMD Rdconv -t example shelf/man/normalise_isbn.Rd
echo "exit status: $?"
### Name: normalise_isbn
### Title: Normalise and validate ISBN-13 strings
### Aliases: normalise_isbn is_valid_isbn13

### ** Examples

normalise_isbn("978-0-00-100010-0")
is_valid_isbn13(c("978-0-00-100010-0", "9780001000101", "", NA))



exit status: 0

The output is the example script of the page normalise_isbn, with the same header as Rd2ex() wrote above. Running the examples of an installed package is also what example(book) does, and the help-lookup section below shows it.

R CMD Rd2pdf and the PDF reference manual

R CMD Rd2pdf builds a PDF from Rd files, given either as files or as the path of a package, in which case it builds the reference manual of the whole package with the information of DESCRIPTION. It converts the Rd files to LaTeX and runs LaTeX on the result, so it needs a TeX installation with pdflatex. The image used for this page has none, so the attempt, run on the shelf package with --no-preview so that it would not try to open a viewer, fails. The failure message and the exit status are what R reports:

R CMD Rd2pdf --no-preview --output=shelf-manual.pdf shelf
echo "exit status: $?"
Hmm ... looks like a package
Converting Rd files to LaTeX
Creating pdf output from LaTeX ...
Warning in texi2dvi(file = file, pdf = TRUE, clean = clean, quiet = quiet,  :
  texi2dvi script/program not available, using emulation
Error in texi2dvi(file = file, pdf = TRUE, clean = clean, quiet = quiet,  :
  pdflatex is not available
Warning in texi2dvi(file = file, pdf = TRUE, clean = clean, quiet = quiet,  :
  texi2dvi script/program not available, using emulation
Error in texi2dvi(file = file, pdf = TRUE, clean = clean, quiet = quiet,  :
  pdflatex is not available
Error in running tools::texi2pdf()
exit status: 1

The first stage, "Converting Rd files to LaTeX", worked; the second, "Creating pdf output from LaTeX", did not, because tools::texi2pdf() looks for the texi2dvi program, falls back to an emulation, and the emulation needs pdflatex on the path ("pdflatex is not available"). It reports it twice because it tries twice. The manual R Installation and Administration states the requirement: building PDF vignettes and manuals needs a distribution of LaTeX with pdflatex. With a TeX distribution (TeX Live, MacTeX, MiKTeX or the smaller TinyTeX) the same command writes shelf-manual.pdf. The consequences for the rest of the work are simple. The conversions to text, HTML and examples need no TeX, and neither does anything else on this page; R CMD check builds the PDF manual as one of its steps and, without TeX, reports a WARNING and then an ERROR in the steps "checking PDF version of manual" (the real lines are in Publishing to CRAN), which is why every check of the shelf package on this page was run with --no-manual; and the HTML vignettes that this page builds need Pandoc and no TeX, while PDF vignettes need TeX. R 4.5.0 changed R CMD Rd2pdf so that the Rd.sty of the running R takes precedence over any other version in the TeX tree.

How help() finds a page

An installed package keeps its Rd files in a parsed form. The script looks at what the installation of the Rd files of shelf left in the library: the help directory with the topic index (aliases.rds maps every alias to the Rd file it came from, AnIndex is the index that help() reads, shelf.rdb and shelf.rdx hold the parsed pages), and the table Meta/Rd.rds from which help.search() and the package index are built.

# What the installation of the Rd files leaves in the library.
help_dir <- system.file("help", package = "shelf")
list.files(help_dir)
#> [1] "aliases.rds" "AnIndex"     "paths.rds"   "shelf.rdb"   "shelf.rdx"

# aliases.rds maps every \alias to the Rd file (the topic name) it came from.
aliases <- readRDS(file.path(help_dir, "aliases.rds"))
aliases[c("format.book", "is_valid_isbn13", "new_book", "clean_loans")]
#>      format.book  is_valid_isbn13         new_book      clean_loans
#>   "book-methods" "normalise_isbn"           "book"    "clean_books"

# Meta/Rd.rds is the table that help.search() and the HTML index are built from.
rd <- readRDS(system.file("Meta", "Rd.rds", package = "shelf"))
names(rd)
#> [1] "File"     "Name"     "Type"     "Title"    "Encoding" "Aliases"  "Concepts"
#> [8] "Keywords"
rd[, c("File", "Name", "Title")]
#>                 File            Name                                   Title
#> 1    book-methods.Rd    book-methods            Methods for the 'book' class
#> 2            book.Rd            book                        The 'book' class
#> 3           books.Rd           books                     The shelf catalogue
#> 4     clean_books.Rd     clean_books Clean the catalogue and the loans table
#> 5    loan_summary.Rd    loan_summary       Loans and overdue rates per genre
#> 6           loans.Rd           loans                         The shelf loans
#> 7 loans_per_month.Rd loans_per_month                         Loans per month
#> 8  normalise_isbn.Rd  normalise_isbn  Normalise and validate ISBN-13 strings
#> 9   overdue_loans.Rd   overdue_loans                           Overdue loans
rd$Aliases[[2L]]
#> [1] "book"          "new_book"      "validate_book"
rd$Concepts[[2L]]
#> [1] "book class"

The lookup functions use those files. help(package = "shelf") lists the topics of the package (its index), help("topic", package = ) finds a page by alias, ?? and help.search() look in titles, aliases, concepts and keywords, and example() runs the \examples of a page. The script shows each one, including what help() says about a topic that does not exist.

# How help() finds the page of an installed package.
library(shelf)
#> shelf 0.1.0: loan period is 21 days

# help(package =) lists the topics of the package: its index.
h <- help(package = "shelf")
class(h)
#> [1] "packageInfo"
cat(h$info[[2L]], sep = "\n")
#> book                    The 'book' class
#> book-methods            Methods for the 'book' class
#> books                   The shelf catalogue
#> clean_books             Clean the catalogue and the loans table
#> loan_summary            Loans and overdue rates per genre
#> loans                   The shelf loans
#> loans_per_month         Loans per month
#> normalise_isbn          Normalise and validate ISBN-13 strings
#> overdue_loans           Overdue loans

# A topic is an \alias. format.book is an alias of the page book-methods.
basename(help("book", package = "shelf"))
#> [1] "book"
basename(help("new_book", package = "shelf"))
#> [1] "book"
basename(help("format.book", package = "shelf"))
#> [1] "book-methods"
help("no_such_topic", package = "shelf")
#> No documentation for ‘no_such_topic’ in specified packages and libraries:
#> you could try ‘??no_such_topic’

# ?? and help.search() look in titles, aliases and concepts. Printing the
# result shows it in the pager; the table of matches is a data frame.
hs <- help.search("ISBN", package = "shelf")
class(hs)
#> [1] "hsearch"
hs$matches[, c("Topic", "Field", "Entry")]
#>            Topic Field                                  Entry
#> 1 normalise_isbn alias                         normalise_isbn
#> 2 normalise_isbn alias                        is_valid_isbn13
#> 3 normalise_isbn Title Normalise and validate ISBN-13 strings

# example() runs the \examples of the page.
example(book)
#>
#> book> b <- book("  the quiet streets ", "Mara Quill", "978-0-00-100010-0", 2019,
#> book+           "Fiction ", 214)
#>
#> book> b
#> <book> the quiet streets (Mara Quill, 2019)
#>
#> book> summary(b)
#> Book: the quiet streets
#> ISBN known: TRUE
#> Pages: 214
#>
#> book> unclass(b)
#> $title
#> [1] "the quiet streets"
#>
#> $author
#> [1] "Mara Quill"
#>
#> $isbn
#> [1] "9780001000100"
#>
#> $year
#> [1] 2019
#>
#> $genre
#> [1] "Fiction"
#>
#> $pages
#> [1] 214
#>

format.book and new_book are not pages of their own; they are aliases of the pages book-methods and book, which is why one Rd file can document a whole family. help.search("ISBN") found the page normalise_isbn three times, by the alias normalise_isbn, by the alias is_valid_isbn13 and by the word in the title. The listing of example(book) is what the \examples section of man/book.Rd did when run through source() with the book> prompt: \dontshow code would have run silently, and \dontrun code would have been shown as a comment.

roxygen2: Rd files from comments

Rd is a good format for a machine and a poor one to maintain: the usage section has to repeat the signature of the function exactly, every argument has to be described in a separate block, and each of these is far from the code that changes. roxygen2 moves the documentation into the R source, in special comments that start with #' and sit immediately above the object they document, and writes the Rd files, the NAMESPACE and the Collate field from them. The usage section is taken from the function itself, so it cannot disagree with the code, and the arguments, the exports and the documentation of a function are in one place. roxygen2 is a CRAN package (8.1.1 on 2026-10-08); the 8.0.0 release of 2026-05-01 is the one that requires R 4.1 and moved the package’s settings into Config/roxygen2/ fields of DESCRIPTION, as the section on DESCRIPTION below explains.

Blocks, tags and the first paragraphs

A block is a run of #' lines. The first paragraph is the title, the second is the description, and any further paragraph is the details, unless a tag says otherwise. A tag is a word that starts with @ at the start of a line, and everything up to the next tag belongs to it. The example below uses a throwaway package in a temporary directory, so that nothing but the generated files is involved: one function with a title, a description with markdown code and a link, a details paragraph with a list, a parameter tag that documents two parameters, a return value, an example, and the two namespace tags @importFrom and @export. The script runs roxygen2::roxygenise() and prints the Rd file and the NAMESPACE that came out.

# A throwaway package with one documented function, turned into Rd by roxygen2.
pkg <- file.path(tempdir(), "demo")
dir.create(file.path(pkg, "R"), recursive = TRUE)
writeLines(c(
  "Package: demo",
  "Title: A Demonstration",
  "Version: 0.0.1",
  "Description: Shows what roxygen2 writes.",
  "License: MIT",
  "Encoding: UTF-8",
  "Config/roxygen2/markdown: TRUE"
), file.path(pkg, "DESCRIPTION"))

writeLines(r"(#' Take a book out
#'
#' Records a loan of `book` to `member`. The loan is overdue after
#' `days` days. Summarise loan lengths with [stats::median()].
#'
#' The second paragraph is the *details* section. A list:
#' * one
#' * two
#'
#' @param book A book (a string).
#' @param member,days Name of the member and length of the loan.
#' @returns A list with the three inputs.
#' @examples
#' loan_book("Dune", "Ana")
#' @importFrom stats median
#' @export
loan_book <- function(book, member, days = 21L) {
  list(book = book, member = member, days = days)
}
)", file.path(pkg, "R", "loan.R"))

invisible(capture.output(roxygen2::roxygenise(pkg), type = "message"))
cat(readLines(file.path(pkg, "man", "loan_book.Rd")), sep = "\n")
#> % Generated by roxygen2: do not edit by hand
#> % Please edit documentation in R/loan.R
#> \name{loan_book}
#> \alias{loan_book}
#> \title{Take a book out}
#> \usage{
#> loan_book(book, member, days = 21L)
#> }
#> \arguments{
#> \item{book}{A book (a string).}
#>
#> \item{member, days}{Name of the member and length of the loan.}
#> }
#> \value{
#> A list with the three inputs.
#> }
#> \description{
#> Records a loan of \code{book} to \code{member}. The loan is overdue after
#> \code{days} days. Summarise loan lengths with \code{\link[stats:median]{stats::median()}}.
#> }
#> \details{
#> The second paragraph is the \emph{details} section. A list:
#> \itemize{
#> \item one
#> \item two
#> }
#> }
#> \examples{
#> loan_book("Dune", "Ana")
#> }
cat(readLines(file.path(pkg, "NAMESPACE")), sep = "\n")
#> # Generated by roxygen2: do not edit by hand
#>
#> export(loan_book)
#> importFrom(stats,median)

The mapping is the one to remember. The title went to \title, the first paragraph after it to \description, the second to \details. @param member,days became one \item{member, days} entry, because a tag can document several arguments with one text. @returns became \value. The \usage section was not written by hand: roxygen2 took it from the function. The markdown in the description was translated (`book` to \code{book}, [stats::median()] to a link to the median topic in stats, *details* to \emph, the list to \itemize), and the two namespace tags went to the NAMESPACE file, not to the Rd file: export(loan_book) and importFrom(stats,median). The tag @importFrom stats median is there only to show the directive; in a real package it sits in the block of a function that calls median().

What roxygenise() does

roxygen2::roxygenise() (spelt roxygenize() too) takes a package directory, loads the package code, parses the blocks and runs the roclets. A roclet is a processor of the parsed blocks that produces one kind of output: rd writes man/*.Rd, namespace writes NAMESPACE, collate writes the Collate field of DESCRIPTION from the @include tags. Those three run by default. By default the package is loaded with pkgload, and the code is evaluated, so that roxygen2 knows what a function, a dataset or a class looks like. devtools::document() is the usual front end: it calls roxygenise() and prints its progress. roxygen2::needs_roxygenize() (new in 8.0.0) answers cheaply whether the Rd files are older than their sources. Every file roxygen2 writes starts with the comment lines "Generated by roxygen2: do not edit by hand" and "Please edit documentation in R/book.R", and roxygen2 only rewrites files that carry that header. The script reads the settings of shelf as roxygen2 sees them.

# The options roxygen2 reads for the package, and where they come from.
read.dcf("DESCRIPTION", fields = c("Roxygen", "Config/roxygen2/version"))
#>      Roxygen                 Config/roxygen2/version
#> [1,] "list(markdown = TRUE)" "8.1.1"
opts <- roxygen2::load_options(".")
opts$roclets
#> [1] "collate"   "namespace" "rd"
opts$markdown
#> [1] TRUE
opts$load
#> [1] "pkgload"

A file that was not written by roxygen2 is left alone. This matters when you convert a package that has hand-written Rd files, and it is the message to know: the script puts a hand-written loan_book.Rd where roxygen2 wants to write one, and roxygen2 refuses to overwrite it, tells you why and leaves the file as it was. (The first two lines are the first-run message of roxygen2 8, which also sets the version field in DESCRIPTION.)

# roxygen2 does not overwrite an Rd file that it did not write itself.
pkg <- file.path(tempdir(), "demo")
dir.create(file.path(pkg, "R"), recursive = TRUE)
dir.create(file.path(pkg, "man"))
writeLines(c(
  "Package: demo",
  "Title: A Demonstration",
  "Version: 0.0.1",
  "Description: Shows what roxygen2 writes.",
  "License: MIT",
  "Encoding: UTF-8",
  "Config/roxygen2/markdown: TRUE"
), file.path(pkg, "DESCRIPTION"))
writeLines(c(
  "#' Take a book out",
  "#' @export",
  "loan_book <- function() 1"
), file.path(pkg, "R", "loan.R"))
writeLines(c(
  "\\name{loan_book}",
  "\\alias{loan_book}",
  "\\title{Written by hand}",
  "\\description{Not generated.}"
), file.path(pkg, "man", "loan_book.Rd"))

msg <- capture.output(roxygen2::roxygenise(pkg), type = "message")
sub("\\(<file://.*>\\)", "(man/loan_book.Rd)", msg)
#> [1] "First time using roxygen2. Upgrading automatically..."
#> [2] "ℹ Setting Config/roxygen2/version to \"8.1.1\""
#> [3] "ℹ Loading demo"
#> [4] "Writing 'NAMESPACE'"
#> [5] "✖ Skipping loan_book.Rd (man/loan_book.Rd)"
#> [6] "ℹ It already exists and was not generated by roxygen2."
cat(readLines(file.path(pkg, "man", "loan_book.Rd")), sep = "\n")
#> \name{loan_book}
#> \alias{loan_book}
#> \title{Written by hand}
#> \description{Not generated.}

The first run and the second run of document()

The first time devtools::document() is run on a package whose blocks link to each other, roxygen2 warns: a link such as [normalise_isbn()] in the description of book() points to a topic whose Rd file does not exist yet, because it is written in the same run, after the link is checked. The second run finds the files and is clean. This is the real sequence on shelf, from a copy of the package in which man/ and NAMESPACE were deleted first, as after writing the R files and before documenting them. The script collects the messages of the first run into first, shows how many there are, the first nine, the lines that start with a cross, and the last ten, and then runs it again. The page shows both runs.

# Start from a package with no man/ and no NAMESPACE, as after use_r() only.
unlink(c("man", "NAMESPACE"), recursive = TRUE)
list.files()
#>  [1] "cran-comments.md" "data"             "data-raw"         "DESCRIPTION"
#>  [5] "inst"             "LICENSE"          "LICENSE.md"       "NEWS.md"
#>  [9] "R"                "tests"            "vignettes"

# First run: every [fn()] link to a topic that does not exist yet is reported.
# The messages are collected so that the listing can be shortened below.
first <- capture.output(devtools::document(), type = "message")
length(first)
#> [1] 83
head(first, 9)
#> [1] "ℹ Updating shelf documentation"
#> [2] "ℹ Loading shelf"
#> [3] "shelf 0.1.0: loan period is 21 days"
#> [4] "✖ book.R:3: @description Could not resolve link to topic \"normalise_isbn\" in"
#> [5] "  the dependencies or base packages.."
#> [6] "ℹ If you haven't documented \"normalise_isbn\" yet, or just changed its name,"
#> [7] "  this is normal. Once \"normalise_isbn\" is documented, this warning goes away."
#> [8] "ℹ Make sure that the name of the topic is spelled correctly."
#> [9] "ℹ Always list the linked package as a dependency."
grep("^[✖]", first, value = TRUE)
#>  [1] "✖ book.R:3: @description Could not resolve link to topic \"normalise_isbn\" in"
#>  [2] "✖ clean.R:10: @param Could not resolve link to topic \"books\" in the"
#>  [3] "✖ clean.R:11: @param Could not resolve link to topic \"loans\" in the"
#>  [4] "✖ data.R:3: @description Could not resolve link to topic \"clean_books\" in the"
#>  [5] "✖ data.R:25: @description Could not resolve link to topic \"books\" in the"
#>  [6] "✖ data.R:25: @description Could not resolve link to topic \"clean_loans\" in the"
#>  [7] "✖ loans.R:8: @param Could not resolve link to topic \"loans\" in the dependencies"
#>  [8] "✖ loans.R:8: @param Could not resolve link to topic \"clean_loans\" in the"
#>  [9] "✖ loans.R:42: @description Could not resolve link to topic \"overdue_loans\" in"
#> [10] "✖ loans.R:47: @param Could not resolve link to topic \"clean_books\" in the"
tail(first, 10)
#>  [1] "Writing 'NAMESPACE'"          "Writing 'book.Rd'"
#>  [3] "Writing 'book-methods.Rd'"    "Writing 'clean_books.Rd'"
#>  [5] "Writing 'books.Rd'"           "Writing 'loans.Rd'"
#>  [7] "Writing 'normalise_isbn.Rd'"  "Writing 'overdue_loans.Rd'"
#>  [9] "Writing 'loan_summary.Rd'"    "Writing 'loans_per_month.Rd'"

# Second run: the topics exist, so the links resolve and nothing is reported.
devtools::document()
#> ℹ Updating shelf documentation
#> ℹ Loading shelf
#> shelf 0.1.0: loan period is 21 days
list.files("man")
#> [1] "book-methods.Rd"    "book.Rd"            "books.Rd"
#> [4] "clean_books.Rd"     "loan_summary.Rd"    "loans_per_month.Rd"
#> [7] "loans.Rd"           "normalise_isbn.Rd"  "overdue_loans.Rd"

The warning is harmless the first time: the output itself says "If you haven’t documented …​ yet, or just changed its name, this is normal". It also names the file and line of the tag that holds the link (book.R:3: @description), which makes a real mistake easy to find. A link that is still unresolved on the second run is a typo or a topic of a package that is not in Imports or Suggests. Note that the ten warnings did not stop the run: the nine Rd files and NAMESPACE were written and the exit status was 0; roxygen2 reports problems without failing, which is why the output has to be read, and why R CMD check is the place that fails.

DESCRIPTION: version and options

roxygen2 8 keeps its settings in Config/roxygen2/ fields of DESCRIPTION. The version of roxygen2 that last documented the package is Config/roxygen2/version; the field that roxygen2 7.x used for this purpose, a note-style field written under another name, is no longer written, and roxygen2 8 migrates a package automatically the next time it documents it (the message "First time using roxygen2. Upgrading automatically…​" and the line setting Config/roxygen2/version in the output above are that migration for a package that had neither). The options of roxygenise() (markdown, load, roclets, packages, r6, old_usage, restrict_image_formats) can be set as Config/roxygen2/markdown: TRUE, and the older Roxygen: list(markdown = TRUE) field is still read. The shelf package has the second form, because that is what usethis 3.2.2 writes when it turns on markdown; its DESCRIPTION is below. Options with a structure, such as rd_family_title (titles for @family) and knitr_chunk_options, go into man/roxygen/meta.R, which overrides DESCRIPTION. Flags in Config/roxygen2/ are read case-insensitively since roxygen2 8.1.0 (TRUE, true and True all work).

Package: shelf
Title: Tools for a Small Library Catalogue
Version: 0.1.0
Authors@R:
    person("Shelf", "Maintainers", , "maintainer@example.com", role = c("aut", "cre", "cph"))
Description: Cleans a small library catalogue and its loans table,
    validates ISBN-13 strings, and reports overdue loans and overdue rates
    by genre. Ships the example 'books' and 'loans' datasets used by the
    documentation.
License: MIT + file LICENSE
Config/roxygen2/version: 8.1.1
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
Suggests:
    knitr,
    rmarkdown,
    testthat (>= 3.0.0),
    withr
Config/testthat/edition: 3
Depends:
    R (>= 4.1.0)
VignetteBuilder: knitr
LazyData: true

The Title and Description fields are documentation too. Title is one line in title case without a final period (the manual says that some listings truncate it to 65 characters), Description is one paragraph, and the checks read both: the manual asks for other packages and external software to be written in single quotes, and for URLs in angle brackets (the Description of shelf writes 'books' and 'loans' in single quotes, as it names the two data sets). The Authors@R field is what roxygen2 uses for the package page of the next sections. Everything else in DESCRIPTION is explained in Creating Packages.

The tags of roxygen2

The tags are listed in the reference of roxygen2 and its articles; the tables here are grouped by what they write. The script after the tables asks roxygen2 for the tags that it can parse, which is a quick way to see how many there are.

Tag What it writes

@title, @description, @details

\title, \description, \details. The first three paragraphs of a block do the same without tags; the tags are needed to write them in another order or to have several paragraphs of description.

@param name text

one entry of \arguments; several names can share one text (@param member,days); @param ... documents the dots.

@returns text, @return text

\value. @returns is an alias of @return that was added in roxygen2 7.0.0 and that the tidyverse style prefers.

@examples

\examples: code, shown and run by example() and R CMD check; \dontrun{}, \donttest{} and \dontshow{} can be written inside it.

@examplesIf condition

\examples that run only if the condition holds (added in roxygen2 7.1.2). The first line after the tag is the condition, the following lines are the example; roxygen2 wraps the code in an if that is hidden from the reader (\dontshow). Use it for examples that need a service, a key or an interactive session.

@example path

example code read from an .R file instead of written in the block.

@section Title:

a \section{Title}{...}; the title ends with a colon and is on one line.

@seealso, @references, @author, @note

\seealso, \references, \author, \note.

@format, @source

\format, \source of a data set.

@keywords, @concept, @aliases

\keyword, \concept, extra \alias entries. @keywords internal hides the page from the topic index.

@name, @rdname, @usage

the page name; the page a block is merged into; an explicit usage.

@noRd

documents without writing an Rd file (useful for internal helpers).

@md, @noMd

turn markdown on or off for one block.

@slot, @field, @prop

the slots of an S4 class, the fields of a reference or R6 class, the properties of an S7 class that are not constructor arguments (@prop is new in roxygen2 8.0.0).

Tag What it writes to NAMESPACE

@export

export(name); on an S3 method, S3method(generic,class); on an S4 class or method, exportClasses() or exportMethods().

@exportS3Method pkg::generic

S3method(pkg::generic,class): registers a method for a generic of another package without importing it (the delayed-registration form).

@importFrom pkg a b, @import pkg

importFrom(pkg,a,b), import(pkg). Since roxygen2 8.1.0, @importFrom writes a single multi-line directive per package, and the tag accepts continuation lines with a hanging indent.

@importClassesFrom, @importMethodsFrom, @exportClass, @exportMethod, @exportPattern

the S4 forms and the pattern form of the directives.

@useDynLib pkg

useDynLib(pkg) for compiled code.

@rawNamespace text, @evalNamespace code

text, or the text produced by code, copied into NAMESPACE, for conditional imports and for functions generated by other functions.

Tag What it does

@inheritParams source

copies the @param entries of another topic for the arguments of this function that are not documented here (the next section).

@inheritSection source Title, @inherit source what, @inheritDotParams source

copies one section; copies the listed parts (return, title, description, details, seealso, sections, references, examples, author, source, note, format, params) or all of them; documents ... with the arguments of another function.

@family name

links the pages of one family with "Other name:" in See Also, and adds the family name to the concepts.

@describeIn dest text, @rdname dest

merges this object into the page of dest, as a short entry in a "Functions" section (@describeIn), or as an equal part of that page (@rdname, which is usually the better choice).

@template name, @templateVar name value

inserts the roxygen text of the file man-roxygen/name.R, with <%= name %> variables; the reference calls it superseded by inline R code in markdown.

@eval code, @evalRd code, @rawRd text

roxygen text, Rd text, or raw Rd produced by code.

@include file

orders the files in Collate (a file that uses setClass() results of another must be sourced after it).

@includeRmd file

converts an .Rmd or .md file to Rd and includes it in the page; the reference calls it superseded by a code chunk with a child document.

@order n

overrides the lexicographic order in which several blocks that share one page are combined.

# The tags roxygen2 8.1.1 parses: one parser method per tag in its namespace.
# This reads an internal naming convention, so it is a look inside the package,
# not an interface to rely on.
methods <- ls(asNamespace("roxygen2"), pattern = "^roxy_tag_parse[.]roxy_tag_")
tags <- sub("^roxy_tag_parse[.]roxy_tag_", "", methods)
length(tags)
#> [1] 59
cat(sort(tags), fill = 75)
#> aliases author backref concept describeIn description details docType
#> encoding eval evalNamespace evalRd example examples examplesIf export
#> exportClass exportMethod exportPattern exportS3Method family field format
#> import importClassesFrom importFrom importMethodsFrom include includeRmd
#> inherit inheritDotParams inheritParams inheritSection keywords md method
#> name noMd noRd note order param prop R6method rawNamespace rawRd rdname
#> references return returns section seealso slot source template templateVar
#> title usage useDynLib

The list is not an interface: the script looks at an internal naming convention of the roxygen2 namespace, one parser method per tag, and prints 59 names. It shows that the tags above are a fraction of the whole (@evalRd, @includeRmd, @rawRd, @useDynLib and so on), and that the language is the same for every roclet. Since roxygen2 8.0.0 a tag that expects one line warns when it spans several, which covers @aliases, @concept, @encoding, @exportClass, @exportMethod, @exportPattern, @exportS3Method, @importFrom, @importClassesFrom, @importMethodsFrom, @include, @includeRmd, @inheritDotParams, @inheritParams, @inheritSection, @keywords, @method, @name, @order, @rdname, @template and @useDynLib; in the 8.1.0 release @importFrom, @importClassesFrom and @importMethodsFrom were allowed to spread over several lines again, with a hanging indent for the continuation lines.

The tables above say what each tag writes. This part shows the tags at work, one script at a time, with the files that roxygen2 generated.

Markdown in roxygen2 comments

With markdown on (Roxygen: list(markdown = TRUE) or Config/roxygen2/markdown: TRUE), roxygen2 translates the text of the tags into Rd with the CommonMark parser. Writing markdown is faster than writing Rd and reads better in the source, and the translation is not a pass-through: it chooses the Rd macro that fits. The script documents a function whose description uses most of the forms and prints the Rd that came out. The function has no body that matters; the page is the point.

# What roxygen2's markdown support writes for links, code, lists and tables.
pkg <- file.path(tempdir(), "mddemo")
dir.create(file.path(pkg, "R"), recursive = TRUE)
writeLines(c(
  "Package: mddemo",
  "Title: Markdown in roxygen2",
  "Version: 0.0.1",
  "Description: Shows the markdown to Rd translation.",
  "License: MIT",
  "Encoding: UTF-8",
  "Config/roxygen2/markdown: TRUE"
), file.path(pkg, "DESCRIPTION"))

writeLines(r"(#' Markdown forms
#'
#' Links: [mean()], [stats::median()], [base::paste], [the mean][mean] and
#' <https://roxygen2.r-lib.org>. Code: `x[1]` and `a b`.
#' Emphasis: *one* and **two**. A literal percent sign: 50% of 100%.
#'
#' @details
#' ## A subsection
#' 1. first
#' 2. second
#'
#' | genre   | loans |
#' | ------- | ----: |
#' | Fiction |    12 |
#'
#' ```
#' x <- 1:3
#' sum(x)
#' ```
#'
#' Evaluated while documenting: 6 * 7 is `r 6 * 7`.
#'
#' @param x Anything.
#' @examplesIf interactive()
#' browseURL("https://roxygen2.r-lib.org")
#' @export
md_forms <- function(x) NULL
)", file.path(pkg, "R", "md.R"))

invisible(capture.output(roxygen2::roxygenise(pkg), type = "message"))
cat(readLines(file.path(pkg, "man", "md_forms.Rd")), sep = "\n")
#> % Generated by roxygen2: do not edit by hand
#> % Please edit documentation in R/md.R
#> \name{md_forms}
#> \alias{md_forms}
#> \title{Markdown forms}
#> \usage{
#> md_forms(x)
#> }
#> \arguments{
#> \item{x}{Anything.}
#> }
#> \description{
#> Links: \code{\link[=mean]{mean()}}, \code{\link[stats:median]{stats::median()}}, \link[base:paste]{base::paste}, \link[=mean]{the mean} and
#> \url{https://roxygen2.r-lib.org}. Code: \code{x[1]} and \verb{a b}.
#> Emphasis: \emph{one} and \strong{two}. A literal percent sign: 50\% of 100\%.
#> }
#> \details{
#> \subsection{A subsection}{
#> \enumerate{
#> \item first
#> \item second
#> }\tabular{lr}{
#>    genre \tab loans \cr
#>    Fiction \tab 12 \cr
#> }
#>
#>
#> \if{html}{\out{<div class="sourceCode">}}\preformatted{x <- 1:3
#> sum(x)
#> }\if{html}{\out{</div>}}
#>
#> Evaluated while documenting: 6 * 7 is 42.
#> }
#> }
#> \examples{
#> \dontshow{if (interactive()) withAutoprint(\{ # examplesIf}
#> browseURL("https://roxygen2.r-lib.org")
#> \dontshow{\}) # examplesIf}
#> }

Read the output against the input. [mean()] became \code{\link[=mean]{mean()}}, a link to the topic that has the alias mean; [stats::median()] named the package, \code{\link[stats:median]{stats::median()}}; [base::paste], without parentheses, links to the topic without the code font; [the mean][mean] gave a link with other text; and <https://...> became \url{}. Inline code became \code{x[1]} when it parses as R code and \verb{a b} when it does not, which is the macro that Rd defines for literal text. Emphasis and strong became \emph{} and \strong{}, the percent signs were escaped for you (50\%), a heading under @details made a \subsection (a level 1 heading makes a top-level \section), the numbered list made an \enumerate, the table made a \tabular and the fenced code block a \preformatted with HTML wrappers. The inline code `r 6 * 7` was evaluated while the package was being documented and its value, 42, is in the Rd text. Since roxygen2 8.0.0 the inline form `Rd expr` writes a \Sexpr[stage=render,results=rd] instead, which is evaluated each time the page is displayed.

Two rules avoid the common surprises. A square bracket that is not meant as a link has to be escaped in the comment (\\[), because markdown reads [word] as a link to the topic word and roxygen2 reports a link it cannot resolve. Markdown features that Rd cannot express (block quotes, inline HTML, horizontal rules) give a warning or an error. The last tag, @examplesIf interactive(), produced the three lines at the end: the example is wrapped in an if that is hidden from the reader by \dontshow, and withAutoprint() makes the code print as in a session. A package that has not turned markdown on writes Rd by hand in its comments, and the tag @md (or @noMd) switches it on (or off) for a single block; the script of a later section shows @md.

Parameters and returns

Every argument of every exported function needs an @param entry, and R CMD check reports a WARNING otherwise: the steps "checking for code/documentation mismatches" and "checking Rd \usage sections" compare the arguments in the usage with the \arguments section, and report arguments that are undocumented and arguments that are documented but do not exist. @param takes one or several names (@param member,days), the description may use markdown, and a description that applies to many functions is documented once and inherited, which is the subject of the next section. @returns documents the value: what class it has, what its length is, and whether it is returned invisibly. The tidyverse project’s own conventions (community material, not part of R) ask every function to say what it returns, because \value is the first thing a reader of the page checks. The shelf function overdue_loans() documents the days_out column it adds, loan_summary() the four columns it returns, and print.book() says that it returns its argument invisibly.

Reusing documentation

Documenting the same argument ten times is both tedious and a source of inconsistency, and roxygen2’s reuse tags exist to avoid it. The script builds a throwaway package with six blocks: overdue() documents loans, today, ... and the return value and has a section and a family; overdue_rate() inherits all of it with @inheritParams, @inheritSection, @inherit and @inheritDotParams; format_loan() and print_loan() share one page with @describeIn; squish_loan() is an internal helper with @keywords internal; and hidden_helper() has @noRd. The Rd files it generated for overdue_rate() and format_loan() are printed.

# Reusing documentation: @inheritParams, @inheritSection, @inherit,
# @inheritDotParams, @family, @seealso, @aliases, @concept, @describeIn and
# @keywords internal, in a throwaway package.
pkg <- file.path(tempdir(), "reuse")
dir.create(file.path(pkg, "R"), recursive = TRUE)
writeLines(c(
  "Package: reuse",
  "Title: Reusing Documentation",
  "Version: 0.0.1",
  "Description: Shows the roxygen2 tags that copy documentation.",
  "License: MIT",
  "Encoding: UTF-8",
  "Config/roxygen2/markdown: TRUE"
), file.path(pkg, "DESCRIPTION"))

writeLines(r"(#' Overdue loans
#'
#' @param loans A data frame of loans.
#' @param today Reference date for loans that are still out.
#' @param ... Further arguments passed to [utils::head()].
#' @returns A data frame with the overdue rows.
#' @section Loan period:
#' A loan is overdue after 21 days.
#' @family loan analyses
#' @concept overdue
#' @export
overdue <- function(loans, today, ...) NULL

#' Overdue rate
#'
#' @inheritParams overdue
#' @inheritSection overdue Loan period
#' @inherit overdue returns
#' @inheritDotParams utils::head n
#' @param by The column to group by.
#' @family loan analyses
#' @seealso [overdue()]
#' @aliases overdue_ratio
#' @export
overdue_rate <- function(loans, today, by, ...) NULL

#' Format a loan
#'
#' @param x A loan.
#' @export
format_loan <- function(x) NULL

#' @describeIn format_loan Print a formatted loan.
#' @export
print_loan <- function(x) NULL

#' Helper that users do not need
#' @keywords internal
#' @param x Anything.
squish_loan <- function(x) x

#' Not documented as a page
#' @param x Anything.
#' @noRd
hidden_helper <- function(x) x
)", file.path(pkg, "R", "loans.R"))

invisible(capture.output(roxygen2::roxygenise(pkg), type = "message"))
list.files(file.path(pkg, "man"))
#> [1] "format_loan.Rd"  "overdue_rate.Rd" "overdue.Rd"      "squish_loan.Rd"
rd <- \(name) cat(readLines(file.path(pkg, "man", name)), sep = "\n")
rd("overdue_rate.Rd")
#> % Generated by roxygen2: do not edit by hand
#> % Please edit documentation in R/loans.R
#> \name{overdue_rate}
#> \alias{overdue_rate}
#> \alias{overdue_ratio}
#> \title{Overdue rate}
#> \usage{
#> overdue_rate(loans, today, by, ...)
#> }
#> \arguments{
#> \item{loans}{A data frame of loans.}
#>
#> \item{today}{Reference date for loans that are still out.}
#>
#> \item{by}{The column to group by.}
#>
#> \item{...}{
#>   Arguments passed on to \code{\link[utils:head]{utils::head}}
#>   \describe{
#>     \item{\code{n}}{an integer vector of length up to \code{dim(x)} (or 1,
#>     for non-dimensioned objects).  A \code{logical} is silently coerced to
#>     integer.  Values specify the indices to be
#>     selected in the corresponding dimension (or along the length) of the
#>     object. A positive value of \code{n[i]} includes the first/last
#>     \code{n[i]} indices in that dimension, while a negative value
#>     excludes the last/first \code{abs(n[i])}, including all remaining
#>     indices. \code{NA} or non-specified values (when \code{length(n) <
#>       length(dim(x))}) select all indices in that dimension. Must
#>     contain at least one non-missing value.}
#>   }}
#> }
#> \value{
#> A data frame with the overdue rows.
#> }
#> \description{
#> Overdue rate
#> }
#> \section{Loan period}{
#>
#> A loan is overdue after 21 days.
#> }
#>
#> \seealso{
#> \code{\link[=overdue]{overdue()}}
#>
#> Other loan analyses:
#> \code{\link[=overdue]{overdue()}}
#> }
#> \concept{loan analyses}
rd("format_loan.Rd")
#> % Generated by roxygen2: do not edit by hand
#> % Please edit documentation in R/loans.R
#> \name{format_loan}
#> \alias{format_loan}
#> \alias{print_loan}
#> \title{Format a loan}
#> \usage{
#> format_loan(x)
#>
#> print_loan(x)
#> }
#> \arguments{
#> \item{x}{A loan.}
#> }
#> \description{
#> Format a loan
#> }
#> \section{Functions}{
#> \itemize{
#> \item \code{print_loan()}: Print a formatted loan.
#>
#> }}
# An internal page keeps its topic but is left out of the indices.
squish <- readLines(file.path(pkg, "man", "squish_loan.Rd"))
grep("^.(name|alias|keyword)", squish, value = TRUE)
#> [1] "\\name{squish_loan}"  "\\alias{squish_loan}" "\\keyword{internal}"

overdue_rate() documents only by; the other three arguments came from overdue(). The ... was documented from utils::head() (the tag @inheritDotParams utils::head n names the one argument wanted) and the generated text says "Arguments passed on to" the function; the section "Loan period" and the value came over with @inheritSection and @inherit overdue returns. @family loan analyses on both blocks added "Other loan analyses:" to See Also on both pages, each linking to the other, and a \concept with the family name. @aliases overdue_ratio added an alias to the page, so help("overdue_ratio") finds it. @describeIn merged print_loan() into the page of format_loan() and described it in a "Functions" section; the roxygen2 documentation (and the 7.3.0 release) suggest @rdname, which gives the merged functions equal status and the usage of each, and is what shelf uses for book(), new_book() and validate_book(). The page for the internal helper was written but keeps only the internal keyword and no concept, so it is left out of the index of topics, and @noRd wrote no file at all (there are four Rd files for six blocks).

Three behaviours to know. @inheritParams copies only the arguments that the function has and does not document itself, and since roxygen2 8.0.0 it can be given the names of the arguments to take or to leave out (@inheritParams foo x y and @inheritParams foo -z). The source can be a function in another package (@inheritParams stats::lm), which needs that package to be installed when the documentation is generated. And @family needs the pages to exist in the same package: the "Other …​" list is made of the pages with the same family.

The markdown forms above are the usual way to link. For a topic in the same package [book()] links the alias book; for another package [pkg::fun()] writes a link with that package as an anchor, and the package should be listed in Imports or Suggests of DESCRIPTION, because the check for cross-references needs it ("Always list the linked package as a dependency", in the roxygen2 message that the first run of document() printed). Since roxygen2 8.0.0 all generated links share one code path, links to external packages use the topic alias as the anchor (which removed the R CMD check note about non-topic package-anchored links), and infix operators such as [%in%] can be linked. A link to a class is [Book-class] for S4, [book] for S3 (when the page’s alias is the class name), and [Book] for S7. The alias lists of the shelf pages show that one page often serves several topics; help.search() finds them by title, alias, concept and keyword.

Namespace tags

The same comments write the NAMESPACE. @export is the one every package uses, and its meaning depends on what it is on: a function gives export(name), an S3 method gives S3method(generic,class), an S4 class gives exportClasses(). The rule for S3 methods is strict: always export them, even when the generic is internal, since exporting is what registers the method; a method that is not exported is not found by dispatch in the installed package, although devtools::load_all() hides that mistake by exposing every function. roxygen2 warns about a method without a tag, as the "Messages" section below shows, and @exportS3Method NULL silences the warning for a method that is registered another way. For a method of a generic that lives in another package there are two ways: import the generic with @importFrom pkg generic and @export the method, or write @exportS3Method pkg::generic, which registers the method when the other package is loaded and needs no import. The shelf NAMESPACE, written by roxygen2 from the tags on format.book(), print.book(), summary.book() and print.summary.book() and on the ten exported functions, is this file:

# Generated by roxygen2: do not edit by hand

S3method(format,book)
S3method(print,book)
S3method(print,summary.book)
S3method(summary,book)
export(book)
export(clean_books)
export(clean_loans)
export(is_valid_isbn13)
export(loan_summary)
export(loans_per_month)
export(new_book)
export(normalise_isbn)
export(overdue_loans)
export(validate_book)

The first line is the roxygen2 header, then the S3 methods and then the functions, in alphabetical order. The directives are explained in Packages, Libraries and Namespaces; the point here is that no one edits this file: change a tag and run devtools::document(). A package that imports a function writes @importFrom pkg fun in the block of the function that uses it (or once in a package-level file), and roxygen2 8.1.1 writes the double-quoted form for non-syntactic names such as model<- to work around a base R bug in importFrom() directives.

Other tags: files, templates and generated Rd

A package whose files must be sourced in a given order (S4 classes that extend classes defined in other files) writes @include a-base.R and roxygen2 sets the Collate field of DESCRIPTION. A section shared by many pages can be written once in a file in man-roxygen/ and inserted with @template and @templateVar, although the reference says templates are superseded by inline R code. @evalRd runs R code and inserts the Rd it returns, which is the escape hatch for pages that have to be generated. The script builds a package that uses all of these and prints the DESCRIPTION that roxygen2 completed and the two Rd files.

# More tags: @include (Collate), @template, @evalRd and @md.
pkg <- file.path(tempdir(), "other")
dir.create(file.path(pkg, "R"), recursive = TRUE)
dir.create(file.path(pkg, "man-roxygen"))
writeLines(c(
  "Package: other",
  "Title: More Tags",
  "Version: 0.0.1",
  "Description: Shows a few more roxygen2 tags.",
  "License: MIT",
  "Encoding: UTF-8"
), file.path(pkg, "DESCRIPTION"))

# A template: a file in man-roxygen/ with roxygen text, used by @template.
writeLines(r"(#' @section Loan period:
#' A loan is overdue after <%= days %> days.
)", file.path(pkg, "man-roxygen", "period.R"))

writeLines(r"(#' Base function
#' @export
base_fn <- function() 1
)", file.path(pkg, "R", "a-base.R"))

writeLines(r"(#' @include a-base.R
NULL

#' Overdue
#'
#' Plain Rd here, markdown is off: \code{x} and \emph{y}.
#' @template period
#' @templateVar days 21
#' @evalRd paste0("\\note{This page lists ", length(letters), " letters.}")
#' @export
overdue <- function() base_fn()

#' Markdown on for one block
#'
#' This has `code`, and *emphasis*.
#' @md
#' @export
md_block <- function() 1
)", file.path(pkg, "R", "b-overdue.R"))

invisible(capture.output(roxygen2::roxygenise(pkg), type = "message"))
cat(readLines(file.path(pkg, "DESCRIPTION")), sep = "\n")
#> Package: other
#> Title: More Tags
#> Version: 0.0.1
#> Description: Shows a few more roxygen2 tags.
#> License: MIT
#> Encoding: UTF-8
#> Config/roxygen2/version: 8.1.1
#> Collate:
#>     'a-base.R'
#>     'b-overdue.R'
cat(readLines(file.path(pkg, "man", "overdue.Rd")), sep = "\n")
#> % Generated by roxygen2: do not edit by hand
#> % Please edit documentation in R/b-overdue.R
#> \name{overdue}
#> \alias{overdue}
#> \title{Overdue}
#> \usage{
#> overdue()
#> }
#> \description{
#> Plain Rd here, markdown is off: \code{x} and \emph{y}.
#> }
#> \section{Loan period}{
#>
#> A loan is overdue after 21 days.
#> }
#>
#> \note{This page lists 26 letters.}
cat(readLines(file.path(pkg, "man", "md_block.Rd")), sep = "\n")
#> % Generated by roxygen2: do not edit by hand
#> % Please edit documentation in R/b-overdue.R
#> \name{md_block}
#> \alias{md_block}
#> \title{Markdown on for one block}
#> \usage{
#> md_block()
#> }
#> \description{
#> This has \code{code}, and \emph{emphasis}.
#> }

Collate: was added to DESCRIPTION with the two files in the order the @include tag demanded. The template inserted a section "Loan period" in which <%= days %> was replaced by 21, @evalRd added a \note whose text was computed (26 letters), and the third block shows @md: the whole package has markdown off, so the first function is written in plain Rd (\code{x}, \emph{y}) and the second, with @md, is translated from markdown.

Messages roxygen2 prints

roxygen2 reports mistakes as it documents, and the reports are easy to miss in a long log. The script documents a package with four common ones: a one-line tag that spans two lines, an unknown tag, an S3 method that is not exported, and a @param for an argument that does not exist. It also has a paragraph that begins with a number and a full stop, which markdown reads as a list.

# Messages roxygen2 prints for common mistakes in a throwaway package.
pkg <- file.path(tempdir(), "warn")
dir.create(file.path(pkg, "R"), recursive = TRUE)
writeLines(c(
  "Package: warn",
  "Title: Mistakes",
  "Version: 0.0.1",
  "Description: Shows roxygen2 messages.",
  "License: MIT",
  "Encoding: UTF-8",
  "Config/roxygen2/markdown: TRUE"
), file.path(pkg, "DESCRIPTION"))

writeLines(r"(#' Two lines for a one-line tag
#'
#' The edition is on page
#' 1. Clearly not a list, but commonmark makes one.
#' @concept first
#'   second
#' @param y An argument that does not exist
#' @param x Fine.
#' @export
one <- function(x) x

#' A method that is not exported
#'
#' @param x Anything.
#' @param ... Not used.
print.one <- function(x, ...) x

#' Unknown tag
#' @retruns A value.
#' @param x Anything.
two <- function(x) x
)", file.path(pkg, "R", "bad.R"))

msg <- capture.output(roxygen2::roxygenise(pkg), type = "message")
sub(pkg, "~/warn", msg, fixed = TRUE)
#>  [1] "First time using roxygen2. Upgrading automatically..."
#>  [2] "ℹ Setting Config/roxygen2/version to \"8.1.1\""
#>  [3] "ℹ Loading warn"
#>  [4] "✖ bad.R:5: @concept must be only 1 line long, not 2."
#>  [5] "ℹ The first line is \"first\""
#>  [6] "✖ bad.R:19: @retruns is not a known tag."
#>  [7] "✖ bad.R:16: S3 method `print.one` needs @export or @exportS3Method tag."
#>  [8] "Writing 'NAMESPACE'"
#>  [9] "Writing 'one.Rd'"
#> [10] "Writing 'print.one.Rd'"
#> [11] "Writing 'two.Rd'"

# The line that began with 1. became a list item, and the @param for the
# argument y that does not exist was written to the page anyway.
cat(readLines(file.path(pkg, "man", "one.Rd")), sep = "\n")
#> % Generated by roxygen2: do not edit by hand
#> % Please edit documentation in R/bad.R
#> \name{one}
#> \alias{one}
#> \title{Two lines for a one-line tag}
#> \usage{
#> one(x)
#> }
#> \arguments{
#> \item{x}{Fine.}
#>
#> \item{y}{An argument that does not exist}
#> }
#> \description{
#> The edition is on page
#> \enumerate{
#> \item Clearly not a list, but commonmark makes one.
#> }
#> }
#> \concept{first
#>   second}

Each message starts with a cross, names the file and the line of the block, and says what is wrong: @concept takes one line (the check is new in roxygen2 8.0.0 and covers the tags listed above), @retruns is not a known tag, and print.one is a method without @export. Two mistakes passed without a message. The line that began with 1. became the item of an \enumerate, because commonmark reads it as a list; the fix is to rephrase the sentence or to escape the full stop. And the @param y for an argument that does not exist was written to the page anyway (\item{y}{An argument that does not exist}); that one is found by R CMD check, in the step shown next.

Checking the documentation

The loop of a package author is edit, devtools::document(), read the warnings, check. devtools::check_man() is the shortcut for the documentation part: it documents the package and runs the Rd checks of R CMD check (tools::checkDocFiles() and friends) without the rest. The script breaks a copy of shelf on purpose in three ways, a new argument without a tag, a tag for an argument that does not exist with a link to a missing topic, and an exported function with no page, and shows what each tool reports.

# The quick loop: document the package and check only its documentation.
# Here on a copy of the package that is broken on purpose, in three ways.
src <- normalizePath(".")
tmp <- file.path(tempdir(), "copy")
dir.create(tmp)
invisible(file.copy(src, tmp, recursive = TRUE))
pkg <- file.path(tmp, basename(src))

f <- file.path(pkg, "R", "loans.R")
code <- readLines(f)

# 1. A new argument without a @param.
code <- sub("^loans_per_month <- function\\(loans\\) \\{",
            "loans_per_month <- function(loans, by = \"month\") {", code)

# 2. A @param for an argument that does not exist, with a link to a topic that
#    does not exist, in the block of loan_summary().
at <- which(code == "#' @inheritParams overdue_loans")[[1L]]
param <- "#' @param period Not an argument, see [no_such_topic()]."
code <- append(code, param, after = at)
writeLines(code, f)

# document() and the Rd checks of R CMD check in one call.
devtools::check_man(pkg)
#> ℹ Updating shelf documentation
#> ℹ Loading shelf
#> shelf 0.1.0: loan period is 21 days
#> ✖ loans.R:47: @param Could not resolve link to topic "no_such_topic" in the
#>   dependencies or base packages..
#> ℹ If you haven't documented "no_such_topic" yet, or just changed its name, this
#>   is normal. Once "no_such_topic" is documented, this warning goes away.
#> ℹ Make sure that the name of the topic is spelled correctly.
#> ℹ Always list the linked package as a dependency.
#> ℹ Alternatively, you can fully qualify the link with a package name.
#> Writing 'loan_summary.Rd'
#> Writing 'loans_per_month.Rd'
#> ℹ Checking documentation...
#> Missing link(s) in Rd file 'loan_summary.Rd':
#>   ‘no_such_topic’
#>
#> See section 'Cross-references' in the 'Writing R Extensions' manual.
#> Documented arguments not in \usage in Rd file 'loan_summary.Rd':
#>   ‘period’
#>
#> Undocumented arguments in Rd file 'loans_per_month.Rd'
#>   ‘by’
#>

# 3. An exported function with no help page at all.
cat("undocumented_fn <- function() 1\n", file = f, append = TRUE)
cat("export(undocumented_fn)\n", file = file.path(pkg, "NAMESPACE"),
    append = TRUE)
tools::undoc(dir = pkg)
#> Undocumented code objects:
#>   ‘undocumented_fn’

The output has three parts. The roxygen2 warning is the cross line about the unresolved link, produced while documenting. The Rd checks come after "Checking documentation…​": "Missing link(s)" for the link that cannot be resolved, "Documented arguments not in \usage" for period, which loan_summary() does not have, and "Undocumented arguments" for the new argument by of loans_per_month(). The last call, tools::undoc(), is the check behind "checking for missing documentation entries": it lists the exported objects that have no help page. tools::codoc() compares the usage with the code, and tools::checkDocFiles() the arguments with the \arguments sections. The same broken copy under the real tools, R CMD build and R CMD check, fails three steps; the platform lines at the top of the log are left out and only the documentation steps are shown:

# The same broken copy of the package, checked with R CMD build and R CMD check.
src <- normalizePath(".")
tmp <- file.path(tempdir(), "copy")
dir.create(tmp)
invisible(file.copy(src, tmp, recursive = TRUE))
pkg <- file.path(tmp, basename(src))

f <- file.path(pkg, "R", "loans.R")
code <- readLines(f)
code <- sub("^loans_per_month <- function\\(loans\\) \\{",
            "loans_per_month <- function(loans, by = \"month\") {", code)
at <- which(code == "#' @inheritParams overdue_loans")[[1L]]
param <- "#' @param period Not an argument, see [no_such_topic()]."
code <- append(code, param, after = at)
code <- c(code, "undocumented_fn <- function() 1")
writeLines(code, f)
invisible(capture.output(roxygen2::roxygenise(pkg), type = "message"))
cat("export(undocumented_fn)\n", file = file.path(pkg, "NAMESPACE"),
    append = TRUE)

# R CMD build and R CMD check run in the directory that holds the package.
r_cmd <- \(...) {
  withr::with_dir(tmp, system2("R", c("CMD", ...),
                               stdout = TRUE, stderr = TRUE))
}
invisible(r_cmd("build", "--no-build-vignettes", "shelf"))
res <- r_cmd("check", "--no-manual", "--ignore-vignettes", "--no-tests",
             "shelf_0.1.0.tar.gz")

# The platform lines at the top of the log are left out.
first <- grep("checking Rd files", res)
last <- grep("checking Rd contents", res)
res[first:last]
#>  [1] "* checking Rd files ... OK"
#>  [2] "* checking Rd metadata ... OK"
#>  [3] "* checking Rd cross-references ... WARNING"
#>  [4] "Missing link(s) in Rd file 'loan_summary.Rd':"
#>  [5] "  ‘no_such_topic’"
#>  [6] ""
#>  [7] "See section 'Cross-references' in the 'Writing R Extensions' manual."
#>  [8] "* checking for missing documentation entries ... WARNING"
#>  [9] "Undocumented code objects:"
#> [10] "  ‘undocumented_fn’"
#> [11] "All user-level objects in a package should have documentation entries."
#> [12] "See chapter ‘Writing R documentation files’ in the ‘Writing R"
#> [13] "Extensions’ manual."
#> [14] "* checking for code/documentation mismatches ... OK"
#> [15] "* checking Rd \\usage sections ... WARNING"
#> [16] "Documented arguments not in \\usage in Rd file 'loan_summary.Rd':"
#> [17] "  ‘period’"
#> [18] ""
#> [19] "Undocumented arguments in Rd file 'loans_per_month.Rd'"
#> [20] "  ‘by’"
#> [21] ""
#> [22] "Functions with \\usage entries need to have the appropriate \\alias"
#> [23] "entries, and all their arguments documented."
#> [24] "The \\usage entries must correspond to syntactically valid R code."
#> [25] "See chapter ‘Writing R documentation files’ in the ‘Writing R"
#> [26] "Extensions’ manual."
#> [27] "* checking Rd contents ... OK"
grep("^Status", res, value = TRUE)
#> [1] "Status: 3 WARNINGs"

Each of the three mistakes is a WARNING, and the package would be refused on CRAN with any of them. The passing log of the real package, shown at the start of this page, has all of those steps as OK. Do not run these checks by hand each time: devtools::check() does them, and the lintr and testthat pages show the other parts of the loop.

Documenting data, the package and classes

Functions are the easy case. Three other kinds of object need their own habits: data sets, which are not exported and are documented by name; the package itself, which has an overview page; and classes of the four object systems, which are documented through their constructors, generics and methods.

Data sets

A data set is not a function and is not exported: it lives in data/ (the shelf package builds data/books.rda and data/loans.rda from the CSV files in data-raw/ with usethis::use_data()) and it is documented by a block above a string with the name of the data set. The block for books is in R/data.R; the data are made by the script data-raw/books.R, which the package keeps (and leaves out of the tarball through .Rbuildignore) so that the data can be rebuilt. An excerpt of ~/shelf/R/data.R:

#' The shelf catalogue
#'
#' Sixteen books of a small library, as they were typed into the catalogue:
#' titles with stray blanks and inconsistent case, two spellings of one
#' author, genres in several spellings, hyphenated and bare ISBNs (one
#' missing, one with a wrong check digit) and missing years and page counts.
#' [clean_books()] tidies it.
#'
#' @format A data frame with 16 rows and 6 columns:
#' \describe{
#'   \item{title}{Title (character).}
#'   \item{author}{Author (character).}
#'   \item{isbn}{ISBN-13, hyphenated or bare (character).}
#'   \item{year}{Publication year (integer).}
#'   \item{genre}{Genre (character).}
#'   \item{pages}{Number of pages (integer).}
#' }
#' @source Synthetic data generated for this package; the ISBNs have valid
#'   check digits but are not the ISBNs of real books.
#' @family datasets
"books"

The two tags that are specific to data are @format, which describes the structure (for a data frame, one entry per column, as a \describe list that markdown cannot yet write, so this is one of the few places where Rd is written inside a roxygen2 comment), and @source, which says where the data come from. roxygen2 turns the block into the Rd file below. Note that the usage is just books, the name of the object: that is the form for a package with LazyData: true (the shelf DESCRIPTION has it); for a package without lazy data the usage is written data(books), as roxygen2 8.0.0 does. Documenting a data set with its object (x <- 1:10 followed by a block) no longer adds \docType{data}, \keyword{datasets} and a format section since roxygen2 8.0.0; the quoted name is the supported form, and it still writes them.

% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/data.R
\docType{data}
\name{books}
\alias{books}
\title{The shelf catalogue}
\format{
A data frame with 16 rows and 6 columns:
\describe{
\item{title}{Title (character).}
\item{author}{Author (character).}
\item{isbn}{ISBN-13, hyphenated or bare (character).}
\item{year}{Publication year (integer).}
\item{genre}{Genre (character).}
\item{pages}{Number of pages (integer).}
}
}
\source{
Synthetic data generated for this package; the ISBNs have valid
check digits but are not the ISBNs of real books.
}
\usage{
books
}
\description{
Sixteen books of a small library, as they were typed into the catalogue:
titles with stray blanks and inconsistent case, two spellings of one
author, genres in several spellings, hyphenated and bare ISBNs (one
missing, one with a wrong check digit) and missing years and page counts.
\code{\link[=clean_books]{clean_books()}} tidies it.
}
\seealso{
Other datasets:
\code{\link{loans}}
}
\concept{datasets}
\keyword{datasets}

The page is what a user sees with ?books. The conversion of the same file to text, with the \describe items that R 4.6.0 renders with a line break after each label:

# How the page of a data set renders in a terminal.
tools::Rd2txt("man/books.Rd", options = list(underline_titles = FALSE))
#> The shelf catalogue
#>
#> Description:
#>
#>      Sixteen books of a small library, as they were typed into the
#>      catalogue: titles with stray blanks and inconsistent case, two
#>      spellings of one author, genres in several spellings, hyphenated
#>      and bare ISBNs (one missing, one with a wrong check digit) and
#>      missing years and page counts. ‘clean_books()’ tidies it.
#>
#> Usage:
#>
#>      books
#>
#> Format:
#>
#>      A data frame with 16 rows and 6 columns:
#>
#>      title
#>           Title (character).
#>
#>      author
#>           Author (character).
#>
#>      isbn
#>           ISBN-13, hyphenated or bare (character).
#>
#>      year
#>           Publication year (integer).
#>
#>      genre
#>           Genre (character).
#>
#>      pages
#>           Number of pages (integer).
#>
#> Source:
#>
#>      Synthetic data generated for this package; the ISBNs have valid
#>      check digits but are not the ISBNs of real books.
#>
#> See Also:
#>
#>      Other datasets: ‘loans’
#>

The manual’s rules for data pages are two. One data object per Rd file (the usage is books or data(books)), and the keyword datasets. The script that builds the data is a documentation file in a sense too: it is the record of where the data come from, and usethis::use_data_raw() creates it.

# Build the `books` and `loans` datasets from the CSV files in data-raw/.
books <- read.csv("data-raw/books.csv")
loans <- read.csv("data-raw/loans.csv", colClasses = "character")
loans$loan_date <- as.Date(loans$loan_date, format = "%Y-%m-%d")
loans$return_date <- as.Date(loans$return_date, format = "%Y-%m-%d")
usethis::use_data(books, loans, overwrite = TRUE)

# Internal data: the ISBN-13 check-digit weights used by is_valid_isbn13().
isbn_weights <- rep(c(1L, 3L), length.out = 13L)
usethis::use_data(isbn_weights, internal = TRUE, overwrite = TRUE)

Internal data, the isbn_weights vector that is_valid_isbn13() uses, goes to R/sysdata.rda with internal = TRUE and is not documented, since users never see it. R CMD check examines the data: "checking contents of 'data' directory", "checking data for non-ASCII characters", "checking LazyData", "checking data for ASCII and uncompressed saves" and "checking R/sysdata.rda", all OK in the log at the start of this page.

The package itself: "_PACKAGE", README and pkgdown helpers

A package can have an overview page, with the alias pkgname-package, so that ?shelf and package?shelf open it. In roxygen2 the page is written by documenting the string "_PACKAGE": the title, the description and the author section are generated from DESCRIPTION, with the roles of Authors@R, and the block can add a @keywords internal (to keep the page out of the topic index) and the ## usethis namespace: start comments that usethis writes for imports. usethis::use_package_doc() creates the file R/shelf-package.R; usethis::use_readme_rmd() creates a README.Rmd (a README with code that is rendered by devtools::build_readme()), and usethis::use_pkgdown() writes _pkgdown.yml, adds docs to .gitignore and the pkgdown files to .Rbuildignore. The script runs all three on a copy of shelf, in a temporary directory, and then documents the copy.

# The usethis helpers that set up documentation files, run on a copy.
src <- normalizePath(".")
tmp <- file.path(tempdir(), "uc")
dir.create(tmp)
invisible(file.copy(src, tmp, recursive = TRUE))
pkg <- file.path(tmp, basename(src))

msg <- capture.output(
  usethis::with_project(pkg, {
    usethis::use_pkgdown()
    usethis::use_readme_rmd(open = FALSE)
    usethis::use_package_doc(open = FALSE)
  }),
  type = "message"
)
msg[!grepl("active project", msg)]
#>  [1] "✔ Adding \"^_pkgdown\\\\.yml$\", \"^docs$\", and \"^pkgdown$\" to '.Rbuildignore'."
#>  [2] "✔ Adding \"docs\" to '.gitignore'."
#>  [3] "✔ Writing '_pkgdown.yml'."
#>  [4] "☐ Edit '_pkgdown.yml'."
#>  [5] "✔ Writing 'README.Rmd'."
#>  [6] "✔ Adding \"^README\\\\.Rmd$\" to '.Rbuildignore'."
#>  [7] "☐ Update 'README.Rmd' to include installation instructions."
#>  [8] "☐ Use `devtools::build_readme()` to render 'README.Rmd'."
#>  [9] "✔ Writing 'R/shelf-package.R'."
#> [10] "☐ Run `devtools::document()` to update package-level documentation."

cat(readLines(file.path(pkg, "_pkgdown.yml")), sep = "\n")
#> url: ~
#> template:
#>   bootstrap: 5
#>
cat(readLines(file.path(pkg, "R", "shelf-package.R")), sep = "\n")
#> #' @keywords internal
#> "_PACKAGE"
#>
#> ## usethis namespace: start
#> ## usethis namespace: end
#>
#> ## mockable bindings: start
#> ## mockable bindings: end
#> NULL
head(readLines(file.path(pkg, "README.Rmd")), 4)
#> [1] "---"                     "output: github_document"
#> [3] "---"                     ""

# The package-level page that roxygen2 writes for "_PACKAGE".
invisible(capture.output(roxygen2::roxygenise(pkg), type = "message"))
cat(readLines(file.path(pkg, "man", "shelf-package.Rd")), sep = "\n")
#> % Generated by roxygen2: do not edit by hand
#> % Please edit documentation in R/shelf-package.R
#> \docType{package}
#> \name{shelf-package}
#> \alias{shelf}
#> \alias{shelf-package}
#> \title{shelf: Tools for a Small Library Catalogue}
#> \description{
#> Cleans a small library catalogue and its loans table, validates ISBN-13 strings, and reports overdue loans and overdue rates by genre. Ships the example 'books' and 'loans' datasets used by the documentation.
#> }
#> \author{
#> \strong{Maintainer}: Shelf Maintainers \email{maintainer@example.com} [copyright holder]
#>
#> Authors:
#> \itemize{
#>   \item Shelf Maintainers \email{maintainer@example.com} [copyright holder]
#> }
#>
#> }
#> \keyword{internal}

The generated page shelf-package.Rd has the alias shelf-package and also shelf (roxygen2 adds the alias of the package name unless a function of that name exists), the title and the description of DESCRIPTION, and an author section that lists the maintainer first and then the authors; a person with the roles aut and cre is listed in both parts since roxygen2 8.0.0. Here the only person is the maintainer and the copyright holder. The manual advises that a package overview page be short and that "more extensive documentation is better placed into a package vignette".

S3 classes and methods

An S3 class has no definition to document: the constructor is a function, the generics and methods are functions, and the class is described by the constructor’s page. The roxygen2 article gives the working rules: document a generic like any function and say that it is a generic, document a class by its constructor, and document a method only when it has behaviour of its own, with a link back to the generic; always export S3 methods, because exporting registers them. shelf follows them. The constructor family book(), new_book() and validate_book() is one page (the @rdname book tags put the three blocks into the same file), and the four methods of the class are one more page, book-methods, that has no object of its own: the first block is written above NULL with @name book-methods, and the method blocks are @rdname book-methods and @export. This is an excerpt of ~/shelf/R/book.R:

#' Methods for the `book` class
#'
#' `format()` renders a book as `"<title> (<author>, <year>)"`, `print()`
#' shows that line with a `<book>` marker and `summary()` returns a small
#' `summary.book` object with its own print method.
#'
#' @param x,object A `book` object (for `print.summary.book()`, a
#'   `summary.book` object).
#' @param ... Ignored, present for compatibility with the generics.
#' @returns `format.book()` returns a string; `print.book()` and
#'   `print.summary.book()` return their argument invisibly;
#'   `summary.book()` returns a list of class `summary.book` with the
#'   elements `title`, `has_isbn` and `pages`.
#' @examples
#' b <- book("Salt and Static", "Mara Quill", "978-0-01-400145-3", 2018,
#'           "Mystery", 244)
#' format(b)
#' print(b)
#' s <- summary(b)
#' class(s)
#' s
#' @family book class
#' @name book-methods
NULL

#' @rdname book-methods
#' @export
format.book <- function(x, ...) {
  year <- if (is.na(x$year)) "n.d." else x$year
  sprintf("%s (%s, %s)", x$title, x$author, year)
}

#' @rdname book-methods
#' @export
print.book <- function(x, ...) {

The result is the file book-methods.Rd:

% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/book.R
\name{book-methods}
\alias{book-methods}
\alias{format.book}
\alias{print.book}
\alias{summary.book}
\alias{print.summary.book}
\title{Methods for the \code{book} class}
\usage{
\method{format}{book}(x, ...)

\method{print}{book}(x, ...)

\method{summary}{book}(object, ...)

\method{print}{summary.book}(x, ...)
}
\arguments{
\item{x, object}{A \code{book} object (for \code{print.summary.book()}, a
\code{summary.book} object).}

\item{...}{Ignored, present for compatibility with the generics.}
}
\value{
\code{format.book()} returns a string; \code{print.book()} and
\code{print.summary.book()} return their argument invisibly;
\code{summary.book()} returns a list of class \code{summary.book} with the
elements \code{title}, \code{has_isbn} and \code{pages}.
}
\description{
\code{format()} renders a book as \code{"<title> (<author>, <year>)"}, \code{print()}
shows that line with a \verb{<book>} marker and \code{summary()} returns a small
\code{summary.book} object with its own print method.
}
\examples{
b <- book("Salt and Static", "Mara Quill", "978-0-01-400145-3", 2018,
          "Mystery", 244)
format(b)
print(b)
s <- summary(b)
class(s)
s
}
\seealso{
Other book class:
\code{\link[=book]{book()}}
}
\concept{book class}

The usage is written with \method{format}{book}(x, ...), the macro that says “the S3 method of format for class book”: printed pages show it as format(x, ...), as the manual explains, and the checks compare the full name format.book. The five aliases make help("print.book") and help("summary.book") find the page. The See Also line is the @family book class of both pages. And the description of the methods shows the choice between \code{} and \verb{} again: the markdown code that spells out the format string (a string literal) parses as R and became \code{}, while the marker <book> does not parse and became \verb{}. How S3 methods, generics and dispatch work is in S3 Classes and Generics and in S3 Group Generics and Operator Overloading.

The roxygen2 article adds two more things. For a generic with many methods, the doclisting package (not installed here) generates the list of methods in the page of the generic, either at documentation time with inline R code or at run time with an inline Rd code block, so that the list also shows methods registered by other packages. For a method of a generic defined in another package, @exportS3Method pkg::generic registers the method without importing the generic, as the previous section said.

S4 classes and methods

roxygen2 documents S4 without extra tags for the usual cases. A class is documented above setClass(), with @slot for each slot; a generic is documented above setGeneric() like a function; a method above setMethod() is merged into the generic’s page with @rdname, or, for show, into the class’s page. The generated topics follow the naming rules of Writing R Extensions: Book-class for the class and describe,Book-method for the method, and the NAMESPACE receives exportClasses() and exportMethods() directives. The script documents a small S4 version of the book class in a throwaway package and prints the two Rd files and the NAMESPACE.

# Documenting S4: a class with @slot, a generic and its methods.
pkg <- file.path(tempdir(), "s4demo")
dir.create(file.path(pkg, "R"), recursive = TRUE)
writeLines(c(
  "Package: s4demo",
  "Title: Documenting S4",
  "Version: 0.0.1",
  "Description: Shows how roxygen2 documents S4 classes.",
  "License: MIT",
  "Encoding: UTF-8",
  "Imports: methods",
  "Config/roxygen2/markdown: TRUE"
), file.path(pkg, "DESCRIPTION"))

writeLines(r"(#' A book (S4)
#'
#' @slot title Title of the book (a string).
#' @slot author Author (a string).
#' @slot isbn ISBN-13 without hyphens (a string).
#' @slot year Publication year (an integer).
#' @slot genre Genre (a string).
#' @slot pages Number of pages (an integer).
#' @export
setClass("Book", slots = c(
  title = "character", author = "character", isbn = "character",
  year = "integer", genre = "character", pages = "integer"
))

#' Describe an object
#'
#' @param x An object.
#' @returns A string.
#' @export
setGeneric("describe", function(x) standardGeneric("describe"))

#' @rdname describe
#' @export
setMethod("describe", "Book", function(x) x@title)

#' @param object A [Book-class] object.
#' @rdname Book-class
#' @export
setMethod("show", "Book", function(object) {
  cat("<Book>", object@title, "\n")
})
)", file.path(pkg, "R", "book.R"))

invisible(capture.output(roxygen2::roxygenise(pkg), type = "message"))
list.files(file.path(pkg, "man"))
#> [1] "Book-class.Rd" "describe.Rd"
cat(readLines(file.path(pkg, "man", "Book-class.Rd")), sep = "\n")
#> % Generated by roxygen2: do not edit by hand
#> % Please edit documentation in R/book.R
#> \docType{class}
#> \name{Book-class}
#> \alias{Book-class}
#> \alias{show,Book-method}
#> \title{A book (S4)}
#> \usage{
#> \S4method{show}{Book}(object)
#> }
#> \arguments{
#> \item{object}{A \link[=Book-class]{Book} object.}
#> }
#> \description{
#> A book (S4)
#> }
#> \section{Slots}{
#>
#> \describe{
#> \item{\code{title}}{Title of the book (a string).}
#>
#> \item{\code{author}}{Author (a string).}
#>
#> \item{\code{isbn}}{ISBN-13 without hyphens (a string).}
#>
#> \item{\code{year}}{Publication year (an integer).}
#>
#> \item{\code{genre}}{Genre (a string).}
#>
#> \item{\code{pages}}{Number of pages (an integer).}
#> }}
#>
cat(readLines(file.path(pkg, "man", "describe.Rd")), sep = "\n")
#> % Generated by roxygen2: do not edit by hand
#> % Please edit documentation in R/book.R
#> \name{describe}
#> \alias{describe}
#> \alias{describe,Book-method}
#> \title{Describe an object}
#> \usage{
#> describe(x)
#>
#> \S4method{describe}{Book}(x)
#> }
#> \arguments{
#> \item{x}{An object.}
#> }
#> \value{
#> A string.
#> }
#> \description{
#> Describe an object
#> }
cat(readLines(file.path(pkg, "NAMESPACE")), sep = "\n")
#> # Generated by roxygen2: do not edit by hand
#>
#> export(describe)
#> exportClasses(Book)
#> exportMethods(describe)
#> exportMethods(show)

The class page has a Slots section built from the @slot tags, an alias Book-class and, because the show method was added to it with @rdname Book-class, an alias show,Book-method and a usage written with \S4method{show}{Book}(object). The generic page has the alias of each method, describe,Book-method, and lists the method’s usage after the generic’s. Writing R Extensions says that every user-visible S4 class and method should have at least an alias in one of the package’s Rd files, and that a package which only adds methods to a generic defined elsewhere documents its methods, not the generic. The S4 classes themselves are the subject of S4 Classes and Methods; the methods package has promptClass() and promptMethods() for the older, manual route.

R6 classes

An R6 class is an environment of methods and fields built by R6::R6Class(), and the documentation of its members is written inside the class definition, above each member, in comments. roxygen2 produces one page for the class with the fields, the active bindings, an index of the methods and a subsection for each method with its usage, arguments and return value, links to the methods of the superclass and an example if there is one. @field documents a field or an active binding, @description and @param a method, and the method blocks may also carry @returns and @examples. The R6 option of roxygen2 is on by default. The script documents a small R6 class and prints the beginning of the generated page (the file has 94 lines).

# Documenting R6: fields and methods are documented inside the class.
pkg <- file.path(tempdir(), "r6demo")
dir.create(file.path(pkg, "R"), recursive = TRUE)
writeLines(c(
  "Package: r6demo",
  "Title: Documenting R6",
  "Version: 0.0.1",
  "Description: Shows how roxygen2 documents R6 classes.",
  "License: MIT",
  "Encoding: UTF-8",
  "Imports: R6",
  "Config/roxygen2/markdown: TRUE"
), file.path(pkg, "DESCRIPTION"))

writeLines(r"(#' A shelf of books (R6)
#'
#' Keeps the titles on one shelf.
#' @export
Shelf <- R6::R6Class("Shelf",
  public = list(
    #' @field name Name of the shelf.
    name = NULL,

    #' @description
    #' Create a shelf.
    #' @param name Name of the shelf.
    #' @returns A new `Shelf`.
    initialize = function(name) {
      self$name <- name
    },

    #' @description
    #' Add a title.
    #' @param title Title to add.
    #' @returns The shelf, invisibly, so calls can be chained.
    add = function(title) {
      private$titles <- c(private$titles, title)
      invisible(self)
    }
  ),
  active = list(
    #' @field size Number of titles (read-only).
    size = function() length(private$titles)
  ),
  private = list(titles = character())
)
)", file.path(pkg, "R", "shelf.R"))

invisible(capture.output(roxygen2::roxygenise(pkg), type = "message"))
list.files(file.path(pkg, "man"))
#> [1] "Shelf.Rd"
shelf_rd <- readLines(file.path(pkg, "man", "Shelf.Rd"))
length(shelf_rd)
#> [1] 94
cat(head(shelf_rd, 44), sep = "\n")
#> % Generated by roxygen2: do not edit by hand
#> % Please edit documentation in R/shelf.R
#> \name{Shelf}
#> \alias{Shelf}
#> \title{A shelf of books (R6)}
#> \description{
#> Keeps the titles on one shelf.
#> }
#> \section{Public fields}{
#>   \if{html}{\out{<div class="r6-fields">}}
#>   \describe{
#>     \item{\code{name}}{Name of the shelf.}
#>   }
#>   \if{html}{\out{</div>}}
#> }
#> \section{Active bindings}{
#>   \if{html}{\out{<div class="r6-active-bindings">}}
#>   \describe{
#>     \item{\code{size}}{Number of titles (read-only).}
#>   }
#>   \if{html}{\out{</div>}}
#> }
#> \section{Methods}{
#> \subsection{Public methods}{
#>   \itemize{
#>     \item \href{#method-Shelf-initialize}{\code{Shelf$new()}}
#>     \item \href{#method-Shelf-add}{\code{Shelf$add()}}
#>     \item \href{#method-Shelf-clone}{\code{Shelf$clone()}}
#>   }
#> }
#> \if{html}{\out{<hr>}}
#> \if{html}{\out{<a id="method-Shelf-initialize"></a>}}
#> \if{latex}{\out{\hypertarget{method-Shelf-initialize}{}}}
#> \subsection{\code{Shelf$new()}}{
#>   Create a shelf.
#>   \subsection{Usage}{
#>     \if{html}{\out{<div class="r">}}
#>     \preformatted{Shelf$new(name)}
#>     \if{html}{\out{</div>}}
#>   }
#>   \subsection{Arguments}{
#>     \if{html}{\out{<div class="arguments">}}
#>     \describe{
#>       \item{\code{name}}{Name of the shelf.}

The page is a long Rd file whose structure is out of the ordinary: the fields and active bindings are \describe lists inside HTML-only \if{html}{\out{<div>}} wrappers, the methods are \subsection blocks with an anchor (method-Shelf-initialize) so that the index can link to them, and usage is written as Shelf$new(name) for the constructor and obj$method(args) for the others. The same article lists what roxygen2 8.0.0 added: methods added with $set() can be documented with a block above the $set() call; @R6method Class$method documents methods anywhere; @noRd before a method hides it; classes inherit parameter documentation from the overridden method of their superclass and fields from their superclasses; the initialize() method inherits the documentation of the fields of the same name; and @returns works in method blocks. How R6 classes work is in Reference Classes and R6. roxygen2 documents reference classes (from setRefClass()) with @field too.

S7 classes

S7 is a newer object system; roxygen2 8.0.0 added initial support for it, and S7 0.2.2 is described by its authors as somewhat experimental, so the details below are those of these versions and may change. A class is documented like its constructor function, because new_class() returns one: @param documents the arguments, which are the properties, @returns the object, and @prop the properties that are not constructor arguments, such as a read-only computed property. A generic made with new_generic() is documented like any function. A method registered with method(generic, class) <- fn is detected automatically and documented with a usage line of the form "S7 method for class". The script documents all three in a throwaway package, whose .onLoad() calls S7::methods_register() as an S7 package must, and prints the three generated files.

# Documenting S7 (roxygen2 8.x with S7 0.2.2): class, generic and method.
pkg <- file.path(tempdir(), "s7demo")
dir.create(file.path(pkg, "R"), recursive = TRUE)
writeLines(c(
  "Package: s7demo",
  "Title: Documenting S7",
  "Version: 0.0.1",
  "Description: Shows how roxygen2 documents S7 classes.",
  "License: MIT",
  "Encoding: UTF-8",
  "Depends: R (>= 4.3.0)",
  "Imports: S7",
  "Config/roxygen2/markdown: TRUE"
), file.path(pkg, "DESCRIPTION"))

writeLines(r"(#' A book (S7)
#'
#' @param title Title of the book.
#' @param year Publication year.
#' @prop age Age of the book in 2026 (read-only).
#' @returns A `Book` object.
#' @export
Book <- S7::new_class("Book",
  properties = list(
    title = S7::class_character,
    year = S7::class_integer,
    age = S7::new_property(
      S7::class_integer,
      getter = function(self) 2026L - self@year
    )
  )
)

#' Describe an object
#'
#' @param x An object.
#' @param ... Not used.
#' @returns A string.
#' @export
describe <- S7::new_generic("describe", "x")

#' Describe a book
#'
#' The description of a [Book] is its title; see [describe()].
#'
#' @param x A [Book].
#' @param ... Not used.
#' @returns The title.
S7::method(describe, Book) <- function(x, ...) x@title

.onLoad <- function(libname, pkgname) {
  S7::methods_register()
}
)", file.path(pkg, "R", "book.R"))

invisible(capture.output(roxygen2::roxygenise(pkg), type = "message"))
man <- file.path(pkg, "man")
list.files(man)
#> [1] "Book.Rd"                        "describe-s7demo-Book-method.Rd"
#> [3] "describe.Rd"
cat(readLines(file.path(man, "Book.Rd")), sep = "\n")
#> % Generated by roxygen2: do not edit by hand
#> % Please edit documentation in R/book.R
#> \name{Book}
#> \alias{Book}
#> \title{A book (S7)}
#> \usage{
#> Book(title = character(0), year = integer(0))
#> }
#> \arguments{
#> \item{title}{Title of the book.}
#>
#> \item{year}{Publication year.}
#> }
#> \value{
#> A \code{Book} object.
#> }
#> \description{
#> A book (S7)
#> }
#> \section{Additional properties}{
#>
#> \describe{
#> \item{\code{@age}}{Age of the book in 2026 (read-only).}
#> }}
#>
method_rd <- list.files(man, pattern = "method", full.names = TRUE)
cat(readLines(method_rd), sep = "\n")
#> % Generated by roxygen2: do not edit by hand
#> % Please edit documentation in R/book.R
#> \name{describe,s7demo::Book-method}
#> \alias{describe,s7demo::Book-method}
#> \title{Describe a book}
#> \usage{
#> ## S7 method for class <s7demo::Book>
#> \special{describe(x, ...)}
#> }
#> \arguments{
#> \item{x}{A \link{Book}.}
#>
#> \item{...}{Not used.}
#> }
#> \value{
#> The title.
#> }
#> \description{
#> The description of a \link{Book} is its title; see \code{\link[=describe]{describe()}}.
#> }
cat(readLines(file.path(pkg, "NAMESPACE")), sep = "\n")
#> # Generated by roxygen2: do not edit by hand
#>
#> export(Book)
#> export(describe)

The class page has the constructor’s usage (Book(title = character(0), year = integer(0))), the two arguments and an "Additional properties" section for @age; the generic is a plain function page; and the method got a page of its own, named describe,s7demo::Book-method after the generic and the class, with a usage that roxygen2 8.1.1 wraps in \special{} (which removed a "codoc mismatch" note in R CMD check). The NAMESPACE has the two export() lines and nothing for the method, because the method is registered by methods_register() at load time and needs no @export. roxygen2 recognises the walrus := as an assignment since 8.1.1, so Book := new_class("Book") is documented like Book <- new_class("Book"). The S7 package and its objects are described in The S7 Object System.

Vignettes

A help page documents one function. A vignette documents how the functions work together: a long-form guide, written in a literate format in which prose and R code are mixed, whose code is run when the document is built, so that the output it shows is real and a change that breaks it is noticed. R’s name for the idea (and for the document) is "vignette", and it is part of the package system: sources live in vignettes/, R CMD build turns them into HTML or PDF in inst/doc, the installed package has them in its doc directory, vignette() and browseVignettes() find them, and R CMD check runs their code and checks that the outputs are up to date. Sweave is the engine that ships with R, for LaTeX documents with the extension .Rnw; every other engine is provided by a package, so a package with vignettes in another format says which in DESCRIPTION.

The files and the fields

The shelf vignette is an R Markdown file, vignettes/shelf.Rmd, processed by knitr (1.52) and rmarkdown (2.32), which uses Pandoc for the final conversion to HTML. Three things in DESCRIPTION make that work: Suggests: knitr, rmarkdown (the packages must be available when the package is built and checked, and are not needed to use it), VignetteBuilder: knitr (the name of the package that provides the vignette engine; without the field, R CMD check does not know that the package has non-Sweave vignettes) and, in the file itself, a \VignetteEngine line. The file, as usethis::use_vignette() created it and as the package has it, is the following. The vignette: block in the YAML header carries the three lines that R reads (the %\Vignette... lines are comments for R and are ignored by Pandoc), and output: rmarkdown::html_vignette selects an HTML format with a light style and small output that was designed for package vignettes.

---
title: "Analysing library loans with shelf"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Analysing library loans with shelf}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

The shelf package ships a small, deliberately messy library catalogue and
its loans, and the few functions needed to clean them and answer two
questions: which loans are overdue, and how the overdue rate differs by
genre. This vignette walks through that analysis with a fixed reference
date so that its results do not change from one build to the next.

```{r setup}
library(shelf)
today <- as.Date("2026-06-30")
```

## Cleaning the catalogue

```{r clean}
catalogue <- clean_books(books)
head(catalogue, 4)
table(catalogue$genre)
```

## Overdue loans

```{r overdue}
out <- overdue_loans(loans, today = today)
nrow(out)
head(out[order(-out$days_out), ], 5)
```

## Overdue rate by genre

```{r summary}
loan_summary(loans, catalogue, today = today)
```

## Loans per month

```{r months, fig.width = 7, fig.height = 3.5}
monthly <- loans_per_month(loans)
barplot(monthly$loans, names.arg = monthly$month, las = 2,
        main = "Loans per month", ylab = "Loans")
```

The metadata lines are \VignetteIndexEntry{title} (the title in the index of vignettes; it need not equal the document title), \VignetteEngine{knitr::rmarkdown} (package and engine), and \VignetteEncoding{UTF-8} (the encoding of the source, required when the file has non-ASCII characters; without a declaration the encoding of the package is assumed). Others that may be seen are \VignetteDepends (packages needed to build the vignette; since R 4.3.0 tools::buildVignettes() has an argument skip that R CMD check uses to skip, and note, vignettes whose dependencies are not available) and \VignetteKeyword. They are read at installation time into the file Meta/vignette.rds, and tools::vignetteInfo() reads them from a source file. The first chunk, include = FALSE, sets collapse = TRUE and comment = "#>" for all chunks, which is why the output of the vignette has the same #> look as the outputs on this page.

usethis::use_vignette() is the helper that writes the template, and makes the changes in DESCRIPTION and .gitignore that go with it. The script creates a second vignette in a copy of the package. It also shows that the helper accepts a .qmd name: in that case it would require the quarto R package, which is not installed here, and writes VignetteBuilder: quarto instead (this is what the source of the function in the image does; the Quarto vignettes below are described and not run).

# usethis::use_vignette() on a copy of the package: a second vignette.
src <- normalizePath(".")
tmp <- file.path(tempdir(), "vig")
dir.create(tmp)
invisible(file.copy(src, tmp, recursive = TRUE))
pkg <- file.path(tmp, basename(src))

msg <- capture.output(
  usethis::with_project(
    pkg,
    usethis::use_vignette("loans-by-genre", "Overdue loans by genre")
  ),
  type = "message"
)
msg[!grepl("active project", msg)]
#> [1] "✔ Writing 'vignettes/loans-by-genre.Rmd'."
#> [2] "☐ Edit 'vignettes/loans-by-genre.Rmd'."
list.files(file.path(pkg, "vignettes"), all.files = TRUE, no.. = TRUE)
#> [1] ".gitignore"         "loans-by-genre.Rmd" "shelf.Rmd"
cat(readLines(file.path(pkg, "vignettes", "loans-by-genre.Rmd")), sep = "\n")
#> ---
#> title: "Overdue loans by genre"
#> output: rmarkdown::html_vignette
#> vignette: >
#>   %\VignetteIndexEntry{Overdue loans by genre}
#>   %\VignetteEngine{knitr::rmarkdown}
#>   %\VignetteEncoding{UTF-8}
#> ---
#>
#> ```{r, include = FALSE}
#> knitr::opts_chunk$set(
#>   collapse = TRUE,
#>   comment = "#>"
#> )
#> ```
#>
#> ```{r setup}
#> library(shelf)
#> ```
# The end of DESCRIPTION, where the helper works.
desc <- readLines(file.path(pkg, "DESCRIPTION"))
desc[seq(grep("^Suggests", desc), length(desc))]
#>  [1] "Suggests: "                 "    knitr,"
#>  [3] "    rmarkdown,"             "    testthat (>= 3.0.0),"
#>  [5] "    withr"                  "Config/testthat/edition: 3"
#>  [7] "Depends: "                  "    R (>= 4.1.0)"
#>  [9] "VignetteBuilder: knitr"     "LazyData: true"

The helper wrote vignettes/loans-by-genre.Rmd with the header and the setup chunk of the first vignette, and made sure that knitr and rmarkdown are in Suggests and that VignetteBuilder is knitr: here they already were, so the end of DESCRIPTION did not change. For a package that did not have them, the helper adds the entries, and it adds inst/doc to .gitignore, so that the built vignettes stay out of the repository. vignettes/.gitignore is a second line of defence:

*.html
*.R

The two patterns keep the .html and .R files, which are what building the vignette in place leaves in vignettes/, out of version control. They are build products: the tarball gets them in inst/doc.

Building: R CMD build and the tarball

R CMD build is where the vignettes are rendered. It installs the package into a temporary library (the vignette calls library(shelf)), runs the engine on every file in vignettes/, and copies the sources and the outputs into inst/doc of the tarball, which is also what lets an author use private packages, screenshots and anything else that only exists on one machine. R keeps the outputs in the tarball so that they need not be rebuilt at installation, and R CMD check verifies that the outputs are not older than the sources. The script builds a copy of shelf with R CMD build and lists the files of the tarball that matter, and then builds it again with --no-build-vignettes:

# R CMD build renders the vignette into inst/doc of the tarball.
src <- normalizePath(".")
tmp <- file.path(tempdir(), "build")
dir.create(tmp)
invisible(file.copy(src, tmp, recursive = TRUE))
r_cmd <- \(...) {
  withr::with_dir(tmp, system2("R", c("CMD", ...),
                               stdout = TRUE, stderr = TRUE))
}
tarball <- file.path(tmp, "shelf_0.1.0.tar.gz")
in_tarball <- \(pattern) {
  grep(pattern, utils::untar(tarball, list = TRUE), value = TRUE)
}

r_cmd("build", "shelf")
#> [1] "* checking for file ‘shelf/DESCRIPTION’ ... OK"
#> [2] "* preparing ‘shelf’:"
#> [3] "* checking DESCRIPTION meta-information ... OK"
#> [4] "* installing the package (it is needed to build vignettes)"
#> [5] "* creating vignettes ... OK"
#> [6] "* checking for LF line-endings in source and make files and shell scripts"
#> [7] "* checking for empty or unneeded directories"
#> [8] "* building ‘shelf_0.1.0.tar.gz’"
#> [9] ""
in_tarball("vignettes|inst/doc|build/")
#> [1] "shelf/build/"              "shelf/build/vignette.rds"
#> [3] "shelf/inst/doc/"           "shelf/inst/doc/shelf.R"
#> [5] "shelf/inst/doc/shelf.Rmd"  "shelf/inst/doc/shelf.html"
#> [7] "shelf/vignettes/"          "shelf/vignettes/shelf.Rmd"

# The same build without the vignettes (as --no-build-vignettes asks).
unlink(tarball)
r_cmd("build", "--no-build-vignettes", "shelf")
#> [1] "* checking for file ‘shelf/DESCRIPTION’ ... OK"
#> [2] "* preparing ‘shelf’:"
#> [3] "* checking DESCRIPTION meta-information ... OK"
#> [4] "* checking for LF line-endings in source and make files and shell scripts"
#> [5] "* checking for empty or unneeded directories"
#> [6] "* building ‘shelf_0.1.0.tar.gz’"
#> [7] ""
in_tarball("vignettes|inst/doc|build/")
#> [1] "shelf/vignettes/"          "shelf/vignettes/shelf.Rmd"

# R CMD check notices that the sources have no outputs next to them.
res <- r_cmd("check", "--no-manual", "--no-tests", "--no-examples",
             "shelf_0.1.0.tar.gz")
step <- \(title) {
  i <- grep(title, res, fixed = TRUE)
  res[i:(i + 3L)]
}
step("checking files in ‘vignettes’")
#> [1] "* checking files in ‘vignettes’ ... WARNING"
#> [2] "Files in the 'vignettes' directory but no files in 'inst/doc':"
#> [3] "  ‘shelf.Rmd’"
#> [4] "* checking examples ... SKIPPED"
step("checking package vignettes")
#> [1] "* checking package vignettes ... WARNING"
#> [2] "Directory 'inst/doc' does not exist."
#> [3] "Package vignette without corresponding single PDF/HTML:"
#> [4] "  ‘shelf.Rmd’"
grep("^Status", res, value = TRUE)
#> [1] "Status: 2 WARNINGs"

The log of the first build has the step "installing the package (it is needed to build vignettes)" and "creating vignettes …​ OK". The tarball then holds vignettes/shelf.Rmd (the source), inst/doc/shelf.html (the output), inst/doc/shelf.R (the code of the vignette extracted by "tangling"; it is the file that the field R of the object returned by vignette() names) and inst/doc/shelf.Rmd (the source again, so that vignette("shelf") can offer it), and build/vignette.rds, the index that the installation uses. The second build skips the whole step and the tarball only has vignettes/shelf.Rmd. The check of that tarball, at the end of the script, shows why it is not what you want to ship: the source has no output next to it, and two steps give a WARNING ("Files in the 'vignettes' directory but no files in 'inst/doc'" and "Package vignette without corresponding single PDF/HTML"). The option --ignore-vignettes of R CMD check is the escape for a check of such a tarball, as in the earlier script.

The functions behind the commands are in the tools package. tools::buildVignettes() is what R CMD build calls, and the script calls it directly on a copy of the package, after trying the function of devtools that exists for the same purpose, which is deprecated.

# Rendering the vignettes of a source tree without building the tarball.
src <- normalizePath(".")
tmp <- file.path(tempdir(), "dev")
dir.create(tmp)
invisible(file.copy(src, tmp, recursive = TRUE))
pkg <- file.path(tmp, basename(src))

# devtools::build_vignettes() is deprecated since devtools 2.5.0; in this
# image it also stops, because the package remotes is not installed.
res <- withCallingHandlers(
  tryCatch(devtools::build_vignettes(pkg), error = \(e) conditionMessage(e)),
  warning = \(w) {
    cat("warning:", conditionMessage(w), "\n")
    invokeRestart("muffleWarning")
  }
)
#> warning: `build_vignettes()` was deprecated in devtools 2.5.0.
res
#> [1] "The package \"remotes\" is required."

# tools::buildVignettes() is the function under R CMD build.
out <- tools::buildVignettes(dir = pkg, tangle = TRUE)
#> --- re-building ‘shelf.Rmd’ using rmarkdown
#> --- finished re-building ‘shelf.Rmd’
#>
names(out)
#>  [1] "docs"      "names"     "engines"   "patterns"  "encodings" "dir"
#>  [7] "pkgdir"    "msg"       "outputs"   "sources"
basename(out$outputs)
#> [1] "shelf.html"
list.files(file.path(pkg, "vignettes"))
#> [1] "shelf.html" "shelf.R"    "shelf.Rmd"

devtools::build_vignettes() was deprecated in devtools 2.5.0, and in this image it stops with "The package "remotes" is required", because the package remotes is not installed; use devtools::build() (which builds the tarball like R CMD build) or call tools::buildVignettes(). The call returned a list with the vignettes' sources and outputs; shelf.html and shelf.R appeared next to shelf.Rmd in vignettes/, which is why the two files are in vignettes/.gitignore.

Reading the installed vignette

The installed package has the guide and the index that points to it. The script uses vignette(), browseVignettes(), tools::vignetteInfo() and tools::vignetteEngine(); the last shows the engines that knitr registers when it is loaded, which are the values that can follow \VignetteEngine.

# Finding the installed vignette of the package.
v <- vignette(package = "shelf")
class(v)
#> [1] "packageIQR"
data.frame(v$results[, c("Item", "Title"), drop = FALSE], row.names = NULL)
#>    Item                                             Title
#> 1 shelf Analysing library loans with shelf (source, html)

vig <- vignette("shelf", package = "shelf")
class(vig)
#> [1] "vignette"
names(vig)
#> [1] "Package" "Dir"     "Topic"   "File"    "Title"   "R"       "PDF"
basename(vig$File)
#> [1] "shelf.Rmd"
basename(vig$R)
#> [1] "shelf.R"

# The files in the installed doc directory; index.html is made at install time.
list.files(system.file("doc", package = "shelf"))
#> [1] "index.html" "shelf.html" "shelf.R"    "shelf.Rmd"

# browseVignettes() returns the same information (printing it opens a browser).
bv <- browseVignettes("shelf")
class(bv)
#> [1] "browseVignettes"
colnames(bv$shelf)
#> [1] "Package" "Dir"     "Topic"   "File"    "Title"   "R"       "PDF"
unname(bv$shelf[, "Title"])
#> [1] "Analysing library loans with shelf"

# The metadata lines of the source file, as R reads them.
info <- tools::vignetteInfo(system.file("doc", "shelf.Rmd", package = "shelf"))
info$title
#> [1] "Analysing library loans with shelf"
info$engine
#> [1] "knitr::rmarkdown"

# knitr registers its engines when it is loaded.
invisible(loadNamespace("knitr"))
sort(names(tools::vignetteEngine(package = "knitr")))
#>  [1] "knitr::docco_classic"          "knitr::docco_classic_notangle"
#>  [3] "knitr::docco_linear"           "knitr::docco_linear_notangle"
#>  [5] "knitr::knitr"                  "knitr::knitr_notangle"
#>  [7] "knitr::rmarkdown"              "knitr::rmarkdown_notangle"
#>  [9] "knitr::rtyp"                   "knitr::rtyp_notangle"

vignette(package = "shelf") lists the vignettes of the package with the title from \VignetteIndexEntry and the formats available (source, html), vignette("shelf", package = "shelf") returns an object of class vignette that opens in a browser when printed, and browseVignettes() returns the table that the HTML index is built from. The doc directory also has index.html, written at installation from the index entries unless the package supplies its own. For a script that wants the code of the vignette, vignette()$R is the path of shelf.R, which is inst/doc/shelf.R of the tarball. R CMD check examines the same files: "checking installed files from 'inst/doc'", "checking files in 'vignettes'", "checking package vignettes", "checking for unstated dependencies in vignettes" and "checking re-building of vignette outputs" are the steps in the log at the top of the page, and the last one is the one that rebuilds each vignette and fails when a chunk gives an error.

knitr and R Markdown in a vignette

A knitr-based vignette is an ordinary R Markdown document with the header lines above; the knitr site and the rmarkdown site document the chunk options and the output formats. The R chunks are evaluated in order, each chunk’s results and figures are inserted, and the chunk options (collapse, comment, fig.width, eval, echo, include) are knitr’s; a chunk with eval = FALSE is shown and not run, which is how a vignette shows code that needs a service or a long computation. All the chunks run when the package is checked, so a vignette is a test too. Of the engines that knitr registers, listed by the script above, knitr::rmarkdown (R Markdown to HTML or PDF) is the one used here, knitr::knitr processes Sweave-like .Rnw files with knitr, and the names that end in _notangle are the same engines without the extraction of the code. A vignette in HTML needs Pandoc, and one in PDF needs LaTeX.

Four rules keep a vignette from causing trouble on CRAN. Make it deterministic, as the shelf vignette does by fixing the reference date (today <- as.Date("2026-06-30")) instead of asking the system for the current date. Keep it fast, since the CRAN Repository Policy asks for the checks to take as little CPU time as possible (see Publishing to CRAN). Keep files small: PDF or HTML output that is too large is a known problem (the manual mentions a PDF of 72 MB), and the CRAN policy sets a 5 MB guideline for documentation. And use only packages that are declared, because the check "unstated dependencies in vignettes" fails on a package that is used but not listed in Suggests or Imports.

If a document should be on the website of the package but not in the tarball (because it is large, or needs a service), usethis::use_article() creates an "article" that only pkgdown builds, in the next section.

Quarto vignettes

Quarto, the multi-language successor of R Markdown from Posit, can render the same kind of document, and R packages can use it for vignettes. Quarto executes R cells with knitr, so most R Markdown documents render without change. The command-line program is separate from R (version 1.10.19 is installed in the image), and what connects it to the package system is the quarto R package, which registers the vignette engines quarto::html and a PDF one. The setup, as the package’s own article describes it, is VignetteBuilder: quarto in DESCRIPTION, quarto in Suggests, and the header of the .qmd file:

---
title: "Analysing library loans with shelf"
format: html
vignette: >
  %\VignetteIndexEntry{Analysing library loans with shelf}
  %\VignetteEngine{quarto::html}
  %\VignetteEncoding{UTF-8}
---

This is a fragment, not run: the quarto R package is not installed in the image, so R CMD build of such a package cannot run here. The Quarto program can render the file by itself, though. The shell script below makes a throwaway directory, writes a .qmd file with that header and one R chunk that loads the installed shelf and counts the overdue loans, and renders it with the program. The plain quarto render ignores the vignette: metadata and uses the default HTML format of Quarto:

export NO_COLOR=1
cd "$(mktemp -d)"
cat > shelf.qmd <<'QMD'
---
title: "Analysing library loans with shelf"
format: html
vignette: >
  %\VignetteIndexEntry{Analysing library loans with shelf}
  %\VignetteEngine{quarto::html}
  %\VignetteEncoding{UTF-8}
---

```{r}
library(shelf)
nrow(overdue_loans(loans, today = as.Date("2026-06-30")))
```
QMD
quarto render shelf.qmd
ls

Source: Quarto: Using R

processing file: shelf.qmd
1/3
2/3 [unnamed-chunk-1]
3/3
output file: shelf.knit.md

pandoc
  to: html
  output-file: shelf.html
  standalone: true
  section-divs: true
  html-math-method: mathjax
  wrap: none
  default-image-extension: png

metadata
  document-css: false
  link-citations: true
  date-format: long
  lang: en
  title: Analysing library loans with shelf
  vignette: |
    %\VignetteIndexEntry{Analysing library loans with shelf} %\VignetteEngine{quarto::html} %\VignetteEncoding{UTF-8}

Output created: shelf.html

shelf_files
shelf.html
shelf.qmd

Source: Quarto: Using R

The output shows what a Quarto render looks like: the chunk was executed (1/3, 2/3 [unnamed-chunk-1], 3/3) by knitr, Pandoc was called with the HTML options, and the result is shelf.html and a shelf_files directory. The vignette block is in the listed metadata as plain text, which is how Quarto passes it through; only the quarto R package turns it into an engine. The package’s article states two limits: the HTML vignette engine produces a deliberately minimal format (a standalone file, Bootstrap off, a small stylesheet of its own, and settings that cannot be overridden from the header, although new ones such as toc: true can be added), and an interactive quarto render uses the default HTML format of Quarto and not the vignette one, so only building the package shows the real result. The intermediate files of an authoring workflow (vignettes/*_files) belong in .Rbuildignore and .gitignore. For PDF there is a separate engine; and usethis::use_vignette("name.qmd") sets a package up for Quarto if the quarto R package and pkgdown 2.1.0 or later are installed.

The choice between the two is practical. R Markdown with knitr is the default of the package tooling, has no dependency outside R and Pandoc, and works everywhere R CMD check does. Quarto vignettes need the Quarto program on the machine that builds the package, and give access to Quarto’s features; for a package whose vignettes are an ordinary guide, R Markdown is the shorter road, and the Quarto road is the one for a package that already uses Quarto for its other documents. Reports with Quarto and R Markdown is the page about the formats themselves.

Sweave and other vignette formats

Sweave (.Rnw files, LaTeX with R chunks, run by utils::Sweave() and extracted by Stangle()) is the engine that needs no VignetteBuilder, and the manual’s section on vignettes is written in terms of it; R CMD Sweave and R CMD Stangle are the command-line forms. Documents in inst/doc can be in any format; the manual recommends HTML or PDF, so that users on almost all platforms can read them. New code uses R Markdown or Quarto.

pkgdown websites

pkgdown builds a static website from a package: a home page from README.md (or index.md), a function reference with a page for every Rd topic, the vignettes as "articles", the news from NEWS.md and the authors. Each Rd topic is converted to an HTML page with its links resolved across the site, the examples are run and their output is inserted, and the vignettes are rendered again with the pkgdown layout. The script builds the site of a copy of shelf completely offline: nothing is deployed, and the temporary directories are removed from the log, which is long. It starts with the report of pkgdown_sitrep() on a package with no configuration, adds the smallest _pkgdown.yml and shows what check_pkgdown() reports, completes it, and builds.

# Build the pkgdown site of a copy of the package (offline, nothing deployed).
src <- normalizePath(".")
tmp <- file.path(tempdir(), "site")
dir.create(tmp)
invisible(file.copy(src, tmp, recursive = TRUE))
pkg <- file.path(tmp, basename(src))
show_error <- \(expr) {
  tryCatch(expr, error = \(e) cat(conditionMessage(e), "\n"))
}

# With no configuration file, pkgdown reports what it would have to guess.
pkgdown::pkgdown_sitrep(pkg)
#> ── Sitrep ──────────────────────────────────────────────────────────────────────
#> ✖ Bootstrap 3 is deprecated; please switch to Bootstrap 5.
#> ℹ Learn more at
#>   <https://www.tidyverse.org/blog/2021/12/pkgdown-2-0-0/#bootstrap-5>.
#> ✖ URLs not ok.
#>   No '_pkgdown.yml' found.
#>   See details in `vignette(pkgdown::metadata)`.
#> ✔ Favicons ok.
#> ✔ Open graph metadata ok.
#> ✔ Articles metadata ok.
#> ✔ Reference metadata ok.

# A first _pkgdown.yml: the site URL and a reference index with one group.
writeLines(c(
  "url: https://example.github.io/shelf/",
  "template:",
  "  bootstrap: 5",
  "reference:",
  "  - title: The book class",
  "    contents:",
  "      - book"
), file.path(pkg, "_pkgdown.yml"))

# pkgdown wants the site URL in the URL field of DESCRIPTION, too.
show_error(pkgdown::check_pkgdown(pkg))
#> In DESCRIPTION, URL is missing package url
#> (https://example.github.io/shelf).
#> ℹ See details in `vignette(pkgdown::metadata)`.
cat("URL: https://example.github.io/shelf/\n",
    file = file.path(pkg, "DESCRIPTION"), append = TRUE)

# Every topic that is not internal must be in the reference index.
show_error(pkgdown::check_pkgdown(pkg))
#> In _pkgdown.yml, 8 topics missing from index: "book-methods", "books",
#> "clean_books", "loan_summary", "loans", "loans_per_month", "normalise_isbn",
#> and "overdue_loans".
#> ℹ Either add to the reference index, or use `@keywords internal` to drop from
#>   the index.

# The complete configuration.
writeLines(c(
  "url: https://example.github.io/shelf/",
  "template:",
  "  bootstrap: 5",
  "reference:",
  "  - title: The book class",
  "    desc: Constructor, validator and methods.",
  "    contents:",
  "      - book",
  "      - book-methods",
  "  - title: Cleaning",
  "    contents:",
  "      - clean_books",
  "      - normalise_isbn",
  "  - title: Loans",
  "    contents:",
  "      - overdue_loans",
  "      - loan_summary",
  "      - loans_per_month",
  "  - title: Data",
  "    contents:",
  "      - books",
  "      - loans",
  "articles:",
  "  - title: Analysis",
  "    contents:",
  "      - shelf"
), file.path(pkg, "_pkgdown.yml"))
pkgdown::check_pkgdown(pkg)
#> ✔ No problems found.

log <- capture.output(
  pkgdown::build_site(pkg, preview = FALSE, new_process = FALSE),
  type = "message"
)
# The section headings and the checks of the log; the per-file lines and the
# paths of the temporary directory are left out.
log[grepl("^(──|✔|✖)", log)]
#>  [1] "── Installing package shelf into temporary library ─────────────────────────────"
#>  [2] "── Initialising site ───────────────────────────────────────────────────────────"
#>  [3] "── Building pkgdown site for package shelf ─────────────────────────────────────"
#>  [4] "── Sitrep ──────────────────────────────────────────────────────────────────────"
#>  [5] "✔ URLs ok."
#>  [6] "✔ Favicons ok."
#>  [7] "✔ Open graph metadata ok."
#>  [8] "✔ Articles metadata ok."
#>  [9] "✔ Reference metadata ok."
#> [10] "── Initialising site ───────────────────────────────────────────────────────────"
#> [11] "── Building home ───────────────────────────────────────────────────────────────"
#> [12] "── Building function reference ─────────────────────────────────────────────────"
#> [13] "── Building articles ───────────────────────────────────────────────────────────"
#> [14] "── Building news ───────────────────────────────────────────────────────────────"
#> [15] "── Building sitemap ────────────────────────────────────────────────────────────"
#> [16] "── Building docs for llms ──────────────────────────────────────────────────────"
#> [17] "── Building redirects ──────────────────────────────────────────────────────────"
#> [18] "── Building search index ───────────────────────────────────────────────────────"
#> [19] "── Checking for problems ───────────────────────────────────────────────────────"
#> [20] "── Finished building pkgdown site for package shelf ────────────────────────────"
sum(grepl("^Adding redirect", log))
#> [1] 8
list.files(file.path(pkg, "docs"))
#>  [1] "404.html"          "404.md"            "articles"
#>  [4] "authors.html"      "authors.md"        "deps"
#>  [7] "index.html"        "index.md"          "katex-auto.js"
#> [10] "LICENSE-text.html" "LICENSE-text.md"   "LICENSE.html"
#> [13] "LICENSE.md"        "lightswitch.js"    "link.svg"
#> [16] "llms.txt"          "news"              "pkgdown.js"
#> [19] "pkgdown.yml"       "reference"         "search.json"
#> [22] "sitemap.xml"       "tutorials"
list.files(file.path(pkg, "docs", "reference"))
#>  [1] "book-methods.html"       "book-methods.md"
#>  [3] "book.html"               "book.md"
#>  [5] "books.html"              "books.md"
#>  [7] "clean_books.html"        "clean_books.md"
#>  [9] "clean_loans.html"        "format.book.html"
#> [11] "index.html"              "index.md"
#> [13] "is_valid_isbn13.html"    "loan_summary.html"
#> [15] "loan_summary.md"         "loans_per_month.html"
#> [17] "loans_per_month.md"      "loans.html"
#> [19] "loans.md"                "new_book.html"
#> [21] "normalise_isbn.html"     "normalise_isbn.md"
#> [23] "overdue_loans.html"      "overdue_loans.md"
#> [25] "print.book.html"         "print.summary.book.html"
#> [27] "summary.book.html"       "validate_book.html"
list.files(file.path(pkg, "docs", "articles"))
#> [1] "index.html"  "index.md"    "shelf_files" "shelf.html"  "shelf.md"

Read it in order. With no _pkgdown.yml, pkgdown_sitrep() reports that the URLs are not set ("No '_pkgdown.yml' found") and that Bootstrap 3 is deprecated, so template: bootstrap: 5 is the first line to write. The url is the second: it must be the address where the site will live, and pkgdown wants it also in the URL field of DESCRIPTION, which it reported. The first check_pkgdown() after that complained that eight topics were missing from the reference index: once a reference: field exists, every topic has to appear in it (or be marked @keywords internal), because an index that omits a topic is a broken index. The complete configuration has four groups of reference topics (the class, cleaning, loans and the data) and one group of articles; check_pkgdown() found no problems, and build_site() ran its steps, from installing the package into a temporary library to the search index. The result is a docs directory. Its reference directory has an HTML page for each of the nine topics, an index, and eight further small pages, one for each alias that has no page of its own (the "Adding redirect" messages count them), and articles has shelf.html, the vignette, and an index.

The _pkgdown.yml of this script is a good minimum. Its fields are url, template (the Bootstrap version and the theme), reference (a list of groups, each with a title, an optional desc and a contents list of topic names or selectors such as starts_with("loan")), and articles. The documentation lists more: the navigation bar, the home page sidebar, news, authors, a deploy field, and the headings of the site. Sites are written to docs/ by default; publishing is a step of its own. pkgdown’s recommended path for a GitHub repository is usethis::use_pkgdown_github_pages(), which sets up a GitHub Actions workflow built on the r-lib/actions repository that builds the site on every push and publishes it to GitHub Pages; for another host, pkgdown::build_site() and a copy of docs/ are enough. Both are fragments, not run (they need a remote repository and a token):

usethis::use_pkgdown_github_pages()
pkgdown::build_site()

pkgdown does not create documentation; it publishes what the package has. A site is therefore only as good as the Rd files, the vignettes, the README and the NEWS.md, and the usual problem is the reference index. One more point is worth knowing: pkgdown runs every example, so an example that needs a service must be guarded as for CRAN.

inst/CITATION: how to cite the package

citation() with no argument gives the citation of R, and citation("pkg") the citation of a package. The text comes from the file inst/CITATION if the package has one, and otherwise is built from DESCRIPTION. The file is R code, in the encoding of the package, with one or more calls of bibentry(); it must produce no output when sourced and, because CRAN requires it, it must not call functions such as packageDescription() that assume the package is installed in a library on the search path. The object meta, a parsed version of DESCRIPTION, is available inside the file, so that a title and a version need not be repeated and get out of date. The shelf file is the smallest that works:

bibentry(
  bibtype = "Manual",
  title = "shelf: Tools for a Small Library Catalogue",
  author = person("Shelf", "Maintainers"),
  year = "2026",
  note = "R package version 0.1.0"
)

The script prints the citation of R itself (a good example of the output), the citation of shelf, reads the installed file by hand with readCitationFile(), formats it in text style, and prints what R would have generated if the file did not exist, in the form of the bibentry() call.

# citation(): R itself, the shelf package, and the same data read by hand.
citation()
#> To cite R in publications use:
#>
#>   R Core Team (2026). _R: A Language and Environment for Statistical
#>   Computing_. R Foundation for Statistical Computing, Vienna, Austria.
#>   doi:10.32614/R.manuals <https://doi.org/10.32614/R.manuals>.
#>   <https://www.R-project.org/>.
#>
#> A BibTeX entry for LaTeX users is
#>
#>   @Manual{,
#>     title = {R: A Language and Environment for Statistical Computing},
#>     author = {{R Core Team}},
#>     organization = {R Foundation for Statistical Computing},
#>     address = {Vienna, Austria},
#>     year = {2026},
#>     doi = {10.32614/R.manuals},
#>     url = {https://www.R-project.org/},
#>   }
#>
#> We have invested a lot of time and effort in creating R, please cite it
#> when using it for data analysis. See also ‘citation("pkgname")’ for
#> citing R packages.

citation("shelf")
#> To cite package ‘shelf’ in publications use:
#>
#>   Maintainers S (2026). _shelf: Tools for a Small Library Catalogue_. R
#>   package version 0.1.0.
#>
#> A BibTeX entry for LaTeX users is
#>
#>   @Manual{,
#>     title = {shelf: Tools for a Small Library Catalogue},
#>     author = {Shelf Maintainers},
#>     year = {2026},
#>     note = {R package version 0.1.0},
#>   }

# The file the package ships: inst/CITATION is installed as CITATION.
cit <- readCitationFile(system.file("CITATION", package = "shelf"))
class(cit)
#> [1] "citation" "bibentry"
format(cit, style = "text")
#> [1] "Maintainers S (2026). _shelf: Tools for a Small Library Catalogue_. R\npackage version 0.1.0."
cit$title
#> [1] "shelf: Tools for a Small Library Catalogue"

# What would be generated without a CITATION file, as bibentry() code.
print(citation("shelf", auto = TRUE), style = "R")
#> bibentry(bibtype = "Manual",
#>          header = "To cite package ‘shelf’ in publications use:",
#>          title = "shelf: Tools for a Small Library Catalogue",
#>          author = person(given = "Shelf",
#>                          family = "Maintainers",
#>                          role = c("aut", "cre", "cph"),
#>                          email = "maintainer@example.com"),
#>          year = "2026",
#>          note = "R package version 0.1.0")

The names are shown as "Maintainers S" because the author is person("Shelf", "Maintainers"), given name Shelf and family name Maintainers. The citation has no url: the CITATION file of shelf had one at first, but R CMD check --as-cran checks URLs and answered that it was a 404, so it was removed, the same trap described in Publishing to CRAN. The auto-generated version (the last call) is the one that R shows when no file exists, and Writing R Extensions suggests starting from it: print(citation("pkg", auto = TRUE), style = "R") prints the code. In R 4.5.0 the auto-generated entries began to include the DOIs that CRAN and Bioconductor assign to their packages. A package with a paper to cite, or one that should be cited in a particular way, adds entries. The second script writes a fuller file in a temporary directory, with two entries, a header on each, and the title and note built from meta, then reads it with readCitationFile() and the description of the installed package as meta:

# A fuller CITATION: the package itself, built from DESCRIPTION, plus a paper.
file <- file.path(tempdir(), "CITATION")
writeLines(r"(
bibentry(
  bibtype = "Manual",
  title = paste0(meta$Package, ": ", meta$Title),
  author = person("Shelf", "Maintainers"),
  year = format(as.Date("2026-06-30"), "%Y"),
  note = paste("R package version", meta$Version),
  header = "To cite the shelf package:"
)

bibentry(
  bibtype = "Article",
  title = "Cleaning a Small Library Catalogue",
  author = c(person("Mara", "Quill"), person("Tomas", "Arden")),
  journal = "Journal of Imaginary Libraries",
  year = "2025",
  volume = "3",
  number = "1",
  pages = "10--20",
  header = "And the method behind it:"
)
)", file)
cit <- readCitationFile(file, meta = packageDescription("shelf"))
length(cit)
#> [1] 2
print(cit)
#> To cite the shelf package:
#>
#>   Maintainers S (2026). _shelf: Tools for a Small Library Catalogue_. R
#>   package version 0.1.0.
#>
#> And the method behind it:
#>
#>   Quill M, Arden T (2025). “Cleaning a Small Library Catalogue.”
#>   _Journal of Imaginary Libraries_, *3*(1), 10-20.
#>
#> To see these entries in BibTeX format, use 'print(<citation>,
#> bibtex=TRUE)', 'toBibtex(.)', or set
#> 'options(citation.bibtex.max=999)'.

The first entry is computed from meta (paste0(meta$Package, ": ", meta$Title) gave the title shown), and the second is a journal article that does not exist (the journal is imaginary, as the authors are). The header argument of each entry becomes the line above it. citation() prints the text form of every entry and a note about BibTeX; print(cit, bibtex = TRUE) or toBibtex() gives the BibTeX entries. If a bibentry contains LaTeX markup in names or titles, the argument textVersion supplies the plain text to print.

NEWS.md: what changed

A NEWS file is the changelog that news() reads and that users, CRAN and pkgdown show. R looks for it, in order, in inst/NEWS.Rd, NEWS.md (since R 3.6.0, and it needs the commonmark and xml2 packages), NEWS and inst/NEWS. For NEWS.md the format is Markdown: a first-level heading gives the package name and the version (and optionally a date in parentheses), a second-level heading gives a category, and the bullets are the entries. usethis::use_news_md() writes a first file. The shelf file has a heading for version 0.1.0 and one bullet:

# shelf 0.1.0

* Initial version: the `book` class, `clean_books()`, `clean_loans()`,
  `overdue_loans()`, `loan_summary()` and `loans_per_month()`, with the
  `books` and `loans` datasets.

The script reads the installed file back with news(): the result is a data frame with the version, date, category, text (and an HTML rendering), print() formats it, and news(Version >= "0.1.0", package = "shelf") selects by an expression over the columns.

# news(): the NEWS.md of the installed package, as a data frame.
n <- news(package = "shelf")
class(n)
#> [1] "news_db_from_md" "news_db"         "data.frame"
names(n)
#> [1] "Version"  "Date"     "Category" "Text"     "HTML"
n$Version
#> [1] "0.1.0"
cat(n$Text, sep = "\n")
#>   - Initial version: the book class, clean_books(), clean_loans(),
#>     overdue_loans(), loan_summary() and loans_per_month(), with the
#>     books and loans datasets.

# print() formats the entries; doBrowse = FALSE keeps the text in the console.
print(n, doBrowse = FALSE)
#>                         Changes in version 0.1.0
#>
#>   - Initial version: the book class, clean_books(), clean_loans(),
#>     overdue_loans(), loan_summary() and loans_per_month(), with the
#>     books and loans datasets.

# A query over the entries.
news(Version >= "0.1.0", package = "shelf")$Version
#> [1] "0.1.0"

The print method shows "Changes in version 0.1.0" and the bullet with the code formatting removed. A good NEWS.md is written for the users, not for the commit log: it says what changed in a way that affects a user, puts the newest version at the top, mentions each deprecated function with its replacement, and names the person or issue only if that helps. R 4.3.0 made R CMD check report problems it meets when reading NEWS.md, so a malformed file is found by the check. pkgdown publishes the file on the "News" page of the site.

README and the other files

A README.md is not part of R’s documentation system but is where people arrive: GitHub shows it, CRAN shows it on the package’s page, and pkgdown uses it as the home page. It should say what the package is for, how to install it and one short example. When the example should run, README.Rmd is rendered to README.md with devtools::build_readme(), as usethis::use_readme_rmd() sets up (the script of the package-page section created one). usethis adds README.Rmd to .Rbuildignore when it creates it, as the output of that script shows. CONTRIBUTING.md and CODE_OF_CONDUCT.md are other files of the same kind, and shelf also has a cran-comments.md, the notes that the author sends to CRAN with a submission, which is explained in Publishing to CRAN.

Pitfalls

  • Editing a generated file. A file that starts with "Generated by roxygen2: do not edit by hand" is overwritten by the next devtools::document(); edit the comment in the R file. The reverse also holds: roxygen2 leaves a hand-written Rd file alone (it says so, as shown above), so a package can mix the two, and a file that was once generated and is now edited by hand must lose its header.

  • Forgetting to document after a change. R CMD build does not run roxygen2: it packages man/ and NAMESPACE as they are. Commit both, run devtools::document() before every commit that touches a tag, and let devtools::check() catch the rest. devtools::load_all() does not regenerate documentation either, and it exposes every function, which hides an S3 method that lacks @export.

  • The percent sign and the braces in Rd. In hand-written Rd, % starts a comment and eats the closing brace, and unbalanced braces are an error: \% and balanced braces. roxygen2 with markdown escapes the percent signs for you; roxygen2 without markdown does not.

  • Examples that cannot run on a check farm. Examples may not need a network, a key, a display or write access outside a temporary directory. \dontrun{} hides code from every check, which reviewers dislike for code that could run; \donttest{} is run by --as-cran since R 4.0.0; @examplesIf runs the example only when its condition holds. Examples are also run by pkgdown and by example().

  • Arguments that do not match. The usage is taken from the code, so a renamed argument shows in the usage at once, but the @param for the old name stays and is reported ("Documented arguments not in \usage"), and the new one is "Undocumented". Run devtools::check_man() after every change of a signature.

  • A description that repeats the title. A block with a title and nothing else gives a page whose description is the title, as the generated page for overdue_rate() above shows. R CMD check accepts it; readers and CRAN reviewers do not.

  • Markdown surprises. A sentence in a comment that starts with 1. becomes a list. [word] becomes a link. A heading level 1 makes a new section. Inline code that does not parse as R becomes \verb{}, not \code{}. Block quotes, inline HTML and horizontal rules are not supported.

  • The warnings of the first run. The first document() of a new package warns about links to topics that are written in the same run, and the second run is clean. If a warning is still there on the second run, the link is wrong, or the linked package is not in DESCRIPTION.

  • The index of a pkgdown site. Once _pkgdown.yml has a reference field, every topic has to be listed in it or be internal. pkgdown::check_pkgdown() finds the missing ones, before a build fails.

  • Non-deterministic vignettes. A vignette that prints the current date, uses a random number without a seed, or reads the current state of a service changes at every build, and a changed output with no changed source confuses reviewers. The shelf vignette fixes the date.

  • Built vignettes in the repository. inst/doc and the files in vignettes/ that the build leaves (*.html, *.R) are products. usethis::use_vignette() ignores them with .gitignore; the tarball gets them from R CMD build.

  • A CITATION file that depends on installation. The file is run when the package is not installed; calling packageDescription() or utils::packageVersion() in it works on the author’s machine and fails on CRAN. Use meta.

  • A PDF manual needs TeX. Without pdflatex, R CMD Rd2pdf fails as shown above, and a plain R CMD check reports a WARNING and an ERROR for the PDF manual. The Rd files themselves are checked by checkRd() and by the other steps of the check without TeX, so --no-manual loses only the PDF.

  • A hand-written man directory and roxygen2 together. A page of the same name that roxygen2 would write is skipped, and nothing says that the roxygen2 block above the function is then not documentation: read the "Skipping" line.

Version notes

The tables list changes that affect what this page teaches. The R entries come from the NEWS files of the corresponding releases (R NEWS) and the manual Writing R Extensions; the roxygen2 entries from its changelog (roxygen2 package documentation: Changelog), which was read on 2026-10-21.

Version Change

R 3.6.0

news() reads NEWS.md (it needs the commonmark and xml2 packages).

R 4.0.0

R CMD check --as-cran runs the \donttest examples instead of telling the tester to run them; _R_CHECK_DONTTEST_EXAMPLES_ switches that off.

R 4.1.0

\link[pkg]{foo} and \link[pkg:bar]{foo} link to topics. Before, they were interpreted as links to the files foo.html and bar.html.

R 4.2.0

Help pages typeset \eqn and \deqn with KaTeX or MathJax in HTML.

R 4.3.0

R CMD check checks .Rd files more (invalid email addresses and some invalid URIs, empty \item labels in description lists), reports problems reading NEWS.md, and optionally (included in --as-cran) checks that KaTeX renders the math of the Rd files; tools::buildVignettes() has an argument skip that R CMD check uses for vignettes whose \VignetteDepends packages are unavailable.

R 4.4.0

The Rd macro \dontdiff{} marks example code whose output is ignored when comparing check output with a reference; tools::checkRd() reports titles and section names that end in a period (ignored by R CMD check unless _R_CHECK_RD_CHECKRD_MINLEVEL_ is -5 or smaller); R CMD check notes Rd files without an \alias.

R 4.5.0

\link[=dest]{...} accepts markup in the link text; citation() results generated from package metadata include the DOIs of CRAN and Bioconductor packages; R CMD Rd2pdf makes sure that the Rd.sty of the running R is used.

R 4.6.0

Bibliographic citations and references can be generated in Rd files from bibentries (\bibcitet, \bibcitep, \bibshow, \bibinfo, evaluated at the build stage); new macros \manual{name}{node} and \linkS4methods{}; plain-text help renders \describe lists with line breaks after the labels (tools::Rd2txt_options(descStyle = NULL) restores the old look); error messages of a failed \Sexpr include the call of the original condition and warnings include Rd source locations.

The R 3.6.0 entry is in the R 3.x NEWS (R NEWS: Changes in R 3.x). The R-devel NEWS (the upcoming 4.7.0, never run here) says at the time of writing that tools::Rd2txt() will replace --- and -- by Unicode dashes when the output encoding is UTF-8, that tools::texi2pdf() will report BibTeX warnings (so that R CMD check notes missing references when it rebuilds PDF vignettes), and that R CMD build will exclude an obsolete data/datalist file when the package uses LazyData.

roxygen2 Change

5.0.0

"_PACKAGE" documents a package.

7.0.0

@returns as an alias of @return; documentation of R6 classes; @includeRmd and @order; Markdown headings and tables; the removal of @S3method (deprecated since 4.0.0, 2014).

7.1.2

@examplesIf for conditional examples.

7.3.0

The NAMESPACE roclet reports S3 methods without @export; @describeIn suggests @rdname.

7.3.3

Custom @family titles support Markdown; @examplesIf uses withAutoprint().

8.0.0 (2026-05-01)

Requires R 4.1; options in Config/roxygen2/ fields and the version in Config/roxygen2/version (migrated automatically); needs_roxygenize(); support for S7 (@prop); more documentation inherited by R6 classes, @R6method; one-line tags warn when they span several lines; documenting a plain value no longer adds \docType{data}; the rd-other article is split into rd-datasets, rd-packages, rd-S3, rd-S4 and rd-R6.

8.1.0 (2026-08-04)

Flags in Config/roxygen2/ are case-insensitive; the migration of the version field happens whenever needed; multi-line @importFrom accepted again, as one directive per package; setAs() and setGroupGeneric() recognised; link targets resolved with cached indexes (the rdtools package).

8.1.1 (2026-10-08)

:= recognised as assignment (S7); S7 method usage wrapped in \special{}; non-syntactic names in @importFrom are double-quoted; fixes to @inherit and @inheritParams.

Other tools: devtools 2.5.0 deprecated devtools::build_vignettes() (the message the script printed); pkgdown 2.2.1 (the version here) warns that Bootstrap 3 is deprecated and builds an llms.txt file among the pages of the site; usethis 3.2.2 writes Roxygen: list(markdown = TRUE) in DESCRIPTION when markdown is turned on, which roxygen2 8 still reads; S7 is 0.2.2, and its roxygen2 support is in its first release; the Quarto command-line program is 1.10.19.

Where older tutorials differ

Older tutorial writes Current practice Why Since

Rd files written by hand, or prompt() skeletons kept in man/

roxygen2 comments and devtools::document()

the usage cannot disagree with the code, and the text sits next to it

always (roxygen2 is from 2011)

The version note that roxygen2 7.x wrote into DESCRIPTION, and Roxygen: list(markdown = TRUE)

Config/roxygen2/version and Config/roxygen2/markdown: TRUE (the old option field is still read)

all the package’s settings in one namespace of fields

roxygen2 8.0.0

@return

@returns (both work)

the name matches what the section is called; the tidyverse style prefers it

roxygen2 7.0.0

@docType package with @name pkg

documenting the string "_PACKAGE"

the defaults come from DESCRIPTION

roxygen2 5.0.0

@docType data, @name x, @usage data(x) and NULL

a block above the quoted name "x"

roxygen2 works out the usage and type

roxygen2 4.0.0 (2014)

@S3method

@export (or @exportS3Method)

one tag for every kind of export

removed in roxygen2 7.0.0

\code{\link{foo}} in comments, @seealso lists typed by hand

[foo()] with markdown, @family

less typing, and the links are checked

markdown by roxygen2 6.0.0

\dontrun{} for slow or fragile examples

smaller examples, \donttest{} for the truly slow, @examplesIf for the conditional

\dontrun hides code from every check, and --as-cran runs \donttest

R 4.0.0

devtools::build_vignettes()

devtools::build() or R CMD build

the function is deprecated, and R CMD build is what CRAN does

devtools 2.5.0

Sweave .Rnw vignettes that need LaTeX

R Markdown (knitr::rmarkdown) or Quarto

HTML needs no TeX

always

A plain-text NEWS file

NEWS.md

news() reads it, pkgdown publishes it, and R CMD check examines it

R 3.6.0

citEntry() and citHeader() in inst/CITATION, or hard-coded titles and versions

bibentry() with meta$Title and meta$Version

it is in the manual’s example, and it cannot go out of date

—

Manuals as PDF files kept in the repository

a pkgdown site, built from the package

the PDF needs TeX, and the site is linked from CRAN

always

pkgdown with template: bootstrap: 3

bootstrap: 5

pkgdown 2.2.1 warns that Bootstrap 3 is deprecated

pkgdown 2.x

Where to go next

References