Neu veröffentlicht: E-Commerce mit Power Pages, Stripe & Analytics

· Webentwicklung  · 6 minuten Lesezeit

Chrome Extension mit Shadow DOM Preview und Tailwind CSS v4

Phase 2 bringt drei Features: ein Shadow DOM Preview-Overlay vor dem Speichern, farbkodierte Duplikatserkennung beim Hover und ein Popup mit Tailwind CSS v4.

Phase 2 bringt drei Features: ein Shadow DOM Preview-Overlay vor dem Speichern, farbkodierte Duplikatserkennung beim Hover und ein Popup mit Tailwind CSS v4.

Inhalt

Was Phase 1 noch offengelassen hatte

Ich hatte eine Chrome Extension gebaut, die funktioniert. Element hovern, Ctrl+Q, Screenshot wird zugeschnitten und mit Instagram-Metadaten an das RAG-Backend gesendet. Der Aufbau dieser Extension und die Architektur der drei isolierten Laufzeitkontexte habe ich im Artikel zur Chrome Extension MV3 Architektur beschrieben.

Aber nach einigen Wochen im Einsatz wurde klar: funktionieren und benutzbar sein sind zwei verschiedene Dinge.

Das Popup hatte kein echtes visuelles Design. Bevor ein Post gespeichert wurde, gab es keine Möglichkeit, den Screenshot zu prüfen. Und das größte Problem: Die Extension wusste nicht, ob ein Post bereits in der Datenbank gespeichert war. Jeder Hover sah gleich aus, egal ob der Post neu oder bereits fünf Mal erfasst war.

Phase 2 löst genau diese drei Punkte.

Tailwind CSS v4 im Extension-Popup

Das erste Problem war kosmetisch, aber nicht unwichtig. Vertrauen in ein Tool entsteht auch durch visuelle Qualität. Ein Popup, das wie eine HTML-Seite aus 2012 aussieht, kommuniziert keine Professionalität.

Die Entscheidung fiel auf Tailwind CSS v4 mit dem @tailwindcss/vite-Plugin, das den Tailwind-Compiler direkt in die Vite-Build-Pipeline einbindet. Die Kombination mit @crxjs/vite-plugin funktioniert ohne Konfigurationsänderungen. In vite.config.ts reicht ein einzelner Eintrag:

import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [tailwindcss(), crx({ manifest })],
});

Die CSS-Datei des Popups enthält damit nur eine Zeile:

@import 'tailwindcss';

Dynamisch gesetzte Klassen, also Zustände, die JavaScript zur Laufzeit an ein Element hängt, können nicht vollständig als Utility-Klassen im HTML hinterlegt werden. Für diese Fälle bleibt @apply in der CSS-Datei die sauberere Lösung:

.health-dot.ok {
  @apply bg-emerald-500;
}
.health-dot.error {
  @apply bg-red-500;
}

Das Popup zeigt jetzt: Backend-Healthcheck mit Statusdot, aktive Capture-Kürzel mit Remap-Link, Retry-Queue mit Badge-Count und die letzten fünf Captures als History-Liste.

Shadow DOM als Preview-Overlay

Das zweite Feature war architektonisch anspruchsvoller. Nach dem Drücken von Ctrl+Q sollte ein Vorschau-Overlay erscheinen, das den zugeschnittenen Screenshot zeigt, bevor irgendwelche Daten das Backend erreichen.

Das klingt einfach. Ist es nicht.

Das Problem: Ein Extension-Content-Script injiziert Elemente direkt in das DOM der Seite. Wenn die Seite eigene Stile hat, CSS-Resets oder z-index-Probleme, wird das Overlay entweder optisch inkonsistent dargestellt oder funktioniert gar nicht. Das gilt besonders für Instagram, das ein sehr opinionated CSS-Framework mitbringt.

Die Lösung ist Shadow DOM. Ein Shadow Root ist vollständig vom Host-Dokument isoliert. Kein CSS der Seite greift durch. Keine Styles der Extension leaken nach außen.

Shadow DOM Isolation zwischen Extension-Overlay und Instagram-Seite Abbildung: Shadow DOM als Isolationsschicht. Das Overlay-HTML lebt in einem geschlossenen Shadow Root. CSS der Seite erreicht das Overlay nicht, CSS des Overlays erreicht die Seite nicht.

// src/content/preview-overlay.ts
export const showCapturePreview = (dataUrl: string, width: number, height: number): Promise<boolean> => {
  return new Promise((resolve) => {
    const host = document.createElement('div');
    const shadow = host.attachShadow({ mode: 'closed' });

    // Fully isolated overlay DOM in the Shadow Root
    shadow.innerHTML = `
      <style>/* eigene Stile, von außen unsichtbar */</style>
      <div class="backdrop">
        <div class="card">
          <img src="${dataUrl}" />
          <button id="save">Speichern</button>
          <button id="cancel">Abbrechen</button>
        </div>
      </div>
    `;

    shadow.getElementById('save')?.addEventListener('click', () => {
      host.remove();
      resolve(true);
    });

    shadow.getElementById('cancel')?.addEventListener('click', () => {
      host.remove();
      resolve(false);
    });

    document.body.appendChild(host);
  });
};

Das Overlay ist ein Promise<boolean>. Der Background Service Worker wartet darauf. Wenn der User abbricht, wird der gesamte Capture-Flow still beendet. Wenn er bestätigt, läuft die Extraktion weiter.

