Architecture Decision Record · 0032

Project-Offer-Sync-Hygiene — URL-Kanon, Title-Fallback, Outbound-Field-Binding, Sitemap-IA

accepted

TL;DR. Vier verbindliche Regeln für jedes künftige Migrate-Sync-Modul (initialer Anwender: solcom_project_offer): (1) Kanonische URL /projektpartnerportal. (2) Kein null_coalesce-Fallback auf SF-IDs für Pflicht-Felder — skip_on_empty(method: row) + SOQL-Filter. (3) Canvas-Bindings an outbound-only Felder nur mit optionalem SDC-Schema + Twig-{% if %}-Guard. (4) Menü-Struktur folgt der Sitemap-IA (docs/context/0002-sitemap.html) — keine parallelen Top-Level-Items für IA-Sub-Knoten.

Kontext

Die Fix-Iteration auf feature/P2503-108 (2026-05-15) hat fünf Drift-Klassen aufgedeckt, die durch das Zusammenspiel von migrate_plus, Pathauto, Canvas-Templates und manueller Menü-Pflege entstanden sind:

  • URL-Drift: Plan/PRD-Drafts (2026-05-13/14) führten /projektportal/projektangebote/ als Pfadschema; CONTEXT.md + ADR-0031 wurden später auf /projektpartnerportal aktualisiert. Code spaltete sich auf: View an /projektportal, Pathauto-Pattern an /projektpartnerportal/[node:title], Canvas-Buttons an /projektpartnerportal.
  • Title-Fallback auf SF-ID: process.title: { plugin: null_coalesce, source: [TR1__Job_Title__c, Id] }. Records ohne Job_Title bekamen die SF-ID als Titel, Pathauto sluggifizierte das zu /a1wau000001ylat2au.
  • Canvas-Binding an outbound-only Felder: Das Canvas-Template node.project_offer.full bindet sticky-apply-bar.apply_url an field_sf_application_url — laut Migrate-Kommentar „outbound-only" und damit NIE durch den Sync gefüllt. Ergebnis: Twig-Runtime-Errors auf jeder Detail-Seite.
  • High-Water-Fallstrick: migrate_plus' high_water_property: LastModifiedDate hielt die letzte Sync-Marke fest. 219 SF-Records waren am Source, nur 5 mit LastModifiedDate > high_water wurden importiert — kein Watchdog-Eintrag, weil High-Water-Skips silent passieren.
  • Sitemap-IA-Verletzung in der Planung: Ein erster Plan-Entwurf wollte „Projektpartnerportal" als neuen Top-Level-Eintrag ins Main-Menü hängen. docs/context/0002-sitemap.html definiert die View aber explizit als N16 Projektliste — Sub-Knoten unter dem bestehenden Top-Level N14 „Für Freiberufler".

Jede einzelne dieser fünf Klassen ist mehrfach in der Doku-Strange dokumentiert, aber bisher in keinem ADR konsolidiert. Dieser ADR macht die Regeln für künftige Migrate-Sync-Module verbindlich.

Entscheidung

1) URL-Kanon: /projektpartnerportal superseded by ADR-0033

Supersede-Note (2026-05-25): Diese Regel ist durch ADR-0033 ersetzt. Der kanonische Pfad ist /fuer-freiberufler/projektliste/<slug> (Sitemap-Node N16 unter N14, Context-0002); /projektpartnerportal/ bleibt als Legacy-301-Redirect. ADR-0033 enthält Begründung, Hook-Strategie und Idempotenz-Garantien. Die übrigen Regeln R2–R4 unten bleiben in Kraft.

Original-Wortlaut (zur Nachvollziehbarkeit): Listing-View und Pathauto-Pattern für project_offer-Nodes liegen unter /projektpartnerportal. CONTEXT.md und docs/adr/0031-sf-publication-frontend-decomposition.html sind die Wahrheitsanker. Ältere PRD/Plan-Drafts (2026-05-13 PRD, 2026-05-14 Plan) mit /projektportal/projektangebote/ sind superseded; Inline-Worklogs in diesen Dokumenten markieren das.

2) Migrate-Title-Pflicht: kein ID-Fallback

Records ohne den Pflicht-Title-Source-Wert werden nicht zu Drupal-Nodes. Verbindliches Migrate-Pattern (belt-and-suspenders):

source:
  urls:
    - "...?q=...+WHERE+RecordType.DeveloperName='Publication'+AND+TR1__Job_Title__c+!%3D+null"

process:
  title:
    -
      plugin: skip_on_empty
      method: row
      source: TR1__Job_Title__c
    -
      plugin: get
      source: TR1__Job_Title__c

Kein null_coalesce mit einer ID-artigen Quelle als Fallback. Das produziert unbenutzbare Nodes mit der SF-ID als Titel und damit als Pathauto-Slug.

