Wie ich ein europäisches Unternehmen rettete, das von einer Agentur betrogen wurde. Migration von PHP 5.6 und jQuery auf React, Django, PostgreSQL, Redis und Rust. Code, Metriken und Lektionen.
DE

Wie ich ein Unternehmen rettete, das von einer Agentur betrogen wurde: Umbau auf modernen Technologie-Stack

5.00 /5 - (31 Stimmen )
Zuletzt überprüft: 1. Mai 2026
18Min. Lesezeit
Fallstudie
Full-Stack-Entwickler

Er rief mich an einem Freitagabend an. Die Stimme ruhig, aber ich hoerte darin die Erschoepfung eines Menschen, der seit Wochen gegen ein Problem kaempft, über das er keine Kontrolle hat. Er führt ein Dienstleistungsunternehmen, das Kunden in vier europaeischen Ländern betreut. Er bezählte einer Agentur für eine professionelle Internet-Plattform. Er bekam etwas, das wie eine professionelle Plattform aussah. Darunter verbarg sich eine Katastrophe.

Aufgrund einer Vertraulichkeitsvereinbarung kann ich weder den Namen des Unternehmens noch die Branche offenlegen. Ich kann jedoch genau erzaehlen, was ich vorfand, was ich tat und warum ich diese und keine anderen Technologien waehlte. Diese Geschichte ist eine Warnung für jeden, der den Aufbau einer Plattform an eine externe Agentur auslagert.


#Was ich beim Audit der Agentur-Plattform vorfand

Der erste Schritt ist immer ein Audit. Ich bewerte nicht, ich kritisiere nicht. Ich sammle Fakten. Nach drei Tagen Analyse hatte ich ein vollstaendiges Bild.

Spaghetti-Code auf PHP 5.6: Die Agentur verwendete kein Framework. Die gesamte Plattform besteht aus monolithischem prozeduralem Code in PHP 5.6 (Ende des Supports 2018) mit SQL-Abfragen, die direkt in HTML-Templates eingefuegt wurden. Kein ORM, keine Abstraktionsschicht, kein Router. Dateien mit 3.000 Zeilen, die Geschaeftslogik mit Praesentation vermischen.

