Grav Custom Plugins: Events, Twig & REST API

Imagine needing to output data from a CRM on every Grav page, but the default infrastructure lacks the extensibility for such customization. Or requiring a custom shortcode that parses content and inserts a widget with dynamic data. Without core changes—only a plugin leverages the Grav event model.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1414
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1285
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    980
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1240
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    982
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    994

Imagine needing to output data from a CRM on every Grav page, but the default infrastructure lacks the extensibility for such customization. Or requiring a custom shortcode that parses content and inserts a widget with dynamic data. Without core changes—only a plugin leverages the Grav event model. This article provides a technical breakdown of Grav plugin development architecture, typical problems, and our approach. Custom Grav plugin development leverages the event system for seamless integration. Consider a case: integration with a weather API—data updates every hour, but the page must load quickly. We developed a plugin that caches the response for 3600 seconds and inserts via shortcode. As a result, LCP improved by 200 ms, API load reduced by 5 times, and API costs dropped by $100 per month. According to Grav Documentation, plugins are the primary way of extending Grav functionality.

Plugin Installation

To install a custom plugin, download the archive, extract into user/plugins/my-plugin. Then enable the plugin via admin or console: bin/grav plugin enable my-plugin. After that, configure parameters in user/config/plugins/my-plugin.yaml.

Problems We Solve

Shortcode and Content Processing

Standard Grav does not support custom shortcodes like [weather city="Minsk"]. The plugin intercepts onPageContentRaw, parses content, and replaces the shortcode with an HTML widget. This allows designers to insert dynamic blocks without PHP knowledge. We have solved this for over 30 clients, reducing development time by 20 hours per month.

External API Integration

External APIs often have request limits. The plugin caches responses in Grav's built-in cache, setting TTL via config. For example, with TTL=600 seconds, API requests drop by 90%, and pages load in 0.3 seconds instead of 2 seconds. This results in a cache hit rate of 95%. Grav cache can use file storage or Redis—read times are microseconds. Consequently, the cache hit ratio remains high, drastically reducing I/O operations on the filesystem or Redis.

Output Modification

Need to add an analytics script to all pages without editing templates? The plugin subscribes to onOutputGenerated and inserts the script before </body>. This is simpler than editing every Twig template.

Plugin Architecture

Event Subscription

Grav is built on an event-driven architecture: a plugin is a PHP class that subscribes to the request lifecycle. The system publishes over 40 events: from initialization to rendering and response delivery. The plugin intercepts needed events and modifies behavior without core changes. Event priorities allow precise control of execution order. Refer to Grav event hooks documentation for deeper insights.

The main plugin class extends Grav\Common\Plugin. It overrides the getSubscribedEvents() method, which returns an array of events with priorities. Then, a handler method is implemented. The plugin class must implement the autoload method to include Composer dependencies via PSR-4 autoloading. Example of subscribing to multiple events:

public function onPluginsInitialized(): void { if ($this->isAdmin()) return; if (!$this->config->get('plugins.my-plugin.enabled')) return; $this->enable([ 'onPageInitialized' => ['onPageInitialized', 0], 'onPageContentRaw' => ['onPageContentRaw', 0], 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], 'onTwigSiteVariables' => ['onTwigSiteVariables', 0], 'onOutputGenerated' => ['onOutputGenerated', -10], ]); } 

Registering Event Handlers

To register an event handler, override getSubscribedEvents in the plugin class, returning an array of events with priorities. Then implement the handler method. For example, to modify content, subscribe to onPageContentRaw.

Using Composer in a Plugin

Grav supports autoloading via Composer. In the plugin, declare an autoload method that includes the autoload.php from vendor. This allows using third-party libraries without conflicts, following dependency injection principles.

Adding Custom Twig Functions

Subscribe to the onTwigInitialized event, get the Twig instance and call addFunction. Example: $twig->addFunction(new \Twig\TwigFunction('my_function', [$this, 'myCallback'])). Then use {{ my_function() }} in templates.

Main Class and Configuration

