Grav Plugin Admin — Custom Admin Pages, Widgets and Panels
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:
- Custom admin pages — Full pages under
/admin/plugin-name - Dashboard widgets — Small panels on the admin dashboard
- Admin menu items — Navigation entries
- Admin tools — Utility pages in the Tools section
- Page form fields — Custom fields in page editing
- 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
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.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.
Hardcoding admin routes: Use
admin_route('/path')instead of hardcoded paths like/admin/path. This ensures compatibility if the admin path prefix changes.Not following Admin UI conventions: Use Grav's existing CSS classes and layout patterns. Custom styles that break the admin design system confuse users.
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
What method adds a navigation entry to the admin menu? Answer:
$this->grav['admin']->addMenuEntry([...])with parameters for label, location, icon, and permissions.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.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.What is the purpose of
admin_route()in admin templates? Answer: It generates the correct URL for admin pages.admin_route('/pages')outputs/admin/pagesor whatever the configured admin prefix is.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
Mini Project
Goal: Build a comprehensive admin plugin with multiple features.
- Create a plugin with a custom admin page for system health monitoring
- Add a dashboard widget showing: server status, recent errors, cache size
- Create admin menu entries under Tools and Settings sections
- Implement permission checks for each admin page
- Add a custom admin form for plugin configuration
- Create admin CSS that matches the existing admin design
- Add Twig extensions for admin templates
- Implement nonce-based CSRF protection
- Add AJAX endpoints for admin actions
- 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