Embedding a Quarto book within a Quarto website

Note

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 book

This 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

NoteWhat is a shell 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.sh

4. 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.sh

6. 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 render

Then 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: Book
  • render: ["*.qmd", "!book/"] tells the website project to render top-level .qmd files but explicitly exclude the book/ 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
      - brand

Nothing 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:

  1. quarto render book builds the book project independently, producing book/_book/.
  2. The script ensures _site/book/ exists and is emptied of any stale content.
  3. 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 render

This triggers, in order:

  1. Quarto renders the website project’s own .qmd files into _site/.
  2. Quarto’s post-render hook runs post-render.sh.
  3. The script renders book/ and copies its output into _site/book/.

A few practical implications:

  • quarto preview limitations: Quarto’s live-preview server does not re-run post-render hooks on every file save, so book content may appear stale or missing during quarto preview of the site. Use a full quarto render (or preview the book project separately from book/) when working on book content.
  • Build order matters for links: because the navbar references book/index.html as 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.

Gotchas and recommendations

  • Theming consistency: both projects currently share the cosmo + brand theme, but the styles.css applied to the website is not automatically applied to the book. If visual consistency matters, add the same CSS (or reference a shared file) in book/_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 render from the site root - rendering only book/ 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.