Skip to content

Declare content from a plugin

A plugin that needs a content type should not ask the site owner to build one by hand. Declare it in code, and every site the plugin is compiled into gets it at startup.

Implement DeclareTypes and the host hands you a registrar before anything serves:

func (p *Plugin) DeclareTypes(ctx context.Context, types sdk.TypeRegistrar) error {
if err := types.DeclareType(ctx, sdk.TypeDeclaration{
Key: "event", SingularLabel: "Event", PluralLabel: "Events", RouteWord: "events",
}); err != nil {
return err
}
return types.DeclareGroup(ctx, sdk.GroupDeclaration{
Key: "event-details",
Title: "Event details",
Location: [][]sdk.Rule{{{Source: "content_type", Operator: "==", Value: "event"}}},
Fields: []sdk.FieldDeclaration{
{Key: "venue", Label: "Venue", Kind: "text", Required: true},
{Key: "ticketed", Label: "Ticketed", Kind: "boolean"},
{Key: "price", Label: "Price", Kind: "number", Conditions: [][]sdk.Rule{
{{Source: "ticketed", Operator: "==", Value: "true"}},
}},
{Key: "schedule", Label: "Schedule", Kind: "section", Fields: []sdk.FieldDeclaration{
{Key: "starts-at", Label: "Starts at", Kind: "date"},
}},
},
})
}

Conditions says when a field shows, reading the fields standing beside it. Declare them in any order you like, the host writes the fields first and their conditions afterwards. Listed: true gives the field a column in the content list, and a filter above it when the field is a switch or a choice.

Keys follow the same shape everywhere in Gophenberg. Start with a lowercase letter, then lowercase letters, numbers and hyphens.

The registrar is not a one time installer. It runs at every startup, and it is written to be safe to repeat:

  • A definition that is not there yet is created.
  • One that is there and matches is left alone.
  • A changed label, required flag, setting or location is carried onto the stored one.

So the way to change a field is to edit the declaration and restart. There is no migration to write and no version to bump.

Changing a field’s Kind and changing a type’s Route word both strand content that is already stored. The host refuses them and the site will not start, which is loud on purpose.

To change either one, declare a new key beside the old one, move what you need, and stop declaring the old key.

A definition your plugin declares belongs to your plugin. The admin shows it with a badge naming the plugin. Nobody can edit or delete it from the screens, though they can turn it off, and they can move it in the order.

If the site already holds a type or group under a key you declare, your declaration is skipped. The site’s own definition wins, always. The start log says which keys were skipped, and the Field Groups screen names the clash so the site owner can see it.

That rule is why you should pick keys nobody else would: prefix them with your plugin’s own name if you expect company.

A type you declare without Hierarchical: true does not nest. If any of its items still sits inside another, the ones in the trash included, the site keeps the type nesting. It still starts, the start log warns, and the Field Groups screen says so. Once those items move to the top level, the next start turns nesting off.

Stop declaring a definition and the rows stay where they are, with your plugin’s name still on them. The site owner sees them on the Field Groups screen as coming from a plugin that no longer declares them, and gets one button, Adopt, which hands the definition to the site.

Nothing is deleted behind anyone’s back. A definition that holds content keeps holding it until a person decides otherwise.

Field values reach a plugin through the read seam:

items, err := deps.Content.ListPublished(ctx, "event", 10)
venue := items[0].Fields["venue"]

Fields holds the values keyed by field key, shaped the way the content API serves them. They are data, not markup, so escape them before serving them as HTML. The plugin SDK page covers the rest of what Deps gives you.