Theme Framework How the LeaseLeads theme frontend works

Data islands

A script[type=application/json] child carrying a collection or structured payload too big for an attribute.

On this page 4

Some payloads are too big or too nested to live in an attribute — a gallery's list of rendered image items, a map's markers plus their info-window HTML. For those a Module emits a Data island: a <script type="application/json"> child element whose text content is the JSON. The host reads it once, parses it, and builds DOM from it.

The PHP side

Emit the island as a direct child of the module element and json_encode a collection into it. Here LocationMap writes its markers and info windows:

php
<section class="module location-map animate ...">

  <script type="application/json"><?= json_encode(['markers' => $markers, 'infoWindows' => $info_windows]); ?></script>

  <div class="location-map__markers"></div>
  <div class="location-map__info-windows"></div>

The payload is a structured object of two collections — exactly what an attribute can't hold cleanly. Gallery does the same with a list, pre-rendering each item's rich-media markup into the island so the JS only has to materialize strings into nodes:

php
<script type="application/json">
  <?php
  // ...builds $media, each entry { element, category, key, id }
  echo json_encode($media);
  ?>
</script>

The JS side

The host reads its own island with a :scope > script selector and JSON.parses the text content. LocationMap scopes to the direct child so it can't accidentally read an island belonging to a nested module:

js
const island = this.element.querySelector(':scope > script[type="application/json"]')

if ( ! island ) {
  return
}

const { markers = [], infoWindows = [] } = JSON.parse(island.textContent)

MasonryGallery uses the same scoped read, decoding the list straight into wrapped DOM nodes:

js
const parsed = JSON.parse(this.element.querySelector(':scope > script[type="application/json"]').textContent)
Important

Always scope the read with :scope > script. A Module can contain nested Modules, each with its own island — an unscoped querySelector('script') would grab the first one in the subtree, not necessarily yours.

Once parsed, the collection becomes DOM. LocationMap turns each marker into an element and hands it back through the data-* bridge — the island seeds the markup, each generated node then carries its own scalar data-map-marker options:

js
markers.forEach(data => {
  const el = document.createElement('div')
  el.className = 'map-marker'
  el.dataset.mapMarker = JSON.stringify(data)
  markerContainer.appendChild(el)
})
Note

Why a script tag and not a giant attribute. type="application/json" is inert — the browser never executes or renders it, and it imposes no length or escaping limits the way an attribute value does. It's the right home for a payload whose shape is a collection or nested structure rather than flat scalar config.

The dividing line

Bridge Carries Read by
data-* option scalar config, flags, library options parseOptionsthis.options
Data island a collection or structured payload :scope > scriptJSON.parse

Scalar config → attribute. A collection or deeply nested payload → island. When in doubt, ask whether you'd be comfortable reading the value inline in the HTML; if it's a list, it's an island.