Tailoring documents
Beyond rebranding, most tenants tailor what their documents contain: new sections, new document types, new action variants, and copy changes. Every one of these follows the same test-driven workflow — extend the QA manifest first (red), then implement (green). The manifest is the contract the test harness enforces; see How QA works.
Adding a new section to a template
-
Manifest first. Add the section id to the registry comment at the top of
qa-manifest.yaml, and add it to theexpect_selectors(orabsent_selectors) of every dataset where it should (or should not) appear. Run the suite — it goes red. -
Style via semantic tokens only. All styling goes through the Layer 2 semantic API (
--color-*,--space-*,--font-*) inmain.css. No raw hex, no new primitive tokens for a single section. -
Implement with the correct
_displaypattern. Sections follow one of two conventions, depending on their default state:-
Content sections default to shown and use the unless-false pattern:
-
Banner-style sections that default to hidden (such as
hold-banner,agent-banner,price-lines) use the positive pattern:
-
-
Add the empty-collapse guard. Every section also collapses entirely when its backing data is empty — no headers over missing data, regardless of display flags. Wrap the section in a data presence check as well as its
_displaycheck. -
Consider all five sample datasets. The proposal document ships five vertical variants — touring, fit, cruising, hybrid-uk, hybrid-us — and the suite renders the section against every one of them. A section that assumes day-based itineraries will break the services-based variant; declare the correct expectations per dataset.
Adding a new document type
Work through the artifacts in this order — each feeds the next:
- Schema. A new JSON Schema with the tenant's
$idpattern. Optional fields must be nullable (type: ["string", "null"]) because the data-transformation layer emits nulls; required fields stay strict. See Schemas. - Sample data. A render-ready dataset that validates against the schema — the suite runs schema validation on every dataset.
- Manifest entry. A
doc_typesentry inqa-manifest.yamldeclaring the schema, template path, and dataset with its selector expectations. - Template folder.
templates/<doc_type>/v1.0/template.html, using the canonical naming convention. See Templates. - Blueprint action. A
create_document_actionsentry inblueprint.yamlso users can create the document. See The blueprint. - Data transformation coverage. Confirm the document type is covered by your data-transformation configuration — some document types map inside the master transformation, others need their own file. See Data transformation.
Adding an action variant
An action variant is the same document type rendered with different sections — for example a no-price proposal, or a trade proposal that surfaces the line-level breakdown.
- Add a
create_document_actionsentry inblueprint.yamlwithhide_sections/show_sectionslists. These translate into_displayflags at generation time (hide_sectionsinjects the flag asfalse;show_sectionsinjects it astrue, which is needed for sections whose default is hidden). Stay consistent with the token-to-flag mapping. - Add a matching
actionsentry inqa-manifest.yamlwith the samedisplay_overridesand the selector expectations the variant must produce. When one blueprint action is tested against several datasets, useaction_refto bind extra test entries to the existing action.
The suite then renders the variant on every change, so a later template edit cannot silently break it.
Changing copy
Copy lives in configuration, not templates, wherever it varies per action or brand:
- Document validity wording — the
document_validityblock inblueprint.yamlcarries per-actionprice_validitytext (rendered in the document) andproposal_expiredmessages (the viewer banner after expiry), plus the superseded-version banner copy. - Email subjects and bodies — each action's
email_templateblock. The shared body resolves all brand values from{{branding.*}}; keep it that way. - Payment page copy — the message blocks in
payment.yaml, for example the eligibility rule messages (title,body) shown when payment is blocked.
Design laws — do not break these
Each of these rules protects against a class of failure the test suite checks for. Changing one is a design decision to raise with Kaptio, not a refactor.
Frozen document-type names
option, booking_confirmation, invoice, cancel_invoice, commission_invoice,
commission_statement, financial_summary, final_travel_docs. These strings
propagate into schema $ids, template folder names, data-transformation files, and
cross-tenant tooling. Renaming one breaks all of them at once. Trim the set if you do
not need a document type; never re-spell what remains.
tour.departureType is required and never defaulted
The departure type (Fixed, Anyday, or Seasonal) selects the content source and
the itinerary layout. A silent Fixed default renders tailor-made documents as broken
tours — and, worse, hides the data bug that caused the value to be missing. If the
field is absent, that is a data problem to fix, not a default to paper over.
Numeric pricing only
All monetary values are numbers in the document's currency; formatting happens at render time based on the document's locale. Never put preformatted amounts or raw currency codes in data prose, and never put amounts in inclusions copy — no-price documents render inclusions, so an amount written into inclusions text leaks pricing into a document that must show none.
Commission fields only in commission schemas
The trade firewall is data-level: guest documents never receive commission vocabulary of any kind. Commission fields exist exclusively in the commission document family. A template bug can therefore never expose commission data to a guest, because the data was never delivered to the guest template in the first place.
Structured-disabled configuration, never commented-out
Optional capability ships as a real YAML object with enabled: false and named
placeholder tokens (for example :onRequestStatusId), plus an in-body setup note.
Commented-out configuration dies in forks — it cannot be validated, linted, or tested,
and it gets deleted by accident. Forks fill the tokens and flip the flag; see the
fork playbook.
Helpers must exist in the platform render registries
A document renders in more than one platform context, and a Handlebars helper present
in one render registry but not another renders fine in preview and fails in
production. Run the helper parity gate before using a new helper (see
Running the test harness). Inline partials are safe;
and/or/not helpers do not exist — nest blocks instead.
Workflow summary
Whatever you tailor, the loop is the same:
- Extend
qa-manifest.yamlwith the new expectation — the suite goes red. - Implement the change in schema, template, blueprint, or payment configuration.
- Run the test harness until green, including the action matrix.
- Check the change against the quality bar before opening a merge request.