Asciidoctor PDF lets you display
{chapter-title}
in running headers, but that always mirrors the == literal heading text.
There is no built-in mechanism to show alternative text in the header from what
appears as the chapter heading in the body.
This Gist shows a minimal, self-contained solution using a Ruby extension that
patches SectionInfoByPage#[]= (the internal class that Asciidoctor PDF uses
to record which chapter is current for each page).
-
book.adoc– the AsciiDoc document -
theme.yml– the PDF YAML theme -
running-header.rb– the Ruby extension -
a minimal command line
-
book.pdf– the PDF result, and -
versions and operating systems details.
:doctype: book
:pdf-theme: ./theme.yml
= Book Title
[running-header="Ch 1 - changed 2026-04-01"]
== One
Chapter One’s text. Note the header is picked up from the _running-header_
attribute, so this page, and the following two pages, have “Ch 1 - changed
2026-04-01” as the header.
<<<
More of Chapter One’s text....
<<<
Final page of Chapter One’s text....
[running-header="Ch 2 - changed 2026-03-31"]
== Two
Chapter Two’s text. Note the header is picked up from the _running-header_
attribute, so this page and the next page have “Ch 2 - changed 2026-03-31” as
the header.
<<<
Final page of Chapter Two’s text....
== Three
Chapter Three’s text. Note there is no _running-header_ attribute, so the
Chapter heading is reflected in the header verbatim.
//vim:ft=asciidoc:tw=78:noet:norl:|
Note
|
The running-header attribute is set on the block attribute line
immediately before each == heading. Chapters without a running-header
attribute fall through to the default behaviour and display the heading text
as-is.
|
extends: default
header:
height: 48pt
line-height: 1
recto:
center:
content: '{chapter-title}'
verso:
center:
content: '{chapter-title}'Only the standard attribute, {chapter-title}, is used in the theme extension.
The extension patches the value that gets stored under that attribute for each
page.
module SectionInfoOverride
def []= pgnum, val
if ::Asciidoctor::Section === val && (override = val.attr('running-header', nil, false))
@table[pgnum] = { title: override, numeral: val.numeral }
else
super
end
end
end
Asciidoctor::PDF::SectionInfoByPage.prepend SectionInfoOverrideInside ink_running_content (in converter.rb, which should be in the gem
environment gemdir location – for me using WSL and Debian 13.4 that was
/var/lib/gems/3.1.0/gems/asciidoctor-pdf-2.3.9/lib/asciidoctor/pdf/),
Asciidoctor PDF builds a local variable chapters_by_page, which is an
instance of SectionInfoByPage. It iterates every page, finds which chapter
is current, and calls:
chapters_by_page[pgnum] = last_chapLater, for each page during the stamp pass, it reads:
doc.set_attr 'chapter-title', ((chap_info = chapters_by_page[pgnum])[:title] || '')So, {chapter-title} in the theme resolves to whatever :title was stored in
chapters_by_page for that page number.
SectionInfoByPage#[]= (in section_info_by_page.rb – in the same gems path
as converter.rb) is where the section node gets converted to
a { title:, numeral: } hash:
def []= pgnum, val
if ::Asciidoctor::Section === val
@table[pgnum] = { title: val.send(*@title_method), numeral: val.numeral }
else
@table[pgnum] = { title: val }
end
endBecause SectionInfoByPage#[]= receives the raw section node, it has access to
all of the node’s attributes, including any running-header attribute
set in book.adoc. The prepended module (see [running-headerrb], above)
intercepts the storage step at the class level: if a running-header attribute
is present, the override title is written to @table instead of the heading
text; otherwise super fires and the literal chapter title is stored
unchanged. Since the extension is loaded via -r before processing begins,
the patch is in place for every chapters_by_page[pgnum] = last_chap call.
The result is that {chapter-title} resolves per page to the override string
on chapters that have one, and to the normal heading text on chapters that do
not.