#Install
composer require 4rn0/statamic-cp-barphp artisan statamic:static:clear
Clearing the static cache once makes sure pages cached before the install get the bar too. Then tick View CP Bar on the roles that should have it. Super users have it already. The bar shows for a user from their next login, or after they've opened the control panel once.
The bar's scripts are published to public/vendor/statamic-cp-bar by Composer. If your deploy skips Composer's scripts, publish them yourself:
php artisan vendor:publish --tag=statamic-cp-bar --force
#Who sees it
The bar shows for users with Access the Control Panel and View CP Bar. You find View CP Bar under Control Panel in the role editor. Each item then checks the permissions of the user:
| Item | Shown when the user may |
|---|---|
| New › an entry | create entries in that collection on this site |
| New › a term | create terms in that taxonomy on this site |
| New › User | create users |
| Edit entry, Edit term | edit this entry or term |
| Forms › a form | view that form's submissions |
| Cache | use the Cache utility |
| SEO | view SEO reports, or edit section or site defaults (SEO Pro) |
| Create redirect | create redirects (SEO Pro or Redirect) |
| New › a resource, Edit resource | create or edit that resource's models (Runway) |
What a user may not use is left out, not greyed out.
Every user can switch the bar off for themselves with Show CP Bar in their preferences. As with any preference, you can set it for a role or for everyone.
Roles, multisite and impersonation are Statamic Pro features. On Solo your one user is a super user and gets the whole bar.
#What's in the bar
Your site's name links to the dashboard.
New has one item per blueprint, named after it: Article, Page. The collection of the current page comes first, and clicking New itself opens that one. Taxonomies follow the collections, then User. Hidden blueprints are skipped.
Edit entry or Edit term opens what's on screen in the control panel. On a page that is neither, such as a custom route, search results or a 404, it isn't there. An entry that isn't published says why next to it: Draft, Scheduled or Expired.
Sites appears on a multisite and lists the other sites this entry or term is published on, each linking to the page there.
Forms appears when the entry or term has a form field, also inside Replicator, Bard, Grid or Group. Each form shows its number of submissions and links to them. Forms that are only in the template, like a newsletter form in the footer, aren't found.
Cache appears when static caching is on. The dot is green when this page is in the cache, and grey when it is not. With full measure, the menu also tells how long ago the page was stored. Refresh this page removes this page from the cache, query string variants included, and reloads it. To clear the whole cache, use the control panel's Cache utility.
Your avatar opens your name and email, Edit profile, Preferences and Log out. While impersonating, the bar turns amber and the menu has Stop impersonating. Someone without access to the control panel has no bar, so impersonating them hides it until you stop.
With the keyboard, Tab moves from item to item along the bar. Enter, Space or ↓ opens a menu and goes into it. ↑ and ↓ move through the menu. → and ← open and close a menu in a menu. Escape closes the menu. A menu also opens when its item has focus, so you see what's in it.
In the control panel, View Site opens in the same tab while you have the bar: the bar is the way back.
#With SEO Pro, Redirect or Runway
Installed, they get items of their own.
SEO Pro adds SEO to entries and terms. The dot shows the result of the page in the latest SEO report: green, amber or red. The menu lists what to fix. It links to the report and to the section and site defaults. It follows the report, so it changes when a new report is generated, not when you save.
SEO Pro or Redirect adds Create redirect on a 404, with the address filled in as the source. If the addon logs 404s, a badge shows how often the address was hit. With both installed, Redirect wins. SEO Pro has redirects from version 7.7.
Runway adds its resources to New, and Edit Product (or whatever the resource is called) on the page of a routed model. Hidden and read-only resources are left out.
Tested with SEO Pro 7.14, Redirect 4.2 and Runway 9.7.
#The look
The bar looks like the control panel's header, in the theme color each user picked in their preferences. It uses the language of the user's control panel. It uses the text direction of the control panel, not of the page. On a right-to-left site, the bar stays left to right for an English control panel. In high contrast mode it takes the system's colors.
#Your layout
When the bar shows, <html> gets the class cp-bar, a top margin to make room, and the custom property --cp-bar-height. It also gets a scroll-padding-top of that height, so a link to an anchor stops below the bar. If your site has a sticky header, set scroll-padding-top on html.cp-bar to the two heights together. In print the bar is left out. A header with position: fixed or sticky needs to move down:
.cp-bar .site-header { top: var(--cp-bar-height); }
The bar sits at z-index: 99999, so a dialog of your own with a higher one goes over it. A full-screen overlay below that needs to start under the bar, like the header above.
The bar lives in a shadow DOM, so your site's CSS doesn't reach it and its CSS doesn't reach your site. To style it on purpose, see Styling.
#Placement
The bar is added to every HTML page Statamic renders, 404s included. To place it yourself, publish the config and set inject to false:
php artisan vendor:publish --tag=statamic-cp-bar-config
// config/statamic-cp-bar.phpreturn [ 'enabled' => env('STATAMIC_CP_BAR', true), 'inject' => false,];
Then put the tag in your layout, {{ cp_bar }} in Antlers or @cpBar in Blade. Laravel routes that don't go through Statamic need the tag too. Placed by hand, the page shifts down once when the bar arrives.
Some sites swap the body instead of loading the next page, with Turbo, Livewire's wire:navigate or htmx. These sites get the bar of each new page too.
#Static caching
The page itself contains only an empty placeholder, a few lines of CSS and a script that checks for a cookie. That is about 1 KB, the same for every visitor. So the page is safe to cache with half or full measure. The rest is in files only a browser with that cookie asks for. The script fetches the bar from /!/statamic-cp-bar, a request that is never cached. Cached pages keep that markup after an update. They get the rest from files that update with the addon. So you do not need to clear the cache after an update. A release that does change the markup says so in the changelog.
Only a browser that has been in the control panel sends that request. The control panel sets a statamic_cp_bar cookie for users who may see the bar. The cookie holds nothing more than that fact. Logging out removes it. Visitors never make it. In those browsers, the request starts from the <head>, before the browser reads the rest of the page. localStorage keeps how the last bar looked: its height and color, and the items that are on every page (your site's name, New and an empty avatar). The bar shows those immediately. The items of the page itself follow when the request returns. They hold no name or picture, so whoever uses the browser next sees nothing of yours, and logging out removes them.
Does your Content Security Policy forbid inline scripts? Then set Statamic's script_delivery to external in config/statamic/static_caching.php (Statamic 6.34 or later). The bar then loads its script as a file. It then needs no inline scripts, inline styles or data: images. Every visitor loads that file, 1.7 KB, once. Without the CSS in the head, the page can move down once when the bar arrives.
#Switching it off
| How | What it does |
|---|---|
STATAMIC_CP_BAR=false |
No bar anywhere |
?cp-bar=off in the address |
No bar on that page |
X-CP-Bar: off request header |
No bar, on pages from the static cache too, and the response isn't cached |
The last two are for end-to-end tests and screenshot tools that run while logged in. The bar never shows in Live Preview, inside an iframe, or in pages generated on the command line, such as with statamic/ssg.
#Extending CP Bar
The bar is a list of items. Each item has an id and a parent, as in the WordPress admin bar. Your code adds, changes and removes items. A site does this in its own service provider. An addon does this in its service provider too, and it does not need CP Bar to work.
#From an addon
Put CP Bar in suggest in your composer.json, not in require. You can also put it in require-dev, for your tests and your static analysis.
"suggest": { "4rn0/statamic-cp-bar": "Shows broken links in CP Bar."}
Add your items in bootAddon(). Check for CP Bar first. On a site without CP Bar, class_exists() returns false and your addon skips the rest. The use line does not load a class, so it is safe without CP Bar.
use Arnohoogma\StatamicCpBar\Facades\CpBar; public function bootAddon(){ if (! class_exists(CpBar::class)) { return; } CpBar::extend(function ($bar, $context) { $bar->add([ 'id' => 'broken-links', 'title' => __('Broken links'), 'href' => cp_route('broken-links.index'), ]); }); }
The README has a complete service provider. Follow these rules:
- Call
CpBar::extend()inbootAddon(). Do not call it inregister(). There, the facade gets an object that the bar does not use, and your items do not show. - Call
CpBar::extend()one time. Do not call it for each request. With Octane, each call adds one more callback. - Start your ids with the handle of your addon:
broken-links-report, notreport. Then your ids do not collide with the ids of other addons. - Give your items a
canwith a permission of your addon. Then only users who can use your page see the item.
#From a site
Use the boot() method of App\Providers\AppServiceProvider:
use Arnohoogma\StatamicCpBar\Facades\CpBar; CpBar::extend(function ($bar, $context) { $bar->add(['id' => 'edit', 'title' => 'Bewerken']);}); CpBar::remove('new-content');
A site runs before the addons. To change an item of an addon, put your call in Statamic::booted():
Statamic::booted(fn () => CpBar::extend(fn ($bar) => $bar->add(['id' => 'broken-links', 'priority' => 20])));
#Methods
| Method | What it does |
|---|---|
CpBar::extend($callback) |
Adds a callback. CP Bar calls it with $bar and $context each time it builds the bar. The callback is a closure, an invokable object or the name of an invokable class. CP Bar gets a class from the container. |
CpBar::add($item) |
Adds an item for all users on all pages. It cannot change a core item: CP Bar adds the core items later, when it builds the bar. Use $bar->add() in a callback for that. |
CpBar::remove($id) |
Removes an item and all items under it. This works for core items too. |
CpBar::css($css) |
Adds CSS to the bar. See Styling. |
$bar->add(), $bar->remove(), $bar->css() |
The same, in a callback, for this user on this page. |
CP Bar runs the callbacks when a user opens the bar, not when it renders a page. A callback can use the user and the page, and it costs visitors nothing. CP Bar runs the callbacks in the language of the user's control panel, so __() translates.
CP Bar runs the callbacks in this order:
- The core items of CP Bar.
- The site, from its
boot(). - The addons, from their
bootAddon(), in the order that Statamic boots them.
An add() with an id that exists changes that item. A later change wins. A remove() wins over each add().
#Context
A callback gets $context. Read its properties. Do not change them.
| Property | |
|---|---|
user |
The logged-in user. |
url |
The full address of the page, with the query string. |
site |
The site of that address. |
page |
The entry or term at that address, or null. |
notFound |
true on a 404 page. |
#Item types
- Link. An item with an
href. - Button. An item with an
action. A click runs code on the server. See Actions. - Menu. An item with items under it. Put an item in a menu with
parent. A menu opens under the bar. - Submenu. A menu in a menu. It opens next to its item.
- Line of text. An item with
meta.text. It has no link. It can have asubtitle. - Status dot. Any item with
meta.dot. Always addmeta.statustoo: it tells screen readers what the dot means.
An item shows only if it has an href, an action, items under it, or meta.text. A menu with nothing left in it goes.
$bar->add(['id' => 'acme-tools', 'title' => 'Tools', 'href' => cp_route('utilities.index')]);$bar->add(['id' => 'acme-seo', 'parent' => 'acme-tools', 'title' => 'SEO']);$bar->add(['id' => 'acme-seo-check', 'parent' => 'acme-seo', 'title' => 'Check this page', 'action' => SeoCheck::class]);
#Properties
| Key | Type | Default | |
|---|---|---|---|
id |
string | Required. Unique. top-secondary is not allowed. |
|
title |
string | Required for a new item. A change to an item can leave it out. Plain text. | |
parent |
string | null |
null for the left side, top-secondary for the right side, or the id of the menu. |
href |
string | null |
The address of the link. A javascript: address is not allowed. |
action |
closure, object or class | null |
Code that runs on the server. See Actions. |
confirm |
string | null |
A question. The user confirms it before the action runs. |
icon |
string | null |
An SVG or an <img>. CP Bar inserts it as HTML. Escape all text that you did not write with e(). |
priority |
int or float | 100 |
The order. The lowest comes first. |
before, after |
string | null |
The id of an item in the same menu. The item goes next to it. |
can |
string or closure | null |
A permission, or a closure that gets $context and returns true or false. |
meta |
array | [] |
See below. A value is text, a number, true, false or null. |
CP Bar ignores keys that it does not know. A string can also be a Stringable, such as an HtmlString.
meta key |
|
|---|---|
target, rel |
The target and rel of a link. |
badge |
A number or a short text after the title. It does not show at 0. |
dot |
A CSS color for a status dot. |
status |
The meaning of the dot, in words, for screen readers. |
separator |
true puts a line above the item. |
text |
true makes the item a line of text. |
subtitle |
A second line under a line of text. |
initials |
An avatar with these letters. |
An add() with an existing id merges meta. A badge on a core item keeps the dot of that item.
#Visibility and permissions
- The bar shows for users with Access the Control Panel and View CP Bar, and with Show CP Bar on in their preferences.
- An item with a
canshows only to users with that permission. A closure gets$contextand returnstrueorfalse. - An item under a hidden item is hidden too. The
canof a menu protects all items in it. - If a
canclosure throws an exception, CP Bar hides the item and logs a warning. - CP Bar builds the bar for each user and each page. Two users can see a different bar on the same page.
#Actions
An item with an action is a button. The action gets $context:
'action' => function ($context) { BrokenLinks::check($context->page); return ['message' => __('Checked.')];},
When the user clicks the button, CP Bar sends a POST request with the address of the page and a CSRF token. CP Bar then builds the bar again, for this user and this page. It runs the action only if the user can see the item there. The can of the item and of each menu above it protect the action.
An action returns null or an array with one of these keys:
| Key | What the bar does |
|---|---|
message |
Shows the text for a few seconds. |
reload |
true reloads the page. |
redirect |
Opens this address. A javascript: address is not allowed. |
CP Bar ignores other keys. If the action throws an exception or returns something else, CP Bar logs a warning. The user then sees "That didn't work." To ask the user before the action runs, add a confirm.
#Ordering
- The lowest
prioritycomes first. The default is100. beforeandafterput an item next to another item in the same menu. If that item is not there,prioritydecides.- Two items
afterthe same item keep theirpriorityorder. - The priorities of the core items are in The core items.
#Removing items
CpBar::remove($id) removes an item for all users on all pages. $bar->remove($id) in a callback removes it for this user on this page. Both remove all items under it too. Both work for core items:
CpBar::remove('static-cache'); CpBar::extend(function ($bar, $context) { if (! $context->page) { $bar->remove('new-content'); }});
#Styling
CpBar::css(<<<'CSS' [data-id="broken-links"] .bar__badge { background: #b45309; } CSS);
CP Bar puts your CSS after its own CSS, so your CSS wins a tie. Each item has its id as data-id. Other hooks are .bar, .is-impersonating, .bar__link, .bar__badge, .bar__dot, .menu and .menu__link. These class names are internal and can change in an update. The data-id stays.
#Errors
CP Bar checks each add(), remove() and css(). If the data is not valid, CP Bar skips that call and logs a warning. The rest of the callback continues. If a callback throws an exception, CP Bar removes all that the callback added and logs a warning. The rest of the bar shows as usual. The page itself does not change.
Each warning starts with CP Bar::
CP Bar: Skipped the item [broken-links-report]: priority is not a number.CP Bar: Skipped the extension Acme\BrokenLinks\BarItems: Division by zeroCP Bar: Hid the item [broken-links-report]: its can check failed: No such reportCP Bar: The action of [broken-links-check] failed: It returned something other than a message, a reload or a redirect.
CP Bar logs these warnings when it builds the bar for a user. A page view of a visitor never logs a warning. CP Bar cannot catch a fatal error, a timeout, or output from echo, dump() or dd() in a callback.
#The core items
| id | parent | priority |
|---|---|---|
site-name |
10 | |
new-content |
30 | |
new-entry-{collection}, new-entry-{collection}-{blueprint} |
new-content |
10 |
new-term-{taxonomy}, new-term-{taxonomy}-{blueprint} |
new-content |
12 |
new-runway-{resource} |
new-content |
15 |
new-user |
new-content |
20 |
edit, create-redirect |
40 | |
forms |
45 | |
sites |
50 | |
site-{handle} |
sites |
10 |
form-{handle} |
forms |
10 |
seo-pro |
55 | |
seo-pro-{rule}, seo-pro-report, seo-pro-section-defaults, seo-pro-site-defaults |
seo-pro |
10, 20, 30, 40 |
static-cache |
500 | |
static-cache-status, static-cache-refresh |
static-cache |
10, 20 |
my-account |
top-secondary |
1000 |
user-info, edit-profile, preferences, stop-impersonating, logout |
my-account |
1, 10, 15, 20, 30 |
#Stability
CP Bar follows semantic versioning. A change that breaks the public API comes only in a new major version.
This is the public API:
- The facade
Arnohoogma\StatamicCpBar\Facades\CpBarwithextend(),add(),remove()andcss(). - The classes
Arnohoogma\StatamicCpBar\CpBarandArnohoogma\StatamicCpBar\Context, for type hints. On$bar:add(),remove()andcss(). On$context: the properties above. - The signatures of a callback
($bar, $context), an action($context)and acanclosure($context). - The keys of an item, the
metakeys, and the keys that an action returns. - The ids, parents and priorities of the core items.
- The rules on this page: the default priority, the order of the callbacks, and "a removal wins".
- For sites:
{{ cp_bar }}and@cpBar- the config keys
enabledandinject, andSTATAMIC_CP_BAR ?cp-bar=offand theX-CP-Barheader- the permission
view cp barand the preferencecp_bar - the class
cp-barand the property--cp-bar-heighton<html>, anddata-id - the cookie
statamic_cp_barand the address/!/statamic-cp-bar - the publish tags and
lang/vendor/cpbar - the class name
Arnohoogma\StatamicCpBar\ServiceProvider
All code with @internal can change in any release. So can the JSON of the bar, the POST request of an action, and the CSS class names. There are no events.
A minor version can add items, keys and meta keys. CP Bar ignores keys that it does not know, so data that 1.0 accepts stays valid in each 1.x.
#Translations
Publish the translations to change them or add a language:
php artisan vendor:publish --tag=cpbar-lang
They land in lang/vendor/cpbar/{locale}/cp.php.
#Troubleshooting
No bar. Check, in this order:
- The role has Access the Control Panel and View CP Bar, and the user hasn't switched off Show CP Bar in their preferences.
- You've opened the control panel since installing, so the cookie is set.
- The page isn't a cached copy from before the install:
php artisan statamic:static:clear. - The page's HTML contains
<cp-bar. If not, the response doesn't go through Statamic (use@cpBar) orinjectis off. - The network tab shows
/!/statamic-cp-bar. A 401 means that you are not logged in. A 403 means a missing permission, or the bar is off in the preferences. A 404 means that the bar is off, or that the request has anX-CP-Bar: offheader. Is there no request, or a 200 without a bar? Thenloader.jsorbar.jsis missing frompublic/vendor/statamic-cp-bar/build. Publish them, see Install.
The bar covers a fixed header. See Your layout.
Only one domain of a multisite shows the bar. You're logged in per domain.
#Limits
- A
Route::statamic()page that loads an entry has no Edit: the bar finds pages by their URL. - Forms only finds forms in the entry's own fields.
#Uninstalling
composer remove 4rn0/statamic-cp-bar
Then remove public/vendor/statamic-cp-bar. Remove config/statamic-cp-bar.php and lang/vendor/cpbar if you published them. Remove each {{ cp_bar }} and @cpBar, and clear the static cache. The View CP Bar permission stays in resources/users/roles.yaml, and the Show CP Bar preference stays in the preferences of the users. Both do nothing.