Pagefind in der Praxis: Volltextsuche ohne Server, Datenbank und Abo

Pagefind von Einbindung bis Indizierung: Filter, Mehrsprachigkeit, Barrierefreiheit, Performance, Kosten — mit Messwerten aus zwei Projekten und klaren Grenzen.

von ·

Drück auf dieser Seite einmal Strg+K (auf dem Mac ⌘+K) oder tipp auf die Lupe. Es öffnet sich eine Suche über knapp tausend Seiten in zwei Sprachen, und die Treffer stehen da, bevor du mit dem Tippen fertig bist. Auf meinem Server läuft dafür nichts: kein PHP, keine Datenbank, kein Suchdienst, kein Abo. Was du durchsuchst, sind ein paar Dateien, die beim Build entstanden sind. Das Werkzeug dahinter heißt Pagefind (öffnet in neuem Tab) (englisch), und ich setze es inzwischen in zwei Projekten ein: in dieser Knowledge Base und im JPKCom Desktop, dessen Suche unter jpkc.com/desktop/ denselben Index mitbenutzt.

Ich bin mit Pagefind sehr zufrieden, und dieser Artikel macht daraus keinen Hehl. Er soll aber mehr sein als eine Liebeserklärung. Ich zeige dir, wie die Einbindung funktioniert, worauf es bei der Indizierung wirklich ankommt, wie Filter, Tags und Mehrsprachigkeit zusammenspielen, was das für die Barrierefreiheit heißt, was eine Suche tatsächlich an Daten kostet — gemessen, nicht geschätzt — und wo Pagefind an Grenzen stößt, die du kennen solltest, bevor du dich entscheidest.

Die Kurzfassung

Frage Antwort
Was ist Pagefind? Eine statische Volltextsuche: Ein Programm indiziert nach dem Build das fertige HTML, der Browser durchsucht den Index per WebAssembly
Was läuft auf dem Server? Nichts. Der Index besteht aus statischen Dateien, die jeder Webserver ausliefert
Was kostet es? Nichts. Open Source unter MIT-Lizenz, kein Dienst, kein Konto
Was lädt eine Suche? Hier gemessen: rund 100 KB beim ersten Öffnen, danach 70–125 KB pro Suchbegriff (bei langsamem Tippen mehr), bei knapp 1.000 Seiten
Wie lange dauert das Indizieren? Hier 6,6 Sekunden für 996 Seiten, bei einem Gesamt-Build von gut vier Minuten
Mehrsprachig? Ja, automatisch: ein Index pro Sprache, gewählt über <html lang>, Wortstamm-Erkennung für Deutsch, Englisch und viele weitere Sprachen
Die wichtigsten Grenzen Keine Tippfehler-Toleranz, keine Zerlegung deutscher Komposita, kein Echtzeit-Index, alles im Index ist öffentlich

Was Pagefind ist

Pagefind ist eine Suchbibliothek für statische Websites. Liam Bigelow (öffnet in neuem Tab) (englisch) hat sie im Juli 2022 bei CloudCannon vorgestellt; heute bezeichnet sich das Projekt als unabhängiges Open-Source-Projekt, das ursprünglich bei CloudCannon entstanden ist, und das Repository liegt unter github.com/Pagefind/pagefind (öffnet in neuem Tab) (englisch). Geschrieben ist Pagefind in Rust (öffnet in neuem Tab), lizenziert unter MIT. Aktuell ist Version 1.5.2 vom 12. April 2026, die ich hier auch einsetze.

Das Prinzip ist schnell erklärt und zugleich der Grund, warum ich Pagefind so mag:

  1. Nach dem Build läuft das Pagefind-Programm über den Ausgabeordner deines Static-Site-Generators — hier _site/ von Eleventy. Es liest das fertige HTML, nicht deine Markdown-Quellen. Damit ist es egal, womit die Seite gebaut wurde: Eleventy, Hugo, Astro, Jekyll oder ein Ordner voller handgeschriebener HTML-Dateien.
  2. Pagefind schreibt einen Index als Ordner pagefind/ mitten in die gebaute Seite. Der Index ist in viele kleine Dateien zerlegt, alphabetisch nach Wortanfängen, dazu ein Fragment pro Seite mit Titel, URL und Text.
  3. Im Browser lädt eine kleine JavaScript-Datei ein WebAssembly (öffnet in neuem Tab)-Modul, das die eigentliche Suche ausführt. Es holt nur die Index-Stücke, die zum Suchbegriff passen, und die Fragmente der Treffer, die du anzeigst.

Die Projektseite formuliert das Ziel so: Eine Volltextsuche über eine Website mit 10.000 Seiten soll insgesamt unter 300 KB übertragen, die Bibliothek eingeschlossen, bei den meisten Websites eher um 100 KB. Weiter unten zeige ich dir, wie das bei mir aussieht.

Einbindung: ein Befehl nach dem Build

Für die meisten Projekte reicht eine Zeile. In dieser Knowledge Base steht in der package.json (gekürzt):

"build": "npm-run-all --serial \"build:*\" pagefind",
"pagefind": "rimraf ./_site/pagefind && pagefind --site ./_site"

Zwei Details stecken darin. Pagefind hängt bei mir direkt im Build-Skript und nicht als postbuild-Hook, weil mein Projekt in der .npmrc ignore-scripts=true setzt — warum, steht in Sicheres Arbeiten mit Node.js und npm. Das schaltet auch Hooks wie postbuild ab, der Index entsteht deshalb als ausdrücklicher Schritt. Und vor jedem Lauf räume ich den alten Index weg: Die Dateinamen tragen Hashes ihres Inhalts, und ohne Aufräumen blieben die Stücke früherer Builds liegen und würden mit ausgeliefert. Das Paket selbst braucht übrigens kein postinstall: Seit Version 1.0 kommt die passende Binary für dein Betriebssystem über plattformspezifische, optionale Abhängigkeiten.

