Vor Weihnachten habe ich noch schnell die Prüfung zum TYPO3 CMS Certified Developer abgelegt und bin damit (wieder) zertifiziert.
Seitentitel anpassen
Das ist wieder so ein Fall von „ich kann es mir nicht merken und die Google-Suche liefert immer die falschen Ergebnisse“.
Früher konnte TYPO3 nur Seitentitel der Form [Sitename]: [Seitentitel] generieren. Die einzige Möglichkeit, das zu ändern, bestand darin, die Option config.noPageTitle zu aktivieren und den Titel unter headerData selbst zusammenzustellen. Dabei gibt es schon seit langem Konfigurationsmöglichkeiten für den Seitentitel. Zum einen kann man die Reihenfolge einstellen, zum anderen kann man den Trenner angeben.
Ich war mir nicht sicher, wie die dafür notwendigen Konfigurationen heißen, daher wollte ich mal die Suche im Internet bemühen. Aber die Google Suche leitet mich nur auf Seiten mit dem config.noPageTitle Beispiel. Dabei ist es so einfach:
config { pageTitleFirst = 1 pageTitleSeparator = - pageTitleSeparator.noTrimWrap = | | | }
Ergebnis:
Meine Site - Seite ABC
Abschnittsnavigation (aka menu_section) mit Gridelements
Wenn man mit Gridelements und somit verschachtelten Inhaltselementen arbeitet, stellt man vielleicht irgendwann fest, dass die Sortierung nicht funktioniert, sobald in der Abschnittsnavigation anzuzeigende Inhaltselemente in Container (2-Spalten etc.) angeordnet werden. Beispiel (in Klammern ist die Position intern):
- Container A (1)
- Element A.1 (1)
- Element A.2 (2)
- Container B (2)
- Element B.1 (1)
- Element B.2 (2)
Je nachdem, in welcher Reihenfolge man die Elemente erstellt hat, kann z.B. folgende Abschnittsnavigation ausgegeben werden:
- B.1
- A.1
- A.2
- B.2
Ich habe z.B. folgende Lösung mit einem ViewHelper gefunden. Ich wollte jedoch eine Lösung, die auch für bereits vorhandene Inhaltselemente funktioniert und die unabhängig vom Template eingesetzt werden kann. So habe ich mich für einen DataProcessor entschieden.
Als erstes hole ich mir alle Inhaltselemente – unabhängig davon, ob sectionIndex gesetzt ist oder nicht. Dann hänge ich meinen eigenen DataProcessor ein.
tt_content.menu_section.dataProcessing.10.dataProcessing.20 { where > } tt_content.menu_section.dataProcessing.10.dataProcessing.30 = NP\MyExtension\DataProcessing\SectionProcessor
Im DataProcessor werden die Datensätze gefiltert, so dass nur die mit sectionIndex = 1 übrig bleiben. Weiterhin wird die Sortierung korrigiert und es wird neu sortiert. Beim neuen Sorting wird der Wert der Elemente, die direkt auf der Seite abgelegt sind, mit 10000 multipliziert. Falls das Element in einem Container liegt, dann wird die Sortierung auf den Sorting-Wert des Containers mal 10000 plus die eigene Sortierung gesetzt.
Den DataProcessor lege ich unter Classes\DataProcessing\SectionProcessor.php ab:
namespace NP\MyExtension\DataProcessing; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface; class SectionProcessor implements DataProcessorInterface { /** * Process content object data * * @param ContentObjectRenderer $cObj The data of the content element or page * @param array $contentObjectConfiguration The configuration of Content Object * @param array $processorConfiguration The configuration of this processor * @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View) * @return array the processed data as key/value store */ public function process( ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData ) { $processedContent = []; $sortMapping = []; foreach ($processedData['content'] as $content) { if ($content['data']['sectionIndex'] == 1) { $content['data']['sorting'] = $this->getRealSorting($content, $processedData['content']); $processedContent[] = $content; } } // debug($processedContent, 'processed content'); usort($processedContent, [$this, 'sortContent']); $processedData['content'] = $processedContent; return $processedData; } /** * @param array $content * @param array $allElements */ protected function getRealSorting($content, $allElements) { if ($content['data']['colPos'] != '-1') { return $content['data']['sorting'] * 10000; } $sorting = $content['data']['sorting']; foreach ($allElements as $element) { if ($element['data']['uid'] == $content['data']['tx_gridelements_container']) { $sorting += $element['data']['sorting'] * 10000; } } return $sorting; } /** * @param array $a * @param array $b * @return bool */ protected function sortContent($a, $b) { return $a['data']['sorting'] > $b['data']['sorting']; } }
Dieser Ansatz wird limitiert durch das Verschachtelungslevel der Elemente. Da könnte man diesen auf die schnelle implementierten Ansatz noch optimieren.
TYPO3 auf Ddev installieren
Ja, es gibt Anleitungen da draußen, in denen das drin steht. Ich kann es mir einfach nicht merken … weder den Link noch den Inhalt.
Projekt-Ordner erstellen
mkdir spielwiese11
In Projekt-Ordner wechseln
cd spielwiese11
Ddev Projekt initalisieren
ddev config
Es kommen ein paar Fragen, bei docroot „public“ eingeben und bei Project type „typo3“.
Ddev starten
ddev start
In Ddev wechseln
ddev ssh
TYPO3 per Composer installieren
Hier ist der TYPO3 Composer Helper sehr hilfreich. Einfach die Version auswählen, die installiert werden soll (Minimal, Default oder Full), kopieren, einfügen und ausführen.
Dann kann man die Seite bereits unter https://spielwiese11.ddev.site aufrufen und den Rest im Browser erledigen.
TYPO3 Extension Settings im Scheduler Task auslesen
Ich brauche die Settings eines Plugins im Scheduler. Es gibt zwar zahlreiche Snippets und Hinweise, wie es gehen soll, die alle nicht funktioniert haben. Dieses Snippet ist getestet unter TYPO3 10.
Da man sich im Backend befindet, müssen die Settings von plugin nach module kopiert werden:
module.tx_myextension < plugin.tx_myextension
Meine Extension heißt beispielhaft my_extension und ich habe im TypoScript die Konfiguration für alle Plugins gesetzt.
Im Scheduler-Task kann man auf die Settings dann wie folgt zugreifen. Zu beachten dabei die Schreibweise des Extensionnames, es ist CamelCase.
use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Configuration\ConfigurationManager; $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class); $configuration = $configurationManager->getConfiguration( ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'MyExtension' );
Sonderbare Meldung im Modal beim Löschen der Seite
Es ist ein sehr spezieller Fall, den wahrscheinlich kaum jemand kennen wird, aber mir hat es nochmal etwas neues über die Konfiguration in TYPO3 beigebracht.
Mir wurde gemeldet, dass beim Löschen einer Seite aus dem Kontext-Menü im Seitenbaum folgende Meldung im Modal erscheint:
Ich wusste nicht direkt, welche Modal-Konfiguration gemeint ist, und habe mich durch den Code gehangelt. Erstmal die Meldung suchen – der Locallang-Key ist „mess.delete“. Dann suchen, wo diese Meldung generiert wird – das war im \TYPO3\CMS\Backend\ContextMenu\ItemProviders\RecordProvider in der Funktion getDeleteAdditionalAttributes. Dann rausgefunden, dass die Funktion nicht die korrekten Attribute liefert und dass es an if ($this->backendUser->jsConfirmation(JsConfirmation::DELETE)) { ... }
scheitert.
Schaut man sich die Funktion jsConfirmation genaue an, dann bekommt sie als Parameter eine Bitmaske. Diese Bitmaske steht in User TSconfig unter options.alertPopus. Ich hatte in meinem Fall diesen Wert gesetzt auf 250. Vergleicht man es mit der Dokumentation, ist dort 255 für „Alle Popups“ angegeben.
255 steht für die Bitmaske 11111111 und
250 steht für die Bitmaske 11111010
Das erste Bit von hinten steht für „On Type Change“, das dritte Bit von hinten für „Löschen“. Das ergibt sich aus den Angaben der Werte in der Dokumentation. 1 entspricht der Bitmaske 1, 4 entspricht der Bitmaske 100. Wenn man nun die Zahlen vergleicht, dann unterscheidet sich 255 und 250 genau an diesen zwei Stellen.
Es wäre natürlich besser, wenn das Modal gar nicht erst angezeigt wird, wenn in der Bitmaske das Löschen-Bit 0 ist. In diesem Fall habe ich es einfach dadurch „gelöst“, dass ich den Wert für options.alertPopups auf 255 gesetzt habe. Dann kommt zumindest keine verstörende Meldung 🙂
Weiterleitung auf mit www und https
Meiner Meinung nach war das die beste Lösung für eine Weiterleitung von ohne www auf mit www. Gefunden bei Stackoverflow.
RewriteCond %{HTTP_HOST} !^www\..+$ [NC] RewriteRule ^ https://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Es enthält keinen Domain-Namen und kann damit überall eingesetzt werden.
TCA für Image Manipulation (crop) in eigener Extension
Es gibt ein paar Beispiele im Netz, wie man Bild-Manipuation für tt_content aktiviert und Crop-Varianten (cropVariants) definiert. Ich habe kein Beispiel gefunden, wie man in seiner eigenen Extension direkt einem Bild-Feld Crop-Eigenschaften mitgeben kann. Nach langer Suche und viel rumprobieren ist das mein Ergebnis, das funktioniert.
'stage_image' => [ 'exclude' => true, 'label' => 'LABEL', 'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig( 'stage_image', [ 'appearance' => [ 'createNewRelationLinkTitle' => 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:images.addFileReference' ], 'overrideChildTca' => [ 'types' => [ '0' => [ 'showitem' => ' --palette--;;imageoverlayPalette, --palette--;;filePalette' ], \TYPO3\CMS\Core\Resource\File::FILETYPE_IMAGE => [ 'showitem' => ' --palette--;;imageoverlayPalette, --palette--;;filePalette' ], ], 'columns' => [ 'crop' => [ 'config' => [ 'cropVariants' => [ 'default' => [ 'title' => 'Desktop', 'allowedAspectRatios' => [ 'default' => [ 'title' => 'Rechteckig 22:9', 'value' => 2.4 ], ], ], ], ], ], ], ], 'foreign_match_fields' => [ 'fieldname' => 'stage_image', 'tablenames' => 'xxx', 'table_local' => 'sys_file', ], 'maxitems' => 1, ], $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] ), ],
Redirect von URLs mit .html auf /
Vor TYPO3 9 und dem Routing im Core war RealURL sehr gnädig, was URLs anging. So konnte eine Seite mit .html Suffix und auch mit / aufgerufen werden (mein Empfinden). In TYPO3 10 funktioniert diese Verhalten nur in eine Richtung. Konfiguriert man eine .html Endung über den PageSuffix, ist die Seite weiterhin mit Trailing Slash aufrufbar. Es ist eigentltich gut, dass entweder das eine oder das andere geht. Wenn sie jemand die URL mit .html Endung gebookmarked hat und dann statt der Seite 404 bekommt, ist es ärgerlich.
Dank dieses sehr guten Beitrags auf stackoverflow hier mein Redirect-Schnipsel für die htaccess-Datei.
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)\.html$ /$1/ [L,R=302]
Als Tipp stand in dem Beitrag, dass man während des Testings den Fehlercode 302 verwenden sollte (found) und erst wenn es final ist, dann 301, weil dieser vom Browser gecached wird.
TYPO3 Meldung bei gesperrten Records im Backend
Ich arbeite ja fast immer in der englischen Version, daher ist mir dieser Fehler nicht aufgefallen. Wenn ein anderer Benutzer einen Datensatz (z.B. eine Seite) zum Bearbeiten geöffnet hat, dann wird im TYPO3 Seitenbaum eine Meldung angezeigt. Im Englischen lautet die Meldung wie folgt:
„The user ‚%s‘ began to edit this record %s ago.“ Dann wird der Benutzertyp und das Datum eingesetzt, passt soweit.
Ist das Backend auf deutsch eingestellt, sieht diese Meldung etwas komisch aus.
Die Anführungsstriche werden mit HTML-Entities dargestellt.
Die Suche im Netz gab diesen wertvollen Beitrag aus der TYPO3-Doku aus: https://docs.typo3.org/m/typo3/reference-coreapi/9.5/en-us/ApiOverview/Internationalization/ManagingTranslations.html
Die Übersetzungen fürs Backend können also in einer eigenen Extension überschrieben werden.
Das in die ext_localconf.php einer eigenen Extension:
$GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride']['de']['EXT:core/Resources/Private/Language/locallang_core.xlf'][] = 'EXT:my_extension/Resources/Private/Language/de.locallang-core.xlf';
Dann unter dem angegebenen Pfad die xlf-Datei anlegen und folgendes rein:
<?xml version="1.0" encoding="utf-8" standalone="yes" ?> <xliff version="1.0"> <file source-language="en" datatype="plaintext" date="2013-03-09T18:44:59Z" product-name="examples"> <header/> <body> <trans-unit id="labels.lockedRecord" resname="labels.lockedRecord" approved="yes"> <source>The user '%s' began to edit this record %s ago.</source> <target state="translated">Der Benutzer '%s' hat mit der Bearbeitung dieses Datensatzes vor %s begonnen.</target> </trans-unit> <trans-unit id="labels.lockedRecord_content" resname="labels.lockedRecord_content" approved="yes"> <source>The user '%s' began to edit content on this page %s ago.</source> <target state="translated">Der Benutzer '%s' hat mit der Bearbeitung dieser Seite vor %s begonnen.</target> </trans-unit> <trans-unit id="labels.lockedRecordUser" resname="labels.lockedRecordUser" approved="yes"> <source>The %s '%s' began to edit this record %s ago.</source> <target state="translated">Der %s '%s' hat mit der Bearbeitung dieses Datensatzes vor %s begonnen.</target> </trans-unit> <trans-unit id="labels.lockedRecordUser_content" resname="labels.lockedRecordUser_content" approved="yes"> <source>The %s '%s' began to edit content on this page %s ago.</source> <target state="translated">Der %s '%s' hat mit der Bearbeitung dieser Seite vor %s begonnen.</target> </trans-unit> </body> </file> </xliff>
Klappt! Hab ich schon erwähnt, dass ich TYPO3 toll finde 😀
Neueste Kommentare