Skip to main content

Using Quarto extensions

Overview

Extensions add capabilities to Quarto projects and documents. An extension lives in a directory under _extensions/ at your project root, described by an _extension.yml manifest:

my-project/
  _quarto.yml
  _extensions/
    acme/                  # optional organization directory
      fancy-docs/
        _extension.yml     # the extension manifest
        theme.scss         # files bundled with the extension
        assets/...

The manifest declares what the extension contributes:

_extension.yml
title: Fancy Docs
author: Acme
version: 1.0.0
contributes:
  # one or more of:
  shortcodes: [...]
  filters: [...]
  formats: {...}
  project: {...}
  metadata: {...}

This page covers installing extensions, project types, and metadata contributions. Shortcodes and filters have their own pages (Shortcodes, Lua filters).

Installing extensions

q2 add installs an extension from GitHub, an archive URL, or a local folder or archive:

q2 add quarto-ext/fontawesome          # GitHub: <org>/<repo>
q2 add quarto-ext/fontawesome@v1.2.0   # a tag or branch
q2 add org/repo/subdir                 # an extension in a repository subfolder
q2 add https://example.com/ext.zip     # a .zip or .tar.gz URL
q2 add ./my-extension                  # a local folder or archive

It prints what it will do ([Install], [Update] 1.0.0 -> 1.1.0, [Revert], or [No Change]) and asks before continuing. Extensions from GitHub go under the organization’s folder, as in _extensions/quarto-ext/fontawesome/.

q2 add works like Quarto 1’s quarto add, with these differences:

  • Remote sources must be trusted. Extensions can run code when documents render, so q2 add asks whether you trust the authors, and the default answer is no. When it cannot ask (--no-prompt, --json, CI), a remote source is refused unless you pass --trust.
  • It installs at the project root, even when you run it from a subfolder. Outside a project it installs in the current folder. --dir <folder> overrides both.
  • It records the install. Each extension gets a .quarto-source.yml next to its _extension.yml, with the source and a hash of every file. Don’t edit this file. Commit it along with the extension.
  • It protects your local changes. If you edited an installed extension’s files, q2 add refuses to replace them and lists what changed. Pass --force to replace them anyway. --force also reinstalls an extension that reports [No Change]. Extensions installed by Quarto 1 or copied by hand have no record, so their changes can’t be detected. q2 add updates them and prints a note.
  • [No Change] writes nothing.

Other options: --dry-run shows the table without writing, and --json prints a machine-readable result.

A dry run of a remote source still downloads it to build the table, so it needs the same trust as a real install. In a terminal, q2 add --dry-run asks the trust question before it downloads. Without a terminal, pass --trust with --dry-run, for example q2 add quarto-ext/fontawesome --dry-run --trust --no-prompt. --force cannot be combined with --dry-run, because a dry run never overwrites anything.

Custom project types

An extension can define a custom project type: a named bundle of configuration that projects opt into by setting project.type. This is how a team ships a house style — theme, navigation defaults, analytics — as one reusable package.

In your project:

_quarto.yml
project:
  type: fancy-docs
website:
  title: "My Site"

In the extension:

_extensions/acme/fancy-docs/_extension.yml
title: Fancy Docs
contributes:
  project:
    project:
      type: website        # the built-in base type
    website:
      bread-crumbs: true
      navbar:
        logo: assets/logo.svg
    format:
      html:
        theme: [cosmo, theme.scss]
        include-in-header:
          - assets/analytics.html

Everything under contributes: project: is an ordinary _quarto.yml fragment. When Quarto sees type: fancy-docs, it:

  1. finds the extension by name (use the full acme/fancy-docs form in type: if two extensions share a name);
  2. reads the base type from the fragment’s project.type — the built-in type the project actually renders as (website or default);
  3. merges the fragment underneath your _quarto.yml.

The project then behaves exactly as if you had written the combined configuration yourself.

Merge rules

Quarto’s standard configuration merging applies, uniformly:

  • Your value wins wherever both you and the extension set the same option.
  • Lists combine: the extension’s entries come first, then yours. In the example above, a project-level include-in-header is appended after the extension’s analytics include.
  • To replace an extension-provided list instead of appending, tag your value with !prefer:

    _quarto.yml
    format:
      html:
        include-in-header: !prefer
          - only-mine.html

Bundled files

Files the extension ships next to its manifest — SCSS themes, CSS, header includes, favicons, logos, scripts — are referenced with paths relative to the extension directory, as in the example above. Quarto resolves them automatically; your project does not need to spell out _extensions/acme/fancy-docs/theme.scss.

A path that does not name a bundled file keeps its _quarto.yml meaning: it is relative to the project root. A navbar or sidebar entry with href: index.qmd links to the project’s own index.qmd, not to a file inside the extension.

The same resolution applies to format contributions (contributes: formats:): a format extension can bundle theme SCSS, CSS, or header includes next to its manifest and reference them by bare name. A theme: entry that names a .scss/.css file Quarto cannot find — bundled or otherwise — fails the render with Q-14-4.

Errors you might see

  • Unknown project type (Q-5-17): project.type is neither built-in nor provided by any extension in _extensions/. The message lists the project-type extensions that were found — check for typos, and check whether the extension failed to load.
  • Invalid project type contribution (Q-16-7): the extension declares a base type that is not available. Custom types must use a built-in base (website or default); book and manuscript bases are not supported yet, and custom types cannot chain to other custom types.

Metadata contributions

An extension can also contribute configuration unconditionally — applied whenever the extension is installed, without being named in project.type. This is contributes: metadata::

_extensions/posit-dev/quarto-openapi/_extension.yml
title: quarto-openapi
contributes:
  metadata:
    project:
      pre-render:
        - openapi-to-markdown.ts
  • Keys under metadata.project merge into your project configuration — the example adds a pre-render script that runs before every project render.
  • Other keys under metadata merge into every document’s metadata.

The same merge rules apply: your own configuration always wins over extension contributions, and lists combine with the extension’s entries first.