Für die Oberfläche hast du drei Wege:

  • Die fertige Oberfläche. Seit Version 1.5 empfiehlt Pagefind die neue Component UI: Web Components wie <pagefind-modal>, <pagefind-searchbox> oder <pagefind-results>, die du einfach ins HTML setzt. Die ältere Default UI und die Modular UI gibt es weiter, und sie werden weiter unterstützt. Neue Funktionen kommen aber zuerst in die Component UI, und die Doku nennt die Default UI ausdrücklich „nicht mehr die erste Empfehlung“.
  • Die JavaScript-API. Du importierst pagefind.js, rufst search() auf und baust die Oberfläche selbst. Das ist mein Weg in beiden Projekten.
  • Die Indizierungs-API für Node.js und Python, wenn du nicht HTML-Dateien, sondern eigene Datensätze indizieren willst. Dazu weiter unten mehr.

Die API ist angenehm klein. Im Kern sieht meine Suche so aus:

js
const pagefind = await import("/db/pagefind/pagefind.js");
await pagefind.init();

const search = await pagefind.search("docker");
const results = await Promise.all(
  search.results.slice(0, 10).map((result) => result.data())
);
// results[0].url, results[0].meta.title, results[0].excerpt

search() liefert zunächst nur Treffer-IDs mit Gewichtung. Titel, URL und Textauszug lädt erst data(), und zwar für jeden Treffer einzeln. Das ist der Trick, mit dem Pagefind Bandbreite spart: Was du nicht anzeigst, wird nicht geladen.

Unterverzeichnis statt Domain-Wurzel. Diese Seite liegt unter /db/, nicht auf der Wurzel der Domain. Eleventys HTML-Transform schreibt interne Links automatisch auf /db/… um, einen JavaScript-Import aber nicht. Deshalb steht der Pfad im Code fest auf /db/pagefind/pagefind.js. Den Ort der Index-Dateien erkennt Pagefind selbst anhand der Adresse, von der pagefind.js geladen wurde; bei Bedarf setzt du ihn über die Option basePath.

Indizierung: was in den Index gehört — und was nicht

Hier entscheidet sich, ob deine Suche gut wird. Pagefind macht es dir leicht, aber nicht narrensicher.

Die Grundeinstellung. Ohne weitere Angaben indiziert Pagefind den kompletten <body> jeder Seite. Von sich aus lässt es unter anderem <nav>, <footer>, <script>, <form>, <label>, <svg>, <iframe> und <template> weg. Das klingt nach genug, ist es aber nicht: <header>, <aside> und jedes <div> landen im Index. Wie viel das ausmacht, zeigt eine Zahl aus dieser Knowledge Base. Das Menü „Seite kopieren“ ist ein <div> mit den Einträgen „In Claude öffnen“ und „In ChatGPT öffnen“, und es steht auf jeder Artikelseite. Ohne Ausschluss träfe eine Suche nach „Claude“ praktisch jede der knapp 500 Seiten ihrer Sprache statt der knapp 50, auf denen Claude tatsächlich vorkommt — ausgerechnet bei einem der Begriffe, um die sich diese Knowledge Base am meisten dreht.

Zwei Wege, das zu regeln. Mit data-pagefind-body auf dem Artikel-Container sagst du: Nur das hier zählt. Mit data-pagefind-ignore auf einzelnen Elementen sagst du: Das hier nicht. Der erste Weg ist strenger, hat aber eine scharfe Kante: Sobald auch nur eine Seite data-pagefind-body trägt, indiziert Pagefind ausschließlich Seiten mit diesem Attribut. Ein neues Layout, das du in einem Jahr anlegst und bei dem du das Attribut vergisst, verschwindet still aus der Suche. Ich habe mich deshalb für den zweiten Weg entschieden: Jedes Element, das auf vielen Seiten gleich aussieht — Kopfleiste, Seitenleisten, Inhaltsverzeichnis, Blätter-Leiste, das Kopier-Menü — trägt data-pagefind-ignore. Schlimmstenfalls rutscht dann zu viel in den Index, und das sehe ich. Ein fehlender Treffer fällt dagegen niemandem auf.

Auch generierter Inhalt kann stören. Die Listen auf meinen Übersichtsseiten wiederholen Titel und Beschreibung von Artikeln, die längst einzeln im Index stehen. Damit trafen die Übersichtsseiten fast jede Anfrage aus ihrem Bereich und verdrängten den Artikel, den du eigentlich suchst. Seit diese Listen ebenfalls ignoriert werden, sank der indizierte Text der zehn Bereichs-Übersichten von 7.186 auf 671 Wörter. Meine Faustregel daraus: Steht ein Text schon auf einer anderen indizierten Seite, gehört er hier nicht in den Index.

Ganze Seiten ausschließen. Die 404-Seite mit ihrer Tag-Wolke wäre ein Treffer bei fast jeder Themensuche. Sie bekommt in meinem Layout über einen Frontmatter-Schalter data-pagefind-ignore auf den <body>. Seiten ganz ohne <html>-Element verarbeitet Pagefind standardmäßig gar nicht — praktisch für die HTML-Schnipsel meiner htmx-Demo, die so automatisch draußen bleiben.

