Um Events aus co*pilot exportieren zu können, wurde ein flexibler Event-Export geschaffen. Hiermit lassen sich sowohl Dokumente wie z.B. Production Sheets auf Basis einzelner Events wie auch Auswertungen über mehrere Events hinweg generieren. Exporte werden auf Basis von Export-Konfigurationen durchgeführt. In einer solchen Export-Konfiguration wird festgelegt, welche Daten eines Events in welchem Format ausgespielt werden.

Über den Unterpunkt “Exporte” des Event-Menus können im Anschluss Events selektiert und exportiert werden. Zudem lassen sich einzelne Events auch über die Event-Detail-Seite exportieren.

Wir haben einige Beispiel-Exporte ausgearbeitet, die du als Grundlage für deine Anforderungen verwenden kannst. Komplexere Anpassungen oder komplett neuartige Exporte erfordern grundlegende Kenntnisse in Markdown/CSS und ggfs. JavaScript. Gerne können wir das für euch im Auftrag erledigen.

Für experimentierfreudige Menschen, empfehlen wir auch die Nutzung von ChatGPT. Hier für findet ihr einen superprompt zum sofortigen Einfügen für direkte Anweisungen und Beispiel.

Export-Konfiguration

Beim Anlegen einer neuen Export-Konfiguration in den Einstellungen werden folgende Felder belegt:

(1) Name

Name der Konfiguration

(2) Format

  • HTML
  • CSV

HTML

HTML ist eine Seitenbeschreibungssprache um Inhalte zu formatieren. Dieses Format eignet sich sehr gut, um Detailseiten auszugeben.

Ein HTML-Export kann als Word oder PDF File ausgegeben werden.

CSV

CSV ist ein gängiges Format um Informationen tabellarisch darzustellen. Hierbei werden Spalten mittels Komma oder Semikolon separiert.

(3) Default Seitenlayout

Das Seitenlayout ist für die Darstellung der Exporte nötig.

  • A4 Hochformat – Klassisches A4-Layout ohne Seitenpaginierung.
  • A4 Hochformat (Paged) – A4-Layout mit automatischer Seitenpaginierung. Der Export wird in echte Seiten aufgeteilt, mit Seitenumbrüchen und Seitenzahlen. Ideal für mehrseitige Dokumente wie Verträge oder Berichte. Alle mitgelieferten Beispiel-Templates sind für dieses Layout optimiert. Unterstützt eigene Kopf-/Fußzeilen und Seitenzahlen über CSS @page margin boxes.
  • A4 Querformat – Wie A4 Hochformat, aber im Querformat.
  • A4 Querformat (Paged)* – Wie A4 Hochformat (Paged), aber im Querformat.
  • Frei – Keine festen Seitenmaße. Geeignet für tabellarische Daten oder Exporte, die nicht als Druckseite dargestellt werden sollen.

(4) Beispiel Template laden

Hinterlegte Vorlagen laden. Beim Laden eines Beispiel-Templates wird automatisch das passende Seitenlayout gesetzt (z.B. “A4 Hochformat (Paged)” für alle HTML-Templates oder “Frei” für CSV-Exporte).

(5) Preprocessing Script (Fortgeschritten)

Hier können die Input-Daten via JavaScript transformiert werden. Der Rückgabewert dieses Scripts ersetzt die eigentlichen Input-Daten des Exports. Es ist nur der Funktions-Rumpf anzugeben, wobei die Input-Daten als ctx zur Verfügung stehen.

Hier ein Beispiel:

  return { ...ctx, 
  // Folgende Zeile ersetzt 
  // die ursprünglichen Events 
  events: ctx.events.map(e => e.start), 
 foo: "bar" 
}
  

(6) Template

Die Vorlage wird in einer Handlebars-kompatiblen Templating-Sprache verfasst. Auf Basis dieser Vorlage und den Input-Daten wird die Ausgabe generiert. Daten werden mittels Platzhalter (= “Handlebars Expressions”) übergeben, die in “{{ }}” eingeschlossen sind.

(7) Vertraulich

Wird ein Export als “vertraulich” deklariert, werden zudem vertrauliche Daten (wie z.B. vertrauliche Benutzerfelder, Deals und Angebote) im Export verfügbar.

