Write a plugin
A Gophenberg plugin is a Go package compiled into the binary, with
its own route namespace, its own database schema if it wants one,
and a read view of published posts. The built-in RSS feed in
plugins/feed is the reference to keep open.
1. The manifest
Section titled “1. The manifest”A plugin is a directory under plugins/ with a plugin.json:
{ "id": "hello", "name": "Hello", "backend": "github.com/gopherium/gophenberg/plugins/hello"}| Field | Rules |
|---|---|
id | Required. Lowercase letters, digits, and hyphens, starting with a letter. Must equal the directory name |
name | Required. The human readable name |
backend | The Go import path of the plugin package |
frontend | The package name of an admin screen, if any |
At least one of backend and frontend is required.
A few ids are refused because they would clash with names in the
generated wiring, such as a Go keyword, err or plugins. The
pluginkit docs
list them all. The id also names the plugin’s commands, so
help, list, version, check, serve, migrate, seed and
account are refused too. Those names belong to the command line.
2. The package
Section titled “2. The package”The entry point is Register(deps sdk.Deps) (sdk.Plugin, error),
receiving the SDK’s Deps. A minimal
plugin serving one route:
package hello
import ( "context" "net/http"
"github.com/gopherium/gophenberg/sdk")
type Plugin struct{}
// Register builds the plugin from its dependencies.func Register(deps sdk.Deps) (sdk.Plugin, error) { return &Plugin{}, nil}
// ID names the plugin.func (p *Plugin) ID() string { return "hello" }
// Start begins serving.func (p *Plugin) Start(ctx context.Context) error { return nil }
// Stop ends serving.func (p *Plugin) Stop(ctx context.Context) error { return nil }
// Routes serves the plugin's namespace.func (p *Plugin) Routes() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/greeting", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) }) return mux}Register reads settings and builds the plugin, nothing more.
Commands such as list and check register every plugin and stop
it again without starting it, so open connections in Start, and
make Stop work when Start never ran.
3. Wire it in
Section titled “3. Wire it in”make generateThis regenerates the wiring from every manifest, and the next
build compiles your plugin in. There is no list to edit by hand.
Then gophenberg check registers your plugin and checks its
settings and command names without touching the database.
What the host gives you
Section titled “What the host gives you”- Routes mount at
/api/plugins/hello, prefix stripped, and require a login by default. A successful answer on those routes is markedprivate, no-store. A failed one, a refused login included, is markedno-store. Either way no cache keeps it, whatever header your plugin sets. Public paths keep your own header. - Public paths: declare
PublicPaths() []stringand those exact paths answer without a session, for every method. Exact match, never a prefix. This is how the feed serves/api/plugins/feed/rss.xmlpublicly. A write a browser sends from a page on another site is refused before it reaches you, with the coderequest_cross_origin. When the site names its public address, so is any write sent to another address, a webhook included. A read never is, so a public path must not change anything on a GET. - Migrations: implement
Migrate(ctx) errorand it runs before anything starts, and again ongophenberg migrateandgophenberg seed -yes. Keep your tables and your migration record in a schema of your own. If you run goose, turn on its session locker withgoose.WithSessionLockerandlock.NewPostgresSessionLocker(). Every core schema step waits for that same Postgres lock, so two processes never apply your migrations together. - Configuration arrives through
deps.Env, which reads the settings under theGOPHENBERG_prefix. The feed readsGOPHENBERG_FEED_TITLEandGOPHENBERG_FEED_ITEMSthroughdeps.Env.Within("FEED_").deps.Getenvstill reads any other environment variable. - Commands: implement
Commands() []sdk.Commandand thegophenbergcommand line offers them ashello:<command>. The plugin SDK page shows one. - Content declarations: implement
DeclareTypesand the content types, field groups and fields your plugin needs exist on every site it is compiled into. The plugin SDK page shows one.
One honest boundary
Section titled “One honest boundary”Compiling a plugin in is a trust decision. The SDK is a clean
interface, not a sandbox: deps.DatabaseURL reaches the same
database the core uses. Review what you compile in.