PHP-native build-time full-text search indexer with a dependency-free JavaScript query runtime. No Node, no server-side query API, no Elasticsearch — index at build time, query in the browser.
Trawl fills a specific gap: PHP shops that want client-side full-text search without adding Node to their toolchain.
| Trawl | Pagefind | Hosted (Algolia, etc.) | |
|---|---|---|---|
| Indexer runtime | PHP (Composer) | Node/npx | SaaS |
| Search runtime | Browser-only JS | Browser-only JS | Server API |
| Node dependency | None | Required for indexing | None |
| Composer install | composer require --dev |
N/A | N/A |
| Chunked index | Yes (CRC32 shards) | Yes | N/A |
| On-premise | Fully self-contained | Fully self-contained | Third-party |
If you already have Node in your build pipeline, Pagefind is excellent and more mature. If you don't, or you want your entire dev dependency tree in Composer, Trawl is your option.
composer require --dev commonform/trawl
trawl init
trawl build --source=dist/ --output=dist/trawl/Then in your HTML:
<script type="module">
import { Trawl } from '/trawl.js';
const trawl = await Trawl.init({ baseUrl: '/trawl/' });
const results = await trawl.search('reverse proxy');
// [{ url, title, score, excerpt: '...<mark>reverse proxy</mark>...' }]
</script>Copy runtime/trawl.js from the package into your public directory, or use it directly from vendor/commonform/trawl/runtime/trawl.js.
trawl build Build the search index
trawl verify Validate manifest + shard + doc consistency
trawl watch Watch for file changes and rebuild incrementally
trawl init Scaffold a trawl.json configuration file
trawl build --source=dist/ --output=dist/trawl/
trawl build --adapter=markdown --source=content/ --output=dist/trawl/
trawl build --incremental # skip unchanged docs
trawl build --config=trawl.json # use config filetrawl verify --index=dist/trawl/{
"source": "dist/",
"output": "dist/trawl/",
"adapter": "filesystem",
"include": ["**/*.html"],
"exclude": ["**/404.html", "drafts/**"],
"selectors": {
"title": "title",
"heading": "h1, h2, h3",
"body": "main",
"ignore": "[data-trawl-ignore], nav, footer"
},
"field_weights": { "title": 10, "heading": 5, "body": 1 },
"facets": ["tags", "category"],
"language": "en",
"stemmer": "porter2",
"stopwords": "default",
"incremental": true,
"cache_file": ".trawl-cache.json"
}| Adapter | Input | Description |
|---|---|---|
filesystem |
.html files |
Walks a directory of rendered HTML, extracts content via CSS selectors |
markdown |
.md / .mdx files |
Parses YAML frontmatter + Markdown body via league/commonmark |
array |
iterable<Document> |
Programmatic input from CMS integrations |
import { Trawl } from './trawl.js';
const trawl = await Trawl.init({ baseUrl: '/trawl/' });
const results = await trawl.search('query', {
limit: 10,
filters: { tags: 'docs' },
mode: 'and' // 'and' (default) or 'or'
});
// results: [{ url, title, score, excerpt, meta }]- Indexer: PHP 8.3+, Composer
- Runtime: Any browser with native
fetch, ES modules, andURL
MPL-2.0