P2503 · Content-Type · SDC · Canvas-Component · P2503-97

Personen / Team Content-Type — node.person + card-person SDC + CardPerson Canvas-Code-Component

approved P2503-97 Erstellt: 2026-05-25 · Target-Release: 2026-07-08 · Figma: 62110:27008 · Authority: ADR-0046
TL;DR: Drei neue Artefakte als kohärentes Paket. (1) Generischer Custom-Content-Type node.person mit Pflichtfeldern Name, Funktion, Foto und optionalen Kontakt-Feldern Telefon, E-Mail, LinkedIn-URL. (2) Neue prop-driven SDC card-person als zweiter prop-driven Sibling neben card nach ADR-0009-Pattern, die node.person im View-Mode card_quote rendert (Figma-Layout 62110-27008 + Kontakt-Extension). (3) Canvas-Code-Component CardPerson.jsx als Editor-Anker, die node.person via JSON:API (JsonApiClient + SWR) lädt und an die SDC delegiert. MVP-Recruiter-Anbindung an Vacancies erfolgt via config_pages.career_settings.field_default_recruiter als Fallback; granulare Vacancy↔Person-Verknüpfung ist deferred zu einer Folge-Story.

Problem Statement

SOLCOM präsentiert Mitarbeitende (HR-Recruiter, Sales-Kontakte, Team-Mitglieder) heute ausschließlich als hartcodierte Textblöcke innerhalb von Personio-importierten Vacancy-Bodies oder als manuell platzierte Twig-Snippets in Canvas-Pages. Eine Recruiterin wie Laura Gerardi (HR Manager) erscheint auf Vacancy-Detailseiten als unstrukturierter Block mit Name, Funktion und Telefonnummer — ohne Foto, ohne wiederverwendbare Datenstruktur, ohne View-Mode-Listing-Fähigkeit. Es existiert kein Drupal-Content-Type für Personen, kein wiederverwendbares Render-Pattern und keine Verknüpfung zwischen Vacancy und zuständigem Recruiter.

