Zum Inhalt springen

Module (NXM)

Kundenportal

Ein Paket kann einen Teil seiner Datensätze im Internet zeigen, für die Kunden der Firma, die es installiert hat. Wie der Abschnitt portal im Manifest funktioniert, was nexxus prüft und was ein Moduldienst antwortet.

Für
Entwickler
Dauer
20 Min.

Manche Pakete bedienen Menschen, die sich nie anmelden: Die Gäste einer Ferienhausagentur suchen ein Haus, prüfen die Daten und schicken eine Buchungsanfrage. Das Kundenportal ist diese öffentliche Seite. Das Paket erklärt in seinem Manifest, welche Datensätze und Felder öffentlich sind und welche Seiten sie zeigen; eine eigene Portal-App zeichnet die Seiten unter der Adresse der Firma. Einen Webserver oder eigenen Code bringt das Paket dafür nicht mit. Code gibt es nur in seinem Moduldienst, für das, was gerechnet werden muss: freie Tage, ein Preis, eine Buchungsanfrage.

  1. Portal-Domain

    Die eigene Adresse der Firma, etwa stay.agentur.example, als Portal-Domain des Mandanten eingetragen.

  2. Portal-App

    Zeichnet die Seiten, die das Paket erklärt, auf dem Server, damit Suchmaschinen sie lesen.

  3. Öffentliche Schnittstelle von nexxus

    Gibt nur die öffentlichen Felder der öffentlichen Datensätze heraus, für den Mandanten dieser Adresse.

  4. Moduldienst

    Beantwortet, was gerechnet werden muss: Verfügbarkeit, Preis, Buchungsanfrage.

nexxus steht zwischen Besucher und Paket: Jede Antwort wird auf dem Server gefiltert oder geprüft.

Eine Domain eines Mandanten hat eine Rolle. Seine eigenen Domains (Eigene Domains) tragen die App und ihre Anmeldung. Seine Portal-Domains (Portal-Domains) tragen nur das Portal: Unter so einer Adresse kann sich niemand anmelden, und Verwaltung, Schnittstelle und Live-Kanal antworten mit 404, als gäbe es sie nicht. Beide setzt ein Superadmin unter AdministrationUnternehmenMandanten. Links, die nexxus für Mitarbeitende schreibt, zeigen immer auf die eigenen Domains.

Jede Antwort der Portal-Schnittstelle gehört dem Mandanten der Adresse, unter der sie aufgerufen wurde. Eine Seite unter stay.agentur.example kann keine Datensätze eines anderen Mandanten lesen.

Ein Paket mit Portal je Mandant: Installiert ein Mandant zwei, ist das zuerst installierte die Website.

Das Beispiel ist das Ferienhaus-Paket: Gäste blättern durch die Unterkünfte, öffnen eine und schicken eine Anfrage.

"portal": {
"name": { "en": "Stays", "de": "Unterkünfte" },
"siteNameSetting": "company_name",
"public": {
"listing": {
"filter": { "status": "active" },
"fields": ["name", "kind", "region", "city", "description", "bedrooms", "max_guests",
"amenities", "base_price", "photos", "location"],
"geo": { "field": "location", "precision": "approximate" }
}
},
"pages": [
{ "key": "stays", "type": "catalog", "entity": "listing", "path": "",
"title": { "en": "Holiday homes", "de": "Ferienhäuser" },
"card": { "titleField": "name", "subtitleFields": ["city", "region"], "imageField": "photos",
"facts": ["bedrooms", "max_guests"], "priceField": "base_price",
"priceSuffix": { "en": "per night", "de": "pro Nacht" } },
"filters": [ { "field": "region" }, { "field": "kind" },
{ "field": "max_guests", "op": "gte", "label": { "en": "Guests", "de": "Gäste" } } ],
"dates": { "quote": "quote" }, "map": true, "detail": "stay" },
{ "key": "stay", "type": "detail", "entity": "listing", "path": "stays/:slug", "slugField": "name",
"title": { "en": "Stay", "de": "Unterkunft" },
"sections": [
{ "type": "gallery", "field": "photos" },
{ "type": "facts", "fields": ["kind", "bedrooms", "max_guests"] },
{ "type": "text", "field": "description" },
{ "type": "chips", "field": "amenities" },
{ "type": "map", "field": "location" },
{ "type": "availability", "endpoint": "availability" },
{ "type": "booking", "quote": "quote", "request": "booking_request" } ] }
],
"endpoints": [
{ "key": "availability", "method": "GET", "service": "concierge", "params": ["record", "from", "to"] },
{ "key": "quote", "method": "GET", "service": "concierge", "params": ["record", "check_in", "check_out", "guests"] },
{ "key": "booking_request", "method": "POST", "service": "concierge", "params": ["record"],
"form": [ { "key": "name", "type": "string", "required": true },
{ "key": "email", "type": "email", "required": true },
{ "key": "guests", "type": "number", "required": true },
{ "key": "check_in", "type": "date", "required": true },
{ "key": "check_out", "type": "date", "required": true },
{ "key": "message", "type": "text" },
{ "key": "consent", "type": "consent", "required": true,
"label": { "en": "I agree that my details are used to answer my request.",
"de": "Ich bin einverstanden, dass meine Angaben zur Beantwortung meiner Anfrage verwendet werden." } } ],
"rateLimit": { "perIpPerHour": 10 } }
]
}

