React to events
In this guide you write a small plugin called notice from scratch. It writes a line to the server log each time something is published, and it adds an author tag to the head of every public page. You end with a working plugin you can extend with any other event.
Before you start
- You have built and loaded the example plugin in Build the hello plugin, so Go 1.26 and a local server are ready.
- You know where your server's log appears (the terminal that runs
hatamoto serve).
A hook is a point where a plugin joins in. There are two kinds: an action runs when something happens and returns nothing, and a filter changes a value before the server uses it. This plugin uses one of each.
Steps
Create the plugin
- Make a directory for the plugin outside the Hatamoto checkout, and start a Go module in it:
mkdir notice cd notice go mod init example.com/notice 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. - Create
plugin.json, the manifest that describes the plugin.configholds fixed values the plugin reads at run time:{ "name": "notice", "version": "0.1.0", "sdk": "^0.11", "description": "Logs every publish and adds an author tag to each page.", "config": { "author": "Example Studio" } }The plugin asks for no permissions, the rights an owner grants a plugin, because writing to the log with
guest.Logand readingconfigneed none. - Create
main.go:package main import ( "fmt" "hatamoto.dev/sdk" "hatamoto.dev/sdk/guest" ) func init() { // Action: runs after a page or post is published. guest.OnAction(sdk.EntityPublished, func(ev sdk.EntityEvent) error { guest.Log(guest.LogInfo, fmt.Sprintf("notice: published %s %q", ev.SchemaSlug, ev.Name)) return nil }) // Filter: adds one meta tag to the head of every public page. guest.OnFilter(sdk.RenderHead, func(c sdk.HeadContext) (sdk.HeadContext, error) { author, ok := guest.Config("author") if !ok || author == "" { return c, nil } c.Tags = append(c.Tags, sdk.HeadTag{ Kind: "meta", Attrs: map[string]string{"name": "author", "content": author}, }) return c, nil }) } //go:wasmexport on_entity_published func onEntityPublished() int32 { return guest.Run(sdk.EntityPublished.Export()) } //go:wasmexport filter_render_head func filterRenderHead() int32 { return guest.Run(sdk.RenderHead.Export()) } func main() {}Each hook has a handler, registered in
init, and an exported function the server calls by name. The name is the hook name with.replaced by_, afteron_for an action orfilter_for a filter:entity.publishedbecomeson_entity_published. The filter returns the value it received, with one tag added. The server builds and escapes the HTML for the tag, so a plugin cannot inject markup. - Fetch the remaining dependencies:
go mod tidy
Build and load it
- Build the plugin and put the manifest next to it:
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o build/plugin.wasm . cp plugin.json build/ - Copy the build into the plugin directory your server reads, next to
hello:mkdir -p <plugins>/notice cp build/* <plugins>/notice/Replace
<plugins>with the directory you set inHATAMOTO_PLUGIN_DIRin the hello guide. - Restart the server with
HATAMOTO_PLUGIN_DIRset, as before.
Check it worked
- At startup the log shows
msg="plugin loaded" plugin=noticewithhooks=[entity.published render.head]. The list names every hook the plugin exports. - Publish any page in the admin. The log shows a line like this one:
level=INFO msg="notice: published page \"About us\"" plugin=notice - Open any public page and view its source. With the default theme, the head contains
<meta name="author" content="Example Studio">.
If the tag is missing but the log line appears, the page came from the page cache. Publishing a page clears the cache, so reload the page after step 2.
If the filter returns an error or takes longer than its time limit (250 ms by default), the page still loads, without the tag, and the server logs the failure. This is true for every filter.
To react to other events, such as a form being sent or an import finishing, or to refuse a save with entity.before_save, pick the hook from Hooks and events and add a handler and an export the same way.