<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="https://ext.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ext.dev/" rel="alternate" type="text/html" /><updated>2026-07-30T18:15:03+00:00</updated><id>https://ext.dev/feed.xml</id><title type="html">ext.dev GmbH - Ihr Partner für Webentwicklung, TYPO3, CRM-Integration und mehr</title><subtitle>Mit ext.dev GmbH, Ihrem Experten für Webentwicklung, TYPO3, CRM-Integration und selbstgehostete Lösungen, erreichen Sie Ihre Ziele im Web.</subtitle><entry><title type="html">Endlos-Spinner nach dem TYPO3-Upgrade: wenn ein Altlast-Feld das ganze Formular blockiert</title><link href="https://ext.dev/blog/2026/07/30/endlos-spinner-typo3-upgrade-tca-altlasten/" rel="alternate" type="text/html" title="Endlos-Spinner nach dem TYPO3-Upgrade: wenn ein Altlast-Feld das ganze Formular blockiert" /><published>2026-07-30T08:00:00+00:00</published><updated>2026-07-30T08:00:00+00:00</updated><id>https://ext.dev/blog/2026/07/30/endlos-spinner-typo3-upgrade-tca-altlasten</id><content type="html" xml:base="https://ext.dev/blog/2026/07/30/endlos-spinner-typo3-upgrade-tca-altlasten/"><![CDATA[<h2 id="wenn-das-backend-nur-noch-dreht">Wenn das Backend nur noch dreht</h2>

<p>Ein TYPO3-Upgrade gilt gemeinhin als geschafft, wenn das Frontend wieder sauber ausgeliefert wird und die Tests grün sind. Der unangenehmste Teil kommt aber manchmal erst danach – im Backend, im ganz normalen Redaktionsalltag. In zwei von uns betreuten TYPO3-Installationen zeigte sich nach dem Sprung von Version 12 auf 13 dasselbe Bild: Ein Klick auf „Neuer Datensatz” öffnete kein Eingabeformular mehr, sondern einen Ladespinner, der sich endlos weiterdrehte. Keine Fehlermeldung, kein Hinweis, kein Abbruch – nur eine Seite, die nie fertig wird.</p>

<p>Betroffen waren die TYPO3-Website von <a href="https://www.profenster.de" target="_blank">Profenster</a> sowie eine weitere von uns betreute Installation. Die Ursache war am Ende eine einzige Feldkonfiguration, die noch aus deutlich älteren TYPO3-Zeiten stammte. Dieser Artikel zeigt, warum ein einzelnes Altlast-Feld ein komplettes Formular lahmlegen kann, warum genau dieser Fehler so schwer zu finden ist – und warum das Aufräumen solcher Altlasten in jede Upgrade-Vorbereitung gehört.</p>

<h2 id="die-herausforderung">Die Herausforderung</h2>

<p>Das Fehlerbild war denkbar unkomfortabel für die Fehlersuche, denn es fehlte schlicht jeder Anhaltspunkt:</p>

<ul>
  <li><strong>Kein Serverfehler.</strong> Die Anfrage lieferte einen sauberen HTTP-200. Im PHP-Fehlerlog und im TYPO3-Log stand nichts – aus Sicht des Servers war alles in Ordnung.</li>
  <li><strong>Keine Fehlermeldung im Backend.</strong> TYPO3 zeigte weder eine Exception noch einen Hinweis an. Die Redaktion sah nur einen Spinner, der nicht aufhörte.</li>
  <li><strong>Nicht überall reproduzierbar.</strong> Für manche Tabellen liess sich problemlos ein neuer Datensatz anlegen, für andere nie. Ein Muster war auf den ersten Blick nicht erkennbar.</li>
  <li><strong>Vorher lief alles.</strong> Unter TYPO3 12 funktionierten exakt dieselben Formulare einwandfrei. Der Fehler entstand erst durch das Upgrade – ohne dass jemand die betroffenen Felder angefasst hätte.</li>
</ul>

<p>Genau diese Kombination macht solche Regressionen teuer: Man sucht zwangsläufig zuerst dort, wo man Fehler gewohnt ist – in den Logfiles, in der Datenbank, in den eigenen PHP-Klassen. Und findet dort nichts, weil der Fehler gar nicht auf dem Server passiert.</p>

<h2 id="unsere-lösung">Unsere Lösung</h2>

<h3 id="1-die-suche-dorthin-verlagern-wo-der-fehler-entsteht">1. Die Suche dorthin verlagern, wo der Fehler entsteht</h3>

<p>Wenn der Server einen fehlerfreien HTTP-200 liefert und trotzdem nichts erscheint, ist die Arbeit im Browser noch nicht abgeschlossen. Das TYPO3-Backend baut seine Eingabeformulare zu einem grossen Teil clientseitig auf: Die sogenannte FormEngine initialisiert nach dem Laden jedes einzelne Feld – Validierungsregeln, Datumsauswahl, Abhängigkeiten zwischen Feldern.</p>

<p>Der Blick in die Entwicklerkonsole des Browsers lieferte deshalb in Sekunden, was Stunden im Serverlog nicht hergegeben hätten: einen abgebrochenen JavaScript-Fehler aus der Datums-Bibliothek, mit der TYPO3 13 arbeitet. Der Spinner drehte sich nicht, weil etwas dauerte – er drehte sich, weil der Code, der ihn hätte ausblenden sollen, nie erreicht wurde.</p>

<h3 id="2-die-ursache-eine-0-die-kein-datum-ist">2. Die Ursache: eine „0”, die kein Datum ist</h3>

<p>Dahinter steckt eine Änderung, die mit dem Upgrade auf TYPO3 13 kam. Die clientseitige Formularvalidierung schickt jedes Feld, das in der Feldkonfiguration (dem TCA) als <code class="language-plaintext highlighter-rouge">eval =&gt; 'datetime'</code> markiert ist, durch die Datums-Bibliothek <a href="https://moment.github.io/luxon/" target="_blank">Luxon</a> und deren Funktion <code class="language-plaintext highlighter-rouge">DateTime.fromISO()</code>. Erwartet wird also ein Datum im ISO-8601-Format.</p>

<p>In über Jahre gewachsenen Erweiterungen sind diese Felder aber häufig ganz anders angelegt: als reine Ganzzahl-Spalten mit dem Standardwert <code class="language-plaintext highlighter-rouge">0</code> – typischerweise die von älteren TYPO3-Vorlagen kopierten Sichtbarkeitsfelder <code class="language-plaintext highlighter-rouge">starttime</code> und <code class="language-plaintext highlighter-rouge">endtime</code>. Das Feld liefert der Validierung damit den Wert <code class="language-plaintext highlighter-rouge">"0"</code>. Und <code class="language-plaintext highlighter-rouge">"0"</code> ist kein gültiges ISO-8601-Datum. Luxon wirft einen Fehler.</p>

<p>Der entscheidende Punkt ist, was dann passiert: Dieser Fehler wird nicht abgefangen. Er bricht nicht nur die Initialisierung des einen kaputten Feldes ab, sondern die gesamte Initialisierungsschleife – und damit den Aufbau des <strong>kompletten</strong> Formulars. Ein einziges Alt-Feld, das im Zweifel nicht einmal sichtbar ist, legt so ein Formular mit dreissig anderen, völlig intakten Feldern still. Das erklärt auch, warum der Fehler nur bestimmte Tabellen traf: genau jene, deren Konfiguration noch die alte Schreibweise enthielt.</p>

<h3 id="3-statt-workaround-auf-den-modernen-feldtyp-migrieren">3. Statt Workaround: auf den modernen Feldtyp migrieren</h3>

<p>Die naheliegende Schnellreparatur wäre gewesen, den Standardwert von <code class="language-plaintext highlighter-rouge">0</code> auf etwas „Datumsähnliches” zu ändern. Das hätte das Symptom kaschiert und die eigentliche Altlast konserviert. Wir haben stattdessen migriert.</p>

<p>TYPO3 kennt seit Version 12 einen eigenständigen Feldtyp <code class="language-plaintext highlighter-rouge">type =&gt; 'datetime'</code>, der die frühere Konstruktion aus Texteingabefeld plus Render-Typ plus <code class="language-plaintext highlighter-rouge">eval</code>-Kette ersetzt. In den betroffenen Konfigurationsdateien haben wir konsequent darauf umgestellt und die obsoleten Angaben (<code class="language-plaintext highlighter-rouge">renderType</code>, <code class="language-plaintext highlighter-rouge">eval</code>, <code class="language-plaintext highlighter-rouge">size</code>, <code class="language-plaintext highlighter-rouge">checkbox</code>) entfernt. Zwei Eigenschaften blieben bewusst unverändert: der Standardwert und der zulässige Wertebereich, beide weiterhin als klassische Zeitstempel.</p>

<p>Das ist der wichtigste Punkt für den Betrieb: <strong>An der Datenhaltung ändert sich nichts.</strong> Die Spalten bleiben Ganzzahl-Spalten mit Unix-Zeitstempeln, es ist keine Datenmigration nötig, und bestehende Inhalte bleiben unangetastet. Der moderne Feldtyp interpretiert die <code class="language-plaintext highlighter-rouge">0</code> schlicht als „leer”, statt sie an eine Datumsbibliothek weiterzureichen, die daran scheitert. Damit ist die Fehlerquelle nicht umgangen, sondern beseitigt.</p>

<h3 id="4-vollständigkeit-statt-symptombehandlung">4. Vollständigkeit statt Symptombehandlung</h3>

<p>Ein Fix, der nur das gemeldete Formular repariert, ist bei diesem Fehlerbild wenig wert – denn jedes weitere Alt-Feld wartet nur darauf, beim nächsten Klick dasselbe zu tun. Deshalb gehört zu einem solchen Fix zwingend eine systematische Nachkontrolle: Wir haben die gesamte Erweiterung nach der alten Schreibweise durchsucht, alle Fundstellen migriert und anschliessend verifiziert, dass keine einzige Konfiguration mit dem alten Muster mehr übrig ist. Bei einem der beiden Projekte betraf das vier Konfigurationsdateien quer über Inhaltselemente und eigene Datenmodelle hinweg – gefunden wurden sie nur durch die vollständige Suche, gemeldet worden war ein einziges Formular.</p>

<h3 id="5-die-stillen-altlasten-gleich-mit-aufräumen">5. Die stillen Altlasten gleich mit aufräumen</h3>

<p>Bei dieser Gelegenheit kam eine zweite, unauffälligere Altlast zum Vorschein: Verweise auf Beschriftungstexte, die auf Erweiterungen zeigten, die es im TYPO3-Core so gar nicht mehr gibt. Beim Umbau des Cores in einzelne Composer-Pakete sind diese Pfade umgezogen; die alten Referenzen laufen seither ins Leere.</p>

<p>Diese Verweise legen nichts lahm – sie sorgen nur dafür, dass Felder im Backend gar nicht oder mit kryptischen Platzhaltern beschriftet sind. Für die Redaktion ist das trotzdem täglich spürbar. Wir haben die Pfade auf ihre heutigen Entsprechungen im Core-Paket korrigiert und gegen die tatsächlich installierte Core-Version geprüft, statt sie aus dem Gedächtnis zu setzen.</p>

<h2 id="das-ergebnis">Das Ergebnis</h2>

<p>Das Anlegen neuer Datensätze funktioniert in beiden Installationen wieder – und zwar nicht nur in dem gemeldeten Formular, sondern in allen, die dieselbe Altlast in sich trugen. Die Änderung blieb dabei bewusst klein und gut überprüfbar: reine Feldkonfiguration, kein Eingriff in Programmlogik, keine Datenmigration, kein sichtbarer Unterschied im Frontend. Die Umstellung wurde vor dem Ausrollen unabhängig gegengeprüft – auf Korrektheit der Migration, auf Datenkompatibilität der Zeitstempel und vor allem auf Vollständigkeit.</p>

<h2 id="fazit">Fazit</h2>

<p>Der teuerste Teil dieses Fehlers war nicht die Reparatur, sondern die Suche. Ein Formular, das ohne jede Fehlermeldung im Ladespinner hängen bleibt, gibt von sich aus keinen einzigen Hinweis darauf, dass die Ursache eine unscheinbare Feldkonfiguration ist, die niemand mehr bewusst gepflegt hat.</p>

<p>Daraus lassen sich drei Dinge mitnehmen. Erstens: Ein Upgrade ist nicht mit dem grünen Frontend abgeschlossen – die Backend-Abläufe der Redaktion gehören genauso auf die Testliste, inklusive „Neuer Datensatz” für jede eigene Tabelle. Zweitens: Wenn der Server einen sauberen HTTP-200 liefert und trotzdem nichts erscheint, lohnt der Blick in die Browserkonsole mehr als jede weitere Runde im Serverlog. Und drittens, am wichtigsten: Veraltete Feldkonfigurationen sind keine Kosmetik, die man „irgendwann mal” angeht. Sie sind stille Blockaden, die genau dann zuschlagen, wenn eine neue Major-Version die Spielregeln ändert. Ein sauberes Upgrade räumt sie systematisch mit ab – nicht erst, wenn die Redaktion vor einem drehenden Spinner sitzt.</p>