Die Seiten rund um ein Objekt kommen aus demselben Paket. Die Objektseite bekommt drei weitere Abschnitte, und vier Seiten kommen dazu: eine für Nachrichten an den Gastgeber, eine Kontaktseite und zwei Rechtsseiten. Gekürzt aus higgins.rentals 1.2.1, dessen Objekte für die FAQ außerdem house_rules, checkin_time, checkout_time, min_nights und cleaning_fee veröffentlichen:

"portal": {
"public": {
// Einstellungen für Fußzeile, Kontaktkarte, Gastgeber und Rechtsseiten
"settings": ["company_name", "company_address", "portal_phone", "portal_email", "portal_hours",
"portal_host_intro", "portal_hosting_since", "agency_languages",
"cancellation_policy", "privacy_text", "legal_text"],
"listing": { /* wie oben */ },
// Die Bewertungen: zweite öffentliche Entität, mit Anzeigenamen statt Gastnamen
"review": { "fields": ["display_name", "rating", "received_at", "text"] }
},
"pages": [
{ "key": "stay", "type": "detail", /* wie oben */
"sections": [
/* die Abschnitte von oben, mit amenities statt chips */
{ "type": "amenities", "field": "amenities" },
{ "type": "reviews", "entity": "review", "via": "listing",
"fields": ["display_name", "rating", "received_at", "text"],
"nameField": "display_name", "dateField": "received_at",
"ratingField": "rating", "textField": "text" },
{ "type": "host", "setting": "portal_host_intro",
"fields": ["portal_hosting_since", "agency_languages"], "page": "inquiry" } ] },
{ "key": "inquiry", "type": "inquiry", "entity": "listing", "path": "stays/:slug/contact",
"title": { "en": "Message the host", "de": "Gastgeber kontaktieren" },
"sections": [
{ "type": "faq", "amenities": "amenities", "groups": [
{ "title": { "en": "Arrival and departure", "de": "Anreise und Abreise" },
"items": [ { "field": "checkin_time" }, { "field": "checkout_time" },
{ "amenity": "parking",
"text": { "en": "Parking at the house", "de": "Parkplatz am Haus" } } ] },
{ "title": { "en": "Price and availability", "de": "Preis und Verfügbarkeit" },
"items": [ { "field": "min_nights" }, { "field": "cleaning_fee" },
{ "setting": "cancellation_policy" } ] } ] },
{ "type": "form", "endpoint": "contact" } ] },
{ "key": "contact", "type": "contact", "path": "contact", "nav": true,
"title": { "en": "Contact", "de": "Kontakt" },
"sections": [
{ "type": "contact",
"fields": ["company_address", "portal_phone", "portal_email", "portal_hours"] },
{ "type": "form", "endpoint": "contact" } ] },
{ "key": "privacy", "type": "info", "path": "privacy", "footer": true,
"title": { "en": "Privacy notice", "de": "Datenschutz" }, "setting": "privacy_text" },
{ "key": "legal", "type": "info", "path": "legal", "footer": true,
"title": { "en": "Legal notice", "de": "Impressum" }, "setting": "legal_text" }
],
"endpoints": [
// Für beide Formulare; nur die Anfrageseite schickt ein Objekt als record
{ "key": "contact", "method": "POST", "service": "concierge", "params": ["record"],
"form": [ { "key": "name", "type": "string", "required": true },
{ "key": "email", "type": "email", "required": true },
{ "key": "message", "type": "text", "required": true, "maxLength": 2000 },
{ "key": "consent", "type": "consent", "required": true,
"label": { "en": "I agree that my details are used to answer my message.",
"de": "Ich bin einverstanden, dass meine Angaben zur Beantwortung meiner Nachricht verwendet werden." } } ],
"rateLimit": { "perIpPerHour": 10 } }
]
}

