PRD · F-360 · P2503-113

Gulp HTML-Feed für project_offer

Drupal publiziert unter /feeds/gulp.html eine simple HTML-Linkliste aller für den Kanal Gulp freigegebenen Projektangebote — analog zu F-340 (XML-Feeds für Freelancermap und freelance.de), nur in HTML statt XML.

APPROVED P2503-113 Erstellt 2026-05-21 · Target Release 2026-07-08
TL;DR. Pattern-Spiegel zu F-340. Drupal liefert unter /feeds/gulp.html eine HTML-Linkliste, deren Einträge die kanonischen Drupal-Detail-URLs der Projektangebote sind (/fuer-freiberufler/projektliste/<slug>, siehe ADR-0032). Filterung pro Vendor über das bereits existierende Boolean-Feld field_sf_channel_gulp. Bewerben passiert auf der Drupal-Detailseite über den vorhandenen Minggo-Flow (F-330) — der Feed selbst trägt keine Apply-URL. Erweiterung des bestehenden Sub-Moduls solcom_project_offer_feeds; kein neues Modul.

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. Als Backend-Entwickler möchte ich, dass die Gulp-Feed-Logik in das bestehende Sub-Modul solcom_project_offer_feeds integriert wird, damit ich denselben Builder + Cache-Strategie nutze wie für die XML-Vendor-Feeds aus F-340.
  10. 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.gulp mit path: /feeds/gulp.html in solcom_project_offer_feeds.routing.yml.
  • Controller: zusätzliche Methode VendorFeedController::gulp(): CacheableResponse. Eine-zu-eins-Spiegel zu freelancermap() und freelance(), mit Content-Type: text/html; charset=utf-8 in 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.twig mit <!doctype html>-Hülle und einer <table> (oder <ul>) mit einem <a> pro Offer.
  • Theme-Hook: solcom_feed_gulp in solcom_project_offer_feeds.module registrieren, Variable positions analog 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:

FeldBedingungSource-of-Truth
type= 'project_offer'Drupal-Bundle
status= 1Drupal-Publish-Flag
field_sf_pub_status= 'Online'SF: SC_PublicationStatus__c
field_sf_channel_gulp= TRUESF: SC_Publishing_Channel_Gulp__c
field_sf_publish_start≤ NOW()SF: hohr__Publishing_start_date__c
field_sf_publish_end≥ NOW() oder leerSF: 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. CacheableResponse mit Cache-Tags node_list:project_offer + einzelne node:<nid>-Tags. max-age = 3600 als 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

  1. Routing- und Controller-Methode anlegen.
  2. Builder-Methode buildGulp() + Theme-Hook + Twig-Template.
  3. Kernel-Test der Builder-Methode.
  4. Functional-Test der Route.
  5. 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

  1. VendorFeedBuilderTest::testBuildGulp() (Kernel). Drei bis fünf Fixture-Nodes. Assertion: das gerenderte HTML enthält genau ein <a>-Element pro publizierbarem Offer, jedes mit korrektem href (absolute Drupal-Detail-URL) und korrektem Linktext (Jobtitel inklusive (m/w/d)).
  2. 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 enthalten node_list:project_offer, Response-Body enthält die erwarteten Detail-URLs.
  3. LegacyDetailRedirectTest (Functional). HTTP-GET ohne Follow gegen /asp/robots/detail.aspx?mode=GULP&id=<fixture-sf-id>. Assertions: HTTP-301, Location zeigt 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.de zeigt am Go-Live auf Drupal.
  • Salesforce-Pflege des Channel-Flags SC_Publishing_Channel_Gulp__c ist 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.