$discardedLessThemes vs. Shopware: Es könnte so einfach sein

Ich mag das Theme-Konzept von Shopware ja eigentlich. Es gibt ein Bare-Theme, das außer HTML nichts enthält. Kein Styling, kein Javascript, einfach nur reines HTML.
Darauf aufbauend gibt es ein Responsive-Theme das, wie der Name schon sagt, responsive ist und über Variablen im Backend farblich auch ein bisschen angepasst werden kann. Das ist für den Wohnzimmerhändler sicherlich toll und man sieht auch mittlerweile unzählige Shops, die das Responsive-Theme nutzen und außer Logo und Farbe nichts geändert haben. Kann man machen, ist dann halt kacke.

Für unsere Ansprüche ist das natürlich nichts. Wir haben ein komplett selbst gestaltetes Theme, das auch nach allen Regeln der Kunst umgesetzt werden soll. Und da es so ganz anders ist als das Default-Theme wäre es natürlich gut, wenn man nicht den ganzen CSS-Ballast, den das Responsive-Theme mitbringt, rumschleppen müsste.

Weil sind wir doch mal ehrlich: Shopware 5 und damit die Grundlage des Responsive-Themes ist von 2015, der letzte Commit auf das Repo von pocketgrid, welches das Responsive-Theme nutzt, ist sogar noch ein Jahr länger her. Mittlerweile haben wir echtes CSS Grid und es wäre fahrlässig, das nicht zu nutzen.

Nur: Der Weg von

zu

ist ganz schön steinig und unnötig beschwerlich.

Vom Bare-Theme erben

Funktioniert total gut. Also bestimmt. Wenn man einen frisch installierten Shop hat ohne ein einziges Plugin. Das dürfte aber eher die Ausnahme sein. Schon bei einem Shop, der lediglich das PayPal-Modul installiert hat, scheitert das Kompilieren des Themes:

