Skip to content

Grav Plugin Events — Complete Event Reference and Lifecycle

DodaTech Updated 2026-06-27 8 min read

In this tutorial, you'll learn Grav plugin events — the complete event reference, request lifecycle from initialization to output, event-driven development patterns, and practical examples of the most important events.

What You'll Learn

  • The complete Grav event lifecycle
  • Request events: initialization, routing, page handling
  • Content events: raw content, processed content, metadata
  • Twig events: template paths, variables, extensions
  • Output events: assets, final output
  • Admin events: page management, plugin configuration
  • Debugging and profiling events

Why It Matters

In WordPress, you use add_action() and add_filter() throughout your code. In Grav, events are dispatched at specific, predictable points. Knowing the event lifecycle lets you inject your code at exactly the right moment — modify content after it is loaded but before it is cached, add assets after plugins are initialized but before the page renders, or intercept form submissions before they are processed.

Real-World Use

A security plugin needs to scan all content for malicious code before it reaches the browser. It subscribes to onOutputGenerated, the last event before output is sent. The plugin scans the HTML for known attack patterns and blocks suspicious content. Because it runs at the latest possible event, it catches content modified by all other plugins too.

Learning Path

flowchart LR
    A["Plugin Architecture"] --> B["Plugin Events
← You are here"]:::current B --> C["Plugin Forms"] C --> D["Plugin Admin"] D --> E["Plugin CLI"] E --> F["Multilingual"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Event Lifecycle

Grav events fire in a specific order during each request:

Phase 1: Initialization

Event When What You Can Do
onPluginsInitialized All plugins loaded Initialize plugin, check conditions
onAssetsInitialized Asset manager ready Register CSS/JS assets
onSessionInitialized Session started Set session values, check auth
onUserLogin User logged in Log activity, update last login

Phase 2: Page Handling

Event When What You Can Do
onPageNotFound Page not found (404) Show custom 404, redirect
onPageContentRaw Raw Markdown loaded Modify Markdown before processing
onPageProcessed Page YAML and content parsed Read/modify processed page data
onFolderMedia Media files in page folder Filter or modify page media

Phase 3: Twig Processing

Event When What You Can Do
onTwigTemplatePaths Template paths collected Add custom template directories
onTwigSiteVariables Twig variables prepared Add custom Twig variables
onTwigExtensions Twig extensions registered Add custom filters/functions
onTwigLoader Twig loader initialized Custom template loading

Phase 4: Output

Event When What You Can Do
onOutputGenerated Final HTML ready Modify HTML, add tracking scripts
onShutdown Request complete Cleanup, log analytics

Event Handler Patterns

Modifying Page Content

<?php
namespace Grav\Plugin;

use Grav\Common\Plugin;
use Grav\Common\Page\Page;
use RocketTheme\Toolbox\Event\Event;

class ContentPlugin extends Plugin
{
    public static function getSubscribedEvents()
    {
        return [
            'onPageContentRaw' => ['onPageContentRaw', 0],
            'onPageProcessed' => ['onPageProcessed', 0],
        ];
    }

    // Modify raw Markdown before processing
    public function onPageContentRaw(Event $event)
    {
        $page = $event['page'];
        $content = $page->getRawContent();

        // Replace custom markdown syntax
        $content = str_replace('[current-year]', date('Y'), $content);

        $page->setRawContent($content);
    }

    // Modify processed page data
    public function onPageProcessed(Event $event)
    {
        $page = $event['page'];
        $header = $page->header();

        // Add reading time to frontmatter
        $word_count = str_word_count(strip_tags($page->content()));
        $header->reading_time = ceil($word_count / 200);
    }
}

Adding Twig Variables

public static function getSubscribedEvents()
{
    return [
        'onTwigSiteVariables' => ['onTwigSiteVariables', 0],
    ];
}

public function onTwigSiteVariables()
{
    // Add a global Twig variable
    $this->grav['twig']->twig_vars['current_year'] = date('Y');
    $this->grav['twig']->twig_vars['site_version'] = '2.3.1';

    // Add data from external API
    $stats = $this->getCachedStats();
    $this->grav['twig']->twig_vars['site_stats'] = $stats;
}

In Twig:

<p>&copy; {{ current_year }} DodaTech</p>
<p>Version {{ site_version }}</p>

Handling 404 Pages

public function onPageNotFound(Event $event)
{
    $page = $event['page'];

    // Log the 404
    $this->grav['log']->warning('404: ' . $this->grav['uri']->route());

    // Custom redirect for known old URLs
    $redirects = [
        '/old-page' => '/new-page',
        '/deprecated' => '/current',
    ];

    $route = $this->grav['uri']->route();
    if (isset($redirects[$route])) {
        $this->grav->redirect($redirects[$route], 301);
    }
}

Modifying Output

public function onOutputGenerated(Event $event)
{
    $output = $event->getOutput();

    // Add Google Analytics tracking code before </body>
    $tracking = <<<HTML
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
<script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', 'GA_ID');
</script>
HTML;

    $output = str_replace('</body>', $tracking . '</body>', $output);

    // Add a performance header
    $event->setOutput($output);
    $this->grav->header->set('X-Grav-Time', microtime(true) - GRAV_REQUEST_TIME);
}

Handling Assets

public function onAssetsInitialized()
{
    $assets = $this->grav['assets'];

    // Add conditional CSS based on page type
    $page = $this->grav['page'];
    if ($page && $page->template() == 'blog') {
        $assets->addCss('theme://css/blog.css');
    }

    // Add JS with defer
    $assets->addJs('theme://js/lazy.js', [
        'loading' => 'defer',
        'group' => 'footer'
    ]);
}

Admin Events

Event When What You Can Do
onAdminPage Admin page rendered Modify admin page content
onAdminSave Page saved in admin Validate, modify before save
onAdminAfterSave Page saved successfully Post-save actions (cache clear)
onAdminDelete Page about to be deleted Prevent deletion, cleanup
onDashboard Admin dashboard rendered Add dashboard widgets
onAdminMenu Admin menu built Add menu items

Plugin Admin Hook Example

public static function getSubscribedEvents()
{
    return [
        'onAdminSave' => ['onAdminSave', 0],
        'onAdminAfterSave' => ['onAdminAfterSave', 0],
    ];
}

public function onAdminSave(Event $event)
{
    $page = $event['page'];
    $header = $page->header();

    // Validate custom field
    if (isset($header->featured) && !is_bool($header->featured)) {
        throw new \RuntimeException('Featured must be true or false');
    }
}

public function onAdminAfterSave(Event $event)
{
    // Regenerate sitemap after page save
    $sitemap = $this->grav['sitemap'] ?? null;
    if ($sitemap) {
        $sitemap->generate();
    }
}

Plugin Events in Admin Context

Check if running in admin:

public function onPluginsInitialized()
{
    if ($this->isAdmin()) {
        $this->enable([
            'onAdminSave' => ['onAdminSave', 0],
            'onAdminDashboard' => ['onAdminDashboard', 0],
        ]);
        return;
    }

    // Frontend events
    $this->enable([
        'onPageContentRaw' => ['onPageContentRaw', 0],
        'onOutputGenerated' => ['onOutputGenerated', 0],
    ]);
}

Learning Path

flowchart LR
    A["Plugin Architecture"] --> B["Plugin Events
← You are here"]:::current B --> C["Plugin Forms"] C --> D["Plugin Admin"] D --> E["Plugin CLI"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Common Mistakes

  1. Modifying content in the wrong event: If you modify Markdown after onPageProcessed, your changes may be overwritten. Use onPageContentRaw for Markdown modifications and onPageProcessed for post-processing.

  2. Not using event priority correctly: If your plugin depends on another plugin's output, use a higher priority number to run later. If your plugin provides data for others, use a lower number to run earlier.

  3. Heavy processing in onPluginsInitialized: This event runs on every request. Keep initialization light. Move heavy processing to later events or cache results.

  4. Not checking isAdmin() for admin-only events: Admin events like onAdminSave should be registered inside an isAdmin() check to prevent errors on frontend requests.

  5. Forgetting to return the event object: Some events expect you to return or modify the event object. Check the event's behavior before writing the handler.

Practice Questions

  1. What is the last event before Grav sends output to the browser? Answer: onOutputGenerated. This event receives the final HTML output and can modify it before it is sent to the client.

  2. How do you add a custom variable to all Twig templates? Answer: Subscribe to onTwigSiteVariables and set $this->grav['twig']->twig_vars['my_var'] = $value. The variable is then available in all templates.

  3. What event would you use to modify page content before it is cached? Answer: onPageContentRaw, which fires before the content is processed and cached. Changes made here are reflected in the cached version.

  4. How do you register admin-specific events conditionally? Answer: Check $this->isAdmin() inside onPluginsInitialized() and call $this->enable([...]) with admin-specific events only when in the admin context.

  5. Challenge: Build a plugin that monitors page saves and sends notifications. Subscribe to onAdminAfterSave, detect if the page was published, construct a notification message with the page title and URL, and send it through email or a Webhook. Include logging of all notifications sent, proper error handling if the webhook fails, and a setting to enable/disable notifications per page type.

FAQ

How many events does Grav have?

Grav has over 50 events covering initialization, page handling, Twig processing, output generation, admin operations, and plugin lifecycle. The complete list is in the Grav developer documentation.

Can I create custom events in my plugin?

Yes. Use $this->grav->fireEvent('myCustomEvent') to dispatch events. Other plugins can subscribe to your custom event using the same event system.

What is the priority parameter in event subscription?

Priority determines the order in which handlers for the same event are executed. Lower values execute first. Default is 0. Range is typically 0-10.

How do I stop event propagation?

Grav events do not support stopping propagation like JavaScript events. All subscribers receive the event. Use priority ordering to control execution order.

Can I subscribe to an event from a theme?

Yes. Theme PHP files can also subscribe to events. Create a onTwigExtensions() method in the theme class to register Twig extensions, for example.

Mini Project

Goal: Build an event-driven plugin that hooks into 6 different lifecycle points.

  1. Create a plugin that subscribes to onPageContentRaw, onPageProcessed, onTwigSiteVariables, onOutputGenerated, onAdminSave, and onShutdown
  2. In onPageContentRaw: add a [last-modified] placeholder replacement
  3. In onPageProcessed: calculate and store reading time
  4. In onTwigSiteVariables: add current_year and site_build_time
  5. In onOutputGenerated: add a performance comment at the end of HTML
  6. In onAdminSave: validate that the title is not empty
  7. In onShutdown: log the request time and URL
  8. Add a debug mode that outputs all events with their execution times
  9. Test the plugin with different page types and admin operations
  10. Document event execution order with timing output

What's Next

Now you understand all Grav events. Next, learn plugin form handling:

Continue to Lesson 26: Plugin Forms — Form state, validation, file uploads, and multi-step forms.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro