Skip to content

Grav Plugin Admin — Custom Admin Pages, Widgets and Panels

DodaTech Updated 2026-06-27 7 min read

In this tutorial, you'll learn Grav plugin admin development — creating custom admin pages, dashboard widgets, admin panels, extending the Admin plugin, and building admin UI components with Twig and JavaScript.

What You'll Learn

  • The Admin plugin architecture and extension points
  • Creating custom admin pages with routes
  • Adding dashboard widgets and panels
  • Admin template overrides and custom CSS
  • Admin form fields and configuration
  • Security considerations for admin extensions

Why It Matters

In WordPress, admin pages use add_menu_page() and the Settings API. In Grav, the Admin plugin provides a structured way to extend the admin interface. You can add custom pages for reporting, configuration, data management, or any backend functionality. Custom admin pages give your plugin a professional interface without building a separate admin panel.

Real-World Use

A statistics plugin tracks page views and downloads. It adds a dashboard widget showing today's traffic summary and a full admin page with charts, export functionality, and per-page analytics. The admin page is built with Twig templates and uses Grav's existing admin UI components — consistent look and feel without reinventing the design.

Learning Path

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

Admin Plugin Extension Points

The Admin plugin provides several ways to extend the admin interface:

  1. Custom admin pages — Full pages under /admin/plugin-name
  2. Dashboard widgets — Small panels on the admin dashboard
  3. Admin menu items — Navigation entries
  4. Admin tools — Utility pages in the Tools section
  5. Page form fields — Custom fields in page editing
  6. Admin templates — Override admin template blocks

Creating a Custom Admin Page

Step 1: Create the Controller

user/plugins/myadmin/myadmin.php:

<?php
namespace Grav\Plugin;

use Grav\Common\Plugin;
use Grav\Plugin\Admin\AdminController;

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

    public function onPluginsInitialized()
    {
        if (!$this->isAdmin()) {
            return;
        }

        $this->enable([
            'onAdminMenu' => ['onAdminMenu', 0],
            'onAdminTwigTemplatePaths' => ['onAdminTwigTemplatePaths', 0],
            'onAdminPage' => ['onAdminPage', 0],
        ]);
    }

    public function onAdminMenu()
    {
        $this->grav['admin']->addMenuEntry([
            'label' => 'My Admin',
            'location' => 'my-admin',
            'icon' => 'fa-plug',
            'permissions' => 'admin.pages',
        ]);
    }

    public function onAdminTwigTemplatePaths()
    {
        $this->grav['twig']->twig_paths[] = __DIR__ . '/admin/templates';
    }

    public function onAdminPage()
    {
        $route = $this->grav['admin']->getCurrentRoute();

        if ($route === '/my-admin') {
            $this->grav['admin']->setPageTitle('My Admin Page');

            echo $this->grav['twig']->template('admin/my-admin.html.twig', [
                'plugin_data' => $this->getPluginData(),
            ]);
            exit;
        }
    }

    private function getPluginData()
    {
        return [
            'version' => '1.0.0',
            'stats' => [
                'total_pages' => count($this->grav['pages']->all()),
                'total_users' => count($this->grav['accounts']->all()),
            ],
        ];
    }
}

Step 2: Create the Admin Template

user/plugins/myadmin/admin/templates/admin/my-admin.html.twig:

{% extends 'partials/base.html.twig' %}

{% block content %}
<div class="admin-block">
    <h1>{{ admin_title ?: 'My Admin Page' }}</h1>

    <div class="admin-grid">
        <div class="card">
            <h3>Site Stats</h3>
            <ul>
                <li>Version: {{ plugin_data.version }}</li>
                <li>Total pages: {{ plugin_data.stats.total_pages }}</li>
                <li>Total users: {{ plugin_data.stats.total_users }}</li>
            </ul>
        </div>

        <div class="card">
            <h3>Quick Actions</h3>
            <ul class="admin-actions">
                <li><a href="{{ admin_route('/pages') }}" class="button">Manage Pages</a></li>
                <li><a href="{{ admin_route('/plugins') }}" class="button">Manage Plugins</a></li>
                <li><a href="{{ admin_route('/users') }}" class="button">Manage Users</a></li>
            </ul>
        </div>
    </div>
</div>
{% endblock %}

Dashboard Widgets

Create widgets that appear on the admin dashboard:

public function onAdminDashboard()
{
    $dashboard = $this->grav['admin']->getDashboard();

    $dashboard->addWidget([
        'name' => 'my_stats',
        'title' => 'Site Statistics',
        'position' => 'right',
        'content' => $this->grav['twig']->template(
            'admin/widgets/stats.html.twig',
            [
                'total_pages' => count($this->grav['pages']->all()),
                'total_users' => count($this->grav['accounts']->all()),
                'recent_pages' => $this->getRecentPages(5),
            ]
        ),
    ]);
}

private function getRecentPages($count)
{
    $pages = $this->grav['pages']->all();
    usort($pages, function($a, $b) {
        return $b->modified() - $a->modified();
    });
    return array_slice($pages, 0, $count);
}

Widget template admin/widgets/stats.html.twig:

<div class="dashboard-widget">
    <div class="stats-grid">
        <div class="stat-item">
            <span class="stat-value">{{ total_pages }}</span>
            <span class="stat-label">Pages</span>
        </div>
        <div class="stat-item">
            <span class="stat-value">{{ total_users }}</span>
            <span class="stat-label">Users</span>
        </div>
    </div>
    <h4>Recently Modified</h4>
    <ul class="recent-list">
        {% for page in recent_pages %}
        <li>
            <a href="{{ admin_route('/pages/' ~ page.route) }}">{{ page.title }}</a>
            <span class="hint">{{ page.modified|nicetime }}</span>
        </li>
        {% endfor %}
    </ul>