Keyboard-Support (Enter zum Bestätigen, Escape zum Abbrechen) und Backdrop-Click sind ebenfalls eingebaut. Das Overlay verhält sich wie ein modales Dialogfenster, ohne dass es eines ist.

Preview-Overlay der Chrome Extension mit Vorschau-Screenshot und Bestätigen-Dialog Das Overlay erscheint direkt auf der Instagram-Seite, isoliert im Shadow DOM. Links der zugeschnittene Bild-Ausschnitt, rechts die zwei Aktionen. Die Seite dahinter bleibt sichtbar, aber abgedunkelt. Der Fokus liegt auf der Entscheidung: speichern oder verwerfen.

Duplikatserkennung mit farbigem Hover-Highlight

Das dritte Feature ist das, das den größten praktischen Unterschied macht.

Ohne Duplikatserkennung ist jeder Hover identisch. Die Extension weiß nicht, ob ein Post bereits 20 Mal gespeichert wurde. Der Analyst muss das aus dem Gedächtnis wissen oder jedes Mal manuell prüfen.

Mit Phase 2 kommuniziert die Extension den Status direkt über die Umrandungsfarbe beim Hover:

FarbeBedeutung
🟢 GrünNeuer Post, noch nicht in Qdrant
🟡 GelbDuplikat, bereits erfasst
🔴 RotKein Instagram-Post erkennbar

Das Backend bekommt einen neuen Endpunkt: GET /exists?postId=X. Er nutzt Qdrants Scroll-API mit einem Payload-Filter, ohne Embedding-Berechnung, da keine Ähnlichkeitssuche benötigt wird, nur ein exakter Treffer:

// backend/src/routes/exists.ts
const { points } = await qdrantClient.scroll(COLLECTION_NAME, {
  filter: { must: [{ key: 'metadata.postId', match: { value: postId } }] },
  limit: 1,
  with_payload: false,
  with_vector: false,
});
return res.json({ exists: points.length > 0 });

Das Content Script extrahiert beim Hover die PostId aus dem Article-Link und schickt eine Anfrage über den Background Service Worker. Warum nicht direkt vom Content Script? Content Scripts können fetch() auf localhost nicht direkt aufrufen, wenn die Seite eine restriktive CSP hat. Der Background Worker dient als Proxy. Das ist ein bekanntes Muster in MV3, das ich im Artikel zur MV3-Architektur beschrieben habe.

Das Ergebnis: Beim Hovern über einen Instagram-Post weiß der Analyst sofort, ob dieser Post neu ist oder bereits in der Datenbank liegt. Keine manuellen Checks. Kein Klicken ins Backend.

Ablauf der farbkodierten Duplikatserkennung beim Hover Abbildung: Ablauf der Duplikatserkennung. Beim mouseover-Event extrahiert das Content Script die PostId, schickt sie über den Background Worker an GET /exists und setzt die Umrandungsfarbe je nach Antwort.

Was Phase 2 aus der Extension macht

Phase 1 war eine funktionierende Extension. Phase 2 macht daraus ein Arbeitsgerät.

Der Unterschied ist subtil, aber er ist real. Ein Tool, das dir vor dem Speichern zeigt was du speicherst, und das dir beim Hovern schon sagt ob du das überhaupt speichern musst, reduziert mentalen Overhead. Das ist besonders relevant in Szenarien, in denen Analysten täglich hunderte Posts sichten.

Wie diese Entscheidungen beim Testen auf unerwartete Probleme gestoßen sind und was das für die Architektur bedeutete, beschreibe ich in den nächsten zwei Artikeln der Serie.

Alle Artikel der Serie

  1. Vision und Systemübersicht: Chrome Extension, RAG-Architektur, Projekthintergrund: Artikel lesen
  2. RAG-System Aufbau: Qdrant, Embeddings, Cosine-Ähnlichkeit in TypeScript: Artikel lesen
  3. AI Provider Abstraktion: Ollama vs. OpenAI, Interface-Design, kein Vendor-Lock-in: Artikel lesen
  4. Chrome Extension MV3: Drei isolierte Laufzeitkontexte, Message Passing, Strategy Pattern: Artikel lesen
  5. Docker Compose Strategie: Override-Pattern, von lokal zu Azure: Artikel lesen
  6. Ollama lokal vs. Docker: Die Entscheidung und ihre Konsequenzen: Artikel lesen
  7. Ollama Auto-Pull Entrypoint: Automatisiertes Modell-Setup beim Container-Start: Artikel lesen
  8. tsconfig und Vite: Node16 vs. bundler, warum Vite eigene Regeln hat: Artikel lesen
  9. Instagram Caption mit MutationObserver vollständig laden: Artikel lesen
  10. Chrome Extension Foundation mit Health-Dot und Retry-Queue: Artikel lesen
  11. Phase 2 Features: Shadow DOM Overlay, Tailwind v4, Duplicate Detection (dieser Artikel)
  12. Race Condition bei der Plattformerkennung: Wie ein UI-Event die Instagram-Erkennung bricht: Artikel lesen
  13. PostId-Extraktion in zwei Instagram-Layouts: querySelector vs. Ancestor-Traversal: Artikel lesen

Du baust eine Chrome Extension für interne Tools oder automatisiertes Content-Monitoring? Lass uns das gemeinsam einschätzen.

Zurück zum Blog

Ähnliche Beiträge

Alle Beiträge ansehen