bagusindrayana/laravel-map
Laravel Map is a lightweight package for adding interactive maps to Laravel applications, with Blade components, configurable markers and popups, and a simple API for rendering maps from coordinates or addresses with minimal setup.
Install and publish everything the package ships (config, views, and the Leaflet/JS assets), because nothing works until the frontend assets exist under public/vendor/:
composer require bagusindrayana/laravel-map
php artisan vendor:publish --provider="Bagusindrayana\LaravelMap\LaravelMapServiceProvider"
composer dump-autoload -o
php artisan optimize:clear
The provider/namespace string is the first thing to confirm — v1.0.6 changed the PSR-4 name, so check the package's composer.json autoload.psr-4 block and match the provider class exactly. A wrong provider string means vendor:publish silently publishes nothing.
Where to look first, in order:
config/laravel-map.php (or whatever the published config is named) — center lat/lng, zoom, tile provider URL and attribution, asset paths. This is what you will change most often.resources/views/vendor/laravel-map/ — the published Blade view/component that emits the map container and the init script. Read it before you try to style or wrap it.public/vendor/laravel-map/ — the bundled Leaflet and package JS. If these are missing, everything else is moot.fetch method — read it to learn whether it lives server-side (a facade/service returning JSON config) or client-side (a JS helper). Its contract determines your dynamic-map design.First use case — the one you should get working before anything else: render one marker from an Eloquent model.
// Controller
public function show(Store $store)
{
return view('stores.show', [
'store' => $store,
'map' => $store->only(['latitude', 'longitude', 'name']),
]);
}
{{-- resources/views/stores/show.blade.php --}}
@push('styles')
<link rel="stylesheet" href="{{ asset('vendor/laravel-map/leaflet.css') }}">
@endpush
<x-laravel-map :center="[$map['latitude'], $map['longitude']]" :zoom="14" />
@push('scripts')
<script src="{{ asset('vendor/laravel-map/leaflet.js') }}"></script>
@endpush
Exact component/directive names and asset filenames vary — copy them from the published view rather than guessing. Confirm one marker renders on a real page before wiring a list of models. If that single marker works, the tiles, asset paths, and config are all correct, and every later problem is data-shape or JS, not installation.
Because the package is archived (last release 2021-12-28, 4 stars), treat installation as "vendor it deliberately": pin the exact version in composer.json, commit the published assets, and note in your README that the package is unmaintained. Day-to-day leverage then comes from the small stable surface (config + Blade + a bit of JS), not from upstream fixes.
Controller shapes the data; Blade only renders. Keep the map dumb. Build a plain array or JsonResource in the controller and hand it to the component. Do not call Model::all() from a Blade template or loop over Eloquent models inside the view — you lose N+1 visibility and you cannot test the shape.
// A stable marker shape you can reuse for both Blade and JSON
private function markers(Collection $stores): array
{
return $stores->map(fn ($s) => [
'lat' => (float) $s->latitude,
'lng' => (float) $s->longitude,
'label' => $s->name,
'url' => route('stores.show', $s),
])->all();
}
Use this same mapper for the server-rendered component and for the fetch endpoint so markers look identical however they arrive.
Model plotting via latitude/longitude. The package accepts Eloquent models and reads latitude/longitude attributes. If your columns are named differently (lat/lng, geo_lat, casts to Point), do not rename your schema to suit the package — map to the expected keys in the controller as above. Keep the model's raw attributes out of the view.
Static map (server-rendered, recommended default). For dashboards, detail pages, and admin views with a bounded marker set, render everything in the initial page load. Fewer moving parts, no endpoint to secure, no empty-state flicker. Cache the page fragment if the query is expensive.
Dynamic map via fetch. Use v1.0.6's fetch path when markers depend on user interaction (filters, search, bounding-box pan, pagination) or when the marker set is large. Give it a dedicated, versioned JSON route:
Route::get('/maps/{map}/markers', [MapMarkerController::class, 'index'])
->name('maps.markers');
// controller
public function index(Request $request, Map $map)
{
$this->authorize('view', $map);
return response()->json([
'data' => $this->markers(
$map->stores()
->when($request->query('q'), fn ($q, $term) => $q->where('name', 'like', "%{$term}%"))
->limit(500)
->get()
),
])->header('Cache-Control', 'private, max-age=30');
}
Freeze the response contract (data[].lat, .lng, .label, .url) and document it next to the endpoint — the package JS is coupled to those keys, and a rename breaks rendering without a server error.
Guard against double-initialization. In Livewire, Turbo, or any partial-reload setup, wrap asset pushes and map init in @once and initialize in an updated/mounted hook, not on every render. Leaflet initialized twice on the same container produces a broken, grey, or doubled map rather than a clear error.
@once
@push('scripts')
<script src="{{ asset('vendor/laravel-map/leaflet.js') }}"></script>
@endpush
@endonce
Vite vs. published assets — pick one. If your app already bundles Leaflet through Vite, do not also load the package's copy. Either import Leaflet in app.js and let the package only supply config/markup, or use the published vendor files and keep Leaflet out of your bundle. Two Leaflet instances on one page is the single most common cause of a "map renders but markers/controls are dead" bug.
Livewire / Inertia.
mounted, and on filter changes dispatch a browser event ($this->dispatch('markers-updated', markers: $markers)) that your JS listens for — then call the map's marker layer update, don't re-init.fetch pattern on prop changes instead of a full page visit, and clean up the Leaflet instance in onUnmounted (or before re-init) to avoid leaked handlers.Testing. Feature-test the JSON endpoint — status, auth scoping, payload shape, and that a forbidden model returns 403. You cannot meaningfully test Leaflet rendering in PHPUnit; keep a documented manual smoke checklist (map renders, tiles load, markers clickable, fetch returns after a filter change) and run it on every dependency or asset change.
The archived status is the real gotcha. No upstream fixes, no security patches for the bundled Leaflet, single-maintainer history. Pin the exact tag, commit published assets into your repo, and assign an owner. If you need a fix, plan to fork or patch in place — budget for that rather than filing an issue.
PSR-4 rename broke more than the provider string. Any reference to the old namespace — a facade alias in config/app.php, use statements in your code, fully-qualified class names inside published views, cached config — fails with a class-not-found or, worse, renders an empty map. Before upgrading, grep: grep -rn "Old\\\\Namespace" app/ config/ resources/views/.
Published assets never update themselves. The v1.0.6 Leaflet and JS bumps only reach your app if you re-publish with --force:
php artisan vendor:publish --tag=laravel-map-assets --force
Then cache-bust (asset() with a version query, or Vite hashing) so browsers don't serve the old JS against new markup.
Leaflet version jump can break add-ons. If you added marker clustering, draw tools, or heatmap plugins on top of the bundled Leaflet, retest them. Plugins are the usual casualty of a Leaflet bump, and you will not get an upstream compatibility note.
fetch failures are silent by default. Handle non-200s, malformed JSON, and empty arrays explicitly and show an error/empty state — otherwise the user sees a blank map with no clue why. Include the CSRF token header for any POST-based fetch, set credentials/X-Requested-With as the endpoint expects, and set cache headers for slow-moving data.
CSRF 419s on dynamic maps. A fetch hitting a web-middleware route without the token will 419 and — if errors are swallowed — look like "no markers." Either use a GET endpoint (preferred for map data) or read the token from <meta name="csrf-token">.
Tile provider is a hidden dependency. Gray tiles usually mean a missing/invalid API key or an exhausted quota, not a bug in the package. Put the tile URL and key in config/env, and log or surface a visible message when tiles fail — this presents as a plain grey background far more often than as an error.
Clear caches after any namespace or config change. php artisan config:clear view:clear route:clear — stale cached config pointing at an old namespace is the classic "works locally, dead in staging" failure.
Debugging order. Browser console (JS/Leaflet errors, double-load warnings) → Network tab (the fetch request, status, payload shape) → storage/logs/laravel.log (server-side) → php artisan about to confirm the installed version matches what you think. Do not start in the package source; it is short and rarely the culprit.
Performance ceiling. Leaflet is client-side, so server scaling is not the concern. Thousands of markers need clustering (add a Leaflet plugin, which reintroduces the version-compatibility risk above), and large GeoJSON payloads need server-side pagination or bbox filtering in the fetch endpoint. Cache slow queries.
Extension points — prefer the safe ones. Change behavior through the published config and published Blade views, not by editing public/vendor/... or the package's JS bundle. For custom marker rendering (icons, popups with HTML, badges), write your own wrapper Blade partial or a thin component that composes the package's component, and pass a pre-built marker array. That keeps your code upgradeable and makes a future fork straightforward.
Separate your risks. If you ever touch both the namespace/asset upgrade and the fetch integration, do it in two commits/releases. A blank map after a combined change is much harder to attribute.
How can I help you explore Laravel packages today?