Extending
Writing a plugin: the repository contract
What a repository must hold for collage plugin add to install it — the name, the manifest, a New that needs no setup, a release tag — and how collage plugin check proves it.
Writing a plugin is about the Go side: the Plugin
interface and its hooks. This page is about the repository around it. A plugin
that meets the rules below can be installed with one command, by anyone, and
removed with another:
collage plugin add you/stamp # github.com/you/collage-stamp
collage plugin del you/stamp
collage plugin add refuses a module that breaks a rule, says which one, and
points here. collage plugin check runs the same
rules against your repository before anyone else does.
The rules
The repository is named collage-name
A short reference, you/stamp, means github.com/you/collage-stamp. Name the
repository collage-<name> and it can be added that way. A plugin hosted
elsewhere, or named otherwise, is added by its module path
(collage plugin add gitlab.com/you/stamper); the other rules still hold.
The root has a collage.json
The manifest sits at the module root, and its name is owner/name, each part
lowercase letters, digits and hyphens:
{
"name": "you/stamp",
"description": "Adds a build stamp to every page.",
"repository": "https://github.com/you/collage-stamp"
}
That name is also what your plugin's Name() returns, and the key of its section
in plugins-config.json. For a short reference the manifest's name must equal
the one typed: collage plugin add you/stamp needs "name": "you/stamp".
The manifest is read strictly: a key the schema does not know is refused. The schema ships with the editor extension, which completes the keys and checks the file as you type.
The plugin is the module root
The package that holds New is the root package of the module, so its import
path is the module path. It cannot be package main, since nothing can import
that. Subpackages are fine for the rest of your code.
New needs no configuration
add writes a call to New into the user's plugins.go without knowing what
your options are. So New has to be a function that can be called with nothing
set up. One of these shapes:
New takes |
add writes |
|---|---|
| nothing | stamp.New() |
| only a variadic parameter | stamp.New() |
| one struct type of your package | stamp.New(stamp.Options{}) |
| one pointer, interface, slice, map or func | stamp.New(nil) |
It returns exactly one value, a collage.Plugin. Refused: two or more
parameters, a basic type such as string, a struct type from another package
(whose zero value the user's file could not always write), and an unexported
type, which no other package can name.
The call has to succeed. What the zero value means is up to you: it should be a working plugin with defaults. A setting with no sensible default is a different case, covered below.
There is a release tag
The version add resolves is a semver tag, v0.1.0. A repository with no tag
only has a pseudo-version (v0.0.0-2026…-abcdef), which is refused: there would
be nothing stable to pin.
Optional manifest fields
Three fields tell add more about where and how your plugin goes.
requires is a list of plugin names the project must also have. add adds
each missing one first, and del refuses to remove one while another plugin
requires it. The websocket plugin requires ["elagoht/live"].
order is an integer, 0 when absent, that says where in plugins.go the
entry goes. Lower is earlier, and earlier in Config.Plugins is the outer
middleware. add puts the new entry before the first existing one whose order
is greater, and never moves an entry that is already there. The published
plugins that ask for a place:
| Plugin | Order | Why |
|---|---|---|
| elagoht/health | −30 | before anything that refuses or answers a request |
| elagoht/fail2ban | −25 | right after health |
| elagoht/compress | −20 | before any plugin that rewrites response bodies |
| elagoht/websocket | −1 | configured before elagoht/live |
| elagoht/devtoolbar | 100 | last, so it sees the other plugins' findings |
Every other plugin is 0. Leave the field out unless position matters.
setup is a short note printed after add succeeds, for what only Go code
can supply. The feed plugin says its feeds are passed to feed.New(...) in
plugins.go.
{
"name": "you/stamp",
"requires": ["elagoht/live"],
"order": -5,
"setup": "Pass your stamps to stamp.New(...) in plugins.go."
}
A required setting fails in Init
add writes the section "you/stamp": {} to plugins-config.json: the
defaults. If your plugin cannot work without a setting (an API key, a site
URL), the empty section is missing it. That is not a contract failure, since
New succeeded. Refuse in Init, in words the user can act on:
func (p *Plugin) Init(ctx context.Context, host collage.Host) error {
if p.cfg.APIKey == "" {
return errors.New(`you/stamp: "apiKey" is required; set it in the "you/stamp" section of plugins-config.json`)
}
return nil
}
The user adds the plugin, starts the application, reads that message and fills in the section. Nothing else is needed from you.
A plugin that needs another
When your plugin works through another one, find it by type instead of asking
the user to pass it in. collage.FindPlugin[T](host) looks through the
application's plugins and returns the plugin of type T (the first, if there are several), from Configure or
from Init:
live, ok := collage.FindPlugin[*live.Plugin](host)
if !ok {
return errors.New("you/stamp needs elagoht/live in the plugin list")
}
It sees every plugin in the list, whether that one has been configured or
initialised yet or not. Then name the other plugin in requires, so that add
brings it along. The websocket plugin does exactly this, which is why
New(nil) works for it.
Check it before you tag
Run it inside your repository:
collage plugin check # the manifest and New
collage plugin check -release # also: a release tag at HEAD (needs git)
collage plugin check -run # also: compile and run the call
collage plugin check -release -run ./path/to/repo
Without a flag it reads the repository and checks the manifest, the name, the
package and the shape of New. -release wants a release tag at HEAD.
-run goes one step further. It builds a small throwaway program that calls
New exactly as add would write it, runs that call, and compares the plugin's
Name() with the manifest's name. It fails on a build error, a panic in
New, or a name that differs. It does not start an application, call
Configure or Init, or read any configuration; so a setting an empty section
lacks is not something it reports. The program's files are removed afterwards,
and go.mod and go.sum put back.
The published plugins run collage plugin check -run on every release.
Do the same in your CI, and a plugin that passes can be added by anyone with one
line.