Gewichtung. Überschriften zählen mehr als Fließtext: h1 mit 7,0, h2 mit 6,0 und so weiter bis 1,0 für normalen Text. Mit data-pagefind-weight setzt du eigene Werte zwischen 0 und 10, und die wirken quadratisch — eine 2,0 zählt also etwa viermal so viel. Seit Version 1.5 durchsucht Pagefind auch die Metadaten und gewichtet einen Treffer im Titel standardmäßig fünffach.

Metadaten. Den Titel nimmt Pagefind aus der ersten h1, das Bild aus dem ersten <img> danach. Mit data-pagefind-meta überschreibst du beides oder hängst eigene Felder an, etwa ein Datum oder einen Autor, die deine Oberfläche dann anzeigt. Und mit data-pagefind-index-attrs="title,alt" landen auch Attribut-Texte im Index, zum Beispiel die Alternativtexte deiner Bilder.

Unterergebnisse. Seit Version 1.0 zerlegt Pagefind eine Seite an Überschriften mit id in Abschnitte und liefert sie als sub_results mit eigenem Sprungziel. Wer lange Artikel hat, kann so direkt zum passenden Abschnitt führen statt nur zur Seite.

Prüfen statt hoffen. Der Index ist kein schwarzer Kasten. Die Fragment-Dateien sind gzip-komprimiert und enthalten — nach einer kurzen Kennung pagefind_dcd — JSON mit dem kompletten indizierten Text jeder Seite. Nach jeder Änderung an Layouts entpacke ich sie per Skript und zähle, auf wie vielen Seiten verdächtige Begriffe wie „Claude“ oder „Auf dieser Seite“ vorkommen. Rückt eine Trefferzahl in die Nähe der Gesamtzahl der Seiten, ist irgendwo Bedien-Text in den Index gerutscht. Seit Version 1.4 gibt es außerdem den Pagefind Playground, der zeigt, wie ein Ergebnis gewertet wurde. Mit --serve ist er immer dabei, auf die Live-Seite kommt er nur auf ausdrücklichen Wunsch.

Filter, Kategorien und Tags

Pagefind kann mehr als Volltext. Mit data-pagefind-filter hängst du einer Seite Filterwerte an, nach denen die Suche einschränken kann — Kategorien, Tags, Sprache, Autor, was immer du brauchst. Dazu kommt data-pagefind-sort für eigene Sortierungen, etwa nach Datum.

In dieser Knowledge Base trägt jede Artikelseite bis zu vier Arten von Filtern: Bereich (Blog, Cheat-Sheets, Tools, …), Sprache, Kategorie und jeden ihrer Tags. Das Markup dafür ist unspektakulär, mit einer Lehre darin:

<span hidden data-pagefind-filter="bereich:blog"></span>
<span hidden data-pagefind-filter="sprache:de"></span>
<span hidden data-pagefind-filter="tag:accessibility"></span>
<span hidden data-pagefind-filter="tag:performance"></span>

Jeder Wert steht in einem eigenen Element. Ein fester Wert in der Form schlüssel:wert darf in einem Attribut nur einmal und nur am Ende einer Liste stehen: Pagefind nimmt alles nach dem ersten Doppelpunkt als Wert, Kommas eingeschlossen. Aus bereich:blog, sprache:de wird so ein einziger Filter bereich mit dem Wert „blog, sprache:de“, und sprache fehlt ganz — ohne Fehlermeldung. Die schnellste Kontrolle ist die Build-Ausgabe: Hier meldet sie „Indexed 8 filters“, also vier Filter je Sprache, zusammengezählt über beide Sprachen. Diese Zeile schaue ich mir nach jeder Änderung an den Layouts an.

Tags ohne Tag-Seiten. Auf diesen Filtern baut die Themennavigation dieser Seite auf. Klickst du im Kopf eines Artikels auf einen Tag-Chip, öffnet sich die Suche mit dem Filter tag und der Sprache der aktuellen Seite — ohne Suchbegriff, also mit allen Seiten zu diesem Thema:

const search = await pagefind.search(null, {
  filters: { tag: ["accessibility"], sprache: ["de"] },
});

Tippst du danach etwas ein, hebt die Suche den Tag-Filter auf und sucht wieder überall. Dedizierte Tag-Übersichtsseiten brauche ich so nicht; bei Hunderten Tags wären das Hunderte dünne Seiten, die niemand pflegt. Pagefind liefert über filters() übrigens auch die Anzahl der Treffer pro Filterwert, falls du eine Facettensuche mit Zählern bauen willst.

Mehrsprachigkeit: ein Index pro Sprache, ohne Konfiguration

Das ist eine der Stellen, an denen Pagefind still und gut arbeitet. Beim Indizieren liest es das lang-Attribut des <html>-Elements und baut für jede Sprache einen eigenen Index. Im Browser schaut es auf dieselbe Angabe und lädt nur den passenden. Auf einer deutschen Seite durchsuchst du also die 498 deutschen Seiten, auf einer englischen die 498 englischen, und das ganz ohne Einstellung. Beim Build meldet Pagefind einfach „Discovered 2 languages“ und zählt die Seiten je Sprache auf.

Für Deutsch und Englisch bringt Pagefind Stemming mit, also eine Wortstamm-Erkennung (öffnet in neuem Tab): Wer „Suchmaschine“ sucht, findet auch „Suchmaschinen“, und „Kontaktformulare“ findet „Kontaktformular“. Die Sprachtabelle der Doku (öffnet in neuem Tab) (englisch) nennt Stemming für gut 30 Sprachen, von Arabisch bis Türkisch, dazu Übersetzungen der Oberfläche für über 40. Für Sprachen ohne Stemming funktioniert die Suche trotzdem, nur eben ohne Abgleich über Wortformen. Chinesisch, Japanisch und Thai, die Wörter nicht durch Leerzeichen trennen, zerlegt die erweiterte Pagefind-Version beim Indizieren in Wörter, und seit 1.5 auch die Suchanfrage im Browser.

