Skip to main content

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

  1. Manifest first. Add the section id to the registry comment at the top of qa-manifest.yaml, and add it to the expect_selectors (or absent_selectors) of every dataset where it should (or should not) appear. Run the suite — it goes red.

  2. Style via semantic tokens only. All styling goes through the Layer 2 semantic API (--color-*, --space-*, --font-*) in main.css. No raw hex, no new primitive tokens for a single section.

  3. Implement with the correct _display pattern. Sections follow one of two conventions, depending on their default state:

    • Content sections default to shown and use the unless-false pattern:

      {{#unless (eq _display.showMySection false)}}
      <section id="my-section">...</section>
      {{/unless}}
    • Banner-style sections that default to hidden (such as hold-banner, agent-banner, price-lines) use the positive pattern:

      {{#if _display.showHoldBanner}}
      <section id="hold-banner">...</section>
      {{/if}}
  4. 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 _display check.

  5. 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:

  1. Schema. A new JSON Schema with the tenant's $id pattern. Optional fields must be nullable (type: ["string", "null"]) because the data-transformation layer emits nulls; required fields stay strict. See Schemas.
  2. Sample data. A render-ready dataset that validates against the schema — the suite runs schema validation on every dataset.
  3. Manifest entry. A doc_types entry in qa-manifest.yaml declaring the schema, template path, and dataset with its selector expectations.
  4. Template folder. templates/<doc_type>/v1.0/template.html, using the canonical naming convention. See Templates.
  5. Blueprint action. A create_document_actions entry in blueprint.yaml so users can create the document. See The blueprint.
  6. 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.

  1. Add a create_document_actions entry in blueprint.yaml with hide_sections / show_sections lists. These translate into _display flags at generation time (hide_sections injects the flag as false; show_sections injects it as true, which is needed for sections whose default is hidden). Stay consistent with the token-to-flag mapping.
  2. Add a matching actions entry in qa-manifest.yaml with the same display_overrides and the selector expectations the variant must produce. When one blueprint action is tested against several datasets, use action_ref to 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_validity block in blueprint.yaml carries per-action price_validity text (rendered in the document) and proposal_expired messages (the viewer banner after expiry), plus the superseded-version banner copy.
  • Email subjects and bodies — each action's email_template block. 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:

  1. Extend qa-manifest.yaml with the new expectation — the suite goes red.
  2. Implement the change in schema, template, blueprint, or payment configuration.
  3. Run the test harness until green, including the action matrix.
  4. Check the change against the quality bar before opening a merge request.