// Gefundener Code -- SQL-Abfrage direkt im Template (anonymisiert)
<?php
$result = mysql_query("SELECT * FROM services 
    WHERE category = '" . $_GET['cat'] . "' 
    ORDER BY id DESC");
// SQL injection -- brak jakiejkolwiek walidacji danych wejściowych
while ($row = mysql_fetch_assoc($result)) {
    echo "<div class='service'>";
    echo "<h2>" . $row['title'] . "</h2>"; // XSS -- brak escapowania
    echo "<p>" . $row['description'] . "</p>";
    echo "</div>";
}
?>

MySQL 5.5 ohne Indizes: Eine Datenbank mit 47 Tabellen, von denen keine Indizes ausser Primärschluesseln hatte. Eine Abfrage, die Dienste mit Filtern auflistet, führte einen Full Table Scan auf 200.000 Datensaetzen durch, mittlere Antwortzeit 4,7 Sekunden.

jQuery 1.x + Bootstrap 3: Frontend aus dem Jahr 2014. Zwoelf jQuery-Dateien wurden auf jeder Seite geladen, darunter drei verschiedene Versionen der Bibliothek. Keine Minifizierung, kein Bundler, kein Tree-Shaking. Gesamtgröße der Skripte: 2,8 MB.

FTP als “Deployment”: Kein Git-Repository, kein CI/CD, keine Staging-Umgebung. Die Agentur lud Dateien direkt per FTP auf den Produktionsserver hoch. Kein Versionskontrollsystem. Keine Tests.

Null Sicherheit: Benutzerpasswoerter in MD5 ohne Salt gespeichert. Sitzungen in Dateien auf einem gemeinsam genutzten Server. SQL-Injection an 23 Stellen. XSS in Formularen. Keine CSRF-Token. Kein HTTPS auf dem Login-Panel.

#Vorgefundene Metriken

MetrikWertBewertung
PageSpeed (mobil)18Kritisch
LCP12,4 sKritisch
INP1100 msKritisch
CLS0,52Kritisch
TTFB4,7 sKritisch
Seitengröße11,2 MBÜbermassig
API-Antwortzeit4,7 s (Durchschnitt)Kritisch
Organischer TrafficRueckgang 72% j/jKritisch
Erkannte Sicherheitslücken23 SQL-Injection, 14 XSSKritisch

Das Schlimmste war, dass der Kunde von keinem dieser Probleme wusste. Die Agentur schickte ihm ein Jahr lang Berichte über “Optimierungen”, die keine Entsprechung in der Realitaet hatten.


#Warum ich diesen Technologie-Stack waehlte

Die Entscheidung über die Zielarchitektur ist der wichtigste Moment des Projekts. Der Kunde hatte berechtigte Bedenken: Die vorherige Agentur versprach eine “moderne Lösung” und lieferte Code aus dem Jahr 2014. Ich musste Technologien wählen, die konkrete Probleme loesen, nicht solche, die gerade im Trend liegen.

Ich analysierte die Anforderungen und passte die Werkzeuge den Aufgaben an:

Python + Django (Backend-API): Der Kunde benötigt ein solides Backend mit Adminpanel, Authentifizierung, Datenvalidierung und REST-API. Django bietet das alles von Haus aus. Das Django REST Framework ist ein ausgereiftes, stabiles Oekoystem mit hervorragender Dokumentation. Der Kunde bedient 4 europaeische Märkte, und Django hat eingebaute Internationalisierung.

PostgreSQL (Datenbank): Die Migration von MySQL 5.5 auf PostgreSQL ist keine Laune. PostgreSQL bietet bessere Indizes (GIN, GiST für Volltextsuche), bessere Datentypen (JSONB, Arrays), ausgereiftes Tabellenpartitionierung und zuverlässige ACID-Transaktionen. Für 200.000 Datensaetze mit mehrsprachiger Volltextsuche ist das die natürliche Wahl.

Redis (Cache und Warteschlangen): Die API-Antwortzeit von 4,7 Sekunden musste unter 100 Millisekunden sinken. Redis cached Abfrageergebnisse, speichert Benutzersitzungen und verwaltet asynchrone Aufgabenwarteschlangen (Celery). Ein Werkzeug, drei kritische Funktionen.

React + TypeScript (interaktives Frontend): Kunden-Dashboard, Suche mit Filtern, mehrstufige Formulare. Das alles erfordert eine reaktive Benutzeroberflaeche. React mit TypeScript bietet typisierte Komponenten, hervorragende Entwicklerwerkzeuge und ein riesiges Bibliotheks-Oekosystem.

Rust (Performance-Microservice): Suchindizierung von 200.000 Datensaetzen in 4 Sprachversionen, Verarbeitung von CSV/Excel-Dateien von Kunden, Datentransformationen. Diese Aufgaben erforderten rohe Leistung. Rust verarbeitet den Suchindex in 1,8 Sekunden statt 47 Sekunden in der alten PHP-Implementierung. Das ist kein prozentualer Unterschied. Das ist ein Unterschied um eine Größenordnung.

Astro (Marketing-Website): Homepage, Blog, Angebot, Dienstleistungsseiten. Das sind statische Inhalte, die kein JavaScript benötigen. Astro generiert reines HTML ohne Runtime-Kosten. Interaktive Elemente (Suche, Kontaktformular) funktionieren als isolierte React-Inseln. Als erfahrener Astro-Entwickler war dieses Framework die natürliche Wahl für die öffentliche Website.

Architektura docelowa:

┌─────────────────────────────────────────────────────┐
│                  Cloudflare CDN                       │
├──────────────┬───────────────┬────────────────────────┤
│  Astro SSG   │  React SPA    │  Django REST API       │
│  (marketing) │  (dashboard)  │  (backend)             │
│  HTML/CSS    │  TypeScript   │  Python 3.12           │
├──────────────┴───────────────┴────────────────────────┤
│                                                       │
│  ┌─────────────┐  ┌──────────┐  ┌──────────────────┐ │
│  │ PostgreSQL   │  │  Redis   │  │  Rust service    │ │
│  │ (primary DB) │  │  (cache) │  │  (search index,  │ │
│  │              │  │  (queue) │  │   data processing│ │
│  └─────────────┘  └──────────┘  └──────────────────┘ │
│                                                       │
│  ┌─────────────────────────────────────────────────┐ │
│  │  Python AI pipeline (content processing, NLP)    │ │
│  └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘

#Backend-API mit Django REST Framework

Das Herzstuck der neuen Plattform ist Django mit dem Django REST Framework. Ich erstellte eine API, die einen mehrsprachigen Dienstleistungskatalog, ein System für Kundenanfragen, JWT-Authentifizierung und ein Adminpanel verwaltet.

# services/models.py -- model usługi z wielojęzycznością
from django.db import models
from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVectorField

class Service(models.Model):
    slug = models.SlugField(max_length=200, unique=True)
    category = models.ForeignKey(
        'Category', on_delete=models.PROTECT, related_name='services'
    )
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created_at']

class ServiceTranslation(models.Model):
    service = models.ForeignKey(
        Service, on_delete=models.CASCADE, related_name='translations'
    )
    language = models.CharField(max_length=5, choices=[
        ('pl', 'Polski'), ('en', 'English'),
        ('de', 'Deutsch'), ('fr', 'Français'),
    ])
    title = models.CharField(max_length=200)
    description = models.TextField()
    meta_title = models.CharField(max_length=70)
    meta_description = models.CharField(max_length=160)
    search_vector = SearchVectorField(null=True)

    class Meta:
        unique_together = ['service', 'language']
        indexes = [
            GinIndex(fields=['search_vector']),
            models.Index(fields=['language', 'service']),
        ]


# services/serializers.py -- serializator z walidacją
from rest_framework import serializers

class ServiceSerializer(serializers.ModelSerializer):
    translations = ServiceTranslationSerializer(many=True, read_only=True)
    category_name = serializers.CharField(
        source='category.name', read_only=True
    )

    class Meta:
        model = Service
        fields = [
            'id', 'slug', 'category_name',
            'is_active', 'translations', 'created_at',
        ]


# services/views.py -- widoki API z cache Redis
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend

class ServiceViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Service.objects.filter(
        is_active=True
    ).select_related(
        'category'
    ).prefetch_related(
        'translations'
    )
    serializer_class = ServiceSerializer
    filter_backends = [DjangoFilterBackend, filters.SearchFilter]
    filterset_fields = ['category__slug']
    search_fields = ['translations__title', 'translations__description']

    @method_decorator(cache_page(60 * 15))  # Cache 15 minut
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

Konfiguration von Redis als Cache-Backend und Celery-Aufgaben-Broker:

# settings.py -- konfiguracja Redis
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/0',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
            'SERIALIZER': 'django_redis.serializers.json.JSONSerializer',
            'CONNECTION_POOL_KWARGS': {'max_connections': 50},
        },
        'KEY_PREFIX': 'platform',
        'TIMEOUT': 900,  # 15 minut domyślnie
    }
}

