Contensio logo

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