Vertrauliche Exporte können nur von Benutzer:innen mit der Berechtigung “ACCESS_CONFIDENTIAL” oder “ADMIN” erzeugt werden.

(8) Ansicht

Eine Ansicht aus dem Eventgrid auswählen um Events für die Vorschau zu laden.

(9) Event(s) nach Datum oder Name suchen

Bestimmte Events anhand des Namens oder des Datums suchen, um diese für die Vorschau zu laden.

(10) Vorschau

Vorschau der Ausgabe

(11) Code

Erzeugten Code anzeigen

(12) Input-Daten

Geladene Beispiel-Daten anzeigen

Beispiel-Exporte

/docs-category/exporte/

Input-Daten

Aktuell steht die Liste an Events in der Vorlage zur Verfügung. Das können die selektierten Events auf der Export-Seite sein, alle Events, die eine Perspektive zurückliefert oder auch nur ein einzelnes Event, wenn ein Export von einer Event-Detail-Seite aus gestartet wird.

  {
  baseData: BaseDataPayload,
  events: EventExportPayload[]
}
  

BaseData (Stammdaten)

  BaseDataPayload {
    "addStreet": string
    "additionalName": string
    "city": string
    "countryIsoCode": string
    "createdAt": Date
    "createdById": Date
    "department": string
    "email": string
    "homepage": string
    "logo": ExportPayloadImage
    "memo": string
    "name": string
    "phone": string
    "street": string
    "updatedAt": Date
    "updatedById": string
    "vvk_email": string
    "zip": string
}
  

Event

  EventExportPayload {
  copilotUrl: string
  id: string
  slug: string
  status: string
  optionEnd?: Date
  kind: string
  start: Date
  name: string
  subTitle?: string
  eventType: string
  memo?: string
  createdAt: Date
  updatedAt: Date
  displayNames: {
    title: string
    eventTitleWithArtists: string
    eventTitleWithArtistsAndShows: string
    artists: string
    artistsAndShows: string
    rooms: string
    locationsWithRooms: string
    locationsWithCityAndRooms: string
    locationsWithAddressAndRooms: string
  }
  // Achtung deprecated **
  activePromotions: { name: string }[]
  images?: ExportPayloadImage[]
  files?: ExportPayloadFile[]
  links?: ExportPayloadLink[]
  locations?: {
    id: string
    name: string
    own: boolean
    address: ExportPayloadAddress
    operator?: {
      displayName: string
      email: string
      phone: string
    }
    contacts?: ExportPayloadContact[]
    benutzerfelder?: ExportPayloadCustomField[]
    images: ExportPayloadImage[]
    files: ExportPayloadFile[]
    rooms?: {
      name: string
      blocked: boolean
      images?: ExportPayloadImage[]
      files?: ExportPayloadFile[]
    }[]
  }[]
  artists?: {
    name: string
    description: string
    deals?: {
      name: string
      description: string
      guarantee: number
      artistPercentage: number
      breakEvenSales: number
    }[]
    show?: {
      slug: string
      name: string
      releasesAt?: Date
      memo?: string
      images?: ExportPayloadImage[]
      files?: ExportPayloadFile[]
      artistShowInformations?: ExportPayloadCustomField[]
    }
    contacts?: ExportPayloadContact[]
    images: ExportPayloadImage[]
    files: ExportPayloadFile[]
    links?: ExportPayloadLink[]
    artistInformations?: ExportPayloadCustomField[]
  }[]
  contacts?: ExportPayloadContact[]
  schedule?: {
    name: string
    date: Date
    memo: string
  }[]
  eventInformations?: ExportPayloadCustomField[]
  series?: {
    id: string
    title: string
    subTitle: string
  }[]
  offersOnEvent?: {
    offerStatus: string
    name: string
    description: string
    discount: number
    netTotal: number
    grossTotal: number
    netTotalByVat: Record<number, number>
    roomsOnOffer?: {
      room: string
      roomConfiguration?: {
        id: string
        name: string
      }
      description: string
      price: number
      discount: number
      vat: number
      netTotal: number
      grossTotal: number
      commodityGroup?: {
        name: string
        vatBusiness: number
        vatPrivate: number
      }
    }[]
    resourcesOnOffer?: {
      resource: string
      description: string
      amount: number
      unit: string
      price: number
      discount: number
      vat: number
      netTotal: number
      grossTotal: number
      categories?: string[]
      commodityGroup?: {
        name: string
        vatBusiness: number
        vatPrivate: number
      }
    }[]
  }[]
  ticketing?: {
    ticketOffice: string
    category: string
    contingent: number
    vat: number
    netPrice: number
    grossPrice: number
    timestamp: Date
    soldTickets: number
    saleTotal: number
    zeroPriceTickets: number
  }[]
  shopEvent?: {
    active: boolean
    start: Date
    name: string
    subTitle?: string
    description?: string
    location?: string
    image?: ExportPayloadImage
    url?: string
    fallbackUrl?: string
  }
}

