Yeti Search Pro isn't a settings page. Open it in Admin Next and you get a real search dashboard: index management, document counts, a live search tester, analytics, all in one place. SEO Magic is the same idea pointed at SEO: a crawl report, per-page scores, a re-crawl button, CSV export. These are mini-applications living inside the admin, not config forms with a fancy coat of paint.
That's what this post is about. The custom fields post covered dropping a single web component into a form. This is the next step up: giving your plugin a top-level entry in the sidebar and a whole page of its own to do with as you like. It's the most involved Admin Next integration, and also the one that lets your plugin feel like a first-class part of the admin instead of a tab buried in Plugins.
There are two pieces to it: an item in the sidebar, and the page that item opens. Let's take them in that order.
The Sidebar Entry
You add yourself to the sidebar by handling the onApiSidebarItems event. Admin Next asks the API for its sidebar list on load, the API fires this event, and every plugin that wants a spot appends one.
public static function getSubscribedEvents(): array
{
return [
'onApiSidebarItems' => ['onApiSidebarItems', 0],
'onApiPluginPageInfo' => ['onApiPluginPageInfo', 0],
];
}
public function onApiSidebarItems(Event $event): void
{
// Don't show the entry if the plugin is switched off
if (!$this->config->get('plugins.my-plugin.enabled', true)) {
return;
}
$items = $event['items'] ?? [];
$items[] = [
'id' => 'my-plugin',
'plugin' => 'my-plugin',
'label' => 'My Plugin',
'icon' => 'fa-gauge', // Font Awesome, full class with the fa- prefix
'route' => '/plugin/my-plugin', // the Admin Next route this opens
'priority' => 5, // higher sorts earlier
'authorize' => 'api.my-plugin.view', // who's allowed to see it
];
$event['items'] = $items;
}
A few things to know, because some of them used to work differently in classic admin:
- The route is always
/plugin/{slug}. That's the Admin Next URL your page lives at. Stick to the convention. - Icons are Font Awesome with the
fa-prefix. Admin Next doesn't add the prefix for you, so it'sfa-gauge, notgauge. authorizecontrols visibility, and it's checked on the server. Pass a permission string, or an array of strings for an any-of test. The API filters the list before it ever reaches the browser, and it stripsauthorizeout of the response, so the client never even learns the entry existed. You can also pass abadgefor a little count or status pill next to the label.- The sidebar is a flat list. There's no nesting, no parent/child trees. Items are sorted by
priorityand that's it. If you have several related screens, you put one entry in the sidebar and switch between the screens inside your page (more on that below).
That's the whole sidebar story. Now the page it opens.
The Page: Two Ways to Build It
When someone clicks your sidebar entry and lands on /plugin/my-plugin, Admin Next asks the API for the page definition (GET /gpm/plugins/{slug}/page). You provide that definition by handling onApiPluginPageInfo. The one decision that shapes everything else is the page_type, and there are exactly two: blueprint or component.
Blueprint mode hands Admin Next a Grav blueprint and lets it render the form. You write no JavaScript. This is the right call when your page is really a data screen: settings, a key-value editor, a structured config panel.
Component mode hands Admin Next a web component and gets out of your way. You get an empty container in the content area and you build whatever you want inside it: dashboards, tables, charts, a search tester. Yeti Search Pro and SEO Magic are both component-mode pages.
Pick one. A page is entirely one or entirely the other. There's no hybrid page where some parts are blueprint-rendered and some are custom, so don't go looking for one. I'll come back to how you get the best of both, because there's a clean answer that isn't a hybrid renderer.
Blueprint Mode
public function onApiPluginPageInfo(Event $event): void
{
// Guard clause: only answer for your own plugin
if ($event['plugin'] !== 'my-plugin') {
return;
}
$event['definition'] = [
'id' => 'my-plugin',
'plugin' => 'my-plugin',
'title' => 'My Plugin',
'icon' => 'fa-gauge',
'page_type' => 'blueprint',
'blueprint' => 'my-plugin', // the blueprint to render
'data_endpoint' => '/my-plugin/data', // GET: returns the current form values
'save_endpoint' => '/my-plugin/save', // PATCH: receives the edited values
'actions' => [
['id' => 'save', 'label' => 'Save', 'icon' => 'fa-check', 'primary' => true],
],
];
}
Admin Next fetches the blueprint, calls data_endpoint to fill it in, renders every field with the same native components the page editor uses, and saves back to save_endpoint. Your custom field types work here too, exactly as they did in the previous post. If the user has auto-save turned on, edits get debounced straight to the save endpoint with no Save click needed.
You don't get any of that for free in classic admin, and you write zero front-end code to get it here. The endpoints themselves are ordinary API routes from the API integration post.
Component Mode
When a blueprint can't express what you need, switch to a component. The definition is shorter, because Admin Next isn't rendering anything for you:
$event['definition'] = [
'id' => 'my-plugin',
'plugin' => 'my-plugin',
'title' => 'My Plugin',
'icon' => 'fa-gauge',
'page_type' => 'component',
];
The component itself goes in admin-next/pages/my-plugin.js, one file named after your slug. Admin Next loads it on demand (GET /gpm/plugins/{slug}/page-script), hands it a tag name, and mounts it. It's the same web-component contract you already know from custom fields, minus the field and value properties, because a page isn't bound to a single form value. It fetches its own data and owns its own state.
const TAG = window.__GRAV_PAGE_TAG; // e.g. "grav-my-plugin--page"
function apiUrl(path) {
return (window.__GRAV_API_SERVER_URL || '') +
(window.__GRAV_API_PREFIX || '/api/v1') + path;
}
function apiHeaders() {
const headers = {};
if (window.__GRAV_API_TOKEN) headers['X-API-Token'] = window.__GRAV_API_TOKEN;
return headers;
}
class MyPluginPage extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' });
this._render();
this._loadData();
}
async _loadData() {
const resp = await fetch(apiUrl('/my-plugin/stats'), { headers: apiHeaders() });
const json = await resp.json();
this._stats = json.data || json;
this._render();
}
_render() {
this.shadowRoot.innerHTML = `
<style>
/* Read Admin Next's theme tokens so you match light/dark automatically */
:host { display: block; color: var(--foreground); }
</style>
<div class="dashboard">...</div>
`;
}
}
customElements.define(TAG, MyPluginPage);
The same injected globals you get in a field component are here too: window.__GRAV_API_SERVER_URL, __GRAV_API_PREFIX, and __GRAV_API_TOKEN for talking to the API, plus __GRAV_DIALOGS for confirmations and __GRAV_NAVIGATE for moving around the admin. Send your token as X-API-Token, not Authorization: Bearer, for the same FastCGI reason covered in the last post.
Header Actions
Both modes get a row of action buttons in the page header, declared in the actions array. This is where Save, Export, Re-crawl, and friends live. SEO Magic's real definition is a good tour of what's possible:
'actions' => [
[
'id' => 'recrawl',
'label' => 'Re-crawl',
'icon' => 'fa-refresh',
'children' => [ // a split button with a dropdown
['id' => 'recrawl-changed', 'label' => 'Re-crawl (changed only)', 'icon' => 'fa-refresh'],
],
],
['id' => 'export-csv', 'label' => 'Export CSV', 'icon' => 'fa-download'], // triggers a download
['id' => 'options', 'label' => 'Options', 'icon' => 'fa-cog', 'navigate' => '/plugins/seo-magic'],
],
The keys you'll reach for most: primary highlights a button (your Save), endpoint plus method makes the button call an API route directly, download and upload turn it into a file download or upload, confirm puts a confirmation in front of it, children makes it a split button with a dropdown, and navigate sends the user to another admin route.
That last one is the trick I promised. When an action has no endpoint, Admin Next fires a page-action event at your component instead, so you handle it in JavaScript:
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.addEventListener('page-action', (e) => {
if (e.detail.id === 'export-csv') this._exportCsv();
});
this._render();
this._loadData();
}
What About Tabs?
The outline I started from had a whole section on tabbed pages, with some tabs being blueprints and some being custom components in the same page. I'll be straight with you: that hybrid doesn't exist. There's no tabs array in the page definition, and no mode where Admin Next stitches a blueprint tab next to a component tab.
What you actually have is two honest options.
If you're in blueprint mode, tabs come for free from the blueprint itself. Grav blueprints have always supported tab sections, and Admin Next renders them as tabs just like the page editor does. Group your fields into tabs in the YAML and you're done.
If you're in component mode, the tabs are yours to build. That sounds like more work than it is. Both Yeti Search Pro and SEO Magic build their own tab strips in plain JavaScript inside the page component, swapping the content area when you click. You're rendering a few buttons and a content div, not adopting a framework.
So how do Yeti Search Pro and SEO Magic give you a rich dashboard and a normal settings form without a hybrid page? They don't try. The dashboard is a component page, and the plain settings stay where settings have always lived, on the regular plugin configuration page. The link between them is that options action up in the header, the one with navigate: '/plugins/seo-magic'. Click it and you jump to the standard config form. Two purpose-built screens, each rendered the way it's best rendered, one button connecting them. That's the pattern to copy.
The Filesystem Shortcut
If your page definition is static, you can skip the PHP event entirely. Drop a file in admin-next/pages/ and the API infers the rest:
admin-next/pages/my-plugin.yamlgives you a blueprint-mode page (the YAML is the definition).admin-next/pages/my-plugin.jsgives you a component-mode page, with the title auto-generated from the slug.
You'll still want the event when the definition depends on config or the current user (gating the page, building actions conditionally), since the event handler receives $event['user']. But for a simple dashboard, the bare .js file is all it takes.
Reference Implementations
Both of these are component-mode pages you can read top to bottom:
- Yeti Search Pro registers a
fa-searchsidebar entry gated onapi.yetisearch-pro.view, and a component page that builds its own tabs for index management, the search tester, and analytics. Its single header action is anoptionsbutton that navigates to the plugin config page. A good model for a data-heavy dashboard. - SEO Magic registers a
fa-chart-lineentry and a component page with the split-button, download, and navigate actions shown above. It also ships custom field types (inadmin-next/fields/) that appear inside its config blueprint, so it's a nice example of one plugin using several Admin Next integration points together.
If you have either installed, the page components live at admin-next/pages/{slug}.js in each plugin. Read them next to this post.
A Few Things Worth Getting Right
- Gate the page the same way you gate the sidebar. A hidden sidebar entry doesn't stop someone typing the route. Check the permission in
onApiPluginPageInfotoo, and remember that in API context you read permissions with$user->get('access.api.my-plugin.view'), not$user->authorize(). - Read the theme tokens. Use
var(--foreground),var(--background),var(--border),var(--primary)and the rest so your page follows light and dark mode without you maintaining two palettes. - Handle loading and error states. Your component fetches its own data, so render a sensible empty state first and don't assume the fetch succeeded.
- Keep it responsive. Admin Next runs down to phone widths and so should your page. Tables especially need a plan for narrow screens.
- Use
window.__GRAV_DIALOGS.confirm()for anything destructive, never the nativeconfirm(). It matches the rest of the admin and handles focus and keyboards properly.
Quick Links and Help
- Learn site reference: Custom Admin Pages.
- Previous post in the series: Admin Next Custom Form Fields.
- The API endpoints your pages talk to: API Integration for Plugins.
- Discord: chat.getgrav.org if something isn't rendering the way you expect.
A custom page is the moment your plugin stops feeling like a guest in the admin and starts feeling like part of it. Pick the mode that fits the screen, lean on blueprint mode whenever a form will do, and reach for component mode when you genuinely need the canvas. The two reference plugins between them cover almost everything you'll run into.
Andy
Next in the developer series: Grav 2.0 Developer Guide: Admin Next Panels & Menu Bar