Autor: Florian Freiburg

  • Lange nicht gelesen, v2

    Papa, was machst du da?

    Ok, wow. 2019 war der letzte Beitrag. Mittlerweile ist da ganz schön viel passiert. Aus „ich schreib jetzt mal schnell was“ wurde „schnell noch das Kind die Kinder ins Bett bringen“ und „dann an die Tastatur“ „der Hund muss noch raus“ und „jetzt noch eine Runde Rad“ „Shit, Garten, giessen!“.

    Alles um mich herum wächst, gedeiht und vergeht auch stellenweise. Der Lauf der Dinge eben.

    Nur meine Zeit zum schreiben nicht – die will nicht wachsen oder gedeihen, aber auch nicht vergehen. Das ist etwas das man nicht mit Geld, oder wie im Gemüsebeet mit Dünger oder Wasser, beheben könnte.

    Ich arbeite nach wie vor bei KRUU, bin mittlerweile als Stv. Leiter der Software-Entwicklung (und nach über acht Jahren immer noch glücklich dort!). Ich programmiere nach wie vor mit dem Symfony Framework, konnte einen Haufen Erfahrung mit den verschiedensten Technologien sammeln. Wir sind Maintainer der libholiday geworden, haben eine eigene kleine PHP Erweiterung für die Erstellung von RFID Codes nach ISO 17364 geschrieben (krieg heute immer noch nicht auf die Kette dass es das wirklich nicht gab) und so viel Code refakturiert-verbessert-gelöscht …
    Mehr als genug Input/Output um hier demnächst mal wieder was zu schreiben.

    Seid gespannt.

  • Lange nicht gesehen (gelesen)

    Mittlerweile ist es ja schon über ein Jahr her, dass ich hier etwas geschrieben habe. Dabei hatte ich mir fest vorgenommen, meine Erkenntnisse immer niederzuschreiben und mit anderen zu teilen. Hat leider nicht so funktioniert wie ich mir das vorgestellt habe. Arbeit hier und dort, Familie, ein Gartengrundstück. All das frisst mehr Zeit als ich erwartet habe.

    Aber: Ein guter Vorsatz für das neue Jahr 2019 ist es doch, endlich mal wieder Beiträge zu schreiben!

    Man liest sich 🙂

  • WordPress: Multisite in Foldern und nginx

    WordPress bietet von Haus aus die Verwaltung von so genannten Multisite-Umgebungen an. Sprich man kann mehrere Blogs / Webseiten mit einer Installation verwalten. Selbiges kennt man auch aus Typo3. Ich kannte diese Installationen bisher eher domainbasiert. Ich habe also entweder Subdomains oder „richtige“ Domains, unter welchem der jeweilige Blog läuft. Beispiel: blog.kundenname1.de, www.kundenname1.de, www.kundenname2.de. Das funktioniert im Regelfall recht gut ohne weitere Anpassungen von nginx.

    Aufgabe

    Neulich hatte ich jedoch einen Sonderfall: Es soll für den Blog zunächst eine Auswahlseite geben. Von dieser aus kommt man auf die unterschiedlichen Blogs. Das liesse sich auch über einfach über Kategorien lösen. Allerdings sollen die einzelnen Benutzer innerhalb ihres „Blogs“ bleiben und nicht die Beiträge der anderen Benutzer sehen. Die beiden Blogs sollten in Unterverzeichnissen verfügbar gemacht werden:

    kundenname.de – Blogauswahl
    kundenname.de/blog1 – Blog zum Unternehmensbereich 1
    kundenname.de/blog2 – Blog zum Unternehmensbereich 2

    Um dies zu erreichen kann man einfach die Standard Multisite-Installation von WordPress durchführen.Eine gute Anleitung findet sich hier. Dabei gilt es darauf zu achten diese mit Subdirectories einzurichten, eine spätere Änderung hat sich als nicht ganz so trivial herausgestellt. Anschliessend richtet man sich über die Netzwerkverwaltung die einzelnen Blogs „Blogauswahl“, „Blog 1“ und „Blog 2“ ein. Jedoch sind bis auf die Blogauswahl die beiden Blogs mit ihren Verzeichnissen nicht erreichbar. Man kommt nur auf den Blog Übersicht, die anderen beiden Blogs bringen im Backend wie Frontend einen Fehler 500

    Hierfür muss man die Konfiguration in nginx etwas anpassen, was jedoch mit ein paar zusätzlichen Zeilen erledigt ist.

    server {
        listen   80;
    
        root /var/www/kundenname.de/htdocs;
            index index.php;
    
        server_name kundenname.de;
    
        error_log /var/log/nginx/kundenname.de.error.log;
        access_log /var/log/nginx/kundenname.de.access.log;
    
    
        client_max_body_size 100M;
        client_body_buffer_size 128k;
    
    
        if (!-e $request_filename) {
            rewrite /wp-admin$ $scheme://$host$uri/ permanent;
            rewrite ^(/[^/]+)?(/wp-.*) $2 last;
            rewrite ^(/[^/]+)?(/.*\.php) $2 last;
        }
    
        location / {
            try_files $uri $uri/ /index.php?$args ;
        }
    
        location ~ \.php$ {
            try_files $uri =404;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_pass 127.0.0.1:9001;
            fastcgi_read_timeout 300;
        }
    
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|webp)$ {
            expires 8d;
            log_not_found off;
            gzip  on;
        }
    }

     

  • PHP: Funktionstests auf Bilder mit Codeception

    In einem Symfony-Projekt soll mit Codeception ein Funktionstest erstellt werden, der die Webseite auf nicht geladene Bilder untersucht. Weiterhin soll die Funktionalität natürlich nicht nur in einem einzelnen Test, sonder über die gesamte Suite und ggfs. in weiteren Testsuites verfügbar sein.

    Also muss man sich einen eigenen Helper erstellen. Darin kann man aus dem bestehenden Set des PhpBrowser oder Selenium Moduls die benötigte Funktionalität zusammenbauen und so dem gesamten Projekt zu Verfügung stellen, ganz nach Anforderung.

    Vorbereitung: Der eigene Helper

    Zunächst einmal benötigt man einen eigenen Helper. Diesen legt man im Ordner _support/Helper/ an, in meinem Beispiel heisst dieser Helper „myHelper.php“.

    <?php
    namespace Helper;
    
    // here you can define custom actions
    // all public methods declared in helper class will be available in $I
    
    class myHelper extends \Codeception\Module {
    
    }

    Anschliessend muss der Helper noch der Testsuite bekannt gemacht werden. Dafür reicht folgender Eintrag bei den aktivierten Modulen der Testsuite.

    modules:
        enabled:
            - \Helper\myHelper

    Ansatz 1: Ist das gewünschte Bild sichtbar?

    Die einfachste Prüfung lässt sich mit XPath und der seeElement Funktion erledigen. seeElement prüft ob ein Element im DOM vorhanden und sichtbar ist. Die nachfolgende Funktion erwartet die URL der Grafik, ruft den PhpBrowser auf und prüft das Bild.

        public function seeImagewithSource($imageUrl) {
            $phpBrowser = $this->getModule('PhpBrowser');
            $phpBrowser->seeElement('//img[@src="'.$imageUrl.'"]');
        }

    Diese Variante deckt jedoch nicht alle Grafiken ab. Man müsste für einen Test jedes Bild kennen und angeben. Das kann sich bei redaktionell gepflegten Seiten als schwierig herausstellen.

    Ansatz 2: Prüfung mit Selenium

    Das nächste was mir einfiel war alle Bilder per JavaScript zu prüfen. Der folgende Script loopt über alle Bilder und prüft die Darstellung.

    Array.prototype.slice.call(document.images).every(function (img) {return img.complete && img.naturalWidth > 0;});

    Kann ein Bild nicht geladen oder dargestellt werden, liefert das Script false zurück. In Codeception lässt sich das mit dem Modul Selenium prüfen.

    function checkAllImages() {
            $result = $this->getModule('Selenium2')->session->evaluateScript("return (function(){ return Array.prototype.slice.call(document.images).every(function (img) {return img.complete && img.naturalWidth > 0;}); })()");
            return $result;
    }

    Solange die Bilder direkt verfügbar sind ist das eine annehmbare Lösung. Werden die Bilder jedoch von extern geladen, kann es zu Zeitverzögerungen kommen und somit würde der Test unter Umständen fehlschlagen – das Bild wäre aber vorhanden.

    Ansatz 3: Prüfen des Statuscodes

    Die Zeitverzögerung hat mich dann zum nächsten Ansatz geführt: Die Überprüfen aller Bilder einer Seite auf ihren Statuscode. Der zurückgelieferte Code 200 heisst „OK“, das Bild kann geladen werden. Alle anderen Codes lassen den Test fehlschlagen.

    Hierfür nutzt man wieder den PHPBrowser. Mit grabMultiple holt man sich alle Bilder und loopt über das Ergebnis. Mit amOnPage prüft man die Grafik, seeResponseCodeIs liefert den gewünschten Statuscode zurück. Mit diesem Test lassen sich alle Grafiken auf einer Seite prüfen.

        public function checkAllImagesSource() {
            $phpBrowser = $this->getModule('PhpBrowser');
            $results = $phpBrowser->grabMultiple('img', 'src');
            foreach($results as $result) {
                \Codeception\Util\Debug::debug($result);
                $phpBrowser->amOnPage($result);
                $phpBrowser->seeResponseCodeIs(200);
            }
        }

     

  • TYPO3: Seitentitel mit Daten aus Extension befüllen

    Oft setzt man auf seiner Typo3 Seite Plugins ein, welche die Funktion Listenansicht und Detailansicht beinhalten. Klassische Beispiele hierfür sind Plugins wie News oder Stellenangebote. Für die SEO Optimierung ist es meist notwendig den Seitentitel entsprechend anzupassen, da man mit einer Seite „Stellenangebot“ meist relativ wenig anfangen kann. Hier wäre es gut, wenn z.B. der Titel des Stellenangebotes nach „Stellenangebot“ erscheint.

    Wenn die Erweiterung dies aber nicht von selbst macht, kann man dies einfach über ein Typoscript Template auf der Detailseite des Plugins erledigen. Man muss lediglich prüfen, an welcher Stelle der Titel gesetzt wird (falls man diesen schon in der Seitenkonfiguration überschrieben hat).

    page.headerData.5 >
    page.headerData.5 = TEXT
    page.headerData.5.data = GP:tx_jobfair_pi1|job
    page.headerData.5.wrap = <title>Stellenangebot - {DB:tx_jobfair_domain_model_job:|:job_title}</title>
    page.headerData.5.insertData = 1

     

  • Ich wünsche…

    …allen ein frohes Fest und einen guten Rutsch ins neue Jahr 2017!

  • TYPO3: formhandler Daten aus der Datenbank übernehmen

    Offenbar mutiert formhandler gerade zu einem meiner Lieblingsthemen in Typo3. Auch an dieser Stelle wieder der Hinweis: formhandler wird nicht mehr aktiv weiterentwickelt!

    Der Kundenwunsch ist eine Double-Opt-In Anmeldung für einen Newsletter mit tt_address und formhandler. Als kleines Schmankerl soll jedoch nach erfolgreicher Freischaltung bzw. Aktivierung der Newsletteranmeldung durch den Abonnenten der Administraotr der Seite noch zusätzlich per Email darüber informiert werden. Hintergrund für das Email an den Administrator ist die Verwendung einer eigenen Adressverwaltung, welche jedoch nicht extern erreichbar ist.

    Als Grundlage dient mir hierbei die Konfiguration von Browserwerk, welche bereits eine recht gute Anleitung zum Thema geschrieben haben.

    Konfiguration AuthCode-Validierung

    Die Konfiguration für das Email ist recht einfach. Hier gilt es nur zu beachten, dass der „AuthCodeValidator“ keine redirectPage hat – sonst leitet er bei erfolgreicher Validierung weiter an die „Erfolg“-Seite, noch bevor das Email verschickt werden kann. Hier ist es also wichtig, nur die errorRedirectPage zu setzen.

        preProcessors {
            1.class = PreProcessor_LoadGetPost
    
            10.class = PreProcessor_ValidateAuthCode
            10.config {
                errorRedirectPage = 123
                hiddenField = hidden
                uidField = uid
                table = tt_address
            }
        }

    Konfiguration Email-Finisher

    Das ist soweit kein Problem, man kann ja einen entsprechenden Finisher setzen, der dies übernimmt. Dort werden auch die Settings für das Versenden des Bestätigungsmails an den Admin eingestellt.

    finishers {
            3.class = Finisher_Mail
            3.config {
                checkBinaryCrLf = message
                admin {
                    templateFile = TEXT
                    templateFile.value = Pfad-zu/html/email-admin.html
                    sender_email = newsletteranmeldung@example.com
                    to_email = admin@example.com
                    subject = TEXT
                    subject.data = LLL:Pfad-zu/lang/lang.xml:email_admin_subject
                }
                user >
            }
            4.class = Finisher_Redirect
            4.config {
                redirectPage = 134
            }
    }

    Nun funktioniert das Formular soweit wie vom Kunden gewünscht. Der Abonennt meldet sich an, bekommt ein Email mit einem Link zur Bestätigung, und erst nach Klick auf den Link ist er freigeschaltet. Aber wie bekommt man aber nun die Information der Anmeldung (z.B. Email oder Name) in das Bestätigungsemail an den Admin? Man könnte es sich ja einfach machen und die Email mit als URL Parameter mitgeben. Das ist aber weder schön, noch gut, noch sinnvoll. Aber man bekommt ja die uid mitgeliefert.

    Markers

    In meiner Lösung verwende ich die Markers von formhandler, um zum einen die uid aus der URL auszulesen und dann den Datensatz aus tt_address auszulesen. Das Ganze findet noch vor dem PreProcessor statt. Bei pidInList sollte der Speicherort der tt_address-Datensätze angegeben werden.

    markers {
    	registermail = CONTENT
            registermail {
                table = tt_address
                select {
                    pidInList = 154
                    selectFields = email
                    where.dataWrap = uid={GP:subscription|uid}
                }
                renderObj = COA
                renderObj {
                    10.wrap = "|"
                    10 = TEXT
                    10.field = email
                }
            }
    }

    Jetzt kann man ohne weiteres im mastertemplate an beliebiger Stelle, zum Beispiel im Admin-Mail, mit ###registermail### auf die aus der Datenbank ausgelesenen Emailadresse zugreifen und diese so an den Admin übermitteln.

    <!-- ###master_email-admin-start-plain### -->
    
    ###LLL:email_admin_text###
    ###registermail###
    
    <!-- ###master_email-admin-start-plain### -->
    
    <!-- ###master_email-admin-start-html### -->
    
    <p><strong>###LLL:email_admin_text###</strong></p>
    <p>###registermail###</p>
    <br />
    <table>
    <!-- ###master_email-admin-start-html### -->

    Die fertige Typoscript-Konfiguration

    Die komplette Typoscript-Konfiguration kann dann so aussehen:

    plugin.Tx_Formhandler.settings.predef.double-opt-in-confirm {
    
    	name = Newsletter Registrierung - Validierung
        skipView = 1
    	formValuesPrefix = subscription
    
    	langFile.1 = TEXT
    	langFile.1.value = {$formhandler.double-opt-in-confirm.rootPath}/lang/lang.xml
    
    	templateFile = TEXT
    	templateFile.value = {$formhandler.double-opt-in-confirm.rootPath}/html/step-1.html
    
    	# The master template is a file containing the markup for specific field types or other sub templates (e.g. for emails). You can use these predefined markups in your HTML template for a specific form.
    	masterTemplateFile = TEXT
    	masterTemplateFile.value = {$formhandler.double-opt-in-confirm.rootPath}/html/mastertemplate.html
    
    	# CSS files
    	cssFile {
    		10 = TEXT
    		10.value = {$formhandler.double-opt-in-confirm.rootPath}/skin/css/none.css
    		10.if.isTrue = {$formhandler.double-opt-in-confirm.includeFoundationCSS}
    		20 = TEXT
    		20.value = {$formhandler.double-opt-in-confirm.rootPath}/skin/css/none.css
    	}
    	
    	# In case an error occurred, all markers ###is_error_[fieldname]### are filled with the configured value of the setting "default".
    	isErrorMarker {
    		default = error
    	}
    
    	markers {
    	    registermail = CONTENT
            registermail {
                table = tt_address
                select {
                    pidInList = {$formhandler.double-opt-in-confirm.recordStorage}
                    selectFields = email
                    where.dataWrap = uid={GP:subscription|uid}
                }
                renderObj = COA
                renderObj {
                    10.wrap = "|"
                    10 = TEXT
                    10.field = email
                }
            }
    	}
    
        preProcessors {
            1.class = PreProcessor_LoadGetPost
    
            10.class = PreProcessor_ValidateAuthCode
            10.config {
                errorRedirectPage = {$formhandler.double-opt-in-confirm.redirectErrorPage}
                hiddenField = hidden
                uidField = uid
                table = tt_address
            }
        }
    
        finishers {
            3.class = Finisher_Mail
            3.config {
                checkBinaryCrLf = message
                admin {
                    templateFile = TEXT
                    templateFile.value = {$formhandler.double-opt-in-confirm.rootPath}/html/email-admin.html
                    sender_email = {$formhandler.double-opt-in-confirm.email.admin.sender_email}
                    to_email = {$formhandler.double-opt-in-confirm.email.admin.to_email}
                    subject = TEXT
                    subject.data = LLL:{$formhandler.double-opt-in-confirm.rootPath}/lang/lang.xml:email_admin_subject
                }
                user >
            }
            4.class = Finisher_Redirect
            4.config {
                redirectPage = {$formhandler.double-opt-in-confirm.redirectSuccessPage}
            }
    
        }
    	
    	# These wraps define how an error message looks like. The message itself is set in the lang file.
    	singleErrorTemplate {
    		totalWrap = <small class="error">|</small>
    	}
    }

     

     

     

  • TYPO3: SSL Zertifikat in einer Multidomainumgebung

    Angenommen man hat eine Typo3 Multidomainumgebung, welche die Domains kundenname1.de, kundenname2.de und kundenname3.de ohne ein SSL Zertifikat verwaltet, der Webserver selbst befindet sich hinter einem Loadbalancer, welcher die Anfragen intern entsprechend weitergibt. Nach außen Port 443, intern werden Anfragen über Port 80 verarbeitet.

    Nun entscheidet der Kunde, dass die Domain kundenname3.de ein SSL Zertifikat erhalten muss. Das Zertifikat ist bereits auf dem Server eingerichtet, die Redirects im Webserver sind gesetzt, die Webserver-Konfiguration wurde angepasst und die BaseURL wurde für diese eine Domain für die Multidomainumgebung im Typo3 angepasst.

    Auf einmal steht man dann vor dem folgenden Problem: Im Backend ist über SSL der Seitenbaum nicht mehr sichtbar. Das liegt an den folgenden fehlenden Einträgen in der LocalConfiguration.php.

    'SYS' => array(
    	'trustedHostsPattern' => '.*\\.kundendomain1|.kundendomain2|.kundendomain3\\.de:*',
    	'reverseProxyIP' => '*.*.*.*',
    	'reverseProxySSL' => '*',
    ),

    Mit den Einträgen funktioniert nun auf jeden Fall mal das Backend mit SSL Zertifikat unter der Domain kundenname3.de – allerdings leiten die ersten Aufrufe der Domains im Frontend kundenname1.de und kundenname2.de jetzt auf SSL um. Somit werden die benötigten Sourcen wie z.B. CSS nicht korrekt ausgeliefert (das Zertifikat ist ja nicht für die Domain verfügbar), und die Webseiten ohne Zertifikat sehen aus wie … Kraut und Rüben. Alle weiteren Aufrufe, z.B. Klicks auf Unterseiten, sind jedoch kein Problem, da die baseURL weiterhin ohne SSL hinterlegt ist und den Links vorangestellt wird.

    Um das zu umgehen kann man sich mit folgender Einstellung in der LocalConfiguration.php helfen.

    ACHTUNG: Der Code sollte später in eine Extension ausgelagert werden, um ein Überschreiben durch Typo3 zu verhindern.

    Noch vor dem „return“ fügt man folgende Zeilen php-Code ein:

    $host = $_SERVER['HTTP_HOST'];
    switch( $host ) {
    	case 'kundenname1.de':
    	case 'www.kundenname1.de':
    	case 'kundenname2.de':
    	case 'www.kundenname2.de':
    		$reverseProxyIP = '';
    		$reverseProxySSL = '';
    		break;
    	default:
    		$reverseProxyIP = '*.*.*.*';
    		$reverseProxySSL = '*';
    		break;
    }

    Anschliessend werden die Einträge unter ‚SYS‘ angepasst.

    'SYS' => array(
    	'reverseProxyIP' => $reverseProxyIP,
    	'reverseProxySSL' => $reverseProxySSL,
    ),

    Mit dieser kleinen und einfachen Anpassung sollte nun sowohl das Typo3 Backend als auch das Frontend jeweils mit dem korrekten Protokoll ausgeliefert werden und funktionieren.

     

    Und damit das Ganze in der LocalConfiguration.php dann auch nicht zufälligerweise von Typo3 aus Versehen überschrieben wird packt man die Konfiguration am allerbesten in die ext_localconf.php einer eigenen Extension.

    $host = $_SERVER['HTTP_HOST'];
    switch( $host ) {
    	case 'kundenname1.de':
    	case 'www.kundenname1.de':
    	case 'kundenname2.de':
    	case 'www.kundenname2.de':
            $reverseProxyIP = '';
            $reverseProxySSL = '';
            break;
        default:
            $reverseProxyIP = '*.*.*.*';
            $reverseProxySSL = '*';
            break;
    }
    
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] = $reverseProxyIP;
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxySSL'] = $reverseProxySSL;

     

  • TYPO3: sendy-Integration mit formhandler

    Da ich jedoch für ein Projekt kurz zuvor eine kleine Extension geschrieben habe, welche sendy (siehe Beitrag „Newsletterversand mit sendy“) in Typo3 integriert, möchte ich diese Lösung dennoch hier vorstellen.

    Hinweis: Die Entwicklung von formhandler wurde die Tage (Stand Oktober 2016) eingestellt.

    Ziel ist die Erstellung eines Formulares mit formhandler und Speichern der eingegebenen Daten in der sendy-Datenbank, um später darüber sowohl die Abonnenten als auch den Newsletterversand zu verwalten.

    1. Erstellung eines Formulares
    2. Erstellung eines Plugins „formhandler_sendy“
    3. Einbindung ins Formular

    Erstellung eines Formulares

    Zunächst erstellt man mit formhandler ein simples Formular. Bei einer Newsletteranmeldung reicht das Feld Email für den Anfang vollkommen aus (auch aus datenschutzrechtlichen Gründen). Ich habe in meinem Projekt den Beispielcode von Formhandler mit sr_freecap genutzt, um Spam zu vermeiden.

    Erstellung eines Plugins für sendy

    Als nächster Punkt muss ein neues Plugin erstellt werden. Als Name habe ich formhandler_sendy gewählt.

    Struktur:

    formhandler_sendy

    • Classes
      • Validator
        • Sendy.php
        • sendy_signup.php
    • Configuration
      • TypoScript
        • setup.txt
    • ext_emconf.php

    ext_emconf.php:

    $EM_CONF[$_EXTKEY] = array (
      'title' => 'Formhandler Sendy Integration',
      'description' => '',
      'category' => '',
      'version' => '0.0.1',
      'state' => 'stable',
      'uploadfolder' => false,
      'createDirs' => '',
      'clearcacheonload' => true,
      'author' => 'Florian Freiburg',
      'author_email' => ' ',
      'author_company' => 'Schreiber & Freunde GmbH & Co. KG',
      'constraints' => 
      array (
        'depends' => 
        array (
          'typo3' => '6.2.0-8.0.999',
          'php' => '5.3.2-7.0.999',
          'realurl' => '2.0.14-3.0.0',
          'formhandler' => '',
        ),
        'conflicts' =>
            array(),
        'suggests' => 
        array (
        ),
      ),
    );

    Classes/Validator/Sendy.php:

    <?php
    
    namespace Vendor\Formhandler_Sendy\Validator;
    /*                                                                        *
     * This script is part of the TYPO3 project - inspiring people to share!  *
     *                                                                        *
     * TYPO3 is free software; you can redistribute it and/or modify it under *
     * the terms of the GNU General Public License version 2 as published by  *
     * the Free Software Foundation.                                          *
     *                                                                        *
     * This script is distributed in the hope that it will be useful, but     *
     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHAN-    *
     * TABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General      *
     * Public License for more details.                                       *
     *
     *                                                                        */
    
    /**
     * This finisher sends submitted form data to sendy (sendy.co).
     *
     * @author	Florian Freiburg
     * @package	Tx_Formhandler
     * @subpackage	Finisher
     */
    class Sendy extends \Typoheads\Formhandler\Validator\AbstractValidator {
    
    	/**
    	 * Validates the submitted values using given settings
    	 *
    	 * @param array &$errors Reference to the errors array to store the errors occurred
    	 * @return boolean
    	 */
    	public function validate(&$errors) {
    
    		//First validator returned errors, do nothing
    		if(!empty($errors)) {
    			return TRUE;
    		}
    
    		$params = array(
    			'sendyUrl' => $this->settings['sendyUrl'],
    			'listId' => $this->settings['listId']
    		);
    
    		require_once('sendy_signup.php');
    		$api = new sendy_signup();
    		$result = $api->add_subscription($params);
    
    		return $result;
    	}
    }
    ?>
    

    Classes/Validator/sendy_signup.php

    Sollten im Formular mehr Felder als nur die Emailadresse vorhanden sein, sollten diese natürlich mit aufgenommen werden. Weiterhin müssen diese Felder in der Empfängerliste im Sendy als Custom-Fields angelegt werden. Sendy kennt von sich aus nur die Felder Email und Name.

    <?php
    
    namespace Vendor\Formhandler_Sendy\Validator;
    
    class sendy_signup {
    
        public function __construct() {
    
        }
    
       public function add_subscription($params) {
    
            //------------------- Edit here --------------------//
           $sendy_url = $params['sendyUrl'];
           $list = $params['listId'];
    
           //------------------ /Edit here --------------------//
    
           //--------------------------------------------------//
           //POST variables
           $email = $_POST['sr_freecap']['email'];
    
           //subscribe
           $postdata = http_build_query(
               array(
                   'email' => $email,
                   'list' => $list,
                   'boolean' => 'true'
               )
           );
           $opts = array('http' => array('method'  => 'POST', 'header'  => 'Content-type: application/x-www-form-urlencoded', 'content' => $postdata));
           $context  = stream_context_create($opts);
           $result = file_get_contents($sendy_url.'/subscribe', false, $context);
           //--------------------------------------------------//
    
           return $result;
    
       }
    }
    
    ?>

    Configuration/TypoScript/setup.txt

    plugin.tx_formhandlersendy {
    
    }

     

    Einbindung ins Formular

    Die Einbindung ins Formular ist denkbar einfach. Hier wird einfach ein neuer Validator im Setup des Formulares hinzugefügt, welcher die sendy-Klasse anspricht. Die Werte in der Config des Validators für sendy sind natürlich anzupassen.

    plugin.Tx_Formhandler.settings.myExampleForm {
    
    	name = Newsletter registration with sendy.co
    
    	... your definitions ...
    	
    	# This block defines the error checks performed when the user hits submit.
    	validators {
    		1.class = Validator_Default
    		1.config.fieldConf {
    		
    			email.errorCheck.1 = required
    			email.errorCheck.2 = email
    			email.errorCheck.4 = emailExists
    			email.errorCheck.5 = betweenLength
                		email.errorCheck.5 {
    		                minValue = 8
                    		maxValue = 70
    			}
    			email.errorCheck.6 = containsAll
    			email.errorCheck.6 {
    			    words = @,.
    			}
    			freecapfield.errorCheck.1 = required
    			freecapfield.errorCheck.2 = srFreecap
    		}
    
    		# This special Validator will take care of adding the new subscriber. In case of an error, the according message will be displayed.
    	        2.class = Vendor\Formhandler_Sendy\Validator\Sendy
            	2.config {
    	            sendyUrl = http://path-to-your-sendy.com
            	    listId = sendy-subscriber-list-id
    	        }
    	}
    
    	... your finishers and loggers ...
    
        }
    }
    

     

    Da die Abmeldung über den versendeten Newsletter läuft (Abmeldelink in Email), wird an dieser Stelle auf ein weiteres Formular zu Abmeldung über Typo3 verzichtet. Prinzipiell ist dies ebenfalls über die Sendy-API möglich.

  • TYPO3: formhandler Version 2.4.0 & sr_freecap

    Wer die Kombination formhandler mit der Extension sr_freecap zur Vermeindung von Spam nutzt und die Tage ein Update der Erweiterung gemacht hat wird auf folgendes Problem stoßen:

    Es wird kein Captcha mehr angezeigt *PANIK*

    In Version 2.3.1 wurde von Formhandler simpel abgefragt, ob sr_freecap installiert ist. Anschliessend wurde sr_freecap instanziert, ein Captcha generiert und an das Template übergeben – auch wenn es gar nicht verwendet wird.

    Die neueste formhandler-Version 2.4.0 fragt zusätzlich noch ab, ob der Marker ###SR_FREECAP_### vorhanden ist. Diesen gibt es im Template aber nicht, auch nicht in den offiziellen Beispielen. Folglich kommt formhandler zum (Trug-)Schluss, kein Captcha generieren zu müssen.

    Da laut Homepage formhandler (leider) nicht mehr weiterentwickelt wird, habe ich mir mit einer kleinen Modifikation meines ausgelagerten Templates beholfen. In der Sektion von sr_freecap habe ich einen versteckten div untergebracht, welcher den Marker aus der Programmierung enthält.

    Das Captcha (Cache leeren!) danach sofort wieder korrekt angezeigt.

  • Arbeiten im Homeoffice – meine persönlichen Do’s and Dont’s

    In den Zeiten der Digitalisierung, verteilter Teams, besserer Kommunikationsmöglichkeiten und technischer Fortschritte nehmen viele Arbeiter gerne die Möglichkeit eines Heimarbeitsplatzes durch den Arbeitgeber an. Für Viele, die zum Beispiel eine längere Strecke zu pendeln haben, ist das eine reizvolle Möglichkeit 8 Stunden zu Arbeiten ohne den lästigen Arbeitsweg auf sich zu nehmen – und Sprit spart man dabei auch noch. Weiterhin kann man daheim recht ungestört von kollegialen Telefonaten oder Gesprächen konzentriert arbeiten.

    Der Heimarbeitsplatz, in den meisten Verträgen und Stellenanzeigen auch Homeoffice genannt, hat einige Stolperfallen und Ablenkungen parat. Wie ich diese Produktivitätskiller für mich persönlich eliminiert und somit eine für mich passende Arbeitssituation für daheim geschaffen habe, möchte ich hier aufführen.

    1. Aufgaben festlegen

      Wichtig für ein produktives Arbeiten sind die Aufgaben, die man im Homeoffice erledigen möchte. Diese sollte man sich im Idealfall bereits einen Tag vorher zurechtlegen beziehungsweise briefen lassen. Wenn man morgens erst schaut, was es zu erledigen gibt, kommt man unter Umständen schlechter an benötigte Informationen und hält eventuell auch die Kollegen mit Rückfragen von der Arbeit ab.

    2. Motivation

      Zunächst einmal sollte man sich selbst motivieren, daheim mit der selben Motivation an die Aufgaben zu gehen wie im Büro. Dem einen fällt das trotz „Hausanzug mit drei Sport-Streifen“ einfacher sich an den heimischen Schreibtisch zu setzen und zu arbeiten, als das einem anderen fällt.
      Ich persönlich versuche meine morgendliche Routine auch im Homeoffice weiter durchzuziehen. Für mich heisst das konkret: Pünktlich Aufstehen, Bad und Morgentoilette, Kaffee und ein kleines Frühstück, Nachrichten, Emails – und sofort die ersten Aufgaben. Wer zuerst zum Bäcker geht, Brötchen holt und in sonntaglicher Ruhe frühstückt wird schwerer in den Arbeitsmodus finden. Auch wenn man im Büro bestimmte Kleidung (Jeans und Hemd) trägt, kann es helfen das daheim wie gewohnt anzulegen.

    3. Der Zuhause-Faktor

      Den „Ich-bin-Zuhause-im-Feierabend“ Faktor, der einen von der Arbeit abhält, kann man ebenfalls relativ einfach abschalten. Am Abend zuvor oder vor direkt nach dem Aufstehen vor der Morgentoilette entferne ich alles Private (Rechnungen, priv. ToDo Listen, Zeitungen) vom Schreibtisch. Dieser darf zum Arbeitsbeginn keinerlei privaten Dinge enthalten, welche einen gleich zu Anfang von der Arbeit abhalten (Stichwort: „Och, die paar Rechnungen kann ich doch schnell noch…“).

    4. Der Haushalt

      Piepsender Trockner / Waschmaschine, die Spülmaschine, überfälliges Rückschneiden der Balkonpflanzen, Ausmisten der Lebensmittel. Alles Aufgaben, die man manchmal lieber erledigen würde als die aktuelle Aufgabe für die Arbeit. Den piepsenden Trockner schalte ich natürlich aus – aufgehängt wird, wenn ich wirklich eine Pause benötige – oder nach der Arbeit. Wenn man hier mit einer Aufgabe aus dem Haushalt anfängt, macht man oft direkt mit der nächsten weiter – und schon sind zwei Stunden oder mehr hinüber.
      Hier heißt es „eisern bleiben“ und weiterarbeiten.

    5. Pausen einlegen

      Auch bei der Arbeit von daheim aus muss man genau wie auch im Büro zwischendurch eine Pause einlegen, um den Akku aufzuladen und konzentriert zu bleiben. Egal wie kalt es draussen ist: Ihr braucht auch daheim frische Luft! Ich öffne bei mir im Arbeitszimmer und im gegenüberliegenden Zimmer die Fenster komplett und gehe mit einem frischen Kaffee kurz auf den Balkon.

     

  • TYPO3: formhandler per Typoscript einbinden

    Manchmal werden Formulare in Bereichen benötigt, die auf der gesamten Webseite die selbe Darstellung und Position haben. Dem Redakteur die Pflege zuzumuten ist etwas, das mir persönlich missfällt. Die Arbeit nehme ich ihm gerne ab. Somit muss/soll/kann man das Formular, welches man mit Formhandler angelegt hat, per Typoscript einbinden.

    Wie das funktioniert ist hier kurz beschrieben, ich gehe von einem existierenden Formular aus.

    Typoscript:

    lib.myform < plugin.tx_formhandler_pi1
    lib.myform.settings < plugin.Tx_Formhandler.settings.predef.meinFormular

    Fluid-Template:

    <f:cObject typoscriptObjectPath="lib.myform" />

     

     

  • Newsletterversand mit sendy

    Vor ein paar Tagen bin ich im Zuge eines Projektes über ein recht interessantes Tool gestoßen.

    Zuerst aber eine kleine Einführung:

    Für ein Projekt musste eine Newsletter-Lösung aufgesetzt werden. Als CMS kommt Typo3 zum Einsatz, der Versand erfolgt in regelmäßigen Abständen, Versandgröße ca. 10.000 (steigend).

    Klar, es gibt schon integrierte Lösungen, um über Typo3 einen Newsletter zu versenden – eine dieser Lösungen nennt sich direct_mail (zu finden im TER). Diese bietet auch einige Optionen, samt Double Opt In, Verwaltung der Abonnenten, terminierter Versand und Erstellung des Mailings direkt aus Typo3 mit seinen Inhaltselementen.
    Jedoch hat direct_mail den Nachteil, dass es mitunter recht lange dauern kann bis die Emails verschickt sind. Bei der Menge kann es auch schon mal passieren dass der Versand am Stück abbricht. Lässt man das über den lokalen Server geschehen, bietet sich ein Task an der die Emails alle 5 Minuten in 50er Paketen versendet. Somit sollte man auch auf keiner Blacklist wegen Spam-Verdachtest landen, und der Versand kann nicht abbrechen. direct_mail mit dem Versand über einen eigenen Mailserver ist kostenfrei.

    Alternativ kann man natürlich auch einen professionellen Dienst dazu verwenden. Am bekanntesten sind hier wohl die beiden Dienste Cleverreach und Mailchimp. Jedoch können hier schnell Kosten auf einen zukommen. Beide Dienste bieten generell auch ein Gratismodel an. Bei Cleverreach heisst das 250 Empfänger verwalten und 1.000 Email pro Monat frei. Bei Mailchimp kann man bis 2.000 Empfänger verwalten und hat 12.000 Emails frei. Alles darüber hinaus verursacht Kosten, die recht unterschiedlich ausfallen können. Wenn man darüber hinaus versenden möchte hat man die Möglichkeit, Kontingente einzukaufen oder eine „Flatrate“ zu buchen.

    Es gibt aber noch einen weiteren Dienst, den wenige auf dem Schirm haben: Amazon SES (Simple Email Service). Für diesen Dienst hat sendy (sendy.co) ein kleines, aber mächtiges Tool geschaffen. Die Empfängerlisten und Mailings können auf dem eigenen Server in der eigenen Datenbank verwaltet werden, während der Versand über Amazon abläuft – sendy ist eine sogenannte „self hosted newsletter application“.

    Ausgehend von einem „normalen“ Firmennewsletter mit 10.000 Abonnenten und monatlich einmaligem Versand komme ich dabei auf folgende Kosten

    Mailchimp
    ca. 80€

    Cleverreach
    ca 90$

    Sendy/AmazonSES
    1$

    Sendy kostet einmalig 59$ und muss noch installiert und konfiguriert werden. Für 79$ bietet sendy einen Installationsservice an. Die Installation habe ich selbst auf einem lokalen Server ausgeführt und getestet, was innerhalb von zwei Stunden mit Doku einlesen, Nachinstallieren von Serverkomponenten, Einrichtung Datenbank und Test erledigt sein kann.

    In weiteren Artikeln werde ich die Installation unter MariaDB und nginx sowie die Integration in Typo3 mittels einer eigenen Extension zeigen.

  • woocommerce: Related products nur nach Tags anzeigen

    Über das beliebte E-Commerce Plugin woocommerce kann man sich in der Detailansicht verwandte Produkte anzeigen lassen. Woocommerce zieht sich in der Detailansicht für verwandte Produkte alle Produkte, die in der selben Kategorie liegen oder die selben Tags (Schlagwörter) gepflegt haben. Weiterhin kann man das Limit, wie viele verwandte Produkte angezeigt werden sollen, verändert werden. Alles in Allem kann man sagen: Äusserst Redakteursfreundlich, da es einfach angezeigt wird und 99% von selbst erledigt wird bzw. erledigt ist.

    Wenn man aber eine Kategorie hat, in der sich alle Produkte befinden müssen, passt das oben aufgeführte Verhalten „verwandte Produkte aus Kategorie und Tags“ leider nicht mehr. Als Beispiel ein kleiner Kategoriebaum:

    • Alle Produkte
      • Hosen
        • Jeans
        • Anzughosen
      • Hemden
        • Kurzarm
        • Langarm
      • Spiritousen
        • Gin
        • Rum

    Da alle Produkte in der obersten Kategorie „alle Produkte“ liegen, werden zum Beispiel bei der Anzeige einer Jeans unter Umständen auch Produkte aus der Kategorie „Gin“ angezeigt. Das passt nicht zusammen.

    Nach einer etwas Recherche und Prüfung des Codes bin ich auf folgenden Filter gestoßen:

    add_filter( 'woocommerce_get_related_product_cat_terms', '__return_empty_array' );

    In der Funktion get_related der Klasse WC_Product (plugins/woocommerce/includes/abstract/abstract-wc-products.php) kann man durch den Filter eingreifen und so die Produktkategorien leeren. Wenn jedoch der Cache für die verwandten Produkte vorhanden ist oder nicht genug Produkte vorhanden sind, sucht Woocommerce wieder Produkte aus der selben Kategorie heraus. Somit kann es sein dass hier bei der Jeans wieder ein Gin, ein Rum oder ein sonstiges nicht passendes Produkt erscheint.

    Es gibt aber noch eine weitere Stelle, an welcher man eingreifen kann. Diese befindet sich in der selben Klasse und heisst build_related_query. Hier kann man über den Filter

    add_filter( 'woocommerce_product_related_posts_relate_by_category', function() {
    	return false;
    } );

    recht einfach, schnell und sicher die Kategorien zurücksetzen. Wenn das erledigt wurde sollte man noch die gecachten verwandeten Produkte leeren, was ich über die Console mit WP-CLI erledigt habe:

    wp transient delete-all

     

  • TYPO3: Extension vhs – Viewhelper für FLUID

    Mit Fluid lässt sich viel Logik aus Typoscript und PHP in die Frontend-Templates auslagern. Aber auch Fluid sind Grenzen gesetzt. Für Funktionalitäten, die nicht nativ in Fluid vorhanden sind, gibt es die TYPO3 Erweiterung vhs (das External Manual der Erweiterung findet ihr hier). Diese wurde 2012 das erste Mal im TER bereitgestellt und zählt mittlerweile über 30.000 Dowloads. Ich setze die Erweiterung mittlerweile selbst in fast jedem Projekt ein.

    Einmal über den Extensionmanager installiert, lässt sie sich fast sofort im Template verwenden. Hierzu muss man lediglich den Viewhelper über den Namespace im Template bekannt machen. Das kann man auf zwei Arten erledigen.

    Einbindung 1:

    {namespace v=FluidTYPO3\Vhs\ViewHelpers}
    
    <!-- Your code -->

    Einbindung 2:

    <div xmlns:v="http://typo3.org/ns/FluidTYPO3/Vhs/ViewHelpers"
         v:schemaLocation="https://fluidtypo3.org/schemas/vhs-master.xsd">
    	<!-- Fluid goes here -->
    </div>

    Und schon kann man loslegen. Aber für was braucht man die Erweiterung? Für alles, was man sonst über Typoscript lösen und erst dann dem Template übergeben würde. Aus meiner Sicht lassen sich manche Anforderungen nicht so leicht im Typoscript erledigen. Weiterhin ist aus meiner Sicht die Lesbarkeit von FLUID für Einsteiger besser geeignet.

     

    Beispiel 1:

    Gruppierte Ausgabe einer Liste

    Im folgenden Beispiel soll eine Nachrichtenliste (aus der Erweiterung „news“) ausgeben werden. Diese Liste soll durch den jeweiligen Monat, in dem sie einstellt wurde, getrennt werden. Also eine klassische Gruppierung nach Monat.

    Dazu merkt man sich der Einfachkeit halber einfach den Monat der Nachricht und vergleicht ihn mit der vorherigen. Geht aber nicht. Die nächste Variante wäre es den Monat der aktuellen News in einer Variablen zu speichern und beim nächsten Durchlauf mit dem aktuellen Monat zu vergleichen. Geht so aber auch nicht (eine Korrektur falls diese Aussagen nicht stimme ist erwünscht). Dafür kann vhs aber Variablen anlegen, speichern und abrufen.

    {namespace v=FluidTYPO3\Vhs\ViewHelpers}
    <v:variable.set name="oldMonth" value="00"></v:variable.set>
    <f:for each="{paginatedNews}" as="newsItem" iteration="iterator">
        <f:if condition="{oldMonth} != {f:format.date(date: newsItem.datetime, format: 'F')}">
            <div class="event-month row">{f:format.date(date: newsItem.datetime, format: 'F')}</div>
        </f:if>
        <f:render partial="List/EventsItem" arguments="{newsItem: newsItem,settings:settings,iterator:iterator}" />
        <v:variable.set name="oldMonth" value="{f:format.date(date: newsItem.datetime, format: 'F')}"></v:variable.set>
    </f:for>

    Zunächst wird per variable.set der Monatsname auf 00 gesetzt.

    Innherhalb der for-each-Schleife wird nun der Monat der zuvor gesetzten Variablen mit dem Monat der aktuellen Nachricht verglichen. Ist dieser nicht identisch, wird der Monatsname ausgegeben. Anschliessend wird die Variable auf den Monat der aktuellen News.

     

    Beispiel 2:

    Ausgabe der Inhalte aller untergeordneten Seiten (Onepager)

    Mit ein paar Zeilen lässt sich ein einfacher Onepager einrichten, der alle Inhalte der (in erster Ebene) untergeordneten Seiten ausgibt.

    {namespace v=FluidTYPO3\Vhs\ViewHelpers}
    <v:page.menu pageUid="{pageUid}" as="sections">
    
    </v:page.menu>

    pageUid übermittelt dem Viewhelper, welche Seite aus dem Menu ausgegeben werden soll. Das kann man statisch hinterlegen oder über Typoscript dynamisch an die variables des Templates übergeben.

    Nun durchläuft man in einer Schleife alle Menupunkte, zur Kontrolle kann man schon mal den Titel der jeweiligen Seite ausgeben.

    {namespace v=FluidTYPO3\Vhs\ViewHelpers}
    <v:page.menu pageUid="{pageUid}" as="sections">
        <f:for each="{sections}" as="section" iteration="iteration">
            <div class="section" id="section{section.uid}">
                <div class="content">
                    <h2>{section.title}</h2>
                </div>
            </div>
        </f:for>
    </v:page.menu>

    Jetzt benötigt man nur noch den in der jeweiligen Seite gepflegten Inhalt. Auch hier gibt es ein kleines Helferlein namens „content.render“ in der Erweiterung. Durch Übergabe der pageUid werden die Inhalte der Seite ausgelesen und gerendert. In meinem Fall hat das Template nur eine Spalte im Backend, weswegen keine zusätzlichen Argumente übergeben werden.

    Das fertige Snippet kann dann so aussehen:

    {namespace v=FluidTYPO3\Vhs\ViewHelpers}
    <v:page.menu pageUid="{pageUid}" as="sections">
        <f:for each="{sections}" as="section" iteration="iteration">
            <div class="section" id="section{section.uid}">
                <div class="content">
                    <h2>{section.title}</h2>
                    <v:content.render pageUid="{section.uid}" />
                </div>
            </div>
        </f:for>
    </v:page.menu>

     

    Natürlich kann die Erweiterung noch Vieles mehr als nur die beiden oben genannten Beispiele – ein Blick in die Dokumentation der Extension lohnt sich eigentlich immer.

  • TYPO3: Variablen in FLUID an ein Partial übergeben

    Durch Fluid können Templates wunderbar in Partials unterteilt werden, was der allgemeinen Übersicht sehr dienlich ist. Weiterhin können Partials in mehreren Templates wiederverwendet werden, ohne jedesmal den benötigten „Grund“-HTML-Code zu kopieren (ja, das funktioniert in TemplaVoila auch recht gut, aber darauf gehe ich hier nicht tiefer ein).

    Aber wie bekommt man seine Variablen aus dem Typoscript und dem Haupttemplate nun in das entsprechende Partial? Hierfür bietet der Viewhelper das arguments-Attribut. So kann man mit dem folgenden Beispiel-Code alle Variablen an das Partial „menu“ übergeben:

     
    <f:render partial="menu" arguments="{_all}" />
    

     

  • LESS: Viewport Größen mit Pixelangaben verrechnen

    Wenn man mit calc die viewport height bzw. viewport width mit einer Pixelangabe verrechnen möchte, stolpert man oft über eine „inkorrektes“ Ergebnis

    Bsp: min-height: calc( 100vh – 90px) ergibt min-height: calc(10vh)

    Der Grund hierfür ist dass LESS die Viewport Höhe nicht korrekt erkennt und berechnen kann.

    Lösung: min-height: calc( ~“100vh“ – 90px)

    Durch escapen kann LESS nun den korrekten Wert berechnen und gibt keinen CSS Fehler aus.

    Die Pixelangabe lässt sich natürlich auch durch eine less-Variable ersetzen.

  • SVGs im Hintergrund: Paint it black – oder in jeder anderen beliebigen Farbe

    Früher hat man gerne mit png oder Sprites für die Darstellung von Icons auf einer Webseite gearbeitet, dann kam FontAwesome mit einer großen Sammlung standarisierter Icons – und manchmal schafft man halt auch mit SVG.

    Die Farbe von inline eingebundenen SVG lassen sich ja recht einfach anpassen:

    Bsp. für ein SVG mit direkter Einbindung in HTML, das SVG stellt eine Lupe dar.

    <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve">
    <path class="st0" d="M63.3,59.8L52.5,49l0.1-0.2c1.9-2.7,3-5.9,3-9.3c0-8.8-7.2-16-16-16s-16,7.2-16,16s7.2,16,16,16
    	c3.3,0,6.5-1,9.3-3l0.2-0.1l10.8,10.8c1,1,2.5,1,3.5,0l0,0c0.5-0.5,0.7-1.1,0.7-1.7C64,60.9,63.7,60.2,63.3,59.8z M39.6,52.2
    	c-7,0-12.6-5.7-12.6-12.6S32.6,27,39.6,27s12.6,5.7,12.6,12.6S46.5,52.2,39.6,52.2z" style="
        fill: #000000;
    "></path>
    
    </svg>

    CSS

    svg .st0 {
        fill: #FF0000;
    }

    Und schon ist die Lupe rot statt schwarz.

    Möchte man SVG jedoch als Hintergrundgrafik nutzen und bei Hover die Farbe ändern, hat man keinen Zugriff auf das SVG an sich. Viele weichen an der Stelle auf die klassische Einbindung aus und erledigen dies durch Icons (einzelne oder Sprites). Das hat jedoch den Nachteil dass man jeden Status vorsehen muss.

    Dabei kann man dies recht einfach mit LESS durch ein mixin erledigen. Denn LESS kann

    .icon-fill(@src, @fill) {
       @data-uri: data-uri('image/svg+xml;charset=UTF-8', "@{src}");
       @replace-src: replace("@{data-uri}", "fill\%3D\%22\%23[\w]{3,6}\%22", escape('fill="@{fill}"'), "g");
       background-image:e(@replace-src);
    }

    LESS

    a {
       &:before {
          .icon-fill("../images/icons/ICON.svg", "#FFFFFF");
       }
       &:hover {
          &:before {
             .icon-fill("../images/icons/ICON.svg", "#000000");
          }
       }
    }

    Übergeben wird nur der Pfad zum SVG sowie die gewünschte Farbe. In oben aufgeführten Fall ersetzt LESS bei der Generierung der späteren CSS Datei sämtliche Farbangaben im SVG, welche im „fill“-Attribut stehen, zunächst durch #FFFFFF (weiß). Für den :hover-Status werden diese dann durch #000000 (schwarz) ersetzt. Somit muss man nur eine einzige Grafik erstellen. Die gewünschte farbliche Anpassung kann dadurch extrem schnell ausgeführt werden. Man trägt die gewünschte Farbe ein, kompiliert mit gulp oder grunt, fertig.

    Wenn die Farbangaben als CSS im SVG hinterlegt sind, funktioniert das oben aufgeführte Script nicht – hierfür muss es angepasst werden.

    Gefunden bei zslabs.com

     

  • Zählen mit CSS

    Mit reinem CSS3 lassen sich Elemente durchzählen – eine Option die man beim erstellen einer Webseite nicht ausser Acht lassen sollte, wenn es z.B. darum geht Listen für mobil anzupassen.

    Hierbei ist zu beachten, dass sich die Styles hier nicht auf das Elternelement basierend auf der Anzahl der Kindelemente anwenden lassen. Man kann jedoch mit CSS auf Kindelemente basierend auf der Anzahl ihrer Geschwister zugreifen.

    So lassen sich z.B. Breiten anhand der Anzahl der Elemente innerhalb eine Liste festlegen, ohne dies umständlich durch JS berechnen zu müssen.

    Im nachfolgenden Beispiel werden die Elemente einer Liste in der Breite verändert. In der Voreinstellung haben alle Listenelemente die Breite 33.3333%. Das sieht jedoch bei 5 Listenpunkten unschön aus, da zwei Elemente umbrechen. Hier wäre es wünschenswert wenn das erste und letzte Element jeweils 100% Breite einnehmen und dadurch eine 1-3-1 Darstellung erreicht wird.

    HTML:

    <ul>
        <li>Element 1</li>
        <li>Element 2</li>
        <li>Element 3</li>
        <li>Element 4</li>
        <li>Element 5</li>
    </ul>

    CSS

    ul {
        li {
            width: 33.33333%;
        }
    }

     

    Durch nachfolgendes CSS werden die Breiten des ersten und letzten Elements in der Liste angepasst, wenn diese fünf Listenpunkte enthält.

    ul {
        > *:nth-last-child(n+5) ~ *:last-child,
        > *:nth-last-child(n+5):first-child {
            width: 100%;
        }
    }

    Sollte die Liste mehr oder weniger Einträge enthalten muss das CSS natürlich angepasst werden.

     

    Ein weiterer Anwendungsfall wäre die Einschränkung einer langen Liste. Üblicherweise erledigen das viele per Javascript. Wenn man jedoch auch z.B. eine Animation bei der ersten Darstellung der Listenpunkte verzichten kann, ist folgendes CSS hilfreich:

    .list .item {
        display: none;
    }
    
    .list .item:nth-child(-n+5){
        display:block;
    }

    Zunächst werden alle Einträge ausgeblendet. Der Selektor :nth-child(-n+5) setzt die ersten fünf Einträge wieder sichtbar.

     

  • JS: Swiper von idangero.us – mobil und touchfreundlich …

    … jedoch mit einer kleinen Macke.

    Generell bin ich sehr begeistert von Swiper und setze diesen auch sehr gerne und oft ein. Er ist auch in eigenen Plugins schnell eingebunden und einsatzbereit, unkompliziert, einfach zu anzupassen, mobil- und touchfreundlich.

    Bei einem Projekt sollte ein OnePager mit WordPress umgesetzt werden. Die einzelnen Inhalte, also Posts und Pages, sollten als eine Art Galerie mit dem Swiper navigierbar gemacht werden. Der Grund hierfür war es Platz zu sparen und langes Nach-unten-Scrollen für den User zu verhindern. Statt dessen sollten die Unterseiten eines Menupunktes horizontal gescrollt werden können, die Hauptpunkte normal in vertikaler Richtung. Somit mussten auf einer Seite gleich mehrere Swiper mit unterschiedlichen Darstellungsoptionen eingebunden werden.

    Strukturell soll das dann so aussehen:

    • Seite 1
      • Unterseite 1-1
      • Unterseite 1-2
      • Unterseite 1-3
    • Seite 2
    • Seite 3
      • Unterseite 3-1
      • Unterseite 3-2

    und so weiter …

    Soweit eigentlich kein Problem. Die Beispielseite von Swiper stellt ja auch mehrere Galerien auf einer Seite dar.

    Also habe ich mein Theme (zum Einsatz kam hier das Sage-Theme) aufgesetzt, mir die Ausgabe auf einen OnePager mit Swiper zur Navigation gemünzt. Weiterhin hatte ich bei den unterschiedlichen Swipern, je nach gewähltem Template, noch zusätzliche Attribute hinterlegt um diese unterschiedlich steuern zu können (Anzahl einzelner Bilder je Swipe, Geschwindigkeiten, Start-Delay etc.).

    Die beschriebene Einbindung laut Dokumentation funktioniert jedoch nicht mit mehreren Gallerien. Alle Swiper haben gleichzeitig „geswiped“, stellenweise wurden Einstellungen überschrieben.

    Nach kurzer Prüfung habe ich dann festgestellt, dass die Beispielseite von Swiper jede Galerie als iFrame eingebunden hat. Eine einfache und schnelle Lösung, die zusätzlich in Verbindung mit Lazyload noch eine schnelle Ladezeit garantiert – aber nichts für mein Projekt 🙂

    Mit dem folgenden Javascript konnte ich hierbei Abhilfe schaffen: Die vorhandenen Swiper werden in ein Array gelegt und können somit einzeln initiiert und gesteuert werden.

    $swipers = jQuery('.swiper-container');
    if($swipers.length) {
      var $swiperArray = [];
      $swipers.each(function(index) {
        $swiper = jQuery(this);
        $swiper.addClass('s' + index);
        var swiperIntervall = 6000,
        swiperLoop = true,
        swiperAutoplayStopOnLast = false,
        swiperFreeMode = false,
        swiperSpeed = 800;
     
        $swiperArray[index] = $swiper.swiper({
          loop: swiperLoop,
          autoplay: swiperIntervall,
          autoplayStopOnLast: swiperAutoplayStopOnLast,
          freeMode: swiperFreeMode,
          speed: swiperSpeed,
          nextButton: $swiper.find('.swiper-button-next'),
          prevButton: $swiper.find('.swiper-button-prev'),
          onInit: function() {
     
          }
        });
      });
    }