public.<datenobjekt> ist das Einzige, was das Portal je von diesem Datenobjekt bekommt.

SchlüsselBedeutung
filterFeld und Wert (oder eine Liste von Werten), den jeder öffentliche Datensatz hat, etwa nur aktive Unterkünfte. Die Felder müssen indexed sein. Ein Filter des Besuchers hebt ihn nie auf
fieldsDie Felder, die das Portal bekommt, sonst nichts. Erlaubt sind string, text, number, boolean, date, datetime, enum, geo und ein ref auf core:dmsDocument (Bilder). Ein Verweis auf einen Kontakt oder einen Benutzer und json werden abgelehnt
geofield und precision: approximate (der Standard für jedes öffentliche Geo-Feld) rundet die Koordinaten auf dem Server auf zwei Nachkommastellen, etwa 1 km; exact gibt sie unverändert weiter

public.settings ist eine Liste von Einstellungen der Installation, die die Website zeigen darf, etwa Telefonnummer, Adresse oder Datenschutzhinweis. Erlaubt sind nur Einstellungen vom Typ string, text oder enum; alle anderen Einstellungen der Installation bleiben privat.

Ein ref auf core:dmsDocument in fields macht diese Dokumente zu öffentlichen Bildern. Das Portal bekommt sie als { "id", "url", "alt" }, alt aus der Beschreibung des Dokuments. Die Adresse ist signiert und an Mandant, Datensatz und Dokument gebunden; nexxus liefert das Bild nur, solange ein öffentliches Feld eines öffentlichen Datensatzes es noch nennt, und nur als JPEG, PNG, WebP, GIF oder AVIF. Mit ?w=400, 800, 1200 oder 1600 liefert es das Bild auf diese Breite verkleinert, für srcset auf dem Handy.

SchlüsselBedeutung
key, titleName der Seite und ihre Überschrift
typecatalog (eine Liste mit Karten, Filtern und auf Wunsch einer Karte), detail (ein Datensatz), inquiry (eine Frage zu einem Datensatz, mit dem Slug der Detailseite), contact (die Kontaktseite der Agentur, ohne Datensatz) oder info (eine Textseite wie der Datenschutzhinweis, ohne Datensatz)
entityEin Datenobjekt aus public; nicht auf contact- und info-Seiten
pathUnterhalb der Startseite, eindeutig; "" ist die Startseite. Eine detail- oder inquiry-Seite enthält genau einmal :slug, die anderen Typen keins
nav, footertrue setzt einen Link auf die Seite in die Kopfzeile oder in die Fußzeile
settinginfo-Seite: eine öffentliche Einstellung vom Typ string oder text, als Absätze gezeigt (eine Leerzeile beginnt einen neuen)
slugFieldDetailseite: ein öffentliches string-Feld, aus dem die Adresse entsteht (aus Ocean View Villa wird ocean-view-villa; ein zweiter Datensatz gleichen Namens bekommt seine Id angehängt)
cardKatalog: titleField, subtitleFields, imageField (Bilder; ohne ist die Karte reiner Text), facts, priceField (eine Zahl), priceSuffix. Ohne priceField und dates.quote hat die Karte keine Preiszeile
filtersKatalog: öffentliche Felder, die indexed sind; op eq (Standard), gte oder lte (Zahlen und Daten). Auswahlfelder werden Chips; Textfelder mit Werten zur Auswahl, eine gte-Zahl (Gäste) und dates kommen in die Suchleiste, die wegfällt, wenn sie nichts davon hat
dates.quoteKatalog: ein GET-Endpunkt, der einen Aufenthalt bepreist, sobald Daten gewählt sind
map, detailKatalog: eine Karte zeigen (braucht ein öffentliches Geo-Feld); der Schlüssel der Detailseite. Eine Markierung trägt den Preis aus card.priceField, wenn dieses Feld eine currency hat, und den angebotenen Gesamtpreis, sobald Daten gesetzt sind und die Unterkunft frei ist. Eine Detailseite führt zurück zum Katalog ihrer eigenen entity
sectionsdetail- und inquiry-Seiten: gallery (Bilder), facts, text, chips, amenities (ein Auswahlfeld, mit Symbolen gezeigt), map (Geo), availability (ein GET-Endpunkt), booking (quote: GET, request: POST), reviews, host, faq. Nur auf detail-Seiten: slots (endpoint: GET, request: POST). Jede Seite: contact (öffentliche Einstellungen in fields) und form (das Formular eines POST-Endpunkts, direkt auf der Seite)

