What plugins are
Plugins live in data/plugins/ (or
DEGOOG_PLUGINS_DIR). Each plugin is a
folder
with an entry file. One plugin can expose several capabilities: a bang
command, a slot, search bar actions, HTTP routes, and/or request
middleware.
Folder structure
data/plugins/
my-plugin/
index.js # required — entry point
template.html # optional
style.css # optional
script.js # optional
card.html # optional — read via ctx.readFile()
author.json # optional — for Store display
The entry file must be index.js, index.ts,
index.mjs, or index.cjs.
Asset files
-
template.html — HTML fragment for your plugin
output. Use
{{placeholders}}and replace them inexecute(). Injected into search results, not a full page. - style.css — Loaded when the plugin is active. Use class names scoped to your plugin to avoid conflicts. See Styling for CSS variables.
- script.js — Client-side JavaScript, loaded automatically.
Add other files and read them at runtime via
ctx.readFile("filename") in init(ctx).
Plugin context and init()
The system calls your optional init(ctx) once at startup,
before configure(). The context provides:
init(ctx) {
// ctx.template — contents of template.html (or "")
// ctx.dir — absolute path to the plugin folder
// ctx.readFile — async function to read any file from your plugin folder
}
Example: a plugin with both a command template and a separate card
template (e.g.
RSS
uses card.html via
ctx.readFile("card.html")).
Bang commands
Bang commands run when the user types
!trigger something in the search bar. Export a single
object (as export default or
export const command) with:
-
name, description — For Settings
and
!help. -
trigger — The word after
!(e.g."weather"→!weather). -
execute(args, context) (async) —
argsis the text after the trigger. Return an object withtitle(string) andhtml(string); optionaltotalPages(number).
Second argument to execute: context may have
clientIp, page.
Optional: aliases (array of extra triggers),
naturalLanguagePhrases (array of phrases that trigger
without ! when natural language is on in Settings),
settingsSchema and configure(settings),
init(ctx), isConfigured() (async; return
false to hide from !help until configured).
For settingsSchema, each field has key,
label, type (text,
password, url, toggle,
textarea, select, urllist), and
optional required, placeholder,
description, secret (never sent to browser).
For select, add options (array of strings).
How settings work
-
Declare
settingsSchema— a Configure button appears in Settings → Plugins. -
User saves; values go to
data/plugin-settings.jsonunderplugin-<folderName>. -
configure(settings)is called after save and on server restart if settings exist. -
Use
isConfigured()to return false when required settings are missing — the command is then hidden from!help. -
Users can disable any plugin with the toggle in Settings → Plugins;
disabled plugins are hidden from
!helpand return an error when invoked.
Examples from the official store:
- Weather — bang command with template, styles, settings (default city, Fahrenheit), aliases, naturalLanguagePhrases.
- Define, QR, Time, Password — command-only plugins.
- Jellyfin, Meilisearch — search your own backend via a bang command.
Slot plugins
Slots inject panels into the search results page when the query
matches. Export slot or slotPlugin (same
module can also export a bang command):
- id, name — Unique id and display name.
-
position —
"above-results","below-results","above-sidebar","below-sidebar","knowledge-panel", or"at-a-glance". (Use above-sidebar or below-sidebar instead of the removed"sidebar".) - trigger(query) — Return true (or a Promise that resolves true) if the slot should show for this query.
-
execute(query, context) (async) — Return
{ title?: string, html: string }. Return{ html: "" }to show nothing.contexthasclientIpand, forat-a-glanceslots only,results(the search results array).
Optional: settingsId — Key under which settings are
stored (default slot-<id>). Optional:
settingsSchema, configure(settings),
init(ctx). Multiple slots can match the same query; all
are shown.
Optional: slotPositions — Array of position values
(e.g. ["knowledge-panel", "above-sidebar", "below-sidebar"]).
If present and non-empty, the app adds a Position
dropdown in the plugin settings; the user can choose which of these
positions the slot uses. If empty or omitted, the slot uses the
fixed position only.
Sidebar positions
"above-sidebar" — Renders at the top of the sidebar,
before the main sidebar content. "below-sidebar" —
Renders at the bottom of the sidebar, after engine timings and
related searches.
Knowledge panel slot
position: "knowledge-panel" replaces only the first block in
the sidebar (the Wikipedia/knowledge panel). Engine Performance and
People also search for stay as-is. When a plugin returns content for
this position, it is shown instead of the knowledge panel.
At a glance slot
Slots with position: "at-a-glance" fill the
At a glance block above the search results. The
search response is not delayed: the client shows results immediately
and fetches at-a-glance content in a separate request (POST /api/slots/glance
with body { query, results }). Your
execute(query, context) receives
context.results so you can use the result list (e.g. for
an AI summary).
Examples from the official store:
- TMDb — slot above results, movie/TV details, secret API key in settings.
- Math, GitHub — slots above results.
-
RSS
— slot with feed URLs in settings,
card.htmlviactx.readFile().
Slot API: GET /api/slots?q=<query>
returns panels for above-results, below-results, above-sidebar,
below-sidebar, knowledge-panel. POST /api/slots/glance with
body { query, results } returns only at-a-glance panels.
Slots also run for custom tabs (e.g. weeb): the client fetches panels
after a tab-search and renders them in the same positions.
Search result tabs
Search result tabs add entries to the tab bar (e.g. “Web”, “Images”).
Export tab or searchResultTab from a plugin
folder. Same folder structure as other plugins.
Required: id, name.
Provide either engineType (string:
web, images, videos,
news, or a custom type from
engines) or
executeSearch(query, page?, context?) (async)
returning
{ results: Array<{ title, url, snippet, source, thumbnail?,
duration? }>, totalPages?: number }.
Optional: icon, settingsId,
settingsSchema, configure(settings),
init(ctx). When distributing via the Store, use
type: "search-result-tab" in
package.json and dependencies (array of
URLs) if the tab needs an engine (e.g. File tab depends on Internet
Archive engine). See Store.
API: GET /api/search-tabs returns the
tab list;
GET
/api/tab-search?tab=<id>&q=<query>&page=<n>
runs the tab’s search.
Search bar actions
Plugins can add buttons next to the search bar. Export
searchBarActions — an array of objects with
id, label, type:
"navigate" (open URL; provide url),
"bang" (fill bar with !trigger ; provide
trigger), or "custom" (your
script.js listens for
search-bar-action event with
detail: { actionId, inputId, input }). Optional:
icon (image URL).
Plugin routes
Plugins can expose HTTP endpoints under
/api/plugin/<folderName>/.... Export
routes — array of
{ method, path, handler(req) }. method:
get, post, put,
delete, patch. path is the
segment after the plugin id. handler receives the
standard Request and returns Response or
Promise<Response>. Routes are available as soon as
the plugin is loaded; script.js can call them with
fetch("/api/plugin/my-plugin/...").
Request middleware
Middleware lets a plugin hook into specific request flows. Export
middleware — object with id,
name, handle(req, context).
context can include
{ route: "settings-auth" } etc. Return a
Response, { redirect: url }, or
null to continue. The app uses middleware for the
settings gate. To select a plugin for the gate, use
“Use as settings gate” in that plugin’s Configure in Settings →
Plugins; that sets middleware.settingsGate in
plugin-settings.json to
plugin:<folderName>.
Settings gate (login flow)
Settings can be protected with a password
(DEGOOG_SETTINGS_PASSWORDS) or a
middleware plugin: when the user opens Settings, they
go through your plugin’s flow (e.g. OIDC, magic link), then back to
Settings with a session token.
What your middleware must do
-
route "settings-auth" — Return JSON with
required: true,valid: false,loginUrl. -
route "settings-auth-callback" — After auth, return
{ redirect: "/settings" }. The app issues a session token and redirects with?token=.... - route "settings-auth-post" — If you don’t support password POST, return 400 or similar.
Expose a GET /login route that redirects to
/api/settings/auth/callback?returnTo=/settings. Clear
degoog-settings-token in Session Storage to test.
Example: minimal bang command
Folder: data/plugins/greeting/ with
index.js, template.html,
style.css.
let template = "";
export default {
name: "Greeting",
description: "Say hello",
trigger: "hello",
init(ctx) { template = ctx.template; },
async execute(args) {
const name = args.trim() || "world";
const html = template.replace("{{name}}", name);
return { title: "Hello", html };
},
};
template.html
<div class="command-result greeting">
<h3 class="greeting-title">Hello, {{name}}!</h3>
</div>
style.css — use
var(--text-primary) etc.; see
Styling.