# Sesje w Redis (szybsze niż baza danych)
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'

# Celery z Redis jako broker
CELERY_BROKER_URL = 'redis://127.0.0.1:6379/1'
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/2'
CELERY_TASK_SERIALIZER = 'json'

Auswirkungen nach der Bereitstellung von Django + PostgreSQL + Redis: die mittlere API-Antwortzeit sank von 4,7 Sekunden auf 45 Millisekunden. Abfragen aus dem Redis-Cache werden in 3-5 Millisekunden beantwortet.


#Rust-Microservice für die Suchindizierung

Die interessanteste technische Herausforderung ist die Suchmaschine. Der Kunde hat einen Katalog mit 200.000 Datensaetzen in 4 Sprachversionen. Die alte PHP-Implementierung führte LIKE '%term%' auf MySQL ohne Indizes aus: 47 Sekunden pro Abfrage. Nicht verwendbar.

PostgreSQL mit GIN-Indizes und tsvector loeste das Problem für Standardabfragen. Aber der Kunde benötigt auch:

  • Suche mit Tippfehlertoleranz (Fuzzy Matching),
  • Filterung nach mehreren Attributen gleichzeitig mit sofortigen Ergebnissen,
  • Neuaufbau des Index nach dem Import von CSV/Excel-Dateien.

Für diese Aufgaben erstellte ich einen Microservice in Rust mit der Tantivy-Bibliothek (das Aequivalent von Apache Lucene für Rust):

// search-service/src/indexer.rs -- indeksowanie wyszukiwania w Rust
use tantivy::{
    schema::*, doc, Index, IndexWriter,
    tokenizer::NgramTokenizer,
};
use serde::Deserialize;
use std::time::Instant;

#[derive(Deserialize)]
pub struct ServiceRecord {
    pub id: i64,
    pub slug: String,
    pub title: String,
    pub description: String,
    pub category: String,
    pub language: String,
    pub attributes: Vec<String>,
}

pub struct SearchIndexer {
    index: Index,
    schema: Schema,
}

impl SearchIndexer {
    pub fn new(index_path: &str) -> Result<Self, Box<dyn std::error::Error>> {
        let mut schema_builder = Schema::builder();

        schema_builder.add_i64_field("id", STORED | INDEXED);
        schema_builder.add_text_field("slug", STORED);
        schema_builder.add_text_field("title", TEXT | STORED);
        schema_builder.add_text_field("description", TEXT | STORED);
        schema_builder.add_text_field("category", STRING | STORED);
        schema_builder.add_text_field("language", STRING | STORED);
        schema_builder.add_text_field("attributes", TEXT | STORED);
        // N-gram field for fuzzy/partial matching
        schema_builder.add_text_field("title_ngram", TEXT);

        let schema = schema_builder.build();
        let index = Index::create_in_dir(index_path, schema.clone())?;

        // Register n-gram tokenizer for typo tolerance
        let ngram_tokenizer = NgramTokenizer::new(2, 4, false)
            .expect("Failed to create ngram tokenizer");
        index
            .tokenizers()
            .register("ngram", ngram_tokenizer);

        Ok(Self { index, schema })
    }

