Websites
Search widget
Add Hayfork search to any website with one script tag. Visitors get a search box that opens a search dialog, with results that link straight to the right page. It works on any site, including Docusaurus (which also has a theme package), Starlight, VitePress, MkDocs and hand-written HTML, and needs no build step.
1. Create a website key
The widget runs in your visitors' browsers, so anyone can read the key in your page source. A website key is made for that. It can only search, only the collections you choose, and only from the websites you list. A copied key does not work on anyone else's site.
Create one from the API with an admin key. List every address the widget will run on, and the collection it should search:
curl https://api.hayfork.dev/api-keys \
-H "Authorization: Bearer $ADMIN_KEY" -H "Content-Type: application/json" \
-d '{
"name": "Docs search widget",
"scopes": ["search"],
"allowed_origins": ["https://docs.example.com", "http://localhost:3000"],
"collection_ids": ["COLLECTION_ID"],
"expires_in_days": 365
}'
The response includes the key in a key field, once. Find the collection id on the console's Collections page.
- Websites are exact. Give the scheme and host only, such as
https://docs.example.com.https://example.comandhttps://www.example.comare different websites, so list both if you use both. Paths and wildcards are refused. Up to 10 per key. - Add your local address while you build, such as
http://localhost:3000, then create a new key without it for production. - Search scope only. A key with
allowed_originsmust have exactly thesearchscope and at least one collection.
2. Add the script
Put this where the search box should appear, usually in your site header:
<script src="https://hayfork.dev/widget.js"
data-key="YOUR_WEBSITE_KEY"
data-collection="COLLECTION_ID"
async></script>
The search box appears where the script tag is. To place it somewhere else, point data-target at an element, such as data-target="#search", and load the script anywhere on the page.
Visitors open the dialog by clicking the box, or with ⌘K on a Mac, Ctrl K elsewhere, or /. Arrow keys move through results, Enter opens one and Esc closes the dialog.
Options
| Attribute | Default | Notes |
|---|---|---|
data-key | required | A website key. |
data-collection | required | The collection id to search. It must be one of the key's collections. |
data-placeholder | Search | Text in the search box and the dialog. |
data-target | none | A CSS selector for the element to put the search box in. Without it, the box goes where the script tag is. |
data-theme | auto | light, dark, or auto to follow the visitor's system setting. |
data-shortcut | true | false turns off the ⌘K, Ctrl K and / shortcuts. |
data-results | 8 | Results to show, 1 to 20. |
The widget draws inside a shadow root, so your site's CSS does not change it and its CSS does not leak into your site. To open it from your own button, call open() on the element it creates:
document.querySelector("[data-hayfork-search]").hayfork.open();
Usage and limits
- The widget waits until a visitor pauses typing and needs at least two characters, then sends one search. Each search counts against your plan's monthly searches, the same as an API call. A query repeated on the same page is answered from memory and is not sent again.
- One visitor can make up to 120 searches a minute with a website key. Past that they see "Search is busy", which protects your monthly allowance from a runaway script.
- Results show the page title, the section heading and a snippet with the matched words in bold. Each result links to its source page.
Security
- A website key cannot change anything. It can call
POST /searchand nothing else, on the collections it lists. It does not work with the MCP server or any other endpoint. - The website check uses the browser's
Originheader. Browsers set it and pages cannot change it, so other websites cannot use your key. A script outside a browser can fake the header, which is why searches are also rate-limited per visitor and capped by your plan. Give each site its own key, set an expiry, and revoke a key withDELETE /api-keys/KEY_IDif you see traffic you do not recognise. - Indexed text is shown as text. The widget never renders content from your pages as HTML, and only follows
httpandhttpslinks. - No cookies. Requests are sent without cookies or credentials, and the widget stores nothing in the visitor's browser.
Troubleshooting
| What you see | What to do |
|---|---|
No search box, and The search widget needs data-key and data-collection in the browser console | Add both attributes to the script tag. |
| "This search box is not set up for this website." | The key was refused. Check that the page's exact address, with https:// and any www., is in the key's allowed_origins, that the collection is one of the key's collection_ids, and that the key has not expired or been revoked. |
| "Search is busy. Try again in a minute." | Either one visitor passed the per-visitor limit, which resets after a minute, or the plan's searches for the month are used up. The browser console says which. Check usage in the console. |
| "Search is unavailable right now." | The API could not be reached. Check your network, or try again shortly. |