Während der Bearbeitung von Shop "Demoshop" ist ein Fehler aufgetreten: variable @tabletViewportWidth is undefined in file custom/plugins/SwagPaymentPayPalUnified/Resources/views/frontend/_public/src/less/_modules/index/sidebar.less in sidebar.less on line 11, column 30 09| } 10| 11| @media screen and(min-width: @tabletViewportWidth) { 12| .paypal--sidebar { 13| .unitize(margin-bottom, 20); 14|

Öh ja, das ist so semi-gut. Kann man prinzipiell nachrüsten, sobald man aber noch ein paar komplexere Plugins am Start hat wie beispielsweise die Advanced Promotion Suite knallt es auch bei den Farbwerten, die man eigentlich über das Backend konfigurieren kann (aber eben nicht beim Bare-Theme).

$discardedLessThemes beim Responsive-Theme

Also vom Responsive-Theme erben. Seit Shopware 5.4 gibt es da die Möglichkeit, sowohl in den Vererbungspfad des CSS/LESS als auch das Javascripts einzugreifen. Für LESS/CSS mittels discardedLessThemes, für das Javascript mittels discardedJavascriptThemes.

Da also das Responsive-Theme eintragen und kompilieren und alles ist super. Natürlich nicht. Auch hier hat man die Rechnung ohne die Plugin-Autoren gemacht. Direkt als erstes fliegt einem wieder der Tablet-Viewport um die Ohren. Hat man sich dafür ein eigenes LESS-File angelegt knallt zumindest in unserem Fall das Amazon-Pay-Plugin. Und zwar mehrfach aufgrund des Zugriffs auf jetzt nicht mehr vorhandene Definitonen aus dem Bereich der Buttons. Also auch hier eigene LESS-Files anlegen und leere Definitionen dort eintragen.
So geht das Spielchen, das einem Marathon gleicht, munter weiter, bis alle Plugins, die noch nichts davon gehört haben, dass die Vererbung mittlerweile abschaltbar ist, zufrieden gestellt sind. Erst dann hat man für einen Shop mit diversen Plugins auch endlich eine wirklich CSS-freie Darstellung. Also quasi Bare-Theme ohne die Probleme mit den fehlenden Farbdefinitionen.

Nur: CSS-frei ist das ganze deswegen noch lange nicht, da sorgt schon jedes Plugin dafür, dass da wieder ein bisschen was zusammen kommt, aber zumindest lässt sich so der Umfang des CSS schon mal auf ca. 2500 Zeilen reduzieren, während der gleiche Shop mit Responsive-Theme auf 25.000 Zeilen kommt. Es ist noch ein weiter Weg…

Wie Vollpfosten-Versender die Kunden in die Arme von Amazon treiben…

Das ist quasi ne Fortsetzung zu Der deutsche E-Commerce versagt schon bei den Basics. Wird wohl ein Mehrteiler.

Braucht mal wieder jemand Futter, warum Amazon so beliebt beim Kunden ist? Weil viele andere Versender einfach Vollpfosten sind. Hier gleich zwei Vollpfosten des Monats August.

Ich hatte Bedarf nach mehr Ventilatoren. War ja auch mal heiß dieses Jahr. Erste Bestellung über zwei Stück des gleichen Typs am 26. Juli aufgegeben bei nem großen Elektroladen in Karlsruhe, Ware angeblich sofort lieferbar. In der Bestellbestätigung steht dann plötzlich 1. August als Liefertermin. OK, die bringen den wohl persönlich, aber egal, ist ja die nächste Woche auch noch heiß.
Meine Zahlung über PayPal wurde dann am nächsten Tag per Mail bestätigt. Kann man bei ner sofortigen Zahlungsmethode natürlich machen, ist mit einem Tag Verzug aber halt lächerlich und zeigt wie schlecht das Warenwirtschaftssystem hinten dran sein muss.
Sollte man daran noch Zweifel haben: Vier Tage nach Bestelleingang kommt ne E-Mail, dass man auch den Termin am 1. August leider reißen wird, neuer Liefertermin wäre der 13. August, man offeriert aber eine Teillieferung. Ich antworte sofort und frage wie die Teillieferung denn aussehen wird. Man braucht zwei Arbeitstage um mir zu antworten, dass das eine Standardmail vom System sei und eine Teillieferung anders als offeriert doch nicht möglich wäre. Wie schlecht bitte kann ein Warenwirtschaftssystem eigentlich sein, das vor Mailversand nicht erkennt, dass auch eine Teilmengenlieferung nicht möglich ist? Ich antworte erneut direkt, bekomme aber keine Antwort, sondern eine Rechnung. Am nächsten Tag steht ohne Versandmitteilung die Ware plötzlich auf dem Hof.
Noch einen Tag später hat sich dann sogar der Twitter-Account des Händlers, den ich zwischenzeitlich angeschrieben hatte, gemeldet. Auch mit einer Reaktionszeit von zwei Tagen. Der Händler verkauft auch über Amazon, es ist mir ein Rätsel, wie er dort die Antwortzeiten einhält während er auf allen anderen Kanälen 48 oder mehr Stunden benötigt.

Neuer Händler, gleiche Ware

Ich bin aber noch nicht fertig. Mit dem neu avisierten Liefertermin für den 13. August habe ich direkt eine Ersatzbeschaffung vorgenommen. Am 31. Juli habe ich also bei einem eBay-Händler, der mit 17.000 Bewertungen auch eher nicht aus dem Wohnzimmer versendet, eine weitere Bestellung aufgegeben, Liefertermin zwischen dem 2. und 3. August. Nachdem am 2. August dann ja aber überaschenderweise die Ware vom ersten Händler eingetroffen ist und die vom eBay-Händler noch nicht versendet gemeldet war, habe ich dort versucht den Kauf abzubrechen:

Hiermit widerrufe ich den Kauf. Der Artikel wird nicht mehr benötigt. Da er auch noch nicht versendet ist bitte Zahlung wieder auf mein PayPal-Konto gutschreiben.

Die Antwort irritierte mich dann aber:

Bitte lehnen Sie die Lieferung ab, wenn Sie bei Ihnen eintrifft, zurückhalten können wir sie leider nicht mehr.

Aha, also doch schon versendet? Einen Tag haben sie ja noch um den versprochenen Liefertermin einzuhalten. Und natürlich kam am 3. August keine Ware. Und auch die nächsten Tage nicht, weshalb ich am 6. August erneut schrieb:

Bislang ist kein Paket hier eingetroffen, obwohl das schon bis spätestens Freitag der Fall hätte sein sollen und es ja angeblich auch schon versendet ist. Bitte lassen Sie mir die Sendungsnummer zukommen, es ist etwas müßig, jeden Tag den DHL-Fahrer abpassen zu müssen, damit er das Paket ja nicht irgendwo in der Nachbarschaft abgibt, wo niemand was von der Annahmeverweigerung weiß.

Die Antwort einen Tag später ist ein Musterbeispiel für extrem schlechte Kundenkommunikation:

Leider können wir momentan aufgrund der Hitzewelle nicht alle Tickets / Anfragen zeitnah beantworten. Wir versichern Ihnen, dass wir mit Hochdruck daran arbeiten Ware zu versenden und Ihre Anfragen schnellstens zu beantworten.

Bitte geben Sie uns daher ein, zwei Tage Zeit. Es wird nichts vergessen und wir sind uns sicher alle Ihre Anfragen nach dieser Zeit zu Ihren Zufriedenheit beantworten zu können.

Wie jetzt? Die Hitzewelle ist schuld, dass auf eBay offensichtlich falsche Lagerbestände standen, E-Mails nicht zeitnah beantwortet werden und der Verpackungsbereich gleich hitzefrei hat? Also bei uns wurde durchgearbeitet in allen Abteilungen und ja, wir hatten auch warm.

Wir schreiben mittlerweile übrigens den 7. August. Und es geht nach wie vor um die Ware, deren Stornierung ich am 1. August angefragt hatte, die aber zu dem Zeitpunkt angeblich schon nicht mehr zurückgehalten werden konnte. Am 9. August habe ich dann sowohl von eBay als auch vom Händler eine Versandbestätigung erhalten. Was in beiden Fällen fehlt: Eine Trackingnummer. Richtig krotesk wird es bei der Mail des Händlers:

Die Stellen, an die das Warenwirtschaftssystem eigentlich die Trackingnummer einfügen sollte sind einfach leer. Und ich habe den Screenshot absichtlich etwas größer gemacht, damit man sieht, dass der Link zum Adobe Reader ein Link ist, man das aber beim Trackinglink nicht hinbekommen hat.

Die Ware ist übrigens immer noch nicht da. Es scheint als hätte da ein Dropshipper seine Lagerbestände nicht im Griff. Das auf dem Rücken des Kunden auszutragen ist aber halt auch ein Unding. Eine Verschärfung des Tons beim Händler schien mir daher angebracht:

Ich werde hier seit 14 Tagen verarscht und hingehalten, die Ware ist bis heute nicht angekommen. Ich verlange umgehend das Geld zurück!

Das hat dann dazu geführt, dass binnen zwei Tagen das Geld da war. Kommentarlos. Ware ist nie gekommen. Ich wurde also nicht nur verarscht, ich wurde nachweisliche angelogen was den Versendet-Status der Ware angeht.

Umziehen Part 5: Die Schmerzen mit dem SSL

Das Webseiten nur noch über HTTPS erreichbar sind ist mittlerweile eher die Regel denn die Ausnahme. Zeit, die alte Serie aus 2012 noch mal aufzugreifen, denn durch die Omnipräsenz von HTTPS kommen plötzlich neue Probleme ins Spiel die es vorher so noch nicht gab.

Das Szenario dürfte der Standard sein:
Man hat einen Webshop und betreibt diesen auf www.example.com. Für genau diese Domain besitzt man auch ein Zertifikat, evtl. sogar mit Extended Validation. Außerdem hat man noch, um Trittbrettfahrer abzuhalten, example.org und example.net registriert.
Und alles was nicht www.example.com ist leitet man entsprechend um. Das ist bei HTTP auch kein wirkliches Problem und über zwei Zeilen in der .htaccess zu lösen:

1
2
3
4
5
6
<IfModule mod_rewrite.c>
RewriteEngine On
 
RewriteCond %{HTTP_HOST} !^www.example.com$
RewriteRule ^(.*)$ http://www.example.com/$1 [L,R=301]
</IfModule>

Kommt jetzt HTTPS ins Spiel ist es vorbei mit der Einfachheit. Denn https://example.com/ und https://www.example.com/ sind technisch nun mal zwei verschiedene URLs, können völlig unterschiedliche Inhalte zeigen und brauchen für eine fehlerfreie Funktionsweise entsprechend für jede URL ein Zertifikat.
Safari und Firefox quittieren den Umleitungsversuch von https://example.com/ nach https://www.example.com/ mit dem Zertifikat von https://www.example.com/ korrekterweise mit einer Fehlermeldung – Chrome erstaunlicherweise nicht (IE und Edge wurden nicht getestet).

Die Kosten für das eigentlich unnötige Zertifikat können sich schnell aufsummieren, neben dem Zertifikat selbst (evtl. mit EV) braucht man unter Umständen nämlich auch noch eine weitere IP-Adresse, weil der Provider aus nicht nachvollziehbaren Gründen noch immer kein SNI unterstüzt.

Will man also allen Traffic ohne www-Prefix unabhängig von HTTP und HTTPS fehlerfrei umleitung brauch man eine Lösung und die Rettung kommt, mal wieder, von Uberspace. Denn dort gibt es SNI, IPv6 und Zertifikate von Let’s Encrypt. Man muss also nur einen Uberspace klicken, mit uberspace-add-domain die gewünschten Domains hinzufügen und dann für diese nach dieser Anleitung Zertifikate erzeugen. Dann stellt man nur noch die DNS-Einträge für die umzuleitenden URLs auf Uberspace um und leitet alles ohne Zertifikatsfehler richtig um. Die .htaccess ist quasi identisch zu der oben, nur dass das Umleitungsziel jetzt natürlich die HTTPS-URL ist.

1
2
3
4
5
6
<IfModule mod_rewrite.c>
RewriteEngine On
 
RewriteCond %{HTTP_HOST} !^www.example.com$
RewriteRule ^(.*)$ https://www.example.com/$1 [L,R=301]
</IfModule>

Bleibt noch die Frage: Braucht man das? Niemand dürfte das händisch falsch eingeben und dann an einer Fehlermeldung hängenbleiben. Nun, selbst der Fall dürfte hin und wieder vorkommen, aber es braucht nur jemand einen falschen Link zu setzen damit Besucher mit entsprechenden Fehlermeldungen konfrontiert werden. Und auch die Search Console von Google meckert das falsche Zertifikat an (man muss ja beide Properties anlegen um die bevorzugte Domain in den SERPs einstellen zu können). Um so unverständlicher, dass Chrome da einfach drüber „hinwegsieht“.

Der deutsche E-Commerce versagt schon bei den Basics

Ich bestelle derzeit sehr viel nicht bei Amazon weil ich mich über die etwas aufgeregt habe, die Kundenorientierung dort ist auch nicht mehr das was sie mal war. Außerdem ist es immer gut über den Tellerrand zu schauen.

Und gerade die Tage erzählt Trusted Shops was man machen sollte um mit dem eigenen Shop gegen Amazon zu bestehen. Das ist alles so weit richtig was im Artikel steht (die Bezeichnung „Fachgeschäft“ benutzen wir übrigens schon seit Jahren für unsere Online-Shops), aber bei vielen Händlern fängt das Problem viel früher an: Bei den Basics.

Dehner

Die Gartencenter-Kette, 2016 mit einem Jahresumsatz von knapp 700 Millionen Euro, schafft es nicht, einen richtigen Trackinglink zu versenden:

https://tracking.dpd.de/parcelstatus?query=00340434177295186556&locale=de_DE

Das geübte Auge sieht auf den ersten Blick, dass es sich bei der Trackingnummer um eine von DHL handelt. Ich kann bei solchen Fehlern nur mit dem Kopf schütteln, denn den richtigen Carrier zu einer Trackingnummer anhand von regulären Ausdrücken zu erkennen ist ein gelöstes Problem. Die Deliveries App von Juneclod macht es vor. Für den deutschen Markt ist das auch sehr übersichtlich (ok, FedEx fehlt, aber niemand™ versendet in Deutschland mit FedEx):

if (
    preg_match("/^1Z\s?[0-9A-Z]{3}\s?[0-9A-Z]{3}\s?[0-9A-Z]{2}\s?[0-9A-Z]{4}\s?[0-9A-Z]{3}\s?[0-9A-Z]$/i", $sTrackId)) {
        $sCarrier = "UPS";
} elseif(
    preg_match("/^0\d{13}$/", $sTrackId)) {
        $sCarrier = "DPD";
} elseif(
    preg_match("/^\d{14}$/", $sTrackId)) {
        $sCarrier = "HLG";
} elseif(
    preg_match("/^\d{11}$/", $sTrackId)) {
        $sCarrier = "GLS";
} elseif(
    preg_match("/[A-Z]{3}\d{2}\.?\d{2}\.?(\d{3}\s?){3}/", $sTrackId) ||
    preg_match("/[A-Z]{3}\d{2}\.?\d{2}\.?\d{3}/", $sTrackId) ||
    preg_match("/(\d{12}|\d{16}|\d{20})/", $sTrackId)) {
	    $sCarrier = "DHL";
} elseif (
    preg_match("/RR\s?\d{4}\s?\d{5}\s?\d(?=DE)/", $sTrackId) ||
    preg_match("/NN\s?\d{2}\s?\d{3}\s?\d{3}\s?\d(?=DE(\s)?\d{3})/", $sTrackId) ||
    preg_match("/RA\d{9}(?=DE)/", $sTrackId) || preg_match("/LX\d{9}(?=DE)/", $sTrackId) ||
    preg_match("/LX\s?\d{4}\s?\d{4}\s?\d(?=DE)/", $sTrackId) ||
    preg_match("/LX\s?\d{4}\s?\d{4}\s?\d(?=DE)/", $sTrackId) ||
    preg_match("/XX\s?\d{2}\s?\d{3}\s?\d{3}\s?\d(?=DE)/", $sTrackId) ||
    preg_match("/RG\s?\d{2}\s?\d{3}\s?\d{3}\s?\d(?=DE)/", $sTrackId)) {
        $sCarrier = "DPAG";
} else {
        $sCarrier = "NONE";
}

Shopify

Das mit den Trackingnummern ist aber auch für Unternehmen, die sich auf E-Commerce spezialisiert haben und 200 Millionen US-Dollar umsetzen, scheinbar ein großes Problem. Die SaaS-Lösung von Shopify bietet neuerdings die Möglichkeit, sich über den Bestellfortschritt per Facebook Messenger benachrichtigen zu lassen. Das sieht auch richtig nett gemacht aus. Nur das mit den Trackingnummern, das läuft nicht so. Klickt man auf den Link ‘Sendung verfolgen’ landet man bei DHL USA. Der selbe Fehler tritt auch auf der Shopify-Bestellübersicht auf.

Sortimo

Spezialist für Fahrzeugeinrichtungen, Umsatz in 2014 knapp unter 125 Millionen, Bosch als Miteigentümer, die ihre Profiwerkzeuge standardmäßig in der Sortimo L-Boxx ausliefern. Aber eine Mail mit Versandinformationen senden bevor die Ware ausgeliefert wurde? Nicht möglich. „Ihre Ware hat unser Logistik Zentrum soeben verlassen und wird in den nächsten Tagen bei Ihnen eintreffen.“ ist in einer Mail von 17:06 relativ witzlos wenn die Ware schon am selben Tag um 9:46 zugestellt wurde.

Sortimo Service-Station

Neben Sortimo selbst wurschteln auch einige der Service-Stationen, von denen es knapp 30 in Deutschland gibt, online mit rum, u.a. bei eBay. Von den fünf bestellten Anti-Rutschmatten kamen drei hier so dermaßen deformiert an, dass sie nicht benutzbar sind (und sich auch nicht in ihren Ursprungszustand zurückversetzen lassen). Die Mail bzgl. Umtausch wurde dann eine Woche lang einfach nicht bearbeitet. Erst auf erneute Nachfrage (und unter Hinzunahme einer generischen service@-E-Mail-Adresse, die ich mir erst raussuchen musste) kam eine Reaktion.

Fazit

Von Shops, die außer manchmal besseren Preisen ein Totalausfall sind und Bewertungsportalen, auf denen der Shopanbieter lügen kann, bis sich die Balken biegen, ohne dass man die Möglichkeit hat, das korrigiert zu bekommen, will ich hier gar nicht reden, das würde genug Material für einen eigenen Artikel geben. Ich will es bei diesen vier Beispiele belassen, wo die einfachsten Basics nicht funktionieren. Denn die Dinge fallen auch dem unbedarften Kunden auf, es ist sofort offensichtlich, dass da was nicht rund läuft auf Seiten des Anbieters und ob man dem dann ein zweites Mal das Vertrauen schenkt? Dass sich der Kunde diese Frage stellt, dazu sollte man es erst gar nicht kommen lassen.

Tackling this above-the-fold CSS issue

Performance Optimization is somewhat like housekeeping. If you don’t take care of it regularly, the shit piles up. My history of our web performance goes back almost five years now and what started as a blazin’ 94 once was now a 76 while mobile, which was never really good, dropped from the mid-seventies to the high-fifties. Even though I optimize all images and write as less code as possible.

I came across these horrifying numbers because one of our main competitors relaunched recently and while pointing at him and laughing at his numbers (52 Desktop, 51 Mobile) I had to admit that ours aren’t really great either.

Running the Erzgebirge-Palace through PageSpeed Insights spoiled two obvious issues: The long-avoided above-the-fold render-blocking stuff and the trust seal. But that will be a different story, we will focus on the render-blocking css for now.

The core layout for Erzgebirge-Palace is ten years old now and also it has clean markup which made reponsive retrofitting quite easy there is no build process or anything near to it. So the CSS is a little bit messy. Identifying the above-the-fold styles manually was not an option, but hey, Smashing Magazine tackled this issue already two years ago. I came across this article after I unsucessfully tried to run a simple gulp task with the critical plugin by Addy Osmani.

While gulp just spit errors into my console the grunt task by Ben Zörb worked right away and returned a pretty good result. I had to add some more code as there happen things on the page the plugin cannot be aware of as different headers during the holiday season and different headers for different languages. But in the end it was less work than I expected and the result gives me now 92 on Desktop and 91 on Mobile (also resolved the trust seal, though).

All numbers given are from Google PageSpeed Insights. While this tool is ok to do a quick check on your site’s performace you should rely on other tools when actually optimizing your website, my tool of choice here is Webpagetest.

Alexa und FRITZ!Box – und es bewegt sich doch…

Basierend auf diesem Blogpost habe ich mir eine Lösung für die AVM-DECT-Steckdosen FRITZ!Dect 200 gebastelt. Das ganze war als reine Fingerübung gedacht, da es aber immer wieder Nachfragen gibt, hier Details zur Umsetzung.

Das ganze läuft auf einem Uberspace als Webserver. Nehmen wir mal an, der Uberspace-Name ist jarvis und läuft auf dem Server stark. Dieser Webspace ist dann über https://jarvis.stark.uberspace.de/ erreichbar. Wichtig: jarvis immer durch den Namen eures Uberspaces ersetzen (vergebt ihr beim Anlegen) und stark immer durch den Namen des Servers, auf dem euer Uberspace liegt (bekommt ihr nach dem Anlegen automatisch zugewiesen und ist auf dem Reiter ‘Datenblatt’ ersichtlich).

Wir verbinden uns per ssh und wechseln in das DocumentRoot, klonen dort das git-Repo und benennen den Ordner in was brauchbares um:

cd /var/www/virtual/jarvis/
git clone https://github.com/gaiterjones/amazon-alexa-php-hello-world-example
mv amazon-alexa-php-hello-world-example/ alexa

Als nächstes müssen wir mittels Symlink dafür sorgen, dass der Webserver ein anderes Verzeichnis als DocumentRoot benutzt:

ln -s alexa/PAJ/www/Amazon/ jarvis.stark.uberspace.de

Abschließend brauchen wir noch die Datei, die mit der AVM-AHA-Interface spricht. Ich hab da schon mal was vorbereitet. Diese kommt in das folgende Verzeichnis:

/var/www/virtual/jarvis/alexa/PAJ/Application/Amazon/Alexa/Intent

Damit das funktioniert muss in Zeile 25 der Host geändert werden unter dem die FRITZ!Box aus dem Internet erreichbar ist. Außerdem ist den Zeilem 239 und 240 Benutzername und Passwort. Es ist zwingend notwendig, dass für die Anmeldung an der FRITZ!Box sowohl Benutzername als auch Passwort benötigt werden. Wenn das noch nicht der Fall ist muss man das entsprechend umstellen.
Auf jeden Fall sollte ein eigener User für die Kommunikation mit Alexa angelegt werden!

Jetzt loggen wir uns auf developer.amazon.com mit unserem Amazon-Developer-Account ein. Wer noch keinen hat registriert sich schnell einen, es muss sich dabei um den gleichen Account handeln der auch für Alexa hinterlegt ist, sonst steht der Skill anschließend nicht zur Verfügung.

Der Skill basiert auf dem Custom Interaction Model, deshalb ist auch ein Codewort erforderlich. Wenn man direkt die Smart Home Skill API verwendet benötigt man das nicht, damit habe ich mich aber nicht beschäftigt, da man dafür zwingend alles als AWS Lambda ARN hosten muss. Damit kann sich gerne AVM auseinandersetzen.

Unser Skill hat den Invocation Name ‘fritzbox’, das Intent Schema sieht so aus:

{
  "intents": [
    {
      "intent": "FritzBox",
      "slots": [
        {
          "name": "trigger",
          "type": "TRIGGER"
        },
        {
          "name": "state",
          "type": "STATE"
        },
        {
          "name": "action",
          "type": "ACTION"
        },
        {
          "name": "device",
          "type": "DEVICE"
        }
      ]   
    }
  ]
}

Die Slots so:

Und die Sample Utterances so:

FritzBox schalte {trigger} {state}
FritzBox liste meine {device} auf
FritzBox ich brauche die Liste der {device}
FritzBox ich brauche die {device} Liste
FritzBox wie ist die {action} von {trigger}
FritzBox nach dem {action} von {trigger}
FritzBox nach den {action} zu {trigger}

Wichtig ist beim SSL-Zertifikat die Option ‘My development endpoint is a sub-domain of a domain that has a wildcard certificate from a certificate authority’ zu wählen, sonst läuft es nicht.

Im Testbereich können wir jetzt beliebige Test durchführen, z.B. ‘sag fritzbox schalte Bad ein’. Die Antwort sollte in etwa so aussehen:

{
  "version": "1.0",
  "response": {
    "outputSpeech": {
      "type": "PlainText",
      "text": "Bad wurde eingeschaltet"
    },
    "card": {
      "text": "Bad wurde eingeschaltet",
      "title": "FRITZ!Box",
      "image": {
        "smallImageUrl": "https://jarvis.stark.uberspace.de/alexaCardImage.php?size=small&amp;image=default",
        "largeImageUrl": "https://jarvis.stark.uberspace.de/alexaCardImage.php?size=large&amp;image=default"
      },
      "type": "Standard"
    },
    "shouldEndSession": true
  },
  "sessionAttributes": {}
}

Und damit sollten sich die Steckdosen entsprechend schalten und abfragen lassen. Prinzipiell müsste das auch mit Thermostaten funktionieren, ich hab aber keine und kann das daher nicht testen oder implementieren.

Ein CDN für den modified-Shop – xtcModified on steroids

Steter Tropfen höhlt den Stein. Augrund immer wiederkehrender Nachfragen folgt hier Teil fünf meiner kleinen Serie zur Geschwindigkeitsoptimierung von xtcModified. Es geht um die Anbindung eines CDN für statische Inhalte und daher direkt hier die Warnung: Das ist nichts für Anfänger!

Seit dem letzten Artikel sind fast drei Jahre vergangen, die Software hat sich dem xtc im Namen entledigt und der alte Testserver existiert nicht mehr.
Ich habe daher eine neue Installation aufsetzen müssen. Dieses Mal bei Uberspace. Wenn man auf der Suche nach Shared Hosting mit möglichst großer Konfigurationsfreiheit ist, dann ist das das Beste, was man für Geld bekommen kann. Ich hab natürlich die Standardinstallation erstmal grundlegend optimiert, um so Dinge wie Javascript im Footer aber einen Bogen gemacht, weil es den Aufwand für den Testbetrieb nicht rechtfertigt.

Damit auch ein paar Produkte im Shop sind wurde dieser Dump eingespielt. Anders als vorgeschlagen wurde aber nicht das Image-Processing gestartet. Wer das Image-Processing für eine gute Idee hält braucht gar nicht erst weiterlesen. Die Bilder wurden mittels Photoshop auf die passende Größe gebracht und dann mit einem mehrstufigen Prozess aus ImageOptim und JPEGmini optimiert (ähnliche Software gibt es sicher auch für Windows). Nur so hat man wirklich kleine Dateien. Und die sind aus zwei Gründen sinnvoll: Zum einen sind weniger Bytes schneller durch die Leitung geschickt, zum anderen rechnen CDNs meist nach Datenvolumen ab und hier ist jedes Byte dann bares Geld.
Der Demodump ist leider etwas kaputt, was Kategorie 1 angeht, das soll uns aber für ein Testsystem egal sein.

CDN – was ist das überhaupt?

Ein CDN, oder in Langform Content Delivery Network, ist ein aus mehreren Rechnern bestehendes Netz, das Inhalte optimiert ausliefert – entweder möglichst schnell (Performanceoptimierung) oder mit möglichst wenig Bandbreite (Kostenoptimierung) oder beides zusammen. Im konkreten Fall soll es eher um die Performanceoptimierung gehen, denn aus finanzieller Sicht wird das ganze für den Shopbetreiber auf den ersten Blick teurer als wenn er die Inhalte von seinem eigenen Server ausliefern lässt.
Das CDN macht die Auslieferung aus zwei Gründen schneller:

  • Zum einen sind die Server darauf spezialisiert, schnell statische Inhalt auszuliefern.
  • Zum anderen befinden sich die Server auf der ganzen Welt verteilt, ein Besucher aus Japan wird daher von der „Edge Location“ beliefert, die ihm physikalisch am nähsten ist. Weder die Anfrage noch die Antwort müssen um den halben Erdball, wie das bei einem in Deutschland stehenden Server der Fall wäre.

Und brauch ich das?

Wie bei so vielem ist auch hier die richtige Anwort: Kommt drauf an.
Wer einen Server hat, der in Spitzenzeiten in die Knie geht, weil der Apache nicht mehr hinterherkommt mit dem Ausliefern von Shopseiten und statischen Inhalten kann hier schnell für Abhilfe sorgen, ohne direkt den Server wechseln zu müssen.
Wer viele Kunden aus dem Ausland hat, diesen aber trotzdem Inhalte schnell ausliefern will, ist mit einem CDN ebenfalls gut bedient.
Außerdem kann man, wenn man es geschickt angeht, Browserlimitierungen umgehen was den gleichzeitigen Download von Dateien angeht. Das geht aber prinizipiell auch ohne CDN.

Und was kostet das?

Da es hier um Amazons Cloudfront gehen wird gibt darüber die Preisliste von Amazon Auskunft. Da kann man sich natürlich nicht wirklich viel drunter vorstellen, daher hier noch ein paar Zahlen aus der Praxis. Wir haben für den Erzgebirge-Palast alles auf dem CDN liegen: CSS-Dateien, Layoutbilder und Produktbilder. Unser Monatsmittel 2013 lag bei ca. $21, im vergangenen Jahr bei ca. $26. Für das Geld bekommt man ganz sicher nirgends ein vernünftiges Serverupgrade, mit dem man Lastspitzen wegpuffern kann. Und für uns, die einen sehr deutlichen Peak am Jahresende haben, ist das natürlich sehr wichtig.

Los geht’s: CDN-Distribution anlegen

Man braucht für das, was jetzt folgt, einen Amazon-AWS-Account und Zugriff auf seine Domainkonfiguration. Den Account kann man sich auf der Amazon-AWS-Seite anlegen.
Hat man das erledigt und auch eine Zahlungsweise hinterlegt kann man in der AWS Console ‘CloudFront’ auswählen. Nach dem Klick auf ‘Create Distribution’ wählen wir die Option ‘Web’, denn wir wollen ja Inhalte über HTTP und HTTPS ausliefern.

Jetzt geht es ans Ausfüllen des Formulars. Der einzige wichtige Eintrag ist Origin Domain Name. Hier tragen wir die URL unseres Shops ein (im Beispielfall ladeze.it) und nutzen somit automatisch Custom Origin, was vom Wartungsaufwand deutlich angenehmer ist als beispielsweise das Ausliefern der Dateien über Amazon S3.

Wer darüber hinaus eine regionale Einschränkung bei seinen Besuchern vornehmen kann, weil er beispielsweise gar nicht nach Asien verkauft, kann in den ‘Distribution Settings’ das bei ‘Price Class’ entsprechend auswählen. Wer eigene Domainnamen verwenden will statt abc.cloudfront.net kann diese bei ‘Alternate Domain Names (CNAMEs)’ angeben. Das machen wir mal und tragen cdn0.ladeze.it bis cdn5.ladeze.it dort ein, pro CNAME eine Zeile. Das bringt uns nämlich Vorteile wenn wir die Downloadbeschränkung der Browser umgehen wollen. Bei SSL müssen wir dann aber auf die von Amazon zur Verfügung gestellten URLs zurückgreifen, die Konfiguration mit einem eigenen SSL-Zertifikat würde den Rahmen dieses Artikels sprengen.

Nach dem Klick auf ‘Create Distribution’ startet Amazon das Deployment unserer neuen Distribution. Das dauert einen Moment und daher können wir die CNAME-Einträge für unsere Domains anlegen. Die Ziel-URL für den CNAME-Eintrag zeigt uns Amazon im Distribution-Dashboard bei ‘Domain Name’ an.

cdn-deployment

Wenn beides fertig ist, sowohl das Deployment der Distribution als auch das Einrichten der DNS-Einträge, können wir unser neues CDN zum ersten Mal testen. Das geht ganz einfach im Browser. Statt http://ladeze.it/images/product_images/thumbnail_images/53_0.jpg rufen wir aber http://cdn5.ladeze.it/images/product_images/thumbnail_images/53_0.jpg auf. Da DNS-Einträge bis zu 48 Stunden dauern können kann man, sollten die eigenen sich noch nicht herumgesprochen haben, den Test auch mit der von Amazon zur Verfügung gestellten URL durchführen und http://d4qtfj4qyflow.cloudfront.net/images/product_images/thumbnail_images/53_0.jpg aufrufen. Kommt das richtige Bild haben wir alles richtig gemacht.

Ein Blick in die Header-Angaben zeigt uns auch durch den vorhandenen Via-Record, dass die Datei von Cloudfront ausgeliefert wurde. Auch finden wir hier den X-Cache-Record. Beim ersten Aufruf steht dort ‘Miss from cloudfront’. Hier lag das Bild also nicht in Cloudfront vor und der Amazon-Server musste es von unserem Server laden. Alle subsequenten Aufrufe haben dann den Eintrag ‘Hit from cloudfront’.

cf-misscf-hit

Auch sieht man hier schön, dass unsere Einstellungen für max-age übernommen werden. Das Header-Plugin im Screenshot ist übrigens HTTP Headers.

Anpassungen am Shop

Jetzt geht es darum, dass unser modified-Shop die Bilder auch vom CDN ausliefert und nicht mehr von unserem Server.

Cache-Busting

Daten auf dem Cloudfront-CDN werden in der Default-Konfiguration erst nach 24 Stunden erneut auf Änderungen überprüft. Unsere .htaccess sagt sogar, dass Bilder 30 Tage im Cache gehalten werden können, ohne erneut zu prüfen, ob sich etwas verändert hat. Wir kommen also um ein Cache-Busting nicht herum. Dieses manuell zu machen wäre aber viel zu aufwendig. Daher betrifft die erste Änderung die Datei includes/classes/product.php, Zeile 492 wird dort ersetzt durch:

      $iTime = filemtime(DIR_FS_CATALOG.$path.$name);
      $aPath = pathinfo($name);
      return $path.$aPath['filename']."__".$iTime.".".$aPath['extension'];

Wir bauen also das letzte Modifikationsdatum mit in den Dateinamen ein und trennen es vom eigentlichen Namen mit zwei Unterstrichen ab. Völlig egal also ob wir über den Webbrowser oder direkt per SFTP ein Produktbild erneuern, es bekommt dann ein neues Modifikationsdatum und damit auch einen neuen Namen, der noch nicht im Cloudfront-CDN vorhanden ist und daher frisch angefordert wird.

Damit das auch funktioniert müssen wir außerdem im images/-Ordner eine .htaccess anlegen mit folgendem Inhalt:

RewriteEngine On
RewriteBase /
 
RewriteRule (.*)/(.*)__(.*).jpg$ images/$1/$2.jpg [L]

Weitere Änderungen am Core

Für das weitere Vorgehen braucht es eine Änderung in includes/configure.php, die wir nach dem define für HTTP_SERVER einfügen:

	define('HTTP_SERVER_CDN', 'http://cdn{i}.ladeze.it');
	define('HTTPS_SERVER_CDN', 'https://d4qtfj4qyflow.cloudfront.net');

Als nächstes modifizieren wir inc/xtc_image.inc.php.

Ersetzt wird

	// alt is added to the img tag even if it is null to prevent browsers from outputting
	// the image filename as default
	$image = '<img src="' . xtc_parse_input_field_data($src, array('"' => '&quot;')) . '" alt="' . xtc_parse_input_field_data($alt, array('"' => '&quot;')) . '"';

durch

	if(preg_match("/__/", $src)) {
		$aImage = pathinfo($src);
		$sSrc = preg_replace("/(.*)__(.*)/", "$1", $aImage['filename']);
		$sRealSrc = $aImage['dirname']."/".$sSrc.".".$aImage['extension'];
	}
	if(file_exists($src)) {
		$iCDN = (filesize($src)%5);
	} elseif(isset($sRealSrc) && file_exists($sRealSrc)) {
		$iCDN = (filesize($sRealSrc)%5);
	}
	if(REQUEST_TYPE!="SSL") {
		$sCDNSrc = str_replace("{i}", $iCDN, HTTP_SERVER_CDN)."/".$src;
	} else {
		$sCDNSrc = str_replace("{i}", $iCDN, HTTPS_SERVER_CDN)."/".$src;
	}
 
	// alt is added to the img tag even if it is null to prevent browsers from outputting
	// the image filename as default
	$image = '<img src="' . xtc_parse_input_field_data($sCDNSrc, array('"' => '&quot;')) . '" alt="' . xtc_parse_input_field_data($alt, array('"' => '&quot;')) . '"';

Damit werden jetzt schon diverse Bilder über das CDN geladen, aber vor allem die Produktbilder noch nicht. Um das zu erreichen braucht es ein Smarty-Plugin und Anpassungen an allen betroffenen Template-Dateien. Das kann in Arbeit ausarten, muss aber nur einmal gemacht werden. An dieser Stelle machen wir es nur anhand der Produktinfo-Seite.

Außerdem nutzen wir statt einer Domain für die Bilder jetzt mehrere. Da wir die Subdomain aufgrund des Modulos der Dateigröße festlegen ist es auch immer die gleiche Subdomain, die genutzt wird. Da Browser im Maximum sechs gleichzeitige Verbindungen zu einer Domain aufbauen haben wir damit die Anzahl der parallelen Zugriffe vervielfacht – und das CDN steckt das performancetechnisch locker weg.

Templates

Zunächst das Smarty-Plugin, dieses kommt nach templates/xtc5/smarty/function.cdn_image.php:

<?php
/**
 * Smarty plugin
 * @package Smarty
 * @subpackage plugins
 */
 
 
/**
 * Smarty {cdn_image} function plugin
 *
 * Type:     function<br>
 * Name:     cdn_image<br>
 * Purpose:  print an cdn_image()
 * @author Matthias Slovig
 * @param array parameters
 * @param Smarty
 * @return string|null
 */
function smarty_function_cdn_image($params, &$smarty) {
	require_once (DIR_FS_INC.'xtc_image.inc.php');
 
	return xtc_image($params['src'], $params['alt'], $params['width'], $params['height'], $params['params']);
}
 
/* vim: set expandtab: */
 
?>

Und dann die Anpassungen in templates/xtc5/module/product_info/product_info_tabs_v1.html:

<img src="{$PRODUCTS_IMAGE}" alt="{$PRODUCTS_NAME}" class="productimage" />

wird ersetzt durch

{cdn_image src=$PRODUCTS_IMAGE params="class='productimage'" alt=$PRODUCTS_NAME}

Das ist wie gesagt überall dort zu wiederholen, wo Bilder, die eigentlich über das CDN geladen werden sollen, bislang noch durch einen normalen img-Tag eingebunden sind, also Produktlistings, Kategorienlistings, Boxen etc.

CSS und Javascript

Zuletzt sind CSS und Javascript dran, dafür bedarf es auch zunächst eines Cache-Bustings, dieses Mal in der normalen .htaccess des Shops direkt nach der Defintion der RewriteBase:

  RewriteRule (.*)-([0-9]+)\.css$ $1.css [L]
  RewriteRule (.*)-([0-9]+)\.js$ $1.js [L]

Die neue templates/xtc5/css/general.css.php sieht so aus:

<?php
/* -----------------------------------------------------------------------------------------
   $Id: general.js.php 1262 2005-09-30 10:00:32Z mz $
 
   XT-Commerce - community made shopping
   http://www.xt-commerce.com
 
   Copyright (c) 2003 XT-Commerce
   -----------------------------------------------------------------------------------------
   Released under the GNU General Public License
   ---------------------------------------------------------------------------------------*/
 
   // Put CSS-Definitions here, these CSS-files will be loaded at the TOP of every page
 
	$iCDN['stylesheet'] = filesize(DIR_FS_CATALOG."templates/".CURRENT_TEMPLATE."/stylesheet-min.css")%5;
	$iCDN['thickbox'] = filesize(DIR_FS_CATALOG."templates/".CURRENT_TEMPLATE."/css/thickbox-min.css")%5;
	$iCDN['jquery-ui'] = filesize(DIR_FS_CATALOG."templates/".CURRENT_TEMPLATE."/css/jquery-ui.css")%5;
	$sURL['stylesheet'] = str_replace("{i}", $iCDN['stylesheet'], REQUEST_TYPE=='SSL' ? HTTPS_SERVER_CDN : HTTP_SERVER_CDN)."/templates/".CURRENT_TEMPLATE."/stylesheet-min-".filemtime(DIR_FS_CATALOG."templates/".CURRENT_TEMPLATE."/stylesheet-min.css").".css";
	$sURL['thickbox'] = str_replace("{i}", $iCDN['thickbox'], REQUEST_TYPE=='SSL' ? HTTPS_SERVER_CDN : HTTP_SERVER_CDN)."/templates/".CURRENT_TEMPLATE."/css/thickbox-min-".filemtime(DIR_FS_CATALOG."templates/".CURRENT_TEMPLATE."/css/thickbox-min.css").".css";
	$sURL['jquery-ui'] = str_replace("{i}", $iCDN['jquery-ui'], REQUEST_TYPE=='SSL' ? HTTPS_SERVER_CDN : HTTP_SERVER_CDN)."/templates/".CURRENT_TEMPLATE."/css/jquery-ui-".filemtime(DIR_FS_CATALOG."templates/".CURRENT_TEMPLATE."/css/jquery-ui.css").".css";
?>
<link rel="stylesheet" href="<?php echo $sURL['stylesheet'] ?>" type="text/css" />
<link rel="stylesheet" href="<?php echo $sURL['thickbox'] ?>" type="text/css" media="screen" />
 
<?php // BOF - web28 - 2010-07-09 - TABS/ACCORDION in product_info ?>
<?php
if (strstr($PHP_SELF, FILENAME_PRODUCT_INFO )) {
?>
<link rel="stylesheet" href="<?php echo $sURL['jquery-ui'] ?>" type="text/css" media="screen" />
<?php
}
?>
<?php // EOF - web28 - 2010-07-09 - TABS/ACCORDION in product_info ?>

Das Laden der CSS-Datei über das CDN führt automatisch dazu, dass auch alle relativ referenzierten Bilder ebenfalls über Cloudfront ausgeliefert werden.

Und so sieht die neue templates/xtc5/javascript/general.js.php aus:

<?php
/* -----------------------------------------------------------------------------------------
   $Id: general.js.php 1262 2005-09-30 10:00:32Z mz $
 
   XT-Commerce - community made shopping
   http://www.xt-commerce.com
 
   Copyright (c) 2003 XT-Commerce
   -----------------------------------------------------------------------------------------
   Released under the GNU General Public License
   ---------------------------------------------------------------------------------------*/
 
 
   // this javascriptfile get includes at the BOTTOM of every template page in shop
   // you can add your template specific js scripts here
 
	$iCDN['jquery'] = filesize(DIR_FS_CATALOG."templates/".CURRENT_TEMPLATE."/javascript/jquery.js")%5;
	$iCDN['thickbox'] = filesize(DIR_FS_CATALOG."templates/".CURRENT_TEMPLATE."/javascript/thickbox.js")%5;
	$iCDN['jquery-ui'] = filesize(DIR_FS_CATALOG."templates/".CURRENT_TEMPLATE."/javascript/jquery-ui.js")%5;
	$sURL['jquery'] = str_replace("{i}", $iCDN['stylesheet'], REQUEST_TYPE=='SSL' ? HTTPS_SERVER_CDN : HTTP_SERVER_CDN)."/templates/".CURRENT_TEMPLATE."/javascript/jquery-".filemtime(DIR_FS_CATALOG."templates/".CURRENT_TEMPLATE."/javascript/jquery.js").".js";
	$sURL['thickbox'] = str_replace("{i}", $iCDN['thickbox'], REQUEST_TYPE=='SSL' ? HTTPS_SERVER_CDN : HTTP_SERVER_CDN)."/templates/".CURRENT_TEMPLATE."/javascript/thickbox-".filemtime(DIR_FS_CATALOG."templates/".CURRENT_TEMPLATE."/javascript/thickbox.js").".js";
	$sURL['jquery-ui'] = str_replace("{i}", $iCDN['jquery-ui'], REQUEST_TYPE=='SSL' ? HTTPS_SERVER_CDN : HTTP_SERVER_CDN)."/templates/".CURRENT_TEMPLATE."/javascript/jquery-ui-".filemtime(DIR_FS_CATALOG."templates/".CURRENT_TEMPLATE."/javascript/jquery-ui.js").".js";
?>
 
<script src="<?php echo $sURL['jquery'] ?>" type="text/javascript"></script>
<script src="<?php echo $sURL['thickbox'] ?>" type="text/javascript"></script>
 
<?php // BOF - web28 - 2010-07-09 - TABS/ACCORDION in product_info ?>
<?php
if (strstr($PHP_SELF, FILENAME_PRODUCT_INFO )) {
?>
<script src="<?php echo $sURL['jquery-ui'] ?>" type="text/javascript"></script>
<script type="text/javascript">
/* <![CDATA[ */
	//Laden einer CSS Datei mit jquery
    $.get("<?php echo 'templates/'.CURRENT_TEMPLATE; ?>"+"/css/javascript.css", function(css) {
		$("head").append("<style type='text/css'>"+css+"<\/style>");
	});
 
	$(function() {
		$("#tabbed_product_info").tabs();
		$("#accordion_product_info").accordion({ autoHeight: false });
	});
/*]]>*/
</script>
<?php
}
?>
<?php // EOF - web28 - 2010-07-09 - TABS/ACCORDION in product_info ?>

Optimizing JPEG images – save 20% in file size

Optimizing your image file size is one of the low hanging fruits when it comes to page speed optimization for your website.

My current workflow for JPEGs looks something like this:

  • ‘Save for Web’ in Photoshop with 60% quality
  • Run through ImageOptim to reduce file size

Yesterday, Marc Thiele asked on Twitter:

In the unfolding discussion some folks stated that they use both JPEGmini and ImageOptim when it comes to JPEGs and that this would reduce the file size even more. Fortunately I had to add pictures to 50 new products today. Every product has three pictures from different angles, so this sums it up to 150 images in various sizes, 900 images in total, all JPEG.
So I gave it a try and here are the results (I bought JPEGmini for this test as 20 images on the lite version might not be enough to get reliable results, still, your mileage may vary).

‘Save for Web’

With just using ‘Save for Web’ the images had a total size of 84MB. Quite heavy.
photoshop

ImageOptim

Running these through ImageOptim saved nearly 10% and lowered the total size to 75.7MB.
imageoptim

JPEGmini

Using the ‘Save for Web’ images again (not touched by ImageOptim yet) results in 74.2MB.
jpegmini

ImageOptim after JPEGmini

Now adding another run of ImageOptim to the images already optimized by JPEGmini lowers the total size to 66.9MB. Thats more than 20% in file size saved without compromising image quality.
jpegmini-imageoptim

As far as I can tell it is not necessary to run the tools multiple times. At least in ImageOptim it is obvious that the latest version does this by itself. And I wasn’t able to spot any major changes on a second and third run with JPEGmini.

To give you the ability to compare the results, below are two images. The left one is the image just saved from Photoshop, the right one is the image after the whole optimization process.
photographer_ps photographer_optimized