<p>Steht bei Ihnen ein TYPO3-Upgrade an, oder verhält sich Ihr Backend seit dem letzten Versionssprung merkwürdig? <a href="/kontakt/">Sprechen Sie uns an</a> – wir prüfen Ihre Installation auf genau solche Altlasten, bevor sie im Alltag auffallen.</p>]]></content><author><name>christopher.zechendorf</name></author><category term="development" /><summary type="html"><![CDATA[Nach dem Upgrade auf TYPO3 13 hängt das Anlegen neuer Datensätze im endlosen Ladespinner – ohne Fehlermeldung. Wie ein Altlast-Feld das Formular blockiert.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ext.dev/img/post/endlos-spinner-typo3-upgrade-tca-altlasten.webp" /><media:content medium="image" url="https://ext.dev/img/post/endlos-spinner-typo3-upgrade-tca-altlasten.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">22 Sicherheitslücken auf einen Streich: Composer-Abhängigkeiten sicher aktuell halten</title><link href="https://ext.dev/blog/2026/07/20/composer-abhaengigkeiten-sicher-aktuell-halten/" rel="alternate" type="text/html" title="22 Sicherheitslücken auf einen Streich: Composer-Abhängigkeiten sicher aktuell halten" /><published>2026-07-20T10:00:00+00:00</published><updated>2026-07-20T10:00:00+00:00</updated><id>https://ext.dev/blog/2026/07/20/composer-abhaengigkeiten-sicher-aktuell-halten</id><content type="html" xml:base="https://ext.dev/blog/2026/07/20/composer-abhaengigkeiten-sicher-aktuell-halten/"><![CDATA[<h2 id="die-herausforderung">Die Herausforderung</h2>

<p>An einem ganz normalen Morgen meldete unser täglicher, automatisierter Sicherheits-Scan für eine von uns betreute TYPO3-Website auf einen Schlag <strong>22 bekannte Schwachstellen</strong> (CVEs) – und zwar nicht im selbst entwickelten Code, sondern in den quelloffenen Bibliotheken, auf denen die Seite aufbaut.</p>

<p>Das ist keine Ausnahme, sondern der Normalfall moderner Webentwicklung: Eine TYPO3-Anwendung besteht zu einem grossen Teil aus Dritt-Komponenten, die über den Paketmanager <a href="https://getcomposer.org/" target="_blank">Composer</a> eingebunden werden – vom TYPO3-Core selbst über HTTP-Clients bis zu spezialisierten Sicherheits-Bibliotheken. In genau diesen Komponenten werden fortlaufend neue Sicherheitslücken entdeckt und – verantwortungsvoll – veröffentlicht. Eine Website, die einmal live gegangen ist und danach „eingefroren” bleibt, sammelt deshalb mit jedem Tag stillschweigend Risiko an, ohne dass sich an ihrem sichtbaren Verhalten irgendetwas ändert.</p>

<p>Der erste Reflex bei 22 Meldungen ist oft Hektik: jede Lücke einzeln nachschlagen, einzeln patchen, einzeln testen. Das ist langsam, fehleranfällig und bringt den Abhängigkeitsbaum leicht in einen inkonsistenten Zustand. Wir gehen anders vor.</p>

<h2 id="unsere-lösung">Unsere Lösung</h2>

<h3 id="1-lücken-finden-bevor-sie-zum-problem-werden">1. Lücken finden, bevor sie zum Problem werden</h3>

<p>Am Anfang steht nicht der Fix, sondern die Sichtbarkeit. Für die von uns betreuten Projekte läuft täglich ein automatisierter Sicherheits-Scan, der die tatsächlich installierten Paketversionen mit den offiziellen Schwachstellen-Datenbanken abgleicht. So landet eine neu veröffentlichte Lücke bei uns auf dem Tisch, bevor sie in freier Wildbahn ausgenutzt werden kann – inklusive Einordnung, welche Pakete betroffen sind und welche Zielversion das Problem behebt.</p>

<h3 id="2-ein-koordinierter-lockfile-refresh-statt-einzelfixes">2. Ein koordinierter Lockfile-Refresh statt Einzelfixes</h3>

<p>Der entscheidende Punkt: In sauber gepflegten Projekten sind die Versions-Vorgaben (die sogenannten Constraints in der <code class="language-plaintext highlighter-rouge">composer.json</code>) bereits so gesetzt, dass die abgesicherten Patch-Versionen erlaubt sind – im konkreten Fall etwa <code class="language-plaintext highlighter-rouge">^13.4</code> für alle TYPO3-Pakete. Es musste also <strong>keine einzige Versionsvorgabe angefasst werden</strong>. Der Fix bestand darin, die bereits erlaubten, gepatchten Versionen tatsächlich zu ziehen – ein reiner Refresh der <code class="language-plaintext highlighter-rouge">composer.lock</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>composer update "typo3/cms-*" guzzlehttp/guzzle guzzlehttp/psr7 typo3/html-sanitizer --with-dependencies
</code></pre></div></div>

<p>Weil alle betroffenen Pakete in <strong>einem einzigen Lauf</strong> aktualisiert werden, bleibt der Abhängigkeitsbaum in sich konsistent – kein Flickenteppich aus 22 halb zusammenpassenden Einzelständen. Die <code class="language-plaintext highlighter-rouge">composer.json</code> bleibt dabei unangetastet; geändert wird ausschliesslich die Lock-Datei, die exakt festhält, welche Version wovon installiert ist.</p>

<h3 id="3-was-konkret-aktualisiert-wurde">3. Was konkret aktualisiert wurde</h3>

<p>In diesem einen Lauf wurden alle 22 gemeldeten Schwachstellen geschlossen:</p>

<ul>
  <li><strong>TYPO3-Core</strong> von 13.4.30 auf <strong>13.4.32</strong> – dieser Patch bündelt gleich mehrere offizielle <a href="https://typo3.org/help/security-advisories" target="_blank">TYPO3 Security Advisories</a> (TYPO3-CORE-SA-2026-006 bis -019).</li>
  <li><strong>Guzzle</strong> (HTTP-Client) von 7.10.5 auf 7.14.1 und das zugehörige <strong>PSR-7</strong>-Paket auf 2.12.5.</li>
  <li>Den <strong>HTML-Sanitizer</strong> von TYPO3 auf 2.3.2 – die Komponente, die Redaktions-Eingaben von schädlichem Markup befreit.</li>
</ul>

<p>Für die Redaktion und die Besucherinnen und Besucher der Seite war davon nichts zu spüren: keine Änderung an Inhalten, Funktionen oder Darstellung – nur intern deutlich sicherere Fundamente.</p>

<h3 id="4-verifikation-statt-vertrauen">4. Verifikation statt Vertrauen</h3>

<p>Ein Update gilt bei uns erst dann als erledigt, wenn nachweisbar ist, dass es wirkt. Der aktualisierte Stand wurde in einem sauberen PHP-8.4-Container gebaut und geprüft:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">composer audit</code> meldete anschliessend <strong>keine</strong> offenen Sicherheitshinweise mehr – alle 22 CVEs waren nachweislich ausgeräumt.</li>
  <li><code class="language-plaintext highlighter-rouge">composer validate</code> bestätigte, dass Constraint-Datei und Lock-Datei zueinander passen.</li>
  <li>Der Diff berührte <strong>ausschliesslich</strong> die <code class="language-plaintext highlighter-rouge">composer.lock</code> – ein starkes Signal dafür, dass wirklich nur Versionen bewegt und keine Vorgaben verändert wurden.</li>
  <li>Nach dem Deploy folgte ein Smoke-Test der Live-Seite: Frontend und Backend luden fehlerfrei, die Caches wurden im Zuge des Deployments geleert.</li>
</ul>

<h3 id="5-regelmässige-minor-upgrades-als-wartungsroutine">5. Regelmässige Minor-Upgrades als Wartungsroutine</h3>

<p>Der grösste Hebel gegen solche Sammelmeldungen ist, sie gar nicht erst gross werden zu lassen. Deshalb ist die Dependency-Pflege bei uns kein Feuerwehreinsatz, sondern Routine: Über mehrere Kundenprojekte hinweg heben wir TYPO3 planmässig auf die jeweils aktuelle Patch-Version im 13.4-LTS-Zweig (zuletzt auf 13.4.33) an. Wer regelmässig kleine, sichere Schritte geht, muss selten einen grossen, riskanten Sprung machen.</p>

<h2 id="das-ergebnis">Das Ergebnis</h2>

<p>Alle 22 gemeldeten Schwachstellen wurden in einem einzigen, koordinierten Durchlauf geschlossen – ohne Änderung an Inhalten oder Funktionen, mit einem sauber begrenzten Diff und einem grünen Smoke-Test nach dem Deploy. Aus einer potenziell hektischen 22-fach-Baustelle wurde ein ruhiger, nachvollziehbarer und vollständig verifizierter Wartungsvorgang.</p>

<h2 id="fazit">Fazit</h2>

<p>Sicherheit ist kein Zustand, den man einmal herstellt, sondern ein Prozess. Die Meldung von 22 CVEs an einem Tag klingt dramatisch – ist aber vollkommen beherrschbar, wenn drei Dinge zusammenkommen: kontinuierliche Überwachung, die Lücken früh sichtbar macht; sauber gepflegte Versionsvorgaben, die einen ruhigen Lockfile-Refresh statt hektischer Einzelfixes erlauben; und regelmässige Minor-Upgrades, die den Rückstand gar nicht erst entstehen lassen. So bleibt eine Website dauerhaft auf einem abgesicherten Fundament – unsichtbar für die Nutzer, aber entscheidend für den sicheren Betrieb.</p>

<p>Betreiben Sie eine TYPO3- oder PHP-Website und sind unsicher, wie es um die Aktualität Ihrer Abhängigkeiten steht? <a href="/kontakt/">Sprechen Sie uns an</a> – wir richten die kontinuierliche Überwachung ein und halten Ihre Seite planmässig auf einem sicheren Stand.</p>]]></content><author><name>christopher.zechendorf</name></author><category term="development" /><summary type="html"><![CDATA[Ein Security-Scan meldete 22 CVEs in den Composer-Abhängigkeiten einer TYPO3-Website. Wie wir sie in einem koordinierten Lockfile-Refresh geschlossen haben.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ext.dev/img/post/composer-abhaengigkeiten-sicher-aktuell-halten.webp" /><media:content medium="image" url="https://ext.dev/img/post/composer-abhaengigkeiten-sicher-aktuell-halten.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Alte Links, entfernte Funktionen: wiederkehrende 500-Fehler elegant abfangen</title><link href="https://ext.dev/blog/2026/07/20/alte-links-entfernte-funktionen-500-fehler/" rel="alternate" type="text/html" title="Alte Links, entfernte Funktionen: wiederkehrende 500-Fehler elegant abfangen" /><published>2026-07-20T08:00:00+00:00</published><updated>2026-07-20T08:00:00+00:00</updated><id>https://ext.dev/blog/2026/07/20/alte-links-entfernte-funktionen-500-fehler</id><content type="html" xml:base="https://ext.dev/blog/2026/07/20/alte-links-entfernte-funktionen-500-fehler/"><![CDATA[<h2 id="wenn-der-code-verschwindet-die-links-aber-bleiben">Wenn der Code verschwindet, die Links aber bleiben</h2>

<p>Software wird umgebaut, Abläufe werden vereinfacht, alte Funktionen fliegen raus – das ist gesunde Weiterentwicklung. Was dabei gern übersehen wird: Die Links, die auf diese Funktionen zeigen, verschwinden nicht mit dem Code. Sie stecken in längst versendeten E-Mails, in Kalendereinträgen, in Lesezeichen und in den Indizes von Suchmaschinen – und sie bleiben dort, lange nachdem die dazugehörige Funktion entfernt wurde.</p>

<p>Genau dieses Muster haben wir auf der von uns betreuten Weinplattform <a href="https://www.vinum.eu" target="_blank">VINUM</a> beobachtet. Nach einem Umbau der Event-Anmeldung endete jeder Aufruf einer alten Anmelde-Aktion nicht etwa auf einer freundlichen Hinweisseite, sondern in einem harten Serverfehler. 41 solcher Fehler in nur sieben Tagen, verteilt über die deutsche und die französische Seite – ausgelöst von echten Nutzern, die auf einen völlig legitimen, korrekt signierten Link geklickt hatten. Dieser Artikel zeigt, wie man Funktionen abkündigt, ohne Besucher auf Fehlerseiten laufen zu lassen – ein Thema, das jede über Jahre gewachsene Website betrifft.</p>

<h2 id="die-herausforderung">Die Herausforderung</h2>

<p>Die Event-Anmeldung von VINUM lief früher über einen Bestätigungslink per E-Mail: Nach der Anmeldung erhielten Interessenten eine Mail mit einem Link, der die Anmeldung abschloss. Bei einem Umbau wurde dieser Ablauf durch mehrstufige, serverseitige Anmeldeschritte ersetzt – die alte, mailbasierte Aktion wurde ersatzlos aus dem Code entfernt.</p>

<p>Was dabei nicht bedacht wurde: Diese Bestätigungslinks tragen in TYPO3 eine kryptografische Signatur (den sogenannten cHash), die sie gegen Manipulation absichert. Diese Signatur läuft nicht ab. Ein Link, der vor Monaten in einer Bestätigungsmail verschickt wurde, ist technisch bis heute gültig – er zeigt nur eben auf eine Aktion, die es nicht mehr gibt.</p>

<p>Und genau hier lag die Tücke im Detail:</p>

<ul>
  <li><strong>Der Absturz passiert, bevor der eigene Code überhaupt läuft.</strong> In TYPO3 muss jede aufrufbare Aktion einer Erweiterung ausdrücklich als „erlaubt” registriert sein. Trifft eine Anfrage auf einen Aktionsnamen, der nicht mehr in dieser Liste steht, bricht das Framework die Verarbeitung mit einer <code class="language-plaintext highlighter-rouge">InvalidActionNameException</code> ab – noch bevor der Controller mit unserem eigenen Code aufgerufen wird. Ohne Auffangnetz wird daraus ein HTTP-500.</li>
  <li><strong>Es waren echte Nutzer, keine Bots.</strong> Die aufgerufenen Links trugen allesamt gültige Signaturen. Bot-Anfragen oder verstümmelte URLs hätten diese Prüfung gar nicht erst bestanden. Die 41 Treffer stammten also von realen Personen, die eine noch im Umlauf befindliche Mail geöffnet hatten – auf Deutsch wie auf Französisch, über acht verschiedene Tage verteilt.</li>
  <li><strong>Niemand beschwert sich.</strong> Wer auf einen alten Anmeldelink klickt und eine Fehlerseite sieht, ruft selten die Agentur an – er zuckt mit den Schultern und schliesst den Tab. Der Fehler bleibt damit unterhalb der Beschwerdeschwelle und taucht nur als wiederkehrende Zeile im Produktions-Log auf.</li>
</ul>

<p>Sichtbar wurde das Problem überhaupt erst durch unsere <a href="/blog/2026/06/15/aus-logzeilen-werden-tickets/">tägliche, automatisierte Auswertung der Produktions-Logs</a>, die wiederkehrende Fehler clustert und als fertig recherchiertes Ticket aufbereitet – aus „einige 500er pro Tag” wurde so ein priorisierter Auftrag mit fertiger Ursachenanalyse.</p>

<h2 id="unsere-lösung">Unsere Lösung</h2>

<p>Die naheliegende Reaktion – „die Funktion ist weg, dann sind die Links eben tot” – greift zu kurz. Solange gültige Links auf eine Adresse zeigen, ist diese Adresse Teil der öffentlichen Oberfläche und muss sich anständig verhalten. Unser Ziel war deshalb nicht, den Fehler zu unterdrücken, sondern die alten Links wieder an ein sinnvolles Ziel zu führen.</p>

<h3 id="1-schlanke-legacy-aktionen-statt-einer-ersatzlosen-lücke">1. Schlanke „Legacy”-Aktionen statt einer ersatzlosen Lücke</h3>

<p>Statt die entfernten Aktionen ganz verschwinden zu lassen, haben wir sie in abgespeckter Form wieder eingeführt – nicht mit ihrer alten Logik, sondern als reine Weiterleitungen. Die frühere Bestätigungs-Aktion nimmt jetzt nur noch die Event-Kennung entgegen und leitet den Besucher direkt auf die zugehörige Event-Seite weiter. Wer also einen alten Anmeldelink anklickt, landet nicht auf einer Fehlerseite, sondern genau bei der Veranstaltung, um die es ging – inklusive des aktuellen, neuen Anmeldewegs.</p>

<p>Für die codebasierte Bestätigung, die statt einer Event-Kennung einen individuellen Anmeldecode trägt, geht die Weiterleitung einen Schritt weiter: Der Code wird nachgeschlagen, die zugehörige Anmeldung und deren Event ermittelt, und der Besucher landet auf der richtigen Event-Seite. Aus einem Serverfehler wird so eine saubere Weiterleitung mit erhaltenem Kontext.</p>

<h3 id="2-die-aktionsnamen-wieder-als-erlaubt-registrieren">2. Die Aktionsnamen wieder als „erlaubt” registrieren</h3>

<p>Die eigentliche Ursache des 500ers – der Abbruch, noch bevor der Controller läuft – lässt sich nur an einer Stelle beheben: in der Konfiguration der Erweiterung, die festlegt, welche Aktionen ein Plugin ausführen darf. Wir haben die beiden alten Aktionsnamen dort wieder eingetragen, und zwar in beiden Varianten der Liste (der gecachten und der ungecachten). Erst dadurch reicht das Framework die Anfrage überhaupt an unseren Code weiter, statt sie vorzeitig mit einer <code class="language-plaintext highlighter-rouge">InvalidActionNameException</code> abzuweisen.</p>

<h3 id="3-robuste-fallbacks-für-fehlende-oder-manipulierte-parameter">3. Robuste Fallbacks für fehlende oder manipulierte Parameter</h3>

<p>Alte Links kommen nicht immer sauber an. Manche tragen gar keine Kennung mehr, manche eine veraltete oder abgelaufene, manche eine durch mehrfaches Weiterleiten verstümmelte. Eine Weiterleitung, die stur mit einem gültigen Parameter rechnet, würde bei genau diesen Fällen wieder abstürzen – diesmal mit einem anderen Fehler.</p>

<p>Deshalb sind alle Parameter der neuen Aktionen bewusst optional. Fehlt eine Kennung, lässt sich ein Code nicht auflösen, oder ist ein Event längst vergangen, führt die Weiterleitung sauber auf die allgemeine Event-Übersicht zurück, statt einen Fehler zu werfen. Diese Nachsicht ist kein Beiwerk, sondern die Kernanforderung: Der Besucher soll immer auf einer funktionierenden Seite landen, egal in welchem Zustand sein alter Link ist.</p>

<h3 id="4-tote-verweise-entfernen-damit-keine-neuen-altlasten-entstehen">4. Tote Verweise entfernen, damit keine neuen Altlasten entstehen</h3>

<p>Beim Aufräumen zeigte sich, dass im Code noch alte Vorlagen schlummerten, die weiterhin Links auf die entfernte Aktion erzeugten – eine potenzielle Quelle für immer neue tote Links. Diese Vorlagen haben wir entfernt. Damit gibt es keine lebende Stelle mehr, die frische Verweise auf eine nicht mehr existierende Funktion ausgibt; das Problem kann sich nicht aus sich selbst heraus nachbilden.</p>

<h2 id="das-ergebnis">Das Ergebnis</h2>

<p>Aus jedem der bisher 41 Serverfehler pro Woche wird jetzt eine unauffällige Weiterleitung. Wer heute einen Monate alten Anmeldelink anklickt – aus einer archivierten Mail, einem Kalendereintrag oder einem Lesezeichen – landet direkt bei der gewünschten Veranstaltung oder, falls diese vorbei ist, auf der Event-Übersicht. Die <code class="language-plaintext highlighter-rouge">InvalidActionNameException</code> für die alten Aktionen ist aus den Logs verschwunden, und kein Besucher sieht mehr eine Fehlerseite, nur weil er einem völlig legitimen Link gefolgt ist.</p>

<p>Wichtiger als der einzelne Fix ist das Prinzip dahinter: Wir haben eine entfernte Funktion nicht wiederbelebt, sondern nur einen schmalen Migrationspfad für die noch kursierenden Links gebaut. Der neue Code ist bewusst minimal – er nimmt einen alten Aufruf entgegen und übergibt ihn an die aktuelle Oberfläche. So bleibt die Vereinfachung, die der Umbau gebracht hat, vollständig erhalten, ohne dass die Nutzer den Preis dafür zahlen.</p>

<h2 id="fazit">Fazit</h2>

<p>Eine Funktion aus dem Code zu entfernen ist einfach. Die Spuren, die sie in der Aussenwelt hinterlassen hat, verschwinden aber nicht mit ihr: Signierte Links in E-Mails leben unbegrenzt weiter, Kalender und Lesezeichen sind ein Archiv, das man nicht kontrolliert, und Suchmaschinen halten alte Adressen hartnäckig fest. Wer eine Funktion abkündigt, braucht deshalb einen Plan für die URLs, die bereits im Umlauf sind – und in aller Regel heisst dieser Plan: weiterleiten statt ersatzlos streichen.</p>

<p>Das kostet wenig. Eine schlanke Weiterleitung, die einen alten Aufruf auf ein sinnvolles Ziel führt und mit fehlenden Parametern gnädig umgeht, ist in kurzer Zeit gebaut – und erspart realen Nutzern die Sackgasse einer Fehlerseite. Der teure Weg ist der, diese Altlasten erst dann zu bemerken, wenn sie sich als wiederkehrende 500er im Log stapeln.</p>

<p>Planen Sie einen Umbau, bei dem Funktionen oder ganze Seitenbereiche wegfallen? <a href="/kontakt/">Sprechen Sie uns an</a> – wir sorgen dafür, dass Ihre bestehenden Links den Übergang überstehen und Besucher weich aufgefangen werden, statt in Serverfehlern zu landen.</p>]]></content><author><name>christopher.zechendorf</name></author><category term="development" /><summary type="html"><![CDATA[Nach einer Funktionsentfernung liefen alte, noch gültige Links auf 500er-Fehler. Wie wir sie mit schlanken Weiterleitungen sauber abgefangen haben.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ext.dev/img/post/alte-links-entfernte-funktionen-500-fehler.webp" /><media:content medium="image" url="https://ext.dev/img/post/alte-links-entfernte-funktionen-500-fehler.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Success-Story: Zu Lebzeiten oder erst danach? Ein abgestuftes Zugriffsmodell für die digitale Nachlassvorsorge</title><link href="https://ext.dev/blog/2026/07/06/digitale-nachlassvorsorge-abgestufte-berechtigungen/" rel="alternate" type="text/html" title="Success-Story: Zu Lebzeiten oder erst danach? Ein abgestuftes Zugriffsmodell für die digitale Nachlassvorsorge" /><published>2026-07-06T08:00:00+00:00</published><updated>2026-07-06T08:00:00+00:00</updated><id>https://ext.dev/blog/2026/07/06/digitale-nachlassvorsorge-abgestufte-berechtigungen</id><content type="html" xml:base="https://ext.dev/blog/2026/07/06/digitale-nachlassvorsorge-abgestufte-berechtigungen/"><![CDATA[<h2 id="zu-lebzeiten-oder-erst-danach-ein-abgestuftes-zugriffsmodell-für-die-digitale-nachlassvorsorge">Zu Lebzeiten oder erst danach? Ein abgestuftes Zugriffsmodell für die digitale Nachlassvorsorge</h2>

<p>Für ein Projekt im sozialen Bereich haben wir eine Plattform umgesetzt, auf der Menschen ihre wichtigsten Unterlagen sicher hinterlegen können: Notfallinformationen, Vorsorgewünsche, ein persönliches Tagebuch, Botschaften an Angehörige und ein handschriftliches Testament. Eine benannte Vertrauensperson darf genau definierte Teile davon einsehen – manches schon zu Lebzeiten, anderes erst nach einem bestätigten Ableben. Die Zielgruppe ist besonders schutzbedürftig, entsprechend hoch sind die Ansprüche an Datenschutz und Vertrauenswürdigkeit. Die Plattform beschreiben wir hier anonymisiert – ohne Kundennamen.</p>

<h3 id="die-herausforderung">Die Herausforderung</h3>

<p>Eine digitale Nachlass- und Notfallvorsorge lebt von einem scheinbar einfachen Versprechen: Die richtigen Menschen sehen die richtigen Informationen – aber immer erst zum richtigen Zeitpunkt. Genau das ist technisch anspruchsvoll:</p>

<ul>
  <li><strong>Ein einzelnes Ja/Nein reicht nicht.</strong> Ob eine Vertrauensperson etwas sehen darf, hängt nicht nur davon ab, <em>ob</em> ein Todesfall gemeldet wurde, sondern auch von der <em>Art</em> der Information. Eine Notfallverfügung soll bereits zu Lebzeiten sichtbar sein, das Tagebuch dagegen erst danach.</li>
  <li><strong>Höchst sensible Daten.</strong> Ausweiskopien, ein handschriftliches Testament, persönliche Botschaften – all das sind Unterlagen, die niemals in falsche Hände geraten und auch nicht über Umwege abrufbar sein dürfen.</li>
  <li><strong>Nachvollziehbarkeit im Ernstfall.</strong> Wird nach einem Todesfall auf die Daten zugegriffen, muss dieser Vorgang dokumentiert und an eine ausdrückliche Einwilligung gebunden sein.</li>
  <li><strong>Vertrauen als Grundvoraussetzung.</strong> Für eine besonders schutzbedürftige Zielgruppe ist Datenschutz kein Zusatz, sondern die Bedingung dafür, die Plattform überhaupt zu nutzen.</li>
</ul>

<h3 id="unsere-lösung">Unsere Lösung</h3>

<h4 id="1-ein-abgestuftes-zeitlich-gestaffeltes-berechtigungsmodell">1. Ein abgestuftes, zeitlich gestaffeltes Berechtigungsmodell</h4>

<p>Statt eines einzelnen „verstorben ja/nein”-Schalters haben wir die Sichtbarkeit pro Inhalt konfigurierbar gemacht. Jede Rubrik trägt die Information, ob sie einer Vertrauensperson <strong>schon zu Lebzeiten</strong> oder <strong>erst nach einem bestätigten Ableben</strong> gezeigt wird.</p>

<ul>
  <li>Bereits zu Lebzeiten sichtbar: Notfallverfügung, Vorsorgewünsche und die Notfallkarte.</li>
  <li>Erst nach dem Ableben: Tagebuch, Lebensgeschichte, persönliche Botschaften, Testament, persönliche Dokumente sowie Nachlass- und Bestattungswünsche.</li>
</ul>

<p>Der sichere Standard ist dabei „erst nach dem Ableben” – neue Inhalte sind also im Zweifel geschützt und nicht versehentlich zu früh sichtbar. Das Modell lässt sich anpassen, ohne dass dafür programmiert werden muss, sollte sich der Bedarf einmal ändern.</p>

<h4 id="2-sicherheit-die-serverseitig-durchgesetzt-wird">2. Sicherheit, die serverseitig durchgesetzt wird</h4>

<p>Wichtig war uns: Einen Inhalt zu Lebzeiten schlicht „auszublenden” genügt nicht. Jeder Abruf wird serverseitig geprüft. Versucht jemand, einen noch gesperrten Inhalt direkt aufzurufen, wird der Zugriff aktiv verweigert – nicht nur in der Oberfläche versteckt. So gibt es keine Hintertür an der Ansicht vorbei.</p>

<h4 id="3-datenexport-nach-dem-todesfall--verschlüsselt-und-protokolliert">3. Datenexport nach dem Todesfall – verschlüsselt und protokolliert</h4>

<p>Ist ein Ableben bestätigt, kann die Vertrauensperson innerhalb eines festen Aufbewahrungsfensters alle hinterlegten Daten in einem einzigen Paket herunterladen. Dieser Vorgang ist bewusst mehrstufig abgesichert:</p>

<ul>
  <li>Vor dem Download muss die Vertrauensperson die Datenschutz-Hinweise ausdrücklich bestätigen. Diese Einwilligung wird protokolliert.</li>
  <li>Das Export-Paket ist ein <strong>passwortgeschütztes ZIP-Archiv</strong> (AES-Verschlüsselung). Das Passwort wird genau einmal angezeigt und niemals im Klartext gespeichert – abgelegt wird nur ein Prüfwert.</li>
  <li>Die Einwilligung hat eine begrenzte Gültigkeit; danach muss sie erneut erteilt werden.</li>
  <li>Ein beiliegendes Inhaltsverzeichnis dokumentiert, was das Paket enthält und wer es wann angefordert hat.</li>
</ul>

<p>So bleibt der Zugriff im Ernstfall praktikabel – und trotzdem lückenlos nachvollziehbar.</p>

<h4 id="4-read-only-freigaben-für-testament-und-tagebuch">4. Read-only-Freigaben für Testament und Tagebuch</h4>

<p>Handschriftliches Testament und Tagebuch werden der Vertrauensperson nach dem Ableben zusätzlich direkt im geschützten Bereich zur Ansicht bereitgestellt – ausschließlich lesend. Die zugehörigen Dateien liegen in einem privaten Speicher und sind nie über eine direkte Adresse abrufbar; sie werden nur über eine authentifizierte, auf die berechtigte Person geprüfte Ausgabe ausgeliefert.</p>

<h4 id="5-ein-sicherer-ort-für-persönliche-dokumente">5. Ein sicherer Ort für persönliche Dokumente</h4>

<p>Ergänzend haben wir einen geschützten Bereich für wichtige persönliche Dokumente umgesetzt – etwa Ausweis, Geburtsurkunde oder Versichertenkarte. Gerade bei Verlust oder Diebstahl bleiben die Unterlagen so digital verfügbar. Uploads werden serverseitig auf ihren tatsächlichen Dateityp geprüft (nicht nur anhand der Dateiendung), unter zufälligen Namen privat gespeichert und sind ebenfalls nur über die geprüfte, authentifizierte Ausgabe erreichbar.</p>

<h4 id="6-sicherheits-audit-und-härtung">6. Sicherheits-Audit und Härtung</h4>

<p>Weil auf dieser Plattform besonders schützenswerte Daten liegen, haben wir das gesamte System einem gründlichen Sicherheits-Audit unterzogen und die bestätigten Befunde direkt behoben. Geprüft und gehärtet wurden unter anderem:</p>

<ul>
  <li>der Schutz vor Injection-Angriffen und vor unberechtigtem Zugriff auf fremde Datensätze,</li>
  <li>die sichere Behandlung von Datei-Uploads,</li>
  <li>der Umgang mit Zugangsdaten und Geheimnissen (keine davon im Code),</li>
  <li>sowie sicherheitsrelevante Server- und HTTP-Einstellungen.</li>
</ul>

<p>Datenschutz und Vertrauen waren hier kein nachträglicher Gedanke, sondern von Anfang an ein zentraler Bestandteil des Produkts.</p>

<h3 id="das-ergebnis">Das Ergebnis</h3>

<p>Entstanden ist eine Plattform, die einem sensiblen Thema mit der nötigen Sorgfalt begegnet:</p>

<ul>
  <li><strong>Die richtigen Informationen zum richtigen Zeitpunkt</strong> – abgestuft nach Inhalt und Zeitpunkt, nicht nach einem einzelnen Schalter.</li>
  <li><strong>Serverseitig durchgesetzte Zugriffsregeln</strong> – geschützte Inhalte bleiben geschützt, auch gegen direkte Aufrufe.</li>
  <li><strong>Ein nachvollziehbarer, verschlüsselter Datenexport</strong> für den Ernstfall, gebunden an eine protokollierte Einwilligung.</li>
  <li><strong>Sichere, private Ablage</strong> aller hochsensiblen Dokumente, ausgeliefert nur über geprüfte Endpunkte.</li>
  <li><strong>Ein auditiertes, gehärtetes Fundament</strong>, das dem Vertrauen der Zielgruppe gerecht wird.</li>
</ul>

<p>Das Projekt steht kurz vor dem Livegang. Es zeigt, wie sich hohe Datenschutz-Ansprüche und eine praxistaugliche Bedienung verbinden lassen – gerade dort, wo es um die persönlichsten Unterlagen eines Menschen geht.</p>

<h3 id="fazit">Fazit</h3>

<p>Digitale Nachlass- und Notfallvorsorge ist Vertrauenssache. Ein durchdachtes, abgestuftes Berechtigungsmodell, konsequent serverseitig durchgesetzt und durch ein Sicherheits-Audit abgesichert, macht aus einem heiklen Thema ein belastbares Produkt.</p>

<p>Sie planen eine Anwendung, bei der Datenschutz und differenzierte Zugriffsrechte im Mittelpunkt stehen? <a href="/kontakt/">Sprechen Sie mit uns</a> – wir begleiten Sie von der Konzeption bis zum sicheren Livegang.</p>]]></content><author><name>christopher.zechendorf</name></author><category term="success-stories" /><summary type="html"><![CDATA[Wie wir eine Plattform für digitale Nachlass- und Notfallvorsorge mit abgestuftem, zeitlich gestaffeltem Zugriffsmodell umgesetzt haben.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ext.dev/img/post/digitale-nachlassvorsorge-abgestufte-berechtigungen.webp" /><media:content medium="image" url="https://ext.dev/img/post/digitale-nachlassvorsorge-abgestufte-berechtigungen.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Vom Regionsfilter zum durchsuchbaren Verzeichnis: Facettensuche, Karte und Profilseiten</title><link href="https://ext.dev/blog/2026/07/04/weingueter-kompass-facettensuche-verzeichnis/" rel="alternate" type="text/html" title="Vom Regionsfilter zum durchsuchbaren Verzeichnis: Facettensuche, Karte und Profilseiten" /><published>2026-07-04T08:00:00+00:00</published><updated>2026-07-04T08:00:00+00:00</updated><id>https://ext.dev/blog/2026/07/04/weingueter-kompass-facettensuche-verzeichnis</id><content type="html" xml:base="https://ext.dev/blog/2026/07/04/weingueter-kompass-facettensuche-verzeichnis/"><![CDATA[<h2 id="wenn-aus-einer-liste-ein-werkzeug-wird">Wenn aus einer Liste ein Werkzeug wird</h2>

<p>Viele Websites führen ein Verzeichnis: Mitglieder, Anbieter, Standorte, Fachbetriebe. Oft ist es historisch als lange Liste mit einem einzelnen Filter gewachsen – funktional, aber mühsam zu durchsuchen. Diese Woche ist auf <a href="https://www.vinum.eu" target="_blank">VINUM</a> der komplett überarbeitete <strong>Weingüter-Kompass</strong> live gegangen: Aus einer simplen Regionsliste wurde ein vollwertiges, durchsuchbares Verzeichnis der Winzer – mit kombinierbarer Suche, synchroner Kartenansicht und eigenen Profilseiten pro Weingut.</p>

<p>Der Beitrag zeigt am echten Beispiel, wie man ein Verzeichnis in TYPO3 vom reinen Auflisten zu einem echten Rechercheinstrument entwickelt – und warum wir dabei bewusst <em>nicht</em> zu einer Suchmaschine wie Solr gegriffen haben.</p>

<h3 id="die-herausforderung">Die Herausforderung</h3>

<p>Der bisherige Weingüter-Kompass zeigte die Weingüter nach Region gruppiert, mit genau einem Filter: der Region. Wer ein Weingut mit einem bestimmten Merkmal suchte – etwa Bio-Betriebe im Wallis, die eine bestimmte Rebsorte anbauen – musste sich durch die Liste scrollen. Jeder Treffer war zudem nur ein Stück Text: Der Klick auf einen Pin in der Karte führte auf die externe Website des Weinguts, eine eigene Profilseite im Portal gab es nicht.</p>

<p>Drei Dinge sollten sich ändern, ohne die bestehende Darstellung von heute auf morgen umzuwerfen:</p>

<ul>
  <li>Eine <strong>erweiterte Suche</strong>, die mehrere Kriterien gleichzeitig kombiniert – vergleichbar mit der Filtersuche grosser Online-Shops.</li>
  <li><strong>Eigene Profilseiten</strong> pro Weingut mit sprechender Adresse, auf die auch die Kartenpins verweisen.</li>
  <li>Ein <strong>sanfter Rollout</strong>, damit die vertraute Ansicht so lange bestehen bleibt, bis die neue wirklich rund ist.</li>
</ul>

<h3 id="unsere-lösung">Unsere Lösung</h3>

<h4 id="1-facettensuche-statt-einzelfilter">1. Facettensuche statt Einzelfilter</h4>

<p>Statt vieler einzelner Filterfelder gibt es jetzt ein zentrales Suchfeld mit Auswahlliste und Tippautomatik. Vier Dimensionen sind darin nach Gruppen sortiert kombinierbar: <strong>Region/Kanton</strong>, <strong>Angebote und Merkmale</strong> (etwa Degustation vor Ort, Bio, Übernachtung, Gastronomie), <strong>Rebsorten</strong> sowie <strong>Besonderheiten</strong>. Zusätzlich lässt sich frei nach Name, Ort oder Postleitzahl tippen.</p>

<p>Entscheidend ist die Logik dahinter: Mehrere Werte <em>derselben</em> Kategorie wirken als „oder” (Wallis oder Waadt), Werte <em>unterschiedlicher</em> Kategorien als „und” (Region Wallis <strong>und</strong> Rebsorte Pinot Noir). So lässt sich die Trefferliste präzise eingrenzen, ohne dass die Bedienung kompliziert wird. Umgesetzt haben wir das mit der bereits im Projekt vorhandenen Auswahlkomponente – ohne eine neue Abhängigkeit einzuführen.</p>

<p>Zwei Details aus den echten Daten waren dabei wichtig:</p>

<ul>
  <li><strong>Akzent- und Gross-/Kleinschreibung ignorieren:</strong> „Genève” und „geneve”, „Pinot Noir” und „pinot noir” müssen gleich behandelt werden. Wir normalisieren die Suchbegriffe deshalb intern (Zerlegung in Grundbuchstaben und diakritische Zeichen), sodass die Suche unabhängig von Akzenten trifft.</li>
  <li><strong>Robustheit bei Lücken:</strong> Ein Weingut ohne hinterlegte Rebsorte oder ohne Merkmale muss trotzdem über Name oder Ort auffindbar bleiben – und darf den Aufbau der Suche nicht durcheinanderbringen.</li>
</ul>

<p>Die Liste aktualisiert sich <strong>sofort und ohne Neuladen</strong> der Seite; „Zurücksetzen” stellt die vollständige Liste wieder her.</p>

<h4 id="2-lokale-filterung-statt-suchmaschine--die-bewusste-abwägung">2. Lokale Filterung statt Suchmaschine – die bewusste Abwägung</h4>

<p>Naheliegend wäre gewesen, für die Suche auf Solr zu setzen – die Suchmaschine, die auf VINUM die eigentliche Weinsuche mit über 100.000 Degustationsnotizen antreibt. Für den Weingüter-Kompass haben wir uns bewusst dagegen entschieden.</p>

<p>Der Grund ist die Datenmenge: Die vollständige Liste der Weingüter steht ohnehin bereits vollständig auf der Seite. In dieser Grössenordnung braucht es keine serverseitige Suchabfrage – die Filterung passiert direkt im Browser auf den bereits geladenen Daten. Das hat zwei handfeste Vorteile: Die Treffer erscheinen <strong>verzögerungsfrei</strong> (kein Server-Roundtrip, kein Neuladen), und es entsteht <strong>keine zusätzliche Infrastruktur</strong>, die betrieben, aktualisiert und indexiert werden müsste.</p>

<p>Die Faustregel, die wir dabei anlegen: Eine Suchmaschine wie Solr lohnt sich, wenn Datenmengen in die Tausende gehen, Volltextrelevanz und Rankings gefragt sind oder die Daten gar nicht erst vollständig in den Browser passen. Für ein überschaubares, kuratiertes Verzeichnis ist die schlanke, clientseitige Lösung schneller <em>und</em> günstiger im Betrieb. Das richtige Werkzeug hängt an der Datenmenge – nicht an der Frage, was technisch am meisten hermacht.</p>

<h4 id="3-karte-und-profilseiten-die-zusammenspielen">3. Karte und Profilseiten, die zusammenspielen</h4>

<p>Jedes Weingut hat jetzt eine <strong>eigene Profilseite</strong> mit sprechender Adresse – aufgebaut wie die etablierten Mitgliederprofile im Portal: Kopfbereich, Beschreibung, Merkmale als Icons, Kontaktdaten, Öffnungszeiten und Kartenausschnitt. Damit diese Adressen sauber und suchmaschinenfreundlich sind, haben wir eine entsprechende Routing-Regel ergänzt, statt kryptische Parameter-URLs auszugeben.</p>

<p>Der zweite Schritt betrifft die Karte: Die <strong>Pins verweisen jetzt auf die Profilseite</strong> im Portal statt auf die externe Website des Weinguts. Karte und Liste ziehen dadurch am selben Strang – der Besucher bleibt im Verzeichnis und findet alle Informationen an einem Ort, statt auf verstreute externe Seiten geschickt zu werden.</p>

<h4 id="4-sanfter-rollout-über-einen-umschalter">4. Sanfter Rollout über einen Umschalter</h4>

<p>Ein solcher Umbau darf die bestehende Seite nicht von einem Tag auf den anderen ersetzen. Deshalb steckt hinter dem Kompass ein <strong>Umschalter im Redaktionsbereich</strong>: In der Voreinstellung „Legacy” bleibt die klassische Listendarstellung Byte für Byte unverändert. Erst der Modus „Neu” aktiviert das überarbeitete Verzeichnis mit Facettensuche, Profilseiten und profilverlinkten Kartenpins.</p>

<p>So liessen sich die neuen Funktionen in Ruhe entwickeln und prüfen, während die Live-Seite unangetastet blieb. Ein Nebeneffekt der Überarbeitung: In der neuen Ansicht sind alle Regionen vollständig auswählbar – die 26 Schweizer Kantone und die 13 deutschen Anbaugebiete – und nicht nur jene, zu denen bereits ein Profil erfasst ist.</p>

<h4 id="5-self-service-für-die-winzer">5. Self-Service für die Winzer</h4>

<p>Ein Verzeichnis ist nur so gut wie seine Pflege. Damit die Inhalte aktuell bleiben, pflegen die Winzer ihre Angaben <strong>selbst</strong> – im bereits bestehenden, login-geschützten Bearbeitungsbereich. Im Zuge des Umbaus haben wir dort zwei neue Felder ergänzt: <strong>Instagram</strong> und <strong>Facebook</strong>. Sie funktionieren genau wie das bestehende Website-Feld und erscheinen anschliessend auf der Profilseite.</p>

<p>Der Clou liegt im Aufwand: Weil die Bearbeitung die Daten ohnehin schon direkt speichert, genügten das Datenmodell, das Formularfeld und die Datenbankspalte – kein Eingriff in die Ablauflogik. Neue Felder ohne Backend-Umbau, gepflegt von denen, die sie am besten kennen: den Weingütern selbst.</p>

<h3 id="das-ergebnis">Das Ergebnis</h3>

<p>Aus einer statischen Regionsliste ist ein Verzeichnis geworden, mit dem Besucher tatsächlich arbeiten: Sie kombinieren Kriterien über mehrere Dimensionen, sehen Treffer sofort in Liste und Karte, und gelangen mit einem Klick auf eine vollständige Profilseite – alles ohne Wartezeiten und ohne den Umweg über externe Websites. Die Winzer halten ihre Profile selbst aktuell, und die vertraute Darstellung blieb während des gesamten Umbaus verfügbar.</p>

<h3 id="fazit">Fazit</h3>

<p>Ein durchsuchbares Verzeichnis muss keine schwere Suchinfrastruktur bedeuten. Wer die Datenmenge realistisch einschätzt, das richtige Werkzeug wählt und den Umbau über einen Umschalter absichert, kommt schnell zu einer Lösung, die sich verzögerungsfrei bedient und günstig betreiben lässt. Facettensuche, Kartenanbindung und Self-Service-Pflege lassen sich so auch auf Mitglieder-, Anbieter- oder Standortverzeichnisse ganz unterschiedlicher Branchen übertragen.</p>

<p>Betreiben Sie ein Verzeichnis, das mehr sein soll als eine lange Liste? <a href="/kontakt/">Sprechen Sie uns an</a> – wir schauen uns Ihre Such- und Übersichtsseiten gerne an.</p>]]></content><author><name>christopher.zechendorf</name></author><category term="development" /><summary type="html"><![CDATA[Aus einer simplen Regionsliste wurde ein durchsuchbares Winzer-Verzeichnis: Facettensuche, synchrone Kartenansicht und eigene Profilseiten in TYPO3.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ext.dev/img/post/weingueter-kompass-facettensuche-verzeichnis.webp" /><media:content medium="image" url="https://ext.dev/img/post/weingueter-kompass-facettensuche-verzeichnis.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Double-Opt-In neu gedacht: Wartbare Newsletter-Anmeldung mit Mautic</title><link href="https://ext.dev/blog/2026/07/03/double-opt-in-mautic-wartbar/" rel="alternate" type="text/html" title="Double-Opt-In neu gedacht: Wartbare Newsletter-Anmeldung mit Mautic" /><published>2026-07-03T09:00:00+00:00</published><updated>2026-07-03T09:00:00+00:00</updated><id>https://ext.dev/blog/2026/07/03/double-opt-in-mautic-wartbar</id><content type="html" xml:base="https://ext.dev/blog/2026/07/03/double-opt-in-mautic-wartbar/"><![CDATA[<p>Ein Double-Opt-In ist für den Newsletter-Versand rechtlich Pflicht – jede Anmeldung muss per Klick in einer Bestätigungsmail verifiziert werden. Doch wie man diesen Bestätigungsschritt technisch aufhängt, entscheidet darüber, ob die Marketing-Datenbank auf Dauer wartbar bleibt. Bei <a href="https://www.vinum.eu" target="_blank">VINUM</a>, der internationalen Wein-Plattform, haben wir die Double-Opt-In-Strecke einer dreisprachigen Newsletter-Anmeldung mit <a href="https://www.mautic.org" target="_blank">Mautic</a> von Grund auf entkoppelt – und dabei einen Fallstrick beseitigt, der die größte Tabelle der Datenbank blockiert hatte.</p>

<h3 id="die-herausforderung">Die Herausforderung</h3>

<p>In der ursprünglichen Struktur lag der Versand der Bestätigungsmail als Schritt innerhalb einer veröffentlichten Mautic-Kampagne. Das klingt naheliegend, hat aber eine unangenehme Nebenwirkung: Die Tabelle <code class="language-plaintext highlighter-rouge">email_stats</code> – die mit Abstand größte Tabelle der Mautic-Datenbank – wird dadurch <strong>load-bearing</strong>. Löscht man alte Versand-Statistiken, um Platz zu schaffen, „scharf” sich der Kampagnenschritt erneut und Mautic verschickt bereits zugestellte Bestätigungsmails ein zweites Mal. Ein einfaches Aufräumen war damit ausgeschlossen.</p>

<p>Betroffen waren die drei Kern-Newsletter für Deutschland, die Deutschschweiz und die französische Schweiz mit zusammen rund 120.000 „festverdrahteten” Statistikzeilen. Erschwerend kam die Mehrsprachigkeit hinzu: Jede Sprachvariante braucht ihre eigene Bestätigungsmail und ihren eigenen Bestätigungslink – und die französische Schweiz nutzt einen abweichenden französischen Bestätigungspfad.</p>

<h3 id="unsere-lösung">Unsere Lösung</h3>

<p>Der Grundgedanke: Der Mailversand gehört nicht in die Kampagne, sondern ans Formular. Wenn die Bestätigungsmail direkt beim Absenden des Anmeldeformulars ausgelöst wird, ist sie von der Kampagnen-Statistik entkoppelt – und <code class="language-plaintext highlighter-rouge">email_stats</code> wird wieder frei aufräumbar.</p>

<h4 id="1-bestätigungsmail-direkt-aus-dem-formular">1. Bestätigungsmail direkt aus dem Formular</h4>

<p>Für jede Sprache haben wir ein eigenständiges Anmeldeformular aufgebaut, das die Bestätigungsmail selbst per Form-Action verschickt. Kein Kampagnenschritt hängt mehr am Mailversand, und die Statistiktabelle verliert ihre kritische Rolle.</p>

<h4 id="2-tags-als-statusmaschine-pending--confirmed">2. Tags als Statusmaschine: pending → confirmed</h4>

<p>Beim Absenden markiert das Formular den Kontakt als „ausstehend” – mit einem generischen und einem sprachspezifischen Tag. Erst der Klick in der Bestätigungsmail überführt ihn in den Status „bestätigt”. Diese Zustandsführung über Tags macht den gesamten Ablauf transparent und nachvollziehbar, ohne dass Kontakte vorschnell in die Verteiler geraten.</p>

<h4 id="3-ein-sprachübergreifendes-trigger-segment">3. Ein sprachübergreifendes Trigger-Segment</h4>

<p>Ein einziges Trigger-Segment sammelt genau die Kontakte ein, die „ausstehend” sind <strong>und</strong> die Bestätigungsseite aufgerufen haben. Weil die französische Schweiz einen anderen Bestätigungspfad verwendet, matcht der URL-Filter beide Varianten per regulärem Ausdruck (REGEXP) statt über einen simplen Textvergleich. Das Segment startet leer und saugt so keine historischen Besucher ein – es reagiert ausschließlich auf frische Bestätigungen.</p>

<p>Eine schlanke Double-Opt-In-Kampagne ohne jeden Mailversand-Schritt schaltet die Kontakte anschließend anhand ihres Sprach-Tags in das richtige Empfänger-Segment – und räumt die „pending”-Tags gleich wieder auf.</p>

<h4 id="4-saubere-anbindung-von-typo3">4. Saubere Anbindung von TYPO3</h4>

<p>Die Website läuft auf TYPO3. Der <code class="language-plaintext highlighter-rouge">NewsletterHelper</code>, der die Anmeldedaten an Mautic übergibt, zeigt jetzt pro Sprache auf die neuen Formulare statt auf die alten Kampagnen-Formulare. Weil die neuen Formulare feldidentische Klone sind, blieb der Rest der Integration – inklusive der Abmelde-Strecke – unverändert.</p>

<h4 id="5-varianten-versand-über-segmente-statt-kampagnen">5. Varianten-Versand über Segmente statt Kampagnen</h4>

<p>Auch den eigentlichen Newsletter-Versand haben wir von Kampagnen auf Segment-Mails (Broadcasts) umgestellt. Die zwei Fassungen jeder Ausgabe – mit und ohne Werbebanner – bilden wir über je zwei überschneidungsfreie Versand-Segmente pro Sprache ab. So bleibt auch der reguläre Versand frei von der <code class="language-plaintext highlighter-rouge">email_stats</code>-Kopplung, und die Auswahl der Empfänger ist über die Segmentdefinition sauber dokumentiert statt in Kampagnenlogik versteckt.</p>

<h3 id="das-ergebnis">Das Ergebnis</h3>

<p>Die Statistiktabellen sind wieder aufräumbar (prune-bar), ohne dass jemals eine alte Bestätigungsmail erneut verschickt wird. Die gesamte Strecke wurde end-to-end über alle drei Sprachvarianten getestet – von der Anmeldung über die Bestätigungsmail und den Klick bis zur korrekten Einsortierung in das jeweilige Empfänger-Segment. Der rechtlich vorgeschriebene Double-Opt-In-Schritt bleibt dabei vollständig erhalten; entkoppelt haben wir nur die Technik dahinter.</p>

<h3 id="fazit">Fazit</h3>

<p>Double-Opt-In ist kein Häkchen, das man einmal setzt – es ist eine Architekturentscheidung. Ob der Bestätigungsversand in der Kampagne oder am Formular hängt, klingt nach einem Detail, entscheidet aber darüber, ob die Datenbank in ein paar Jahren noch pflegbar ist. Wer solche Weichen früh richtig stellt, spart sich später aufwendige Migrationen und riskante Aufräumaktionen im laufenden Betrieb.</p>

<p>Sie planen eine mehrsprachige Newsletter-Strecke oder kämpfen mit einer Marketing-Datenbank, die sich nicht mehr aufräumen lässt? <a href="/kontakt/">Sprechen Sie mit uns</a> – wir schauen uns Ihre Double-Opt-In- und Segment-Architektur gerne an.</p>]]></content><author><name>christopher.zechendorf</name></author><category term="development" /><summary type="html"><![CDATA[Wie wir bei VINUM die Double-Opt-In-Strecke der mehrsprachigen Newsletter-Anmeldung mit Mautic entkoppelt haben, damit die Statistiktabellen wartbar bleiben.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ext.dev/img/post/double-opt-in-mautic-wartbar.webp" /><media:content medium="image" url="https://ext.dev/img/post/double-opt-in-mautic-wartbar.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Schluss mit den 500ern: robuste Datenhaltung gegen Edge-Case-Eingaben in TYPO3</title><link href="https://ext.dev/blog/2026/06/30/robuste-datenhaltung-edge-case-eingaben-typo3/" rel="alternate" type="text/html" title="Schluss mit den 500ern: robuste Datenhaltung gegen Edge-Case-Eingaben in TYPO3" /><published>2026-06-30T08:00:00+00:00</published><updated>2026-06-30T08:00:00+00:00</updated><id>https://ext.dev/blog/2026/06/30/robuste-datenhaltung-edge-case-eingaben-typo3</id><content type="html" xml:base="https://ext.dev/blog/2026/06/30/robuste-datenhaltung-edge-case-eingaben-typo3/"><![CDATA[<h2 id="wenn-nutzer-ihre-eigenen-daten-pflegen">Wenn Nutzer ihre eigenen Daten pflegen</h2>

<p>Self-Service-Formulare im Frontend sind komfortabel: Statt jede Änderung über eine Agentur oder eine Redaktion laufen zu lassen, pflegen Verkoster, Winzer und Veranstalter ihre Inhalte selbst. Auf der von uns betreuten Weinplattform <a href="https://www.vinum.eu" target="_blank">VINUM</a> reichen sie so Verkostungsnotizen ein, legen Events an und aktualisieren ihre Profilangaben.</p>

<p>Genau diese Formulare sind aber auch ein Magnet für Eingaben, mit denen das Datenmodell nie gerechnet hat. Ein leer gelassenes Feld, eine zweistellige Jahreszahl, ein zu langer Freitext, ein Emoji – alltägliche Kleinigkeiten, die in Produktion einen harten Serverfehler auslösen, die Eingabe verwerfen und den Nutzer auf einer generischen Fehlerseite zurücklassen. Innerhalb einer Woche haben wir gleich mehrere solcher Abstürze aufgespürt und behoben. Dieser Artikel zeigt an echten Fällen, warum sie entstehen – und wie man sie systematisch verhindert, statt nur Symptome zu bekämpfen.</p>

<h2 id="die-herausforderung">Die Herausforderung</h2>

<p>Die Fehler hatten alle dasselbe Grundmuster: Zwischen dem, was ein Formular zulässt, und dem, was die Datenbankspalte tatsächlich akzeptiert, klafft eine Lücke. TYPO3 mit seinem Extbase-Framework bildet ein abgeschicktes Formular auf ein Datenobjekt ab und schreibt es in die Datenbank. Passt der Wert nicht in die Spalte, wirft die Datenbank einen Fehler – und ohne sauberes Abfangen wird daraus ein HTTP-500, der beim Nutzer als Absturz ankommt.</p>

<p>Tückisch daran ist, dass diese Fehler unterhalb der Beschwerdeschwelle bleiben. Es sind keine spektakulären Ausfälle, sondern einzelne Nutzer, deren Eingabe „einfach nicht gespeichert” wird. Kaum jemand meldet das – es taucht nur als wiederkehrende Zeile im Produktions-Log auf. Vier Eingabemuster traten dabei besonders zuverlässig als Crash-Auslöser hervor:</p>

<ul>
  <li><strong>Ein leeres Pflichtfeld.</strong> Beim Speichern einer Verkostungsnotiz ohne Rangangabe brach das Speichern mit einer <code class="language-plaintext highlighter-rouge">NOT NULL</code>-Verletzung ab.</li>
  <li><strong>Ein Datum vor 1970.</strong> Wer im Event-Formular eine zweistellige oder unplausible Jahreszahl eintippte, erzeugte einen negativen Zeitstempel, den eine <code class="language-plaintext highlighter-rouge">unsigned</code>-Spalte rundheraus ablehnte.</li>
  <li><strong>Ein zu langer Freitext.</strong> Die Beschreibung von Sehenswürdigkeiten eines Weinguts lief in eine <code class="language-plaintext highlighter-rouge">varchar(255)</code>-Spalte – ab dem 256. Zeichen quittierte die Datenbank mit „Data too long”.</li>
  <li><strong>Ein Emoji.</strong> Ein Smiley oder Sonderzeichen in einer Beschreibung enthält 4-Byte-Zeichen, die der ältere 3-Byte-Zeichensatz der Datenbank nicht abbilden kann – „Incorrect string value”.</li>
</ul>

<h2 id="unsere-lösung">Unsere Lösung</h2>

<p>So unterschiedlich die Auslöser wirken, die Verteidigung folgt demselben Prinzip: Man kann sich nicht auf eine einzelne Schutzschicht verlassen. Robuste Datenhaltung heisst, an mehreren Stellen gleichzeitig dafür zu sorgen, dass eine unerwartete Eingabe entweder sauber gespeichert oder freundlich abgewiesen wird – aber nie zu einem Absturz führt.</p>

<h3 id="1-leere-felder-zuverlässig-normalisieren">1. Leere Felder zuverlässig normalisieren</h3>

<p>Lässt ein Nutzer ein Pflichtfeld leer, macht Extbase daraus nicht etwa eine <code class="language-plaintext highlighter-rouge">0</code>, sondern ein <code class="language-plaintext highlighter-rouge">null</code>. Trifft dieses <code class="language-plaintext highlighter-rouge">null</code> auf eine Spalte, die per Definition keinen Leerwert erlaubt, schlägt die Datenbank-Bedingung zu. Im konkreten Fall existierte sogar bereits eine Absicherung – aber nur in einem von drei Speicherpfaden. Über die beiden ungesicherten Pfade rutschte das <code class="language-plaintext highlighter-rouge">null</code> weiterhin durch.</p>

<p>Die Lösung war, die Normalisierung nicht in jeder einzelnen Aktion zu wiederholen, sondern zentral im Datenmodell zu verankern: Ein fehlender Wert wird genau einmal – direkt beim Setzen der Eigenschaft – auf den etablierten Standardwert <code class="language-plaintext highlighter-rouge">0</code> zurückgeführt. Damit ist jeder heutige <em>und</em> jeder künftige Speicherpfad automatisch abgedeckt, ohne dass jemand daran denken muss.</p>

<h3 id="2-datumseingaben-streng-prüfen--und-schon-im-browser-verhindern">2. Datumseingaben streng prüfen – und schon im Browser verhindern</h3>

<p>Das Event-Formular liess freie Datumseingaben zu. Die zugrunde liegende Parsing-Funktion war dabei zu nachsichtig: Aus einer zweistelligen oder unplausiblen Jahreszahl erzeugte sie trotzdem ein gültiges Datum – bei einem Wert vor 1970 einen negativen Zeitstempel. Die zugehörige Datenbankspalte war jedoch <code class="language-plaintext highlighter-rouge">unsigned</code>, also auf positive Werte beschränkt, und wies den negativen Wert ab.</p>

<p>Hier haben wir auf zwei Ebenen angesetzt. Serverseitig parsen wir das Datum jetzt streng, prüfen es auf Plausibilität und lehnen Werte vor 1970 ab; statt eines Absturzes wird der Nutzer mit einem deutschen Hinweis zum Formular zurückgeführt, und es wird nichts Halbfertiges gespeichert. Zusätzlich erzwingen wir das Format <code class="language-plaintext highlighter-rouge">TT.MM.JJJJ</code> direkt im Browser: Die Datumsfelder prüfen die Eingabe noch vor dem Absenden, und der Kalender-Button trägt automatisch das richtige Format ein. So entsteht der Fehler gar nicht erst – und falls doch etwas durchkommt, fängt ihn die serverseitige Prüfung als Sicherheitsnetz ab.</p>

<h3 id="3-spaltentyp-und-eingabefeld-in-übereinstimmung-bringen">3. Spaltentyp und Eingabefeld in Übereinstimmung bringen</h3>

<p>Ein mehrzeiliges Textfeld ohne Längenbegrenzung, dahinter aber eine Spalte, die nur 255 Zeichen fasst – diese Diskrepanz war für „Data too long” verantwortlich. Bei mehrsprachigen Texten verschärft sich das noch, weil Umlaute und Akzente mehr als ein Byte belegen und die Grenze damit früher erreicht ist als bei reiner Zeichenzählung.</p>

<p>Die richtige Antwort war nicht, die Eingabe künstlich zu beschränken, sondern den Spaltentyp an die tatsächliche Nutzung anzupassen: Aus <code class="language-plaintext highlighter-rouge">varchar(255)</code> wurde ein <code class="language-plaintext highlighter-rouge">text</code>-Feld, das auch lange Beschreibungen vollständig aufnimmt – konsistent mit den benachbarten Freitextfeldern. Als zusätzliche Absicherung kürzen wir überlange Eingaben byte-genau auf die maximale Kapazität, falls jemand einen wirklich exzessiven Text einfügt. Bei der Gelegenheit haben wir die verwandten Felder desselben Formulars gleich mitgeprüft, um dieselbe Falle nicht an anderer Stelle offen zu lassen.</p>

<h3 id="4-den-zeichensatz-auf-die-realität-von-2026-heben">4. Den Zeichensatz auf die Realität von 2026 heben</h3>

<p>Emojis und Sonderzeichen aus dem erweiterten Unicode-Raum bestehen aus 4-Byte-Sequenzen. Der ältere Zeichensatz <code class="language-plaintext highlighter-rouge">utf8</code> in MySQL/MariaDB kann pro Zeichen aber nur 3 Byte speichern und lehnt solche Eingaben mit „Incorrect string value” ab. Für Nutzer, die heute selbstverständlich ein Emoji in eine Beschreibung setzen, ist das eine alltägliche Eingabe – für die Datenbank ein Absturz.</p>

<p>Wir haben deshalb die Datenbankverbindung und die betroffenen Textspalten auf <code class="language-plaintext highlighter-rouge">utf8mb4</code> umgestellt, den vollwertigen 4-Byte-Zeichensatz. Wichtig dabei: Die reine Schema-Migration von TYPO3 fasst die Kodierung bestehender Spalten nicht an – die bestehenden Daten mussten gezielt mit einem eigenen, wiederholbar ausführbaren Migrationsschritt nachgezogen werden. Erst danach werden Emojis verlustfrei gespeichert, statt die Eingabe abzuweisen.</p>

<h3 id="5-die-logs-als-frühwarnsystem-nutzen">5. Die Logs als Frühwarnsystem nutzen</h3>

<p>Der eigentliche Hebel hinter all diesen Fixes ist, dass wir die Fehler überhaupt gesehen haben, bevor sich jemand beschwert hat. Möglich macht das eine <a href="/blog/2026/06/15/aus-logzeilen-werden-tickets/">tägliche, automatisierte Auswertung der Produktions-Logs</a>, die wiederkehrende Fehler clustert, vom Rauschen trennt und als fertig recherchiertes Entwickler-Ticket aufbereitet. Aus „dreimal am Wochenende ein 500er” wird so ein priorisiertes Ticket mit Ursachenanalyse – nicht erst ein verärgerter Anruf Wochen später.</p>

<h2 id="das-ergebnis">Das Ergebnis</h2>

<p>Innerhalb einer Woche wurden aus einer Handvoll unscheinbarer Logzeilen konkrete, behobene Fehler. Jeder einzelne hätte für sich genommen kaum jemandem auffallen müssen – in Summe waren es aber wiederkehrende Datenverluste für echte Nutzer, die ihre Eingaben verloren, ohne zu verstehen, warum.</p>

<p>Genauso wichtig wie die Einzelfixes ist das Muster dahinter: Statt das jeweils nächste defekte Feld zu flicken, haben wir die Schutzschichten dort verankert, wo sie alle Eingabewege abdecken – im Datenmodell, im Spaltentyp, im Zeichensatz und in der Validierung vor dem Absenden. Eine unerwartete Eingabe führt jetzt zu einem klaren Hinweis oder einer sauberen Speicherung, nicht mehr zu einer Fehlerseite.</p>

<h2 id="fazit">Fazit</h2>

<p>Frontend-Self-Service ist ein Gewinn an Autonomie für die Nutzer – aber er verschiebt die Verantwortung für saubere Daten von der Redaktion zur Software. Jedes Feld, das ein Nutzer selbst befüllt, muss damit rechnen, dass das Modell auf eine Eingabe trifft, die es so nie erwartet hat. Robuste Datenhaltung bedeutet, diese Edge Cases nicht als Ausnahme, sondern als Normalfall zu behandeln: passende Spaltentypen, ein zeitgemässer Zeichensatz, serverseitige Validierung statt blossem Vertrauen auf den Browser – und ein Blick in die Produktions-Logs, der echte Probleme sichtbar macht, bevor sie eskalieren.</p>

<p>Häufen sich auf Ihrer Website unerklärliche Speicherfehler oder verlorene Formulareingaben? <a href="/kontakt/">Sprechen Sie uns an</a> – wir schauen uns Ihre Datenhaltung gerne systematisch an und härten sie gegen genau solche Edge Cases.</p>]]></content><author><name>christopher.zechendorf</name></author><category term="development" /><summary type="html"><![CDATA[Wie wir auf einer TYPO3-Plattform wiederkehrende 500er-Fehler aus Self-Service-Formularen aufgespürt und durch robuste Datenhaltung verhindert haben.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ext.dev/img/post/robuste-datenhaltung-edge-case-eingaben-typo3.webp" /><media:content medium="image" url="https://ext.dev/img/post/robuste-datenhaltung-edge-case-eingaben-typo3.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Wenn Inhalte nicht übersetzt werden sollen: Mehrsprachigkeit in TYPO3 richtig modellieren</title><link href="https://ext.dev/blog/2026/06/29/mehrsprachigkeit-typo3-sprachneutrale-inhalte/" rel="alternate" type="text/html" title="Wenn Inhalte nicht übersetzt werden sollen: Mehrsprachigkeit in TYPO3 richtig modellieren" /><published>2026-06-29T08:00:00+00:00</published><updated>2026-06-29T08:00:00+00:00</updated><id>https://ext.dev/blog/2026/06/29/mehrsprachigkeit-typo3-sprachneutrale-inhalte</id><content type="html" xml:base="https://ext.dev/blog/2026/06/29/mehrsprachigkeit-typo3-sprachneutrale-inhalte/"><![CDATA[<h2 id="nicht-alles-was-mehrsprachig-ist-will-übersetzt-werden">Nicht alles, was mehrsprachig ist, will übersetzt werden</h2>

<p>Mehrsprachige Websites gelten als gelöstes Problem: Man legt die Sprachen an, übersetzt die Inhalte, fertig. In der Praxis steckt die Komplexität woanders – nämlich in der Frage, <em>welche</em> Inhalte überhaupt übersetzt gehören. Auf einer Plattform wie <a href="https://www.vinum.eu" target="_blank">VINUM</a>, die in drei Sprachräumen (Deutschland, Schweiz und französische Schweiz) ausgespielt wird, sind manche Daten echte Übersetzungen – ein Titel etwa lautet auf Französisch anders. Andere Daten sind dagegen sprachneutral: Eine Verkostungsnotiz zu einem bestimmten Wein ist dieselbe, egal in welcher Sprachversion sie erscheint.</p>

<p>Vermischt man beide Fälle im Datenmodell, entstehen Probleme, die zunächst nichts mit Sprache zu tun zu haben scheinen: Datensätze vermehren sich, Tabellen wachsen ins Uferlose, und die Suche zeigt veraltete Ergebnisse. Dieser Beitrag zeigt am echten Projekt, wie man die Sprach-Architektur von Anfang an sauber aufsetzt – und wie wir genau diese Wurzel bei einer bestehenden Plattform saniert haben.</p>

<h3 id="die-herausforderung">Die Herausforderung</h3>

<p>Auf der Weinplattform waren mehrere Objekte als “übersetzbar” modelliert, die es inhaltlich gar nicht sind. Eine Degustation (ein Verkostungsereignis) etwa hat zahlreiche untergeordnete Verkostungsnotizen. Diese Notizen sind sprachneutral – und doch hingen sie an einem übersetzbaren Eltern-Objekt.</p>

<p>Das Tückische: TYPO3 nimmt eine solche Modellierung wörtlich. Sobald ein übersetztes Eltern-Objekt gespeichert wird, versucht das System, dessen untergeordnete Datensätze “mitzuübersetzen” – und legt dabei Kopien an. Bei jedem weiteren Speichern entstehen neue Kopien. Eine einzige grosse Degustation wuchs so von gut 800 auf rund 148.000 Notiz-Datensätze. Über alle Objekte hinweg sammelten sich Tausende Dubletten und verwaiste Verknüpfungen an.</p>

<p>Die Folgen waren konkret spürbar: Die öffentliche Weinsuche zeigte für eine aktuelle Verkostung keine frischen Treffer mehr, weil die Notizen mit der falschen Sprach-Kennung im Suchindex landeten. Und ein zuvor selbst gebauter “Aufräum-Mechanismus”, der die Klone nachträglich wieder löschte, kämpfte nur gegen das Symptom – nicht gegen die Ursache.</p>

<h3 id="unsere-lösung">Unsere Lösung</h3>

<h4 id="1-die-eigentliche-ursache-geklonte-datensätze-beim-übersetzen">1. Die eigentliche Ursache: geklonte Datensätze beim Übersetzen</h4>

<p>Der Kern des Problems steckte in einer einzigen Einstellung an den verschachtelten Beziehungen (Inline-Relationen) zwischen Eltern- und Kind-Datensätzen. Sie wies TYPO3 an, die Kind-Datensätze beim Lokalisieren gesondert zu behandeln. Der TYPO3-DataHandler – die zentrale Komponente, die alle Schreibvorgänge verarbeitet – interpretiert das so: Fehlt ein Kind-Datensatz in der Übersetzung, <em>kopiere</em> ihn. Diese Kopien tragen keine saubere Rückverknüpfung zum Original, weshalb das System sie bei der nächsten Prüfung erneut für “fehlend” hält und wieder kopiert – ein Aufschaukeln ohne Obergrenze.</p>

<p>Der erste Schritt war daher, diese Einstellung von den betroffenen Inline-Relationen zu entfernen. Damit endet die Vermehrung beim Speichern. Wichtig dabei: Das ist Vorbeugung, nicht Nachsorge. In der Verifikation haben wir reproduziert, dass dasselbe Speichern vorher Hunderte Kopien erzeugte und danach exakt null.</p>

<h4 id="2-bewusste-entscheidung-sprachneutrale-inhalte-als-alle-sprachen">2. Bewusste Entscheidung: sprachneutrale Inhalte als “alle Sprachen”</h4>

<p>Die Vermeidung der Klone war nur die halbe Miete. Der eigentliche Designfehler war, sprachneutrale Objekte überhaupt als übersetzbar zu deklarieren. Also haben wir sie konsequent als das modelliert, was sie sind: sprachneutral.</p>

<p>TYPO3 kennt dafür eine eigene Sprach-Kennung – den Wert “alle Sprachen” (intern <code class="language-plaintext highlighter-rouge">sys_language_uid = -1</code>). Ein so markierter Datensatz gilt in jeder Sprachversion und wird nie übersetzt oder kopiert. Die Verkostungsnotizen tragen jetzt durchgängig diese Kennung; die übergeordneten Objekte sind nicht mehr übersetzbar.</p>

<p>Damit das auch dauerhaft hält, mussten wir alle Wege absichern, auf denen neue Datensätze entstehen – sowohl die Bearbeitung im Backend als auch die Erstellung über das Frontend. An jeder dieser Stellen wird die Sprach-Kennung “alle Sprachen” erzwungen. Zusätzlich haben wir die bereits falsch gespeicherten Datensätze – über 5.000 Stück – per Migrationsskript auf den korrekten Wert zurückgeführt.</p>

<h4 id="3-das-shadow-field-muster-für-die-wenigen-sprachabhängigen-felder">3. Das Shadow-Field-Muster für die wenigen sprachabhängigen Felder</h4>

<p>Bleibt die Frage: Was ist mit den Feldern, die <em>doch</em> eine eigene französische Fassung brauchen, etwa der Titel einer Kategorie? Die naheliegende Antwort – “dann macht den ganzen Datensatz eben übersetzbar” – führt direkt zurück ins Klon-Problem.</p>

<p>Stattdessen nutzen wir ein Shadow-Field-Muster: Neben dem normalen Titelfeld erhält das Objekt eine zusätzliche Spalte allein für die französische Fassung. Der Datensatz bleibt sprachneutral, nur dieses eine Feld kennt eine Variante. Damit nicht Dutzende Ausgabestellen einzeln angepasst werden müssen, läuft die Titel-Ausgabe über einen zentralen Mechanismus, der im französischen Kontext automatisch das Französisch-Feld liefert und sonst den Standardtitel. So liessen sich rund 57 Stellen in den Templates mechanisch und ohne Regressionsrisiko umstellen.</p>

<h4 id="4-verbundene-übersetzung-statt-selbstgebauter-aufräum-logik">4. Verbundene Übersetzung statt selbstgebauter Aufräum-Logik</h4>

<p>Einige wenige Objekte – etwa Wettbewerbs- und Guide-Kategorien – brauchen tatsächlich eine echte Titelübersetzung. Für sie nutzen wir nun die “verbundene” Übersetzung, die TYPO3 von Haus aus mitbringt: Jede Kategorie bekommt genau <em>eine</em> fest verknüpfte Übersetzung pro Sprache. Anders als beim Kopiermechanismus erkennt das System diese Verknüpfung wieder und dupliziert sie bei späteren Änderungen nicht erneut.</p>

<p>Mit dem sauberen Standardverhalten wurde der selbst gebaute Workaround überflüssig. Wir konnten den nachträglichen “Klone-aufräumen”-Mechanismus und das zugehörige Wartungskommando ersatzlos entfernen. Toter Code, der zuvor jede Bereitstellung mitschleppte, ist verschwunden – das System wurde nicht nur korrekt, sondern auch schlanker.</p>

<h4 id="5-sprach-fallback-und-menüs-das-gate-wieder-einsetzen">5. Sprach-Fallback und Menüs: das Gate wieder einsetzen</h4>

<p>Während der Vorbereitung auf TYPO3 v13 fiel eine vermeintlich überflüssige Einstellung weg, die Seiten ohne Übersetzung in der aktuellen Sprache aus den Menüs ausblendete. Die Folge: Plötzlich tauchten fremdsprachige Seiten in den deutschen, schweizerischen und französischen Menüs auf.</p>

<p>Die Korrektur stellt dieses “Gate” auf eine v13-taugliche Weise wieder her: Ein Menü zeigt nur noch Seiten, die in der aktuellen Sprache tatsächlich übersetzt sind. Der inhaltliche Fallback bleibt davon unberührt – für Deutschland und die Deutschschweiz greift weiterhin der Standardinhalt, wenn keine Übersetzung existiert, während die französische Version bewusst nur französische Inhalte zeigt. Sichtbarkeit der Seite und Fallback des Inhalts sind damit zwei getrennte, jeweils korrekt gesteuerte Mechanismen.</p>

<h4 id="6-auswirkung-auf-die-suche">6. Auswirkung auf die Suche</h4>

<p>Der rote Faden dieses Projekts: Konsistente Sprach-Kennungen sind die Voraussetzung dafür, dass die <a href="/blog/2024/02/11/beschleunigte-suche/">SolR-Suche</a> aktuelle Ergebnisse liefert. Solange die Verkostungsnotizen mit uneinheitlichen Sprach-IDs gespeichert waren, wurde der Index für einzelne Sprachen nicht zuverlässig aktualisiert – genau deshalb fehlten in der Weinsuche die frischen Treffer.</p>

<p>Nachdem die Datensätze einheitlich als “alle Sprachen” geführt und die betroffenen Indizes neu aufgebaut waren, lieferte die Suche in allen drei Sprachräumen wieder konsistente, aktuelle Resultate.</p>

<h3 id="das-ergebnis">Das Ergebnis</h3>

<p>Nach der Sanierung dupliziert weder das Übersetzen noch das Speichern noch irgendeinen Datensatz. Die wenigen wirklich sprachabhängigen Felder erscheinen pro Sprache korrekt, die Suche ist über alle Sprachräume konsistent, und ein ganzer Block selbst gebauter Aufräum-Logik konnte ersatzlos entfallen.</p>

<p>Die wichtigste Lehre ist keine technische, sondern eine konzeptionelle: Mehrsprachigkeit beginnt nicht beim Übersetzen, sondern bei der Entscheidung, <em>was</em> übersetzt werden soll. Wer für jedes Objekt früh festlegt, ob es echt übersetzbar oder sprachneutral ist, und das im Datenmodell sauber abbildet, erspart sich Dubletten, explodierende Tabellen und schwer auffindbare Suchfehler. Nachträglich diese Wurzel zu sanieren ist möglich – es ist nur deutlich aufwendiger, als es von Beginn an richtig zu modellieren.</p>

<p>Planen Sie eine mehrsprachige Website oder kämpfen Sie mit einer bestehenden TYPO3-Installation, die sich beim Übersetzen seltsam verhält? <a href="/kontakt/">Sprechen Sie uns an</a> – wir schauen uns Ihre Sprach-Architektur gemeinsam an.</p>]]></content><author><name>christopher.zechendorf</name></author><category term="development" /><summary type="html"><![CDATA[Manche Inhalte sind übersetzbar, andere sprachneutral. Wie wir bei VINUM Dubletten, explodierende Tabellen und falsche Suchergebnisse an der Wurzel beheben.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ext.dev/img/post/mehrsprachigkeit-typo3-sprachneutrale-inhalte.webp" /><media:content medium="image" url="https://ext.dev/img/post/mehrsprachigkeit-typo3-sprachneutrale-inhalte.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Weniger Handarbeit: Mitglieder, Profile und Abo-Rechte automatisch zuordnen</title><link href="https://ext.dev/blog/2026/06/17/mitglieder-profile-abo-rechte-automatisch-zuordnen/" rel="alternate" type="text/html" title="Weniger Handarbeit: Mitglieder, Profile und Abo-Rechte automatisch zuordnen" /><published>2026-06-17T09:00:00+00:00</published><updated>2026-06-17T09:00:00+00:00</updated><id>https://ext.dev/blog/2026/06/17/mitglieder-profile-abo-rechte-automatisch-zuordnen</id><content type="html" xml:base="https://ext.dev/blog/2026/06/17/mitglieder-profile-abo-rechte-automatisch-zuordnen/"><![CDATA[<p>Mitglieder- und Abo-Plattformen leben von einer Frage: Wer darf was? Hinter jedem Login steht die Entscheidung, welche Inhalte, Profile und Funktionen eine Person sehen und bearbeiten darf. Solange diese Zuordnung von Hand gepflegt wird, kostet sie Zeit – und ist fehleranfällig. Bei <a href="https://www.vinum.eu" target="_blank">VINUM</a>, der internationalen Wein-Plattform, haben wir in derselben Woche gleich an mehreren Stellen die manuelle Pflege durch ein sauberes Datenmodell und eine Schnittstellen-Anbindung ersetzt.</p>

<h3 id="die-herausforderung">Die Herausforderung</h3>

<p>Zwei Pflege-Aufgaben fraßen besonders viel Zeit.</p>

<p>Im <strong>Weingüter-Bereich</strong> konnte ein Winzer-Profil bisher mit genau einem Benutzerkonto verknüpft werden. Die Realität sieht anders aus: Hinter einem Weingut stehen oft mehrere Personen, die das Profil pflegen – und umgekehrt betreut eine Person manchmal mehrere Weingüter. Die starre Eins-zu-eins-Verknüpfung konnte das nicht abbilden, und das Zuordnen von Profil und Konto geschah weitgehend von Hand. Erschwerend kam eine Mehrdeutigkeit hinzu: Teilen sich mehrere Konten dieselbe Adresse, an wen geht dann das Profil?</p>

<p>Bei den <strong>Abo-Rechten</strong> gab es eine Wartezeit. Wer ein Abonnement abschloss und sich ein Konto anlegte, erhielt die zugehörigen Zugriffsrechte erst nach dem nächtlichen Datenabgleich – im Zweifel bis zu einen Tag später. Für jemanden, der gerade bezahlt hat und sofort loslegen möchte, ist das ein spürbarer Bruch im Erlebnis.</p>

<h3 id="unsere-lösung">Unsere Lösung</h3>

<h4 id="1-vom-einzelwert-zur-echten-beziehung">1. Vom Einzelwert zur echten Beziehung</h4>

<p>Aus der starren Eins-zu-eins-Verknüpfung haben wir eine echte n:m-Beziehung gemacht: Ein Weingut kann von mehreren Konten verwaltet werden, und ein Konto kann mehrere Weingüter betreuen. Für Nutzer mit nur einem Profil ändert sich nichts – sie landen wie gewohnt direkt im Bearbeitungsformular. Wer mehrere Weingüter betreut, bekommt einen kurzen Auswahlschritt vorgeschaltet (“Welches Weingut möchten Sie bearbeiten?”). Alle bestehenden Verknüpfungen – mehrere hundert – haben wir mit einer wiederholbaren Migration verlustfrei in das neue Modell überführt.</p>

<h4 id="2-profile-automatisch-über-die-adresse-zuordnen">2. Profile automatisch über die Adresse zuordnen</h4>

<p>Das saubere Modell löst die alte Mehrdeutigkeit auf. Winzer-Profile werden jetzt automatisch den Abonnenten zugeordnet, die sich dieselbe Adresse teilen – eingebettet in die bestehende Logik für Rechte und Benutzergruppen. Teilen sich mehrere Konten eine Adresse, werden eben alle mit dem Profil verknüpft. Genau das war mit dem alten Einzelwert-Feld unmöglich.</p>

<h4 id="3-sofort-freigeschaltet-beim-login">3. Sofort freigeschaltet beim Login</h4>

<p>Statt auf den nächtlichen Abgleich zu warten, fragen wir beim Login eine externe Abonnement-Schnittstelle ab. Existiert für die E-Mail-Adresse ein aktives Abonnement, werden die passenden Zugriffsrechte sofort gewährt. Vier Entscheidungen waren uns dabei wichtig:</p>

<ul>
  <li><strong>Additiv:</strong> Die Abfrage kann Rechte nur <em>vergeben</em>, nie entziehen. Der nächtliche Export bleibt die maßgebliche Quelle – so gehen Rechte, die auf einem anderen Weg vergeben wurden, nicht verloren.</li>
  <li><strong>Nur wenn nötig:</strong> Wer das Recht bereits hat, löst gar keine Abfrage aus. Das vermeidet einen unnötigen Schnittstellen-Aufruf bei jedem Login.</li>
  <li><strong>Ausfallsicher:</strong> Der Login darf niemals blockiert oder spürbar verzögert werden. Schlägt die Abfrage fehl, passiert schlicht nichts – der Nutzer kommt normal hinein.</li>
  <li><strong>Konfigurierbar:</strong> Die Funktion ist nur aktiv, wenn die Zugangsdaten hinterlegt sind. So lässt sich die Schnittstelle später ohne Code-Änderung umstellen.</li>
</ul>

<h4 id="4-zugriffsrechte-vollständig-und-konsistent-halten">4. Zugriffsrechte vollständig und konsistent halten</h4>

<p>Damit die Abo-Rechte über alle Wege gleich gepflegt werden, sorgt zusätzlich eine wiederkehrende Hintergrund-Aufgabe für Ordnung: Sie gleicht alle Konten mit den aktiven Abonnements ab, vergibt die zugehörige Zugriffsgruppe an alle Berechtigten – und entzieht sie denen, die sie nicht mehr haben dürfen. Entscheidend war, dass das Kriterium die <strong>vollständige</strong> Definition eines Abonnenten abbildet: So behalten bestehende Abonnenten ihre Gruppe (keine Regression), während die bisher übersehenen Konten sie endlich automatisch erhalten.</p>

<h3 id="das-ergebnis">Das Ergebnis</h3>

<ul>
  <li><strong>Weniger manuelle Backend-Pflege</strong> – Zuordnungen geschehen automatisch über das Datenmodell statt per Hand.</li>
  <li><strong>Schnellere Freischaltung</strong> – Zugriffsrechte greifen sofort beim Login statt erst am nächsten Tag.</li>
  <li><strong>Weniger Support-Tickets</strong> rund um fehlende oder verspätete Freischaltungen.</li>
  <li><strong>Mehrere Verwalter pro Weingut</strong> sind jetzt möglich – ohne Behelfslösungen.</li>
  <li><strong>Konsistente Rechte</strong> über alle Wege hinweg: beim Login wie im nächtlichen Abgleich.</li>
</ul>

<h3 id="fazit">Fazit</h3>

<p>Manuelle Pflege ist selten ein Personalproblem – meistens ist sie ein Datenmodell-Problem. Wo ein Einzelwert die Wirklichkeit nicht abbilden kann, entsteht Handarbeit; wo eine Schnittstelle fehlt, entsteht Wartezeit. Eine saubere n:m-Modellierung und eine ausfallsichere API-Anbindung räumen beides aus dem Weg – und senken den Pflegeaufwand spürbar, ohne die Datenhoheit aus der Hand zu geben.</p>

<p>Stehen auch bei Ihnen wiederkehrende Zuordnungen im Backend oder Wartezeiten bei der Freischaltung im Weg? <a href="/kontakt/">Sprechen Sie mit uns</a> – wir schauen uns Ihr Datenmodell und Ihre Schnittstellen gemeinsam an.</p>]]></content><author><name>christopher.zechendorf</name></author><category term="development" /><summary type="html"><![CDATA[Wie wir bei VINUM Winzer-Profile und Abo-Rechte automatisch zuordnen: saubere Datenmodellierung, Zuordnung über die Adresse, Sofort-Freischaltung beim Login.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ext.dev/img/post/mitglieder-profile-abo-rechte-automatisch-zuordnen.webp" /><media:content medium="image" url="https://ext.dev/img/post/mitglieder-profile-abo-rechte-automatisch-zuordnen.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Lokales SEO mit schema.org: LocalBusiness- und Person-Markup für Verzeichnisseiten</title><link href="https://ext.dev/blog/2026/06/16/schema-org-strukturierte-daten-verzeichnisseiten/" rel="alternate" type="text/html" title="Lokales SEO mit schema.org: LocalBusiness- und Person-Markup für Verzeichnisseiten" /><published>2026-06-16T08:00:00+00:00</published><updated>2026-06-16T08:00:00+00:00</updated><id>https://ext.dev/blog/2026/06/16/schema-org-strukturierte-daten-verzeichnisseiten</id><content type="html" xml:base="https://ext.dev/blog/2026/06/16/schema-org-strukturierte-daten-verzeichnisseiten/"><![CDATA[<h2 id="wenn-suchmaschinen-ihre-verzeichniseinträge-wirklich-verstehen">Wenn Suchmaschinen Ihre Verzeichniseinträge wirklich verstehen</h2>

<p>Verzeichnis- und Mitgliederseiten sind oft die wertvollsten, aber am schlechtesten auffindbaren Seiten einer Website: hunderte Detailprofile mit echten Daten – Namen, Adressen, Telefonnummern, Bildern – die für Suchmaschinen jedoch nur Fließtext sind. Genau hier setzen strukturierte Daten an. Auf der Weinplattform <a href="https://www.vinum.eu" target="_blank">VINUM</a> haben wir in einer Arbeitswoche mehrere Detailseiten mit <a href="https://schema.org" target="_blank">schema.org</a>-Markup ausgezeichnet: Winzer-Profile aus dem Weingüter-Kompass und Mitglieder des Wine Trade Clubs als <code class="language-plaintext highlighter-rouge">LocalBusiness</code>, Autorinnen und Autoren als <code class="language-plaintext highlighter-rouge">Person</code>. Das Besondere daran: Es war kein einziger Umbau am Datenmodell nötig.</p>

<h2 id="die-herausforderung">Die Herausforderung</h2>

<p>Strukturierte Daten – maschinenlesbare Auszeichnungen im Format <a href="https://json-ld.org" target="_blank">JSON-LD</a> – sind die Eintrittskarte für <a href="https://developers.google.com/search/docs/appearance/structured-data" target="_blank">Rich Results</a> und für die lokale Suche. Eine Suchmaschine, die erkennt, dass es sich bei einem Eintrag um ein konkretes Unternehmen mit Adresse und Telefonnummer handelt, kann diesen Eintrag deutlich prominenter und in passenderen Kontexten ausspielen als eine reine Textseite.</p>

<p>Der typische Reflex lautet: “Dann brauchen wir erst einmal ein neues Datenmodell mit allen Feldern, die schema.org vorsieht.” Das ist teuer, langwierig und meistens unnötig. Unsere Ausgangslage war eine andere:</p>

<ul>
  <li>Es existierten bereits gepflegte Profilseiten mit Namen, Adressen, Bildern und Kontaktdaten.</li>
  <li>Ein Umbau der Datenstruktur (neue Felder, Datenbankmigrationen, Backend-Masken) sollte vermieden werden.</li>
  <li>Trotzdem sollte das Markup <strong>valide</strong> sein – also kein halbfertiges JSON-LD, das Suchmaschinen verwerfen.</li>
</ul>

<p>Die Aufgabe war damit weniger eine Frage des Datenmodells als eine Frage der sauberen Umsetzung: Wie erzeugt man aus vorhandenen Feldern ein gültiges, robustes und wartbares Markup?</p>

<h2 id="unsere-lösung">Unsere Lösung</h2>

<h3 id="1-ein-wiederverwendbares-service-muster">1. Ein wiederverwendbares Service-Muster</h3>

<p>Statt das Markup verstreut in den Templates zusammenzustückeln, kapselt jede Entität ihre Logik in einem eigenen kleinen Service mit genau einer Aufgabe: aus einem Objekt das fertige <code class="language-plaintext highlighter-rouge">&lt;script type="application/ld+json"&gt;</code>-Tag erzeugen – oder einen leeren String, wenn keine sinnvollen Daten vorliegen.</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">final</span> <span class="kd">class</span> <span class="nc">VineyardProfileSchemaService</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">function</span> <span class="n">buildJsonLd</span><span class="p">(</span><span class="kt">VineyardProfile</span> <span class="nv">$profile</span><span class="p">,</span> <span class="kt">string</span> <span class="nv">$absoluteUrl</span><span class="p">):</span> <span class="kt">string</span>
    <span class="p">{</span>
        <span class="c1">// … Felder einsammeln, leere verwerfen, kodieren …</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Der Controller ruft diesen Service auf der Detailseite auf und hängt das Ergebnis – falls nicht leer – in den Seitenkopf:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$jsonLd</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">schemaService</span><span class="o">-&gt;</span><span class="nf">buildJsonLd</span><span class="p">(</span><span class="nv">$profile</span><span class="p">,</span> <span class="nv">$absoluteUrl</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$jsonLd</span> <span class="o">!==</span> <span class="s1">''</span><span class="p">)</span> <span class="p">{</span>
    <span class="nv">$pageRenderer</span><span class="o">-&gt;</span><span class="nf">addHeaderData</span><span class="p">(</span><span class="nv">$jsonLd</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Dieses Muster haben wir vom bereits bestehenden Event-Markup übernommen und für Winzer-Profile, Wine-Trade-Club-Mitglieder und Autoren jeweils 1:1 repliziert. Der Vorteil: Jeder Service ist klein, testbar und folgt derselben Struktur. Neue Entitäten lassen sich in Stunden statt Tagen ergänzen.</p>

<h3 id="2-minimal-gültige-objekte-per-graceful-omission">2. Minimal-gültige Objekte per “graceful omission”</h3>

<p>Der Schlüssel zum Verzicht auf einen Datenmodell-Umbau liegt in einer einfachen Erkenntnis: Für ein <code class="language-plaintext highlighter-rouge">LocalBusiness</code> schreibt schema.org <strong>nur den Namen</strong> als Pflichtfeld vor. Alles andere – Telefon, Bild, Adresse, Öffnungszeiten – ist optional.</p>

<p>Wir bauen deshalb das Objekt aus genau den Feldern, die bereits vorhanden sind, und lassen leere Felder konsequent weg, statt Platzhalter oder neue Felder zu erzwingen. Technisch sorgt ein abschließender Filter dafür, dass keine leeren oder <code class="language-plaintext highlighter-rouge">null</code>-Werte im Ergebnis landen:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$data</span> <span class="o">=</span> <span class="nb">array_filter</span><span class="p">([</span>
    <span class="s1">'@context'</span>  <span class="o">=&gt;</span> <span class="s1">'https://schema.org'</span><span class="p">,</span>
    <span class="s1">'@type'</span>     <span class="o">=&gt;</span> <span class="s1">'LocalBusiness'</span><span class="p">,</span>
    <span class="s1">'name'</span>      <span class="o">=&gt;</span> <span class="nv">$profile</span><span class="o">-&gt;</span><span class="nf">getVineyardName</span><span class="p">(),</span>   <span class="c1">// Pflicht</span>
    <span class="s1">'telephone'</span> <span class="o">=&gt;</span> <span class="nv">$profile</span><span class="o">-&gt;</span><span class="nf">getPhone</span><span class="p">(),</span>          <span class="c1">// wird verworfen, wenn leer</span>
    <span class="s1">'image'</span>     <span class="o">=&gt;</span> <span class="nv">$imageUrls</span><span class="p">,</span>
<span class="p">]);</span>
</code></pre></div></div>

<p>Fehlt der Name, liefert der Service einen leeren String – und es wird gar kein Markup ausgegeben. So entsteht nie ungültiges JSON-LD, egal wie vollständig ein einzelner Eintrag gepflegt ist.</p>

<h3 id="3-der-passende-typ-je-eintragsart">3. Der passende Typ je Eintragsart</h3>

<p>Nicht jede Verzeichnisseite ist gleich. Wir haben pro Eintragsart den semantisch korrekten schema.org-Typ gewählt:</p>

<ul>
  <li><strong>Winzer-Profile</strong> und <strong>Wine-Trade-Club-Mitglieder</strong> sind Unternehmen und damit <code class="language-plaintext highlighter-rouge">LocalBusiness</code>. Da schema.org keinen eigenen Typ “Weingut” kennt, ergänzen wir bei den Winzern ein <code class="language-plaintext highlighter-rouge">additionalType</code>, das auf den passenden <a href="https://www.wikidata.org" target="_blank">Wikidata</a>-Eintrag verweist – so bleibt der Eintrag spezifisch, ohne den Standard zu verlassen.</li>
  <li><strong>Autorinnen und Autoren</strong> sind Personen und damit <code class="language-plaintext highlighter-rouge">Person</code>. Hier hinterlegen wir zusätzlich die Organisation, für die sie arbeiten, als verschachteltes <code class="language-plaintext highlighter-rouge">Organization</code>-Objekt.</li>
</ul>

<p>Als <code class="language-plaintext highlighter-rouge">url</code> dient jeweils die kanonische, absolut aufgebaute Adresse der Detailseite selbst – denn nur die indexierbare Einzelseite trägt SEO-Wert, nicht die Listenansicht.</p>

<h3 id="4-sicher-kodiert--editierbare-inhalte-gehören-abgesichert">4. Sicher kodiert – editierbare Inhalte gehören abgesichert</h3>

<p>Verzeichnisdaten werden oft von Redaktion oder den Mitgliedern selbst gepflegt. Solche Inhalte landen direkt im Seitenkopf – ein klassisches Einfallstor für XSS, wenn man unachtsam kodiert. Wir kodieren das JSON-LD mit <code class="language-plaintext highlighter-rouge">JSON_UNESCAPED_UNICODE</code>, behalten aber das Standard-Escaping der Schrägstriche bewusst bei. So wird ein im Text verstecktes <code class="language-plaintext highlighter-rouge">&lt;/script&gt;</code> zu <code class="language-plaintext highlighter-rouge">&lt;\/script&gt;</code> und kann das umgebende Script-Tag nicht vorzeitig schließen. Umlaute und Akzente bleiben dabei trotzdem lesbar.</p>

<h3 id="5-welche-zusatzfelder-einen-eintrag-wirklich-aufwerten">5. Welche Zusatzfelder einen Eintrag wirklich aufwerten</h3>

<p>Mit dem minimal-gültigen Objekt ist der schnelle, günstige Hebel umgesetzt. Im nächsten Schritt stellt sich die Frage, welche <strong>zusätzlichen</strong> Felder den Aufwand lohnen. Gemeinsam mit dem Kunden haben wir hier priorisiert – nicht jedes denkbare Feld, sondern die mit dem größten Nutzen zuerst:</p>

<ul>
  <li><strong>Strukturierte Öffnungszeiten</strong> statt Freitext: als maschinenlesbare <code class="language-plaintext highlighter-rouge">openingHoursSpecification</code> (Wochentag, Öffnen, Schließen). Das ist der aufwändigste, aber auch wirkungsvollste Schritt, weil er die lokale Suche direkt bedient.</li>
  <li><strong>Eine eigene Beschreibung</strong>, die den Eintrag inhaltlich greifbar macht.</li>
  <li><strong>Gründungsjahr, Anbaufläche und Anbaumethode</strong> als optionale Detailfelder, die Profil und Markup zusätzlich anreichern.</li>
</ul>

<p>Wichtig dabei: Jedes neue Feld bleibt optional und wird – wenn leer – weder auf der Seite noch im Markup ausgegeben. Das hält Profile aufgeräumt und das JSON-LD stets gültig.</p>

<h2 id="das-ergebnis">Das Ergebnis</h2>

<p>Mehrere Hundert Detailseiten der Plattform liefern Suchmaschinen jetzt strukturierte, valide Daten – Unternehmen als <code class="language-plaintext highlighter-rouge">LocalBusiness</code>, Autoren als <code class="language-plaintext highlighter-rouge">Person</code>. Erreicht haben wir das ohne neue Datenbankfelder, ohne Migrationen und ohne Backend-Umbau, allein aus dem vorhandenen Bestand. Das wiederverwendbare Service-Muster macht jede weitere Eintragsart zu einer kleinen, klar umrissenen Erweiterung statt zu einem Projekt.</p>

<h2 id="fazit">Fazit</h2>

<p>Strukturierte Daten sind einer der wenigen SEO-Hebel, die viel bewirken und wenig kosten – vorausgesetzt, man baut sie pragmatisch: minimal-gültige Objekte aus vorhandenen Feldern, leere Werte konsequent weglassen, sicher kodieren und die Logik in kleine, wiederverwendbare Services kapseln. Gerade Verzeichnis-, Mitglieder- und Profilseiten profitieren davon enorm, weil sie viele indexierbare Einzelseiten mit echten Daten besitzen.</p>

<p>Sie betreiben selbst ein Verzeichnis, ein Mitglieder- oder Branchenportal und möchten Ihre Detailseiten für Suchmaschinen besser auffindbar machen? <a href="/kontakt/">Sprechen Sie mit uns</a> – wir schauen uns an, welcher Markup-Hebel bei Ihnen am schnellsten wirkt.</p>]]></content><author><name>christopher.zechendorf</name></author><category term="development" /><summary type="html"><![CDATA[Wie wir Verzeichnis- und Profilseiten der Weinplattform VINUM mit schema.org-JSON-LD ausgezeichnet haben -- ohne Datenmodell-Umbau, aus vorhandenen Feldern.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ext.dev/img/post/schema-org-strukturierte-daten-verzeichnisseiten.webp" /><media:content medium="image" url="https://ext.dev/img/post/schema-org-strukturierte-daten-verzeichnisseiten.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>