TechEarl

Embed a Lazy-Loaded, Privacy-Friendly Google Map in WordPress

Embed a Google map in WordPress without tanking your load time: the free Maps Embed API iframe with loading="lazy", a te_map shortcode that escapes its output, an HTTP-referrer-locked API key, and a click-to-load consent pattern.

Ishan Karunaratne⏱️ 10 min readUpdated
Share thisCopied
Embed a lazy-loaded, privacy-friendly Google map in WordPress: the free Maps Embed API iframe with loading=lazy, a te_map shortcode, a referrer-restricted API key, and a click-to-load consent pattern.

To embed a Google map in WordPress without dragging your page speed down, use the free Maps Embed API iframe and add loading="lazy" so the browser defers the embed until the visitor scrolls near it:

html
<iframe
  title="Our office on Google Maps"
  width="600"
  height="450"
  style="border:0;max-width:100%"
  loading="lazy"
  referrerpolicy="no-referrer-when-downgrade"
  allowfullscreen
  src="https://www.google.com/maps/embed/v1/place?q=221B+Baker+Street,London&key=YOUR_API_KEY">
</iframe>

That is the whole baseline. The place mode of the Embed API takes a URL-escaped address (or place name, plus code, or place ID) in q and your key in key, and renders an interactive map pin. loading="lazy" is the part that protects your performance: an offscreen map iframe is not fetched until the user is about to see it, so a map sitting in your footer costs nothing on first paint.

The rest of this article turns that into something you would actually ship on a real WordPress site: a reusable shortcode that escapes its output, a locked-down API key, and a click-to-load pattern that fires zero Google requests until the visitor opts in.

Why the embed iframe, not the JavaScript API

There are two completely different Google Maps products, and the billing gap between them is the reason this article exists.

  • The Maps Embed API (the /maps/embed/v1/ iframe above) has unlimited free usage with no per-load charge. For "show where my business is," it is the correct tool.
  • The Maps JavaScript API is the pay-as-you-go product. Each dynamic map load is a billable event (roughly $7 per 1,000 loads beyond the monthly free allotment), and it pulls in a much heavier JavaScript payload.

If all you need is a pin on an address, reaching for the JavaScript API is paying real money and real kilobytes for a feature you do not use. Use the Embed API and spend your performance budget elsewhere, the way I argue for on the head-cleanup pass.

A te_map shortcode that escapes its output

Pasting raw iframe HTML into the editor works once. The moment you want the same map in three places, or you let a non-developer place it, you want a shortcode. Here is a small must-use plugin that registers [te_map address="..."] and builds the iframe through esc_url() and esc_attr() so nothing the user types lands in the markup unescaped:

php
<?php
/**
 * Plugin Name: TE Map Embed
 * Plugin URI:  https://techearl.com/wordpress-lazy-load-google-maps-embed
 * Description: A [te_map] shortcode that outputs a lazy-loaded, escaped Google Maps Embed API iframe.
 * Version:     1.0.0
 * Author:      Ishan Karunaratne
 * Author URI:  https://techearl.com
 * License:     GPL-2.0-or-later
 * Text Domain: te-map-embed
 */

defined( 'ABSPATH' ) || exit;

function te_map_embed( $address, $height = 450 ) {
    $key = defined( 'TE_MAPS_EMBED_KEY' ) ? TE_MAPS_EMBED_KEY : '';

    if ( '' === $key || '' === trim( (string) $address ) ) {
        return '';
    }

    $src = add_query_arg(
        array(
            'q'   => rawurlencode( $address ),
            'key' => $key,
        ),
        'https://www.google.com/maps/embed/v1/place'
    );

    return sprintf(
        '<iframe class="te-map" title="%1$s" loading="lazy" height="%2$d" ' .
        'style="border:0;width:100%%;max-width:100%%" ' .
        'referrerpolicy="no-referrer-when-downgrade" allowfullscreen src="%3$s"></iframe>',
        esc_attr( sprintf( __( 'Map of %s', 'te-map-embed' ), $address ) ),
        absint( $height ),
        esc_url( $src )
    );
}

function te_map_shortcode( $atts ) {
    $atts = shortcode_atts(
        array(
            'address' => '',
            'height'  => 450,
        ),
        $atts,
        'te_map'
    );

    return te_map_embed( $atts['address'], $atts['height'] );
}
add_shortcode( 'te_map', 'te_map_shortcode' );

Drop that at wp-content/mu-plugins/te-map-embed.php and define the key once in wp-config.php:

php
define( 'TE_MAPS_EMBED_KEY', 'YOUR_API_KEY' );

Now an author writes [te_map address="221B Baker Street, London"] and gets a properly escaped, lazy-loaded map. A few details worth calling out:

  • rawurlencode() on the address turns spaces and commas into %20/%2C so the q parameter is valid; esc_url() then sanitizes the assembled URL for output.
  • The key lives in wp-config.php, not in post content, so swapping it is a one-line change and it never gets copy-pasted into a dozen posts.
  • The __() / _e() text-domain calls (te-map-embed) keep the title string translatable, which is just good plugin hygiene.