    pub fn build_index(
        &self,
        records: Vec<ServiceRecord>,
    ) -> Result<usize, Box<dyn std::error::Error>> {
        let start = Instant::now();
        let mut writer: IndexWriter = self.index.writer(128_000_000)?; // 128MB buffer

        let title_field = self.schema.get_field("title").unwrap();
        let description_field = self.schema.get_field("description").unwrap();
        let title_ngram_field = self.schema.get_field("title_ngram").unwrap();

        let count = records.len();
        for record in records {
            writer.add_document(doc!(
                self.schema.get_field("id").unwrap() => record.id,
                self.schema.get_field("slug").unwrap() => record.slug,
                title_field => record.title.clone(),
                description_field => record.description,
                self.schema.get_field("category").unwrap() => record.category,
                self.schema.get_field("language").unwrap() => record.language,
                self.schema.get_field("attributes").unwrap() =>
                    record.attributes.join(" "),
                title_ngram_field => record.title,
            ))?;
        }

        writer.commit()?;
        let duration = start.elapsed();
        println!(
            "Indexed {} records in {:.2}s",
            count,
            duration.as_secs_f64()
        );

        Ok(count)
    }
}

HTTP-API in Rust mit dem Actix-web-Framework:

// search-service/src/main.rs -- API wyszukiwania
use actix_web::{web, App, HttpServer, HttpResponse};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct SearchQuery {
    q: String,
    lang: Option<String>,
    category: Option<String>,
    limit: Option<usize>,
}

#[derive(Serialize)]
struct SearchResult {
    id: i64,
    slug: String,
    title: String,
    excerpt: String,
    category: String,
    score: f32,
}

async fn search(
    query: web::Query<SearchQuery>,
    indexer: web::Data<SearchIndexer>,
) -> HttpResponse {
    let limit = query.limit.unwrap_or(20);
    let lang = query.lang.as_deref().unwrap_or("pl");

    let results = indexer.search(
        &query.q, lang, query.category.as_deref(), limit
    );

    HttpResponse::Ok().json(results)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let indexer = SearchIndexer::new("./search_index")
        .expect("Failed to create indexer");
    let indexer_data = web::Data::new(indexer);

    HttpServer::new(move || {
        App::new()
            .app_data(indexer_data.clone())
            .route("/search", web::get().to(search))
            .route("/health", web::get().to(|| async {
                HttpResponse::Ok().body("ok")
            }))
    })
    .bind("127.0.0.1:8081")?
    .run()
    .await
}

Benchmark-Ergebnisse des Rust-Microservices:

OperationAltes PHPNeues RustVerbesserung
Index-Aufbau (200k Datensaetze)47 s1,8 s26x schneller
Einfache Suche4,7 s2 ms2350x schneller
Suche mit Filtern8,3 s5 ms1660x schneller
Fuzzy Matching (Tippfehler)Nicht vorhanden8 msNeue Funktion
Speicherverbrauch512 MB84 MB6x weniger

Rust war keine Wahl “weil es im Trend liegt”. Es war die Wahl, weil es für diese spezifische Aufgabe (Verarbeitung von 200.000 Datensaetzen mit N-Gramm-Indizierung und Fuzzy Matching) eine Leistung bietet, die für Interpreter unerreichbar ist.


#React-Frontend mit interaktivem Dashboard

Das Kunden-Dashboard erforderte eine reaktive Benutzeroberflaeche: Tabellen mit Sortierung, mehrstufige Filter, mehrstufige Formulare, Diagramme mit Echtzeit-Daten. React mit TypeScript ist die natürliche Wahl.

// src/components/ServiceSearch.tsx -- wyszukiwarka z filtrami
import { useState, useCallback, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useDebounce } from '@/hooks/useDebounce';

interface SearchResult {
  id: number;
  slug: string;
  title: string;
  excerpt: string;
  category: string;
  score: number;
}

interface SearchFilters {
  category: string | null;
  language: string;
}

