Neu Linktexte für Erstbesucher/Crawler in Standardsprache – Ursache + Fix (SHOP-6552)

ricketywrecked

Aktives Mitglied
3. Juni 2024
13
8
Hallo zusammen,

beim Update auf 5.8.0 haben wir einen Sprachfehler diagnostiziert, der vermutlich alle mehrsprachigen Shops betrifft, aber im Alltag fast unsichtbar ist — weil er nur cookielose Erstaufrufe trifft. Genau die sehen aber Suchmaschinen-Crawler bei jedem Zugriff.

Umgebung: JTL-Shop 5.8.0 (der betroffene Code ist byte-identisch bereits in 5.7.2 vorhanden), mehrsprachig DE (Standardsprache) / EN / IT, NOVA-Child-Template, Standard-Objektcache aktiv.

Symptom: Ruft man ohne Session-Cookie (Inkognito-Fenster — oder eben als Googlebot) direkt eine EN-/IT-SEO-URL auf, ist die Seite korrekt lokalisiert (Kategorien, Artikel, Bilder, Sprachwahl) — aber alle Texte, die aus Link-Objekten stammen, kommen in der Standardsprache:

- Footer-Linkgruppen-Labels (Impressum, AGB, Datenschutz, Widerrufsrecht … statt Note legali, Termini e condizioni …)
- Breadcrumb auf Link-/Spezialseiten
- H1/Titel von Spezialseiten (bei uns aufgefallen an der neuen Gewährleistungsseite aus 5.8.0: /Garanzia-legale zeigte H1 „Gesetzliche Gewährleistung")
- die Links im Consent-Manager

Ab dem zweiten Aufruf (Session-Cookie vorhanden) ist alles korrekt — deshalb fällt es beim normalen Testen im Browser nie auf.

Reproduktion:
1. Mehrsprachigen Shop mit aktivem Objektcache verwenden, Standardsprache ≠ Zielsprache.
2. Inkognito-Fenster öffnen, direkt eine SEO-URL der Nicht-Standardsprache aufrufen (z. B. die EN-Gewährleistungsseite).
3. Footer-Labels/Breadcrumb/H1 erscheinen in der Standardsprache. Nach einem weiteren Klick (Session existiert) ist alles korrekt.

Ursache (Code-Analyse, 5.8.0 = 5.7.2):
LinkGroupList lädt den Cache-Eintrag „linkgroups" (CACHING_GROUP_CORE) sehr früh im Request. Beim Unserialisieren ruft JTL\Link\Link::__wakeup() → JTL\Router\RoutableTrait::initLanguageID() → Shop::getLanguageID() auf. Zu diesem Zeitpunkt ist die Request-Sprache aus dem SEO-Slug noch nicht ermittelt — es wird also die Standardsprache in currentLanguageID jedes Link-Objekts eingefroren. Alle parameterlosen Aufrufe von getName() / getTitle() / getMetaTitle() liefern anschließend die Standardsprache, obwohl Shop::getLanguageID() zur Renderzeit längst korrekt ist. (DB und Cache-Inhalt sind dabei nachweislich korrekt — es ist rein die eingefrorene Sprach-ID.)

Historie im Issuetracker — dreimal Symptom, nie die Wurzel:
- SHOP-6552 (2022, abgewiesen): beschreibt exakt dieses Verhalten — „Wird eine Sprachseite das allererste Mal geöffnet (noch keine Session), wird der Content in der Standardsprache ausgegeben; beim Aktualisieren dann korrekt" (dort auf der Startseite getestet, ab 5.1.3). Im Ticket bat bereits 07/2022 ein Partner um Prio-Behandlung (B2B-Kunde verlinkt direkt auf Sprachstartseiten) — das Ticket wurde dennoch abgewiesen, der Ablehnungsgrund ist nicht öffentlich einsehbar. Was damals offenbar fehlte, liefere ich unten nach: die konkrete Ursache im Code. Und ein Aspekt, der die Bewertung ändern dürfte: Suchmaschinen-Crawler sind IMMER sessionlos und sehen die falsche Sprache dauerhaft — das ist SEO-relevant, nicht kosmetisch.
- SHOP-3687 (gelöst 5.0.0): gleiche Baustelle, Auslöser „nach Anmeldung".
- SHOP-8352 (gelöst 5.5.0): Consent-Manager-Links — gefixt wurde nur der Sprachwechsel-Fall; im sessionlosen Erstaufruf sind die Consent-Links weiterhin in der Standardsprache (heute in 5.8.0 reproduziert).
- Historisch außerdem SHOP-1751 (2017, gelöst 4.06): CMS-Seiten-Sichtbarkeit griff erst beim zweiten Aufruf nach Login — dieselbe Mechanik-Familie (Link-Zustand hinkt dem Request-Kontext hinterher), damals beim Sichtbarkeits- statt Sprach-Aspekt.

Fix-Vorschlag: currentLanguageID beim __wakeup nicht einfrieren, sondern die parameterlosen Getter zur Laufzeit auf Shop::getLanguageID() zurückfallen lassen — oder die Link-Objekte nach der Sprachermittlung des Routers einmal re-initialisieren.

Workaround (bei uns produktiv im Einsatz und verifiziert): Im Child-Template-Bootstrap einen Listener registrieren, der die Link-Objekte re-initialisiert, sobald die Sprache feststeht:


PHP:
// templates/<Child>/Bootstrap.php, in boot() nach parent::boot():
$reInit = static function (): void {
    try {
        foreach (Shop::Container()->getLinkService()->getAllLinkGroups() as $linkGroup) {
            foreach ($linkGroup->getLinks() as $link) {
                if (\method_exists($link, 'initLanguageID')) {
                    $link->initLanguageID();
                }
            }
        }
    } catch (\Throwable) {
    }
};
$dispatcher = \JTL\Events\Dispatcher::getInstance();
$dispatcher->listen('shop.hook.' . \HOOK_SEOCHECK_ENDE, $reInit);
$dispatcher->listen('shop.hook.' . \HOOK_LETZTERINCLUDE_INC, $reInit);


(HOOK_SEOCHECK_ENDE feuert unmittelbar nachdem Shop::$kSprache gesetzt wurde; HOOK_LETZTERINCLUDE_INC in preRender() direkt vor dem Breadcrumb-Aufbau.) Damit sind Footer, Breadcrumb, Spezialseiten-Titel und Consent-Manager auch für cookielose Zugriffe korrekt lokalisiert.

Viele Grüße
 

OliausderSchweiz

Aktives Mitglied
3. März 2013
41
0
Hallo zusammen,

beim Update auf 5.8.0 haben wir einen Sprachfehler diagnostiziert, der vermutlich alle mehrsprachigen Shops betrifft, aber im Alltag fast unsichtbar ist — weil er nur cookielose Erstaufrufe trifft. Genau die sehen aber Suchmaschinen-Crawler bei jedem Zugriff.

Umgebung: JTL-Shop 5.8.0 (der betroffene Code ist byte-identisch bereits in 5.7.2 vorhanden), mehrsprachig DE (Standardsprache) / EN / IT, NOVA-Child-Template, Standard-Objektcache aktiv.

Symptom: Ruft man ohne Session-Cookie (Inkognito-Fenster — oder eben als Googlebot) direkt eine EN-/IT-SEO-URL auf, ist die Seite korrekt lokalisiert (Kategorien, Artikel, Bilder, Sprachwahl) — aber alle Texte, die aus Link-Objekten stammen, kommen in der Standardsprache:

- Footer-Linkgruppen-Labels (Impressum, AGB, Datenschutz, Widerrufsrecht … statt Note legali, Termini e condizioni …)
- Breadcrumb auf Link-/Spezialseiten
- H1/Titel von Spezialseiten (bei uns aufgefallen an der neuen Gewährleistungsseite aus 5.8.0: /Garanzia-legale zeigte H1 „Gesetzliche Gewährleistung")
- die Links im Consent-Manager

Ab dem zweiten Aufruf (Session-Cookie vorhanden) ist alles korrekt — deshalb fällt es beim normalen Testen im Browser nie auf.

Reproduktion:
1. Mehrsprachigen Shop mit aktivem Objektcache verwenden, Standardsprache ≠ Zielsprache.
2. Inkognito-Fenster öffnen, direkt eine SEO-URL der Nicht-Standardsprache aufrufen (z. B. die EN-Gewährleistungsseite).
3. Footer-Labels/Breadcrumb/H1 erscheinen in der Standardsprache. Nach einem weiteren Klick (Session existiert) ist alles korrekt.

Ursache (Code-Analyse, 5.8.0 = 5.7.2):
LinkGroupList lädt den Cache-Eintrag „linkgroups" (CACHING_GROUP_CORE) sehr früh im Request. Beim Unserialisieren ruft JTL\Link\Link::__wakeup() → JTL\Router\RoutableTrait::initLanguageID() → Shop::getLanguageID() auf. Zu diesem Zeitpunkt ist die Request-Sprache aus dem SEO-Slug noch nicht ermittelt — es wird also die Standardsprache in currentLanguageID jedes Link-Objekts eingefroren. Alle parameterlosen Aufrufe von getName() / getTitle() / getMetaTitle() liefern anschließend die Standardsprache, obwohl Shop::getLanguageID() zur Renderzeit längst korrekt ist. (DB und Cache-Inhalt sind dabei nachweislich korrekt — es ist rein die eingefrorene Sprach-ID.)

Historie im Issuetracker — dreimal Symptom, nie die Wurzel:
- SHOP-6552 (2022, abgewiesen): beschreibt exakt dieses Verhalten — „Wird eine Sprachseite das allererste Mal geöffnet (noch keine Session), wird der Content in der Standardsprache ausgegeben; beim Aktualisieren dann korrekt" (dort auf der Startseite getestet, ab 5.1.3). Im Ticket bat bereits 07/2022 ein Partner um Prio-Behandlung (B2B-Kunde verlinkt direkt auf Sprachstartseiten) — das Ticket wurde dennoch abgewiesen, der Ablehnungsgrund ist nicht öffentlich einsehbar. Was damals offenbar fehlte, liefere ich unten nach: die konkrete Ursache im Code. Und ein Aspekt, der die Bewertung ändern dürfte: Suchmaschinen-Crawler sind IMMER sessionlos und sehen die falsche Sprache dauerhaft — das ist SEO-relevant, nicht kosmetisch.
- SHOP-3687 (gelöst 5.0.0): gleiche Baustelle, Auslöser „nach Anmeldung".
- SHOP-8352 (gelöst 5.5.0): Consent-Manager-Links — gefixt wurde nur der Sprachwechsel-Fall; im sessionlosen Erstaufruf sind die Consent-Links weiterhin in der Standardsprache (heute in 5.8.0 reproduziert).
- Historisch außerdem SHOP-1751 (2017, gelöst 4.06): CMS-Seiten-Sichtbarkeit griff erst beim zweiten Aufruf nach Login — dieselbe Mechanik-Familie (Link-Zustand hinkt dem Request-Kontext hinterher), damals beim Sichtbarkeits- statt Sprach-Aspekt.

Fix-Vorschlag: currentLanguageID beim __wakeup nicht einfrieren, sondern die parameterlosen Getter zur Laufzeit auf Shop::getLanguageID() zurückfallen lassen — oder die Link-Objekte nach der Sprachermittlung des Routers einmal re-initialisieren.

Workaround (bei uns produktiv im Einsatz und verifiziert): Im Child-Template-Bootstrap einen Listener registrieren, der die Link-Objekte re-initialisiert, sobald die Sprache feststeht:


PHP:
// templates/<Child>/Bootstrap.php, in boot() nach parent::boot():
$reInit = static function (): void {
    try {
        foreach (Shop::Container()->getLinkService()->getAllLinkGroups() as $linkGroup) {
            foreach ($linkGroup->getLinks() as $link) {
                if (\method_exists($link, 'initLanguageID')) {
                    $link->initLanguageID();
                }
            }
        }
    } catch (\Throwable) {
    }
};
$dispatcher = \JTL\Events\Dispatcher::getInstance();
$dispatcher->listen('shop.hook.' . \HOOK_SEOCHECK_ENDE, $reInit);
$dispatcher->listen('shop.hook.' . \HOOK_LETZTERINCLUDE_INC, $reInit);


(HOOK_SEOCHECK_ENDE feuert unmittelbar nachdem Shop::$kSprache gesetzt wurde; HOOK_LETZTERINCLUDE_INC in preRender() direkt vor dem Breadcrumb-Aufbau.) Damit sind Footer, Breadcrumb, Spezialseiten-Titel und Consent-Manager auch für cookielose Zugriffe korrekt lokalisiert.

Viele Grüße
Hallo, toller Bericht! Den Canonical-Tag betrifft es ja gerade auch ... wenn ich deinen code-schnippsel bei mir imn der Bootstrap.php einbaue gibts nen Error 500 ... was mach ich da falsch?
Danke und Gruss
 

ricketywrecked

Aktives Mitglied
3. Juni 2024
13
8
Hallo, toller Bericht! Den Canonical-Tag betrifft es ja gerade auch ... wenn ich deinen code-schnippsel bei mir imn der Bootstrap.php einbaue gibts nen Error 500 ... was mach ich da falsch?
Danke und Gruss
Hallo Oli,

danke dir! Ohne die genaue Fehlermeldung ist es Ferndiagnose, aber es gibt drei übliche Verdächtige. Nummer 1 ist mit Abstand am wahrscheinlichsten, und das ist eine Unsauberkeit in meinem Schnipsel oben:

1. Fehlendes use-Statement: Der Schnipsel ruft Shop::Container() auf. Das setzt voraus, dass oben in deiner Bootstrap.php ein "use JTL\Shop;" steht. Fehlt die Zeile, sucht PHP die Klasse im Namespace deines Templates (Template\DeinChild\Shop), findet sie nicht und bricht mit "Class not found" ab: Fehler 500.

2. Falsche Stelle: Der Block muss in die Methode public function boot() deiner Child-Bootstrap, direkt nach parent::boot();. In den Klassenkörper oder ans Dateiende gepastet gibt es einen Parse Error.

3. PHP-Version: catch (\Throwable) ohne Variable gibt es erst ab PHP 8.0. Läuft dein Shop noch auf PHP 7.4, schreib catch (\Throwable $e).

Hier die paste-sichere Variante ohne jede use-Abhängigkeit (alles vollqualifiziert). Ersetze damit die komplette boot()-Methode in templates/DEIN-CHILD/Bootstrap.php (nicht zusätzlich einfügen, sonst existiert die Methode doppelt):


PHP:
public function boot(): void
{
    parent::boot();

    $reInit = static function (): void {
        try {
            foreach (\JTL\Shop::Container()->getLinkService()->getAllLinkGroups() as $linkGroup) {
                foreach ($linkGroup->getLinks() as $link) {
                    if (\method_exists($link, 'initLanguageID')) {
                        $link->initLanguageID();
                    }
                }
            }
        } catch (\Throwable $e) {
            // bewusst still: schlimmstenfalls bleibt das alte Sprachverhalten
        }
    };

    $dispatcher = \JTL\Events\Dispatcher::getInstance();
    $dispatcher->listen('shop.hook.' . \HOOK_SEOCHECK_ENDE, $reInit);
    $dispatcher->listen('shop.hook.' . \HOOK_LETZTERINCLUDE_INC, $reInit);
}

VG

(KI-generiert, weil keine Zeit 😅 )
 

OliausderSchweiz

Aktives Mitglied
3. März 2013
41
0
Hallo Oli,

danke dir! Ohne die genaue Fehlermeldung ist es Ferndiagnose, aber es gibt drei übliche Verdächtige. Nummer 1 ist mit Abstand am wahrscheinlichsten, und das ist eine Unsauberkeit in meinem Schnipsel oben:

1. Fehlendes use-Statement: Der Schnipsel ruft Shop::Container() auf. Das setzt voraus, dass oben in deiner Bootstrap.php ein "use JTL\Shop;" steht. Fehlt die Zeile, sucht PHP die Klasse im Namespace deines Templates (Template\DeinChild\Shop), findet sie nicht und bricht mit "Class not found" ab: Fehler 500.

2. Falsche Stelle: Der Block muss in die Methode public function boot() deiner Child-Bootstrap, direkt nach parent::boot();. In den Klassenkörper oder ans Dateiende gepastet gibt es einen Parse Error.

3. PHP-Version: catch (\Throwable) ohne Variable gibt es erst ab PHP 8.0. Läuft dein Shop noch auf PHP 7.4, schreib catch (\Throwable $e).

Hier die paste-sichere Variante ohne jede use-Abhängigkeit (alles vollqualifiziert). Ersetze damit die komplette boot()-Methode in templates/DEIN-CHILD/Bootstrap.php (nicht zusätzlich einfügen, sonst existiert die Methode doppelt):


PHP:
public function boot(): void
{
    parent::boot();

    $reInit = static function (): void {
        try {
            foreach (\JTL\Shop::Container()->getLinkService()->getAllLinkGroups() as $linkGroup) {
                foreach ($linkGroup->getLinks() as $link) {
                    if (\method_exists($link, 'initLanguageID')) {
                        $link->initLanguageID();
                    }
                }
            }
        } catch (\Throwable $e) {
            // bewusst still: schlimmstenfalls bleibt das alte Sprachverhalten
        }
    };

    $dispatcher = \JTL\Events\Dispatcher::getInstance();
    $dispatcher->listen('shop.hook.' . \HOOK_SEOCHECK_ENDE, $reInit);
    $dispatcher->listen('shop.hook.' . \HOOK_LETZTERINCLUDE_INC, $reInit);
}

VG

(KI-generiert, weil keine Zeit 😅 )
Hallo und danke für deine Antwort.
Dieses Problem auf meiner Seite hatte ich schon gelöst.
Mein Ziel war es, den Canonical-Tag auf der Startseite in der korrekten Sprache (bei Mehrsprachigkeit) zu setzen - der JTL-Shop macht das nicht und setzt immer die deutsche Version.
Idee wie das zu lösen ist?
LG
 

NoOne

Sehr aktives Mitglied
16. März 2024
675
231
Was wird bei dir denn als Canonical auf der Startseite gesetzt? Das ist eigentlich kein Problem.
 

OliausderSchweiz

Aktives Mitglied
3. März 2013
41
0
Sorry, habs etwas falsch beschrieben oben ...

Das Problem: In Deutsch wird auf der Hauptseite nur der URL ohne den Canonical-Text angezeigt!

Beispiel Deutsch:
<link rel="canonical" href="https://Deine Domain.de">
und dann umschalten auf Englisch
<link rel="canonical" href="https://Deine Domain.de/hier steht der englische Text">

Auf den Artikelseiten funktioniert es.

Verhält sich bei dir auch so, oder?
 

Ähnliche Themen