Degoog Plugins
Create custom bang commands, slots, search result tabs, search bar actions, routes, and middleware.
What plugins are
Your plugins belong in the data/plugins/ directory, or
whatever you set DEGOOG_PLUGINS_DIR to. A plugin is
simply a folder containing an entry file. You can
use a single plugin to handle multiple tasks like a bang command, a
slot, search bar actions, HTTP routes, or request middleware.
Folder structure
data/plugins/
my-plugin/
index.js
template.html
style.css
script.js
card.html
author.json
Your entry file needs to be index.js,
index.ts, index.mjs, or
index.cjs.
Asset files
-
template.html: This is the HTML fragment for your
plugin output. You can use
{{placeholders}}and replace them insideexecute(). This gets injected directly into the search results rather than rendering a full page. - style.css: This loads automatically when your plugin is active. Make sure to use class names scoped to your specific plugin to avoid styling conflicts. You can check the Styling page for available CSS variables.
- script.js: Your client side JavaScript that loads automatically.
You can add other files and read them while the app runs using
ctx.readFile("filename") inside your
init(ctx) function.
Plugin context and init()
The system calls your optional init(ctx) function once
during startup before running configure(). The context
provides the following:
init(ctx) {
// ctx.template contains your template.html
// ctx.dir is the absolute path to your plugin folder
// ctx.readFile is an async function to read any file from your plugin folder
}
For example, you might build a plugin that uses both a command
template and a separate card template. The
RSS
plugin does exactly this by reading card.html through
ctx.readFile("card.html").
Bang commands
Bang commands run whenever you type
!trigger something into the search bar. You need to
export a single object using export default or
export const command containing these properties:
-
name and description: These
appear in Settings and the
!helpmenu. -
trigger: The word you type right after the
!symbol. For example, typingweatherbecomes!weather. -
execute(args, context) (async): The
argsparameter contains whatever text follows your trigger. You must return an object containing atitlestring and anhtmlstring. You can also include an optionaltotalPagesnumber.
The second argument passed to execute is
context, which might contain your
clientIp and page.
You can also include optional properties like
aliases for extra triggers, or
naturalLanguagePhrases which are phrases that trigger
the command without needing the ! symbol when natural
language is enabled in Settings. You can also use
settingsSchema, configure(settings),
init(ctx), and isConfigured(). If
isConfigured() is async and returns false, your command
stays hidden from the !help menu until the user
configures it.
For your settingsSchema, each field requires a
key, label, and type. The
type can be text, password,
url, toggle, textarea,
select, or urllist. You can also add
optional flags like required, placeholder,
description, or secret. Secret fields are
never sent back to the browser. If you use the
select type, you will need to add an
options array containing your string choices.
How settings work
-
Declare your
settingsSchemaso a Configure button appears in Settings then Plugins. -
When you save, the values are stored in
data/plugin-settings.jsonunderplugin-<folderName>. -
The
configure(settings)function runs right after saving and every time the server restarts if settings are present. -
Use
isConfigured()to return false when required settings are missing. This hides the command from the!helpmenu. -
You can disable any plugin using the toggle in Settings then
Plugins. Disabled plugins disappear from
!helpand return an error if you try to invoke them.
Examples from the official store:
- Weather is a bang command complete with a template, styles, settings for things like default city or Fahrenheit, aliases, and natural language phrases.
- Define, QR, Time, and Password are examples of simple command only plugins.
- Jellyfin and Meilisearch let you search your own personal backend through a bang command.
Slot plugins
Slots let you inject custom panels directly into the search results
page when a query matches. Just export slot or
slotPlugin. A single module can actually export both a
slot and a bang command.
- id and name: Your unique identifier and display name.
-
position: Choose between
above-results,below-results,above-sidebar,below-sidebar,knowledge-panel, orat-a-glance. Please use the specific sidebar positions instead of the old deprecated sidebar option. - trigger(query): This needs to return true, or a Promise that resolves to true, whenever the slot should appear for a specific query.
-
execute(query, context) (async): Return an object
with an optional
titlestring and anhtmlstring. If you return an empty string for the html, nothing will show up. Thecontextcontains theclientIpand an array ofresults. Note that the results array is only populated if you setwaitForResults: trueon your plugin.
You can add an optional settingsId to specify the
key where your settings are stored. This defaults to
slot-<id>. You can also include
settingsSchema, configure(settings), and
init(ctx). If multiple slots match the exact same
query, they will all display together.
Another optional flag is waitForResults. Set this
to true if you need to receive context.results inside
your execute() function. While results are always
available when execution happens since slots run after the search
finishes, they are only passed to your plugin if you explicitly
enable this flag. It defaults to false so plugins that do not need
results can stay completely independent.
You can also provide an optional
slotPositions array. This lets you list multiple
position values like
["knowledge-panel", "above-sidebar", "below-sidebar"].
If you include this, the application adds a Position dropdown in
your plugin settings so you can choose exactly where the slot
appears. If you leave it empty or omit it, the slot simply uses your
fixed position value.
Sidebar positions
Using above-sidebar renders right at the top of the
sidebar before any main content. Using
below-sidebar renders at the very bottom of the sidebar
below the engine timings and related searches.
Knowledge panel slot
Setting your position to knowledge-panel replaces only
the very first block in the sidebar, which is usually the Wikipedia
or knowledge panel. Your engine performance and related searches
blocks stay exactly where they are. When your plugin returns content
for this position, it shows up instead of the default knowledge
panel.
At a glance slot
Slots using the at-a-glance position will fill the
block directly above the search results. This does not delay your
search response. The client shows your results immediately and
fetches the panel content in a separate background request using
POST /api/slots/glance
with the query and results. Your
execute(query, context) function will always receive
context.results for these types of slots.
Examples from the official store:
- TMDb is a slot above the results showing movie and TV details. It uses a secret API key stored in settings.
- Math and GitHub are examples of slots appearing above the results.
-
RSS
is a slot that reads feed URLs from settings and uses
card.htmlthroughctx.readFile().
Slot API: Calling POST /api/slots with
the query and results returns the panels for the main positions.
Calling
POST /api/slots/glance
returns only the glance panels. Slots also work perfectly for custom
tabs. The client simply fetches the panels after a tab search and
renders them in the exact same positions.
Search result tabs
Search result tabs let you add new entries to the main tab bar, just
like the Web or Images tabs. Simply export tab or
searchResultTab from your plugin folder. You use the
exact same folder structure as any other plugin.
Required properties: You must provide an
id and a name. You also need to
provide either an engineType string like
web, images, videos,
news, or a custom type from your engines, or an
executeSearch(query, page?, context?) async
function. If you use the function, it must return an object
containing your results array and an optional totalPages number.
Each result needs a title, url, snippet, and source, plus an
optional thumbnail and duration.
Optional properties: You can add an
icon, settingsId,
settingsSchema,
configure(settings), and
init(ctx). If you are sharing this on the Store,
make sure to set your type to search-result-tab in your
package.json. You can also add a dependencies array
containing URLs if your tab relies on a specific engine, just like
how the File tab depends on the Internet Archive engine.
API: A GET request to
/api/search-tabs returns the list of available tabs. A
GET request to /api/tab-search along with
your tab id, query, and page number will run the specific search for
that tab.
Search bar actions
Your plugins can also add buttons right next to the search bar. Just
export searchBarActions as an array of objects
containing an id, a label, and a
type. The type can be navigate to open
a specific URL, bang to fill the search bar with your
trigger, or custom. If you use custom, your
script.js will listen for a
search-bar-action event that contains the action
details. You can also include an optional
icon image URL.
Plugin routes
Plugins can expose their own HTTP endpoints under the
/api/plugin/<folderName>/ path. You do this by
exporting routes as an array containing the
method, path, and
handler(req). Your method can be get,
post, put, delete, or
patch. The path is just the URL segment that comes
after your plugin id. Your handler receives the standard Request
object and needs to return a Response or a Promise that resolves to
a Response. These routes become available the moment your plugin
loads, meaning your script.js can call them immediately
using fetch.
Request middleware
Middleware allows your plugin to hook into specific request flows.
You export middleware as an object containing an
id, a name, and a
handle(req, context) function. The context might
include specific route information like settings-auth.
Your function should return a Response, a redirect URL object, or
simply null to let the request continue normally. The application
uses this middleware for the settings gate. To
choose a plugin for the gate, just click Use as settings gate in
that plugin configuration menu under Settings. This updates your
plugin-settings.json file automatically.
Settings gate (login flow)
You can protect your settings using a simple password with
DEGOOG_SETTINGS_PASSWORDS or by using a
middleware plugin. When you use a plugin, anyone
opening the Settings page goes through your custom flow like a magic
link or external login before returning to the settings with a valid
session token.
What your middleware must do
- route "settings-auth": Return a JSON object with required set to true, valid set to false, and your loginUrl.
-
route "settings-auth-callback": After a
successful authentication, return a redirect to
/settings. The application will issue a session token and redirect the user automatically. - route "settings-auth-post": If you do not want to support password submissions via POST, just return a 400 error or something similar.
Make sure to expose a GET /login route that redirects
back to the callback URL. You can clear the
degoog-settings-token in your browser Session Storage
anytime you need to test the flow.
Example: minimal bang command
Here is what a simple folder at
data/plugins/greeting/ looks like with an
index.js, template.html, and
style.css file.
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>
For your style.css, you should use variables like
var(--text-primary) to ensure everything matches. You
can find more details on the
Styling page.
Debugging plugins
If you need to troubleshoot, set LOG_LEVEL=debug to
print the execution times for all your plugin types directly to the
server console. You can check the Environment variables page for
more details.