Zwei Dinge solltest du wissen. Erstens: Willst du über Sprachgrenzen hinweg suchen, geht das nur mit einem Kniff — entweder ein gemeinsamer Index über die Option force_language, dann aber ohne passendes Stemming für die zweite Sprache, oder ein zusätzlich zusammengeführter Index (dazu gleich mehr). Zweitens: Wechselt deine Seite die Sprache ohne Neuladen, musst du Pagefind mit destroy() und init() neu starten, damit es den richtigen Index wählt. Der JPKCom Desktop macht genau das, wenn du dort die Sprache umschaltest.

Seit Version 1.5 findet die Suche außerdem Wörter über Akzente hinweg: „cafe“ findet „Café“, exakte Treffer werden aber höher gewertet.

Barrierefreiheit: Pagefind liefert Daten, die Oberfläche entscheidet

Bei der Barrierefreiheit lohnt es sich, genau hinzuschauen, was Pagefind tut und was nicht. Die Suchmaschine selbst liefert Daten. Ob ein Screenreader mitbekommt, dass es zwölf Treffer gibt, ob du die Ergebnisse mit den Pfeiltasten erreichst und wohin der Fokus beim Schließen springt, entscheidet allein die Oberfläche.

Die alte Default UI hatte hier Lücken. Ein Audit nach dem französischen Standard RGAA hat 2024 in Issue #727 (öffnet in neuem Tab) (englisch) drei Befunde dokumentiert: kein sichtbares Label am Suchfeld, eine leere Liste im Markup und eine Trefferzahl, die Screenreadern nicht angesagt wird. Geschlossen wurde das Issue nicht mit einem Fix der alten Oberfläche, sondern mit dem Verweis auf die neue Component UI.

Die Component UI ist deutlich besser gebaut. Laut Doku folgt sie den Mustern der WAI-ARIA Authoring Practices, und der Quelltext bestätigt das: Das Suchfenster <pagefind-modal> ist ein natives <dialog>, das den Rest der Seite sperrt, mit Escape schließt und den Fokus beim Schließen an den Auslöser zurückgibt. Darin steht ein beschriftetes Suchfeld, und die Pfeiltasten führen den Fokus durch die Ergebnislinks. Die Dropdown-Variante <pagefind-searchbox> setzt dagegen das Combobox-Muster um: ein Suchfeld mit aria-activedescendant über einer Listbox, bedienbar mit Pfeiltasten, Enter und Escape. Trefferzahlen kündigt eine unsichtbare Live-Region höflich an, „keine Treffer“ und Fehler dagegen mit Nachdruck. Unterstützende Texte werden in die Sprache der Seite übersetzt. Mit einem Screenreader getestet habe ich die Component UI nicht — wenn du sie einsetzt, prüf sie in deinem Kontext.

Was das konkret heißt. Die Trefferzahl ist nach WCAG 2.2 eine Statusmeldung (öffnet in neuem Tab) (englisch) im Sinne von Erfolgskriterium 4.1.3, die Liste selbst nicht. Der BITV-Test nennt im Prüfschritt 9.4.1.3 (öffnet in neuem Tab) ausdrücklich „5 Suchergebnisse“ als Beispiel und empfiehlt einen Container, der schon beim Laden leer im DOM steht und als Live-Region ausgezeichnet ist, etwa mit role="status" und aria-atomic="true". Dazu kommen die Grundlagen: Bedienbarkeit per Tastatur (2.1.1), eine schlüssige Fokus-Reihenfolge (2.4.3) — also Fokus ins Suchfenster hinein und beim Schließen zurück zum Auslöser — und ein Suchfeld mit echtem Label (1.3.1).

Meine eigenen Oberflächen. Weil ich in beiden Projekten über die API arbeite, trage ich das selbst. Die Suche im JPKCom Desktop ist eine Combobox mit Listbox, gruppiert nach Apps, Sammlungen und Artikeln, und sie sagt die Trefferzahl an, sobald du mit dem Tippen innehältst. Die Palette dieser Knowledge Base nutzt ein natives <dialog> mit verstecktem Label und eine Live-Region für die Trefferzahl. Das ist der eigentliche Vorteil der API: Du bist nicht auf die Entscheidungen einer fremden Oberfläche angewiesen, trägst dafür aber auch die Verantwortung.

Wenn dich das Thema vertieft interessiert: Wie die Suche als search-Landmark ins Seitengerüst gehört, steht in HTML-Landmarks in der Praxis.

Anpassen: Funktion und Design

Design. Die mitgelieferten Oberflächen lassen sich anpassen, aber in Grenzen: Die Component UI ist als Web Components gebaut, schirmt sich gegen das CSS deiner Seite ab und lässt sich über eigene CSS-Variablen (--pf-*) und eigene Templates gestalten. Baust du über die API, gibt es gar kein Design, das du bekämpfen musst. Meine Palette sieht aus wie der Rest dieser Seite, weil sie aus denselben Design-Tokens und Klassen besteht. Im Desktop folgt die Suche dem jeweiligen Theme, weil sie dort einfach ein weiteres Fenster-Element ist.

