Cross-References and Section Numbering¶
When a template uses {% if %} to conditionally include sections, the section numbering changes depending on the user's answers. A reference like "see section 3.2" might become wrong if section 2 was removed and section 3.2 is now section 2.2.
VIBE handles this automatically for both Markdown and Word (DOCX) templates, but the two formats work differently due to their different natures.
How It Works¶
After your template is rendered with the user's answers, VIBE scans the resulting document for headings and internal references, then updates the references to match the actual numbering.
The key insight: you don't hardcode section numbers. Instead, you create a stable anchor (a name that doesn't change) and link to it. VIBE fills in the correct number.
Markdown Templates¶
Markdown headings (#, ##, ###, etc.) don't have built-in numbering -- any visible numbers like "1.2.3" are added by the presentation layer (CSS, PDF renderer, etc.) based on heading position. This means the source Markdown never contains literal section numbers in headings.
But cross-references in the body text often do contain numbers: "see section 1.2" is a specific string that needs to update when sections shift. VIBE's Markdown reference updater handles this.
Creating an Anchor¶
Place an HTML anchor tag immediately before your heading:
<a id="limited-liability"></a>
## Limited liability
The supplier's total aggregate liability under this agreement
shall not exceed the total fees paid during the preceding
twelve-month period.
The anchor name (limited-liability) is a stable identifier that doesn't change when sections are added or removed. Choose descriptive names that reflect the content, not the position.
You don't always need an explicit <a id> tag. VIBE automatically creates anchors from heading text by converting it to a slug (lowercase, spaces to hyphens, punctuation removed). For example, ## Risk Assessment gets the anchor risk-assessment. You can link to it with [X](#risk-assessment) without any <a id> tag.
Explicit anchors are recommended when you need stability (the anchor survives if you rename the heading) or when duplicate headings make the auto-generated slug ambiguous.
Linking to a Section¶
Use a standard Markdown link with a # anchor:
The display text (1.2 in this example) is what VIBE updates. After rendering, if "Limited liability" is actually section 1.3 (because an earlier section was removed by a conditional), VIBE rewrites the link to [1.3](#limited-liability).
Any display text works. You don't need to guess the correct number. All of these are equivalent:
[1.2](#limited-liability)
[X.X](#limited-liability)
[see here](#limited-liability)
[section number TBD](#limited-liability)
They all get replaced with the computed section number (e.g., [1.3](#limited-liability)).
Section Numbering Scheme¶
Markdown has no intrinsic numbering, so VIBE needs to know how your document numbers its sections. By default, it uses decimal dotted numbering (1, 1.1, 1.1.1), matching the most common CSS counter setup.
If your document uses a different scheme, configure it in config.yml:
Available styles:
| Style | Level 1 | Level 2 | Level 3 |
|---|---|---|---|
decimal |
1, 2, 3 | 1.1, 1.2 | 1.1.1, 1.1.2 |
alpha |
A, B, C | A.1, A.2 | A.1.1, A.1.2 |
roman |
I, II, III | I.1, I.2 | I.1.1, I.1.2 |
These are the same styles available for appendix numbering. For custom schemes (like legal-style A(1)(i)), see Custom Numbering Schemes below.
Full Example¶
A contract template with conditional sections:
<a id="definitions"></a>
## Definitions
In this agreement, the following terms have the meanings set out below.
{% if include_services %}
<a id="services"></a>
## Services
The supplier shall provide the services described in Appendix A.
{% endif %}
<a id="liability"></a>
## Liability
{% if include_indemnity %}
<a id="indemnity"></a>
### Indemnity
Each party shall indemnify the other against losses arising from
breach of this agreement.
{% endif %}
<a id="liability-cap"></a>
### Limitation of liability
The limitations set out in this section [X](#liability) apply to all
claims. See also [X](#indemnity) for indemnity provisions.
## General provisions
The definitions in section [X](#definitions) apply throughout.
The liability provisions in section [X](#liability-cap) shall
survive termination.
When include_services is false and include_indemnity is true, the rendered output becomes:
## Definitions <!-- section 1 -->
## Liability <!-- section 2 (was 3) -->
### Indemnity <!-- section 2.1 -->
### Limitation <!-- section 2.2 -->
## General provisions <!-- section 3 -->
And the references update to:
[X](#liability)becomes[2](#liability)[X](#indemnity)becomes[2.1](#indemnity)[X](#definitions)becomes[1](#definitions)[X](#liability-cap)becomes[2.2](#liability-cap)
Missing References¶
If a conditional removes a section that other parts of the document reference, VIBE leaves the link text unchanged and logs a warning. A missing cross-reference usually indicates a template logic issue -- the section was removed but its references were not made conditional.
Best practice: Wrap references in the same conditional as the section they point to:
Duplicate Headings¶
In legal and regulatory documents, the same heading text often appears multiple times (e.g., "Compliance" under different parent sections). Since VIBE auto-generates anchors from heading text, both headings would get the same slug (compliance). When multiple headings share a slug, VIBE resolves links to the first matching heading.
For duplicate headings you need to reference individually, use explicit <a id> anchors:
<a id="compliance-eu"></a>
### Compliance
... EU-specific compliance ...
<a id="compliance-us"></a>
### Compliance
... US-specific compliance ...
See [X](#compliance-eu) for EU requirements and
[X](#compliance-us) for US requirements.
Word (DOCX) Templates¶
Word documents have a built-in reference system using bookmarks and REF fields. When you insert a cross-reference in Word (Insert > Cross-reference), Word creates a bookmark on the target heading and a REF field at the reference location. This is the same mechanism used by Word's "Update Fields" feature.
VIBE's DOCX reference updater works with this native system. After template rendering (which may remove sections via {% if %} conditionals, just like Markdown), it:
- Parses the document's numbering definitions (the
numbering.xmlpart) - Walks all headings and recalculates their numbers based on the actual document structure
- Updates all REF fields to show the correct numbers
- Updates Table of Contents entries
For Template Authors¶
If you are authoring DOCX templates, cross-references work the way you would expect in Word:
-
Create the reference target: Your heading should already have Word's built-in numbering (via styles like Heading 1, Heading 2, etc.)
-
Insert a cross-reference: Use Word's Insert > Cross-reference dialog, select the heading, and choose what to display (heading number, heading text, etc.)
-
Template conditionals: When you wrap sections in
{% if %}...{% endif %}tags, any REF fields pointing to those sections will be handled by VIBE on render
Unlike Markdown, you don't need to configure a numbering scheme -- VIBE reads it directly from the document's numbering definitions (which Word maintains as part of the .docx file format).
Missing References in DOCX¶
The same principle applies: if a conditional removes a section, references to it become broken. VIBE replaces the reference text with "Error! Reference source not found." (matching Word's own behavior when it encounters a broken field reference).
Custom Numbering Schemes¶
For documents that use non-standard numbering (common in legal drafting), you can define a custom scheme in config.yml:
# Legal-style: A(1)(i)
numbering_style:
levels:
1:
format: alpha # A, B, C...
2:
format: decimal # 1, 2, 3...
prefix: "("
suffix: ")"
3:
format: roman-lower # i, ii, iii...
prefix: "("
suffix: ")"
This produces numbering like A, A(1), A(1)(i), B, B(1), etc.
Available formats for each level:
| Format | Output |
|---|---|
decimal |
1, 2, 3... |
alpha |
A, B, C... |
alpha-lower |
a, b, c... |
roman |
I, II, III... |
roman-lower |
i, ii, iii... |
Each level can have optional prefix and suffix strings that wrap the number (like the parentheses in (1) or (i) above).
Tips¶
- Use descriptive anchor names:
limited-liabilityis better thansection-3-2(which encodes a position that may change). - Anchor names are case-sensitive:
#Riskand#riskare different anchors. - One anchor per referenced section: Only sections that are actually cross-referenced need explicit anchors. Unreferenced sections are fine without them.
- Test with conditionals: Run your template with different answer combinations to verify that references update correctly when sections appear and disappear.