</div>

Admin Form Fields

Add custom fields to page editing forms through blueprints:

# user/plugins/myplugin/blueprints.yaml
form:
    fields:
        tabs:
            type: tabs
            fields:
                custom:
                    type: tab
                    title: Custom Fields
                    fields:
                        header.custom_field:
                            type: text
                            label: Custom Field
                            size: large

                        header.enable_feature:
                            type: toggle
                            label: Enable Feature
                            default: 0

Admin Twig Extensions

Add custom Twig functions available in admin templates:

public function onTwigExtensions()
{
    require_once __DIR__ . '/classes/AdminTwigExtension.php';
    $this->grav['twig']->twig->addExtension(
        new AdminTwigExtension()
    );
}
<?php
// classes/AdminTwigExtension.php
namespace Grav\Plugin\MyPlugin;

use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

class AdminTwigExtension extends AbstractExtension
{
    public function getFunctions()
    {
        return [
            new TwigFunction('admin_stats', [$this, 'getAdminStats']),
        ];
    }

    public function getAdminStats()
    {
        return [
            'php_version' => phpversion(),
            'grav_version' => GRAV_VERSION,
            'memory_usage' => memory_get_usage(true),
        ];
    }
}

Admin CSS and JS

Add custom styles or scripts to admin pages:

public function onAdminPage()
{
    $this->grav['assets']->addCss('plugin://myadmin/admin/css/admin.css');
    $this->grav['assets']->addJs('plugin://myadmin/admin/js/admin.js');
}

Security in Admin Pages

public function onAdminPage()
{
    // Check permissions
    $user = $this->grav['user'];
    if (!$user->authorize('admin.super')) {
        throw new \RuntimeException('Access denied', 403);
    }

    // CSRF protection
    $nonce = $this->grav['admin']->getNonce();
    // Validate nonce on POST requests

    // Sanitize output
    $data = $this->getData();
    array_walk_recursive($data, function(&$value) {
        $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
    });
}

Learning Path

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

Common Mistakes

  1. Not checking isAdmin() before registering admin events: Admin-specific events should only be registered when $this->isAdmin() is true. Registering them on frontend requests causes errors.

  2. Missing permission checks: Any admin page must check that the current user has the appropriate permission. Without this, any admin user can access any page.

  3. Hardcoding admin routes: Use admin_route('/path') instead of hardcoded paths like /admin/path. This ensures compatibility if the admin path prefix changes.

  4. Not following Admin UI conventions: Use Grav's existing CSS classes and layout patterns. Custom styles that break the admin design system confuse users.

  5. Forgetting CSRF protection: POST requests to admin pages must include a nonce for CSRF protection. Without it, the page is vulnerable to cross-site request forgery.

Practice Questions

  1. What method adds a navigation entry to the admin menu? Answer: $this->grav['admin']->addMenuEntry([...]) with parameters for label, location, icon, and permissions.

  2. How do you add a widget to the admin dashboard? Answer: Subscribe to onAdminDashboard, get the dashboard object, and call $dashboard->addWidget([...]) with name, title, position, and content.

  3. How do you check if the current user can access an admin page? Answer: Use $this->grav['user']->authorize('admin.pages') or the specific permission required. Throw a 403 exception if unauthorized.

  4. What is the purpose of admin_route() in admin templates? Answer: It generates the correct URL for admin pages. admin_route('/pages') outputs /admin/pages or whatever the configured admin prefix is.

  5. Challenge: Build a complete admin plugin that adds a custom page for managing site-wide announcements. Include: an admin menu entry under "Tools", a custom admin page with a form to create/edit/delete announcements, a dashboard widget showing the latest 3 announcements, permission checks (only admin.super users can manage), a Twig extension for displaying announcements on the frontend, and proper CSRF protection on all form submissions.

FAQ

How do I add a custom admin page under a specific section?

Use addMenuEntry() with the location parameter. For example, 'location' => 'tools' adds the page under the Tools section. Available sections: pages, themes, plugins, users, tools, settings.

Can I use Vue.js or React in admin pages?

The Admin plugin uses jQuery and its own UI components. You can add Vue.js or React, but be aware of conflicts with existing JavaScript. Test thoroughly before deploying.

How do I add a button to the page list toolbar?

Use onAdminPage to check the current route. If on the pages list, add a button via JavaScript injection or by overriding the admin template.

Can I create admin pages that work without the Admin plugin?

No. Custom admin pages require the Admin plugin. If your plugin needs its own admin interface without Admin, build it as a frontend page with admin-style templates.

How do I add custom action buttons to the page editor?

Create a plugin that subscribes to onAdminPage and modifies the page editor template. Add buttons using JavaScript and handle the actions via AJAX to your plugin's controller.

Mini Project

Goal: Build a comprehensive admin plugin with multiple features.

  1. Create a plugin with a custom admin page for system health monitoring
  2. Add a dashboard widget showing: server status, recent errors, cache size
  3. Create admin menu entries under Tools and Settings sections
  4. Implement permission checks for each admin page
  5. Add a custom admin form for plugin configuration
  6. Create admin CSS that matches the existing admin design
  7. Add Twig extensions for admin templates
  8. Implement nonce-based CSRF protection
  9. Add AJAX endpoints for admin actions
  10. Test the plugin with different user roles and permissions

What's Next

Now you can build admin interfaces. Next, learn CLI commands for plugins:

Continue to Lesson 28: Plugin CLI — Console commands, task scheduling, and cron integration.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro