From cb8086a0019de1a148f1b78d9b928db9ab223e58 Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:40:37 +0000 Subject: [PATCH 1/5] Use nanonext http server for preview --- DESCRIPTION | 1 + NEWS.md | 2 + R/pkgdown-package.R | 2 + R/preview.R | 28 +++++++++----- man/preview_site.Rd | 7 ++-- tests/testthat/test-preview.R | 73 ++++++++++++++++++++++++++++++++++- 6 files changed, 98 insertions(+), 15 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 7f3e4c316..c0848ca0b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -35,6 +35,7 @@ Imports: jsonlite, lifecycle, openssl, + nanonext (>= 1.8.0), purrr (>= 1.0.0), ragg (>= 1.4.0), rlang (>= 1.1.4), diff --git a/NEWS.md b/NEWS.md index 29315dae5..9a6d2e03c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # pkgdown (development version) +* When previewing a site, it is now served via a local http server. This enables dynamic features such as search to work correctly (@shikokuchuo, #2975). + * do not autolink code that is in a link (href) in Rd files (#2972) # pkgdown 2.2.0 diff --git a/R/pkgdown-package.R b/R/pkgdown-package.R index 5bdb226cf..d7c99c816 100644 --- a/R/pkgdown-package.R +++ b/R/pkgdown-package.R @@ -18,3 +18,5 @@ release_bullets <- function() { "Update `vignette/translations.Rmd` with any new languages" ) } + +the <- new.env(parent = emptyenv()) diff --git a/R/preview.R b/R/preview.R index 9c373a9a2..75b2939dc 100644 --- a/R/preview.R +++ b/R/preview.R @@ -1,15 +1,15 @@ #' Open site in browser #' -#' `preview_site()` opens your pkgdown site in your browser. pkgdown has been -#' carefully designed to work even when served from the file system like -#' this; the only part that doesn't work is search. You can use `servr::httw("docs/")` -#' to create a server to make search work locally. +#' `preview_site()` opens your pkgdown site in your browser, served via a +#' local HTTP server. This enables dynamic features such as search to work +#' correctly in preview. #' #' @inheritParams build_article #' @param path Path relative to destination #' @export preview_site <- function(pkg = ".", path = ".", preview = TRUE) { - path <- local_path(pkg, path) + pkg <- as_pkgdown(pkg) + abs_path <- local_path(pkg, path) check_bool(preview, allow_na = TRUE) if (is.na(preview)) { @@ -17,15 +17,26 @@ preview_site <- function(pkg = ".", path = ".", preview = TRUE) { } if (preview) { + root <- pkg$dst_path + + if (is.null(the$server) || !identical(the$server_root, root)) { + the$server <- nanonext::http_server( + url = "http://127.0.0.1:0", + handlers = nanonext::handler_directory("/", root) + ) + the$server$start() + the$server_root <- root + } + cli::cli_inform(c(i = "Previewing site")) - utils::browseURL(path) + url <- paste0(the$server$url, "/", if (path != ".") path) + utils::browseURL(url) } invisible() } local_path <- function(pkg, path, call = caller_env()) { - pkg <- as_pkgdown(pkg) check_string(path, call = call) abs_path <- path_abs(path, pkg$dst_path) @@ -33,9 +44,6 @@ local_path <- function(pkg, path, call = caller_env()) { cli::cli_abort("Can't find file {.path {path}}.", call = call) } - if (is_dir(abs_path)) { - abs_path <- path(abs_path, "index.html") - } abs_path } diff --git a/man/preview_site.Rd b/man/preview_site.Rd index d12fb6750..a99ea8b80 100644 --- a/man/preview_site.Rd +++ b/man/preview_site.Rd @@ -15,8 +15,7 @@ preview_site(pkg = ".", path = ".", preview = TRUE) freshly generated section in browser.} } \description{ -\code{preview_site()} opens your pkgdown site in your browser. pkgdown has been -carefully designed to work even when served from the file system like -this; the only part that doesn't work is search. You can use \code{servr::httw("docs/")} -to create a server to make search work locally. +\code{preview_site()} opens your pkgdown site in your browser, served via a +local HTTP server. This enables dynamic features such as search to work +correctly in preview. } diff --git a/tests/testthat/test-preview.R b/tests/testthat/test-preview.R index 87c29b95d..0fc42d029 100644 --- a/tests/testthat/test-preview.R +++ b/tests/testthat/test-preview.R @@ -1,3 +1,8 @@ +local_preview_clean <- function(env = caller_env()) { + the$server <- NULL + withr::defer(the$server <- NULL, envir = env) +} + test_that("checks its inputs", { pkg <- local_pkgdown_site() @@ -19,6 +24,72 @@ test_that("local_path adds index.html if needed", { dir_create(path(pkg$dst_path, "reference")) expect_equal( local_path(pkg, "reference"), - path(pkg$dst_path, "reference", "index.html") + path(pkg$dst_path, "reference") ) }) + +test_that("preview starts new server when none exists", { + pkg <- local_pkgdown_site() + local_preview_clean() + urls <- character() + withr::local_options(browser = function(url) urls <<- c(urls, url)) + + preview_site(pkg, preview = TRUE) + + expect_false(is.null(the$server)) + expect_equal(the$server_root, pkg$dst_path) + expect_length(urls, 1) + expect_equal(urls[[1]], paste0(the$server$url, "/")) +}) + +test_that("preview reuses server for same root", { + pkg <- local_pkgdown_site() + local_preview_clean() + urls <- character() + withr::local_options(browser = function(url) urls <<- c(urls, url)) + + preview_site(pkg, preview = TRUE) + server1 <- the$server + + file_create(path(pkg$dst_path, "test.html")) + preview_site(pkg, path = "test.html", preview = TRUE) + + expect_identical(the$server, server1) + expect_length(urls, 2) + expect_equal(urls[[2]], paste0(server1$url, "/test.html")) +}) + +test_that("preview starts new server for different root", { + pkg1 <- local_pkgdown_site() + pkg2 <- local_pkgdown_site() + local_preview_clean() + urls <- character() + withr::local_options(browser = function(url) urls <<- c(urls, url)) + + preview_site(pkg1, preview = TRUE) + server1 <- the$server + + preview_site(pkg2, preview = TRUE) + + expect_false(identical(the$server, server1)) + expect_equal(the$server_root, pkg2$dst_path) +}) + +test_that("preview constructs correct URLs for sub-paths", { + pkg <- local_pkgdown_site() + local_preview_clean() + urls <- character() + withr::local_options(browser = function(url) urls <<- c(urls, url)) + + dir_create(path(pkg$dst_path, "reference")) + file_create(path(pkg$dst_path, "reference", "foo.html")) + + preview_site(pkg, preview = TRUE) + base_url <- the$server$url + + preview_site(pkg, path = "reference", preview = TRUE) + expect_equal(urls[[2]], paste0(base_url, "/reference")) + + preview_site(pkg, path = "reference/foo.html", preview = TRUE) + expect_equal(urls[[3]], paste0(base_url, "/reference/foo.html")) +}) From 0abd9580ed191208bb0b1a8dce2fc3338a11cea3 Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:13:20 +0000 Subject: [PATCH 2/5] Update search docs section --- R/build-search-docs.R | 6 +----- man/build_search.Rd | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/R/build-search-docs.R b/R/build-search-docs.R index 28e5eeca0..bee420497 100644 --- a/R/build-search-docs.R +++ b/R/build-search-docs.R @@ -64,11 +64,7 @@ build_sitemap <- function(pkg = ".") { #' search: #' exclude: ['news/index.html'] #' ``` -#' # Debugging and local testing -#' -#' Locally (as opposed to on GitHub Pages or Netlify for instance), -#' search won't work if you simply use pkgdown preview of the static files. -#' You can use `servr::httw("docs")` instead. +#' # Debugging #' #' If search is not working, run `pkgdown::pkgdown_sitrep()` to eliminate #' common issues such as the absence of URL in the pkgdown configuration file diff --git a/man/build_search.Rd b/man/build_search.Rd index 3c787b518..1dff8016f 100644 --- a/man/build_search.Rd +++ b/man/build_search.Rd @@ -30,11 +30,7 @@ Below we exclude the changelog from the search index: }\if{html}{\out{}} } -\section{Debugging and local testing}{ -Locally (as opposed to on GitHub Pages or Netlify for instance), -search won't work if you simply use pkgdown preview of the static files. -You can use \code{servr::httw("docs")} instead. - +\section{Debugging}{ If search is not working, run \code{pkgdown::pkgdown_sitrep()} to eliminate common issues such as the absence of URL in the pkgdown configuration file of your package. From 9f7dfca7cf0d59c60e604ec89d4227507ac541bb Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:08:24 +0000 Subject: [PATCH 3/5] Add `stop_preview()` --- NAMESPACE | 1 + R/preview.R | 19 +++++++++++++++++++ man/preview_site.Rd | 3 +++ man/stop_preview.Rd | 13 +++++++++++++ tests/testthat/test-preview.R | 13 +++++++++++++ 5 files changed, 49 insertions(+) create mode 100644 man/stop_preview.Rd diff --git a/NAMESPACE b/NAMESPACE index 853ed814f..9413240a0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -138,6 +138,7 @@ export(pkgdown_sitrep) export(preview_site) export(rd2html) export(render_page) +export(stop_preview) export(template_articles) export(template_navbar) export(template_reference) diff --git a/R/preview.R b/R/preview.R index 75b2939dc..4a2e2a35c 100644 --- a/R/preview.R +++ b/R/preview.R @@ -4,6 +4,7 @@ #' local HTTP server. This enables dynamic features such as search to work #' correctly in preview. #' +#' @seealso [stop_preview()] to stop the server. #' @inheritParams build_article #' @param path Path relative to destination #' @export @@ -36,6 +37,24 @@ preview_site <- function(pkg = ".", path = ".", preview = TRUE) { invisible() } +#' Stop HTTP preview +#' +#' Stops the HTTP server started by [preview_site()], if active. This can be +#' called manually, but is not strictly necessary as the server is +#' automatically stopped when previewing a new site or ending the R session. +#' +#' @export +stop_preview <- function() { + if (!is.null(the$server)) { + the$server$close() + the$server <- NULL + the$server_root <- NULL + cli::cli_inform(c(i = "Stopped preview")) + } + + invisible() +} + local_path <- function(pkg, path, call = caller_env()) { check_string(path, call = call) diff --git a/man/preview_site.Rd b/man/preview_site.Rd index a99ea8b80..d9c8fe81d 100644 --- a/man/preview_site.Rd +++ b/man/preview_site.Rd @@ -19,3 +19,6 @@ freshly generated section in browser.} local HTTP server. This enables dynamic features such as search to work correctly in preview. } +\seealso{ +\code{\link[=stop_preview]{stop_preview()}} to stop the server. +} diff --git a/man/stop_preview.Rd b/man/stop_preview.Rd new file mode 100644 index 000000000..22929ae6b --- /dev/null +++ b/man/stop_preview.Rd @@ -0,0 +1,13 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/preview.R +\name{stop_preview} +\alias{stop_preview} +\title{Stop HTTP preview} +\usage{ +stop_preview() +} +\description{ +Stops the HTTP server started by \code{\link[=preview_site]{preview_site()}}, if active. This can be +called manually, but is not strictly necessary as the server is +automatically stopped when previewing a new site or ending the R session. +} diff --git a/tests/testthat/test-preview.R b/tests/testthat/test-preview.R index 0fc42d029..ff3081921 100644 --- a/tests/testthat/test-preview.R +++ b/tests/testthat/test-preview.R @@ -75,6 +75,19 @@ test_that("preview starts new server for different root", { expect_equal(the$server_root, pkg2$dst_path) }) +test_that("stop_preview stops server", { + pkg <- local_pkgdown_site() + local_preview_clean() + withr::local_options(browser = function(url) {}) + + preview_site(pkg, preview = TRUE) + expect_false(is.null(the$server)) + + stop_preview() + expect_null(the$server) + expect_null(the$server_root) +}) + test_that("preview constructs correct URLs for sub-paths", { pkg <- local_pkgdown_site() local_preview_clean() From e4a626a4ce551ea4f52f770f6703afa02561277c Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:17:22 +0000 Subject: [PATCH 4/5] Add pkgdown reference entry --- pkgdown/_pkgdown.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index 6a663c22c..55601e652 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -63,6 +63,7 @@ reference: - build_site - clean_site - preview_site + - stop_preview - pkgdown_sitrep - subtitle: Build part of a site From dc7c1a506a749f99475a0e31731ba208644bcf89 Mon Sep 17 00:00:00 2001 From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:09:53 +0000 Subject: [PATCH 5/5] Keywords internal `stop_preview()` --- R/preview.R | 1 + man/stop_preview.Rd | 1 + pkgdown/_pkgdown.yml | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/R/preview.R b/R/preview.R index 4a2e2a35c..41c6efa13 100644 --- a/R/preview.R +++ b/R/preview.R @@ -44,6 +44,7 @@ preview_site <- function(pkg = ".", path = ".", preview = TRUE) { #' automatically stopped when previewing a new site or ending the R session. #' #' @export +#' @keywords internal stop_preview <- function() { if (!is.null(the$server)) { the$server$close() diff --git a/man/stop_preview.Rd b/man/stop_preview.Rd index 22929ae6b..b3748edb5 100644 --- a/man/stop_preview.Rd +++ b/man/stop_preview.Rd @@ -11,3 +11,4 @@ Stops the HTTP server started by \code{\link[=preview_site]{preview_site()}}, if called manually, but is not strictly necessary as the server is automatically stopped when previewing a new site or ending the R session. } +\keyword{internal} diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index 55601e652..6a663c22c 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -63,7 +63,6 @@ reference: - build_site - clean_site - preview_site - - stop_preview - pkgdown_sitrep - subtitle: Build part of a site