Einbettbares Widget
Eine vollständige Suchoberfläche mit einem Script-Tag und einem kurzlebigen Session-Token — Session-Vertrag, alle Datenattribute, Events, Theming und CSP-Hinweise.
aacsearch.js ist ein eigenständiges InstantSearch-Bundle: Suchfeld, Trefferliste mit Hervorhebung, Facetten, Sortierung, Paginierung, letzte Suchen. Kein Framework, keine Build-Kette, ein <script>-Tag. Es spricht über den Embed-Proxy von AACSearch mit Ihrem Index und legt nie einen Plattform-API-Schlüssel offen.
Die Seite hält nie den API-Schlüssel. Ihr Server tauscht den Schlüssel über POST /api/v1/embed/session gegen ein kurzlebiges, reines Such-Session-Token und rendert dieses Token als data-aac-token in die Seite.
Einbinden
Stellen Sie zuerst in Ihrem Backend ein Token aus (siehe Embed-Session):
curl -sS -X POST "$AAC_API/embed/session" \
-H "Authorization: Bearer $AAC_KEY" \
-H "Content-Type: application/json" \
-d '{ "index": "documentation", "expires_in": 900 }'Setzen Sie dann das gelieferte token in die Seite:
<div id="aacsearch"></div>
<script
src="https://search.example.com/embed/v1/aacsearch.js"
data-aac-token="aac_sess_…"
data-aac-target="#aacsearch"
data-aac-placeholder="Dokumentation durchsuchen …"
></script>| Attribut | Pflicht | Wirkung |
|---|---|---|
data-aac-token | ja | Embed-Session-Token aus POST /api/v1/embed/session. Fehlt es, mountet das Script nicht automatisch |
data-aac-target | nein | CSS-Selektor für das Zielelement, Standard #aacsearch |
data-aac-endpoint | nein | API-Basis-URL; sonst aus der Script-URL abgeleitet |
data-aac-placeholder | nein | Überschreibt den Platzhalter aus den Session-Einstellungen |
data-aac-client-id | nein | Stabile Besucher-ID (z. B. Ihre Nutzer-ID), damit Suchen und Klicks geräteübergreifend eine Historie bilden; sonst eine anonyme UUID im localStorage |
Ein data-aac-key gibt es nicht: Ein Plattform-API-Schlüssel darf weder im HTML noch in einem data--Attribut noch in einer URL stehen. Das Token ist nur zum Suchen, an einen Index gebunden und läuft von selbst ab.
Das Script hängt sich an DOMContentLoaded, wenn das Dokument noch lädt, sonst startet es sofort — die Position des Tags ist damit egal, solange das Zielelement existiert, wenn das Script läuft. Fehlt das Ziel, bricht es mit einem console.warn ab, statt die Seite zu stören.
Zwei URLs, zwei Caching-Strategien
| Pfad | Cache | Wofür |
|---|---|---|
/embed/v1/aacsearch.js | immutable, 1 Jahr | Produktion; die Version steckt im Pfad, ein Update braucht /v2/ |
/embed/aacsearch.js | 5 Minuten | Entwicklung; folgt immer der aktuellen Version |
Der versionslose Pfad ist bewusst kurz gecacht, damit Sie beim Entwickeln nicht gegen ein Jahr Browser-Cache kämpfen.
Programmatisch starten
<script src="https://search.example.com/embed/v1/aacsearch.js" async></script>
<script>
window.addEventListener("load", () => {
window.AACSearch.init({
token: "aac_sess_…", // serverseitig ausgestellt, nie der API-Schlüssel
target: "#suche",
endpoint: "https://search.example.com",
placeholder: "Wonach suchen Sie?",
clientId: "user_123", // optional
});
});
</script>init gibt ein Promise zurück, das nach dem Einrichten des Widgets aufgelöst wird. Bei Fehlern wirft es nicht, sondern rendert eine Meldung an Ort und Stelle:
| Situation | Was der Besucher sieht |
|---|---|
Kein token | Search is not configured for this page. (plus ein console.error mit Verweis auf POST /api/v1/embed/session) |
Token abgelaufen (401) | Search session expired — reload the page. |
Anderer Fehler bei /embed/config | Search is unavailable right now (503). (Statuscode variiert) |
window.AACSearch.version gibt die geladene Version ("1.0.0") zurück — nützlich, um in einem Fehlerbericht zu sehen, was tatsächlich lief. Außerdem stehen trackConversion() und getRecommendations() bereit. Ein zweiter init-Aufruf im selben Ziel rendert erneut dorthin und ersetzt den bisherigen Inhalt; das dokumentierte Muster ist ein Widget pro Seite.
Der Session-Vertrag
Jeder Aufruf des Bundles geht an den Proxy /api/v1/embed/* Ihrer Installation und authentifiziert sich mit dem Session-Token im JSON-Body — nie in der URL, damit es nicht in Browser-Verlauf, Referrer und Access-Logs landet.
| Aufruf | Endpunkt | Wann |
|---|---|---|
| Einstellungen | POST /api/v1/embed/config | Einmal beim Mounten: liefert index, scope, expires_at und settings |
| Suche | POST /api/v1/embed/search | Bei jeder Anfrage, Seiten-, Facetten- oder Sortierungsänderung |
| Events | POST /api/v1/embed/events | view beim ersten Rendern eines Treffers, click auf einen Treffer, conversion über trackConversion() |
| Empfehlungen | POST /api/v1/embed/recommendations | Wenn showRelated aktiv ist, und über getRecommendations() |
Die Suche geht nicht direkt vom Browser an den Suchdienst. Der Proxy prüft das Token bei jeder Anfrage, wendet den Mandantenfilter serverseitig an und nutzt dieselbe Pipeline wie die öffentliche API (Query-Log, Regeln, Defaults, Relaxation, Did-you-mean); Widget-Traffic erscheint also in den Analytics. Der Browser sieht weder Engine-Host noch -Schlüssel noch Collection-Namen. Jede proxierte Suche zählt gegen das searches-Kontingent; ein Proxy-Request trägt höchstens 5 Suchen, page ≤ 20, und per_page wird auf 50 gekappt.
Die Session-Laufzeit beträgt standardmäßig 900 Sekunden (höchstens 3600). Das Widget erneuert sein Token nicht selbst: Nach Ablauf liefern Anfragen 401, und das Widget zeigt The search session expired — reload the page. Bei dauerhaft geöffneten Seiten stellen Sie vor Ablauf ein neues Token aus und rufen init erneut auf — oder laden die Seite neu.
Events
Das Widget meldet Verhalten automatisch, nach bestem Bemühen und mit keepalive, sodass ein Tracking-Fehler die Host-Seite nie stört:
// Aus dem eigenen Code, z. B. nach dem Checkout:
window.AACSearch.trackConversion({ documentId: "sku-123", valueCents: 4900, query: "laufschuhe" });Events tragen das Session-token, eine Besucher-session_id (die Client-ID des Widgets), document_id, position und query. Sie landen in denselben Verhaltenstabellen wie die REST-Events-API, mit Zuordnung pro Widget, wenn das Token von einem gespeicherten Widget stammt.
Empfehlungen
const { items, mode } = await window.AACSearch.getRecommendations({
seedDocumentId: "sku-123",
limit: 4,
});getRecommendations liefert { items: [], mode: "unavailable" }, solange das Widget nicht gemountet ist, und { items: [], mode: "error" } bei einem Fehler. Die eingebaute „For you“-Leiste erscheint nur, wenn showRelated für den Index aktiv und Empfehlungen aktiviert sind.
Rendering
Aus settings baut das Widget die InstantSearch-Konfiguration: perPage als hitsPerPage, facets als Checkbox-Listen (bis zu 8 Werte pro Facette), sortOptions als Dropdown (ausgeblendet, wenn nur eine Option), showStats für die Trefferzeile, showRecent für Chips der letzten Anfragen (lokal pro Index gespeichert, erst ab drei Zeichen und nur bei Treffern), showVoice für die Spracheingabe des Browsers.
Oberflächentexte gibt es auf Englisch und Russisch, gewählt über navigator.language; alle anderen Sprachen fallen auf Englisch zurück. Eigene Texte lassen sich derzeit nicht injizieren — dafür braucht es eine eigene Oberfläche auf Basis von /embed/search.
Theming
{
"accent": "#0f766e",
"dark": true,
"placeholder": "Wissen durchsuchen",
"perPage": 10,
"facets": ["category", "tags"],
"showStats": true,
"showRecent": false,
"showVoice": true
}| Schlüssel | Standard | Wirkung |
|---|---|---|
accent | #2563eb | Buttons, Fokusring, Links, aktive Facetten |
dark | false | Dunkle Palette über CSS-Variablen |
placeholder | Search the docs… | Text im Eingabefeld |
perPage | 8 | Treffer pro Seite |
queryBy | title,content | Durchsuchte Felder |
facets | [] | Facettenfelder (Konsole: maximal 6) |
showStats | true | Trefferzahl |
showRecent | true | Chips der letzten Anfragen |
showVoice | false | Spracheingabe |
showImage | false | Trefferbild, wenn das Dokument eines hat |
showRelated | false | „For you“-Leiste unter den Ergebnissen |
sortOptions | Relevanz + created_at:desc | Sortier-Dropdown, erster Wert muss default sein |
Die Einstellungen liegen in SearchIndex.settings.embed und werden bei jeder Session-Ausstellung frisch gelesen: Eine Änderung im Panel „Erscheinungsbild" der Konsole ist beim nächsten Seitenaufruf sichtbar, ohne Deploy und ohne CDN-Invalidierung.
Für Feinarbeit am Styling greifen Sie die gerenderten Klassen ab und überschreiben sie im eigenen Stylesheet — accent und dark decken die häufigen Fälle ab, alles darüber ist CSS.
CSP und Datenschutz
Der gesamte Widget-Traffic geht an einen Origin, Ihre Installation: das Script selbst und die Aufrufe /api/v1/embed/*. Der Browser spricht nicht mit der Such-Engine, ein Engine-Origin ist daher nicht nötig.
Content-Security-Policy:
script-src 'self' https://search.example.com;
connect-src 'self' https://search.example.com;
style-src 'self' 'unsafe-inline';Das Bundle injiziert sein Stylesheet inline, daher braucht style-src 'unsafe-inline'. connect-src ist die Direktive, die man vergisst: Das Script lädt problemlos, aber die Aufrufe /embed/config, /embed/search und /embed/events scheitern ohne sie still im Netzwerk-Tab. Wird die API von einem anderen Host als das Script ausgeliefert, erlauben Sie beide Origins.
Datenschutz: Das Widget setzt keine Cookies. Die letzten Suchen und die Besucher-ID (aac_client_id, sofern Sie keine eigene übergeben) liegen im localStorage des Besuchers. Abgefragte Begriffe und Klick-Events landen im Query-Log bzw. in den Verhaltensdaten des Mandanten, wie bei jeder Suche über die API.
Grenzen
- Kein URL-Routing. Die Suche schreibt den Query-String nicht in die Adresszeile; ein geteilter Link öffnet keine vorbefüllte Suche. Dafür braucht es eine eigene Oberfläche.
- Keine eigenen Templates. Trefferlayout, Facetten- und Sortier-Widgets sind fest.
- Kein automatisches Token-Refresh. Sessions leben standardmäßig 15 Minuten, höchstens eine Stunde; bei langlebigen Seiten neu ausstellen und
initerneut aufrufen. - Ein Index pro Widget. Föderierte Suche über mehrere Indizes unterstützt das Widget nicht.
- Sprache ist UI-Sprache, nicht Suchsprache. Die Stemming-Sprache kommt aus der Anfrage, nicht aus
navigator.language.
Kontingente und Abrechnung
Was eine Einheit ist, wie Blöcke das Wallet belasten und wie Sie den Verbrauch sehen, bevor aus einem Limit eine 429 wird.
Private Felder
Felder wie Einkaufspreise bleiben für vertrauenswürdige Aufrufer nutzbar und erreichen nie Widget, Assistent oder Schlüssel ohne Berechtigung.