mastani/laravel-google-static-map
Generate static map images in Laravel with a fluent builder. Build signed Google Static Maps URLs or saved files with markers, paths, styles, zoom, and custom dimensions—ideal for emails, PDFs, invoices, and cached thumbnails with no JavaScript. A lightweight alternative to headless browsers.
Install and wire up (5 minutes)
composer require mastani/laravel-google-static-map
php artisan vendor:publish --provider="Mastani\GoogleStaticMap\GoogleStaticMapServiceProvider"
On Laravel 5.5+ the service provider and GoogleStaticMap facade are auto-discovered. On older versions, register them manually in config/app.php (providers array + aliases key 'GoogleStaticMap' => ...).
Then put a server-side key in .env (the config file reads it, typically GOOGLE_STATIC_MAP_KEY — confirm the exact key name in the published config/google-static-map.php):
GOOGLE_STATIC_MAP_KEY=AIza...
Where to look first
Mastani\GoogleStaticMap\...) — it's a thin URL builder, so read getMap() plus the marker/format methods end to end. It's a small file; 10 minutes of reading saves a day of guessing at array shapes.First use case
The canonical one: an order/shipping confirmation email with a pin on the delivery address.
use Mastani\GoogleStaticMap\GoogleStaticMap;
$url = GoogleStaticMap::getMap(51.5074, -0.1278, 14, 600, 300);
<img src="{{ $url }}" width="600" height="300" alt="Delivery location">
If that renders, you're done with setup. The next thing to build is almost always a cached, server-proxied version of that URL — see below.
Keep URL generation out of Blade. Build the URL in a controller, view composer, or a small MapImage service and hand the view a string. You will inevitably need caching, fallbacks, and a second size, and none of that belongs in a template.
class MapPreview
{
public function forCoordinates(float $lat, float $lng, int $zoom = 14, string $size = '600x300'): string
{
[$w, $h] = explode('x', $size);
return GoogleStaticMap::getMap($lat, $lng, $zoom, (int) $w, (int) $h);
}
}
Cache the URL, not just the request. Static map URLs are deterministic for the same inputs, so they're ideal cache values. Round coordinates to the zoom's precision so nearby points collapse to one entry:
$key = sprintf('map:%s:%s:%d:%dx%d', round($lat, 4), round($lng, 4), $zoom, $w, $h);
$url = Cache::remember($key, now()->addDay(), fn () => GoogleStaticMap::getMap($lat, $lng, $zoom, $w, $h));
This matters because every render is a billable Google request. Caching the URL string only helps if the browser/CDN also caches the image — which is why the proxy below is usually worth it.
Proxy through your own route. Instead of putting the Google URL (and your key) straight into src, expose /maps/preview/{lat}/{lng}.png that fetches the bytes once, stores them, and serves them from your domain:
Route::get('/maps/preview/{lat}/{lng}.png', function (float $lat, float $lng) {
$path = "maps/{$lat}_{$lng}_600x300.png";
if (! Storage::exists($path)) {
$url = GoogleStaticMap::getMap($lat, $lng, 14, 600, 300);
Storage::put($path, Http::get($url)->throw()->body());
}
return response(Storage::get($path), 200, [
'Content-Type' => 'image/png',
'Cache-Control' => 'public, max-age=2592000',
]);
});
You get: no key in HTML, your own long-lived cache headers, a stable URL that survives key rotation, and images that actually load in PDF renderers and email clients.
Markers. Marker data is the part most likely to bite you on array shape — check the facade for the exact keys. The typical call pattern is one marker per location with a color and a single-character label:
GoogleStaticMap::getMap($lat, $lng, 13, 800, 400)
->addMarker($lat, $lng, 'red', 'A'); // verify signature against source
For multi-stop itineraries, build the marker list from your own data and loop; don't hand-assemble the URL.
Emails vs PDFs vs views — treat them differently.
| Target | Approach |
|---|---|
| Blade view | Direct <img> with generated URL is fine; add width/height attributes to reserve layout space. |
Host the image yourself (proxy route, absolute HTTPS URL). Many clients block remote images by default — design the email to look acceptable with the image missing, and always set alt. |
|
| PDF (dompdf, snappy) | Do not rely on remote image fetching. Download the bytes at generation time and embed as base64 or a local file path. |
Generate a fixed set of sizes. Don't let arbitrary dimensions creep in from user input — pick three canonical sizes (thumb/hero/print) in config and reuse them. Google bills per request and caps dimensions (see Gotchas).
Testing. The package builds a URL; it doesn't call Google. So tests are cheap and should be pure string assertions:
$url = GoogleStaticMap::getMap(51.5074, -0.1278, 14, 600, 300);
$this->assertStringContainsString('center=51.5074,-0.1278', $url);
$this->assertStringContainsString('size=600x300', $url);
$this->assertStringContainsString('zoom=14', $url);
For the proxy route, Http::fake() with a small binary body so you never touch the network in CI.
API key restrictions will break you, and the error is opaque. Google Static Maps keys come in two flavours and they are mutually exclusive in practice:
If you're proxying (recommended), use an IP-restricted key. If you're embedding the Google URL directly, use referrer restriction. Getting this wrong returns a 403 or a grey "for development purposes only" tile, not a helpful message.
Your API key is in the HTML if you skip the proxy. $url contains key=.... Anyone can view-source it. Referrer restrictions mitigate this but don't eliminate it; the proxy route is the actual fix.
Size and zoom limits are real. The free Static Maps tier caps images at 640×640 (larger requires a premium/signed plan), and scale=2 doubles the pixel output — good for retina, but it still counts as the nominal size for limits. Zoom is 0–21; at high zoom over rural areas Google will just render "no imagery" tiles. Clamp both from config rather than trusting caller input.
URL length. Markers, styles, and paths are all query parameters. A handful of markers is fine; a 50-stop route will blow past the ~8 KB URL limit and you'll get a 414. For dense data, switch to encoded polylines or render server-side with a different tool.
Label and color strings need encoding. Marker labels are single characters in most tiers, and colors/labels with #, spaces, or Unicode must be URL-encoded. If the wrapper doesn't encode for you, do it yourself — an unencoded # silently truncates the URL at that point.
config:cache in production. The key is read from config, so after changing .env you must re-run php artisan config:cache (and clear it locally). A stale cached config with the old key is a classic "works locally, 403 in prod" cause.
Fail visibly, not with a broken image. Google returns a PNG error tile with a 400/403 status rather than failing loudly. When you download bytes in the proxy, check the HTTP status and validate the payload (getimagesize() returning false means you cached an error image). Better to serve a placeholder than to permanently cache a grey rectangle with a 30-day Cache-Control.
Watch the format. PNG is right for maps with markers and text (sharp edges); JPEG is smaller for photographic/dense terrain. The package supports both — pick per use case, and make sure your proxy's Content-Type matches what you actually requested.
Pin the version and wrap it. This is a 47-star, single-maintainer wrapper over a URL string builder — the surface area is small enough that the risk is low, but the escape hatch should be cheap. Put every call behind one class (MapPreview above) so swapping to hand-built URLs or another package is a one-file change. Also pin mastani/laravel-google-static-map to a caret-minor in composer.json and check Laravel version constraints on upgrade — small wrapper packages often lag on major framework releases.
The extension point is the config file, not the class. If you need different defaults per context (email vs admin dashboard vs PDF), don't subclass — set the config values at the call site or pass explicit zoom/size/dimensions per call and keep the config as baseline. The facade reads defaults; overriding per call is cleaner than fighting for a config override.
Don't forget attribution/licensing terms. Google's terms require attribution for map imagery in some contexts and prohibit caching beyond limited periods in others — worth a read before you set a 30-day cache TTL on a public-facing map.
How can I help you explore Laravel packages today?