Funktion. Über pagefind.options() stellst du ein, wie lang die Textauszüge sind (excerptLength, Standard 30 Wörter), und an der Rangfolge drehst du über die Option ranking. Pagefind bewertet seit Version 1.1 nach einem an BM25 angelehnten Verfahren, und die Stellschrauben dafür sind Zahlenwerte: wie stark Worthäufigkeit zählt, wie sehr lange Seiten abgewertet werden, wie stark ein Wort bevorzugt wird, das dem Suchwort genau entspricht, statt es nur zu verlängern. Mit highlightParam hängt Pagefind den Suchbegriff an die Ergebnis-URL, und das mitgelieferte Skript pagefind-highlight.js markiert die Treffer dann auf der Zielseite.

Mehrere Indizes zusammenführen. Mit mergeIndex() durchsucht der Browser zusätzlich den Index einer anderen Website oder eines anderen Unterordners in derselben Anfrage, mit eigener Gewichtung und einem künstlichen Filterwert pro Index, damit du die Herkunft anzeigen kannst. Liegt der fremde Index auf einer anderen Domain, braucht sie CORS-Header.

await pagefind.mergeIndex("https://docs.example.com/pagefind/", {
  indexWeight: 0.5,
  mergeFilter: { quelle: "Doku" },
});

Performance: was eine Suche wirklich kostet

Hier die Messwerte, auf die ich mich im Artikel beziehe. Gemessen habe ich am 14. Oktober 2026 am frischen Build dieser Seite mit Pagefind 1.5.2, die Suche selbst mit einem Headless-Chromium gegen einen lokalen Server, also ohne Netzwerk-Latenz.

Der Index. 996 Seiten, je 498 pro Sprache, rund 1,2 Millionen Wörter Text, knapp 70.000 Suchbegriffe in den beiden Sprach-Indizes (rund 45.000 deutsche, 24.000 englische), acht Filter. Pagefind braucht dafür 6,6 Sekunden. Der gesamte Build mit Eleventy, Syntax-Highlighting und allen Transformationen dauert gut vier Minuten; die Suche macht davon also weniger als drei Prozent aus. Auf der Platte belegt der Index-Ordner 8,2 MB, verteilt auf 108 Index-Stücke und 996 Fragmente.

Das Öffnen der Suche. Meine Palette lädt Pagefind erst, wenn du sie zum ersten Mal öffnest. Wer nicht sucht, lädt kein einziges Byte davon, und auf die Core Web Vitals der Seite hat die Suche damit keinen Einfluss. Beim Öffnen kommen fünf Dateien: pagefind.js, der Web Worker, eine kleine Einstiegsdatei, die Metadaten der Sprache und das WebAssembly-Modul. Komprimiert, wie mein Apache sie ausliefert, sind das rund 100 KB, davon allein 70 KB WebAssembly.

Jede Suche. Pro Suchbegriff lädt Pagefind ein Index-Stück von rund 30–40 KB und dann die Fragmente der Treffer, die angezeigt werden. Meine Palette zeigt zehn Treffer und lädt deshalb zehn Fragmente. Zusammen ergab das 70–125 KB pro Suchbegriff, wenn der Begriff am Stück ankommt. Tippst du langsamer, als die Palette wartet, laufen Zwischensuchen mit, und ein langes Wort kann dann auch 200–350 KB kosten. Die Fragmente machen den größeren Teil aus, weil jedes den indizierten Text seiner Seite enthält — die Textauszüge entstehen im Browser. Lokal standen die Ergebnisse nach rund 0,3–0,6 Sekunden, inklusive der 150 Millisekunden, die meine Palette nach dem letzten Tastendruck wartet.

Daraus folgen zwei praktische Hebel. Lade data() nur für Treffer, die du wirklich anzeigst: Bei fünf statt zehn sinkt die Datenmenge pro Suche spürbar. Und die Index-Dateien — Index-Stücke, Fragmente, Metadaten und das WebAssembly-Modul — sind schon von Haus aus gzip-komprimiert; damit die Suche funktioniert, brauchst du keine Server-Einstellung. pagefind.js und der Worker liegen allerdings unkomprimiert vor, zusammen rund 86 KB. Erst die übliche gzip- oder Brotli-Komprimierung des Servers drückt sie auf rund 25 KB, und nur so kommen die 100 KB beim ersten Öffnen zustande. Seit Version 1.5 läuft die Suche zudem in einem Web Worker, sodass der Hauptthread frei bleibt, und die Index-Stücke sind durch eine neue Kodierung etwa 45 % kleiner geworden.

Stabilität und Sicherheit: was nicht läuft, kann nicht überlaufen

Ich arbeite seit vielen Jahren mit WordPress, und die Suche ist dort die Stelle, auf die ich bei der Last am genauesten schaue. Die eingebaute WordPress-Suche ist ab Werk ordentlich, aber nicht mehr: Sie zerlegt den Suchbegriff in Wörter und prüft jedes per SQL-LIKE '%wort%' gegen Titel, Auszug und Inhalt aller veröffentlichten Inhalte, also Beiträge, Seiten und öffentliche eigene Inhaltstypen. Ein Volltext-Index kommt dabei nicht zum Einsatz, und weil das Muster mit einem Platzhalter beginnt, hilft auch kein normaler Datenbank-Index — die Datenbank muss die Inhalte durchsuchen. Treffer im Titel stehen seit WordPress 3.7 oben, aber Wortstämme, Tippfehler-Toleranz oder Vorschläge während des Tippens gibt es ohne Plugin nicht.