Jedes Feld und jede Einstellung, die eine Seite nennt, muss öffentlich sein.

Die neueren Abschnitte im Einzelnen:

AbschnittSchlüssel
amenitiesfield: ein öffentliches Auswahlfeld. Ein Wert davon kann icon tragen, einen kurzen Schlüssel wie wifi aus dem Symbolsatz des Portals; ohne zeigt das Portal ein neutrales Häkchen
reviewsentity (ein anderes öffentliches Datenobjekt), via (sein Verweis auf das Datenobjekt der Seite), fields (öffentliche Felder dieses Datenobjekts) und welches davon Name, Datum, Bewertung und Text ist: nameField, dateField, ratingField, textField. Das Portal zeigt die neuesten zuerst, mit der Durchschnittsbewertung. Veröffentlichen Sie einen Anzeigenamen wie „Anna K.“, nie den vollen Namen oder die E-Mail-Adresse eines Gastes
hostsetting (ein Vorstellungstext), fields (weitere öffentliche Einstellungen, etwa „Gastgeber seit“), page (die inquiry-Seite, zu der „Nachricht an den Gastgeber“ führt; ohne sie fehlt der Knopf), endpoint (wird angenommen, die Portal-App nutzt es aber nicht: Die Anfrageseite bringt ihr eigenes form mit)
faqgroups, jede mit title und items. Ein Eintrag ist eines von {"field": ...} (ein öffentliches Feld des Datensatzes), {"setting": ...} (eine öffentliche Einstellung) oder {"amenity": ..., "text": {...}} (ein Text, der erscheint, wenn der Datensatz diesen Wert des Auswahlfelds hat, das der Abschnitt in amenities nennt); jeder Eintrag kann ein eigenes label tragen
slotsFreie Termine statt eines Zeitraums von Nächten. endpoint: ein GET-Endpunkt mit den Parametern record, from und to, der die Termine auflistet. request: ein POST-Endpunkt mit den Parametern record und dem slotParam, dessen Formular der Besucher für den gewählten Termin ausfüllt. slotParam: der Name, unter dem der gewählte Termin geschickt wird, nie record, lang oder website. days: wie weit ein Besucher vorausblättern kann, 7 bis 90 Tage, ohne Angabe 28. title ersetzt die Überschrift Termin wählen

Eine Praxis, deren Patienten einen Termin anfragen, erklärt slots auf der Seite einer Behandlung. Das Paket gibt "platform": ">=0.13.0 <1.0.0" an, die erste Plattformversion mit diesem Abschnitt:

"pages": [
{ "key": "treatment", "type": "detail", "entity": "treatment", "path": "treatments/:slug", "slugField": "name",
"title": { "en": "Treatment", "de": "Behandlung" },
"sections": [
{ "type": "gallery", "field": "photos" },
{ "type": "text", "field": "description" },
{ "type": "slots", "endpoint": "slots", "request": "appointment", "slotParam": "slot", "days": 28,
"title": { "en": "Choose a time", "de": "Termin wählen" } } ] }
],
"endpoints": [
{ "key": "slots", "method": "GET", "service": "practice", "params": ["record", "from", "to"] },
{ "key": "appointment", "method": "POST", "service": "practice", "params": ["record", "slot"],
"form": [ { "key": "name", "type": "string", "required": true },
{ "key": "email", "type": "email", "required": true },
{ "key": "consent", "type": "consent", "required": true,
"label": { "en": "I agree that my details are used to arrange the appointment.",
"de": "Ich bin einverstanden, dass meine Angaben zur Vereinbarung des Termins verwendet werden." } } ],
"rateLimit": { "perIpPerHour": 10 } }
]

Eine detail-Seite mit slots und ohne booking hat keine Buchungskarte daneben.

Ein Endpunkt ist etwas, das das Portal den Dienst des Pakets fragen darf, über nexxus.