type ExportPayloadContact = {
  contactId: string
  role: string
  displayName: string
  firstName?: string
  lastName?: string
  gender?: string
  title?: string
  salutation?: string
  letterSalutation?: string
  birthdate?: Date
  note?: string
  address: ExportPayloadAddress
  email: string
  phone: string
  contactInformations?: ExportPayloadCustomField[]
  links?: ExportPayloadLink[]
  linksByType?: Record<string, string>
  bankAccounts?: ExportPayloadBankAccount[]
  images?: ExportPayloadImage[]
  files?: ExportPayloadFile[]
}

type ExportPayloadCustomField = {
  name: string
  type: string
  value: any
  confidential?: boolean
  tags?: string[]
}

type ExportPayloadImage = {
  id: string
  name: string
  description: string
  tags: string[]
  sizes: Record<string, string>
}

type ExportPayloadFile = {
  id: string
  name: string
  description: string
  path: string
  preview: string
  download: string
  tags?: string[]
}

type ExportPayloadBankAccount = {
  type: string
  institute: string | null
  username: string | null
  owner: string | null
  iban: string | null
  bic: string | null
}

type ExportPayloadAddress = {
  street: string
  addStreet: string
  zip: string
  city: string
  country: string
  lat: string
  lng: string
}

type ExportPayloadLink = {
  type: string
  value: string
}
  

** Unser Promotion-Modul wird durch eine neue Version ersetzt. Diese befindet sich gerade noch in der Beta-Phase.
[activePromotions] wird daher in Kürze ersetzt.
Weiter Infos bitte via support@copilot.events

Helper-Funktionen

Standard

Folgende Standard-Helper und Block-Parameter aus der Handlebars-Spezifikation werden unterstützt:

