Build the hello plugin
In this guide you build hello, the example plugin in the Hatamoto repository, and load it into a server on your own machine. When you finish, the server logs a line each time you publish a page, and every page carries one extra value the plugin added.
Before you start
- A checkout of the Hatamoto repository.
- A Go toolchain that satisfies the
goline inbackend/pkg/sdk/go.mod(Go 1.26 or newer). - A Hatamoto server you can start on your machine, with
DATABASE_URLset and thehatamotoprogram on yourPATH. See Getting started. - An idea of what a plugin is. Understand how plugins work explains it in a few minutes.
Steps
Look at the files
- Open
examples/plugins/hello. The plugin is aplugin.json, amain.go, aREADME.mdand ago.mod.plugin.jsonis the manifest, the file that describes the plugin: its name, its version, the SDK it was built against, a config value and its limits. The SDK is the Go modulehatamoto.dev/sdk, which defines what a plugin and the server exchange.{ "name": "hello", "version": "0.2.0", "sdk": "^0.8", "description": "Reference plugin: logs published entities and enriches JSON-LD.", "config": { "greeting": "konnichiwa" }, "limits": { "memory_pages": 256, "hook_timeouts_ms": { "entity.published": 2000, "jsonld.enrich": 250 }, "max_instances": 4 } }JSON-LD is the structured data for search engines that each page carries.
hook_timeouts_msis how many milliseconds the server waits for each hook (explained in step 3) before it gives up on it."sdk": "^0.8"loads because the server still serves plugins built for SDK 0.8. - Open
go.mod. Itsreplace hatamoto.dev/sdk => ../../../backend/pkg/sdkline only lets the example build inside the checkout. Your own plugin depends on the published module instead:go get hatamoto.dev/[email protected]SDK 0.12.0 ships with Hatamoto
v0.31.0; until your users run it, declare^0.11. Thesite.scriptspermission belongs to SDK 0.12.0, but the range you declare does not gate it: hosts from v0.30.0 accept it under any range and enforce it, older hosts reject it as unknown (The sdk range). - Open
main.go. It registers two handlers and exports one function per hook, a point where a plugin joins in: an action runs when something happens, a filter changes a value before it is used.package main import ( "fmt" "hatamoto.dev/sdk" "hatamoto.dev/sdk/guest" ) func init() { guest.OnAction(sdk.EntityPublished, func(ev sdk.EntityEvent) error { guest.Log(guest.LogInfo, fmt.Sprintf("published %s/%s (%s)", ev.SchemaSlug, ev.Slug, ev.ID)) return nil }) guest.OnFilter(sdk.JSONLDEnrich, func(d sdk.JSONLDDoc) (sdk.JSONLDDoc, error) { greeting, ok := guest.Config("greeting") if !ok { greeting = "hello" } if d.Doc == nil { d.Doc = map[string]any{} } d.Doc["x-hello"] = greeting return d, nil }) } //go:wasmexport on_entity_published func onEntityPublished() int32 { return guest.Run(sdk.EntityPublished.Export()) } //go:wasmexport filter_jsonld_enrich func filterJSONLDEnrich() int32 { return guest.Run(sdk.JSONLDEnrich.Export()) } // main is never called: a c-shared wasm module is a reactor. func main() {}The action logs each published page. The filter adds
x-helloto the page's structured data for search engines, using the manifest'sgreetingvalue. Each handled hook needs its one-line//go:wasmexportfunction, andExport()turns the hook name into the export name.
Build and load it
- From the repository root, build the plugin:
make plugin-helloBuilt examples/plugins/hello/build/The plugin compiles to WebAssembly, a standard format for small programs that the server runs in a closed space with no access to its files or network. The standard Go toolchain does this; you do not need TinyGo, the separate Go compiler some WebAssembly projects use. The result is
build/plugin.wasmnext to a copy ofplugin.json. - Copy the build into a plugin directory and check that the server can read it:
mkdir -p "$PWD/plugins/hello" cp examples/plugins/hello/build/* "$PWD/plugins/hello/" export HATAMOTO_PLUGIN_DIR="$PWD/plugins" hatamoto plugins listhello 0.2.0 ^0.8HATAMOTO_PLUGIN_DIRis a development setting: it loads each subdirectory as an unsigned plugin, kept in memory only. Never use it in production. - In the same terminal, so
HATAMOTO_PLUGIN_DIRis still set, go to the folder your server normally starts in and start it there:cd /path/to/your/site hatamoto serveStart it from that folder, not from the repository. The server keeps its server secret, a long random value it uses to sign links and protect stored credentials, in
./data/secretsof the folder it starts in, unlessHATAMOTO_SECRETS_DIRsays otherwise. If you start it anywhere else, it creates a new secret, and preview links and stored import credentials made before stop working. After you rebuild the plugin, copy it again and restart the server.
Run the tests
- From
backend, run the tests that load the compiled plugin into the real server code:go test ./internal/pluginhost -run TestHelloLoadsAndHandlesHooksFor your own plugin,
guest.Handle(export, input)runs a handler without the runtime, so ordinary Go unit tests work.
Check it worked
- The server log shows
msg="plugin loaded" plugin=helloat startup. - Publish a page in the admin. The log shows
published page/<slug> (<id>)withplugin=hello. - Open the page and view its source. The
<script type="application/ld+json">block contains"x-hello":"konnichiwa". Removeconfigfrom the manifest, rebuild and restart, and the value becomeshello.
If hatamoto plugins list prints the plugin name followed by an error in parentheses, the manifest is not valid, and the message names the field to fix. If the list looks right but the log shows packages: plugin dir with a plugin package needs plugin.wasm, the module is missing: copy the whole build directory again.