Hook system
Actions, Filters, and UI render hooks - extend Contensio from your plugin without touching core files.
Contensio's hook system lets your plugin respond to events and modify data at well-defined points in the CMS lifecycle - without forking core, without monkey-patching, without fragile overrides.
There are three distinct hook types:
| Type | Class method | Purpose | Returns |
|---|---|---|---|
| Render | Hook::add() / Hook::render() |
Inject HTML into a template slot | HTML string |
| Action | Hook::addAction() / Hook::doAction() |
React to an event (fire-and-forget) | nothing |
| Filter | Hook::addFilter() / Hook::applyFilters() |
Transform a value through a chain | the modified value |
All methods are on Contensio\Support\Hook. Import it once:
use Contensio\Support\Hook;
Render hooks
Render hooks are injection points baked into core and theme Blade templates. Your callback returns an HTML string - it's concatenated with other registered callbacks and inserted in place.
Hook::add(string $name, callable $callback, int $priority = 10): void
Example
// In your service provider's boot():
Hook::add('contensio/admin/login-after-form', function (): string {
return view('acme-awesome::partials.login-buttons')->render();
});
Hook::add('contensio/admin/settings-cards', function (): string {
return view('acme-awesome::partials.settings-hub-card')->render();
});
The callback must return a string. Return '' to render nothing conditionally.
How core calls them
{!! \Contensio\Support\Hook::render('contensio/admin/login-after-form') !!}
{!! \Contensio\Support\Hook::render('contensio/frontend/post-after-content', $content, $translation) !!}
Actions
An action fires at a specific point in execution. Your callback receives context, does something, and returns nothing.
Hook::addAction(string $hook, callable $callback, int $priority = 10): void
Example
Hook::addAction('contensio/content/published', function ($content) {
if ($content->status === 'published') {
SearchIndex::push($content);
}
});
Firing an action (defining a custom hook point)
Plugins can also define their own hook points for other plugins to extend:
// In your plugin's code - fire a custom hook
Hook::doAction('plugins/acme/awesome/item-created', $item, $user);
// In another plugin - listen to it
Hook::addAction('plugins/acme/awesome/item-created', function ($item, $user) {
// react to the event
});
Filters
A filter passes a value through your callback. You receive it, modify it, and return it. The returned value becomes the input for the next callback.
Hook::addFilter(string $hook, callable $callback, int $priority = 10): void
Example
Hook::addFilter('contensio/content/body', function (string $html, $content): string {
// Inject a banner after the third paragraph
if ($content->type !== 'post') return $html;
$banner = '<div class="my-ad"><!-- ad --></div>';
$pos = 0;
for ($i = 0; $i < 3; $i++) {
$found = strpos($html, '</p>', $pos);
if ($found === false) break;
$pos = $found + 4;
}
return $pos > 0 ? substr($html, 0, $pos) . $banner . substr($html, $pos) : $html;
});
Always return the value - even unmodified. A filter that returns null replaces the value with null.
Applying a filter (defining a custom filter point)
// In your plugin - apply a filter to a value
$rendered = Hook::applyFilters('plugins/acme/awesome/render', $html, $item);
Priority
Lower numbers run first. Default is 10. Same-priority callbacks run in registration order.
Hook::add('contensio/frontend/post-after-content', $authorBox, 5); // runs first
Hook::add('contensio/frontend/post-after-content', $relatedPosts, 20); // runs last
Checking if a hook has callbacks
if (Hook::has('contensio/admin/login-after-form')) {
// at least one callback is registered
}
Useful for wrapping output in a container that should only appear when there's something to show:
@if(\Contensio\Support\Hook::has('contensio/admin/login-after-form'))
<div class="mt-6 border-t border-gray-100 pt-6">
{!! \Contensio\Support\Hook::render('contensio/admin/login-after-form') !!}
</div>
@endif
Hook naming convention
Hook names use forward-slash-separated paths. The first segment identifies the owner.
Core hooks use the contensio/ prefix:
contensio/frontend/head
contensio/frontend/body-start
contensio/frontend/body-end
contensio/frontend/post-meta
contensio/frontend/post-before-content
contensio/frontend/post-after-content
contensio/content/body (filter)
contensio/content/published (action)
contensio/content/viewed (action)
contensio/admin/login-after-form
contensio/admin/profile-sections
contensio/admin/settings-cards
contensio/admin/dashboard-widgets
contensio/admin/dashboard-stats
contensio/admin/dashboard-quick-actions
contensio/admin/dashboard-after
Plugin-defined hooks use plugins/{vendor}/{slug}/ as prefix to guarantee uniqueness:
plugins/acme/awesome/item-created
plugins/contensio/polls/after-vote
plugins/contensio/newsletter/subscribed
Built-in hook reference
For full documentation on every built-in hook - arguments, timing notes, and examples - see the Hook Reference.
| Group | Hooks |
|---|---|
| Admin UI render hooks | login form, profile sections, settings cards, dashboard slots |
| Frontend render hooks | head, body-start/end, post-meta, before/after content |
| Frontend filters | contensio/content/body, contensio/frontend/page-title |
| Content lifecycle | contensio/content/published, updated, deleted |
| Media lifecycle | contensio/media/uploaded, deleted |
| API filters | contensio/api/content-item |
Error handling
Callbacks that throw are caught, reported via Laravel's report(), and skipped. Other registered callbacks continue running. For filters, the current value passes through unchanged. A broken plugin never takes down the page.
Full example
<?php
namespace Acme\ContentExtras;
use Contensio\Models\Content;
use Contensio\Support\Hook;
use Illuminate\Support\ServiceProvider;
class ContentExtrasServiceProvider extends ServiceProvider
{
protected string $ns = 'acme-content-extras';
public function boot(): void
{
$this->loadViewsFrom(__DIR__ . '/../resources/views', $this->ns);
// Action - log every new publish to an audit table
Hook::addAction('contensio/content/published', function (Content $content) {
AuditEntry::create([
'type' => 'content.published',
'item_id' => $content->id,
'user_id' => auth()->id(),
]);
});
// Filter - append reading time to every API response
Hook::addFilter('contensio/api/content-item', function (array $data, Content $content): array {
$words = str_word_count(strip_tags($data['excerpt'] ?? ''));
$data['reading_time_minutes'] = max(1, (int) ceil($words / 200));
return $data;
});
// Render - inject an author stats panel on the admin profile page
Hook::add('contensio/admin/profile-sections', function ($user): string {
if (! $user) return '';
return view($this->ns . '::partials.author-stats', compact('user'))->render();
});
// Render - settings hub card
Hook::add('contensio/admin/settings-cards', function (): string {
return view($this->ns . '::partials.settings-hub-card')->render();
});
}
}
See also
- Service provider - where to register your hooks
- Hook reference - every built-in hook with arguments and examples
- Hooks vs WordPress hooks