project_offer-XML-Feeds — Freelancermap, freelance.de
draft
freelance.xml und freelancermap.xml ab dem Cutover (≤ 08.07.2026) selbst. Quelle ist node.type.project_offer (gespiegelt aus Salesforce, F-290/P2503-108). Auslieferung pro Vendor erfolgt über einen eigenen Drupal-Route + Twig-XML-Template; Filterung ausschließlich per field_sf_channel_freelance/_freelancermap + Status + Veröffentlichungs-Fenster — pflege erfolgt zu 100 % in Salesforce, ohne Drupal-Editorial. Code lebt im neuen Sub-Modul solcom_project_offer_feeds unter solcom_project_offer.
Problem Statement
Vertriebsmitarbeiterinnen und -mitarbeiter müssen ihre offenen Projektanfragen heute über zwei Wege gleichzeitig pflegen: einmal in Salesforce (Lead-System für die Auftragsausschreibung) und einmal manuell auf den Freelancer-Plattformen Freelancermap und freelance.de (Reichweite). Die heutige Brücke ist ein altes ASP-System unter www.solcom.de/asp/robots/*.xml, das beim Drupal-Cutover (≤ 08.07.2026) abgeschaltet wird. Ohne Drupal-seitigen Ersatz fällt die automatisierte Reichweite auf den beiden Plattformen ersatzlos weg und der manuelle Pflegeaufwand vervielfacht sich.
Komplikation: Der vorhandene Story-Hub und die Wiki-Page haben den Outbound-Feed historisch fälschlich an node.type.vacancy (Personio-Stellen für die SOLCOM-Karriereseite) gebunden. Vacancies und Project-Offers sind aber zwei verschiedene Bundles für zwei verschiedene Zielgruppen — Festanstellungen vs. Freelance-Aufträge. Marketplaces wie Freelancermap suchen Freelance-Aufträge, nicht Festanstellungen.
Solution
Drupal liefert nach dem Cutover unter /feeds/freelancermap.xml und /feeds/freelance.xml ein XML-Dokument im Format <openPositions> aus — kompatibel zur heutigen ASP-Spec, vendor-spezifisch parametrisiert. Inhalt sind alle publizierten Project-Offers, deren SF-Channel-Flag für den jeweiligen Vendor gesetzt ist und deren Veröffentlichungs-Fenster aktiv ist.
Die alten ASP-Pfade /asp/robots/freelance.xml und /asp/robots/freelancermap.xml liefern einen 301-Redirect auf die kanonischen Drupal-Pfade, sodass Vendoren, die ihre Konfiguration nicht sofort umstellen, keinen Tag Ausfall erleben. Die Vendoren erleben den Cutover idealerweise nur als wenige zusätzliche 301-Hops, dann pollen sie automatisch die neue URL.
Sämtliche Editorial-Pflege (Welcher Offer geht an welchen Vendor? Wann publiziert? Welche Beschreibung?) passiert ausschließlich in Salesforce und fließt über den bestehenden BH4SF→Drupal-Migrate-Sync ins project_offer-Bundle. Drupal-seitig gibt es kein neues Editorial-Feld und keine neue UI.
User Stories
- Als Vertriebsmitarbeiter möchte ich meine offene Projektanfrage nur in Salesforce pflegen, damit Freelancermap und freelance.de sie automatisch übernehmen, ohne dass ich auf der jeweiligen Plattform nachpflegen muss.
- Als Vertriebsmitarbeiter möchte ich pro Projektanfrage entscheiden können, ob sie auf Freelancermap, freelance.de oder beiden Plattformen erscheint — indem ich in Salesforce die entsprechenden Channel-Flags setze.
- Als Vertriebsmitarbeiter möchte ich, dass ein Offer mit Veröffentlichungs-Endedatum in der Vergangenheit automatisch aus beiden Feeds verschwindet, damit kein Freelancer auf eine ablaufene Anfrage reagiert.
- Als Vertriebsmitarbeiter möchte ich, dass ein Offer, dessen
SC_PublicationStatus__cin Salesforce von „Online" auf einen anderen Status wechselt, beim nächsten Vendor-Poll nicht mehr im Feed erscheint. - Als Freelancer möchte ich auf Freelancermap eine SOLCOM-Projektanfrage anklicken und auf einer aussagekräftigen Detail-Seite landen, damit ich entscheiden kann, ob das Projekt für mich passt.
- Als Freelancer möchte ich von der Vendor-Plattform aus direkt in den SOLCOM-Bewerbungsflow (Minggo) springen, damit ich nicht erst einen Anruf oder eine Mail platzieren muss.
- Als Freelancermap-/freelance.de-Vendor-System möchte ich eine wohlgeformte XML mit
<openPositions><position>…-Struktur empfangen, sodass mein bestehender Parser ohne Anpassung weiterläuft. - Als Freelancermap-/freelance.de-Vendor-System möchte ich beim Polling der alten ASP-URL einen
301-Redirect auf die neue URL erhalten, damit mein Crawler-State sich automatisch aktualisiert. - Als Site-Operator möchte ich, dass der Feed auch unter Last (50–300 Offers, mehrere Vendor-Polls pro Stunde) in < 100 ms warm ausgeliefert wird, sodass Vendor-Crawler nicht in Timeouts laufen.
- Als Site-Operator möchte ich, dass eine Salesforce-Pflege-Änderung spätestens innerhalb des Migrate-Intervalls plus einer Stunde im Vendor-Feed sichtbar ist, ohne dass ich Drupal-Caches manuell flushen muss.
- Als Site-Operator möchte ich, dass die Feed-URLs nicht von Suchmaschinen indiziert werden, damit Duplicate-Content gegenüber den project_offer-Detailseiten vermieden wird.
- Als Backend-Entwickler möchte ich die Feed-Logik in einem Sub-Modul kapseln, das ich unabhängig vom Haupt-Modul deaktivieren und in Tests isoliert ausführen kann.
- Als Backend-Entwickler möchte ich, dass vendor-spezifische Konstanten (Suffix, UTM-Parameter, Minggo-Workspace-ID) in einer Drupal-Config-Datei stehen, damit ich sie ohne Code-Deploy ändern kann.
- Als Backend-Entwickler möchte ich, dass die Apply-URL über die SF-ID des Offers automatisch in den Minggo-Deep-Link zusammengesetzt wird, ohne dass dieser Wert pro Offer manuell gepflegt werden muss.
- Als Backend-Entwickler möchte ich, dass die Description-URL aus Drupals kanonischer Route über
entity.toUrl()zusammengebaut wird, damit künftige Pathauto-Änderungen den Feed nicht brechen. - Als QA-Engineer möchte ich den Filter-Code des Builders mit einer Reihe von Fixture-Nodes (verschiedene
status/pub_status/channel/publish-window-Kombinationen) testen, ohne einen vollständigen Salesforce-Sync zu fahren. - Als QA-Engineer möchte ich gegen die Live-Routes HTTP-Requests absetzen können und prüfen, dass Content-Type, X-Robots-Tag und Cache-Tag-Headers korrekt gesetzt werden.
- Als QA-Engineer möchte ich prüfen, dass ein Request gegen
/asp/robots/freelance.xmlmit einem301-Status und einem Location-Header auf/feeds/freelance.xmlantwortet.
Implementation Decisions
Architektur-Map
Neues Sub-Modul solcom_project_offer_feeds unter dem bestehenden Modul solcom_project_offer — Pattern-Parität zu solcom_project_offer_alert. Das Sub-Modul kann unabhängig aktiviert/deaktiviert werden; die Migrate-Schicht im Haupt-Modul bleibt unberührt.
Das Sub-Modul enthält drei Code-Concerns plus zwei Twig-Templates plus eine Config-Datei:
- Deep module —
VendorFeedBuilder(Service): testbare Kern-Logik. Zwei Methoden:getEligibleOffers(string $channelField): array<NodeInterface>— kapselt die Filter-Logik (Status, Pub-Status, Channel-Flag, Veröffentlichungs-Fenster).buildXml(array $offers, VendorContext $ctx): array— gibt ein Drupal-Render-Array zurück.VendorContextist ein Value-Object mit den vendor-spezifischen Konstanten.
- Shallow module —
VendorFeedController: dünner HTTP-Adapter. Eine Methode pro Vendor (freelancermap(),freelance()) — ruft Builder, setzt Response-Headers, packt das Render-Array in eineCacheableResponse. - Side module —
LegacyRedirectSubscriber:kernel.request-EventSubscriber. Liefert301-Responses für die zwei Legacy-Pfade. Eigene Klasse, eigener Test. - Twig-Templates:
solcom-feed-freelancermap.xml.twig,solcom-feed-freelance.xml.twig. Beide rendern die<openPositions>-Struktur; die Vendor-Differenzen (Suffix, UTM-Doppel-URL) sind ausschließlich Template-Logik. - Config-Extension: neue Sektion
feeds:insolcom_project_offer.settings.ymlmitminggo_workspace_id,freelance.job_number_suffix,freelance.utm,freelancermap.utm.
Quelle & Filter
Quelle ist node.type.project_offer — explizit nicht vacancy. Frontend-Detail-Seite und Listing sind Teil von F-290/P2503-108 und müssen vor F-340 produktiv sein.
Per Vendor wird folgende Filterbedingung angewendet (Drupal-Entity-Query mit Subkonditionen):
| Feld | Bedingung | Source-of-Truth |
|---|---|---|
status | = 1 | Drupal-Standard-Publish-Flag |
field_sf_pub_status | = "Online" | SF: SC_PublicationStatus__c |
field_sf_channel_freelancermap bzw. _freelance | = TRUE | SF: SC_Publishing_Channel_*__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 |
Sämtliche Filterung passiert in VendorFeedBuilder::getEligibleOffers() — der Controller weiß nicht, wie gefiltert wird.
URL- & Cache-Strategie
- Kanonische Pfade:
/feeds/freelancermap.xml,/feeds/freelance.xml. - Legacy-Pfade:
/asp/robots/freelancermap.xml,/asp/robots/freelance.xmlliefern301-Redirect auf die kanonische URL. Handled vomLegacyRedirectSubscriber. - Cache-Strategie: Drupal-Page-Cache aktiviert.
CacheableResponsemit Cache-Tagsnode_list:project_offer+ einzelnenode:<nid>-Tags der ausgespielten Offers.max-age = 3600als Sicherheitsnetz für ausgefallene Tag-Invalidation. - SEO: Response-Header
X-Robots-Tag: noindex, nofollow. Kein robots.txt-Eintrag. - Auth: keine. Endpoints sind öffentlich anonym, wie heute das ASP-System.
XML-Schema (gespiegelt aus Live-Samples in docs/context/)
Root <openPositions> → Wiederholungs-Element <position> mit 11 CDATA-Kindern. Alle Text-Felder werden mit <![CDATA[ … ]]> umschlossen.
<openPositions>
<position>
<jobNumber>{sf_id_oder_jobnr}{vendor_suffix?}</jobNumber>
<jobTitle><![CDATA[ {title} ]]></jobTitle>
<jobDescription><![CDATA[ {long_description_html} ]]></jobDescription>
<jobRequirements><![CDATA[ {requirements_html} ]]></jobRequirements>
<jobAddition><![CDATA[ {utilisation_oder_call_to_action} ]]></jobAddition>
<jobStart><![CDATA[ {dd.mm.yyyy aus field_sf_estimated_start} ]]></jobStart>
<jobDuration><![CDATA[ {freier String aus field_sf_duration} ]]></jobDuration>
<jobPlace><![CDATA[ {field_sf_city|fallback "keine Angabe"} ]]></jobPlace>
<jobPlaceISOCode><![CDATA[ {field_geo:country_iso?} ]]></jobPlaceISOCode>
<jobDescriptionURL><![CDATA[ {entity.toUrl(absolute=true)} ]]>
{nur freelance: <![CDATA[ {entity.toUrl} + ?utm_source=…&utm_medium=…&utm_campaign=… ]]>}
</jobDescriptionURL>
<jobApplicationURL><![CDATA[ https://solcom.minggo.io/{minggo_workspace_id}/apply/{field_sf_id} ]]></jobApplicationURL>
</position>
…
</openPositions>
Pflicht-Feldmapping zwischen project_offer-Feldern und XML-Tags wird im Implementations-Plan finalisiert (Field-by-Field-Spec).
Vendor-Config
Drupal-Config in solcom_project_offer.settings.yml, Sektion feeds::
feeds:
minggo_workspace_id: 'da891d49-443e-4269-a378-e5fe7d2d4649'
freelance:
job_number_suffix: '-298'
utm:
utm_source: 'freelance.de'
utm_medium: 'social'
utm_campaign: 'Jobposting'
freelancermap:
utm: {}
Die UTM-Werte für freelance werden bei Implementierung mit dem Vendor abgestimmt (heutige Werte utm_source=freelancercheck.de sind Drift im Legacy-ASP-Feed und werden nicht übernommen — siehe Dependencies).
Apply-URL-Bildung
Pro Position wird der Minggo-Deep-Link aus zwei Werten zusammengesetzt:
minggo_workspace_idaus Drupal-Config (konstant für SOLCOM).field_sf_idam Node (= Salesforce-Record-ID des Offers).
Der Twig baut: https://solcom.minggo.io/{minggo_workspace_id}/apply/{field_sf_id}. Wenn field_sf_id leer ist (Drift-Fall), wird die Position aus dem Feed ausgespielt ohne <jobApplicationURL>-Tag — der Feed bleibt wohlgeformt, der Vendor zeigt den Klick-Button passend nicht an.
Description-URL-Bildung
Drupals kanonische Detail-URL pro Node via node.toUrl('canonical', ['absolute' => TRUE])->toString(). Damit ist die Description-URL automatisch synchron mit dem aktuell gültigen Pathauto-Pattern und überlebt zukünftige IA-Umzüge transparent.
Für freelance-Vendor wird zusätzlich ein zweiter CDATA-Block innerhalb desselben <jobDescriptionURL>-Tags ausgespielt, der die UTM-Parameter trägt — Format gespiegelt aus dem Live-Sample.
Testing Decisions
Tests prüfen externes Verhalten, nicht Implementations-Details. Konkret heißt das: Filter-Tests setzen Fixture-Nodes und prüfen das Ergebnis (welche Nodes kommen durch), nicht den Query-Aufbau. Route-Tests setzen HTTP-Requests ab und prüfen Response-Status, Headers und das XML als Black-Box — nicht den Controller-Service-Wiring.
Pflicht-Tests
VendorFeedBuilderTest(Kernel). Drei bis fünf Fixture-Nodes mit unterschiedlichen Kombinationen ausstatus,field_sf_pub_status,field_sf_channel_*,field_sf_publish_start/_end. Assertion:getEligibleOffers('field_sf_channel_freelancermap')liefert exakt die erwartete Teilmenge. Tests pro Filter-Achse einen Positiv- und einen Negativ-Case.VendorFeedRouteTest(Functional). HTTP-GET gegen/feeds/freelancermap.xmlund/feeds/freelance.xml. Assertions: HTTP-200,Content-Type: application/xml,X-Robots-Tag: noindex, nofollow, Cache-Tag-Headers enthaltennode_list:project_offer, Response-Body ist valides XML mit<openPositions>-Root. Smoke-Assertion: Anzahl<position>-Elemente entspricht der Anzahl publizierter Fixture-Nodes mit Channel-Flag.LegacyRedirectSubscriberTest(Functional). HTTP-GET ohne Follow gegen/asp/robots/freelance.xmlund/asp/robots/freelancermap.xml. Assertions: HTTP-301,Location-Header zeigt auf den kanonischen/feeds/*-Pfad.
Prior Art
Pattern-Vorbild: solcom_project_offer_alert-Tests (Schwester-Sub-Modul). Migrate-Test-Vorbild für die Filter-Logik: solcom_vacancy/tests/ (Kernel-Tests gegen migrate_plus-Pipeline). Funktional-Test-Vorbild: solcom_editorial/tests/Functional/.
Optional (nice-to-have, nicht im DoD)
Schema-Snapshot-Test gegen die in docs/context/freelance.xml und docs/context/freelancermap.xml abgelegten Live-Samples — DOMDocument-basiert, prüft Tag-Namen + CDATA-Wrapping in der erwarteten Reihenfolge. Sinnvoll bei späterer Schema-Drift-Diskussion, nicht MVP-blockierend.
Out of Scope
- Gulp- und Randstad-Feeds — leben unter P2503-113 / F-360 in einer eigenen Story (eigene Vendor-Spec, eigene Config-Block, eigene Twig-Templates; Pattern wird hier etabliert und in F-360 nur wiederverwendet).
- Editorial-Drupal-UI für „dieses Offer aus dem Feed ausschließen" — Pflege passiert ausschließlich in Salesforce.
- Push-Mechanik (Webhook, SFTP-Upload) — die Vendoren pollen, wir liefern auf Anfrage. Wenn ein Vendor künftig Push verlangt, eigene Story.
- Reverse-Tracking der Bewerbungen, die über den Minggo-Link reinkommen — gehört in den Bewerbungs-Flow (F-330 / Minggo-Integration).
- i18n — der Feed liefert ausschließlich Deutsch, weil
project_offerheute DE-only ist. - Pagination des Feeds — die Vendoren erwarten den Vollbestand in einem Response, nicht Pages.
- Pathauto-Pattern-Update auf den neuen IA-Pfad
/fuer-freiberufler/projektliste/<slug>— gehört zu F-290/P2503-108 oder zu einer eigenen URL-Kanon-Story (siehe Dependencies). - ASP-Detail-URL-Redirect für Legacy-Vendor-Backlinks der Form
/asp/robots/detail.aspx?id=<id>— wird im Cutover-Plan adressiert, nicht in F-340 (siehe Dependencies). - Empty-Feed-Behavior — Default: wohlgeformte
<openPositions/>-Response mit Status 200. Kein 404, kein 503. Wenn der Vendor das anders erwartet, eigene Story.
Dependencies / offene Fragen
- F-290 / P2503-108 (project_offer-Sync) muss vor F-340 produktiv sein. Ohne Migrate-Sync gibt es keine project_offer-Nodes, also nichts zu publizieren.
- Pathauto-Pattern-Update:
pathauto.pattern.project_offer_content.ymlzeigt heute auf/projektpartnerportal/[node:title]; Memory + IA-Vorgabe (siehedocs/context/0002-sitemap.html) verlangen/fuer-freiberufler/projektliste/[node:title]. F-340 verwendetentity.toUrl()und folgt damit dem dann gültigen Pattern; aber für die XML-Description-URL muss der neue Pfad spätestens beim Cutover live sein. - Drupal-Cutover-Plan:
www.solcom.demuss am Go-Live-Tag (≤ 08.07.2026) auf das Drupal-System zeigen, damit die/feeds/*- und/asp/robots/*-Routes überhaupt vom Drupal beantwortet werden. - Legacy-Detail-URL-Redirect-Strategie (Advisor-Befund): Vendor-Anzeigen, die heute bereits live sind, zeigen auf
solcom.de/asp/robots/detail.aspx?id=<id>&mode=FREELANCERCHECK. Wenn der ASP-301-Redirect nur die Feed-Pfade abdeckt, brechen alte Vendor-Backlinks beim Cutover. Klärungs-Bedarf: catch-all-Redirect von/asp/robots/detail.aspx?id=*→ Drupal-Detail-Seite (Lookup überfield_sf_idoder überjobNumber) — oder Stakeholder-Akzeptanz, dass alte Vendor-Backlinks ablaufen. - Echte Channel-Flag-Pflege in SF: Migrate-Mapping ist da, aber wenn das Salesforce-Datenmodell
SC_Publishing_Channel_Freelancermap__cundSC_Publishing_Channel_Freelance__cnicht produktiv pflegt, ist der Feed jeden Tag leer. Test mit echten SF-Daten im DDEV-Staging vor Cutover. - Korrekte UTM-Werte für freelance.de: Heutige Werte (
utm_source=freelancercheck.de) sind Drift im Legacy-ASP-Feed und werden nicht übernommen. Korrekte Werte werden bei Implementierung mit dem Vendor abgestimmt; Vorab-Vorschlag siehe Config-Block oben. - Drupal-seitige Pflege von
field_sf_estimated_startalsdd.mm.yyyy: Das DateTime-Field in Drupal liefert ISO-8601; das Twig formatiert um. Field-Spec im Implementations-Plan finalisieren.
Further Notes
Doku-Korrekturen als Folge dieses Refinements:
docs/stories/F-340-asp-xml-feeds-freelancermap-freelance.htmlwird neu geschrieben (Quellevacancy→project_offer, Modul-Heimat, AKs gefüllt, URL-Strategie, Cache-Strategie).docs/wiki/freelancer-feeds.htmlwird in Section „Feed-Übersicht" und „Operative Kurzfassung" korrigiert: F-340/F-360 basieren aufnode.type.project_offer, nicht aufnode.type.vacancy.docs/adr/0032-project-offer-sync-hygiene.htmlhat URL-Kanon-Drift gegen Memoryproject_projektliste_canonical_url— eigener Update-PR, nicht in F-340.config/sync/pathauto.pattern.project_offer_content.ymlhat den alten Pfad — gehört zu F-290 oder eigener URL-Kanon-Story.
Pattern-Spiegelung zu ADR-0035: Die Personio-Entscheidung (inbound XML statt REST) und die F-340-Entscheidung (outbound XML statt Push-API) folgen demselben Prinzip — XML-Feeds sind das Drupal-idiomatische Pattern für strukturierte Vendor-Integrationen mit stabilem Schema und ohne Service-Identity-Anforderung. Die Symmetrie ist absichtlich.
Forcing-Function: Der Drupal-Cutover am 08.07.2026 ist hart. F-340 muss spätestens drei Wochen davor produktiv-testbar sein, weil der Vendor-Poll-Zyklus (täglich) ein erstes Sample-Window für Vendor-Validation braucht.
Performance-Annahme: 50–300 publizierte Project-Offers gleichzeitig. Bei mehr (z. B. > 1000) ist die Annahme „ein Response = Vollbestand" zu überdenken (Pagination, Streaming-Response). Aktuell nicht relevant.