export function ServiceSearch({ locale }: { locale: string }) {
  const [query, setQuery] = useState('');
  const [filters, setFilters] = useState<SearchFilters>({
    category: null,
    language: locale,
  });

  const debouncedQuery = useDebounce(query, 300);

  const { data: results, isLoading } = useQuery<SearchResult[]>({
    queryKey: ['search', debouncedQuery, filters],
    queryFn: async () => {
      const params = new URLSearchParams({
        q: debouncedQuery,
        lang: filters.language,
        ...(filters.category && { category: filters.category }),
      });
      const res = await fetch(`/api/search?${params}`);
      return res.json();
    },
    enabled: debouncedQuery.length >= 2,
    staleTime: 5 * 60 * 1000, // 5 minut cache
  });

  const handleCategoryChange = useCallback((category: string | null) => {
    setFilters(prev => ({ ...prev, category }));
  }, []);

  return (
    <div className="search-container">
      <div className="relative">
        <input
          type="search"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder={locale === 'pl' ? 'Szukaj usług...' : 'Search services...'}
          className="w-full px-4 py-3 rounded-lg border border-gray-200
                     dark:border-gray-700 bg-white dark:bg-gray-800
                     focus:ring-2 focus:ring-emerald-500 focus:outline-none"
        />
        {isLoading && (
          <div className="absolute right-3 top-1/2 -translate-y-1/2">
            <span className="animate-spin h-5 w-5 border-2
                           border-emerald-500 border-t-transparent rounded-full
                           inline-block" />
          </div>
        )}
      </div>

      {results && results.length > 0 && (
        <div className="mt-4 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
          {results.map((result) => (
            <a
              key={result.id}
              href={`/${locale}/${result.slug}/`}
              className="block p-4 rounded-lg border border-gray-100
                         dark:border-gray-700 hover:border-emerald-500
                         transition-colors"
            >
              <span className="text-xs font-medium text-emerald-600
                              dark:text-emerald-400">
                {result.category}
              </span>
              <h3 className="mt-1 font-semibold text-gray-900
                            dark:text-white">
                {result.title}
              </h3>
              <p className="mt-2 text-sm text-gray-600 dark:text-gray-400
                           line-clamp-2">
                {result.excerpt}
              </p>
            </a>
          ))}
        </div>
      )}
    </div>
  );
}

Die React-Komponente ist als interaktive Insel in die Astro-Seite eingebettet:

---
// src/pages/[lang]/services.astro -- strona usług
import { ServiceSearch } from '../../components/ServiceSearch';
import Layout from '../../layouts/Layout.astro';
---

<Layout title="Usługi">
  <section class="services-hero">
    <h1>Nasze usługi</h1>
  </section>

  <!-- Wyspa React -- ładuje się przy scrollu, nie blokuje reszty strony -->
  <ServiceSearch client:visible locale={Astro.params.lang} />
</Layout>

Dank der Islands-Architektur laedt die Marketing-Website standardmaessig 0 KB JavaScript. Die Such-Komponente (38 KB gzipped) laedt erst, wenn der Benutzer zur entsprechenden Sektion scrollt.


#KI-Pipeline zur Inhaltsverarbeitung

Die Migration von 1.200 Inhaltsseiten aus schmutzigem HTML in sauberes Markdown erforderte Automatisierung. Viele Seiten hatten unvollstaendige SEO-Metadaten, fehlende Beschreibungen, unoptimale Überschriften. Statt sie manuell zu korrigieren, erstellte ich eine Pipeline in Python, die ein benutzerdefiniertes Sprachmodell nutzt.

Die Pipeline arbeitete in drei Phasen:

# ai_content_pipeline.py -- przetwarzanie treści z AI
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor

import markdownify

@dataclass
class ContentAnalysis:
    entities: list[dict] = field(default_factory=list)
    meta_description: str = ''
    heading_issues: list[str] = field(default_factory=list)
    word_count: int = 0
    is_thin: bool = False
    suggested_internal_links: list[str] = field(default_factory=list)

def clean_legacy_html(raw_html: str) -> str:
    """Faza 1: Czyszczenie brudnego HTML z legacy platformy."""
    # Usuwanie inline styles, komentarzy warunkowych IE, pustych tagów
    cleaned = re.sub(r'style="[^"]*"', '', raw_html)
    cleaned = re.sub(r'<!--\[if.*?\]>.*?<!\[endif\]-->', '', cleaned,
                     flags=re.DOTALL)
    cleaned = re.sub(r'<(div|span|p)[^>]*>\s*</\1>', '', cleaned)
    cleaned = re.sub(r'&nbsp;', ' ', cleaned)
    return cleaned

def convert_to_markdown(html: str) -> str:
    """Faza 2: Konwersja HTML na Markdown."""
    return markdownify.markdownify(
        html,
        heading_style="ATX",
        strip=['script', 'style', 'iframe', 'object', 'embed'],
        convert=['h1', 'h2', 'h3', 'h4', 'p', 'a', 'img',
                 'ul', 'ol', 'li', 'strong', 'em', 'table'],
    )