Das eigentliche Problem ist die Last. Suchanfragen sind für Seiten-Caches ein schlechter Fall: WP Rocket cacht Suchergebnisse laut Doku standardmäßig nicht, LiteSpeed Cache schließt sie im Code aus, W3 Total Cache lässt URLs mit Parametern ab Werk aus, und WP Super Cache legt für sie keine statische Kopie an, sondern höchstens eine Cache-Datei, die PHP ausliefert — und die hilft nur, wenn exakt derselbe Begriff noch einmal gesucht wird. Jeder neue Suchbegriff startet also PHP und fragt die Datenbank. Das gilt für jeden Besucher, aber eben auch für jeden Bot, und Bots fragen gern massenhaft. Das Muster kenne ich aus der Praxis: Viele Anfragen auf /?s= in kurzer Zeit erzeugen eine Menge PHP-Kindprozesse und eine hohe Last auf dem Datenbankserver, die auch alle anderen Besucher ausbremst — gutartige Crawler genauso wie böswillige Bots. Google beschreibt das Problem inzwischen selbst: In Folge 113 des Podcasts „Search Off the Record“ (Juli 2026) erklärt John Mueller, dass interne Suchseiten meist nicht gecacht sind, jede Anfrage eine Datenbankabfrage samt Ranking auslöst und Spammer gezielt Millionen Such-URLs mit Spam-Begriffen verlinken, damit diese Seiten auf fremden Domains ranken.

Mit Pagefind ist das für mich Geschichte. Eine Suche erreicht den Server nur noch als Abruf statischer Dateien, die sich gut cachen lassen und kaum Rechenzeit kosten. Es gibt keinen Endpunkt, der Suchbegriffe entgegennimmt — keine teure Anfrage pro Suchbegriff, die sich vervielfachen lässt, keine Datenbank, keine SQL-Injection, keine Such-URLs, die Spammer verlinken können. Damit ist Pagefind für mich nicht nur ein Komfort-, sondern auch ein Stabilitäts- und Sicherheitsfaktor. Den Index-Ordner selbst liefere ich mit X-Robots-Tag: noindex, nofollow aus, damit Suchmaschinen die Index-Dateien nicht als Seiten aufnehmen. Eine offizielle Empfehlung von Pagefind dazu gibt es nicht; das ist meine Entscheidung.

Eine Sache zur Content Security Policy. Weil Pagefind WebAssembly kompiliert, braucht es bei einer strengen CSP die Freigabe 'wasm-unsafe-eval' in script-src. Die erlaubt nur das Kompilieren von WebAssembly, kein eval(). Seit Version 1.5 kompiliert Pagefind im Worker, und der läuft unter der CSP, die dein Server mit pagefind-worker.js ausliefert. Scheitert der Worker oder antwortet er nicht binnen fünf Sekunden, weicht Pagefind auf die Seite aus, und dann zählt deren Policy. Sicher bist du also nur, wenn keine der beiden Policies das Kompilieren blockiert: Schränkt eine davon script-src ein, braucht sie 'wasm-unsafe-eval'. Für den Worker selbst genügt im Desktop worker-src 'self'; das prüfe ich dort mit einem eigenen Browsertest unter der echten Policy.

Nachhaltigkeit und Kosten

Was du sparst. Pagefind ist kostenlos, und es braucht nichts, was Geld kostet: keinen Suchdienst, keinen eigenen Suchserver, keinen größeren Tarif für mehr PHP-Prozesse. Zum Vergleich die Preise der gehosteten Dienste, Stand Oktober 2026:

Lösung Preismodell
Pagefind kostenlos, statische Dateien auf deinem vorhandenen Webspace
Algolia Free-Plan mit 10.000 Suchanfragen im Monat; im Grow-Plan danach 0,50 US-Dollar je 1.000 Anfragen und 0,40 US-Dollar je 1.000 Datensätze über 100.000
Algolia DocSearch kostenlos, aber nur für öffentliche technische Dokumentation und technische Blogs, nach Prüfung
Meilisearch Cloud ab 20 US-Dollar im Monat
Typesense Cloud Stundenpreis pro Cluster nach Größe, auch ohne Traffic, plus Datenvolumen
Elastic Cloud Hosted ab 99 US-Dollar im Monat

Für eine kleine Website reicht der Free-Plan von Algolia oft aus, das will ich nicht kleinreden. Aber jede dieser Lösungen ist ein weiterer Dienst mit Konto, Schlüssel, Datenschutzprüfung und Preisliste, die sich ändern kann. Bei Pagefind bleiben die Suchbegriffe deiner Besucher in ihrem Browser — es gibt keinen Dritten, der sie sieht, und nichts, was du in der Datenschutzerklärung erklären musst.

Was die Umwelt spart. Ehrlich gesagt lässt sich die Ersparnis nicht sauber in Gramm CO₂ ausdrücken. Das gängige Sustainable Web Design Model (öffnet in neuem Tab) (englisch) in Version 4 rechnet über die übertragene Datenmenge und schätzt ausdrücklich nicht den Rechenaufwand einzelner Server. Die Richtung ist trotzdem klar: Eine Suche, die nur ein paar statische, gecachte Dateien abruft, ersetzt einen PHP-Prozess und eine Datenbankabfrage pro Suchbegriff — und vor allem die Last durch Bots, die niemandem nützt. Die Web Sustainability Guidelines des W3C empfehlen dasselbe Prinzip unter dem Stichwort „Static over dynamic“ und raten an anderer Stelle, unerwünschte Bots zu begrenzen. Wie viel weniger bewegliche Teile insgesamt bringen, habe ich in Nachhaltige Websites beschrieben.

Weitere Einsatzmöglichkeiten

Ein Index, zwei Oberflächen. Das ist mein Lieblingsbeispiel, weil es so unspektakulär funktioniert. Der JPKCom Desktop unter jpkc.com/desktop/ hat eine eigene Suche über Apps, Sammlungen und Lesezeichen. Als zusätzlichen Anbieter bindet er den Pagefind-Index dieser Knowledge Base ein — dieselben Dateien unter /db/pagefind/, die auch die Palette hier nutzt. Es gibt keinen zweiten Index, keinen Export und keinen Abgleich. In der Konfiguration des Desktops sind das ein paar Zeilen:

js
search: {
	pagefind: {
		path: '/db/pagefind/pagefind.js',
		excerptLength: 16,
		maxHits: 8,
		label: 'JPKCom DB',
	},
},

Der Desktop lädt den Index erst, wenn du die Suche öffnest, und zeigt die Treffer als eigene Gruppe „JPKCom DB“ unter den Apps an. Wie der Desktop entstanden ist, steht im Making-of; die Optionen für Betreiber stehen in der Anleitung zum JPKCom Desktop. Die Pagefind-Anbindung ist dort optional und für jeden Desktop einschaltbar.

Mehrere Websites, eine Suche. Mit mergeIndex() führst du die Indizes mehrerer Websites im Browser zusammen, etwa Hauptseite, Blog und Doku auf verschiedenen Subdomains. Jede Website baut ihren Index selbst, die Suche verbindet sie erst beim Besucher.

Inhalte, die kein HTML sind. Über die Node.js-API — und seit Version 1.2 auch über Python — legst du Datensätze direkt an: Text aus PDFs, Untertitel, Produktdaten aus einer JSON-Datei, Einträge einer App. Pagefind übernimmt dabei nur das Indizieren; den Text aus einem PDF holst du vorher selbst heraus.

js
import * as pagefind from "pagefind";

const { index } = await pagefind.createIndex();
await index.addCustomRecord({
  url: "/downloads/handbuch.pdf",
  content: "Der aus dem PDF extrahierte Text …",
  language: "de",
  meta: { title: "Handbuch (PDF)" },
});
await index.writeFiles({ outputPath: "./_site/pagefind" });
await pagefind.close();

Dokumentation und interne Wissensbasen. Überall, wo ein Ordner voller HTML-Dateien auf einem Webserver liegt — Projektdokumentation, Handbücher, ein Intranet auf einem einfachen Webspace —, bekommt er mit einem Befehl eine Suche. Astro Starlight (öffnet in neuem Tab) bringt Pagefind standardmäßig mit, und auch die offizielle Eleventy-Dokumentation (öffnet in neuem Tab) (englisch) sucht mit Pagefind, mit DuckDuckGo als Ausweichweg ohne JavaScript.

Die Grenzen — klar benannt

So zufrieden ich bin: Pagefind ist kein Ersatz für jede Suche. Diese Grenzen solltest du kennen.

Keine Tippfehler-Toleranz. Pagefind vergleicht Wortanfänge, nicht Ähnlichkeit. Ich habe es an diesem Index getestet: „barrierefrei“ und „Barriere“ finden „Barrierefreiheit“, weil sie Wortanfänge sind. Der Tippfehler „barierefreiheit“ findet dagegen nichts Passendes; Pagefind greift dann auf den längsten Wortanfang zurück, der im Index vorkommt, und liefert Treffer zu „bar“ — Fortschrittsbalken in htop und Ähnliches. Wer Tippfehler abfangen muss, ist bei Algolia, Meilisearch oder Typesense besser aufgehoben.

Keine Zerlegung deutscher Komposita. Das trifft deutschsprachige Seiten besonders. „Formular“ findet „PDF-Formular“, weil der Bindestrich die Wörter trennt, aber „freiheit“ findet nicht „Barrierefreiheit“ — der Wortteil steht nicht am Anfang. Für deutsche Inhalte heißt das: Wichtige Begriffe lieber einmal getrennt oder mit Bindestrich im Text haben.

Kein Echtzeit-Index. Pagefind indiziert beim Build. Was sich ändert, ist erst nach dem nächsten Lauf durchsuchbar. Für Blogs und Dokumentation ist das kein Thema, für Shops mit Lagerbestand oder Foren schon. Inhalte, die erst im Browser per JavaScript entstehen, stehen nicht im gebauten HTML und landen deshalb auch nicht im Index.

Alles im Index ist öffentlich. Die Fragment-Dateien enthalten den indizierten Text jeder Seite, und jeder kann sie herunterladen. Es gibt keine Rechteprüfung bei der Suche. Interne oder geschützte Inhalte gehören nicht in einen öffentlich ausgelieferten Index — schließ sie beim Indizieren aus.

JavaScript und WebAssembly sind Pflicht. Ohne beides gibt es keine Suche. Die Eleventy-Doku zeigt einen guten Ausweg: ein normales Formular, das ohne JavaScript an eine externe Suchmaschine schickt.

Keine Auswertung. Weil die Suche im Browser läuft, erfährst du nicht, wonach gesucht wurde. Die Component UI liefert Ereignisse, die du an deine eigene Statistik anbinden kannst; eingebaut ist nichts. Aus Datenschutzsicht sehe ich das eher als Vorteil.

Ranking nur über Stellschrauben. Du kannst gewichten und Parameter verschieben, aber keine eigene Bewertungsfunktion schreiben, keine Synonyme pflegen und keine Treffer gezielt nach oben schieben, wie es gehostete Dienste anbieten.

Sehr große Websites. Eine feste Obergrenze nennt Pagefind nicht; das erklärte Ziel sind Websites mit Zehntausenden Seiten. Im Hugo-Forum berichtet ein Nutzer, dass das Indizieren bei ihm ab etwa 300.000 Seiten abbrach — ein Einzelbericht ohne Versionsangabe, aber ein Hinweis, dass du bei Millionen Seiten testen solltest, bevor du dich festlegst.

Alternativen: wann etwas anderes besser passt