3) Outbound-Field-Binding-Disziplin

Canvas-Content-Templates dürfen Felder, die ausschließlich outbound beschrieben werden (z. B. field_sf_application_url via Writeback-Queue), nur mit zwei Schutzlagen binden:

  1. Das SDC-Schema deklariert das Prop als optional und nullable (type: ["string", "null"], kein required:-Eintrag).
  2. Das Twig-Template kapselt den Render-Body in einen {% if <prop> is not empty %}…{% endif %}-Guard.

Outbound-Felder werden in docs/context/0008-fcsb-bh4sf-publication-mapping.html markiert; jedes neue Canvas-Template muss dieses Mapping konsultieren, bevor es ein Field bindet.

4) Menü-Struktur folgt der Sitemap-IA

docs/context/0002-sitemap.html ist die Wahrheits-Vorlage für Top-Level- und Sub-Knoten der Main-Navigation. Neue Routes werden als Sub-Item unter dem passenden Strang-Top-Level (N07, N14, N22, …) angelegt, nicht als paralleler Top-Level-Eintrag. Konkrete Anwendung: /projektpartnerportal ist N16 „Projektliste" unter N14 „Für Freiberufler". (N14 wurde im Zuge dieser ADR von „Für Freiberufler / Projektportal" auf „Für Freiberufler" gekürzt, da das URL-Anliegen jetzt vom Sub-Item getragen wird.)

Verbindlichkeit

Diese Regeln gelten für solcom_project_offer (initialer Anwender) und alle künftigen Migrate-Sync-Module (Personio-Feeds, Freelancer-Plattformen). Diff-time-Reviewer (skills) prüfen sie mit:

  • drupal-config-architecture-reviewer: kein null_coalesce-Fallback auf Id-artige Source-Felder im process.title.
  • drupal-twig-sdc-reviewer: Canvas-Bindings an Felder aus der Outbound-Liste (Context-0008) brauchen required:-Removal + Twig-Guard.

Rejected alternatives

AlternativeWarum verworfen
Schema relaxen ohne Twig-Guard Versteckt das Symptom auf SDC-Ebene, fliegt beim ersten Renderer-Wechsel wieder auf. Kein redundanter Sicherheitsgurt — wenn das Schema-Tooling versagt, fehlt der Fallback.
Nur SOQL-Filter, kein skip_on_empty Single point of failure. SOQL-Cache, Index-Drift oder ein versehentlich entfernter WHERE-Teil reichen aus, um NULL-Titel wieder ins Drupal zu kippen.
Neuer Top-Level-Eintrag „Projektpartnerportal" Verletzt die Sitemap-IA. N14 (Strang 2) ist bereits der Anker für Freelancer-Themen — ein paralleler Top-Level fragmentiert den Strang und macht den User-Pfad unklarer.
null_coalesce(..., Id) als Migrate-Komfort Produziert unbenutzbare Drupal-Nodes (SF-ID als sluggifizierter Titel und Alias), und weil das technisch ein Success ist, schweigt migrate:messages. Defekt bleibt unsichtbar.

Consequences

  • Test-Erweiterung: Migrate-Kernel-Tests im solcom_project_offer-Modul müssen einen Test-Fall „Source-Row ohne TR1__Job_Title__c" enthalten, der nachweist, dass kein Node entsteht.
  • Re-Import-Hygiene: Bei Code-Änderungen am Migrate-Pipeline-Prozess muss vor dem nächsten drush mim der High-Water-Marker explizit gelöscht werden (\Drupal::keyValue('migrate:high_water')->delete('solcom_project_offer')) — sonst werden ältere Records still übersprungen. Dieser Hinweis wandert in die README.md des Moduls.
  • Reviewer-Agents: Die zwei oben genannten Diff-time-Reviewer bekommen einen neuen Rule-Check; die Skill-Definitionen unter .claude/skills/drupal-config-workflow/ und .claude/skills/drupal-twig-sdc-authoring/ referenzieren diese ADR als Source-of-truth.
  • IA-Folge-Aufgabe: Die Konsolidierung des Strang-2-Teilbaums (N15, N17, N19 als Sub-Items unter N14) ist nicht in dieser Iteration enthalten — die Canvas-Pages page/14 (Mein Projektpartnerprofil), page/16 (Projekt-Alert) und das Referenzen-Item liegen heute als Top-Level oder mit falschem Parent in der DB. Ein eigener Restructuring-PR muss docs/context/0002-sitemap.html als Soll-Vorlage abarbeiten.
  • Pathauto-Bulk: Nach jedem Title-Pipeline-Change muss drush pathauto:aliases-generate update canonical_entities:node --no-interaction laufen, damit Aliase mit dem aktuellen Pattern neu generiert werden. Stale Aliase aus pre-Rename-Migrates müssen manuell weggeräumt werden.