Für die Stakeholder-Kommunikation („Vertrauen aufbauen, Ansprechpartner schnell finden") ist diese Lücke kritisch. Karriere-Interessenten wollen wissen, wer hinter einer Stelle steht, bevor sie sich bewerben. Redakteur:innen brauchen einen Editor-Workflow, der Personen-Cards auf Canvas-Pages platzieren lässt — ohne pro Card einen neuen Twig-Snippet zu hartcodieren.

Solution

Wir bauen einen generischen, wiederverwendbaren Personen-Stack — drei Artefakte mit klaren Schnittstellen:

  • node.person Content-Type mit Feldern Name (Pflicht), Funktion (Pflicht), Foto (Pflicht), Telefonnummer (Pflicht, klickbarer tel:-Link), E-Mail (optional, klickbarer mailto:-Link), LinkedIn-URL (optional, externer Link). Generisch genug für HR-Recruiter, Sales-Kontakte und perspektivisch Team-Members.
  • card-person SDC als prop-driven Render-Komponente, die alle Person-Felder als typisierte Props entgegennimmt und das Figma-Layout 62110-27008 (Foto links, Position klein + Vorname+Name groß rechts) plus den ergänzenden Kontakt-Stack (Tel / E-Mail / LinkedIn) rendert. Folgt dem Pattern aus ADR-0009, analog zur bestehenden card (Blog-View-Mode), und ist der zweite prop-driven Sibling der Card-Familie.
  • CardPerson.jsx Canvas-Code-Component als Editor-Platzierungs-Anker. Redakteur:innen wählen eine Person über die Prop personNid (Number-Picker). Die Komponente lädt den Person-Node via JSON:API (JsonApiClient + SWR-Cache) und delegiert das Rendering an die card-person SDC. Loading-State und Error-State sind explizit modelliert.

Für die MVP-Anbindung an Vacancies (deren Editorial-Workflow per ADR-0026 ausgeschlossen ist — Personio ist Single-Source-of-Truth) verwenden wir einen schmalen Fallback-Mechanismus: config_pages.career_settings erhält eine neue Entity-Reference-Field field_default_recruiter auf node.person. Die Vacancy-Sidebar zeigt die so referenzierte Default-Person via CardPerson. Granulare Vacancy↔Person-Verknüpfung (per recruiter_id aus Personio oder per Editorial-Override-Feld) ist als Folge-Story dokumentiert.

User Stories

  1. Als Karriere-Interessent:in möchte ich auf einer Vacancy-Detailseite sehen, wer der zuständige Recruiter ist (Name, Foto, Funktion), damit ich vor einer Bewerbung weiß, wer mich begleitet.
  2. Als Karriere-Interessent:in möchte ich den Recruiter direkt per Telefon, E-Mail oder LinkedIn kontaktieren können, damit ich vor einer Bewerbung Fragen stellen kann.
  3. Als Karriere-Interessent:in auf einem Mobilgerät möchte ich auf die Telefonnummer tippen können, damit der Anruf direkt aus dem Browser gestartet wird.
  4. Als Besucher:in möchte ich auf der Team-Seite Mitarbeitende mit Foto, Funktion und Kontaktinformationen sehen, damit ich Ansprechpartner schnell finde und Vertrauen aufbaue.
  5. Als Besucher:in möchte ich, dass Personen ohne LinkedIn-Profil keinen leeren LinkedIn-Link zeigen, damit die Card visuell sauber bleibt.
  6. Als Besucher:in möchte ich, dass die LinkedIn-URL in einem neuen Browser-Tab geöffnet wird, damit ich die SOLCOM-Seite nicht verlasse.
  7. Als Redakteur:in möchte ich neue Personen als Drupal-Content-Nodes anlegen können, damit ich sie mehrfach auf unterschiedlichen Seiten wiederverwenden kann.
  8. Als Redakteur:in möchte ich für jede Person ein Foto, einen Namen, eine Funktion und mindestens eine Telefonnummer angeben können, damit die Person als Ansprechpartner:in eindeutig dargestellt ist.
  9. Als Redakteur:in möchte ich E-Mail und LinkedIn-URL als optionale Felder pflegen können, damit ich nur die wirklich vorhandenen Kontaktwege ausspielen muss.
  10. Als Redakteur:in möchte ich im Canvas-Editor eine CardPerson-Komponente platzieren und über einen Node-Picker die anzuzeigende Person wählen können, damit ich Personen-Cards überall ohne Twig-Code platzieren kann.
  11. Als Redakteur:in möchte ich, dass die Canvas-Komponente nach Auswahl der Person sofort eine Live-Vorschau zeigt, damit ich die richtige Person erwischt habe, bevor ich die Seite veröffentliche.
  12. Als Redakteur:in möchte ich in den Karriere-Settings einen Default-Recruiter definieren können, damit alle Vacancies einen einheitlichen Ansprechpartner haben, solange keine granulare Zuordnung existiert.
  13. Als HR-Stakeholder:in möchte ich, dass die heute auf Vacancy-Bodies sichtbare Recruiterin (Beispiel: Laura Gerardi, HR Manager, Tel. 07121/1277-344) durch den strukturierten Content-Type ersetzt wird, damit der Datenstand zentral pflegbar und nicht mehr in Personio-Free-Text vergraben ist.
  14. Als Site-Builder möchte ich, dass card-person als prop-driven SDC der Card-Familie verfügbar ist, damit ich sie konsistent zu den anderen Card-Siblings (card, card-feature) verwenden kann.
  15. Als Entwickler:in möchte ich, dass die SDC keine Conditional-Schema-Logik enthält (kein „Modus A vs. Modus B"-Switch), damit das Anti-Pattern aus ADR-0004 nicht erneut eingeführt wird.
  16. Als Entwickler:in möchte ich, dass die Canvas-Code-Component die JSON:API-Anfrage über den projektweiten JsonApiClient + SWR-Cache stellt, damit das Caching-Verhalten konsistent zu anderen Canvas-Code-Components ist.
  17. Als Entwickler:in möchte ich, dass die SDC ohne Drupal-Render-Kontext (rein als Twig-Render) im Kernel-Test renderbar ist, damit Render-Logik unabhängig vom Storage testbar ist.

Implementation Decisions

Modul 1 — node.person Content-Type

Generischer Custom-Content-Type, angelegt als Drupal-Config-Bundle. Feld-Schema:

FeldTypPflichtBemerkung
field_namestringjaVorname + Nachname als einzelnes Feld (kein field_firstname / field_lastname-Split — Render-seitig nicht nötig).
field_funktionstringjaStellenbezeichnung / Rolle (z. B. „HR Manager", „Senior Account Manager").
field_photoimagejaImage-Field mit Image-Style für Card-Render.
field_phonetelephonejaDrupal-Core-Modul telephone einmalig aktivieren (drush en telephone) — kein composer require. Wird Render-seitig als klickbarer tel:-Link ausgespielt.
field_emailemailneinWird Render-seitig als klickbarer mailto:-Link ausgespielt, falls vorhanden.
field_linkedin_urllinkneinExterner Link mit target="_blank" und rel="noopener", falls vorhanden.

Der Node-Title wird auf field_name gemappt (Auto-Title via title_pattern oder via hook_node_presave). Das vermeidet, dass Redakteur:innen den Namen redundant in zwei Feldern pflegen.

Modul 2 — entity_view_display.node.person.card_quote

Neuer View-Mode card_quote wird auf node.person angelegt. Das Display rendert via SDC-Field-Formatter byte_theme:card-person und mappt die Felder direkt auf die SDC-Props. Andere View-Modes (card_team, full) sind explizit Out-of-Scope dieser Story.

Bemerkung zum Naming: Der View-Mode heißt card_quote, nicht person_card — das spiegelt das semantische Container-Pattern aus Figma (der Frame heißt dort „ContentItem mit Quote-Variant", die Person ist die Quelle des Quotes). Die SDC selbst trägt aber den fachlichen Namen card-person und ist nicht an den Quote-Kontext gebunden — das Naming-Mismatch ist bewusst, damit der View-Mode später für eigene Quote-Statements wiederverwendet werden kann.

Modul 3 — card-person SDC

Prop-driven SDC. Kein bubble-card-frame-Include, kein Slot-Inhalt, kein Conditional-Schema. Props:

PropTypPflichtBemerkung
namestringjaVorname + Nachname (eine Zeile).
funktionstringjaStellenbezeichnung. In Figma kleiner gerendert als name.
photo_urlstringjaURL zum bereits prozessierten Image-Style. Kein File-Object — pure URL.
photo_altstringjaAlt-Text aus dem Image-Field.
phonestringjaTelefonnummer im darstellbaren Format. Wird im Template zu <a href="tel:...">.
emailstringneinFalls leer/null: kein E-Mail-Element. Wird zu <a href="mailto:...">.
linkedin_urlstringneinFalls leer/null: kein LinkedIn-Element. Wird zu <a href="..." target="_blank" rel="noopener">.

Twig-Template folgt dem Figma-Layout 62110-27008 (Foto links, Text-Stack rechts mit kleinerer Funktion oben + größerem Namen darunter) und ergänzt darunter den Kontakt-Stack (Tel / E-Mail / LinkedIn untereinander, je nur falls Wert vorhanden). Diese „Figma + Extension"-Konfiguration ist explizit in ADR-0046 festgehalten und wird im solcom-figma-reviewer-Diff dokumentiert, damit das Layout nicht als Drift gemeldet wird. CSS via Tailwind-@apply in card-person.css, mit Wurzel-Klasse .solcom-card-person.

Modul 4 — CardPerson.jsx Canvas-Code-Component

Editor-Platzierungs-Anker. Single-Prop personNid (Number, Drupal-Node-ID Picker im Canvas-Editor). Component-Body:

  1. SWR-Hook lädt node/person/{personNid} via projektweitem JsonApiClient.
  2. Loading-State: Skeleton-Render mit Platzhalter-Geometrie der Card (kein Spinner — der erzeugt CLS).
  3. Error-State: Sichtbarer, aber dezenter Fehlerblock („Person konnte nicht geladen werden") — keine Stack-Traces, kein Crash der gesamten Canvas-Page.
  4. Success-State: Mapping der JSON:API-Response-Felder auf die card-person-SDC-Props (über die etablierte SDC-Render-Bridge im byte_theme).

Die JSON:API-Anfrage nutzt nur Standard-Felder — keine custom JSON:API-Resource. Caching läuft über den projektweiten SWR-Cache (Re-Validate-on-Focus aus, da Personen-Daten statisch sind und nicht in Edit-Sessions ändern).

Modul 5 — config_pages.career_settings.field_default_recruiter

Neue Entity-Reference-Field auf der bestehenden config_pages.career_settings-Entity, die auf node.person referenziert. Single-Value (genau ein Default-Recruiter pro Site). Die Vacancy-Sidebar (separater Render-Pfad in node.vacancy's Canvas-Template) liest dieses Feld und rendert die referenzierte Person via CardPerson-Canvas-Component oder direkt via card-person-SDC. Die finale Render-Mechanik (Sidebar-direkt vs. Canvas-Slot) ist Folge-Story-Scope; das PRD legt nur das Feld an.

Verworfen / deferred

  • Erweiterung der bestehenden card-SDC um Person-Felder. Würde Conditional-Schema („wenn name + funktion gesetzt: Person-Modus, sonst Blog-Modus") einführen, was per ADR-0004 verworfenes Anti-Pattern ist.
  • Dual-Mode card-quote-SDC mit Variant-Schalter. Gleicher Grund wie oben.
  • Separates field_recruiter auf node.vacancy. Würde mit ADR-0026 kollidieren (Personio = SoT, keine Editorial-Felder auf Vacancy). Granulare Verknüpfung ist Folge-Story.
  • card_team-View-Mode + Team-Übersichts-Drupal-View. Folge-Story.
  • full-View-Mode mit Person-Detail-Page. Folge-Story (Sitemap-Node N33).
  • Bio-Feld (text_long, Rich-Editor). Folge-Story.
  • Editorial-Quote-Block (card-quote-statement-SDC) für die title-Variant des Figma-Frames. Folge-Story.

Testing Decisions

Was einen guten Test ausmacht

Tests prüfen das beobachtbare Verhalten — gerenderter HTML-Output, sichtbare DOM-Struktur, funktionale Mappings. Nicht: interne Twig-Variablen, JSX-Hooks-Internals, JSON:API-Cache-Details. Gute Tests sind reproduzierbar, isoliert (Kernel statt Functional, wenn möglich) und beschreiben pro Test genau eine fachliche Behauptung.

Kernel-Tests (PHP) — card-person SDC

  • Pflicht-Felder-Render-Test: Rendert card-person mit name="Laura Gerardi", funktion="HR Manager", photo_url="/sites/default/files/photos/laura.jpg", phone="07121/1277-344" und verifiziert: Name + Funktion + Foto-IMG-Tag mit korrekter src + klickbarer tel:-Link mit korrektem href existieren im Output.
  • Optional-Felder-Render-Test: Rendert mit gesetzten email + linkedin_url und verifiziert: mailto:-Link + LinkedIn-Anker mit target="_blank" + rel="noopener" sind im Output.
  • Optional-Felder-leer-Test: Rendert ohne email und ohne linkedin_url und verifiziert: keine mailto:-Element-Sequenz und keine LinkedIn-Element-Sequenz sind im Output (keine leeren <a>-Tags).

Prior Art: SDC-Kernel-Tests im byte_theme, z. B. der existierende Render-Test für card (Tag card). card-person-Tests werden mit dem Tag card-person angelegt und per ddev exec ./vendor/bin/phpunit --group card-person ausgeführt.

Component-Test (JS) — CardPerson.jsx

  • Loading-State-Test: Mock-Fetch verzögert; Test verifiziert, dass die Skeleton-Placeholder-DOM-Struktur sichtbar ist, bevor die Daten ankommen.
  • Success-State-Test: Mock-Fetch liefert eine JSON:API-Person-Response; Test verifiziert, dass die card-person-DOM-Struktur (Name, Funktion, Foto, Tel-Link) gerendert wird.
  • Error-State-Test: Mock-Fetch liefert HTTP 404; Test verifiziert, dass der dezente Fehlerblock sichtbar ist und kein Crash der Test-Renderer-Root.

Prior Art: andere Canvas-Code-Components mit Tests (Stand 2026-05-25 noch sparsam — falls keine vergleichbaren Component-Tests existieren, etabliert CardPerson das Pattern, das später für andere Canvas-Code-Components wiederverwendet wird).

Functional/Browser-Test — node.person Content-Type + View-Display

  • End-to-End-Field-Display-Test: Legt programmatisch einen node.person mit allen Pflicht- und Optional-Feldern an. Navigiert zur Node-Render-Page mit View-Mode card_quote. Verifiziert: alle Felder erscheinen als korrekt markup im DOM (CSS-Selektoren für Name, Funktion, Foto, Tel-Link, E-Mail-Link, LinkedIn-Link).

Prior Art: Functional-Browser-Tests im web/modules/custom/-Ordner für andere Content-Type-Anlagen. Test wird mit dem Tag person oder als Functional-Class unter web/modules/custom/solcom_person/tests/src/Functional/ angelegt.

Config-Import-Test (Kernel) — field_default_recruiter

  • Config-Existenz-Test: Importiert die Config aus config/sync/. Verifiziert: config_pages.career_settings hat ein Feld field_default_recruiter, dieses Feld ist eine Entity-Reference auf node, und das handler_settings.target_bundles enthält ausschließlich person.

QA-Plan im Story-Hub

Der vollständige QA-Plan mit Click-Pfaden pro Akzeptanzkriterium (URL / Action / Expected) ist im Story-Hub F-170-personen-team-content-type.html Sektion #qa-plan dokumentiert. Diese Sektion ist Pflicht-Quelle für die Pre-PR-Acceptance-Stage des Ralph-Loops (ADR-0037) und für die Mensch-QA nach PR-Open.

Out of Scope

  • Granulare Vacancy↔Person-Verknüpfung (Personio-Mapping per recruiter_id, Editorial-Override-Field pro Vacancy). Folge-Story — ADR-pflichtig wegen Kollision mit ADR-0026.
  • View-Mode card_team + Team-Übersichts-Drupal-View + Navigations-Menü-Eintrag „Team".
  • View-Mode full + Person-Detail-Page mit eigener Route (Sitemap-Node N33).
  • Bio-Feld (text_long, Rich-Editor) für Person-Detail-Page.
  • card-quote-statement-SDC für den im Figma-Frame 62110:27008 als title-Variant enthaltenen Editorial-Quote-Block.
  • Massenimport von Personen-Daten aus externem System (HR-System, Personio-People-Endpoint). Alle Personen werden im MVP manuell redaktionell gepflegt.
  • Automatisches Konsistenz-Checking („jede Vacancy muss einen Recruiter haben") — der MVP-Default-Recruiter-Fallback erfüllt diese Anforderung implizit, ein dedizierter Validator ist Folge-Story.
  • Mehrsprachigkeit (i18n) der Personen-Daten. Felder bleiben im MVP einsprachig (Deutsch).
  • Anpassung oder Deprecation existierender Card-SDCs (card, card-feature, card-frame) — alle bleiben unverändert.

Further Notes

  • Figma-Referenz: File CHHDfbpX8Ys2LBiGs5HVK0, Node 62110:27008 („ContentItem" mit Boolean-Variants person/title). Die person-Variant ist die Render-Vorlage für card-person; die title-Variant ist Out-of-Scope (Folge-Story card-quote-statement).
  • Live-Referenz: karriere.solcom.de · Key Account Manager · Vertrieb (2602671) zeigt den Vorgänger-Block (Laura Gerardi, HR Manager, Tel. 07121/1277-344) als hartcodierten Personio-Body-Block. Dieser wird durch den strukturierten node.person + CardPerson-Stack ersetzt.
  • Story-Hub: docs/stories/F-170-personen-team-content-type.html enthält die 7 Akzeptanzkriterien (Stakeholder-Sprache), den QA-Plan mit Click-Pfaden, die Risiko-Items VC-1 und CP-1 sowie das Grilling-Log.
  • Decision-Authority: ADR-0046 (Topologie-Entscheidung), ADR-0009 (prop-driven Card-Pattern), ADR-0026 (Vacancy-Editorial-Exclusion), ADR-0004 (Conditional-Schema-Anti-Pattern).
  • Build-Workflow: Die Implementierung folgt der Skill-Kette solcom-design-mappingsolcom-figma-to-sdcdrupal-canvassolcom-figma-reviewer. Detailliert dokumentiert im Story-Hub-Abschnitt #build-workflow.
  • Canvas-Integration: Detaillierte Canvas-Konventionen (Code-Component-Registrierung, JSON:API-Fetch-Pattern, SWR-Cache-Strategy) sind in docs/wiki/canvas-integration.html zusammengefasst.