Lock the API key down with an HTTP-referrer restriction

Here is the part people skip and then panic about: a Maps Embed API key is exposed in your HTML by design. Anyone can view-source, copy your key=..., and use it on their own site. There is no server-side proxy that hides it, because the iframe src is fetched by the visitor's browser, not your server.

The defense is not to hide the key. It is to make a stolen key useless anywhere but your domain. In Google Cloud Console, under APIs & Services → Credentials, edit the key and set Application restrictions → Websites (an HTTP-referrer restriction). Add your domain entries:

text
https://techearl.com/*
https://www.techearl.com/*

Wildcard support is limited but enough: * substitutes for a single subdomain or a path segment, so *.techearl.com/* covers subdomains. With the referrer restriction in place, a request carrying your key but a referrer of attacker.example is rejected by Google with RefererNotAllowedMapError, and it never counts against you. Pair that with an API restriction limiting the key to just the Maps Embed API, so even a leaked key cannot be turned against your billable services.

This is the one non-negotiable step. An unrestricted Maps key on a public page is the kind of thing that turns into a billing surprise on the other Maps products if you ever reuse the key. Restrict it to the embed, restrict it to your hosts, and the exposure stops mattering.

Click-to-load: zero Google requests until the user opts in

loading="lazy" solves performance for maps that are below the fold. It does not solve two other things:

  1. Privacy / consent. The moment the iframe loads, the visitor's browser talks to Google and ships their IP and referrer. Under GDPR that is a third-party data transfer you may need consent for before it happens, not after.
  2. Above-the-fold maps. A map at the top of a contact page is in the viewport immediately, so lazy-loading never defers it.

The fix for both is click-to-load: render a cheap placeholder, and only inject the real iframe when the visitor clicks it. No Google request fires until they ask for the map.

html
<div class="te-map-consent"
     data-src="https://www.google.com/maps/embed/v1/place?q=221B+Baker+Street,London&key=YOUR_API_KEY">
  <button type="button" class="te-map-load">
    Load map (connects to Google)
  </button>
</div>

A tiny vanilla-JS helper swaps the placeholder for the iframe on click. Note the te-prefixed namespace object, which keeps the helper from colliding with anything else on the page:

javascript
const te = {
  loadMap(box) {
    const src = box.getAttribute("data-src");
    if (!src) return;
    const frame = document.createElement("iframe");
    frame.src = src;
    frame.loading = "lazy";
    frame.title = "Google map";
    frame.allowFullscreen = true;
    frame.referrerPolicy = "no-referrer-when-downgrade";
    frame.style.cssText = "border:0;width:100%;height:450px";
    box.replaceChildren(frame);
  },
};

document.addEventListener("click", (event) => {
  const button = event.target.closest(".te-map-load");
  if (button) te.loadMap(button.closest(".te-map-consent"));
});

The placeholder can be a styled <div> (a grey box with the button) or a static map image if you want it to look like a real map. Either way, the third-party connection is deferred until intent is expressed, which is both the privacy-correct behavior and the fastest possible first load. You can wire the same button into a consent-manager so it only appears once the visitor has accepted "maps" in your cookie banner. The same "do not load the third party until the user opts in" thinking applies to analytics, which is why I load Google Tag Manager behind consent rather than on every page view.

Verify it worked

After placing a map, open the page and check three things:

  • View-source / Elements: the map <iframe> carries loading="lazy", and (for a footer or below-fold map) the browser Network tab shows the maps/embed/v1/place request only firing as you scroll toward it, not on initial load.
  • The key is restricted: temporarily test the embed URL with your key from a different domain (or just trust the Cloud Console "Application restrictions: Websites" entry). A request from a non-allowed referrer returns RefererNotAllowedMapError.
  • Click-to-load fires nothing early: with the consent variant, the Network tab should show no request to google.com/maps until you click the button. If you see one on page load, the placeholder is accidentally rendering the real iframe.

If the map shows a "For development purposes only" watermark or a grey box, the usual causes are an unrestricted-then-mis-restricted key (your domain is not in the Websites list), billing not enabled on the Cloud project, or the Maps Embed API not enabled for that key.

See also

Sources

Authoritative references this article was fact-checked against.

TagsWordPressPerformancePHPGoogle MapsPage SpeedPrivacy

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

Add a Custom WP-CLI Command in WordPress

How to register a custom WP-CLI command: guard it with defined('WP_CLI'), wire it up with WP_CLI::add_command(), turn class methods into subcommands, document args with @synopsis, and show progress with make_progress_bar().

Disable jQuery Migrate in WordPress

How to disable jQuery Migrate in WordPress: remove the jquery-migrate dependency on the front end so the compatibility shim stops loading, plus the testing step that tells you whether it is safe.