Custom Plugin Menu CMS
August 7, 2025 Β· AI Conversations
community_store using this I want our custom plugin main menu will listed under community_store menun in dashboard
Hereβs how to do it:
—
### β Step-by-Step Instructions:
#### 1. **Create Your Page Structure**
Assuming your package is called `my_custom_plugin`, create the dashboard page under this path:
packages/my_custom_plugin/single_pages/dashboard/store/my_plugin.php
#### 2. **Register the Page in `on_start()` or `install()` Method**
In your package controller (`controller.php`):
use ConcreteCorePageSingle as SinglePage;
public function install()
{
$pkg = parent::install();
// Create page under /dashboard/store/
$page = SinglePage::add('/dashboard/store/my_plugin', $pkg);
if (is_object($page) && !$page->isError()) {
$page->update([
'cName' => 'My Plugin',
'cDescription' => 'Custom plugin under Community Store menu.',
]);
}
}
#### 3. **(Optional) Add a Controller for Logic**
If you want to handle logic:
Create a controller file for your page:
packages/my_custom_plugin/controllers/single_page/dashboard/store/my_plugin.php
namespace ConcretePackageMyCustomPluginControllerSinglePageDashboardStore;
use ConcreteCorePageControllerDashboardPageController;
class MyPlugin extends DashboardPageController
{
public function view()
{
// your logic here
}
}
#### 4. **Check Menu Order (Optional)**
If needed, you can set the page position using `$page->setPageIndex($number);` or move it manually in the Dashboard Sitemap (`/dashboard/sitemap/full`).
—
### π Clear Cache
After making changes:
concrete/bin/concrete c5:clear-cache
—
### β Result:
Your custom plugin will now show under:
Dashboard β Store β My Plugin
Let me know if you want to show **multiple pages** or a **submenu** under your plugin as well.
### β To hide the top-level menu:
If your package previously added a top-level dashboard menu like:
/dashboard/my_plugin
You should **remove or avoid adding it**. Hereβs how to clean this up:
—
### π§ Step 1: Remove Top-Level Dashboard Page (if it exists)
In your `package controller` (`controller.php`), update the `uninstall()` method or remove the line that adds `/dashboard/my_plugin`:
public function uninstall()
{
parent::uninstall();
// Optionally remove the top-level page if it was previously added
$page = Page::getByPath('/dashboard/my_plugin');
if ($page && !$page->isError()) {
$page->delete();
}
}
Or remove the line like this from the `install()` method if it was there:
// Do NOT add this:
// SinglePage::add('/dashboard/my_plugin', $pkg);
—
### β Step 2: Only Register Page Under `/dashboard/store/`
Keep only this line in your install method:
SinglePage::add('/dashboard/store/my_plugin', $pkg);
This ensures your page appears only **under Community Store**, not as its own section.
—
### π Step 3: Reinstall or Adjust Manually
To apply this:
– Either uninstall and reinstall the package (careful with data loss),
– Or manually delete `/dashboard/my_plugin` from **Sitemap β Full Sitemap**.
—
Let me know if you also want to **add subpages** (like `/dashboard/store/my_plugin/settings`) or **custom permissions/visibility rules**!
—
### β
Goal
Keep your custom menu available at:
/dashboard/store/feed
…but **remove/hide**:
/dashboard/shopping_feed
from the **main dashboard nav**.
—
### π Solution
Update your `addDashboardPages()` method to set `exclude_nav = true` on the main page `/dashboard/shopping_feed` **after** it's created, like this:
#### β Add this inside your `addDashboardPages()` method:
// After the foreach loop
$mainPage = Page::getByPath('/dashboard/shopping_feed');
if ($mainPage && !$mainPage->isError()) {
$mainPage->setAttribute('exclude_nav', true); // This hides it from the dashboard nav
}
You already hide `add` and `edit` pages β this will now also hide the **main menu** from showing in the sidebar.
—
### π Summary of What Youβve Achieved
| Page URL | Visible in Dashboard Nav | Purpose |
|———————————-|—————————|——————————-|
| `/dashboard/shopping_feed` | β Hidden | Actual page with logic |
| `/dashboard/store/feed` | β
Visible | Alias shown under Store menu |
This approach is clean and user-friendly. Let me know if you want subpages (like `/dashboard/store/feed/settings`) also visible or hidden individually.
– Creates pages under `/dashboard/shopping_feed/…`
– Hides the **main `/dashboard/shopping_feed`** page from the dashboard nav
– Adds a visible alias to `/dashboard/store/feed`
—
### β Updated `addDashboardPages()` Method:
protected function addDashboardPages()
{
$pages = [
'/dashboard/shopping_feed' => ['Shopping Feed', 'Shopping Feed Tools'],
'/dashboard/shopping_feed/view' => ['Manage Feeds', 'List and manage feeds'],
'/dashboard/shopping_feed/tracklog' => ['Tracking Log', 'View tracking log'],
'/dashboard/shopping_feed/add' => ['Add Feed', 'Add a new feed'],
'/dashboard/shopping_feed/edit' => ['Edit Feed', 'Edit an existing feed'],
'/dashboard/shopping_feed/settings' => ['Settings', 'Settings'],
];
foreach ($pages as $path => [$name, $desc]) {
$page = Page::getByPath($path);
if ($page->isError()) {
$page = Single::add($path, $this);
}
if (is_object($page) && !$page->isError()) {
$page->update(['cName' => $name, 'cDescription' => $desc]);
// Hide non-nav pages
if (
in_array($path, [
'/dashboard/shopping_feed/add',
'/dashboard/shopping_feed/edit'
])
) {
$page->setAttribute('exclude_nav', true);
}
}
}
// Hide main /dashboard/shopping_feed from dashboard navigation
$mainPage = Page::getByPath('/dashboard/shopping_feed');
if ($mainPage && !$mainPage->isError()) {
$mainPage->setAttribute('exclude_nav', true);
}
// Add alias under Community Store
$pkgService = Core::make(PackageService::class);
$communityStore = $pkgService->getByHandle('community_store');
if ($communityStore && is_object($communityStore)) {
$mainPage = Page::getByPath('/dashboard/shopping_feed');
$parent = Page::getByPath('/dashboard/store');
$aliasExists = Page::getByPath('/dashboard/store/feed');
$isMainPageValid = ($mainPage instanceof Page && !$mainPage->isError());
$isParentValid = ($parent instanceof Page && !$parent->isError());
$aliasAlreadyExists = ($aliasExists instanceof Page && !$aliasExists->isError());
if ($isMainPageValid && $isParentValid && !$aliasAlreadyExists) {
$alias = $mainPage->addCollectionAlias($parent);
if ($alias instanceof Page && !$alias->isError()) {
$alias->update(['cName' => 'Feed']);
}
}
}
}
—
### π‘ What Changed
– Added: `exclude_nav = true` on `/dashboard/shopping_feed`
– Retained: your aliasing logic under `/dashboard/store/feed`
– Ensured clean sidebar UI: only shows "Feed" under Community Store
Let me know if you also want to hide subpages like `settings` or `tracklog`, or if you'd like to set permissions for specific user groups.
That happens because **the alias (`/dashboard/store/feed`) inherits the `exclude_nav` attribute** from the original page (`/dashboard/shopping_feed`) β so when we hide the original page from the dashboard nav using `exclude_nav = true`, **the alias is hidden too**.
—
### β Goal Recap:
You want:
– `/dashboard/shopping_feed` **not to appear** in the main dashboard menu
– `/dashboard/store/feed` **to appear under Community Store**
—
### β Correct Approach
Instead of setting `exclude_nav` on the **main page**, weβll:
1. Leave `/dashboard/shopping_feed` visible (do NOT use `exclude_nav`)
2. Instead, set **`exclude_nav` = true on all subpages**, like `/view`, `/add`, `/edit`, `/tracklog`, `/settings`
3. Then add a friendly **alias (`/dashboard/store/feed`)** to appear under Community Store
—
### β Updated `addDashboardPages()` Code (Fully Corrected)
protected function addDashboardPages()
{
$pages = [
'/dashboard/shopping_feed' => ['Shopping Feed', 'Shopping Feed Tools'],
'/dashboard/shopping_feed/view' => ['Manage Feeds', 'List and manage feeds'],
'/dashboard/shopping_feed/tracklog' => ['Tracking Log', 'View tracking log'],
'/dashboard/shopping_feed/add' => ['Add Feed', 'Add a new feed'],
'/dashboard/shopping_feed/edit' => ['Edit Feed', 'Edit an existing feed'],
'/dashboard/shopping_feed/settings' => ['Settings', 'Settings'],
];
foreach ($pages as $path => [$name, $desc]) {
$page = Page::getByPath($path);
if ($page->isError()) {
$page = Single::add($path, $this);
}
if (is_object($page) && !$page->isError()) {
$page->update(['cName' => $name, 'cDescription' => $desc]);
// Hide all pages EXCEPT the main one (let it be hidden manually)
if ($path !== '/dashboard/shopping_feed') {
$page->setAttribute('exclude_nav', true);
}
}
}
// Add visible alias under Community Store
$pkgService = Core::make(PackageService::class);
$communityStore = $pkgService->getByHandle('community_store');
if ($communityStore && is_object($communityStore)) {
$mainPage = Page::getByPath('/dashboard/shopping_feed');
$parent = Page::getByPath('/dashboard/store');
$aliasExists = Page::getByPath('/dashboard/store/feed');
$isMainPageValid = ($mainPage instanceof Page && !$mainPage->isError());
$isParentValid = ($parent instanceof Page && !$parent->isError());
$aliasAlreadyExists = ($aliasExists instanceof Page && !$aliasExists->isError());
if ($isMainPageValid && $isParentValid && !$aliasAlreadyExists) {
$alias = $mainPage->addCollectionAlias($parent);
if ($alias instanceof Page && !$alias->isError()) {
$alias->update(['cName' => 'Feed']);
// β
Make sure the alias IS visible
$alias->setAttribute('exclude_nav', false);
}
}
}
}
—
### β What Youβll See Now
| Location | Visible? | Purpose |
|——————————-|———-|——————————|
| `/dashboard/shopping_feed` | β
Yes (can hide manually) |
| `/dashboard/store/feed` | β
Yes | Main menu shown under Store |
| `/dashboard/shopping_feed/*` | β Hidden | Subpages not shown in nav |
—
### Optional: Manually Hide `/dashboard/shopping_feed`
If you want to fully hide `/dashboard/shopping_feed` from the nav **but keep the alias shown**, youβll need to:
1. Skip setting `exclude_nav` programmatically
2. Manually hide it in the sitemap:
– Go to **Dashboard β Sitemap β Full Sitemap**
– Find `/dashboard/shopping_feed`
– Click gear βοΈ β **Properties**
– Set **"Exclude from nav" = Yes**
Let me know if you want to handle this in a more dynamic way via permissions or roles too.
—
### β Final Working Approach
Instead of **creating `/dashboard/shopping_feed` directly**, you can **create your actual page at `/dashboard/store/feed`** **without needing to create a separate section like `/dashboard/shopping_feed`** at all.
This way:
– Your page will appear **only** under `Dashboard β Store β Feed`
– It wonβt show up as a top-level menu
– No aliasing or `exclude_nav` hacks are needed
—
### π How to Implement
#### β Replace your current `/dashboard/shopping_feed` path with `/dashboard/store/feed` directly:
Update your `addDashboardPages()` method like this:
protected function addDashboardPages()
{
$pages = [
'/dashboard/store/feed' => ['Feed', 'Shopping Feed Tools'],
'/dashboard/store/feed/view' => ['Manage Feeds', 'List and manage feeds'],
'/dashboard/store/feed/tracklog' => ['Tracking Log', 'View tracking log'],
'/dashboard/store/feed/add' => ['Add Feed', 'Add a new feed'],
'/dashboard/store/feed/edit' => ['Edit Feed', 'Edit an existing feed'],
'/dashboard/store/feed/settings' => ['Settings', 'Settings'],
];
foreach ($pages as $path => [$name, $desc]) {
$page = Page::getByPath($path);
if ($page->isError()) {
$page = Single::add($path, $this);
}
if (is_object($page) && !$page->isError()) {
$page->update(['cName' => $name, 'cDescription' => $desc]);
// Hide internal pages
if (
in_array($path, [
'/dashboard/store/feed/add',
'/dashboard/store/feed/edit'
])
) {
$page->setAttribute('exclude_nav', true);
}
}
}
}
—
### β Also update your controller paths
Update your controller namespaces and routes accordingly:
– Controller path:
`packages/shopping_feed_manager/controllers/single_page/dashboard/store/feed.php`
namespace ConcretePackageShoppingFeedManagerControllerSinglePageDashboardStore;
use ConcreteCorePageControllerDashboardPageController;
class Feed extends DashboardPageController
{
public function view()
{
// your logic
}
}
– Routes like:
Route::register('/dashboard/store/feed/deletefeed/{id}', ...);
—
### β Result
You now have:
Dashboard β Store β Feed
β³ Manage Feeds
β³ Tracking Log
β³ Settings
β¦and **no `/dashboard/shopping_feed` entry in main nav**.
—
Let me know if you still need an alias instead (for backward compatibility), but this direct placement under `/dashboard/store/feed` is the cleanest and most stable way to solve your need.
> β
**Keep all your pages under `/dashboard/shopping_feed/…`**
> β
**Keep the alias visible under `/dashboard/store/feed`**
> π« **But hide the main `/dashboard/shopping_feed` from the left-hand dashboard menu**
—
### βοΈImportant Caveat:
In Concrete CMS, if you use `setAttribute('exclude_nav', true)` on the main page (`/dashboard/shopping_feed`), **that setting gets inherited by its alias** (e.g., `/dashboard/store/feed`), which makes the alias hidden too.
—
### β 100% Working Solution (With Manual Navigation Page Control)
Hereβs how to **hide `/dashboard/shopping_feed` from the nav** while keeping `/dashboard/store/feed` visible:
—
### β Step-by-Step Fix:
#### 1. **Remove `exclude_nav` from `/dashboard/shopping_feed` in your PHP**
Do **NOT** set `exclude_nav` on `/dashboard/shopping_feed` inside your package code. Thatβs what breaks the alias visibility.
> β
Let it be visible to the system
> π« But weβll hide it from the user by setting it as a **system page**
—
#### 2. **Mark `/dashboard/shopping_feed` as a System Page** (hidden from nav)
After installing the package, run this **once manually** (or script it in your controller after install):
$mainPage = Page::getByPath('/dashboard/shopping_feed');
if ($mainPage && !$mainPage->isError()) {
$mainPage->setSystemPage(true); // Hides from nav but alias remains visible
}
β
This way:
– `/dashboard/shopping_feed` is **still functional**
– But ConcreteCMS **hides system pages from the dashboard sidebar nav**
– Your alias at `/dashboard/store/feed` stays **fully visible**
—
### β Add This to Your `install()` Method (Optional)
public function install()
{
$pkg = parent::install();
$this->addDashboardPages();
$this->createTables();
// Hide main page from nav while keeping alias
$mainPage = Page::getByPath('/dashboard/shopping_feed');
if ($mainPage && !$mainPage->isError()) {
$mainPage->setSystemPage(true); // Hides from dashboard nav
}
}
—
### β Final Result
| Page Path | Nav Visible? | Functional? | Purpose |
|——————————|————–|————-|————————————|
| `/dashboard/shopping_feed` | β No | β
Yes | Main controller hidden from nav |
| `/dashboard/store/feed` | β
Yes | β
Yes | Visible entry under Store menu |
Let me know if you'd like this done conditionally or for multiple languages as well.
Did you mean to call "isSystemPage"?
The method `setSystemPage()` **does not exist** in Concrete CMS 9 β that was a legacy concept from older versions.
—
### β Updated & Working Solution
To **hide `/dashboard/shopping_feed` from the dashboard menu** while keeping the **alias `/dashboard/store/feed` visible**, here's the correct and working solution for Concrete CMS 9:
—
### β Step-by-Step Fix (Final)
#### β
1. **Keep alias at `/dashboard/store/feed`**
You're already doing this correctly:
$alias = $mainPage->addCollectionAlias($parent);
$alias->update(['cName' => 'Feed']);
—
#### β
2. **Set `exclude_nav` = true for the main `/dashboard/shopping_feed`**
This **does hide it from the nav**, but also affects the alias.
To fix that…
—
#### β 3. **Manually override alias `exclude_nav = false`**
Update your `addDashboardPages()` method like this:
protected function addDashboardPages()
{
$pages = [
'/dashboard/shopping_feed' => ['Shopping Feed', 'Shopping Feed Tools'],
'/dashboard/shopping_feed/view' => ['Manage Feeds', 'List and manage feeds'],
'/dashboard/shopping_feed/tracklog' => ['Tracking Log', 'View tracking log'],
'/dashboard/shopping_feed/add' => ['Add Feed', 'Add a new feed'],
'/dashboard/shopping_feed/edit' => ['Edit Feed', 'Edit an existing feed'],
'/dashboard/shopping_feed/settings' => ['Settings', 'Settings'],
];
foreach ($pages as $path => [$name, $desc]) {
$page = Page::getByPath($path);
if ($page->isError()) {
$page = Single::add($path, $this);
}
if (is_object($page) && !$page->isError()) {
$page->update(['cName' => $name, 'cDescription' => $desc]);
// Hide internal pages
if ($path !== '/dashboard/shopping_feed') {
$page->setAttribute('exclude_nav', true);
}
}
}
// Add visible alias under Community Store
$pkgService = Core::make(PackageService::class);
$communityStore = $pkgService->getByHandle('community_store');
if ($communityStore && is_object($communityStore)) {
$mainPage = Page::getByPath('/dashboard/shopping_feed');
$parent = Page::getByPath('/dashboard/store');
$aliasExists = Page::getByPath('/dashboard/store/feed');
$isMainPageValid = ($mainPage instanceof Page && !$mainPage->isError());
$isParentValid = ($parent instanceof Page && !$parent->isError());
$aliasAlreadyExists = ($aliasExists instanceof Page && !$aliasExists->isError());
if ($isMainPageValid && $isParentValid && !$aliasAlreadyExists) {
$alias = $mainPage->addCollectionAlias($parent);
if ($alias instanceof Page && !$alias->isError()) {
$alias->update(['cName' => 'Feed']);
// π THIS LINE IS THE KEY
$alias->setAttribute('exclude_nav', false);
}
}
}
// π Now hide the main page from nav
$mainPage = Page::getByPath('/dashboard/shopping_feed');
if ($mainPage && !$mainPage->isError()) {
$mainPage->setAttribute('exclude_nav', true);
}
}
—
### β Summary
| Path | `exclude_nav` | Visible in Dashboard Nav? |
|—————————–|—————-|—————————-|
| `/dashboard/shopping_feed` | β
`true` | β Hidden |
| `/dashboard/store/feed` | β `false` | β
Visible under Store |
Let me know if you want this handled dynamically based on permissions or user roles as well.
$mainPage = Page::getByPath('/dashboard/shopping_feed');
if ($mainPage && !$mainPage->isError()) {
$mainPage->setAttribute('exclude_nav', true);
}
its also hide alias
> π΄ **Setting `exclude_nav = true` on the original page also hides its alias** in the dashboard navigation.
Thatβs because aliases in Concrete CMS **inherit attributes** like `exclude_nav` from their original page.
—
### β The Only Reliable Solution
To **hide `/dashboard/shopping_feed` from the dashboard nav** but **keep the alias `/dashboard/store/feed` visible**, you must:
> β
Create `/dashboard/store/feed` as a **real standalone page**,
> β Not use `/dashboard/shopping_feed` at all.
—
### β How to Fix It
You must change your implementation to **create your main page directly at `/dashboard/store/feed`**, not via an alias. Hereβs the clean solution:
—
### β Updated `addDashboardPages()` β NO alias, clean setup
protected function addDashboardPages()
{
$pages = [
'/dashboard/store/feed' => ['Feed', 'Shopping Feed Tools'],
'/dashboard/store/feed/view' => ['Manage Feeds', 'List and manage feeds'],
'/dashboard/store/feed/tracklog' => ['Tracking Log', 'View tracking log'],
'/dashboard/store/feed/add' => ['Add Feed', 'Add a new feed'],
'/dashboard/store/feed/edit' => ['Edit Feed', 'Edit an existing feed'],
'/dashboard/store/feed/settings' => ['Settings', 'Settings'],
];
foreach ($pages as $path => [$name, $desc]) {
$page = Page::getByPath($path);
if ($page->isError()) {
$page = Single::add($path, $this);
}
if (is_object($page) && !$page->isError()) {
$page->update(['cName' => $name, 'cDescription' => $desc]);
// Hide subpages (optional)
if (
in_array($path, [
'/dashboard/store/feed/add',
'/dashboard/store/feed/edit'
])
) {
$page->setAttribute('exclude_nav', true);
}
}
}
}
—
### β Update Controller Class and Routes
If your controller is currently in:
ConcretePackageShoppingFeedManagerControllerSinglePageDashboardShoppingFeed
π Move it to:
ConcretePackageShoppingFeedManagerControllerSinglePageDashboardStoreFeed
File:
packages/shopping_feed_manager/controllers/single_page/dashboard/store/feed.php
—
### β Why This Works
– You no longer need `/dashboard/shopping_feed` at all
– No alias needed, so no inherited `exclude_nav`
– Everything shows directly under Community Store menu
– Clean, maintainable, no hacks or attribute overrides
—
### π Summary
| Path | Result |
|—————————-|————–|
| `/dashboard/shopping_feed` | β Removed |
| `/dashboard/store/feed` | β
Direct page, shown in nav |
Let me know if you need help migrating views or controllers to the new location β Iβll help you adjust paths instantly.