Full‑Stack‑Entwickler

Ich entwickle mehrsprachige, sichere und sorgfältig gestaltete Webplattformen, vom CMS bis zur Oberfläche.

Zusammenarbeiten
Porträt von Castole Agossou Yao

Technologien: Next.js, React, TypeScript, Tailwind CSS, Strapi, Supabase, Sanity, PostgreSQL, Node.js, Docker, Linux, Framer Motion, Three.js, GitHub Actions, Vercel.

Vorschau des Projekts Zielscheibe Nachhilfe
Online

Zielscheibe Nachhilfe

Plattform für Nachhilfe und Lerncoaching in Essen, im Ruhrgebiet und online: Anträge auf Bildung und Teilhabe (BuT), Verträge mit digitaler Signatur und PDF, Online-Terminbuchung und Mitgliederbereich, in 7 Sprachen inklusive Arabisch.

Next.js 16Strapi 5TypeScriptTailwind CSSPostgreSQL

zielscheibe-nachhilfe.de

Fallstudie

Zielscheibe Nachhilfe

Plattform für Nachhilfe und Lerncoaching in Essen, im Ruhrgebiet und online: Anträge auf Bildung und Teilhabe (BuT), Verträge mit digitaler Signatur und PDF, Online-Terminbuchung und Mitgliederbereich, in 7 Sprachen inklusive Arabisch.

Meine Rolle. Konzeption und komplette Entwicklung: Frontend, CMS, Mitgliederbereich, Verträge und Deployment.

Next.js 16Strapi 5TypeScriptTailwind CSSPostgreSQL

