Problem Statement
Der Vendor Gulp (gulp.de) erwartet als Distribution-Schnittstelle eine simple HTML-Seite mit einer Liste anklickbarer Links zu den auf solcom.de gelisteten Projektangeboten. Heute liegt diese Liste auf dem alten ASP-System; mit dem Drupal-Cutover (≤ 08.07.2026) muss Drupal sie selbst produzieren, sonst fällt der Gulp-Kanal ersatzlos weg.
Die naheliegende Erstannahme war ein Push-Multiposting-Distributor mit Plugin-Architektur, Custom-Entity und Queue. Refinement hat diese Annahme verworfen: Gulp betreibt selbst kein Multiposting, sondern erwartet einen Pull-Endpunkt — analog zu F-340 für Freelancermap und freelance.de, nur HTML statt XML. Damit ist F-360 architektonisch eine kleine Erweiterung von F-340, kein neues Modell.
Solution
Drupal liefert nach dem Cutover unter /feeds/gulp.html ein text/html-Dokument mit einer HTML-Liste (oder Tabelle) aller publizierten Project-Offers, deren field_sf_channel_gulp-Flag in Salesforce gesetzt ist und deren Veröffentlichungs-Fenster aktiv ist. Jeder Eintrag ist ein <a>-Tag mit der kanonischen Drupal-Detail-URL des Offers als href und dem Jobtitel als Linktext.
Sämtliche Editorial-Pflege (welcher Offer geht an Gulp? Wann veröffentlicht?) passiert ausschließlich in Salesforce. Drupal ist Durchreiche — kein Editorial-Feld, keine eigene UI.
Auf der Detail-Seite, auf die der Link führt, bewirbt sich der Freelancer über den bestehenden Minggo-Flow (F-330). Der Feed selbst trägt keine Apply-URL und keine Tracking-Parameter — Tracking passiert bei Bedarf über die Vendor-eigene URL-Decoration vor dem Klick (siehe Out of Scope).
User Stories
- Als Vertriebsmitarbeiter möchte ich, dass jedes für Gulp freigegebene Projektangebot automatisch im Gulp-Kanal sichtbar wird, damit ich keine Pflege auf der Vendor-Seite leisten muss.
- Als Vertriebsmitarbeiter möchte ich pro Projektangebot in Salesforce ein Flag setzen, das steuert, ob das Angebot bei Gulp erscheint, damit ich den Kanal pro Projekt bewusst freischalten oder verbergen kann.
- Als Vertriebsmitarbeiter möchte ich, dass ein Projektangebot mit Veröffentlichungs-Endedatum in der Vergangenheit automatisch aus dem Gulp-Feed verschwindet, damit kein Freelancer auf eine abgelaufene Anfrage reagiert.
- Als Freelancer möchte ich aus Gulp heraus per Klick auf einer aussagekräftigen Detail-Seite von solcom.de landen, damit ich entscheiden kann, ob das Projekt für mich passt.
- Als Freelancer möchte ich nach dem Klick aus Gulp direkt im SOLCOM-Bewerbungsflow (Minggo) landen können — durch denselben „Bewerben"-Button, den auch direkte Besucher der Detail-Seite sehen.
- Als Gulp-Crawler möchte ich beim Polling der alten ASP-URL einen Redirect auf die neue Drupal-URL erhalten, damit ich meinen Crawler-State automatisch aktualisieren kann.
- Als Site-Operator möchte ich, dass eine Salesforce-Pflege-Änderung (Channel-Flag, Status) spätestens innerhalb des Migrate-Intervalls plus einer Stunde im Gulp-Feed sichtbar ist, ohne Cache manuell zu flushen.
- Als Site-Operator möchte ich, dass die Feed-URL nicht von Suchmaschinen indiziert wird, damit Duplicate-Content gegenüber den Projektpartnerportal-Listings vermieden wird.
- Als Backend-Entwickler möchte ich, dass die Gulp-Feed-Logik in das bestehende Sub-Modul
solcom_project_offer_feedsintegriert wird, damit ich denselben Builder + Cache-Strategie nutze wie für die XML-Vendor-Feeds aus F-340. - Als QA-Engineer möchte ich gegen die Live-Route HTTP-Requests absetzen können und prüfen, dass Content-Type, X-Robots-Tag und Cache-Tag-Headers korrekt gesetzt werden.
Implementation Decisions
Architektur-Map — Erweiterung, kein neues Modul
F-360 erweitert das bestehende Sub-Modul solcom_project_offer/modules/solcom_project_offer_feeds/ (siehe F-340-PRD). Konkret bedeutet das fünf Deltas, alle Vendor-spezifisch additiv:
- Routing: zusätzlicher Eintrag
solcom_project_offer_feeds.gulpmitpath: /feeds/gulp.htmlinsolcom_project_offer_feeds.routing.yml. - Controller: zusätzliche Methode
VendorFeedController::gulp(): CacheableResponse. Eine-zu-eins-Spiegel zufreelancermap()undfreelance(), mitContent-Type: text/html; charset=utf-8in der Response. - Builder: zusätzliche Methode
VendorFeedBuilder::buildGulp(array $offers): array. Filter-Logik (getEligibleOffers('field_sf_channel_gulp')) ist bereits abgedeckt — der Builder wurde in F-340 explizit kanal-parametrisiert gebaut. - Twig-Template: neues
solcom-feed-gulp.html.twigmit<!doctype html>-Hülle und einer<table>(oder<ul>) mit einem<a>pro Offer. - Theme-Hook:
solcom_feed_gulpinsolcom_project_offer_feeds.moduleregistrieren, Variablepositionsanalog zu den XML-Templates.
VendorContext und das gesamte UTM-/Suffix-/Minggo-Apparat werden für Gulp nicht gebraucht. Die Builder-Methode buildGulp() ruft buildFeed() entweder mit einem minimalen Default-VendorContext()-Objekt, oder es entsteht ein dedizierter dünner Render-Pfad ohne VendorContext. Entscheidung zugunsten von Konsistenz: minimaler VendorContext mit Default-Werten (jobNumberSuffix='', utmParams=[], emitUtmUrl=FALSE). Renderpfad bleibt identisch.
HTML-Schema
Eine simple, semantisch klare Hülle. Keine Schema.org-Annotationen, kein CSS, keine externe Dependency. Ziel ist parserfreundlich für Gulp und direkt lesbar im Browser, ohne aktiv von Crawlern indiziert zu werden.
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>SOLCOM Projektliste — Gulp</title>
</head>
<body>
<ul>
<li><a href="https://www.solcom.de/fuer-freiberufler/projektliste/{slug}">{job_title} (m/w/d)</a></li>
…
</ul>
</body>
</html>
Job-Title-Suffix: der Vendor erwartet im sichtbaren Linktext den Marker (m/w/d). Das Suffix wird in der Builder-Methode an den field_sf_job_title-Wert angehängt; nicht in Salesforce gepflegt (Drift-Vermeidung). Wenn der Titel bereits (m/w/d) oder eine Variante enthält, wird nicht doppelt angehängt — Detail-Spec im Implementations-Plan.
Quelle & Filter
Identische Filter-Achse wie F-340, anderer Channel-Feld-Name:
| Feld | Bedingung | Source-of-Truth |
|---|---|---|
type | = 'project_offer' | Drupal-Bundle |
status | = 1 | Drupal-Publish-Flag |
field_sf_pub_status | = 'Online' | SF: SC_PublicationStatus__c |
field_sf_channel_gulp | = TRUE | SF: SC_Publishing_Channel_Gulp__c |
field_sf_publish_start | ≤ NOW() | SF: hohr__Publishing_start_date__c |
field_sf_publish_end | ≥ NOW() oder leer | SF: hohr__Publishing_end_date_time__c |
Die Filter-Methode VendorFeedBuilder::getEligibleOffers() ist bereits parametrisiert für den Channel-Feld-Namen und wird unverändert für Gulp aufgerufen.
URL- & Cache-Strategie
- Kanonischer Pfad:
/feeds/gulp.html. Analog zur Pattern-Spiegelung der XML-Feeds:/feeds/<vendor>.<ext>. - Detail-URL im Link:
node.toUrl('canonical', ['absolute' => TRUE])— folgt der ADR-0032-IA/fuer-freiberufler/projektliste/<slug>. - Cache-Strategie: Drupal-Page-Cache aktiviert.
CacheableResponsemit Cache-Tagsnode_list:project_offer+ einzelnenode:<nid>-Tags.max-age = 3600als Sicherheitsnetz — identisch zu F-340. - SEO: Response-Header
X-Robots-Tag: noindex, nofollow. Kein robots.txt-Eintrag. - Auth: keine. Anonymous-pollable.
Legacy-Vendor-URL
Heutige Gulp-Anzeigen verweisen auf URLs der Form https://www.solcom.de/asp/robots/detail.aspx?mode=GULP&id=<sf-id>&utm_source=gulp.de&utm_medium=social&utm_campaign=Jobposting. Diese URLs zeigen heute auf das ASP-System und müssen nach Cutover funktionieren, sonst brechen aktive Vendor-Anzeigen ihre Klick-Strecke.
Architektonisch ist das ein anderes Problem als der Feed-Pfad-Redirect aus F-340: hier ist die SF-ID Query-Parameter, nicht Teil des Pfads. Lösung: ein dedizierter Drupal-Route /asp/robots/detail.aspx mit Controller-Methode, die per id-Query-Param einen Node-Lookup über field_sf_id durchführt und mit 301 auf die kanonische Drupal-Detail-URL umleitet. Wenn kein Node gefunden wird: 404.
Der bestehende LegacyRedirectSubscriber (statisches Path-Mapping) wird nicht erweitert — er bleibt für die in F-340 etablierte Konvention reserviert. Der parametrisierte Lookup gehört in eine eigene Controller-Klasse LegacyDetailRedirectController.
Anbindungs-Reihenfolge
- Routing- und Controller-Methode anlegen.
- Builder-Methode
buildGulp()+ Theme-Hook + Twig-Template. - Kernel-Test der Builder-Methode.
- Functional-Test der Route.
- Legacy-Detail-URL-Controller + Test (separate Slice, kann parallel laufen).
Testing Decisions
Tests prüfen externes Verhalten, nicht Implementations-Details. Fixture-Nodes setzen verschiedene Kombinationen aus status, field_sf_pub_status, field_sf_channel_gulp, Publish-Window. Assertions prüfen das gerenderte HTML als Black-Box, nicht Service-Internas.
Pflicht-Tests
VendorFeedBuilderTest::testBuildGulp()(Kernel). Drei bis fünf Fixture-Nodes. Assertion: das gerenderte HTML enthält genau ein<a>-Element pro publizierbarem Offer, jedes mit korrektemhref(absolute Drupal-Detail-URL) und korrektem Linktext (Jobtitel inklusive(m/w/d)).VendorFeedRouteTest::testGulpRoute()(Functional). HTTP-GET gegen/feeds/gulp.html. Assertions: HTTP-200,Content-Type: text/html; charset=utf-8,X-Robots-Tag: noindex, nofollow, Cache-Tag-Headers enthaltennode_list:project_offer, Response-Body enthält die erwarteten Detail-URLs.LegacyDetailRedirectTest(Functional). HTTP-GET ohne Follow gegen/asp/robots/detail.aspx?mode=GULP&id=<fixture-sf-id>. Assertions: HTTP-301,Locationzeigt auf die kanonische Drupal-Detail-URL. Zweiter Case: unbekannte SF-ID → HTTP-404.
Prior Art
VendorFeedBuilderTest und VendorFeedRouteTest aus F-340 (Schwester-Tests im selben Sub-Modul) sind die direkte Vorlage — Test-Klassen werden um Gulp-Methoden erweitert, nicht neu angelegt.
Out of Scope
- UTM-Tracking in den
<a>-Links — Drupal liefert kanonische URLs ohne Tracking-Params. Wenn Gulp UTM-Decoration braucht, dekoriert Gulp clientseitig vor dem Outbound-Klick. - Multiposting-Distributor-Pattern (VONQ, GOhiring, Broadbean) — verworfen im Refinement, weil Gulp ein Pull-Vendor ist und kein Push-Distributor. Wenn künftig ein realer Multiposting-Distributor angebunden wird, eigene Story und eigene Architektur-Entscheidung.
- Pagination des Feeds — der Vendor erwartet den Vollbestand in einem Response. Bei > 1000 Offers gleichzeitig zu überdenken; aktuell < 300 (gleicher Performance-Annahme-Korridor wie F-340).
- Editorial-Drupal-UI zum Ausschluss einzelner Offers aus dem Gulp-Feed — Pflege ausschließlich in Salesforce über
field_sf_channel_gulp. - Apply-URL im Feed — der Feed listet nur Detail-Links. Bewerben passiert auf der Drupal-Detail-Seite (F-330 / Minggo).
- Schema.org-Annotationen in den Listenseiten — out of scope, weil
noindex. - i18n — der Feed liefert ausschließlich Deutsch.
Further Notes
Abhängigkeiten
- F-290 / P2503-108 (project_offer-Sync) produktiv — ohne Migrate-Sync gibt es keine Project-Offer-Nodes.
- F-340 / P2503-112 (XML-Vendor-Feeds) produktiv — etabliert das Sub-Modul, Builder, Filter und Cache-Strategie. F-360 ist dünne Erweiterung.
- Pathauto-Pattern auf den IA-Pfad
/fuer-freiberufler/projektliste/[node:title]umgestellt (Vorbedingung wie bei F-340). - Drupal-Cutover-Plan aktiv:
www.solcom.dezeigt am Go-Live auf Drupal. - Salesforce-Pflege des Channel-Flags
SC_Publishing_Channel_Gulp__cist im Vertriebs-Workflow verankert.
Offene Fragen
- Format der Linkliste:
<ul>-Liste oder<table>mit Spalten (Titel / Start / Ort)? Erste Annahme:<ul>mit nur Linktext — analog zum User-Screenshot. Wird beim Gulp-Vendor-Smoketest vor Cutover finalisiert. - Job-Title-Suffix-Logik: immer
(m/w/d)anhängen oder existierende Variante (m/w/d,m/w/x) respektieren? Implementation-Plan-Detail. - Sollen Gulp-Vendor-Anzeigen mit Backlinks auf
/asp/robots/detail.aspx?id=<id>per Catch-all-Redirect funktionieren — oder akzeptiert Stakeholder, dass alte Backlinks ablaufen? Vorschlag in diesem PRD: dedizierter Controller-Redirect (siehe Implementation Decisions).
Pattern-Spiegelung zu F-340 und ADR-0035
F-340 spiegelt das Personio-Inbound-XML-Pattern aus ADR-0035 auf den Outbound-Pfad. F-360 spiegelt F-340 auf einen HTML-Output-Vendor. In allen drei Fällen ist die Architektur dieselbe: Drupal liefert anonym-pollbare, gefilterte, gecachte Listen über strukturiert-getypte Routen. Die Symmetrie ist absichtlich und reduziert kognitiven Overhead beim Onboarding neuer Vendoren.
Forcing-Function
Der Drupal-Cutover am 08.07.2026 ist hart. F-360 muss spätestens drei Wochen davor produktiv-testbar sein, damit Gulp einen ersten Sample-Crawl machen kann.