Xgenious/ docs
Products
Get support

Shortcodes

Shortcodes allow non-technical users to embed dynamic content in product descriptions, page builder content, and email templates.

Registering a Shortcode

$this->register_shortcode('loyalty_balance', function ($attrs, $content = null) {
    $user = auth()->user();
    if (!$user) return '';
    $points = $user->loyalty_points ?? 0;
    return '<span class="loyalty-points">' . $points . ' pts</span>';
});

Shortcode Syntax

<!-- Self-closing -->
[loyalty_balance /]

<!-- With attributes -->
[loyalty_balance color="gold" size="large" /]

<!-- With inner content -->
[my_tag class="highlight"]Your content here[/my_tag]

Manual Parsing

Use the global helper to parse shortcodes in arbitrary HTML strings:

$html = '<p>Your balance: [loyalty_balance /]</p>';
$rendered = parse_shortcodes($html);

Scope

Shortcodes are applied automatically in: product descriptions, page builder content blocks, and email templates.

Scheduled Tasks

Register recurring background tasks from within boot(). Callbacks are collected and injected into Laravel's scheduler, running as part of php artisan schedule:run.

Predefined Frequencies

everyMinute everyFiveMinutes hourly daily weekly monthly

public function boot(): void
{
    // Using a named frequency
    $this->schedule('daily', function () {
        SyncLoyaltyPointsJob::dispatch();
    });

    // Using a cron expression — every Monday at 8 AM
    $this->schedule('0 8 * * 1', function () {
        WeeklyReportJob::dispatch();
    });
}
Warning

💡 Ensure your server's crontab runs php artisan schedule:run every minute: * * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1

Routes: API & Web

Plugins have two route registration helpers: register_api_routes() for JSON/REST endpoints and register_web_routes() for standard web pages (admin, frontend, user dashboard). Both must be called from routes(), not boot(). The routes() method runs for all discovered plugins before tenancy middleware fires, ensuring routes are registered at the correct point in the request lifecycle.

API Routes

Endpoints are automatically mounted under /api/v1/plugins/{plugin-id}/ with the api middleware group (rate limiting, JSON formatting). Register from routes().

public function routes(): void
{
    $this->registerSettings(); // always first

    $this->register_api_routes(function ($router) {
        // GET /api/v1/plugins/acme-seo/status
        $router->get('/status', fn() => response()->json(['ok' => true]));

        // POST /api/v1/plugins/acme-seo/sync
        $router->post('/sync', [AcmeSeoController::class, 'sync']);
    });

    $this->register_web_routes(function () {
        Route::middleware(['tenantUserMailVerify', 'package_expire', 'auth'])
            ->prefix('user-home/my-plugin')
            ->name('tenant.user.my-plugin.')
            ->group(function () {
                Route::get('/', [MyPluginController::class, 'index'])->name('index');
                Route::post('/save', [MyPluginController::class, 'save'])->name('save');
            });
    });
}

Web Routes

Use register_web_routes() for any standard Laravel web route — tenant admin pages, frontend pages, user dashboard pages, etc. Both API and web route helpers must be called from within your routes() method.

// Tenant admin routes example (inside routes())
$this->register_web_routes(function () {
    Route::middleware(['auth:admin', 'tenantadmin'])
        ->prefix('admin/my-plugin')
        ->name('tenant.admin.my-plugin.')
        ->group(function () {
            Route::get('/settings', [MyPluginController::class, 'settings'])->name('settings');
            Route::post('/settings', [MyPluginController::class, 'saveSettings'])->name('settings.save');
        });
});
Warning

⚠️ Do not use register_web_routes() for landlord admin routes. Admin routes for the landlord panel are registered by your Module's own ServiceProvider via the standard routes/web.php mechanism. register_web_routes() is intended for tenant-facing frontend and user dashboard pages.

User Dashboard Integration

After registering your web route, add an entry to the user dashboard sidebar and optionally a dashboard widget using the provided action hooks:

// In boot() — add sidebar link
$this->add_action('nazmart:user_dashboard_sidebar', function () {
    $active = request()->routeIs('tenant.user.my-plugin.*') ? 'active' : '';
    $url    = route('tenant.user.my-plugin.index');
    echo "<li class=\"list {$active}\"><a href=\"{$url}\"><i class=\"las la-star\"></i> My Plugin</a></li>";
});

// In boot() — add dashboard stat card (receives the authenticated user)
$this->add_action('nazmart:user_dashboard_home', function (mixed $user) {
    if (!$user) return;
    echo '<div class="col-xl-6 col-md-6 orders-child"> ... </div>';
});
Note

ℹ️ Hook fire points: nazmart:user_dashboard_sidebar — fires inside the <ul> of the user dashboard sidebar nav, before the logout link. nazmart:user_dashboard_home — fires after the four core stat cards on the dashboard home page. Receives the authenticated $user object as the first argument.

Multi-Tenant Support

Nazmart's multi-tenancy model gives you fine-grained control over how your plugin behaves across different tenant shops.

Plugin Type Behaviour

TypeLandlord boots?Tenant boots?
landlordYesNo
tenantNoYes
bothYesYes

Per-Tenant Settings

get_option() and update_option() automatically scope to the current tenant when running in tenant context. This means each tenant can have independent configuration.

Tenant Overrides

The landlord can control plugin availability per tenant via the plugin_tenant_overrides database table. Landlords can:

  • Allow or deny specific plugins for specific tenants
  • Allow tenants to self-manage (activate/deactivate) a plugin
  • Set global defaults that apply to all tenants

Tenant Boot Timing

Plugins with "type": "tenant" (or "both") are not booted during the initial service provider boot phase, because tenancy middleware has not yet run at that point and tenant() returns null. Instead, the platform listens for the TenancyInitialized event — fired when the tenancy middleware resolves the current tenant from the request domain — and calls bootForTenant() at that moment.

What this means for your plugin:

  • Web routes registered via register_web_routes() are added to the router after tenancy is initialized, so they are available on every tenant request.
  • Hook callbacks registered via add_action() / add_filter() inside boot() fire at the correct time — hooks themselves are still called during the HTTP request cycle.
  • CLI / queue / scheduler contexts do not trigger TenancyInitialized via HTTP. Tenant plugins may not boot automatically in those contexts. Use Stancl's tenancy()->initialize($tenant) explicitly when you need to run tenant plugin logic in queue jobs or scheduled commands.
Warning

⚠️ Never call tenant() directly inside boot() for tenant-type plugins. The method fires after tenancy initialization, but for safety (CLI contexts, test environments) always null-check: if (!function_exists('tenant') || !tenant()) return;

Safe Tenant Context Checks

public function boot(): void
{
    // tenant() may be null during CLI commands — always null-check
    $tenant = tenant();
    if ($tenant === null) {
        return;
    }

    // Safe to use tenant-scoped logic here
    $this->add_action('nazmart:order_completed', function ($order) {
        // runs for this tenant only
    });
}

Data Export

Declare which of your custom tables should be included when a tenant exports their data:

$this->register_export_tables([
    'acme_loyalty_points',
    'acme_loyalty_transactions',
]);
Still stuck?
Our support team is ready to help you get set up.
Get support