SchlüsselBedeutung
keyName, eindeutig im Portal
methodGET oder POST
serviceEin Dienst des Pakets (services)
paramsAbfrageparameter, die weitergegeben werden. record muss einen öffentlichen Datensatz des Datenobjekts der Seiten nennen, die den Endpunkt nutzen; das prüft nexxus vor dem Aufruf. lang ist reserviert
formNur bei POST. Felder mit type string (bis 500 Zeichen), text (bis 5000), email, number, date (JJJJ-MM-TT), consent (muss angehakt sein, wenn required) oder boolean (ein Kästchen zum Anhaken, gesendet als true oder false); required, label, maxLength. website ist reserviert
rateLimit.perIpPerHourAufrufe je Besucheradresse und Stunde, höchstens 10000; ohne Angabe 2000 bei GET (ein Katalog mit Daten fragt je Karte einen Preis an) und 20 bei POST

Bevor etwas beim Dienst ankommt, zählt nexxus den Aufruf gegen das Limit, lässt jeden nicht erklärten Parameter weg, prüft den Datensatz und prüft das Formular: unbekannte Felder, fehlende Pflichtfelder, falsche Typen, eine ungültige E-Mail-Adresse oder eine fehlende Einwilligung werden mit den Feldern abgelehnt. Die Portal-App zeichnet ein verstecktes Feld website, das Menschen nie sehen; eine Anfrage, die es ausfüllt, bekommt eine freundliche Antwort und geht nirgendwohin.

Die Portal-App zeichnet jede Seite zuerst auf dem Server, damit Suchmaschinen den Inhalt lesen, und arbeitet dann im Browser weiter. Sie speichert nichts auf dem Gerät der Besucher: keine Cookies, keinen lokalen Speicher, keinen anderen Browserspeicher. Eine Website damit braucht deshalb keinen Cookie-Banner. Schreiben Sie das stattdessen in den Datenschutzhinweis, wie es das Ferienhauspaket tut.

Jedes Portal bietet Deutsch und Englisch an, die Standardsprache der Website zuerst. Polnisch kommt dazu, wenn die Sprache des Mandanten Polnisch ist oder das Paket für sein Portal Polnisch mitbringt (einen pl-Text in portal.name); eine Website, die Polnisch nie gewählt hat, behält ihre zwei Sprachen. Die Sprache des Besuchers kommt aus ?lang=, sonst aus dem Browser (die erste angebotene Sprache seines Accept-Language-Headers), sonst ist es die Standardsprache der Website. Die eigenen Texte des Portals gibt es in allen drei Sprachen. Die Texte des Pakets erscheinen in der Sprache des Besuchers, sonst in der Standardsprache der Website, sonst auf Englisch; ein Paket ohne polnische Texte zeigt einem polnischen Besucher also die der Standardsprache.

Die Suchleiste enthält die Filter, die Reisedaten und die Zahl der Gäste; Auswahlfilter erscheinen als Chips über der Liste. Sind Daten gewählt, zeigt jede Karte den Preis für diesen Aufenthalt oder dass die Unterkunft dann nicht frei ist.

Mit map: true stehen Liste und Karte auf breiten Bildschirmen nebeneinander, und die Liste folgt der Karte: Verschiebt oder zoomt der Besucher sie, zeigt die Liste nur noch, was im sichtbaren Ausschnitt liegt, 400 ms nachdem die Karte zur Ruhe gekommen ist, mit der Zeile 8 Treffer in diesem Kartenausschnitt. Beim Verschieben der Karte suchen schaltet das für den Besuch ab; dann bietet eine verschobene Karte In diesem Gebiet suchen an. Alle zeigen holt alle Treffer zurück und passt die Karte an sie an. Auf dem Handy ist die Karte eine eigene Ansicht (Karte zeigen); nach dem Verschieben übernimmt In diesem Gebiet suchen den Ausschnitt. Der Ausschnitt wird nie Teil der Adresse: Ein geteilter Link und die Seite, die Suchmaschinen sehen, zeigen immer alle Treffer.

