ruudy/metronic-bundle
A Laravel package that integrates the Metronic admin theme, bundling assets, layouts, and Blade-ready components to help you build polished dashboards and backend interfaces faster with less manual setup.
This is a Symfony2 bundle, not a Composer package for Laravel. composer require ruudy/metronic-bundle inside a Laravel app will either fail on symfony/symfony ~2.x constraints or install dead weight that registers no service provider, publishes no assets, and exposes no Blade views. Treat it as a reference implementation of the Metronic admin shell you port into resources/views and public/vendor/metronic, not as a runtime dependency.
Three concrete starting points before writing any code:
composer.json – confirm the Symfony/PHP floors. Symfony 2.x means PHP 5.3–7.0 era; you will not install this alongside Laravel 10/11.Resources/views/ – this is the actual value. Find the base layout (typically layout.html.twig or base.html.twig). Every block in that file is a @yield or @section waiting to be mapped.Resources/public/ – the Metronic CSS/JS/vendor assets. These are what you copy into public/vendor/metronic/.Minimal porting sequence:
git clone https://github.com/ruudy-es/Metronic-Bundle /tmp/metronic
# Inspect the layout, then copy assets only — never the PHP
mkdir -p public/vendor/metronic
cp -R /tmp/metronic/Resources/public/* public/vendor/metronic/
Convert the base Twig layout to a Blade layout, keeping the DOM structure byte-for-byte so the Metronic JS plugin initializers still find their selectors:
{{-- resources/views/layouts/admin.blade.php --}}
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', config('app.name'))</title>
<link href="{{ asset('vendor/metronic/css/styles.css') }}" rel="stylesheet">
@stack('styles')
</head>
<body id="kt_body" class="header-fixed header-tablet-and-mobile-fixed aside-enabled aside-fixed">
@include('layouts.partials.sidebar')
<div class="wrapper d-flex flex-column flex-row-fluid">
@include('layouts.partials.topbar')
<main class="content flex-column-fluid">@yield('content')</main>
@include('layouts.partials.footer')
</div>
<script src="{{ asset('vendor/metronic/js/scripts.bundle.js') }}"></script>
@stack('scripts')
</body>
</html>
First use case: a single authenticated admin shell (sidebar + topbar + content slot) for an internal dashboard. Do not try to port the whole bundle's demo pages — port one layout, one menu partial, and one CRUD screen, then grow.
Twig → Blade mapping. Convert each construct mechanically rather than redesigning:
| Twig (bundle) | Blade |
|---|---|
{% extends 'X' %} |
@extends('x') |
{% block content %} |
@section('content') … @endsection |
{{ asset('...') }} |
{{ asset('...') }} (same intent, Symfony path base differs) |
{{ path('route_name') }} |
{{ route('route_name') }} |
{% if is_granted('ROLE_ADMIN') %} |
@can('admin') / @role('admin') |
{% include 'X' %} |
@include('x') |
{{ form_widget(form) }} |
Blade components or @include('components.input', [...]) |
{{ dump(x) }} |
@dump($x) / @dd($x) |
Config-driven sidebar. The bundle's Twig menu (if present) is typically a hardcoded tree. Replace it with a config array and a recursive Blade partial — this is the single biggest day-to-day win over the original:
// config/admin_menu.php
return [
['label' => 'Dashboard', 'route' => 'admin.dashboard', 'icon' => 'ki-outline ki-element-11'],
['label' => 'Users', 'icon' => 'ki-outline ki-profile-circle', 'children' => [
['label' => 'All users', 'route' => 'admin.users.index', 'active' => 'admin.users.*'],
['label' => 'Roles', 'route' => 'admin.roles.index', 'active' => 'admin.roles.*'],
]],
];
{{-- resources/views/layouts/partials/menu.blade.php --}}
@foreach (config('admin_menu', []) as $item)
@php($active = isset($item['active']) && request()->routeIs($item['active']))
@if (empty($item['children']))
<a href="{{ route($item['route']) }}"
class="menu-link {{ $active ? 'active' : '' }}">
<i class="fs-2 {{ $item['icon'] }}"></i>
<span class="menu-title ms-2">{{ $item['label'] }}</span>
</a>
@else
<div data-kt-menu-trigger="click"
class="menu-item menu-accordion {{ $active ? 'here show' : '' }}">
<span class="menu-link">
<i class="fs-2 {{ $item['icon'] }}"></i>
<span class="menu-title ms-2">{{ $item['label'] }}</span>
<span class="menu-arrow"></span>
</span>
<div class="menu-sub menu-sub-accordion">
@include('layouts.partials.menu-items', ['items' => $item['children']])
</div>
</div>
@endif
@endforeach
Asset pipeline. Do not carry over Symfony's Assetic/{% stylesheets %} blocks. Funnel the Metronic JS through Vite so it is versioned and tree-shaken with the rest of your app:
// resources/js/app.js
import './bootstrap';
import '../../public/vendor/metronic/js/components/util'; // or the bundle file
For CSS, either copy the compiled styles.css verbatim into public/vendor/metronic/css/ and link it in the layout, or import the SCSS source in resources/css/app.css if you intend to customize the palette. Mixing both causes duplicate Bootstrap resets.
Per-page JS. The Metronic plugins (menu accordion, drawer, stepper, datatables wrappers) initialize on KTMenu.createInstances() or the equivalent for your theme version. Fire them after the DOM is ready and after any SPA-style navigation:
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', () => {
if (window.KTMenu) KTMenu.createInstances();
});
</script>
@endpush
Forms. Symfony form themes become plain Bootstrap markup. Skip porting the theme files; write three or four Blade components (<x-input>, <x-select>, <x-checkbox>) and use @error / old() directly. The bundle's form theme is Bootstrap-3 markup and will fight Laravel's validation display.
Admin CRUD pattern. Pair the ported layout with a resource controller and a table partial, keeping the Metronic table classes on the <table> element and the pagination links styled with ->links('vendor.pagination.metronic') using a custom pagination view matching pagination/page-item/page-link classes.
Licensing is the biggest risk. Metronic is a commercial Keenthemes product. The bundle ships (or references) its assets; redistributing or deploying them without a valid Metronic license violates the theme's terms. Before copying Resources/public into public/, confirm you or your client owns a Metronic license. This is why "0 dependents" matters — nobody packages this responsibly.
Not installable in modern Laravel. Symfony 2.x is EOL and requires legacy PHP. Do not attempt composer require. If you inherit a legacy Symfony 2 project where this bundle is installed, pin symfony/symfony and expect to be stuck on an unsupported PHP branch — plan a migration rather than patching the bundle.
Bootstrap version drift. Verify which Bootstrap major the bundled CSS targets. Metronic 7.x → Bootstrap 4; Metronic 8 → Bootstrap 5. The utility classes (ml-2 vs ms-2, float-right vs float-end) are not interchangeable. Copying markup across versions produces silent layout breakage that looks fine in one viewport.
jQuery ordering. Many Metronic components depend on jQuery being loaded before the theme bundle and after Bootstrap. A Vite import order of import 'bootstrap'; import 'metronic' without jQuery will throw $ is not defined in the console long after the page renders. Load jQuery explicitly and register it as a global in vite.config.js if needed.
asset() path base. Twig asset() resolves against Symfony's web root; Laravel's resolves against public/. Paths copy cleanly, but the bundle may reference bundles/ruudymetronic/... — rewrite those to vendor/metronic/... or you get 404s that only show in the network tab.
No vendor:publish. There is no service provider, so no publish tag, no config merge, no view namespace. Keep a short upgrade note (docs/metronic-upstream.md) recording which bundle commit you copied assets from and which files you edited locally. Without a package manager boundary, an upstream sync means a manual diff.
Debugging checklist. White screen or unstyled page → check public/vendor/metronic/css/styles.css exists and the <link> is inside <head> before @stack('styles') (stack must come after the base stylesheet or your overrides lose the cascade). Sidebar not collapsing → KTMenu not initialized, or data-kt-menu-trigger attribute lost during the Twig→Blade rewrite. Broken icons → the Metronic icon font files under fonts/ weren't copied alongside css/.
Extension points don't transfer. The bundle's DependencyInjection/Configuration.php (if present) offers no analog. Use config/*.php + config()->set() in tests, or a small service class bound in a service provider for anything more than arrays.
Treat it as a code sample, not a dependency. With 7 stars, 0 dependents, and no tests, assume zero maintenance. Freeze a copy of the layout and assets you need inside your repo (with license verified), attribute it in a NOTICE/THIRD_PARTY.md, and never let your composer.json depend on it. That way a framework upgrade — which Laravel will force on you far sooner than this bundle ever sees a release — is a view migration, not a dependency crisis.
How can I help you explore Laravel packages today?