HelperZweckKurz-Beispiel
ifBedingung — rendert Inhalt nur wenn Wert truthy ist{{#if x}}…{{/if}}
unlessNegation von if{{#unless x}}…{{/unless}}
eachIteration über Array oder Objekt{{#each items}}…{{/each}}
withWechsel des Kontexts auf ein Sub-Objekt{{#with obj}}…{{/with}}
lookupDynamischer Property-Zugriff per Variable{{lookup obj key}}

Innerhalb von {{#each}} und {{#with}} stehen außerdem die Block-Parameter @first, @last, @index, @key und @root zur Verfügung.

each

Die each-Funktion iteriert (Schleife) über ein angegebenes Array oder Objekt. Sie beginnt mit {{#each}} und endet mit {{/each}}.

Sie verlangt das Array bzw. Objekt als Parameter. Innerhalb der Schleife referenziert this das aktuelle Element, und es stehen die Block-Parameter @first, @last, @index, @key zur Verfügung.

Beispiel

Eine Schleife über alle events. Die Daten eines Events werden mit this.* angesprochen:

  {{#each events}}
<h1>{{{this.name}}}</h1>
<p>Datum: {{formatDate this.start "d.m.Y"}}</p>
{{/each}}
  

Optional kann mit as |element index| ein Alias für das aktuelle Element und den Index gesetzt werden — nützlich bei verschachtelten Schleifen, wo this bereits durch die innere Schleife belegt ist:

  {{#each events as |event idx|}}
<p>{{idx}}: {{event.name}}</p>
{{/each}}
  

if

Die if-Funktion rendert ihren Inhalt nur, wenn der übergebene Wert “truthy” ist (also z.B. nicht null, leerer String, false oder 0). Sie beginnt mit {{#if}} und endet mit {{/if}} und kann optional einen {{else}}-Zweig enthalten.

Beispiel

Alle Event-Informationen ausgeben, sofern ein Wert gesetzt ist:

  {{#each this.eventInformations}}

{{#if this.value}}
<p>{{ this.name }}</p>
{{ this.value }}
{{/if}}

{{/each}}
  

Mit {{else}} kann ein Alternativ-Zweig angegeben werden:

  {{#if this.subTitle}}
  <h2>{{this.subTitle}}</h2>
{{else}}
  <h2>Kein Untertitel</h2>
{{/if}}
  
includeZero

Standardmäßig wird 0 als “falsy” behandelt und der if-Block nicht gerendert. Soll 0 als gültiger Wert gelten (z.B. bei Verkaufszahlen oder Gagen), kann das Hash-Argument includeZero=true gesetzt werden:

  {{#if this.soldTickets includeZero=true}}
  Verkauft: {{this.soldTickets}}
{{/if}}
  

unless

Die unless-Funktion ist die Negation von if — sie rendert ihren Inhalt nur, wenn der Wert “falsy” ist. Sie beginnt mit {{#unless}} und endet mit {{/unless}} und kann ebenfalls einen {{else}}-Zweig enthalten.

Beispiel

Alle Event-Informationen außer “Gästeliste” auflisten:

  {{#each this.eventInformations}}
{{#unless (isEqual name "Gästeliste")}}
  <h3>{{this.name}}</h3>
  {{this.value}}
{{/unless}}
{{/each}}
  

In Kombination mit den Block-Parametern @first oder @last sehr praktisch, um z.B. einen Trenner nur zwischen Elementen einzufügen:

  {{#each events}}
<p>{{this.name}}</p>
{{#unless @last}}, {{/unless}}
{{/each}}
  

with

Die with-Funktion wechselt den aktuellen Kontext auf ein Sub-Objekt — innerhalb des Blocks beziehen sich Platzhalter ohne Pfad direkt auf dieses Sub-Objekt. Spart wiederholte Pfad-Präfixe.

Beispiel
  {{#with this.locations.[0].address}}
<p>{{street}}<br/>{{zip}} {{city}}</p>
{{/with}}
  

Optional kann mit as |alias| ein Alias gesetzt werden, der auch in tieferen Schleifen weiter ansprechbar bleibt:

  {{#with (contactByRole this.contacts "Mieter") as |mieter|}}
<p>{{mieter.displayName}}</p>
<p>{{mieter.email}}</p>
{{/with}}
  

lookup

Funktion, um per Variable auf ein Property zuzugreifen — nützlich, wenn der Schlüssel zur Render-Zeit nicht statisch ist. Verlangt zwei Parameter:

  • Ein Objekt, z.B. einen Kontakt
  • den Namen des Elements (statisch oder als Variable)
Beispiel
  {{ lookup (contactByRole this.contacts "Mieter") "displayName" }}
  

Block-Parameter (@first, @last, @index, @key, @root)

Innerhalb eines {{#each}}- oder {{#with}}-Blocks stehen automatisch zusätzliche Spezial-Variablen zur Verfügung. Sie werden mit einem führenden @ referenziert.

VariableBedeutungVerfügbar in
@firsttrue bei der ersten Iterationeach
@lasttrue bei der letzten Iterationeach
@indexNumerischer Index (0-basiert)each (über Arrays)
@keySchlüssel des aktuellen Eintragseach (über Objekte)
@rootReferenz auf den Root-Kontext (oberste Ebene der Input-Daten)überall
Beispiel: @first für Trenner zwischen Events
  {{#each events}}
{{#unless @first}}
<hr/>
{{/unless}}
<h1>{{this.name}}</h1>
{{/each}}
  
Beispiel: @root für Zugriff auf baseData aus tiefen Schleifen
  {{#each events}}
<p>Veranstalter: {{@root.baseData.name}}</p>
<h1>{{this.name}}</h1>
{{/each}}
  

Custom

formatDate

Funktion um Datum und Uhrzeit spezifisch auszugeben.
Die Funktion verlangt 2 Parameter:

  • das Datum (Date ISO)
  • das Datumsformat (Die Optionen sind hier gelistet)
Beispiel
  {{ formatDate this.start "d.m.Y" }}
  => Ausgabe: deutsches Datum, z.B. 03.08.2022

{{ formatDate this.start "D d.m.Y" }}
  => Ausgabe: deutsches Datum mit Wochentag,
     z.B. Mi. 03.08.2022

{{ formatDate this.start "H:i" }}
  => Ausgabe: Uhrzeit, z.B. 20:00

{{ formatDate this.start "d.m.Y H:i" }}
  => Ausgabe: Datum mit Uhrzeit, z.B. 20.11.2021 20:00
  

formatEuro

Funktion um Preis (in Cent) formatiert in Euro darzustellen.
Die Funktion verlangt 1 Parameter:

  • Preis (in Cent)
Beispiel
  {{ formatEuro this.netPrice }}
  => Ausgabe: 300,00 €
  

formatCurrency

Funktion um Preis formatiert in einer Währung darzustellen.

Im Moment wird nur EUR | USD | CHF | GBP unterstützt.
Die Funktion verlangt 2 Parameter:

  • Preis (in Cent/Rappen/Pence)
  • Währung (EUR/USD/CHF/GBP)
Beispiel
  {{ formatCurrency this.netPrice "CHF" }}
  => Ausgabe: 300,00 CHF
  

formatNumberDE

Funktion um eine Zahl in ein deutsches Zahlenformat umzuwandeln
Die Funktion verlangt 1 Parameter:

  • Zahl
Beispiel
  {{ formatNumberDE countSeats }}
  => Ausgabe: 1.000
  

contactByRole

Funktion, um einen Kontakt aus einem Array anhand seiner Rolle zu extrahieren.
Die Funktion verlangt 2 Parameter:

  • Ein Array mit Kontakten
  • Den Namen der Rolle
Beispiel
  {{#each (contactByRole this.contacts "Veranstalter:in")}}
  {{@key}}: {{this}}
{{/each}}
  => Ausgabe
    role: Veranstalter:in
    displayName: ABC Firma
    email: info@abc.de
    phone: +030 8877665

{{ lookup (contactByRole this.contacts "Veranstalter:in") "displayName" }}  
  => Ausgabe: ABC Firma
  

scheduleValueByName

Funktion, um einen Schedule-Eintrag anhand des Namens zu extrahieren.
Die Funktion hat 3 Parameter:

  • Den Schedule
  • Den Namen des Schedule-Eintrags
  • Datum einblenden (optional): default false, auf true setzen um Datum und Uhrzeit anzuzeigen
Beispiel
  {{ scheduleValueByName this.schedule "Curfew" }}
  => Ausgabe
  22:00
{{ scheduleValueByName this.schedule "Curfew" true }}
  => Ausgabe
  20.09.2023 22:00
  

linkByType

Funktion, um einen Eintrag der Links anhand des Typs zu extrahieren.
Die Funktion verlangt 2 Parameter:

  • Die Links
  • Den Typ des Eintrags (z.B. Web, Facebook)
  {{ linkByType @root.baseData.links "Web" }}
  

eventInformationByName

Funktion, um einen Eintrag der Event Informationen anhand des Namens zu extrahieren.
Die Funktion verlangt 2 Parameter:

  • Die Event Informationen
  • Den Namen des Eintrags
  {{ eventInformationByName this.eventInformations "Hotel" }}
  

md2html

Konvertiert Markdown to HTML

  {{md2html memo}}
  

csvQuote

Tauchen in CSV Exporten Spalten mit Anführungszeichen auf, müssen diese Spalten mit dem Helper `csvQuote` escaped werden: z.B.

  {{ start }};{{ csvQuote name }}
  

currentDate

Aktuellles Datum

Die Funktion verlangt 1 Parameter:

  {{ currentDate "P" }}
  

Beispiele

Einfaches Markdown Beispiel

  {{#each events}}

<h1>{{this.name}}</h1>
<p>Type: {{this.eventType}}</p>
<p>Status: {{this.status}}</p> 
<p>Datum: {{ formatDate this.start "P" }} | {{ formatDate this.start "p" }} Uhr  </p>
<p>Name: {{this.displayNames.eventTitleWithArtistsAndShows}}  </p>
<p>Ort: {{this.displayNames.locationsWithRooms}}</p>

{{/each}}
  

Einfaches CSV Beispiel

  Status;Datum;Titel
{{#each events}}
{{this.status}};{{formatDate this.start "P"}};{{{this.name}}}
{{/each }}
  

VVK-Zahlen

  <h1>Verkäufe</h1>
<p>Event:  {{this.name}} | {{ formatDate this.start "P" }}</p>
<p>Location: {{this.displayNames.locationsWithRooms}}</p>
{{#if this.ticketing}}
<h2>Tickets</h2>
<table>
    <thead>
        <tr>
            <th>VVK-Stelle</th>
            <th>Kategory</th>
            <th>Ticketpreis (brutto)</th>
            <th>Verkauft</th>
            <th>Kontingent</th>
            <th>Gesamt (brutto)</th>
        </tr>
    </thead>
    <tbody>
      {{#each this.ticketing}}
        <tr>
            <td>{{ this.ticketOffice }}</td>
            <td>{{ this.category }}</td>
            <td>{{ formatEuro this.grossPrice }}</td>
            <td>{{ this.soldTickets }}</td>
            <td>{{ this.contingent }}</td>
            <td>{{ formatEuro this.saleTotal }}</td> 
        </tr>
        {{/each}}
<tr>
            <th>Summe</th>
            <th></th>
            <th></th>
            <th>{{ sumBy this.ticketing "soldTickets" }}</th>
            <th>{{ sumBy this.ticketing "contingent" }}</th>
            <th>{{ formatEuro (sumBy this.ticketing "saleTotal" ) }}</th>
        </tr>
    </tbody>
</table>

{{/if}}
  

Event-Informationen

Alle Event-Informationen außer “Gästeliste” auflisten.

  <h2>Event Informationen</h2>
{{#each this.eventInformations}}
{{#unless (isEqual name "Gästeliste")}}
 
<h3>{{this.name}}</h3>
{{this.value}}
{{/unless}}
{{/each}} 
  

Artist-Tabelle mit Bilder

  <h2>Artists</h2>

<table class="artistTable">
    <thead>
        <tr>
            <th>Pic</th>
            <th>Name</th>
        </tr>
    </thead>
    <tbody>
        {{#each this.artists}}
        <tr>
            <td><img src="{{ lookup this.images.0.sizes "640" }}" /></td>
            <td>
              <h3>{{ this.name }}</h3>
              <p>{{ this.description }}</p>
            </td>
        </tr>
        {{/each}}
    </tbody>
</table>
  

Dateien verlinken

  <h2>Dateien</h2>

{{#each this.files}}

<h3>{{this.name}}</h3>
<a href="{{this.path}}">{{this.path}}</a>

{{/each}}
  

Anmerkung: Nur als „öffentlich“ gekennzeichnete Dateien können im Export eingebunden werden. „Öffentlich“ bedeutet hierbei lediglich, dass eine Datei ohne Login verfügbar wird. Ohne die URL bzw. konkreter die zufallsgenerierte ID besteht aber trotzdem keine Zugriffsmöglichkeit. D.h. es können durchaus Dateien wie Tech Rider auf öffentlich gesetzt und somit in einen Export eingebunden werden.

Tipps

Bedingte Darstellung

Um eine Sektion nur anzuzeigen, wenn das entsprechende Feld gesetzt ist, kann sie mit einem #if Block-Helper umschlossen werden:

  {{#if (eventInformationByName this.eventInformations "Hotel")}}
<h3>Hotel</h3>
{{eventInformationByName this.eventInformations "Hotel"}}
{{/if}}
  

Seitenumbruch

Für Seitenumbrüche in Paged-Layouts wird der Inhalt jedes Events in ein Wrapper-Div mit break-after: page eingeschlossen. Damit der Umbruch zusätzlich im Word-Export funktioniert, wird zwischen den Events ein <p> mit Inline-Style eingefügt — über {{#unless @first}} nur vor jedem Event ab dem zweiten:

  <style>
  .event-page {
    break-after: page;
    page-break-after: always;
  }
  .event-page:last-of-type {
    break-after: auto;
    page-break-after: auto;
  }
</style>

{{#each events}}

{{#unless @first}}
<p style="page-break-before: always; margin: 0;">&nbsp;</p>
{{/unless}}

<div class="event-page">
  <h1>{{this.name}}</h1>
  <!-- Inhalt des Events -->
</div>

{{/each}}
  

Das Pattern deckt alle drei Ausgabeformate ab: das break-after: page der CSS-Klasse greift in der Paged-Vorschau und im PDF, das Inline-<p> mit page-break-before sorgt für den Umbruch im Word-Export. Der CSS-Selektor :last-of-type verhindert eine leere Trailing-Seite nach dem letzten Event.

Wichtig: Der {{pagebreak}}-Helper funktioniert nur in Legacy-Layouts (ohne Paged.js). In Paged-Layouts muss stattdessen das oben gezeigte Pattern verwendet werden.

Styling

Styling von Exporten ist via CSS möglich. Globale Stile können ganz am Anfang einer Exportvorlage in einem Style-Tag angegeben werden:

  <style>
...
</style>
  

Alternativ sind auch Inline-Stile auf z.B. einzelnen Absätzen oder Paragraphen anwendbar. Hierfür muss die entsprechende Passage dann allerdings in HTML statt Markdown geschrieben werden:

  <p style="...">...</p>
  

Schriften

Folgende Schriftarten stehen zur Verfügung:

Beispiel: Überschriften mit Serifen, Absätze serifenlos

  <style>
h1, h2, h3, h4 {
  font-family: DM Serif Display;
}
body, p, th, td {
  font-family: Ubuntu;
}
</style>

<h1>Globale Stile</h1>

<p>Ein Absatz</p>
<ul>
<li>eine</li>
<li>Liste</li>
</ul>

<h2>Tabelle</h2>
<table>
    <thead>
        <tr>
            <th>Spalte A</th>
            <th>Spalte B</th>
            <th>Spalte C</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>A1</td>
            <td>B1</td>
            <td>C1</td>
        </tr>
        <tr>
            <td>A2</td>
            <td>B2</td>
            <td>C2</td>
        </tr>
    </tbody>
</table>
  

Beispiel: Einzelner Absatz in anderer Schriftart

  Erster Absatz

<p style="font-family: Indie Flower">Zweiter Absatz</p>
  

Weitere Möglichkeiten

  <p style="color: blue">blau</p>
<p style="color: blue; font-weight: bold;">blau & fettgedruckt</p>
<mark>highlighted</mark>
<p style="text-decoration: underline;">unterstrichen</p>
  

Formulare

Für eine Parametrisierung oder nachträgliche Bearbeitung von Exporten können HTML Formulare verwendet werden:

  <form action="{{request.formAction}}" method="post">
  Rechnungsnummer: 
  {{#if parameters.form.rechnungsnummer.[0]}}
  {{parameters.form.rechnungsnummer.[0]}}
  {{else}}
  <input name="rechnungsnummer" />
  <button>ok</button>
  {{/if}}
</form>
  

Beim Aktualisieren wird der Export wieder in seinen Ausgangszustand versetzt. Sollte das nicht gewünscht sein, muss über den Export selbst aktualisiert werden:

  <form action="{{request.formAction}}" method="post">
  Rechnungsnummer:
  {{#if parameters.form.rechnungsnummer.[0]}}
  {{parameters.form.rechnungsnummer.[0]}}
  <input name="rechnungsnummer" type="hidden" />
  <button>aktualisieren</button>
  {{else}}
  <input name="rechnungsnummer" />
  <button>ok</button>
  {{/if}}
</form>
  

Mit den Paged-Layouts (A4 Hochformat/Querformat Paged) können eigene Kopf- und Fußzeilen mit automatischen Seitenzahlen erstellt werden. Dafür wird die CSS-Technik @page margin boxes in Kombination mit position: running() verwendet.

So funktioniert’s

Im <style>-Block des Templates werden @page-Regeln definiert, die festlegen, wo Kopf- und Fußzeilen auf jeder Seite erscheinen. Die Inhalte werden als HTML-Elemente im Template platziert und per position: running() den jeweiligen Seitenbereichen zugewiesen.

Verfügbare Seitenbereiche

BereichPosition
@top-leftKopfzeile links
@top-centerKopfzeile mitte
@top-rightKopfzeile rechts
@bottom-leftFußzeile links
@bottom-centerFußzeile mitte
@bottom-rightFußzeile rechts

Seitenzahlen

Für Seitenzahlen stehen zwei Platzhalter-Elemente zur Verfügung:

  • <span class="cp-page-number"></span> – Aktuelle Seitennummer
  • <span class="cp-total-pages"></span> – Gesamtanzahl Seiten

Beispiel-Template

Das folgende Beispiel zeigt einen Export mit Logo in der Kopfzeile rechts, Adresse in der Fußzeile links und Seitenzahlen in der Fußzeile rechts:

  <style>
  @page {
    margin: 20mm;
    @top-right {
      content: element(headerRight);
      vertical-align: middle;
    }
    @bottom-left {
      content: element(footerLeft);
      vertical-align: middle;
    }
    @bottom-right {
      content: element(footerRight);
      vertical-align: middle;
    }
  }

  .running-header-right {
    position: running(headerRight);
  }
  .running-header-right img {
    height: 1.2cm;
    width: auto;
  }

  .running-footer-left {
    position: running(footerLeft);
    font-size: 7pt;
    color: gray;
  }
  .running-footer-right {
    position: running(footerRight);
    font-size: 7pt;
    color: gray;
    text-align: right;
  }
</style>

<div class="running-header-right">
  <img src="{{baseData.logo.url}}" />
</div>
<div class="running-footer-left">
  {{baseData.name}} &middot; {{baseData.street}},
  {{baseData.zip}} {{baseData.city}}
</div>
<div class="running-footer-right">
  Seite <span class="cp-page-number"></span>
  von <span class="cp-total-pages"></span>
</div>

<h1>Mein Dokument</h1>
<p>Inhalt...</p>
  

Wichtig: Dieses Template muss mit einem Paged-Layout (A4 Hochformat (Paged) oder A4 Querformat (Paged)) verwendet werden. Mit einem regulären Layout werden die @page-Regeln und position: running() ignoriert.

Word-Export

HTML-Exporte können zusätzlich als Word-Datei (.docx) heruntergeladen werden.

Die Konvertierung übernimmt die freie Bibliothek LibreOffice, dessen HTML Import leider nur bedingt funktioniert, weshalb ein Export nicht 1:1 dargestellt werden.

Styling

CSS Styles greifen i.d.R. nur inline, so dass z.B. das Styling innerhalb von Fließtext in Benutzerfeldern oft nicht einwandfrei funktioniert.

Seitenumbrüche

LibreOffice respektiert page-break-before bzw. page-break-after nur, wenn der Style als Inline-style-Attribut auf einem <p>-Element steht. CSS aus einem <style>-Block wird beim Import ignoriert, ebenso Inline-Styles auf <div>-Elementen. Für Seitenumbrüche im Word-Export muss daher zwischen den Inhalten ein expliziter Trenner-Absatz eingefügt werden:

  <p style="page-break-before: always; margin: 0;">&nbsp;</p>
  

Für eine Liste von Events kombiniert mit dem Wrapper-Div-Pattern (siehe Seitenumbruch) ergibt sich:

  {{#each events}}

{{#unless @first}}
<p style="page-break-before: always; margin: 0;">&nbsp;</p>
{{/unless}}

<div class="event-page">
  ...
</div>

{{/each}}
  

Der {{#unless @first}}-Block sorgt dafür, dass der Trenner nur ab dem zweiten Event eingefügt wird — sonst würde der Word-Export mit einer leeren ersten Seite starten.

Was nicht funktioniert

Folgende Konstrukte sind im Paged-Layout (Browser-Vorschau, PDF) gängig, werden aber von LibreOffice beim Word-Import ignoriert:

  • page-break-* oder break-* in einem <style>-Block (egal ob über Klasse oder Element-Selektor)
  • style="page-break-before: ..." auf <div>, <section>, <h1> oder anderen Block-Elementen außer <p>
  • Der {{pagebreak}}-Helper (rendert ein <div> mit Inline-Style)

Weitere Hinweise

  • @page-Regeln und Running Headers/Footers aus dem Paged-Layout (siehe oben) werden im Word-Export nicht abgebildet — Word kennt diese CSS-Mechaniken nicht.
  • Seitenzahlen über <span class="cp-page-number"> erscheinen im Word-Export als leere Spans, da sie zur Render-Zeit von Paged.js gefüllt werden.
  • Externe Schriften über @import werden von LibreOffice nicht geladen — das Word-Dokument verwendet die jeweilige System-Standardschrift, sofern die im Template referenzierte Schrift nicht installiert ist.