Embedding a Quarto book within a Quarto website
This report was generated using AI under general human direction. At the time of generation, the contents have not been comprehensively reviewed by a human analyst.
Overview
Quarto does not natively support nesting a book project inside a website project - each _quarto.yml can only declare one project.type. The workaround used in this repository is to treat the book as a separate, independent Quarto project living in a subdirectory (book/), and to stitch its rendered output into the website’s output directory as a post-render step. The two projects are rendered separately and combined only at the file-system level, so each keeps its own configuration, navigation, and (optionally) theme.
At a high level:
flowchart TD
A["quarto render (site root)"] --> B["Renders *.qmd into _site/"]
A --> C["post-render.sh runs"]
C --> D["quarto render book (book/_quarto.yml)"]
D --> E["book/_book/ output"]
C --> F["copy book/_book/ -> _site/book/"]
B --> G["Combined _site/ with site pages + /book/ subfolder"]
F --> G
Step-by-step setup
The sections further down explain the configuration in this repository conceptually. This section walks through the concrete commands and file edits needed to set the same thing up from a clean website project.
1. Create the book as a subdirectory project
From the root of an existing Quarto website project, scaffold a new book project inside a subdirectory (here, book/):
quarto create project book bookThis creates book/_quarto.yml, book/index.qmd, and a couple of starter chapters, all scoped to that subdirectory. Edit book/_quarto.yml to set the book’s title, author, chapter list, and any book-specific format options (see Book configuration).
2. Exclude the book from the website’s own render
Open the website’s top-level _quarto.yml and add a render list under project that excludes the book directory:
project:
type: website
render:
- "*.qmd"
- "!book/"Without this, running quarto render at the root would attempt to render book/*.qmd a second time as ordinary website pages, which conflicts with the book project’s own rendering of those same files.
3. Write the post-render script
A shell script is a plain text file containing a sequence of commands to be run by a command-line shell (e.g., sh, bash, or zsh) - the same interpreter used for typing commands interactively in a terminal. Rather than entering commands one at a time, they’re saved in a file and executed together, in order, top to bottom.
The first line, #!/bin/sh, is a shebang: it tells the operating system which interpreter should run the rest of the file. Before a script can be run directly (e.g., ./post-render.sh), it also needs the executable permission bit set via chmod +x.
Create a shell script at the project root (e.g., post-render.sh) that renders the book and copies its output into the website’s output directory:
#!/bin/sh
set -e
quarto render book
mkdir -p _site/book
rm -rf _site/book/*
cp -R book/_book/. _site/book/Make it executable so Quarto can invoke it directly:
chmod +x post-render.sh4. Register the post-render hook
Back in the website’s _quarto.yml, tell Quarto to run the script after the site renders:
project:
type: website
render:
- "*.qmd"
- "!book/"
post-render:
- ./post-render.sh6. Exclude build artifacts from version control
Add both output directories to .gitignore (unless there’s a specific reason to commit rendered HTML):
/_site/
/book/_book/
7. Render and verify
From the project root:
quarto renderThen confirm the book was stitched in correctly:
ls _site/book/ # should contain index.html and other book pages
open _site/book/index.html # spot-check in a browser (macOS)If _site/book/ is missing or stale, check that post-render.sh is executable and that quarto render book succeeds on its own (run it directly to see book-specific errors in isolation).
Project structure
The repository contains two independent Quarto projects:
askurz/
|-- _quarto.yml # website project
|-- post-render.sh # stitches the book into _site/
|-- index.qmd, about.qmd, styles.css, ...
`-- book/
|-- _quarto.yml # book project (independent)
|-- index.qmd, intro.qmd, summary.qmd, references.qmd
`-- references.bib
Because book/ has its own _quarto.yml with project: type: book, Quarto treats it as a self-contained project with its own render list, output directory (book/_book/ by default), and options - completely separate from the parent site.
Website configuration
The parent _quarto.yml needs three things to make this work:
project:
type: website
render:
- "*.qmd"
- "!book/"
post-render:
- ./post-render.sh
website:
navbar:
left:
- href: book/index.html
text: Bookrender: ["*.qmd", "!book/"]tells the website project to render top-level.qmdfiles but explicitly exclude thebook/directory. Without this exclusion, Quarto would try to render the book’s chapters as ordinary website pages using the website’s format/theme, conflicting with the book’s own settings.post-render: [./post-render.sh]runs a shell script after the site finishes rendering, which is where the book gets built and copied in.- The navbar link points to
book/index.html- a path that only exists after the post-render step has copied the book’s output into_site/book/.
Book configuration
book/_quarto.yml is a normal, standalone book project:
project:
type: book
book:
title: "My Book"
chapters:
- index.qmd
- intro.qmd
- summary.qmd
- references.qmd
format:
html:
theme:
- cosmo
- brandNothing here is aware of the parent website. It renders exactly as it would if book/ were its own repository, using quarto render book and producing output in book/_book/.
The stitching step
post-render.sh is the glue between the two projects:
#!/bin/sh
set -e
quarto render book
mkdir -p _site/book
rm -rf _site/book/*
cp -R book/_book/. _site/book/Steps:
quarto render bookbuilds the book project independently, producingbook/_book/.- The script ensures
_site/book/exists and is emptied of any stale content. - The book’s rendered HTML, assets, and search index are copied into
_site/book/, so the final site serves the book at/book/alongside the rest of the pages.
set -e ensures the whole render fails loudly if the book fails to build, rather than silently publishing a stale or missing book.
Rendering workflow
To produce the combined site from a clean checkout:
quarto renderThis triggers, in order:
- Quarto renders the website project’s own
.qmdfiles into_site/. - Quarto’s
post-renderhook runspost-render.sh. - The script renders
book/and copies its output into_site/book/.
A few practical implications:
quarto previewlimitations: Quarto’s live-preview server does not re-runpost-renderhooks on every file save, so book content may appear stale or missing duringquarto previewof the site. Use a fullquarto render(or preview the book project separately frombook/) when working on book content.- Build order matters for links: because the navbar references
book/index.htmlas a static path rather than a project-relative Quarto link, Quarto does not validate that link at render time - a typo or a failed book render will only surface as a broken link at runtime.
Adding a link back to the website from the book
The website’s navbar links into the book (book/index.html), but nothing links the other way by default - a reader who lands inside the book has no built-in way back to the main site. Quarto books support a sidebar: tools: list (the same mechanism used for GitHub/sharing icons in book templates), which renders as small icon buttons next to the book title in the sidebar header. Adding an entry there that points at the site root gives every book page a persistent “back to site” button:
book:
title: "My Book"
sidebar:
tools:
- icon: house-door-fill
text: "Back to askurz"
href: ../index.htmlThe href is ../index.html rather than index.html because of how the two projects’ outputs are combined: post-render.sh copies the flat contents of book/_book/ into _site/book/, so every book page (_site/book/index.html, _site/book/intro.html, etc.) sits one directory below the site root. icon accepts any name from the Bootstrap Icons set, and text supplies the tooltip/aria-label shown on hover.
This relative link only resolves correctly once the book has been stitched into _site/book/ by the post-render step. Running quarto preview book on the book project by itself will treat ../index.html as one level above book/_book/ - i.e., into book/, not the real site - so the button will look broken until a full quarto render is run from the site root.
If a more visible affordance than an icon button is preferred, the same href can instead be added to a book: page-footer entry, which appears as a text link at the bottom of every page rather than in the sidebar header.
Gotchas and recommendations
- Theming consistency: both projects currently share the
cosmo+brandtheme, but thestyles.cssapplied to the website is not automatically applied to the book. If visual consistency matters, add the same CSS (or reference a shared file) inbook/_quarto.yml. - Git-ignoring build artifacts:
book/_book/and the top-level_site/are build outputs and should be excluded from version control (via.gitignore) unless there’s a specific reason to check in rendered HTML. - CI/deployment: any deployment pipeline (e.g., GitHub Actions, Posit Connect) must run
quarto renderfrom the site root - rendering onlybook/or only the top-level site in isolation will produce an incomplete_site/. - Cross-linking: links from website pages into specific book chapters (or vice versa) must use plain relative URLs (e.g.,
book/intro.html) rather than Quarto’s.qmd-based cross-reference syntax, since the two are independent projects and Quarto cannot resolve links across them. - Search: if both projects use Quarto’s built-in search, each will index only its own project; there is no unified search across the site and the embedded book without additional tooling.