def analyze_with_ai(content: str, model_client) -> ContentAnalysis:
    """Faza 3: Analiza treści za pomocą niestandardowego modelu AI."""
    prompt = f"""Przeanalizuj poniższą treść strony internetowej.
Zwróć JSON z polami:
- entities: lista obiektów {{name, type, relevance_score}}
- meta_description: optymalny opis meta (max 155 znaków, po polsku)
- heading_issues: lista problemów ze strukturą nagłówków
- word_count: liczba słów
- suggested_internal_links: sugerowane frazy do linkowania wewnętrznego

Treść:
{content[:4000]}"""

    response = model_client.generate(prompt, max_tokens=1024)
    data = json.loads(response.text)

    return ContentAnalysis(
        entities=data.get('entities', []),
        meta_description=data.get('meta_description', ''),
        heading_issues=data.get('heading_issues', []),
        word_count=data.get('word_count', 0),
        is_thin=data.get('word_count', 0) < 300,
        suggested_internal_links=data.get('suggested_internal_links', []),
    )

def process_batch(
    content_dir: Path, model_client, max_workers: int = 4
) -> dict:
    """Przetwarzanie wsadowe z wielowątkowością."""
    stats = {'processed': 0, 'thin': 0, 'entities': 0}

    def process_file(md_file: Path) -> ContentAnalysis:
        content = md_file.read_text(encoding='utf-8')
        return analyze_with_ai(content, model_client)

    files = list(content_dir.glob('*.md'))
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        results = executor.map(process_file, files)
        for analysis in results:
            stats['processed'] += 1
            if analysis.is_thin:
                stats['thin'] += 1
            stats['entities'] += len(analysis.entities)

    return stats

Die Pipeline verarbeitete 1.247 Seiten in 4 Stunden auf eigener Infrastruktur, nicht in der Cloud, nicht über externe APIs. Sie fand 3.400 einzigartige Entitäten, generierte fehlende Meta-Beschreibungen für 680 Seiten und identifizierte 89 Seiten mit Thin Content, die erweitert werden mussten.

Das KI-Modell lief auf einem dedizierten Server mit GPU. Ich verwendete weder AWS, Azure noch eine Cloud-verwaltete Lösung. Volle Kontrolle über die Kundendaten, kein Übermitteln von Inhalten an externe APIs.


#Datenmigration von MySQL zu PostgreSQL

Die Datenmigration von MySQL 5.5 zu PostgreSQL erforderte nicht nur den Transfer von Daten, sondern einen fundamentalen Umbau des Schemas. Das alte Schema hatte keine Relationen, Indizes oder Integritaetsbeschraenkungen.

-- Migracja schematu: od chaosu do porządku

-- Stary MySQL (brak relacji, brak indeksów)
-- CREATE TABLE services (id INT AUTO_INCREMENT, title VARCHAR(255), ...);
-- CREATE TABLE categories (id INT AUTO_INCREMENT, name VARCHAR(100), ...);
-- Brak FOREIGN KEY, brak INDEX poza PK