nexxus filtert auf dem Server. Die Abfrage der Datensätze nimmt bbox=W,S,E,N (West, Süd, Ost, Nord) in Grad; ist der Westrand größer als der Ostrand, reicht der Ausschnitt über die Datumsgrenze. Verglichen wird mit derselben gerundeten Lage, die die Markierungen zeigen, damit Liste und Markierungen übereinstimmen.

  • Teilen neben dem Titel öffnet das Teilen-Menü des Handys; wo es keines gibt, kopiert es den Link und meldet Link kopiert.
  • Die Reisedaten öffnen Daten wählen, mit Anreise und Abreise oben. Vergangene und belegte Tage sind durchgestrichen.
  • amenities: unter Das bietet diese Unterkunft die Werte mit ihren Symbolen, ab Tablet-Breite in zwei Spalten. Ab dem elften öffnet ein Knopf wie Alle 13 Ausstattungsmerkmale ansehen die ganze Liste.
  • reviews: Die Überschrift zeigt die Durchschnittsbewertung und die Zahl der Bewertungen. Darunter folgen bis zu sechs Bewertungen mit Name, Monat, Sternen und Text; ein langer Text endet nach vier Zeilen mit Mehr anzeigen. Ein Knopf wie Alle 8 Bewertungen ansehen öffnet alle. Eine Bewertung ohne Anzeigenamen erscheint als Gast; ein Objekt ohne Bewertungen hat keinen Bewertungsabschnitt.
  • host: Ihr Gastgeber zeigt Symbol oder Logo der Firma (sonst ihren Anfangsbuchstaben), ihren Namen, die Einstellungen aus fields mit ihren Bezeichnungen, die Vorstellung und Nachricht an den Gastgeber. Der Knopf öffnet die inquiry-Seite und nimmt die gewählten Daten und Gäste mit.
Drei Bewertungen mit Namen, Monat, Sternen und Text, darunter Ihr Gastgeber mit dem Logo der Agentur, ihren Angaben und der Vorstellung
Bewertungen und Gastgeber auf einer Objektseite. Die Buchungskarte bleibt daneben stehen.

Ein slots-Abschnitt zeigt die freien Termine des Datensatzes Woche für Woche, Montag bis Sonntag, beginnend mit der laufenden Woche und nie mit einem Termin, der schon vorbei ist. Vorherige Woche und Nächste Woche blättern bis days Tage voraus. Auf breiten Bildschirmen ist die Woche sieben Spalten breit, auf Handys eine Liste der Tage mit freien Terminen. Unter einer Uhrzeit steht ihr label, etwa die Ärztin oder der Arzt. Eine Woche ohne freie Termine meldet Keine freien Termine in dieser Woche und bietet die nächste an. Die Uhrzeiten gelten in der Zeitzone, die der Dienst nennt; ist das nicht die Zeitzone des Besuchers, sagt eine Zeile unter der Woche, welche es ist.

Ein gewählter Termin öffnet Ihr Termin mit Datum und Uhrzeit und dem Formular von request; Anderen Termin wählen führt zurück und behält, was schon eingetragen ist. Termin anfragen schickt es ab. Danach sieht der Besucher Termin angefragt mit der Referenz, die der Dienst zurückgibt. Hat jemand anderes den Termin inzwischen genommen, meldet das Portal Dieser Termin ist gerade vergeben worden. und lädt die Woche neu. Auf Handys führt die Leiste unten zu den Terminen.

Die inquiry-Seite (im Ferienhauspaket stays/:slug/contact) trägt die Überschrift Fynbos & Tide Holiday Homes kontaktieren mit dem Namen der Website, zeigt die Angaben zum Gastgeber und die faq-Gruppen unter Das fragen Gäste am häufigsten, dann das Formular unter Noch Fragen? Schreiben Sie dem Gastgeber. Daneben zeigt eine Karte Foto und Namen des Objekts mit Daten und Gästen; Verfügbarkeit prüfen führt mit ihnen zurück zur Objektseite. Nach dem Absenden sieht der Besucher die Referenz, die der Dienst zurückgibt. Die Seite steht nicht in der Sitemap, und Suchmaschinen werden gebeten, sie nicht aufzunehmen.

Fynbos & Tide Holiday Homes kontaktieren, mit den Angaben zum Gastgeber und den häufigsten Fragen der Gäste, daneben eine Karte mit Foto und Reisedaten des Objekts
Die Anfrageseite. Das Formular folgt unter den Fragen.

Seiten mit nav: true stehen in der Kopfzeile, auf dem Handy unter Menü. Die contact-Seite zeigt die Karte der Firma aus dem Abschnitt contact (Anschrift, eine Telefonnummer zum Anrufen, eine E-Mail-Adresse zum Schreiben, Erreichbarkeit) und das Formular des Abschnitts form; nach dem Absenden meldet sie Nachricht gesendet mit der Referenz. Eine info-Seite zeigt den Text ihrer Einstellung als Absätze; solange dieser Text leer ist, antwortet die Seite mit 404, und ihr Link verschwindet aus der Fußzeile.

Die Kontaktseite mit der Karte der Agentur links und dem Nachrichtenformular rechts
Die Kontaktseite: links der Abschnitt contact, rechts der Abschnitt form.