Herausforderungen & Lösungen

  1. 01 · Problem

    Die Familien sprechen Arabisch, Türkisch, Ukrainisch, Russisch … und die Verträge einiger Familien kamen leer heraus.

    Lösung

    7 Sprachen mit vollständiger Rechts-nach-links-Unterstützung, Namen in lateinischer Schrift für alle Familien und ein PDF-Generator mit den richtigen Schriften, damit jeder Vertrag vollständig ist.

    frontend/lib/pdf-contract-generator.ts

    Vorher

    try {
      const form = mainPdfDoc.getForm();
      // …
      const tf1_6 = form.getTextField('Textfeld 1_6');
      if (tf1_6 && studentName) tf1_6.setText(studentName);
    
      const tf1_7 = form.getTextField('Textfeld 1_7');
      if (tf1_7 && data.dateOfBirth) tf1_7.setText(data.dateOfBirth);
      // …

    Nachher

    /**
     * pdf-lib wirft einen Fehler, wenn ein Feldname fehlt, statt undefined zurückzugeben. Deshalb
     * erreichte `getTextField('A') || getTextField('B')` nie B, und ein umbenanntes Feld brach den Rest
     * seines Blocks ab, stillschweigend, in einem try/catch, und ließ einen Teil eines Vertrags oder
     * Einwilligungsformulars leer.
     */
    function lenientForm(form: PDFForm, label: string) {
      const lookup = <T>(kind: string, name: string, get: () => T): T | undefined => {
        try {
          return get();
        } catch {
          console.warn(`[PDF-GENERATOR] ${label}: ${kind} "${name}" fehlt in der Vorlage`);
          return undefined;
        }
      };
      return {
        // Der Text läuft über toWinAnsi: Die Helvetica des Formulars kann „ş“ und „ı“ nicht zeichnen,
        // und ein einziger solcher Fehler ließ früher den ganzen Vertrag leer.
        getTextField: (name: string) => {
          const field = lookup('Textfeld', name, () => form.getTextField(name));
          return field ? { setText: (value?: string) => field.setText(value === undefined ? value : toWinAnsi(value)) } : undefined;
        },
        // …
      };
    }
    
    const form = lenientForm(mainPdfDoc.getForm(), 'Vertrag');
  2. 02 · Problem

    Personenbezogene Daten von Kindern und Eltern: Das Gesetz verlangt Aufbewahrungsfristen und eine saubere Löschung.

    Lösung

    Eine vollständige, von einem Anwalt geprüfte Löschung, die Newsletter-Abonnenten mit noch gültiger Einwilligung nicht mehr abmeldet.

    backend/src/services/gdpr-retention.js

    Vorher

    const stale = await strapi.documents(uid).findMany({
      filters: { createdAt: { $lt: cutoff } },
      limit: BATCH_SIZE,
      // …
    });

    Nachher

    /**
     * Ein bestätigter Newsletter-Abonnent ist eine weiterhin gültige Einwilligung, kein Eintrag über
     * vergangenen Schriftverkehr: Das Löschen nach createdAt meldete jeden Leser drei Jahre nach der
     * Anmeldung ab, ohne dass er das wollte.
     */
    const EXCLUDED_FROM_PURGE = {
      'api::newsletter-subscriber.newsletter-subscriber': { status: { $ne: 'Bestätigt' } },
    };
    
    const stale = await strapi.documents(uid).findMany({
      filters: { createdAt: { $lt: cutoff }, ...(EXCLUDED_FROM_PURGE[uid] || {}) },
      limit: BATCH_SIZE,
      // …
    });
  3. 03 · Problem

    Eine öffentliche Website mit Konten ist ein Ziel, und während des Projekts wurde eine Framework-Lücke veröffentlicht.

    Lösung

    Strikte Content-Security-Policy mit Nonce, zuerst im Report-Modus, CSRF-Token und sofortiges Update von Next.js, sobald die Lücken mit hohem Schweregrad bekannt wurden.

    frontend/proxy.ts

    Vorher

    const csp = `
      default-src 'self';
      script-src 'self' 'unsafe-inline' ${evalDirective} https://challenges.cloudflare.com https://www.googletagmanager.com …;
      style-src 'self' 'unsafe-inline' https://cdn-cookieyes.com;
      // …`

    Nachher

    const STRICT_CSP = process.env.CSP_STRICT === 'true';
    
    function generateNonce(): string {
      const bytes = new Uint8Array(16);
      crypto.getRandomValues(bytes);
      let binary = '';
      for (const b of bytes) binary += String.fromCharCode(b);
      return btoa(binary);
    }
    
    // Skripten per Nonce vertrauen und sie ihre eigenen Abhängigkeiten nachladen lassen
    // (strict-dynamic).
    const scriptSrcPrefix =
      STRICT_CSP && nonce
        ? `'self' 'nonce-${nonce}' 'strict-dynamic' 'unsafe-inline' ${evalDirective}`
        : `'self' 'unsafe-inline' ${evalDirective}`;
    
    const csp = `
      default-src 'self';
      script-src ${scriptSrcPrefix} https://challenges.cloudflare.com …;
      // …`
  4. 04 · Problem

    Bei Problemen im CMS wurden Seiten zu 404 und der Server stieß an seine eigenen Ratenlimits.

    Lösung

    Landingpages, die bei einem Strapi-Ausfall online bleiben, ein Server-Token bei jedem Lesezugriff und ein fehlgeschlagenes Dossier, das nicht mehr endlos neu erzeugt wird.

    frontend/app/[locale]/angebote/[...slug]/page.tsx

    Vorher

    let strapiOffer = await getAngebotBySlugServer(fullSlug, locale).catch(() => null);
    if (!strapiOffer && slugArray.length > 1) {
      strapiOffer = await getAngebotBySlugServer(normalizedSlug, locale).catch(() => null);
    }

    Nachher

    // Ein nicht erreichbares Strapi wird vermerkt, nicht in „nicht gefunden“ umgewandelt: Die
    // statischen Ersatzinhalte unten werden trotzdem versucht, und nur ein Slug, den keiner kennt, löst
    // einen Fehler (nicht zwischengespeichert) statt eines 404 (zwischengespeichert) aus.
    let strapiUnavailable = false;
    const lookup = (slugToFind: string) =>
      getAngebotBySlugServer(slugToFind, locale, { throwWhenUnavailable: true }).catch(() => {
        strapiUnavailable = true;
        return null;
      });
    let strapiOffer = await lookup(fullSlug);
    if (!strapiOffer && slugArray.length > 1) {
      strapiOffer = await lookup(normalizedSlug);
    }
    // …
    if (strapiUnavailable) {
      throw new Error(`Strapi unavailable while resolving /angebote/${fullSlug}`);
    }
    return null;
  5. 05 · Problem

    Strapi begrenzt öffentliche Lesezugriffe auf 120 pro Minute und IP, und jedes Seiten-Rendering geht von denselben wenigen Vercel-Adressen aus: Die eigenen Renderings der Website liefen ins Limit und bekamen 429 (Team fehlte auf der Über-uns-Seite, leere Sitemap, Öffnungszeiten als statischer Fallback).

    Lösung

    Jeder Serveraufruf trägt jetzt ein gemeinsames Server-Token, das Strapi nicht mitzählt, in einer Variable ohne NEXT_PUBLIC_-Präfix, damit sie nie in ein Browser-Bundle gelangt. Ein direkter Aufruf ohne Token bleibt begrenzt.

    frontend/lib/strapi-server.ts

    Code

    // Strapi begrenzt öffentliche GETs pro IP (120/Min., middlewares/rate-limit.js), und jedes
    // Server-Rendering geht von denselben wenigen Vercel-Adressen aus: Die Seiten der Website füllten
    // dieses Fenster und bekamen 429er (Ersatzinhalt, leere Sitemap). Das gemeinsame Server-Token
    // kennzeichnet den Aufruf als von diesem Server kommend, den Strapi nicht mitzählt. Die Variable
    // hat kein Präfix NEXT_PUBLIC_, gelangt also nie in ein Browser-Bundle.
    if (FORM_API_TOKEN && !headers.has('X-Api-Token')) headers.set('X-Api-Token', FORM_API_TOKEN);
  6. 06 · Problem

    Ein Dokumenten-Dossier, das immer wieder fehlschlug (unlesbarer Anhang, Anbieter tagelang down), wurde alle fünf Minuten endlos neu erzeugt und hochgeladen.

    Lösung

    Ein Versuchszähler am Datensatz: Nach 5 Läufen bleibt das Dossier „failed“, damit das Team es prüft, und der Wiederholungsjob überspringt es.

    backend/src/services/document-jobs.js

    Vorher

    await strapi.db.query(uid).updateMany({ where: { documentId, documentLease: lease }, data: { documentStatus: 'failed', documentLease: null, documentLeaseExpiresAt: null } });
    strapi.log.error(`[DOCUMENTS] ${kind} dossier retained for retry: ${error.message}`);
    // …
    filters: { $or: [{ documentStatus: { $in: ['pending', 'failed'] } }, { documentStatus: 'processing', documentLeaseExpiresAt: { $lt: new Date().toISOString() } }] }, limit: 25, sort: ['updatedAt:asc'],

    Nachher

    /**
     * Eine Akte, die immer wieder fehlschlägt (unlesbarer Anhang, Dienstleister tagelang ausgefallen),
     * wurde früher alle fünf Minuten endlos neu erzeugt und hochgeladen. Nach dieser Anzahl von Läufen
     * bleibt sie „fehlgeschlagen“, damit sich das Team darum kümmert.
     */
    const MAX_DOCUMENT_ATTEMPTS = 5;
    
    // …
    const attempts = (record.documentAttempts || 0) + 1;
    await strapi.db.query(uid).updateMany({ where: { documentId, documentLease: lease }, data: { documentStatus: 'failed', documentLease: null, documentLeaseExpiresAt: null, documentAttempts: attempts } });
    // …
    filters: { $or: [
      { documentStatus: 'pending' },
      { documentStatus: 'failed', $or: [{ documentAttempts: { $null: true } }, { documentAttempts: { $lt: MAX_DOCUMENT_ATTEMPTS } }] },
      { documentStatus: 'processing', documentLeaseExpiresAt: { $lt: new Date().toISOString() } },
    ] }, limit: 25, sort: ['updatedAt:asc'],
  7. 07 · Problem

    Mitglieder-Sitzungen sind zustandslose Token mit 7 Tagen Gültigkeit: Nach einer Passwortänderung funktionierten ältere Token (ein gestohlenes, ein vergessenes Gerät) weiter.

    Lösung

    Der Zeitpunkt der Änderung wird am Nutzer gespeichert und jedes davor ausgestellte Token abgelehnt, auf die Sekunde abgerundet, damit das direkt danach ausgestellte Token funktioniert. Das neue Token ersetzt das Cookie, das Mitglied bleibt angemeldet. Bestätigungs-Token werden gehasht gespeichert.

    backend/src/lib/session-revocation.js

    Code

    /**
     * Sitzungen enden, wenn das Passwort geändert wird. Mitglieds-JWTs sind zustandslos und bis zu 7
     * Tage gültig. Beim Ändern oder Zurücksetzen des Passworts wird der Zeitpunkt in
     * user.passwordChangedAt gespeichert; jedes davor ausgestellte Token (sein „iat“, in ganzen
     * Sekunden) wird dann abgelehnt. Der Zeitpunkt wird auf die Sekunde abgerundet, damit das direkt
     * nach der Änderung in derselben Sekunde ausgestellte Token noch funktioniert.
     */
    function passwordChangeInstant(now = Date.now()) {
      return new Date(Math.floor(now / 1000) * 1000);
    }
    
    function issuedBeforePasswordChange(tokenPayload, passwordChangedAt) {
      if (!passwordChangedAt) return false;
      const changedAt = new Date(passwordChangedAt).getTime();
      if (Number.isNaN(changedAt)) return false;
      // Ein Token ohne „iat“ kann nicht beweisen, dass es neuer ist als die Änderung.
      if (typeof tokenPayload?.iat !== 'number') return true;
      return tokenPayload.iat * 1000 < changedAt;
    }

In Zahlen

  • 7

    Sprachen, Arabisch von rechts nach links

  • 2.026

    automatisierte Tests (987 Frontend, 1.039 Backend)

  • 975

    Commits, mit Continuous Integration

Website besuchen
Vorschau des Projekts École Notre Dame du Mont Carmel
Online

École Notre Dame du Mont Carmel

Website der Schule Notre Dame du Mont Carmel in Nikki (Benin), verbunden mit der Stiftung FUNDEBE: Vorstellung, praktische Informationen, Neuigkeiten, Schulstufen und Online-Anmeldung, in 4 Sprachen und als App installierbar (PWA).

Next.js 14Tailwind CSSFramer MotionPWA

ndmc-nikki.netlify.app

Fallstudie

École Notre Dame du Mont Carmel

Website der Schule Notre Dame du Mont Carmel in Nikki (Benin), verbunden mit der Stiftung FUNDEBE: Vorstellung, praktische Informationen, Neuigkeiten, Schulstufen und Online-Anmeldung, in 4 Sprachen und als App installierbar (PWA).

Meine Rolle. Neugestaltung und komplette Entwicklung der Schulwebsite, vom Designsystem bis zum Deployment.

Next.js 14Tailwind CSSFramer MotionPWA

Herausforderungen & Lösungen

  1. 01 · Problem

    Texte waren fest im Code und die Übersetzungsbibliothek funktionierte nicht mit einem statischen Export: Das erste Rendern war unübersetzt oder scheiterte bei der Hydration.

    Lösung

    Ein hybrides Muster: Eine Server-Komponente setzt die Sprache vor dem Rendern, ein Client-Provider ergänzt react-i18next, sodass Übersetzungen auf dem Server und im Browser funktionieren.

    frontend/src/i18n/I18nProvider.tsx

    Vorher

    export default function I18nProvider({ children, locale }: { children: ReactNode; locale: string }) {
      const [Provider, setProvider] = useState<ComponentType<{ i18n: unknown; children: ReactNode }> | null>(null);
    
      useEffect(() => {
        if (i18n.language !== locale) {
          i18n.changeLanguage(locale);
        }
        let cancelled = false;
        import("react-i18next").then((mod) => {
          if (!cancelled) {
            setProvider(() => mod.I18nextProvider as ComponentType<{ i18n: unknown; children: ReactNode }>);
          }
        });
        return () => { cancelled = true; };
      }, [locale]);
    
      if (!Provider) {
        return <>{children}</>;
      }
    
      return <Provider i18n={i18n}>{children}</Provider>;
    }

    Nachher

    // I18nServerSync.tsx: kein „use client“, läuft nur auf dem Server
    export default function I18nServerSync({ locale, children }: { locale: Locale; children: React.ReactNode }) {
      // i18next beim Server-Rendering auf die richtige Sprache zwingen
      if (i18n.language !== locale) {
        i18n.changeLanguage(locale);
      }
      return <>{children}</>;
    }
    
    // I18nProvider.tsx: clientseitig
    if (!i18next.isInitialized) {
      i18next.use(initReactI18next);
    }
    
    export default function I18nProvider({ children }: { children: ReactNode }) {
      useEffect(() => {
        // Sicherstellen, dass react-i18next initialisiert ist
        if (!i18next.hasResourceBundle(i18next.language, "translation")) {
          i18next.use(initReactI18next);
        }
      }, []);
    
      return <I18nextProvider i18n={i18next}>{children}</I18nextProvider>;
    }
    
    // layout.tsx
    <I18nServerSync locale={locale}>
      <I18nProvider>
        <Navbar />
        {/* … */}
      </I18nProvider>
    </I18nServerSync>
  2. 02 · Problem

    Formularfehler blieben auf Französisch: Mit Zod 4 umgingen eigene Meldungen in den Schemas die Fehlerzuordnung und wurden nie übersetzt.

    Lösung

    Meldungen aus den Schemas entfernt und eine einzige Fehlerzuordnung, die Feld und Fehlertyp in einen Übersetzungsschlüssel umsetzt, in allen 4 Sprachen.

    frontend/src/lib/validations.ts

    Vorher

    enfantNom: z
      .string()
      .min(2, "zodErrors.nameMin")
      .max(100, "zodErrors.nameMax")
      .regex(/^[a-zA-ZÀ-ÿ\s'-]+$/, "zodErrors.nameRegex"),
    
    dateNaissance: z
      .string()
      .min(1, "zodErrors.dateNaissanceRequired")
      .refine((val) => {
        // …
      }, "zodErrors.dateNaissanceAge"),

    Nachher

    enfantNom: z
      .string()
      .min(2)
      .max(100)
      .regex(/^[a-zA-ZÀ-ÿ\s'-]+$/),
    
    dateNaissance: z
      .string()
      .min(1)
      .refine((val) => {
        // …
      }),
  3. 03 · Problem

    Die Fotos der Schule waren zu schwer für die Verbindungen in Benin.

    Lösung

    Alle Bilder in WebP umgewandelt, von 6,2 MB auf 2,6 MB (−57 %), und eine installierbare PWA mit Service Worker.

  4. 04 · Problem

    Eine Schulwebsite muss gefunden, von allen gelesen und von Suchmaschinen verstanden werden.

    Lösung

    Sitemap, robots.txt, Canonical- und hreflang-Tags, Schema.org-Daten, Barrierefreiheit (reduzierte Bewegung, ARIA) und ein Designsystem in den Schulfarben.

In Zahlen

  • −57 %

    Bildgewicht (von 6,2 MB auf 2,6 MB)

  • 4

    Sprachen

  • PWA

    installierbar und offline nutzbar

Website besuchen
Vorschau des Projekts QuestPocket
Online

QuestPocket

Landingpage von Pocket Quest, einer mobilen App, die Bildschirmzeit in Belohnungen verwandelt: Wer gesunde Aufgaben erledigt, verdient Zeit in sozialen Netzwerken. Animierter 3D-Pixelart-Hintergrund, flüssiges Scrollen, Warteliste und Countdown, in 3 Sprachen.

Next.js 14Tailwind CSSThree.jsGSAPLenis

questpocket.netlify.app

Fallstudie

QuestPocket

Landingpage von Pocket Quest, einer mobilen App, die Bildschirmzeit in Belohnungen verwandelt: Wer gesunde Aufgaben erledigt, verdient Zeit in sozialen Netzwerken. Animierter 3D-Pixelart-Hintergrund, flüssiges Scrollen, Warteliste und Countdown, in 3 Sprachen.

Meine Rolle. Konzeption und Entwicklung der Landingpage. Die mobile App selbst (Flutter, NestJS) befindet sich noch in der Spezifikationsphase.

Next.js 14Tailwind CSSThree.jsGSAPLenis

Herausforderungen & Lösungen

  1. 01 · Problem

    Der mit Three.js gebaute 3D-Pixelart-Hintergrund brach das Server-Rendering und verursachte Hydration-Fehler, weil Three.js nur im Browser funktioniert.

    Lösung

    Die WebGL-Komponenten werden nur auf dem Client geladen, mit dynamischem Import und abgeschaltetem Server-Rendering, während der Rest der Seite ein statischer Export bleibt.

    landing-react/src/components/PixelBackground.tsx

    Vorher

    export default function PixelBackground() {
      return (
        <div className="fixed inset-0 z-0" style={{ filter: 'blur(60px)', opacity: 0.35, pointerEvents: 'none' }}>
          <Canvas
            // …
          </Canvas>
        </div>
      )
    }

    Nachher

    import dynamic from 'next/dynamic'
    
    function PixelBackgroundInner() {
      return (
        <div className="fixed inset-0 z-0" style={{ filter: 'blur(60px)', opacity: 0.35, pointerEvents: 'none' }}>
          <Canvas
            // …
          </Canvas>
        </div>
      )
    }
    
    export default dynamic(() => Promise.resolve(PixelBackgroundInner), { ssr: false })
  2. 02 · Problem

    Scroll-Animationen hingen von einem JavaScript-Hook ab, der eine Klasse umschaltete: Inhalte blieben bis zur Hydration unsichtbar, und das flüssige Scrollen funktionierte im statischen Export nicht.

    Lösung

    Animationen als reine CSS-Keyframes mit gestaffelten Verzögerungen neu geschrieben, also ohne JavaScript, und Lenis wird in einem Effect geladen, sobald die Seite steht.

    landing-react/src/app/globals.css

    Vorher

    .reveal {
      opacity: 0;
      transform: translateY(

    Nachher

    /* Scroll-gesteuerte Einblend-Animationen: nur CSS, kein JS nötig */
    .reveal {
      animation: revealUp 0.

In Zahlen

  • 3

    Sprachen (Englisch, Französisch, Deutsch)

  • 6

    Design-Iterationen in 3 Tagen

  • 3D

    Pixelart-Hintergrund in WebGL

Website besuchen
Vorschau des Projekts Laserbeni
In Arbeit

Laserbeni

Plattform und installierbare App (PWA) für ein Studio für dauerhafte Laser-Haarentfernung in Deutschland: Behandlungszonen und Preise, Angebote, FAQ, Vorher/Nachher, integrierte Buchung über Beautinda, Inhalte in Sanity und eine 3D-Glas-Atmosphäre.

Next.jsTailwind CSSSanityThree.jsPWA

Lieferung Ende Oktober 2026

Fallstudie

Laserbeni

Plattform und installierbare App (PWA) für ein Studio für dauerhafte Laser-Haarentfernung in Deutschland: Behandlungszonen und Preise, Angebote, FAQ, Vorher/Nachher, integrierte Buchung über Beautinda, Inhalte in Sanity und eine 3D-Glas-Atmosphäre.

Meine Rolle. Konzeption und Entwicklung der Plattform und der installierbaren App.

Next.jsTailwind CSSSanityThree.jsPWA

Herausforderungen & Lösungen

  1. 01 · Problem

    Kundinnen sollen buchen, ohne die App zu verlassen, aber das Studio nutzt bereits das Buchungssystem Beautinda.

    Lösung

    Beautinda öffnet sich in einem modalen Fenster innerhalb der PWA, inklusive Kundenkonto zum Ansehen oder Verschieben von Terminen: ein einziges Erlebnis.

  2. 02 · Problem

    Die Zielgruppe ist auf dem Smartphone und erwartet eine App, ohne Umweg über einen App Store.

    Lösung

    Mobile-First-PWA mit Manifest, Service Worker, Offline-Unterstützung und Installationshinweisen für iOS und Android, bereit für ein späteres natives Packaging.

  3. 03 · Problem

    Preise, Angebote und Fotos ändern sich oft, und die Studioleiterin ist keine Entwicklerin.

    Lösung

    Inhalte in Sanity: Preise, Pakete, Vorher/Nachher-Fotos und FAQ lassen sich ohne Eingriff in den Code bearbeiten.

In Zahlen

  • PWA

    installierbar auf iOS und Android

  • 1

    Buchungsablauf, ohne die App zu verlassen

Lieferung Ende Oktober 2026

Was die Menschen, für die ich baue, über die Zusammenarbeit sagen.

„Herr Agossou entwickelte für uns nicht lediglich eine gewöhnliche Website auf Grundlage einer gekauften Standardvorlage. Vielmehr programmierte und gestaltete er eine vollständig individuelle, technisch anspruchsvolle und exakt auf unsere betrieblichen Anforderungen zugeschnittene Weblösung.“

Websites & Plattformen

Maßgeschneiderte Websites und Anwendungen mit Next.js, React, TypeScript und Tailwind CSS: schnell, gut auffindbar und bei Bedarf als PWA installierbar.

CMS & Back-End

Inhalte ohne Entwickler pflegbar mit Strapi, Sanity oder Supabase, REST-APIs, Mitgliederbereiche mit Authentifizierung, automatische E-Mails und PDF-Dokumente.

Identität & UI/UX

Visuelle Identität, UI/UX-Entwürfe und Styleguides, dazu Marketingmaterial: Flyer, Broschüren und Social-Media-Visuals.

Mehrsprachig & barrierefrei

Websites in bis zu 7 Sprachen, auch Arabisch von rechts nach links, gestaltet für jedes Publikum und jeden Bildschirm.

Infrastruktur & Deployment

Docker-Container, Linux-Server, Continuous Integration, Spam-Schutz und Sicherheits-Header, Deployment auf Vercel, Netlify oder Railway.

Die Bestätigungs-E-Mails meines Kontaktformulars, von Hand gestaltet und programmiert: Tabellen, Inline-Styles, CSS-Animationen und eine Nur-Text-Version. Erst die Vorschau, dann der echte Code.

Eine helle Karte: ein Pixel-Logo, dessen >_ sich selbst tippt, ein gestaffeltes Einblenden, die Zusammenfassung der Anfrage und eine nummerierte Zeitleiste.

Das ist die echte E-Mail, die das Formular sendet, mit Beispieldaten, in der Sprache der Seite. Animationen laufen in Apple Mail und iOS Mail; Gmail, Yahoo und Outlook zeigen dieselbe E-Mail ohne Bewegung. Die echten Quelldateien aus dem Repository, nach Aufgaben getrennt (Vorlage, Abschnitte, Styles, Theme): derselbe Code erzeugt die Vorschau oben und die E-Mail an die Besucher. Der Stil wird mit einer einzigen Variable gewählt, CONFIRMATION_STYLE.

Full-Stack-Entwickler mit solidem Hintergrund in Netzwerk- und Telekommunikationstechnik. Ich entwickle moderne Webplattformen (Next.js, React, TypeScript), baue die Infrastruktur auf, auf der sie laufen, und wende aktuelle Technologien in echten Projekten an, vom CMS bis zur Oberfläche.

Frontend

  • Next.js
  • React
  • TypeScript
  • Tailwind CSS
  • Responsives Design
  • UX-/UI-Design

Backend & CMS

  • Strapi
  • Supabase
  • Sanity
  • PostgreSQL
  • REST-API
  • SEO (Grundlagen)

Infrastruktur & Netzwerke

  • Docker
  • Portainer
  • Linux (Kali)
  • SSH · Tailscale
  • CI/CD
  • Netzwerke & IT-Sicherheit
  • Glasfasertechnik

KI-natives Entwickeln

  • Agentisches Coding
  • KI-gestützte Entwicklung
  • KI-native Workflows
  • LLM-Integration (Groq, OpenAI)

Vermittlung & Sprachen

  • Didaktik
  • Lehrmaterial
  • Französisch (Muttersprache)
  • Deutsch (C1)
  • Englisch (B1)

Homelab

Eigener Cluster (ZimaBoard, Raspberry Pi) für selbst gehostete Dienste, verwaltet mit Docker und Portainer und automatisierten CI/CD-Pipelines.

Gemeinsam können wir etwas Klares und Wirkungsvolles schaffen. Buch einen Termin oder schreib mir: Wir sprechen bei einem (virtuellen) Kaffee darüber.

❯ write --to castoleBerlin --:--

Hallo, hier ist Castole! Erzähl mir, was du vorhast: Ich lese jede Nachricht selbst.

Ihre Angaben werden nur verwendet, um Ihnen zu antworten.

Castole Agossou Yao© 2026 Castole Agossou YaoNach oben