Implementation Plan · F-360 · P2503-113

Gulp HTML-Feed für project_offer

ACTIVE P2503-113 Erstellt 2026-05-21 · Target Release 2026-07-08
TL;DR. Zwei vertikale Slices. Slice 1 ergänzt das in F-340 etablierte Sub-Modul solcom_project_offer_feeds additiv um Route /feeds/gulp.html, Controller-Methode, Builder-Methode buildGulp(), Theme-Hook solcom_feed_gulp und Twig-Template. Slice 2 baut einen eigenständigen LegacyDetailRedirectController für die parametrisierten Gulp-Backlinks unter /asp/robots/detail.aspx?mode=GULP&id=<sf-id>. Beide Slices sind parallel startbar und enden mit grünen Pflicht-Tests im Sub-Modul.

Vertikale Slices

Aufteilung gefolgt der PRD-Anbindungs-Reihenfolge (Schritt 5 explizit als „separate Slice, kann parallel laufen" markiert). Beide Slices sind AFK — der ralph-Loop kann sie ohne HITL durchziehen.

SliceStakeholder-TitelJira-KeyStatusBlocked by
1Gulp-Linkliste unter /feeds/gulp.html liveP2503-169IDEE
2Alte Gulp-Anzeigen-Backlinks landen nach Cutover auf der neuen DetailseiteP2503-170IDEE

Slice 1 — Gulp-Linkliste live

Stakeholder-Outcome (siehe Jira-AKs): Vertrieb sieht jedes für Gulp freigegebene Projektangebot automatisch in /feeds/gulp.html ohne Vendor-Pflege; Vendor-Klick führt auf /fuer-freiberufler/projektliste/<slug>.

Tech-Deltas im Sub-Modul solcom_project_offer/modules/solcom_project_offer_feeds/

  • Routing: Eintrag solcom_project_offer_feeds.gulp mit path: /feeds/gulp.html in solcom_project_offer_feeds.routing.yml. Controller VendorFeedController::gulp.
  • Controller-Methode: VendorFeedController::gulp(): CacheableResponse — eine-zu-eins-Spiegel zu freelancermap()/freelance(), mit Content-Type: text/html; charset=utf-8 + X-Robots-Tag: noindex, nofollow + Cache-Tag node_list:project_offer + einzelne node:<nid>-Tags + max-age=3600.
  • Builder-Methode: VendorFeedBuilder::buildGulp(array $offers, VendorContext $ctx): array. Ruft getEligibleOffers('field_sf_channel_gulp') auf (die bereits kanal-parametrisierte F-340-Filter-Methode). Render-Pfad: minimaler VendorContext mit Default-Werten (jobNumberSuffix='', utmParams=[], emitUtmUrl=FALSE).
  • Twig-Template: templates/solcom-feed-gulp.html.twig mit <!doctype html>-Hülle und <ul> von <a>-Tags. Linktext = {{ job_title }} (m/w/d). href = {{ node.toUrl('canonical', {'absolute': true}) }}.
  • Theme-Hook: solcom_feed_gulp in solcom_project_offer_feeds.module registrieren, Variable positions analog zu den XML-Templates.

Job-Title-Suffix-Logik

Builder-Methode hängt (m/w/d) an, außer der Roh-Titel enthält bereits eine Marker-Variante. Regex (Implementations-Detail): /\((m\s*[\/|]\s*w\s*[\/|]\s*[dxa])\)/i — Match → Suffix weglassen, kein Match → " (m/w/d)" anhängen. Suffix-Pflege in Drupal, nicht in Salesforce (Drift-Vermeidung).

Caching-Strategie

Drupal-Page-Cache aktiviert. CacheableResponse mit Cache-Tags node_list:project_offer + einzelne node:<nid>-Tags pro gerenderter Offer. max-age = 3600 als Sicherheitsnetz. Cache-Invalidation passiert automatisch über Drupal-Cache-Tag-Mechanik bei Migrate-Save eines Offers.

Pflicht-Tests

  • Kernel — VendorFeedBuilderTest::testBuildGulp(): 5–7 Fixture-Nodes mit verschiedenen Kombinationen aus status, field_sf_pub_status, field_sf_channel_gulp, field_sf_publish_start, field_sf_publish_end. Assert: gerendertes HTML enthält genau ein <a> pro publizierbarem Offer, korrekter href (absolute Drupal-URL), korrekter Linktext mit Suffix.
  • Functional — VendorFeedRouteTest::testGulpRoute(): HTTP-GET gegen /feeds/gulp.html. Asserts: HTTP-200, Content-Type: text/html; charset=utf-8, X-Robots-Tag: noindex, nofollow, Cache-Tag-Header enthält node_list:project_offer, Response-Body enthält erwartete Detail-URLs.

AK-Mapping (Stakeholder-AKs aus Jira)

Slice 1 deckt AKs 1, 2, 3, 4, 5, 6, 7 der Sub-Task-Description (Stakeholder-Formulierung). Die Tech-AKs aus dem Story-Hub (AK 9 „Code lebt im Sub-Modul …" und AK 10 „Pflicht-Tests existieren") werden hier durch die Tech-Deltas + Pflicht-Tests-Sektion abgedeckt.

Slice 2 — Legacy-Backlinks 301

Stakeholder-Outcome (siehe Jira-AKs): Aufruf einer alten Gulp-Backlink-URL https://www.solcom.de/asp/robots/detail.aspx?mode=GULP&id=<sf-id> antwortet mit 301 auf die kanonische Drupal-Detail-URL des passenden Projektangebots. Kein Klick-Verlust bei aktiven Vendor-Anzeigen.

Tech-Deltas

  • Routing: Eintrag solcom_project_offer_feeds.legacy_detail mit path: /asp/robots/detail.aspx in solcom_project_offer_feeds.routing.yml. Controller LegacyDetailRedirectController::redirect. Wichtig: der bestehende LegacyRedirectSubscriber (statisches Path-Mapping aus F-340) wird nicht erweitert — dort gehört nur das Path-zu-Path-Mapping rein, nicht parametrisierter Query-Lookup.
  • Controller: LegacyDetailRedirectController::redirect(Request $request) in src/Controller/LegacyDetailRedirectController.php. Liest id-Query-Param, sucht Node via entityTypeManager->getStorage('node')->loadByProperties(['type' => 'project_offer', 'field_sf_id' => $sfId]). Treffer: HTTP-301 auf $node->toUrl('canonical', ['absolute' => TRUE]). Kein Treffer: throw new NotFoundHttpException() → HTTP-404.
  • Mode-Param: der mode=GULP-Query-Param wird nicht ausgewertet — falls in Zukunft weitere Vendor-spezifische Lookups kommen, kann der Controller den Mode lesen, aber für F-360 ist nur der id-Param relevant.
  • UTM-Params durchreichen? Nein — kanonische Drupal-URL als Redirect-Ziel ohne Query-String. Tracking-Pflege passiert vendor-seitig.

Pflicht-Tests

  • Functional — LegacyDetailRedirectTest: Zwei Cases. (a) HTTP-GET ohne Follow gegen /asp/robots/detail.aspx?mode=GULP&id=<fixture-sf-id> → Assert HTTP-301, Location zeigt auf kanonische Drupal-Detail-URL des Fixture-Nodes. (b) HTTP-GET gegen /asp/robots/detail.aspx?mode=GULP&id=unbekannte-sf-id → Assert HTTP-404.

AK-Mapping (Stakeholder-AKs aus Jira)

Slice 2 deckt AKs 1, 2, 3 der Sub-Task-Description (Redirect-Treffer, Nicht-Treffer-404, dauerhaft-aktiv).

Anbindungs-Reihenfolge

Innerhalb Slice 1 sequenziell, Slice 2 läuft parallel (eigener Controller, kein Konflikt mit Slice-1-Code):

  1. Routing + Controller-Methode gulp() → Smoketest gegen leere Liste.
  2. Builder-Methode buildGulp() + Theme-Hook + Twig-Template.
  3. Kernel-Test der Builder-Methode.
  4. Functional-Test der Route inkl. Cache-Tag- und Header-Asserts.
  5. Slice 2 (parallel ab Schritt 1): Routing + LegacyDetailRedirectController + Functional-Test.

Offene Fragen (aus PRD)

  • Listentyp <ul> vs. <table>: Default-Vorschlag <ul>, finalisierung beim Gulp-Vendor-Smoketest vor Cutover. Implementation startet mit <ul>.
  • Suffix-Logik: oben fixiert — Marker-Variante respektieren, sonst " (m/w/d)" anhängen.
  • UTM-Akzeptanz Stakeholder: Drupal-Feed liefert kanonische Links ohne UTM. Wenn Gulp UTM-Decoration braucht, dekoriert Gulp vor dem Outbound-Klick. Position offen, aber Implementation entscheidet sich für „keine UTM im Feed" (Single Source of Truth: Drupal-canonical-URL).

Abhängigkeiten

  • F-340 / P2503-112 produktiv — Sub-Modul + VendorFeedBuilder + Filter-Logik etabliert.
  • F-290 / P2503-108 produktiv — Migrate-Sync liefert Project-Offer-Nodes.
  • Pathauto-Pattern auf /fuer-freiberufler/projektliste/[node:title] (Vorbedingung wie bei F-340, gemäß ADR-0032).
  • Salesforce-Feld SC_Publishing_Channel_Gulp__c existiert und ist im Vertriebs-Workflow verankert.