Die Fußzeile steht auf jeder Seite: der Name der Website mit Anschrift, Telefonnummer und E-Mail-Adresse, die Links zu den Seiten mit footer: true und die Copyright-Zeile mit dem laufenden Jahr. Das Portal findet diese Angaben unter den öffentlichen Einstellungen an ihrem Schlüssel (er enthält address; phone, tel oder mobile; email oder e-mail) und prüft bei Telefon und E-Mail, ob der Wert danach aussieht.

Ein Wert eines amenities-Felds bekommt im Manifest "icon": "<Schlüssel>". Die Schlüssel unten bleiben stabil: Neue kommen dazu, keiner wird umbenannt oder entfernt. Ein unbekannter oder fehlender Schlüssel zeigt ein Häkchen.

accessible, aircon, baby, balcony, bath, bbq, bed, bike, boat, car, check, coffee, dishwasher, dryer, fireplace, fridge, garden, gym, heating, hot-tub, kitchen, mountain-view, microwave, no-smoking, parking, pets, ev-charger, pool, safe, sauna, sea-view, self-checkin, shower, terrace, tv, beach, washer, wifi, winter, workspace

nexxus ruft POST <Dienst-URL>/portal/<Endpunkt> mit einem Aufruf-Token, dessen trigger portal:<Endpunkt> ist. Dahinter steht keine Person: Ruft der Dienst zurück, handelt er als Dienstkonto der Installation, wie bei einem Ereignis. Der Rumpf:

{
"endpoint": "quote",
"method": "GET",
"params": { "record": 12, "check_in": "2026-12-20", "check_out": "2026-12-27", "guests": "4" },
"form": null,
"locale": "en",
"installation": { "id": 7, "version": "1.2.0", "settings": { "company_name": "Higgins Stays" } }
}

locale ist die Sprache des Besuchers, de, en oder pl. params sind die erklärten Parameter, wie der Besucher sie geschickt hat, record als Zahl. Bei POST enthält form die geprüften Felder mit ihren Typen: Zahlen als Zahlen, consent als true.

Für einen slots-Abschnitt fragt das Portal den endpoint je eine Woche: from und to sind Daten (YYYY-MM-DD), beide eingeschlossen. Der Dienst antwortet mit den freien Terminen dieser Tage in der Zeitzone, in der sie gezeigt werden; ein Tag ohne freie Termine darf fehlen oder eine leere Liste haben, und ein Termin in der Vergangenheit darf nicht dabei sein. Bieten zwei Personen dieselbe Uhrzeit an, trägt jeder der beiden Termine eine eigene id, eine beliebige Zeichenfolge bis 200 Zeichen; das Portal liest sie nicht:

{ "timeZone": "Europe/Berlin",
"days": [ { "date": "2026-10-22",
"slots": [ { "id": "beispiel-0830", "start": "2026-10-22T08:30:00+02:00",
"end": "2026-10-22T09:00:00+02:00", "label": "Dr. Beispiel" },
{ "id": "muster-0830", "start": "2026-10-22T08:30:00+02:00",
"end": "2026-10-22T09:00:00+02:00", "label": "Dr. Muster" } ] } ] }

Der request-Endpunkt bekommt die id des gewählten Termins, ohne id seinen start, genau so, wie der Dienst sie geliefert hat, als den Parameter, den slotParam nennt. Ohne id kämen zwei Termine mit demselben start als derselbe Wert an; das Portal zeigt deshalb nur den ersten. Der Dienst prüft den Termin noch einmal: Ist er noch frei, antwortet er wie auf eine Buchungsanfrage ({ "ok": true, "reference": "T-0042", "message": { "de", "en" } }); ist er vergeben, antwortet er 409, und das Portal zeigt seine eigene Meldung und lädt die Woche neu.

Der Dienst antwortet mit JSON, einem Objekt oder einer Liste, höchstens 64 KB. nexxus gibt es unverändert an das Portal. Lehnt der Dienst ab, antwortet er 400, 404, 409 oder 422 mit { "errorCode", "message", "messages": { "de", "en", "pl" }, "fieldErrors": { "<feld>": "..." } }, pl optional. nexxus gibt diese Schlüssel als kurzen reinen Text weiter (eine Meldung als { "de", "en", "pl" } in der Sprache des Besuchers, sonst auf Englisch, sonst auf Deutsch) und weitere Schlüssel nur als kurze Codes, Zahlen oder ja/nein, etwa "reason": "booked"; alles andere fällt weg. Ist der Dienst nicht erreichbar oder braucht er länger als 10 Sekunden, wird daraus 503 SERVICE_UNAVAILABLE, jeder andere Fehler wird 502 SERVICE_ERROR. Den eigenen Fehlertext des Dienstes sieht der Besucher nie.

