Engine KDL Specification
This document specifies the KDL format for defining generic engines in data/engines.kdl. Generic engines are parsed by go-server/generic_engine.go and registered alongside hand-written engines (Brave, Google, etc.) at startup. A single engines.kdl file defines all ~274 generic engines.
Engine block structure
Each engine is a top-level engine node with a quoted string ID and a child block of properties:
engine "my_engine" {
name "My Engine Display Name"
categories "web" "news"
search-url "https://example.com/search?q={query}&page={page}"
method "GET"
result-type "json"
results-path "data.items"
title-path "title"
url-path "link"
content-path "description"
page-size 20
paging true
first-page 1
}
The engine ID (the string after engine) is the unique identifier used in logs, cache keys, weight cookies, and the admin UI. It must be a short slug (lowercase, underscores allowed).
Attributes reference
Identity
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | No | Same as ID | Human-readable display name shown in engine badges and the settings page. |
categories | string list | Yes | (none – engine is skipped if empty) | Space-separated quoted category slugs. Valid values: web, images, videos, news, music, science, it, files, maps, social, books, qa / q&a, packages, weather, lyrics. An engine with no parseable categories is silently skipped during registration. |
Request
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
search-url | string | Yes | – | URL template. Placeholders: {query} (URL-encoded query), {page} / {pageno} (page number = (page-1) + first-page), {offset} (offset = (page-1) * page-size + first-page), {lang} (hardcoded "en" currently). |
method | string | No | "GET" | HTTP method. "GET" or "POST". |
body-template | string | No | "" | POST body template (same placeholders as search-url). Only used when method is "POST". |
headers | sub-block | No | (none) | Custom HTTP headers as key-value pairs. Default headers (User-Agent, Accept, Accept-Language, DNT) are always sent; entries here override or add to them. |
Response parsing – HTML mode
Used when result-type is "html" (or "HTML" or "xpath").
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
result-type | string | Yes | – | Set to "html" for CSS-selector-based parsing. |
results-selector | string | Yes (for HTML) | "" | CSS selector for result container elements. Each match becomes one result. |
title-selector | string | No | "" | CSS selector (relative to result container) for the title text. |
url-selector | string | No | "" | CSS selector for the URL. Extracts href attribute first; falls back to text content. |
content-selector | string | No | "" | CSS selector for the snippet/description text. |
thumbnail-selector | string | No | "" | CSS selector for a thumbnail image URL (used for image/video results). |
Response parsing – JSON mode
Used when result-type is "json".
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
result-type | string | Yes | – | Set to "json" for dot-path-based JSON parsing. |
results-path | string | No | "" (top-level array) | Dot-separated path to the results array in the JSON response. Example: "message.items" navigates {"message":{"items":[...]}}. |
title-path | string | No | "" | Dot-path to the title field within each result object. |
url-path | string | No | "" | Dot-path to the URL field. |
content-path | string | No | "" | Dot-path to the snippet/description field. |
thumbnail-path | string | No | "" | Dot-path to a thumbnail URL field. |
URL construction
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
url-prefix | string | No | "" | String prepended to every extracted URL. Use for APIs that return relative paths (e.g. "https://developer.mozilla.org" + /en-US/docs/...). |
url-template | string | No | "" | Template for building URLs from result fields. Placeholders: {title} (spaces replaced with _), {id}, {url}. Takes precedence over url-prefix when set. Example: "https://www.artic.edu/artworks/{id}". |
Pagination
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
page-size | int | No | 10 | Results per page. Used to compute {offset}. |
paging | bool | No | false | Whether this engine supports pagination. |
first-page | int | No | 0 | The page/offset number for the first page. Most APIs use 0 or 1. |
Content processing
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
content-html-to-text | bool | No | false | Strip HTML tags from the content/snippet field before display. Useful for APIs like StackExchange that return HTML in JSON. |
Lifecycle
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
disabled | bool | No | false | If true, the engine is parsed but not registered. Use to temporarily disable an engine without removing its config. |
Federation
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
share | string | No | auto-detected | Controls whether results from this engine are shared with peer instances. Values: "none" (never share – used for engines with API keys to avoid leaking quota), "cache" (share cached results but don’t proxy live queries – the default), "proxy" (full proxy: friends can trigger live queries through us). When omitted, the engine factory auto-detects: if the URL or headers contain an API key pattern (api_key=, Authorization, etc.), defaults to "none"; otherwise defaults to "cache". |
tor-outbound | bool | No | false | Route this engine’s HTTP requests through the local Tor SOCKS5 proxy (127.0.0.1:9050). Use for engines that aggressively IP-ban search aggregators. Requires a running Tor daemon. |
Examples
JSON API engine (MDN)
engine "mdn" {
name "MDN Web Docs"
categories "web"
search-url "https://developer.mozilla.org/api/v1/search?q={query}&page={page}"
method "GET"
result-type "json"
results-path "documents"
title-path "title"
url-path "mdn_url"
url-prefix "https://developer.mozilla.org"
content-path "summary"
page-size 10
paging true
first-page 1
}
HTML scraping engine (Anaconda)
engine "anaconda" {
name "Anaconda"
categories "web"
search-url "https://anaconda.org/search?q={query}&page={page}"
method "GET"
result-type "html"
url-prefix "https://anaconda.org"
results-selector "tbody tr"
title-selector "td h5"
url-selector "td h5 a:last-of-type"
content-selector "td p"
page-size 10
paging true
first-page 0
}
Engine with custom headers (Crossref)
engine "crossref" {
name "Crossref"
categories "web"
search-url "https://api.crossref.org/works?query={query}&rows=20&offset={offset}"
method "GET"
result-type "json"
results-path "message.items"
title-path "title"
url-path "URL"
content-path "abstract"
content-html-to-text true
headers {
"User-Agent" "4got/1.0 (https://4got.org; mailto:admin@4got.org)"
}
page-size 20
paging true
first-page 0
}
POST engine with body template (ArtStation)
engine "artstation" {
name "www.artstation.com"
categories "images"
search-url "https://www.artstation.com/api/v2/search/projects.json"
method "POST"
result-type "json"
results-path "data"
title-path "title"
url-path "permalink"
content-path "description"
thumbnail-path "smaller_square_cover_url"
body-template "{\"query\":\"{query}\",\"page\":{page},\"per_page\":20,\"sorting\":\"relevance\",\"pro_first\":\"1\",\"filters\":[],\"additional_fields\":[\"description\"]}"
page-size 20
paging true
first-page 1
}
URL template engine (Art Institute of Chicago)
engine "artic" {
name "www.artic.edu"
categories "images"
search-url "https://api.artic.edu/api/v1/artworks/search?q={query}&page={page}&limit=20"
method "GET"
result-type "json"
results-path "data"
title-path "title"
url-path "id"
url-template "https://www.artic.edu/artworks/{id}"
content-path "thumbnail.alt_text"
page-size 20
paging true
first-page 1
}
How the generic engine factory works
At startup, LoadAndRegisterGenericEngines() in generic_engine.go:
- Reads
data/engines.kdlviaresolveKDLSection()(which supports both standalone files andengines { ... }sections inside a unified KDL file). - Parses the raw bytes into
[]GenericEngineConfigviaparseEngineKDL(), a line-by-line KDL parser that handles engine blocks, headers sub-blocks, and all property types. - Skips entries with empty ID, no categories, or
disabled true. - Sets defaults:
page-sizedefaults to 10,namedefaults to the ID. - Creates a
GenericEnginestruct (which implements theEngineinterface) for each config and registers it in the global engine registry. - Determines the share policy: explicit
shareattribute wins; otherwise auto-detects based on whether the URL or headers contain API key patterns.
When Search() is called on a GenericEngine:
- Substitutes
{query},{page},{offset},{lang}in the URL template (and body template if POST). - Makes the HTTP request, routing through Tor if
tor-outboundis set. - Records the raw HTML for drift detection (
drift.go). - Dispatches to
parseHTML()(goquery CSS selectors) orparseJSON()(dot-path navigation) based onresult-type. - Returns
[]WebResultwith each result’sEnginefield set to the engine ID.
Testing a new engine via the admin playground
The admin playground at /admin/engines/new (owner-only) provides a web form for configuring and testing generic engines without editing KDL files directly.
- Log in as owner at
/admin/login. - Navigate to
/admin/engines/new. - Fill in the engine identity (ID, display name, categories).
- Set the search URL template with
{query}and{page}placeholders. - Choose HTML or JSON result type and fill in the corresponding selectors/paths.
- Enter a test query and click “Test Engine” – this POSTs to
/admin/engines/test, which creates a temporaryGenericEnginein memory, runs a search, and returns results as JSON displayed in the form. - Iterate on selectors until results look correct.
- Click “Save Engine” to append the generated KDL block to
data/engines.kdland register it in memory immediately (no restart needed).
The test endpoint does not persist anything – only “Save Engine” writes to disk.