Shopware Migration ohne Bauchschmerzen: mein pragmatischer Leitfaden aus Projekten

Letzte Woche bei einem Kunden: Freitag 16:30, der alte Shop macht Zicken, und plötzlich ist klar: Wenn wir die Migration heute nicht sauber durchziehen, hängt das ganze Wochenende dran. Klingt dramatisch, war’s auch ein bisschen. Und das Verrückte: Nicht die Datenmigration war das Problem, sondern Redirects und ein winziger Unterschied in der URL-Generierung. So ein Ding, das du halt erst merkst, wenn Google & Co. schon anfangen, 404s zu sammeln.
Interessiert an diesem Thema?
Kontaktieren Sie uns für eine kostenlose Beratung →Shopware-Migration ist selten „nur“ ein Update. Es ist eher ein Umzug mit Renovierung, bei dem du nebenbei noch die Klingelschilder (SEO) und die Lagerverwaltung (ERP) nicht verlieren darfst.
Problem Statement: Warum Shopware-Migrationen sich so oft „schmutzig“ anfühlen
In den meisten Projekten ist die Datenbankabfrage der Hauptflaschenhals
Beispiel: Enterprise E-Commerce Platform
Fehlerrate von 2.5% auf 0.3% gesenkt
Migration Checklist
- ✓ Backup erstellt
- ✓ Test-Umgebung aufgesetzt
- ✓ Daten-Mapping definiert
- ✓ Rollback-Plan dokumentiert
- ✓ Stakeholder informiert
Du willst eigentlich nur von A nach B: z.B. Shopware 5 auf Shopware 6, oder Shopware 6 von einer instabilen Historie auf eine saubere Neuinstallation. In der Realität hast du aber:
- Daten, die irgendwie „gewachsen“ sind (alte Custom-Felder, Plugins, doppelte Kunden, kaputte Medienreferenzen).
- SEO, das am seidenen Faden hängt (URLs, Canonicals, Indexierung, Weiterleitungen).
- Integrationen, die keiner mehr anfassen will (ERP, PIM, Payment, Versand, Tracking).
Ein Fehler, den ich oft sehe: Wir migrieren „alles“, ohne vorher zu entscheiden, was eigentlich noch gebraucht wird. Das rächt sich später, weil du Müll halt perfekt konserviert hast.
Technical Background: Was bei Shopware technisch wirklich relevant ist
Wenn wir über Shopware 6 sprechen, sind ein paar Dinge entscheidend, sonst tappst du im Dunkeln:
- DAL & IDs: Interne IDs sind UUIDs. Wenn du aus SW5 kommst, hast du andere Identitäten/Mapping-Themen.
- SEO URLs: Shopware generiert SEO-URLs pro Sales Channel, Sprache und manchmal mit Regeln, die sich je nach Konfiguration unterscheiden.
- Media: Medien sind nicht „nur Dateien“. Es geht um Pfade, Thumbnails, Zuordnungen, und ob du die alten Files überhaupt noch hast.
- Plugins: Viele SW5-Plugins gibt’s in SW6 nicht 1:1. Funktionalität muss man manchmal nachbauen oder ersetzen.
Ich bin nicht 100% sicher, ob das überall gleich ist, aber in den meisten Setups scheitert’s nicht an „zu wenig Know-how“, sondern an zu wenig Klarheit: Was ist Quelle der Wahrheit? DB? PIM? ERP? Oder irgendwo ein CSV-Export von 2019.
Implementation Steps (aber locker): So gehen wir meistens ran
1) Erstmal Inventur: Daten, URLs, Integrationen
Wir ziehen uns am Anfang eine Art Snapshot:
- Produktanzahl, Varianten, Eigenschaften, Hersteller
- Kategorien und Tiefe
- CMS/Shopping Experiences (falls schon SW6)
- SEO: Top-Landingpages, Top-Kategorieseiten, Top-Produkte, wichtige Filterseiten
- Integrationen: ERP, PIM, Payment, Versand, Tracking, Consent
Und dann kommt die Frage, die sich keiner gern stellt: Was davon ist wirklich noch relevant?
2) Migrationsweg wählen (und ehrlich sein)
Typisch:
- SW5 → SW6: Migrations-Assistent + viel Nacharbeit (Mapping, Medien, SEO).
- SW6 → SW6 (Rebuild): Oft die sauberste Variante, wenn das System „verpluginisiert“ ist.
- Big Bang vs. Stufenweise: Big Bang ist schneller, stufenweise ist stressfreier – aber kostet mehr Koordination.
Übrigens: Wenn ein Kunde sagt „Wir wollen am SEO nichts verlieren“, dann ist das kein Satz. Das ist eine Anforderungsliste. 😉
3) Datenmigration: kontrolliert, wiederholbar, messbar
Wir bauen die Migration so, dass wir sie mehrfach laufen lassen können. Einmal „final“ ist nett, aber du brauchst 2–3 Probeläufe, sonst entdeckst du die echten Probleme zu spät.
-- Quick sanity checks in the target DB
-- TODO: später optimieren, aktuell reicht's für Spot-Checks
SELECT COUNT(*) AS products FROM product;
SELECT COUNT(*) AS variants FROM product WHERE parent_id IS NOT NULL;
SELECT COUNT(*) AS categories FROM category;
SELECT COUNT(*) AS customers FROM customer;
-- Find products without media assigned (often a migration pitfall)
SELECT p.product_number
FROM product p
LEFT JOIN product_media pm ON pm.product_id = p.id
WHERE pm.product_id IS NULL
LIMIT 50;
Wenn da plötzlich 30% der Produkte ohne Media sind, weißt du: Da geht’s jetzt nicht um „später mal hübsch machen“, sondern um grundlegende Referenzen.
4) SEO: Redirects, Canonicals, Indexierung – der Teil, der weh tut
Wir exportieren die wichtigsten alten URLs und mappen auf neue. Klingt banal. Ist es nicht, weil:
- Slug-Logik ist anders
- Kategorien können umsortiert sein
- Mehrsprachigkeit ändert Pfade
Ein Fehler, den ich oft sehe: Man macht Redirects „ungefähr“. Also von /produktname auf /. Das ist SEO-technisch halt ein Eigentor.
import csv
import re
# Wir bauen uns hier eine simple Redirect-Mapping-Datei (alt -> neu)
# Quelle kann z.B. ein SW5 URL-Export + ein SW6 SEO-URL-Export sein.
def normalize(url: str) -> str:
url = url.strip()
url = re.sub(r"\?.*$", "", url) # Query weg
if not url.startswith("/"):
url = "/" + url
return url
old_to_new = {}
with open("old_urls.csv", newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
old_to_new[normalize(row["old_url"])]=None
with open("new_urls.csv", newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
# hier nehmen wir mal product_number als Join-Key, je nach Export anders
key = row.get("product_number")
if not key:
continue
new_url = normalize(row["seo_url"]) # z.B. /de/produkt/foo
# TODO: bessere Matching-Strategie, wenn product_number nicht reicht
old_to_new[key] = new_url
# Ausgabe als CSV für Import in Redirect-Tool oder Webserver-Regeln
with open("redirects_out.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["source", "target", "code"])
for source, target in old_to_new.items():
if not target:
continue
w.writerow([source, target, 301])
Ja, das Script ist bewusst simpel. In echten Projekten hängen wir da Matching über IDs, Produktnummern, alte SEO-Pfade, teilweise sogar Content-Hashing dran. Aber als Start ist das okay, weil du schnell siehst, wo du Lücken hast.
5) Theme/Storefront: erst Funktion, dann Pixel
Wenn du aus SW5 kommst: Vergiss, dass du das Theme „mal eben“ portierst. Wir priorisieren:
- Checkout stabil
- Produktdetailseite korrekt (Preis, Varianten, Verfügbarkeit)
- Kategorie/Listing performant
- Tracking & Consent sauber
Pixelperfekt kommt später. Sonst landest du in einem CSS-Sumpf und keiner weiß, ob der Warenkorb überhaupt tut.
// Beispiel: kleines Migrations-Utility, um im Admin-API Kontext zu prüfen,
// ob bestimmte Entities nach Migration vorhanden sind.
// TODO: später um Retry/Rate-Limit Handling erweitern
type TokenResponse = { access_token: string };
async function getToken(baseUrl: string, clientId: string, clientSecret: string) {
const res = await fetch(`${baseUrl}/api/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "client_credentials",
client_id: clientId,
client_secret: clientSecret,
}),
});
if (!res.ok) throw new Error(`Auth failed: ${res.status}`);
return (await res.json()) as TokenResponse;
}
async function countProducts(baseUrl: string, token: string): Promise<number> {
const res = await fetch(`${baseUrl}/api/search/product`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ limit: 1 }),
});
if (!res.ok) throw new Error(`Product search failed: ${res.status}`);
const data = await res.json();
return data.total as number;
}
(async () => {
const baseUrl = process.env.SHOPWARE_URL!;
const token = await getToken(baseUrl, process.env.CLIENT_ID!, process.env.CLIENT_SECRET!);
const total = await countProducts(baseUrl, token.access_token);
console.log(`Total products in target: ${total}`);
})();
Best Practices, die sich wirklich bewährt haben
- Migration wiederholbar bauen: Wenn du nur „einmal final“ kannst, hast du eigentlich schon verloren.
- URL-Strategie vorher festzurren: Sprache im Pfad? Kategorien im Produkt-URL? Das muss vor Redirect-Erstellung klar sein.
- Medien früh testen: Thumbnails, CDN/Proxy, Dateirechte, Storage (S3) – das bricht gern spät und nervt.
- Plugin-Liste radikal kürzen: Alles, was „nice to have“ ist, erstmal raus. Danach gezielt rein.
- Observability ab Tag 1: Logs, Error-Tracking, Core Web Vitals, 404-Monitoring. Sonst tappst du nach Go-Live im Nebel.
Real-World Example: Der Redirect-Fail, der uns fast den Launch gekostet hat
Bei einem Projekt letzten Monat haben wir SW5 → SW6 migriert. Daten sahen okay aus, Checkout lief, Kunde happy. Dann kam der Pre-Launch Crawl: Plötzlich tausende 404s. Warum? Weil im alten Shop Produkt-URLs ohne Kategoriepfad waren, im neuen aber mit Kategoriepfad generiert wurden. Unser Redirect-Mapping basierte auf „Slug-only“. Hat irgendwie gepasst. Bis halt nicht mehr.
Wir haben’s dann so gefixt:
- Top 5.000 URLs aus Analytics/SEO-Tool priorisiert
- Mapping über Produktnummer + Sprache gebaut
- Für den Rest: saubere Fallback-Regel auf Produktdetail via produktnummer (nicht auf Startseite)
War bisschen extra Arbeit, aber dadurch blieb das Ranking stabiler als erwartet. Und ganz ehrlich: Ohne diese Priorisierung hätten wir uns im Long Tail totoptimiert.
Wenn du mir sagst, von welchem Setup du kommst (SW5? SW6? Multishop? Sprachen? ERP/PIM?), kann ich dir auch sagen, wo ich als erstes Bauchschmerzen hätte – und was wir als „Minimalplan“ für einen sicheren Go-Live nehmen würden.


