Personen / Team Content-Type — node.person + card-person SDC + CardPerson Canvas-Code-Component
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.personContent-Type mit Feldern Name (Pflicht), Funktion (Pflicht), Foto (Pflicht), Telefonnummer (Pflicht, klickbarertel:-Link), E-Mail (optional, klickbarermailto:-Link), LinkedIn-URL (optional, externer Link). Generisch genug für HR-Recruiter, Sales-Kontakte und perspektivisch Team-Members. -
card-personSDC 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 bestehendencard(Blog-View-Mode), und ist der zweite prop-driven Sibling der Card-Familie. -
CardPerson.jsxCanvas-Code-Component als Editor-Platzierungs-Anker. Redakteur:innen wählen eine Person über die ProppersonNid(Number-Picker). Die Komponente lädt den Person-Node via JSON:API (JsonApiClient+ SWR-Cache) und delegiert das Rendering an diecard-personSDC. 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
- 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.
- 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.
- 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.
- 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.
- Als Besucher:in möchte ich, dass Personen ohne LinkedIn-Profil keinen leeren LinkedIn-Link zeigen, damit die Card visuell sauber bleibt.
- 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.
- Als Redakteur:in möchte ich neue Personen als Drupal-Content-Nodes anlegen können, damit ich sie mehrfach auf unterschiedlichen Seiten wiederverwenden kann.
- 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.
- 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.
- 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. - 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.
- 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.
- 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.
- Als Site-Builder möchte ich, dass
card-personals prop-driven SDC der Card-Familie verfügbar ist, damit ich sie konsistent zu den anderen Card-Siblings (card,card-feature) verwenden kann. - 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.
- 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. - 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:
| Feld | Typ | Pflicht | Bemerkung |
|---|---|---|---|
field_name | string | ja | Vorname + Nachname als einzelnes Feld (kein field_firstname / field_lastname-Split — Render-seitig nicht nötig). |
field_funktion | string | ja | Stellenbezeichnung / Rolle (z. B. „HR Manager", „Senior Account Manager"). |
field_photo | image | ja | Image-Field mit Image-Style für Card-Render. |
field_phone | telephone | ja | Drupal-Core-Modul telephone einmalig aktivieren (drush en telephone) — kein composer require. Wird Render-seitig als klickbarer tel:-Link ausgespielt. |
field_email | nein | Wird Render-seitig als klickbarer mailto:-Link ausgespielt, falls vorhanden. | |
field_linkedin_url | link | nein | Externer 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:
| Prop | Typ | Pflicht | Bemerkung |
|---|---|---|---|
name | string | ja | Vorname + Nachname (eine Zeile). |
funktion | string | ja | Stellenbezeichnung. In Figma kleiner gerendert als name. |
photo_url | string | ja | URL zum bereits prozessierten Image-Style. Kein File-Object — pure URL. |
photo_alt | string | ja | Alt-Text aus dem Image-Field. |
phone | string | ja | Telefonnummer im darstellbaren Format. Wird im Template zu <a href="tel:...">. |
email | string | nein | Falls leer/null: kein E-Mail-Element. Wird zu <a href="mailto:...">. |
linkedin_url | string | nein | Falls 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:
- SWR-Hook lädt
node/person/{personNid}via projektweitemJsonApiClient. - Loading-State: Skeleton-Render mit Platzhalter-Geometrie der Card (kein Spinner — der erzeugt CLS).
- Error-State: Sichtbarer, aber dezenter Fehlerblock („Person konnte nicht geladen werden") — keine Stack-Traces, kein Crash der gesamten Canvas-Page.
- 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 („wennname+funktiongesetzt: 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_recruiteraufnode.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 dietitle-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-personmitname="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 korrektersrc+ klickbarertel:-Link mit korrektemhrefexistieren im Output. -
Optional-Felder-Render-Test: Rendert mit gesetzten
email+linkedin_urlund verifiziert:mailto:-Link + LinkedIn-Anker mittarget="_blank"+rel="noopener"sind im Output. -
Optional-Felder-leer-Test: Rendert ohne
emailund ohnelinkedin_urlund verifiziert: keinemailto:-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.personmit allen Pflicht- und Optional-Feldern an. Navigiert zur Node-Render-Page mit View-Modecard_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_settingshat ein Feldfield_default_recruiter, dieses Feld ist eine Entity-Reference aufnode, und dashandler_settings.target_bundlesenthält ausschließlichperson.
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-Frame62110:27008alstitle-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, Node62110:27008(„ContentItem" mit Boolean-Variants person/title). Dieperson-Variant ist die Render-Vorlage fürcard-person; dietitle-Variant ist Out-of-Scope (Folge-Storycard-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-mapping→solcom-figma-to-sdc→drupal-canvas→solcom-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.htmlzusammengefasst.