From 0fbc85ff35febf9ceb7610e291aae4943cb409a3 Mon Sep 17 00:00:00 2001 From: Kyle Grealis Date: Tue, 3 Feb 2026 16:43:00 -0600 Subject: [PATCH 1/2] Add .pkgdownignore support to exclude markdown files from site build Adds support for a .pkgdownignore file that allows users to exclude specific markdown files from being rendered to HTML during site builds. The file uses a simple format (one filename per line, # comments supported) and can be placed in the package root, pkgdown/, or _pkgdown/ directories. Files from all locations are combined. Closes #2959 --- NEWS.md | 4 + R/build-home-md.R | 40 +++++++ tests/testthat/test-pkgdownignore.R | 159 ++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 tests/testthat/test-pkgdownignore.R diff --git a/NEWS.md b/NEWS.md index e5087ea93..77642d3b5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # pkgdown (development version) +* `build_home()` now supports a `.pkgdownignore` file to exclude markdown files + from being rendered to HTML. The file can be placed in the package root, + `pkgdown/`, or `_pkgdown/` directories (@kyleGrealis, #2959). + # pkgdown 2.2.0 * Make `build_llm_docs()` more robust to the use of old Pandoc (@nanxstats, @galachad, #2952, #2954) diff --git a/R/build-home-md.R b/R/build-home-md.R index ae156b838..507039589 100644 --- a/R/build-home-md.R +++ b/R/build-home-md.R @@ -16,6 +16,14 @@ package_mds <- function(path, in_dev = FALSE) { # Remove files handled elsewhere handled <- c("README.md", "LICENSE.md", "LICENCE.md", "NEWS.md") + + # Append user-defined ignores from .pkgdownignore + user_ignores <- read_pkgdownignore(path) + if (length(user_ignores) > 0) { + cli::cli_inform("Ignoring: {user_ignores}") + } + handled <- c(handled, user_ignores) + # handled <- c(handled) mds <- mds[!path_file(mds) %in% handled] # Do not build 404 page if in-dev @@ -59,3 +67,35 @@ render_md <- function(pkg, filename) { invisible() } + +#' Read .pkgdownignore file +#' +#' Searches for .pkgdownignore in standard pkgdown config locations. +#' Returns filenames to exclude (one per line, with comments ignored). +#' +#' @param path Path to the package root +#' @return Character vector of filenames to exclude +#' @noRd +read_pkgdownignore <- function(path) { + # Check standard pkgdown config locations: + candidates <- c( + path(path, ".pkgdownignore"), + path(path, "_pkgdown", ".pkgdownignore"), + path(path, "pkgdown", ".pkgdownignore") + ) + + # Read output from all ignore files + ignore_paths <- candidates[file_exists(candidates)] + if (length(ignore_paths) == 0) { + return(character()) + } + + # Combine lines from all ignore files + lines <- unlist(lapply(ignore_paths, readLines)) + + # Remove comments & empty lines and trim whitespace + lines <- lines[!grepl("^\\s*#", lines)] + lines <- lines[nzchar(trimws(lines))] + + unique(trimws(lines)) +} diff --git a/tests/testthat/test-pkgdownignore.R b/tests/testthat/test-pkgdownignore.R new file mode 100644 index 000000000..a4859fe08 --- /dev/null +++ b/tests/testthat/test-pkgdownignore.R @@ -0,0 +1,159 @@ +test_that("read_pkgdownignore returns empty when no file exists", { + pkg <- local_pkgdown_site() + expect_equal(read_pkgdownignore(pkg$src_path), character()) +}) + +test_that("read_pkgdownignore reads from package root", { + pkg <- local_pkgdown_site() + writeLines(c("CLAUDE.md", "AGENTS.md"), path(pkg$src_path, ".pkgdownignore")) + + result <- read_pkgdownignore(pkg$src_path) + expect_equal(result, c("CLAUDE.md", "AGENTS.md")) +}) + +test_that("read_pkgdownignore reads from pkgdown/ directory", { + pkg <- local_pkgdown_site() + dir_create(path(pkg$src_path, "pkgdown")) + writeLines("INTERNAL.md", path(pkg$src_path, "pkgdown", ".pkgdownignore")) + + result <- read_pkgdownignore(pkg$src_path) + expect_equal(result, "INTERNAL.md") +}) + +test_that("read_pkgdownignore reads from _pkgdown/ directory", { + pkg <- local_pkgdown_site() + dir_create(path(pkg$src_path, "_pkgdown")) + writeLines("NOTES.md", path(pkg$src_path, "_pkgdown", ".pkgdownignore")) + + result <- read_pkgdownignore(pkg$src_path) + expect_equal(result, "NOTES.md") +}) + +test_that("read_pkgdownignore combines files from multiple locations", { + pkg <- local_pkgdown_site() + + # Root + + writeLines("CLAUDE.md", path(pkg$src_path, ".pkgdownignore")) + + # pkgdown/ + dir_create(path(pkg$src_path, "pkgdown")) + writeLines("AGENTS.md", path(pkg$src_path, "pkgdown", ".pkgdownignore")) + + result <- read_pkgdownignore(pkg$src_path) + expect_setequal(result, c("CLAUDE.md", "AGENTS.md")) +}) + +test_that("read_pkgdownignore ignores comments and empty lines", { + pkg <- local_pkgdown_site() + writeLines( + c( + "# This is a comment", + "CLAUDE.md", + "", + " # Indented comment", + "AGENTS.md", + " ", + "INTERNAL.md" + ), + path(pkg$src_path, ".pkgdownignore") + ) + + result <- read_pkgdownignore(pkg$src_path) + expect_equal(result, c("CLAUDE.md", "AGENTS.md", "INTERNAL.md")) +}) + +test_that("read_pkgdownignore trims whitespace", { + pkg <- local_pkgdown_site() + writeLines( + c(" CLAUDE.md ", "\tAGENTS.md\t"), + path(pkg$src_path, ".pkgdownignore") + ) + + result <- read_pkgdownignore(pkg$src_path) + expect_equal(result, c("CLAUDE.md", "AGENTS.md")) +}) + +test_that("read_pkgdownignore deduplicates entries", { + pkg <- local_pkgdown_site() + + # Same file in root and pkgdown/ + + writeLines("CLAUDE.md", path(pkg$src_path, ".pkgdownignore")) + dir_create(path(pkg$src_path, "pkgdown")) + writeLines("CLAUDE.md", path(pkg$src_path, "pkgdown", ".pkgdownignore")) + + result <- read_pkgdownignore(pkg$src_path) + expect_equal(result, "CLAUDE.md") +}) + +test_that("package_mds excludes files listed in .pkgdownignore", { + pkg <- local_pkgdown_site() + + # Create test markdown files + writeLines("# CLAUDE", path(pkg$src_path, "CLAUDE.md")) + writeLines("# AGENTS", path(pkg$src_path, "AGENTS.md")) + writeLines("# ROADMAP", path(pkg$src_path, "ROADMAP.md")) + + # Create .pkgdownignore + + writeLines(c("CLAUDE.md", "AGENTS.md"), path(pkg$src_path, ".pkgdownignore")) + + result <- package_mds(pkg$src_path) + result_files <- path_file(result) + + expect_false("CLAUDE.md" %in% result_files) + expect_false("AGENTS.md" %in% result_files) + expect_true("ROADMAP.md" %in% result_files) +}) + +test_that("package_mds excludes .github files listed in .pkgdownignore", { + pkg <- local_pkgdown_site() + + # Create .github directory with markdown files + dir_create(path(pkg$src_path, ".github")) + writeLines("# Internal", path(pkg$src_path, ".github", "INTERNAL.md")) + writeLines("# Contributing", path(pkg$src_path, ".github", "CONTRIBUTING.md")) + + # Ignore the internal one + writeLines("INTERNAL.md", path(pkg$src_path, ".pkgdownignore")) + + result <- package_mds(pkg$src_path) + result_files <- path_file(result) + + expect_false("INTERNAL.md" %in% result_files) + expect_true("CONTRIBUTING.md" %in% result_files) +}) + +test_that("package_mds works when .pkgdownignore is empty", { + pkg <- local_pkgdown_site() + + # Create test file + writeLines("# ROADMAP", path(pkg$src_path, "ROADMAP.md")) + + # Empty ignore file + file_create(path(pkg$src_path, ".pkgdownignore")) + + result <- package_mds(pkg$src_path) + result_files <- path_file(result) + + expect_true("ROADMAP.md" %in% result_files) +}) + +test_that("package_mds works when .pkgdownignore has only comments", { + pkg <- local_pkgdown_site() + + # Create test file + writeLines("# ROADMAP", path(pkg$src_path, "ROADMAP.md")) + + # Ignore file with only comments + writeLines( + c("# comment 1", "# comment 2"), + path(pkg$src_path, ".pkgdownignore") + ) + + result <- package_mds(pkg$src_path) + result_files <- path_file(result) + + expect_true("ROADMAP.md" %in% result_files) +}) From ebd804eec7eb91dfe6db951f2f3b1421dfe9cdbe Mon Sep 17 00:00:00 2001 From: Kyle Grealis Date: Tue, 3 Feb 2026 17:20:34 -0600 Subject: [PATCH 2/2] use write_lines() to satisfy lintr --- tests/testthat/test-pkgdownignore.R | 41 ++++++++++++++++------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/tests/testthat/test-pkgdownignore.R b/tests/testthat/test-pkgdownignore.R index a4859fe08..3a7b14a4b 100644 --- a/tests/testthat/test-pkgdownignore.R +++ b/tests/testthat/test-pkgdownignore.R @@ -5,7 +5,7 @@ test_that("read_pkgdownignore returns empty when no file exists", { test_that("read_pkgdownignore reads from package root", { pkg <- local_pkgdown_site() - writeLines(c("CLAUDE.md", "AGENTS.md"), path(pkg$src_path, ".pkgdownignore")) + write_lines(c("CLAUDE.md", "AGENTS.md"), path(pkg$src_path, ".pkgdownignore")) result <- read_pkgdownignore(pkg$src_path) expect_equal(result, c("CLAUDE.md", "AGENTS.md")) @@ -14,7 +14,7 @@ test_that("read_pkgdownignore reads from package root", { test_that("read_pkgdownignore reads from pkgdown/ directory", { pkg <- local_pkgdown_site() dir_create(path(pkg$src_path, "pkgdown")) - writeLines("INTERNAL.md", path(pkg$src_path, "pkgdown", ".pkgdownignore")) + write_lines("INTERNAL.md", path(pkg$src_path, "pkgdown", ".pkgdownignore")) result <- read_pkgdownignore(pkg$src_path) expect_equal(result, "INTERNAL.md") @@ -23,7 +23,7 @@ test_that("read_pkgdownignore reads from pkgdown/ directory", { test_that("read_pkgdownignore reads from _pkgdown/ directory", { pkg <- local_pkgdown_site() dir_create(path(pkg$src_path, "_pkgdown")) - writeLines("NOTES.md", path(pkg$src_path, "_pkgdown", ".pkgdownignore")) + write_lines("NOTES.md", path(pkg$src_path, "_pkgdown", ".pkgdownignore")) result <- read_pkgdownignore(pkg$src_path) expect_equal(result, "NOTES.md") @@ -34,11 +34,11 @@ test_that("read_pkgdownignore combines files from multiple locations", { # Root - writeLines("CLAUDE.md", path(pkg$src_path, ".pkgdownignore")) + write_lines("CLAUDE.md", path(pkg$src_path, ".pkgdownignore")) # pkgdown/ dir_create(path(pkg$src_path, "pkgdown")) - writeLines("AGENTS.md", path(pkg$src_path, "pkgdown", ".pkgdownignore")) + write_lines("AGENTS.md", path(pkg$src_path, "pkgdown", ".pkgdownignore")) result <- read_pkgdownignore(pkg$src_path) expect_setequal(result, c("CLAUDE.md", "AGENTS.md")) @@ -46,7 +46,7 @@ test_that("read_pkgdownignore combines files from multiple locations", { test_that("read_pkgdownignore ignores comments and empty lines", { pkg <- local_pkgdown_site() - writeLines( + write_lines( c( "# This is a comment", "CLAUDE.md", @@ -65,7 +65,7 @@ test_that("read_pkgdownignore ignores comments and empty lines", { test_that("read_pkgdownignore trims whitespace", { pkg <- local_pkgdown_site() - writeLines( + write_lines( c(" CLAUDE.md ", "\tAGENTS.md\t"), path(pkg$src_path, ".pkgdownignore") ) @@ -79,9 +79,9 @@ test_that("read_pkgdownignore deduplicates entries", { # Same file in root and pkgdown/ - writeLines("CLAUDE.md", path(pkg$src_path, ".pkgdownignore")) + write_lines("CLAUDE.md", path(pkg$src_path, ".pkgdownignore")) dir_create(path(pkg$src_path, "pkgdown")) - writeLines("CLAUDE.md", path(pkg$src_path, "pkgdown", ".pkgdownignore")) + write_lines("CLAUDE.md", path(pkg$src_path, "pkgdown", ".pkgdownignore")) result <- read_pkgdownignore(pkg$src_path) expect_equal(result, "CLAUDE.md") @@ -91,13 +91,13 @@ test_that("package_mds excludes files listed in .pkgdownignore", { pkg <- local_pkgdown_site() # Create test markdown files - writeLines("# CLAUDE", path(pkg$src_path, "CLAUDE.md")) - writeLines("# AGENTS", path(pkg$src_path, "AGENTS.md")) - writeLines("# ROADMAP", path(pkg$src_path, "ROADMAP.md")) + write_lines("# CLAUDE", path(pkg$src_path, "CLAUDE.md")) + write_lines("# AGENTS", path(pkg$src_path, "AGENTS.md")) + write_lines("# ROADMAP", path(pkg$src_path, "ROADMAP.md")) # Create .pkgdownignore - writeLines(c("CLAUDE.md", "AGENTS.md"), path(pkg$src_path, ".pkgdownignore")) + write_lines(c("CLAUDE.md", "AGENTS.md"), path(pkg$src_path, ".pkgdownignore")) result <- package_mds(pkg$src_path) result_files <- path_file(result) @@ -112,11 +112,14 @@ test_that("package_mds excludes .github files listed in .pkgdownignore", { # Create .github directory with markdown files dir_create(path(pkg$src_path, ".github")) - writeLines("# Internal", path(pkg$src_path, ".github", "INTERNAL.md")) - writeLines("# Contributing", path(pkg$src_path, ".github", "CONTRIBUTING.md")) + write_lines("# Internal", path(pkg$src_path, ".github", "INTERNAL.md")) + write_lines( + "# Contributing", + path(pkg$src_path, ".github", "CONTRIBUTING.md") + ) # Ignore the internal one - writeLines("INTERNAL.md", path(pkg$src_path, ".pkgdownignore")) + write_lines("INTERNAL.md", path(pkg$src_path, ".pkgdownignore")) result <- package_mds(pkg$src_path) result_files <- path_file(result) @@ -129,7 +132,7 @@ test_that("package_mds works when .pkgdownignore is empty", { pkg <- local_pkgdown_site() # Create test file - writeLines("# ROADMAP", path(pkg$src_path, "ROADMAP.md")) + write_lines("# ROADMAP", path(pkg$src_path, "ROADMAP.md")) # Empty ignore file file_create(path(pkg$src_path, ".pkgdownignore")) @@ -144,10 +147,10 @@ test_that("package_mds works when .pkgdownignore has only comments", { pkg <- local_pkgdown_site() # Create test file - writeLines("# ROADMAP", path(pkg$src_path, "ROADMAP.md")) + write_lines("# ROADMAP", path(pkg$src_path, "ROADMAP.md")) # Ignore file with only comments - writeLines( + write_lines( c("# comment 1", "# comment 2"), path(pkg$src_path, ".pkgdownignore") )