Das Portal hat auch eine angemeldete Seite: Die Kunden eines Maklers sehen ihre Verträge, die Eigentümer eines Ferienhauses ihre Buchungen. Sie sind Mitglieder, keine Benutzer der App. Jedes Mitglied gehört zu einem Datensatz des Pakets (seiner Firma, seinem Objekt), sieht nur, was das Paket für seine Zielgruppe freigibt, und handelt mit den Rechten der Person, die es eingeladen hat, seiner verantwortlichen Person. Die Historie des Datensatzes zeigt diese Person, das Modulprotokoll nennt das Mitglied.

Das Manifest beschreibt eine Zielgruppe unter portal.members: die Entität, zu der ein Mitglied gehört (scope), die Datensätze und Felder, die es liest, was es anlegen darf, seine Aktionen, Ansichten und Rollen:

"members": {
"kunde": {
"label": { "de": "Kundenbereich", "en": "Client area" },
"scope": "firma",
"start": "konto.start",
"records": {
"firma": { "fields": ["name", "ampel"] },
"vertrag": { "via": "firma", "fields": ["titel", "sparte", "ablauf", "dokument"] }
},
"views": ["konto.start", "konto.vertraege"],
"actions": ["nachweis-einreichen"],
"uploads": { "folder": ["Kundenbereich", "{{scope.title}}"], "maxMb": 20 },
"roles": {
"leitung": { "label": { "de": "Geschäftsführung", "en": "Management" }, "admin": true, "secondFactor": true },
"technik": { "label": { "de": "Technik", "en": "Facilities" }, "views": ["konto.start"] }
}
}
}
  • Anmeldung auf der Portal-Domain unter /konto, mit einem Link per E-Mail, ohne Passwort. Eine Rolle mit secondFactor verlangt zusätzlich einen Code aus einer Authenticator-App. Mitarbeitende laden jemanden auf der Seite des Datensatzes ein, Reiter Kundenbereich, oder bitten den Assistenten darum; ein admin-Mitglied verwaltet seine Kollegen selbst. Eine Einladung trägt eine Sprache, de, en oder pl (Standard: die des Mandanten), und die Mails zur Einladung und zur Anmeldung gibt es in allen drei.
  • Jede Seite nach der Anmeldung ist eine Ansicht des Pakets mit dem Slot member, fensterfüllend. nexxus zeichnet nur die Anmeldeseiten darum, mit dem Logo der Firma, damit der Bereich nach dem Paket aussieht und nicht nach nexxus. Was so eine Ansicht aufrufen darf, steht unter Ansichten und das View-SDK.
  • Was ein Mitglied liest, prüft der Server bei jedem Aufruf: den Weg via vom Datensatz zu seinem eigenen, die Filter der Zielgruppe, die Felder seiner Rolle. Ein Datensatz außerhalb antwortet mit 404, als gäbe es ihn nicht.

Ein Paket mit Portal fragt nach der Zustimmung portal:public, eines mit Kundenbereich zusätzlich nach portal:members (wer, welche Daten, was die Mitglieder tun dürfen). Der Installationsdialog zeigt in einem Satz, den nexxus aus dem Manifest baut, was öffentlich wird: welche Datensätze, welche Felder, dass Bilder und eine gerundete Lage mitgehen und welchen Dienst Besucher aufrufen können. Ein Update, das etwas anderes veröffentlicht (ein weiteres Feld, einen anderen Filter, einen anderen Endpunkt), fragt erneut; ohne Zustimmung bleibt das Paket auf seiner Version.

Das Hochladen wird abgelehnt, mit einer Zeile je Fund, wenn ein Portal ein Datenobjekt, Feld, eine Seite, einen Endpunkt oder Dienst nennt, den es nicht gibt, ein Feld eines Typs veröffentlicht, der nicht öffentlich sein darf, nach einem Feld filtert, das nicht indexed ist, einen Seitenpfad doppelt vergibt, in einem Seitenabschnitt ein Feld des falschen Typs nennt oder einen Endpunkt mit der falschen Methode aufruft. Bei slots prüft sie außerdem, dass der Abschnitt auf einer detail-Seite steht, dass endpoint die Parameter record, from und to erklärt, request die Parameter record und den slotParam, und dass days zwischen 7 und 90 liegt.