Lösung Wann sie besser passt
Algolia / DocSearch Wenn Tippfehler-Toleranz, Synonyme, Auswertungen und — bei Algolia selbst — Echtzeit-Updates zählen und ein externer Dienst kein Problem ist. DocSearch ist für öffentliche technische Dokumentation und technische Blogs kostenlos, indiziert aber per Crawler in festen Abständen
Meilisearch, Typesense Wenn du die Funktionen eines Suchservers willst — Tippfehler, Facetten, Echtzeit —, aber selbst hosten oder Open Source bleiben möchtest
Elasticsearch / OpenSearch Bei großen Datenmengen, komplexen Abfragen und Logsuche; für eine Website meist zu schwer
Fuse.js, MiniSearch, FlexSearch Bei kleinen Datenmengen im Browser, etwa einer Liste von ein paar Hundert Einträgen. Sie halten den Index in der Regel komplett im Speicher, können dafür aber unscharf suchen und Tippfehler tolerieren
Lunr.js Eher nicht mehr: Die letzte Version ist von 2020
Stork Nicht mehr: Sein Autor hat die Entwicklung 2023 eingestellt und Pagefind — neben Tinysearch — selbst als nächste Alternative genannt

Der entscheidende Unterschied zu den Browser-Bibliotheken ist der zerlegte Index. Fuse.js, MiniSearch oder Lunr laden in der Regel alle Daten, bevor die erste Suche läuft. Bei ein paar Hundert Einträgen ist das egal, bei tausend Artikeln mit Volltext nicht mehr. Pagefind lädt nur, was die Anfrage braucht, und skaliert damit weit über das hinaus, was eine reine Browser-Bibliothek sinnvoll schafft.

FAQ

Funktioniert Pagefind mit jedem Static-Site-Generator?

Ja. Pagefind liest das fertige HTML nach dem Build und kennt deinen Generator gar nicht. Eleventy, Hugo, Astro, Jekyll, Zola oder ein handgeschriebener HTML-Ordner — entscheidend ist nur, dass am Ende HTML-Dateien herauskommen. Ein lang-Attribut am <html> solltest du trotzdem setzen, denn daran erkennt Pagefind die Sprache, legt einen Index pro Sprache an und wählt die passende Wortstamm-Erkennung.

Wie bekomme ich Pagefind-Treffer zu genau einem Abschnitt einer Seite?

Über Unterergebnisse. Gib deinen Überschriften eine id — viele Markdown-Renderer tun das automatisch —, dann liefert jeder Treffer zusätzlich sub_results mit dem passenden Abschnitt und Sprungziel. In der Component UI zeigt <pagefind-results> sie von selbst an; bei <pagefind-searchbox> schaltest du sie mit dem Attribut show-sub-results ein.

Brauche ich für Pagefind eine Server-Konfiguration?

Nein. Die Index-Dateien sind schon komprimiert und funktionieren auf jedem Webspace; für pagefind.js und den Worker lohnt sich die übliche Komprimierung des Servers. Nur bei einer strengen Content Security Policy musst du 'wasm-unsafe-eval' freigeben, und wer Indizes über Domains hinweg zusammenführt, braucht CORS-Header.

Ist Pagefind barrierefrei?

Die Suchmaschine liefert Daten, die Barrierefreiheit entsteht in der Oberfläche. Die neue Component UI folgt laut Doku den WAI-ARIA-Mustern und sagt Trefferzahlen an; die alte Default UI hatte dokumentierte Lücken. Baust du selbst, achte auf ein echtes Label, Tastaturbedienung, Fokusführung und eine Live-Region für die Trefferzahl.

Kann Pagefind auch PDFs durchsuchen?

Nicht direkt. Das Kommandozeilen-Programm liest nur HTML. Über die Node.js- oder Python-API kannst du aber eigene Datensätze anlegen und dort den Text aus PDFs einspeisen, den du vorher selbst extrahiert hast.

Fazit

Pagefind ist für mich eines dieser seltenen Werkzeuge, die genau das tun, was sie versprechen, und dabei weniger statt mehr Arbeit machen. Ein Befehl im Build, ein Ordner voller statischer Dateien, und eine Website mit tausend Seiten in zwei Sprachen hat eine schnelle, gefilterte Volltextsuche — ohne Server, ohne Datenbank, ohne Abo und ohne eine teure Anfrage pro Suchbegriff, mit der Bots den Server in die Knie zwingen könnten. Dass derselbe Index nebenbei noch die Suche eines ganz anderen Projekts speist, zeigt, wie gut sich das Prinzip wiederverwenden lässt.

Die Grenzen sind echt: keine Tippfehler-Toleranz, keine Komposita, kein Echtzeit-Index, alles öffentlich. Wenn deine Website eine davon nicht verträgt, nimm einen Suchserver. Für Blogs, Dokumentation und Wissensbasen ist Pagefind aus meiner Sicht die erste Wahl — und die Indizierung ist der Teil, in den du die meiste Sorgfalt stecken solltest.

Weiterlesen

Warum diese Seite überhaupt statisch gebaut ist, erkläre ich in Warum 11ty. Was weniger bewegliche Teile für Strom und Kosten bedeuten, steht in Nachhaltige Websites. Wie der Desktop entstanden ist, dessen Suche den Index hier mitbenutzt, zeigt das Making-of JPKCom Desktop. Und wo die Suche im Seitengerüst hingehört, beschreibt HTML-Landmarks in der Praxis.

Die Primärquellen: die Pagefind-Dokumentation (öffnet in neuem Tab) (englisch), das Changelog (öffnet in neuem Tab) (englisch) und das Repository auf GitHub (öffnet in neuem Tab) (englisch).