Docs / Admin Extension Points

Admin Extension Points

Admin extension points let plugins and themes add behavior to the CMS while keeping the core admin stable.

Every extension point follows the same contract: core defines a stable data shape, hook/provider name, and trigger location; extensions register an implementation during activation; with no extension, filters pass through and actions have no side effects.

Plugin Settings Pages

A plugin can provide an admin settings page by implementing settings provider interfaces. The admin will show a Plugin Settings button on the plugin card and route requests to the plugin-owned template or renderer.

Typical responsibilities:

  • Render settings UI.
  • Load current settings data.
  • Save submitted settings.
  • Return translated labels through the admin locale system.

Content Form Fields

Plugins can add extra fields to content editing pages through admin.content_form.fields.

The seo-extras plugin uses this hook to append a collapsible SEO panel with fields for SEO title, description, Open Graph image, and robots.

Content Save Actions

Plugins can listen to admin.content.saved and persist additional form values. This keeps plugin data separate from the core content model while still making it part of the editorial workflow.

Content List Tabs

The admin exposes admin.HookContentListTabs for plugins that need additional list filters. The multilingual plugin uses it to add language tabs and counts to content list pages.

Tabs normally compose with the Content Scope API: the selected query parameter registers a request scope, and totals, filters, and pagination all use the scoped query.

Taxonomy List Tabs

admin.HookTaxonomyListTabs exposes the same request-aware tab pattern above Category, Tag, and other registered taxonomy lists. The filter receives the current *gin.Context and taxonomy type, so an extension can append a URL such as ?lang=zh and register the matching Taxonomy Scope.

Counts, trees, parent choices, content-reference totals, create/update/delete commands, and content-editor selectors must consume that same scope. A tab is a navigation control, not an authorization boundary; each protected operation still requires its specific taxonomy RBAC capability.

admin.HookContentPermalinkPrefix lets an extension prepend a contextual segment such as /zh or /site-2 to the permalink shown in the content editor. Its value is an empty string by default, and its arguments are the current *gin.Context and content row. The hook changes the editor preview only through the same core URL contract; themes do not need to know why the prefix exists.

Dashboard Widgets

Plugins can append trusted dashboard markup through admin.dashboard.widgets. The filter value is template.HTML, and the first argument is the dashboard template root. A widget must check the current role before rendering and protect every backing API with core authentication plus a specific RBAC capability.

The gopress-analytics plugin uses this slot for its traffic summary, while its JSON endpoint requires analytics.read.

Mail and Notification Hooks

Mail is split into two layers: core/mail owns message delivery, while notification rules listen to core events and call the mail service. Plugins can filter outgoing messages, observe delivery results, or customize the default contact-message notification.

Hook Type Purpose
content.created action Fired after a content row and its meta are saved. Args: *content.Content, map[string]string
mail.message filter Modify mail.Message before delivery
mail.before_send action Observe a message before SMTP delivery
mail.sent action Fired after successful delivery
mail.failed action Fired after failed delivery. Args: mail.Message, error
notification.contact_message.recipients filter Modify new contact-message recipients, value: []string
notification.contact_message.subject filter Modify the contact-message subject, value: string
notification.contact_message.body filter Modify the contact-message plain-text body, value: string

Example: add a sales inbox to contact-message notifications:

e.Hooks.AddFilter(hook.NotificationContactMessageRecipients,
    func(value interface{}, args ...interface{}) interface{} {
        recipients, _ := value.([]string)
        return append(recipients, "sales@example.com")
    }, 20)

Plugins that need to send their own notification emails should use core's mail.Sender capability instead of reading SMTP settings or depending on a concrete driver:

sender := plugin.MailSender(app)
if sender == nil {
    return
}

err := sender.Send(ctx, mail.Message{
    To:      []string{"admin@example.com"},
    Subject: "Plugin notification",
    Text:    "Something happened.",
})

Themes can access the same capability through theme.App.MailSender() or t.MailSender() when embedding BaseTheme, for example in a theme-owned form handler. Themes should still avoid knowing SMTP hosts, Gmail app keys, or whether delivery uses go-mail or stdlib; the preferred default remains: themes save content or fire core hooks, while notification rules or plugins send mail.

SMTP configuration, notification switches, and delivery behavior stay in core or plugin extension points.

Frontend Template Slots

The same core hook bus exposes semantic frontend locations:

Hook Location
theme.head.end Immediately before </head>.
theme.body.open Immediately after <body>.
theme.footer.end After theme scripts and before </body>.
header.nav.after At the end of the primary navigation list.

Themes declare these locations with renderHook; plugins return markup that matches the surrounding semantics. Extensions must not scan or post-process a complete HTML response to find an insertion point.

Translation Requirements

Admin-facing theme and plugin settings should not hard-code Chinese or English in templates. They should use the admin translation helper or component-owned locale files. If a component ships only one language, the admin should fall back to that available language instead of hiding the UI.

Design Rule

Extensions should communicate with the admin through core hooks, providers, and template functions. Avoid direct imports between themes and plugins, and avoid post-processing full HTML responses to inject admin UI.

Every plugin must retain returned hook handles and remove them in Deactivate. Core rebuilds the router and clears relevant cache paths after activation changes so middleware, routes, admin UI, and frontend output reflect the new state without a restart.