diff --git a/.claude/rules/testing/smoke-all-tests.md b/.claude/rules/testing/smoke-all-tests.md
index ea80419bcdf..7900aaeb19b 100644
--- a/.claude/rules/testing/smoke-all-tests.md
+++ b/.claude/rules/testing/smoke-all-tests.md
@@ -119,6 +119,23 @@ _quarto:
Valid OS values: `linux`, `darwin`, `windows`
+## Project Rendering (cross-file resolution)
+
+Each `_quarto.tests` block renders only its own input file (`quarto render --to `). In a multi-file project (book/website) that leaves cross-file references resolved at the project post-render stage (e.g. an appendix cross-reference pointing into another chapter) unresolved.
+
+Set `render-project: true` as a sibling of `tests:` (under `_quarto`) to render the whole project first:
+
+```yaml
+_quarto:
+ render-project: true
+ tests:
+ html:
+ ensureFileRegexMatches:
+ - ['>Whatever A<']
+```
+
+Gotcha: the project pre-render runs `quarto render ` with **no `--to`**, so it builds only the formats **declared in `_quarto.yml`**. To test a format's cross-file output, that format must be declared in the project config — otherwise the pre-render skips it and the per-file render alone cannot resolve cross-file refs. Example: `tests/docs/smoke-all/2026/06/23/issue-11772` (book must declare `format: html` for the HTML appendix cross-reference test).
+
## Pattern Specificity
Avoid patterns that match template boilerplate instead of document content:
diff --git a/llm-docs/testing-patterns.md b/llm-docs/testing-patterns.md
index 2ddcfa0c364..86968475b5f 100644
--- a/llm-docs/testing-patterns.md
+++ b/llm-docs/testing-patterns.md
@@ -394,6 +394,23 @@ Run test **without fix** first to verify it fails, then verify it passes with fi
Smoke-all tests embed test specifications directly in `.qmd` files using `_quarto.tests` metadata. See `.claude/rules/testing/smoke-all-tests.md` for full documentation.
+### Project pre-render for cross-file resolution
+
+A `_quarto.tests` block renders only its own input file (`quarto render --to `). In a multi-file project (book/website), cross-file references that are resolved at the project post-render stage — e.g. an appendix cross-reference in one chapter pointing at another chapter — stay unresolved under a single-file render.
+
+Set `render-project: true` as a sibling of `tests:` to render the whole project first:
+
+```yaml
+_quarto:
+ render-project: true
+ tests:
+ html:
+ ensureFileRegexMatches:
+ - ['>Whatever A<']
+```
+
+The harness runs `quarto render ` (in `smoke-all.test.ts`) before the per-file render. **Gotcha:** that pre-render has no `--to`, so it builds only the formats declared in `_quarto.yml`. To test a format's cross-file output, declare that format in the project config — otherwise the pre-render skips it and the per-file render alone cannot resolve cross-file references. Reference fixture: `tests/docs/smoke-all/2026/06/23/issue-11772` (the book declares `format: html` so the HTML appendix cross-reference resolves; the per-file HTML render then reuses the project crossref index).
+
### YAML String Escaping for Regex
**Critical rule:** In YAML single-quoted strings, `'\('` and `"\\("` are equivalent - both produce a literal `\(` in the regex.
diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md
index 7cf310f1b2b..50156fb6f3c 100644
--- a/news/changelog-1.10.md
+++ b/news/changelog-1.10.md
@@ -50,6 +50,10 @@ All changes included in 1.10:
- ([#14562](https://github.com/quarto-dev/quarto-cli/issues/14562)): Fix a heading inside `content-hidden when-format="llms-txt"` (visible in HTML) losing its `` wrapper and `id` in a website with `llms-txt` enabled, which broke its table-of-contents link, anchors, and cross-references.
- ([#14563](https://github.com/quarto-dev/quarto-cli/issues/14563)): Fix a fatal error when a shortcode is used inside conditional content (e.g. `content-visible when-format="llms-txt"`) in a website with `llms-txt` enabled.
+### Books
+
+- ([#11772](https://github.com/quarto-dev/quarto-cli/issues/11772)): Fix `crossref.appendix-title` not being applied to appendix cross-references in PDF output. Appendix cross-references now use the configured title (e.g. `See Whatever A`) instead of always showing the default `Appendix` prefix.
+
## Commands
### `quarto preview`
diff --git a/src/project/types/book/book-crossrefs.ts b/src/project/types/book/book-crossrefs.ts
index 6b2d3fc6284..cc120e0f555 100644
--- a/src/project/types/book/book-crossrefs.ts
+++ b/src/project/types/book/book-crossrefs.ts
@@ -18,6 +18,8 @@ import {
import { pathWithForwardSlashes } from "../../../core/path.ts";
import {
+ kCrossref,
+ kCrossrefAppendixTitle,
kCrossrefApxPrefix,
kCrossrefChapterId,
kCrossrefChapters,
@@ -37,7 +39,7 @@ import { WebsiteProjectOutputFile } from "../website/website.ts";
import { inputTargetIndex } from "../../project-index.ts";
import { bookConfigRenderItems } from "./book-config.ts";
import { isMultiFileBookFormat } from "./book-shared.ts";
-import { Format } from "../../../config/types.ts";
+import { Format, Metadata } from "../../../config/types.ts";
export async function bookCrossrefsPostRender(
context: ProjectContext,
@@ -305,11 +307,21 @@ function formatCrossref(
// if this is a section we need a prefix
const refNumber = numberOption(entry.order, options, type);
if (type === "sec" && !noPrefix) {
- const prefix = (options[kCrossrefChapters] && isChapterRef(entry))
- ? options[kCrossrefChaptersAppendix]
- ? language[kCrossrefApxPrefix]
- : language[kCrossrefChPrefix]
- : language[kCrossrefSecPrefix];
+ let prefix;
+ if (options[kCrossrefChapters] && isChapterRef(entry)) {
+ if (options[kCrossrefChaptersAppendix]) {
+ // appendix cross-references prefer crossref.appendix-title (the same
+ // option that titles the appendix chapter heading) so the reference
+ // text matches the heading
+ const crossref = format.metadata?.[kCrossref] as Metadata | undefined;
+ prefix = (crossref?.[kCrossrefAppendixTitle] as string) ??
+ language[kCrossrefApxPrefix];
+ } else {
+ prefix = language[kCrossrefChPrefix];
+ }
+ } else {
+ prefix = language[kCrossrefSecPrefix];
+ }
const crossref = prefix + " " + refNumber;
return crossref;
} else {
diff --git a/src/resources/filters/crossref/format.lua b/src/resources/filters/crossref/format.lua
index 6c6908b285b..7f5d7dfc0cf 100644
--- a/src/resources/filters/crossref/format.lua
+++ b/src/resources/filters/crossref/format.lua
@@ -77,6 +77,14 @@ function refPrefix(type, upper)
end
default = stringToInlines(default)
local prefix = crossrefOption(opt, default)
+ -- appendix cross-references prefer crossref.appendix-title (the same option that
+ -- titles the appendix chapter heading) so the reference text matches the heading
+ if type == "apx" then
+ local appendixTitle = crossrefOption("appendix-title")
+ if appendixTitle ~= nil then
+ prefix = appendixTitle
+ end
+ end
if upper then
local el = pandoc.Plain(prefix)
local firstStr = true
diff --git a/tests/docs/smoke-all/2026/06/23/issue-11772/.gitignore b/tests/docs/smoke-all/2026/06/23/issue-11772/.gitignore
new file mode 100644
index 00000000000..ad293093b07
--- /dev/null
+++ b/tests/docs/smoke-all/2026/06/23/issue-11772/.gitignore
@@ -0,0 +1,2 @@
+/.quarto/
+**/*.quarto_ipynb
diff --git a/tests/docs/smoke-all/2026/06/23/issue-11772/_quarto.yml b/tests/docs/smoke-all/2026/06/23/issue-11772/_quarto.yml
new file mode 100644
index 00000000000..a5929d6f7d7
--- /dev/null
+++ b/tests/docs/smoke-all/2026/06/23/issue-11772/_quarto.yml
@@ -0,0 +1,21 @@
+project:
+ type: book
+
+book:
+ title: "issue-11772"
+ author: "Norah Jones"
+ chapters:
+ - index.qmd
+ - intro.qmd
+ appendices:
+ - summary.qmd
+
+format:
+ html: default
+ pdf:
+ documentclass: scrreprt
+ keep-tex: true
+
+crossref:
+ appendix-title: "Whatever"
+ appendix-delim: ":"
diff --git a/tests/docs/smoke-all/2026/06/23/issue-11772/index.qmd b/tests/docs/smoke-all/2026/06/23/issue-11772/index.qmd
new file mode 100644
index 00000000000..726a6cad132
--- /dev/null
+++ b/tests/docs/smoke-all/2026/06/23/issue-11772/index.qmd
@@ -0,0 +1,19 @@
+---
+format:
+ pdf:
+ keep-tex: true
+_quarto:
+ tests:
+ pdf:
+ # crossref.appendix-title ("Whatever") must drive the appendix
+ # cross-reference prefix in PDF, like it does in HTML output.
+ # Currently the prefix falls back to the default "Appendix"
+ # (only language.crossref-apx-prefix is honored) — issue #11772.
+ ensureLatexFileRegexMatches:
+ - ['See Whatever~\\ref\{sec-summary\}']
+ - ['See Appendix~\\ref\{sec-summary\}']
+---
+
+# Preface {.unnumbered}
+
+This is a book testing crossref appendix options in PDF (issue #11772).
diff --git a/tests/docs/smoke-all/2026/06/23/issue-11772/intro.qmd b/tests/docs/smoke-all/2026/06/23/issue-11772/intro.qmd
new file mode 100644
index 00000000000..f55602f21e4
--- /dev/null
+++ b/tests/docs/smoke-all/2026/06/23/issue-11772/intro.qmd
@@ -0,0 +1,17 @@
+---
+_quarto:
+ render-project: true
+ tests:
+ html:
+ # crossref.appendix-title ("Whatever") must drive the appendix
+ # cross-reference prefix in HTML too. This cross-file reference from a
+ # numbered chapter is resolved by the book post-render step, which
+ # previously only honored language.crossref-apx-prefix — issue #11772.
+ ensureFileRegexMatches:
+ - ['>Whatever A<']
+ - ['>Appendix A<']
+---
+
+# Introduction
+
+See @sec-summary.
diff --git a/tests/docs/smoke-all/2026/06/23/issue-11772/summary.qmd b/tests/docs/smoke-all/2026/06/23/issue-11772/summary.qmd
new file mode 100644
index 00000000000..8e2c15d0532
--- /dev/null
+++ b/tests/docs/smoke-all/2026/06/23/issue-11772/summary.qmd
@@ -0,0 +1,3 @@
+# Summary {#sec-summary}
+
+In summary, this book has no content whatsoever.