-- Nowy PostgreSQL z prawidłową strukturą
CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    slug VARCHAR(200) UNIQUE NOT NULL,
    parent_id INTEGER REFERENCES categories(id),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE services (
    id SERIAL PRIMARY KEY,
    slug VARCHAR(200) UNIQUE NOT NULL,
    category_id INTEGER NOT NULL REFERENCES categories(id),
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE service_translations (
    id SERIAL PRIMARY KEY,
    service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE,
    language VARCHAR(5) NOT NULL,
    title VARCHAR(200) NOT NULL,
    description TEXT NOT NULL,
    meta_title VARCHAR(70) NOT NULL,
    meta_description VARCHAR(160) NOT NULL,
    search_vector TSVECTOR,
    UNIQUE(service_id, language)
);

-- Indeksy dla wydajnego wyszukiwania
CREATE INDEX idx_translations_search ON service_translations
    USING GIN(search_vector);
CREATE INDEX idx_translations_lang ON service_translations(language);
CREATE INDEX idx_services_category ON services(category_id)
    WHERE is_active = true;

-- Trigger automatycznej aktualizacji search_vector
CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
    NEW.search_vector :=
        setweight(to_tsvector('simple', COALESCE(NEW.title, '')), 'A') ||
        setweight(to_tsvector('simple', COALESCE(NEW.description, '')), 'B');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_search_vector
    BEFORE INSERT OR UPDATE ON service_translations
    FOR EACH ROW EXECUTE FUNCTION update_search_vector();

#SEO-Migration: Weiterleitungen und strukturierte Daten

Das kritischste Element der Migration ist die Beibehaltung der Google-Rankings. Obwohl die alte Plattform Traffic verlor, hatte sie immer noch Hunderte von indizierten URLs und Dutzende von Backlinks.

Ich mappte jede alte URL auf eine neue und implementierte 301-Weiterleitungen auf Cloudflare Workers-Ebene:

// redirects.ts -- mapa przekierowań ze starej platformy
const redirectMap: Record<string, string> = {
  '/uslugi.php?id=1': '/pl/uslugi/konsulting/',
  '/services.php?id=1': '/en/services/consulting/',
  '/index.php?page=about': '/pl/o-nas/',
  '/kontakt.php': '/pl/kontakt/',
  '/leistungen.php': '/de/dienstleistungen/',
  // ... 963 przekierowania wygenerowane automatycznie
};

export function handleRedirects(url: URL): Response | null {
  // Sprawdź dokładne dopasowanie
  const exactMatch = redirectMap[url.pathname + url.search];
  if (exactMatch) {
    return new Response(null, {
      status: 301,
      headers: { Location: exactMatch },
    });
  }

  // Sprawdź dopasowanie bez query string
  const pathMatch = redirectMap[url.pathname];
  if (pathMatch) {
    return new Response(null, {
      status: 301,
      headers: { Location: pathMatch },
    });
  }

  return null;
}

Ich implementierte vollstaendige Schema.org-strukturierte Daten mit Hreflang für jede Sprachversion. Die alte Plattform hatte keinerlei strukturierte Daten. Google verstand nicht, worum es auf der Seite ging.


#Ergebnisse nach 4 Monaten

MetrikVorherNachherVeränderung
PageSpeed (mobil)1899+450%
LCP12,4 s0,3 s-98%
INP1100 ms22 ms-98%
CLS0,520,01-98%
TTFB4,7 s0,03 s-99%
Seitengröße11,2 MB0,28 MB-97%
API-Antwortzeit4700 ms45 ms-99%
Suche (200k Datensaetze)47 s2 ms23500x
Organischer TrafficBasis+340%Anstieg
Kundenanfragen~2/Woche~14/Woche+600%
Sicherheitslücken370Eliminiert

Vollstaendiger Technologie-Stack des Projekts:

  • Backend-API: Python 3.12, Django 5, Django REST Framework, Celery
  • Datenbank: PostgreSQL 16 mit GIN-Indizes und Volltextsuche
  • Cache und Warteschlangen: Redis 7 (Cache, Sitzungen, Celery-Broker)
  • Such-Microservice: Rust, Actix-web, Tantivy
  • Interaktives Frontend: React 19, TypeScript, TanStack Query, Tailwind CSS
  • Marketing-Website: Astro 5 mit React-Inseln
  • KI-Pipeline: Python, benutzerdefiniertes Sprachmodell auf dedizierter GPU
  • Deployment: Cloudflare Pages + Workers, GitHub Actions CI/CD
  • Monitoring: Sentry, Prometheus, Grafana

#Was mich dieses Projekt lehrte

Nicht urteilen, sondern diagnostizieren. Der Kunde kam verletzt durch eine schlechte Agentur-Erfahrung. Das Letzte, was er brauchte, war ein weiterer “Experte”, der ihm sagt, wie schlimm es ist. Stattdessen praesentierte ich Fakten in Form eines Berichts, erklaerte das Risiko und schlug einen Aktionsplan mit klarem Zeitplan vor.

Passe das Werkzeug der Aufgabe an, nicht die Aufgabe dem Werkzeug. Django für die API, React für die interaktive Benutzeroberflaeche, Rust für die Datenverarbeitung, Astro für statische Seiten. Jede Technologie loest ein konkretes Problem. Ein monolithisches Framework für alles ist ein Rezept für Kompromisse.

KI ist ein Werkzeug, keine Magie. Die KI-Pipeline verarbeitete 1.247 Seiten in 4 Stunden. Das waere manuell Wochen Arbeit gewesen. Aber jedes Ergebnis erforderte menschliche Verifizierung. KI generierte Vorschlaege, der Mensch traf Entscheidungen. Ein benutzerdefiniertes Modell auf einem eigenen Server gibt volle Kontrolle über Kundendaten.

Rust rechtfertigt sich in konkreten Fällen. Ich habe nicht die gesamte Plattform in Rust geschrieben. Ich schrieb in Rust einen Microservice, der 200.000 Datensaetze verarbeitet, und dort ist der Leistungsunterschied um eine Größenordnung. Der Rest des Systems funktioniert hervorragend in Python und TypeScript.

SEO-Migration ist keine Option, sie ist eine Pflicht. Ohne URL-Mapping und 301-Weiterleitungen haette der Kunde den Rest des organischen Traffics verloren. Dank einer korrekten Migration stieg der Traffic um 340% in 4 Monaten.


#Brauchen Sie Hilfe nach einer schlechten Agentur-Erfahrung?

Wenn Ihre Plattform auf veralteten Technologien aufgebaut wurde, langsam, unsicher oder einfach nicht wie erwartet funktioniert, kontaktieren Sie WPPoland. Ich fuehre ein kostenloses Bestandsaudit durch und erstelle einen Sanierungsplan mit klarem Zeitplan und Arbeitsumfang.

Jedes Rettungsprojekt beginnt mit einem Anruf. Dieser Kunde rief an einem Freitagabend an. Am Montagmorgen hatte er einen Bericht. Acht Wochen später hatte er eine Plattform, auf die er stolz ist.

Nächster Schritt

Machen Sie aus dem Artikel eine echte Umsetzung

Dieser Block stärkt die interne Verlinkung und führt Nutzer gezielt zum nächsten sinnvollen Schritt im Service- und Content-System.

Soll das Thema auf Ihrer Website umgesetzt werden?

Wenn Sichtbarkeit in Google und KI-Systemen wichtig ist, baue ich die passende Content-Architektur, FAQ, Schema-Daten und interne Verlinkung auf.

Relevanter Cluster

Weitere WordPress-Dienste und Wissensbasis entdecken

Stärken Sie Ihr Unternehmen mit professionellem technischen Support in den Kernbereichen des WordPress-Ökosystems.

Wie lange dauert die Migration einer Plattform von einem veralteten Stack auf moderne Architektur?
Die Dauer hängt vom Umfang des Projekts ab und wird individuell nach dem Audit festgelegt. Eine einfache Plattform benötigt 4-6 Wochen. Ein komplexes System mit vielen Integrationen, Mehrsprachigkeit und interaktiven Komponenten benötigt 8-14 Wochen.
Warum Rust statt Node.js oder Go für Microservices?
Rust bietet eine mit C/C++ vergleichbare Leistung bei Speichersicherheitsgarantien. Für Aufgaben, die die Verarbeitung großer Datensaetze, Suchindizierung und Dateitransformationen erfordern, bietet Rust einen Leistungsvorteil um eine Groessenordnung gegenüber Node.js.
Beeinflusst die Migration auf einen neuen Stack die Google-Rankings?
Eine korrekt durchgefuehrte Migration mit URL-Mapping und 301-Weiterleitungen schuetzt bestehende Rankings. Die Verbesserung der Core Web Vitals nach der Migration fuehrt typischerweise zu einem Anstieg des organischen Traffics um 30-50 Prozent innerhalb von 3 Monaten.
Was tun, wenn eine Agentur betrogen hat und die Plattform ohne Dokumentation zurueckliess?
Der erste Schritt ist ein Bestandsaudit: Pruefen von Code, Lizenzen, Sicherheit und Performance. Dann Wiederherstellung von Daten und Inhalten aus der bestehenden Installation. Erst danach Planung der Migration auf eine neue Plattform mit vollstaendiger Dokumentation.

Sie brauchen ein FAQ für Branche und Zielmarkt? Wir erstellen eine Version passend zu Ihren Business-Zielen.

Kontakt aufnehmen

Ähnliche Artikel

Erfahren Sie, wann ein Website-Umbau notwendig ist. 7 messbare technische und geschäftliche Signale, dass Ihre Website 2026 eine Modernisierung benötigt.
wordpress

Wann sollten Sie Ihre Website neu aufbauen? 7 Anzeichen für eine Modernisierung

Erfahren Sie, wann ein Website-Umbau notwendig ist. 7 messbare technische und geschäftliche Signale, dass Ihre Website 2026 eine Modernisierung benötigt.

WordPress 7.0 mit AI Client vs Astro 6 nach der Cloudflare-Übernahme. Geschwindigkeit, Kosten, SEO und Sicherheit im Vergleich. Mein Fazit nach 20 Jahren als WP-Entwickler - wann migrieren, wann bleiben.
wordpress

WordPress 7.0 vs Astro 6 nach der Cloudflare-Übernahme - wer gewinnt 2026?

WordPress 7.0 mit AI Client vs Astro 6 nach der Cloudflare-Übernahme. Geschwindigkeit, Kosten, SEO und Sicherheit im Vergleich. Mein Fazit nach 20 Jahren als WP-Entwickler - wann migrieren, wann bleiben.

Eine detaillierte Fallstudie, die zeigt, wie WPPoland einen langsamen WooCommerce-Möbelshop von PageSpeed 40 auf 98 optimiert hat, die Ladezeiten von 8 Sekunden auf unter 1 Sekunde reduziert und die Conversion-Rate verdoppelt hat.
performance

Von 40 auf 98 PageSpeed: Wie Wir einen WooCommerce-Shop Transformiert Haben

Eine detaillierte Fallstudie, die zeigt, wie WPPoland einen langsamen WooCommerce-Möbelshop von PageSpeed 40 auf 98 optimiert hat, die Ladezeiten von 8 Sekunden auf unter 1 Sekunde reduziert und die Conversion-Rate verdoppelt hat.