Skip to content

Private Islands

Gaurav Tiwari edited this page Aug 27, 2026 · 1 revision

Private Islands

A page cache stores one HTML copy and serves it to everyone. That breaks the moment a page contains something per-visitor — a cart count, an "Account" / "Log in" link. The usual outcomes are both bad: stop caching the page, or cache one visitor's cart count and show it to everybody.

Private Islands keeps the page cacheable and fills the per-visitor parts in afterwards.

Off by default. Enable under Integrations → Private islands.

How it works

1. You place an island.

[gtperf_private_island id="cart_count"]

2. The plugin renders a public shell. Only the safe fallback goes in the HTML:

<span data-gtp-private-island="cart_count"
      data-gtp-signature="a1b2c3…"
      aria-live="polite">0</span>

3. The shell is scrubbed before it is cached. This is the part that matters. On the way into the cache, prepareHtml() walks every [data-gtp-private-island] node, removes every child, and re-inserts only the tag-stripped fallback. Even if a theme or another plugin rendered a real customer's data inside that element, it never reaches the stored file. Islands whose ID is not in the registry lose the attribute entirely.

4. The browser asks for the real values. private-islands.js collects every island on the page and sends one POST to admin-ajax.php with credentials: "same-origin", so the visitor's own session identifies them.

5. The endpoint answers only what it can prove. Each island carries an HMAC signature bound to its ID:

hash_hmac( 'sha256', 'v1|' . $fragmentId, $key )

The handler verifies that signature, checks the ID against the registry allowlist, caps the batch at 10, and returns rendered HTML as JSON with nocache_headers(), no-store, and X-GT-Private-Fragments: BYPASS.

6. JavaScript swaps each island in place, setting data-gtp-private-ready="true". aria-live="polite" means a screen reader announces the change rather than missing it.

Why the signature exists

Without it, the endpoint would be an open renderer: anyone could POST an arbitrary fragment ID and have the site render it. The signature is keyed to the site and bound to the specific fragment ID, so the endpoint will only render fragments the page itself actually placed. It is not a nonce and does not expire — it is a capability token proving "this site emitted this island".

Built-in fragments

ID Fallback Renders
cart_count 0 Item count from the active commerce plugin
account Account Account or login link for the current visitor

Both are individually switchable.

Failure behaviour

If the fetch fails — offline, blocked, 500, JSON parse error — the .catch() deliberately does nothing. The public fallback stays on screen. A visitor sees 0 and Account rather than a broken element, and the page is still the cached copy that loaded instantly.

Extending it

add_filter( 'gt_performance_private_fragments', function ( array $fragments ) {
    $fragments['loyalty_points'] = array(
        'fallback' => '',
        'render'   => fn() => esc_html( my_points_for_current_user() ),
    );
    return $fragments;
} );

Two rules for a custom fragment:

  • The fallback must be safe for everyone. It is what gets cached and served to every visitor, including search engines.
  • The renderer must not assume an admin context. It runs on a public AJAX endpoint for logged-out visitors too.

The cart count and account URL can also be overridden directly with gt_performance_private_cart_count and gt_performance_private_account_url.

When not to use it

If the whole page differs per visitor, do not paper over it with islands — bypass the page instead. Islands are for a cacheable page with a few dynamic spots, not for a dashboard.

Related

Page Cache · Commerce Safety · Hooks Reference

Clone this wiki locally