Frequently Asked Questions
This page provides answers to common questions about plugin development in Botble CMS.
Using Thesky9 as a Foundation for Custom Applications
Can I use Botble CMS as a Laravel foundation to build a custom web application?
Yes. Botble CMS is a full Laravel application, so you can extend it to build any custom business system — patient management, CRM, HRM, inventory, booking, project management, school management, etc. — instead of only using it for website content.
You get the following out of the box, so you don't have to build them from scratch:
- Authentication and login (admin and frontend users)
- Role and permission system
- Admin panel UI (Tabler-based, responsive, dark mode)
- Form builder and table builder components
- Dashboard menu and settings API
- Media library with image/file uploads
- Multi-language support
- Notifications, email templates, and queue support
- REST API helpers
- System updater and plugin manager
Can I create custom Models, Controllers, Views, Migrations, and Routes?
Yes, exactly as in a normal Laravel project. The recommended approach is to wrap your custom modules as a plugin inside platform/plugins/ so core updates don't overwrite your code. Each plugin has its own:
src/Models/for Eloquent modelssrc/Http/Controllers/for controllers (admin and public)src/Http/Requests/for form request validationsrc/Forms/for form builder classessrc/Tables/for table builder classessrc/Providers/for service providersdatabase/migrations/for schema migrationsresources/views/for Blade templatesresources/lang/for translationsroutes/web.phpandroutes/api.phpfor routing
You can use every Laravel feature: Eloquent relationships, policies, events, listeners, jobs, notifications, mail, queues, scheduled tasks, broadcasting, etc.
Can I reuse Thesky9's admin panel and authentication for my custom modules?
Yes. When you scaffold a plugin, it automatically integrates with:
- Admin login — no need to build a separate login system. Your module's admin pages are accessible to any authenticated admin user.
- Permissions — register custom permissions in your service provider, and the built-in role manager (
Admin → System → Roles & Permissions) handles assignment. - Admin menu — register menu items via
DashboardMenu::registerItem()so your modules appear in the sidebar alongside core features. - Settings — add settings pages via the
cms_settings_pagesfilter. - Form and table builders — use
FormAbstractandTableAbstractbase classes to get admin UI with validation, pagination, filters, bulk actions, and export, without writing Blade markup. - Media picker — use the
mediaImageform field to reuse the core media library for image uploads.
How do I scaffold a new plugin for my custom module?
Run the artisan command:
php artisan cms:plugin:create patient-managementThis generates a plugin skeleton at platform/plugins/patient-management/ with service providers, a sample model, migration, controller, form, table, and routes already wired up. You can then add more models and controllers using standard Laravel patterns.
You can also scaffold individual components inside an existing plugin:
php artisan cms:make:model Patient --plugin=patient-management
php artisan cms:make:form PatientForm --plugin=patient-management
php artisan cms:make:table PatientTable --plugin=patient-managementWhat are the best reference plugins to study when building custom modules?
Two core plugins ship with Thesky9 and are excellent real-world examples:
- Blog plugin (
platform/plugins/blog) — simple CRUD with categories, tags, status, SEO helpers, and permissions. Great starting point for content-type modules. - Ecommerce plugin (
platform/plugins/ecommerce) — complex plugin with many models, relationships, custom fields, dashboard widgets, settings pages, custom statuses, and REST API. Good reference for large business modules.
Study how they register service providers, permissions, menus, forms, tables, and routes, then mirror the same patterns in your plugin.
Will core CMS updates break my custom plugin?
No, as long as your code lives inside your own plugin in platform/plugins/your-plugin/. The System Updater only overwrites core files (platform/core/, platform/packages/, platform/plugins/* for core plugins). Third-party plugins are never touched. Always keep customizations in a plugin — never modify core files directly.
General Questions
How do I add a settings page for my plugin?
To add a settings page:
- Create a controller method to handle the settings form
- Register a route for the settings page
- Add the settings page to the CMS settings menu
Example controller methods:
public function getSettings()
{
$this->pageTitle(trans('plugins/foo::foo.settings'));
return view('plugins/foo::settings', [
'itemsPerPage' => setting('foo_items_per_page', 10),
'displayAuthor' => setting('foo_display_author', true),
]);
}
public function postSettings(Request $request, BaseHttpResponse $response)
{
setting([
'foo_items_per_page' => $request->input('foo_items_per_page'),
'foo_display_author' => $request->input('foo_display_author'),
])->save();
return $response
->setMessage(trans('core/base::notices.update_success_message'));
}Add to settings menu in your service provider:
if (defined('SETTING_MODULE_SCREEN_NAME')) {
add_filter('cms_settings_pages', function ($pages) {
return array_merge($pages, [
'foo' => [
'name' => 'plugins/foo::foo.settings',
'icon' => 'ti ti-box',
'view' => 'plugins/foo::settings',
'route' => 'foo.settings',
],
]);
});
}How do I add custom validation rules?
To add custom validation rules:
- Create a rule class in the
src/Rulesdirectory - Use the rule in your request classes
Example rule (src/Rules/UniqueItemSlug.php):
<?php
namespace Botble\Foo\Rules;
use Botble\Foo\Models\Item;
use Illuminate\Contracts\Validation\Rule;
class UniqueItemSlug implements Rule
{
protected int $itemId;
public function __construct(int $itemId = 0)
{
$this->itemId = $itemId;
}
public function passes($attribute, $value): bool
{
$item = Item::query()->where('slug', $value)->first();
if (!$item) {
return true;
}
if ($item->id === $this->itemId) {
return true;
}
return false;
}
public function message(): string
{
return trans('plugins/foo::validation.slug_unique');
}
}Usage in a request class:
public function rules(): array
{
return [
'slug' => ['required', 'string', 'max:255', new UniqueItemSlug($this->route('item'))],
];
}How do I add a custom action button to a table?
To add a custom action button to a table:
- Create a custom action class in the
src/Tables/Actionsdirectory - Add the action to your table's
addActions()method
Example custom action (src/Tables/Actions/DuplicateAction.php):
<?php
namespace Botble\Foo\Tables\Actions;
use Botble\Table\Actions\Action;
class DuplicateAction extends Action
{
public static function make(string $name = 'duplicate'): static
{
return parent::make($name)
->label(trans('plugins/foo::foo.duplicate'))
->color('primary')
->icon('ti ti-copy')
->attributes([
'data-action' => 'duplicate',
]);
}
}Add to your table class:
public function setup(): void
{
$this
->model(Item::class)
->addActions([
EditAction::make()
->route('foo.edit'),
DuplicateAction::make()
->route('foo.duplicate'),
DeleteAction::make()
->route('foo.destroy'),
]);
}How do I add a relationship between my plugin's models?
To add relationships between models:
- Define the relationship methods in your model classes
- Set up the necessary foreign keys in your migrations
Example one-to-many relationship:
In your migration:
Schema::create('foo_categories', function (Blueprint $table) {
$table->id();
$table->string('name', 255);
$table->string('slug', 255)->unique();
$table->string('status', 60)->default('published');
$table->timestamps();
});
Schema::create('foo_items', function (Blueprint $table) {
$table->id();
$table->string('name', 255);
$table->string('description', 400)->nullable();
$table->longText('content')->nullable();
$table->string('status', 60)->default('published');
$table->foreignId('category_id')->nullable()->references('id')->on('foo_categories')->onDelete('set null');
$table->timestamps();
});In your Category model:
public function items(): HasMany
{
return $this->hasMany(Item::class, 'category_id');
}In your Item model:
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'category_id');
}How do I create a plugin that depends on another plugin?
To create a plugin that depends on another plugin:
- Check for the dependency in your plugin's service provider
- Register your plugin's services conditionally
Example dependency check:
public function register(): void
{
if (!is_plugin_active('other-plugin')) {
return;
}
// Register your plugin's services here
}Frontend Integration
How do I display my plugin's content on the frontend?
To display your plugin's content on the frontend:
- Create a public controller in your plugin
- Register routes for the public controller
- Create views in the theme directory
Example public controller:
<?php
namespace Botble\Foo\Http\Controllers;
use Botble\Foo\Models\Item;
use Botble\SeoHelper\Facades\SeoHelper;
use Botble\Theme\Facades\Theme;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
class PublicController extends Controller
{
public function index(Request $request)
{
SeoHelper::setTitle(trans('plugins/foo::foo.name'));
$items = Item::query()
->where('status', 'published')
->orderBy('created_at', 'DESC')
->paginate(10, ['*'], 'page', $request->integer('page', 1));
return Theme::scope('foo.index', compact('items'))->render();
}
}How do I add custom meta boxes to my plugin's forms?
To add custom meta boxes:
- Add the meta box fields to your form class
- Create a migration for the meta data table
- Add the meta data relationship to your model
Example meta box in form:
->add('metadata', 'custom_html', [
'label' => trans('plugins/foo::foo.metadata'),
'wrapper' => [
'class' => 'form-group col-md-12',
],
'html' => '<div class="row"><div class="col-md-6">',
])
->add('meta_title', 'text', [
'label' => trans('core/base::forms.meta_title'),
'wrapper' => [
'class' => 'form-group col-md-6',
],
'value' => $this->getModel()->meta_title,
])
->add('meta_description', 'textarea', [
'label' => trans('core/base::forms.meta_description'),
'wrapper' => [
'class' => 'form-group col-md-6',
],
'value' => $this->getModel()->meta_description,
'attributes' => [
'rows' => 3,
],
])
->add('meta_keywords', 'text', [
'label' => trans('core/base::forms.meta_keywords'),
'wrapper' => [
'class' => 'form-group col-md-6',
],
'value' => $this->getModel()->meta_keywords,
])
->add('metadata_end', 'custom_html', [
'html' => '</div></div>',
])In your model, add the meta attributes:
/**
* @var array
*/
protected $fillable = [
'name',
'description',
'content',
'status',
'category_id',
'image',
'meta_title',
'meta_description',
'meta_keywords',
];Advanced Questions
How do I add a custom filter to a table?
To add a custom filter to a table:
- Override the
getFilters()method in your table class - Add your custom filter logic
Example custom filter:
public function getFilters(): array
{
return [
'category_id' => [
'title' => trans('plugins/foo::foo.category'),
'type' => 'select',
'choices' => app(CategoryInterface::class)->pluck('name', 'id'),
'validate' => 'required|integer',
],
'status' => [
'title' => trans('core/base::tables.status'),
'type' => 'select',
'choices' => BaseStatusEnum::labels(),
'validate' => 'required|in:' . implode(',', BaseStatusEnum::values()),
],
'created_at' => [
'title' => trans('core/base::tables.created_at'),
'type' => 'date',
],
];
}
public function applyFilterCondition($query, string $key, string $operator, ?string $value)
{
switch ($key) {
case 'category_id':
if (!$value) {
break;
}
return $query->whereHas('categories', function ($query) use ($value) {
return $query->where('foo_item_categories.category_id', $value);
});
default:
return parent::applyFilterCondition($query, $key, $operator, $value);
}
}How do I add a custom widget area?
To add a custom widget area:
- Register the widget area in your service provider
- Create a view for the widget area
Example widget area registration:
if (is_plugin_active('widget')) {
app(WidgetInterface::class)->registerSidebar([
'id' => 'foo_sidebar',
'name' => trans('plugins/foo::foo.widgets.sidebar_name'),
'description' => trans('plugins/foo::foo.widgets.sidebar_description'),
]);
}To display the widget area in your theme:
{!! dynamic_sidebar('foo_sidebar') !!}How do I add custom CSS and JavaScript to my plugin?
To add custom CSS and JavaScript:
- Place your assets in the
public/vendor/your-plugindirectory - Register and load your assets in your service provider
Example in service provider:
public function boot(): void
{
// ...
$this->app->booted(function () {
add_action(BASE_ACTION_ENQUEUE_SCRIPTS, [$this, 'registerAssets'], 99);
});
}
public function registerAssets(): void
{
Assets::addStylesDirectly([
'vendor/core/plugins/foo/css/foo.css',
])
->addScriptsDirectly([
'vendor/core/plugins/foo/js/foo.js',
]);
}To publish your assets during plugin installation:
$this->publishes([
__DIR__.'/../../resources/assets' => resource_path('vendor/core/plugins/foo'),
__DIR__.'/../../public' => public_path('vendor/core/plugins/foo'),
], 'public');How do I add a custom shortcode?
To add a custom shortcode:
- Register the shortcode in your service provider
- Create a view for the shortcode
Example shortcode registration:
Shortcode::register('foo-items', trans('plugins/foo::shortcodes.foo_items.name'), trans('plugins/foo::shortcodes.foo_items.description'), function ($shortcode) {
$limit = $shortcode->limit ?: 6;
$category = $shortcode->category;
$items = Item::query()
->where('status', BaseStatusEnum::PUBLISHED)
->with('slugable')
->orderBy('created_at', 'DESC')
->limit($limit)
->withCount('comments')
->get();
return view('plugins/foo::shortcodes.items', compact('items', 'shortcode'))->render();
}, [
'name' => [
'title' => trans('plugins/foo::shortcodes.foo_items.name'),
'type' => 'text',
'tab' => trans('core/base::forms.content'),
],
'limit' => [
'title' => trans('core/base::forms.limit'),
'type' => 'number',
'default_value' => 6,
'tab' => trans('core/base::forms.content'),
],
'category' => [
'title' => trans('plugins/foo::shortcodes.foo_items.category'),
'type' => 'customSelect',
'source' => route('foo.categories.list'),
'tab' => trans('core/base::forms.content'),
],
]);