Use Context Variables

The main goal of Antora extensions is to allow you to write code that hooks into the generation process at key transition points and to access variables that are flowing through the system at that time. The fun with extensions really starts once you start to access these context variables.

Access context variables

A context variable is a variable that is in scope at the time an event is fired and that the generator binds to the generator context. By accessing a context variable from an extension, you can:

  • read properties from the object,

  • call methods on the object, or

  • modify properties on the object (provided the object is not frozen).

In Update context variables, you’ll learn how to replace the variable with a proxy of the object, which is another option.

The first positional parameter of each event listener is an object of context variables. You should use object destructuring to pick individual variables out of this object (e.g., { playbook }). The in-scope variables for each event are defined on the Generator Events Reference page.

Let’s build on our extension to retrieve the site catalog and add a .nojekyll file to it as an alternative to using the supplemental UI for this purpose.

Example 1. nojekyll-extension.js
module.exports.register = function () {
  this.on('beforePublish', ({ siteCatalog }) => {
    siteCatalog.addFile({ contents: Buffer.alloc(0), out: { path: '.nojekyll' } })
  })
}

In Example 1, the site catalog is retrieved from the context using { siteCatalog }. To retrieve multiple variables, separate the variable names using commas (e.g., { playbook, siteCatalog }).

Context variables can also be retrieved directly from the generator context using the getVariables method:

const { siteCatalog } = this.getVariables()

In addition to the built-in context variables, your extension can also access context variables documented and published by other extensions.

Update context variables

While most extensions read context variables and interact with the methods of the referenced object, they can also add or replace context variables. One use case is to define new variables that other extensions or listeners of the same extension can access. This is one way to pass additional data through the generator. Another use case is to replace a built-in variable used by the generator, perhaps by proxying it. You may want to do this if you need to drastically alter Antora’s behavior and you can’t do it by adding or removed files from a catalog.

Let’s consider the case where we want to modify the playbook to remove private content sources. We can’t change properties on the playbook because the object is frozen. But we can create a new playbook, change it’s properties, and pass our copy back to the context. The following example shows how this would work by listening to the playbookBuilt event (when the playbook is not yet locked) and creating a replacement.

Example 2. exclude-private-content-sources-extension.js
module.exports.register = function () {
  this.on('playbookBuilt', function ({ playbook }) {
    playbook = JSON.parse(JSON.stringify(playbook))
    playbook.content.sources = playbook.content.sources.filter((source) => !source.url.startsWith('git@'))
    this.updateVariables({ playbook })
  })
}

Notice that the example uses the formal function keyword to declare the listener instead of an arrow function. Defining the function this way gives us access to the standard this keyword, which is a reference to the generator context. When the listener is registered, Antora binds the function to the generator context, making the generator context accessible within the function using the standard this keyword.

Let’s consider another case where we proxy the content catalog to prevent it from registering any aliases. In Example 3, we will listen for the contentClassified event, retrieve the contentCatalog context variable, and replace the variable with a proxy of the object.

Example 3. Replace variable with a proxy of the object
module.exports.register = function () {
  this.on('contentClassified', function ({ contentCatalog }) {
    contentCatalog = new Proxy(contentCatalog, {
      get(target, property) {
        return property === 'registerPageAlias' ? () => undefined : target[property]
      },
    })
    this.updateVariables({ contentCatalog })
  })
}

Example 3 gives you the starting point to replace the registerPageAlias function with your own implementation.

Context variable locking

Once a built-in context variable is deemed established, which is typically after the event in which it was introduced is emitted, that variable becomes locked. There are exceptions to this rule, but by-in-large it holds. A variable that is locked can’t be replaced. Any attempt to do so results in an error.

The built-in variables that are locked, and when they’re locked, are indicated on the Generator Events Reference page.

The reason built-in variables are locked is two fold. First, it signals when a variable should be replaced if it must be. Second, it allows the site generator and other extensions to store a local reference to that variable without having to worry about checking whether it was replaced.

A locked variable only prevents that variable itself from being replaced. It’s still possible to modify the object that the variable references, such as to add, update, or remove a property of the object. The one exception is the playbook, which is a frozen object.