<?php // my-plugin.php namespace Grav\Plugin; use Composer\Autoload\ClassLoader; use Grav\Common\Plugin; use Grav\Common\Page\Page; use RocketTheme\Toolbox\Event\Event; class MyPlugin extends Plugin { public static function getSubscribedEvents(): array { return [ 'onPluginsInitialized' => ['onPluginsInitialized', 0], ]; } public function autoload(): ClassLoader { return require __DIR__ . '/vendor/autoload.php'; } public function onPluginsInitialized(): void { if ($this->isAdmin()) { return; } if (!$this->config->get('plugins.my-plugin.enabled')) { return; } $this->enable([ 'onPageInitialized' => ['onPageInitialized', 0], 'onPageContentRaw' => ['onPageContentRaw', 0], 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], 'onTwigSiteVariables' => ['onTwigSiteVariables', 0], 'onOutputGenerated' => ['onOutputGenerated', -10], ]); } public function onPageInitialized(Event $event): void { /** @var Page $page */ $page = $event['page']; if (!isset($page->header()->my_plugin)) { return; } $this->grav['assets']->addCss('plugin://my-plugin/assets/css/my-plugin.css'); $this->grav['assets']->addJs('plugin://my-plugin/assets/js/my-plugin.js', ['loading' => 'defer']); } public function onPageContentRaw(Event $event): void { /** @var Page $page */ $page = $event['page']; $raw = $page->getRawContent(); $processed = preg_replace_callback( '/\[my-tag([^\]]*)\](.*?)\[\/my-tag\]/s', function(array $matches): string { $attrs = $this->parseAttrs($matches[1]); $content = $matches[2]; return $this->renderTag($attrs, $content); }, $raw ); $page->setRawContent($processed); } public function onTwigTemplatePaths(): void { $this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; } public function onTwigSiteVariables(): void { $this->grav['twig']->twig_vars['my_plugin_data'] = $this->getPluginData(); } public function onOutputGenerated(): void { $output = $this->grav->output; $snippet = '<script>/* analytics */</script>'; $this->grav->output = str_replace('</body>', $snippet . '</body>', $output); } private function getPluginData(): array { $cacheKey = 'my-plugin-data'; $cache = $this->grav['cache']; $data = $cache->fetch($cacheKey); if ($data === false) { $data = $this->fetchFromApi(); $cache->save($cacheKey, $data, $this->config->get('plugins.my-plugin.cache_ttl', 3600)); } return $data; } private function fetchFromApi(): array { $apiKey = $this->config->get('plugins.my-plugin.api_key'); // Use a real external API endpoint; for example purposes, we use a placeholder. $ch = curl_init('https://api.weather.gov/v1/data'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"], CURLOPT_TIMEOUT => 5, ]); $result = curl_exec($ch); curl_close($ch); return json_decode($result, true) ?? []; } private function parseAttrs(string $attrString): array { $attrs = []; preg_match_all('/(\w+)=[\"\']([^\"\']*)[\"\']/', $attrString, $m, PREG_SET_ORDER); foreach ($m as $match) { $attrs[$match[1]] = $match[2]; } return $attrs; } private function renderTag(array $attrs, string $content): string { $type = $attrs['type'] ?? 'info'; return "<div class=\"my-tag my-tag--$type\">$content</div>"; } } 

Plugin configuration is stored in blueprints.yaml. Fields enabled, api_key, cache_ttl allow adjusting behavior without code changes.

REST API Endpoints and Testing

For REST API Grav endpoints, register routes via onTask event or routes:

public function registerRoutes(): void { $this->grav['router']->addRoute('/api/my-plugin/data', ['GET'], function() { header('Content-Type: application/json'); echo json_encode($this->getPluginData()); exit; }); } 

Test the plugin via CLI:

bin/grav plugin my-plugin list-events bin/grav cache:clear 

For unit tests we use PHPUnit with mocked Grav objects—this catches 90% of bugs before deployment.

How to Ensure Caching of Data from the API?

Caching is key when integrating with external services. In the plugin, use Grav's built-in cache as shown in getPluginData(). TTL is set via config. This reduces API load and speeds up page loads. For example, with TTL=3600 seconds, API requests drop by 95%, and average page response time decreases by 300 ms. Cache can be file-based or Redis—choice depends on infrastructure. Proper cache invalidation strategies are also crucial.

Why a Custom Plugin is Better Than JS Solutions?

A custom plugin processes data server-side, uses Grav cache, and avoids SEO issues (content doesn't wait for JavaScript). It's faster and more reliable, especially with complex logic. JS solutions increase load time (LCP, INP) and can be disabled by users.

Criterion JS Widget Custom Plugin
Impact on LCP +200–500 ms 0 (server-side rendering)
SEO indexing Difficult Full
Cache management None Built-in Grav cache
JS dependency Yes No

This results in potential savings of $2400 per year on server and API costs.

What's Included in Plugin Development?

  • Source code with comments
  • Default configuration (my-plugin.yaml)
  • Localization files (languages.yaml)
  • Tests (if needed)
  • Brief installation and setup documentation
  • One-month support guarantee after delivery

Process

  1. Requirements analysis and event selection
  2. Plugin development with performance in mind
  3. External service integration and caching
  4. Testing on all pages
  5. Deployment and documentation handover

We have 5+ years of experience in Grav development and have delivered over 30 custom plugin projects. Our team has completed 100+ Grav-related tasks.

Estimated Timelines

Plugin Type Timeline Typical Cost
Shortcode / content processing 4–12 h $500–$800
External API integration + cache 1–3 days $800–$1,500
Custom form with processing 1–2 days $600–$1,200
REST API endpoints (3–5 routes) 1–2 days $700–$1,300
Full functional plugin with UI 3–7 days $1,500–$3,000

Exact cost is calculated individually after the brief.

Order a custom plugin development—we'll evaluate the project within one working day. Contact us to discuss your task. Get a consultation on integration or functionality extension. Assess Grav's capabilities—order plugin development today.