Serving PyDeck Output Through 11ty Data Pipelines

Part of the Static vs Dynamic Export Methods guide.

Operative rule: treat a PyDeck map as build-time data, not a runtime service — generate the deck spec once in a Python step, hand it to 11ty as data, and let the static build template it into the page.

How the Build-Time Handoff Works

pydeck is a Python library and 11ty is a Node static-site generator, so they never share a process. The clean architecture is a two-stage build: a Python step produces a portable artifact, and 11ty consumes that artifact as ordinary data during its own build. The artifact of choice is the output of pdk.Deck().to_json(), a self-describing JSON string listing the deck’s layers, initial_view_state, and map style. Because it is plain JSON, it drops straight into 11ty’s global data system with no glue code.

There are two consumption patterns, and the choice depends on how much control you want over the surrounding markup. When the Python step writes the deck spec to a file under the _data/ directory, 11ty exposes it to every template as a global data key, and a Nunjucks template injects it into a <script> block that the Deck.gl JSON renderer reads at page load. When the Python step instead produces a complete standalone HTML file — the same output described in exporting PyDeck visualizations to standalone HTML — you register it as a passthrough copy and publish it verbatim, optionally embedding it via <iframe>. Either way the map is frozen at build time, exactly as with exporting Folium maps to static HTML with embedded assets, placing this squarely on the static side of the static vs dynamic export methods spectrum.

PyDeck to 11ty build-time data handoff Stage one: Python runs pydeck and writes deck.json into the 11ty _data directory. Stage two: 11ty reads it as global data and a Nunjucks template renders the deck into a static HTML page. The two stages run in sequence in one build. Stage 1 · Python Stage 2 · 11ty (Node) pdk.Deck() .to_json() write file _data/deck.json in source tree read as data Nunjucks template injects spec static page deck baked in one build, two sequential stages · map artifact is data, not a runtime service

Production-Ready Implementation

The Python step below builds a deck, captures its JSON spec, and writes it into the 11ty source tree under _data/. The filename (deck.json) becomes the global data key deck, available in every template. Writing valid JSON — not a Python repr — is the one detail that matters, so the spec is parsed and re-serialized to guarantee it round-trips.

from __future__ import annotations

import json
from pathlib import Path

import pandas as pd
import pydeck as pdk


def build_deck_spec(data_dir: Path) -> Path:
    """Generate a PyDeck spec and emit it as an 11ty global-data file."""
    df = pd.DataFrame(
        {
            "lat": [40.7128, 34.0522, 41.8781],
            "lon": [-74.0060, -118.2437, -87.6298],
            "weight": [120, 85, 200],
        }
    )

    layer = pdk.Layer(
        "ScatterplotLayer",
        data=df.to_dict(orient="records"),
        get_position=["lon", "lat"],   # Deck.gl expects [longitude, latitude]
        get_radius="weight * 100",
        get_fill_color=[255, 140, 0, 180],
        pickable=True,
    )

    deck = pdk.Deck(
        layers=[layer],
        initial_view_state=pdk.ViewState(latitude=38.5, longitude=-96.0, zoom=3),
        map_style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
    )

    # to_json() returns a JSON *string*; parse then re-dump to validate it.
    spec: dict = json.loads(deck.to_json())

    data_dir.mkdir(parents=True, exist_ok=True)
    out = data_dir / "deck.json"          # -> global data key: `deck`
    out.write_text(json.dumps(spec, indent=2), encoding="utf-8")
    return out.resolve()


if __name__ == "__main__":
    # Write into the 11ty _data directory so the next build picks it up.
    written = build_deck_spec(Path("_data"))
    print(f"Emitted deck spec: {written}")

The Nunjucks template consumes the deck global and renders it. The Deck.gl JSON runtime reads the spec from an inline <script type="application/json"> block — no data is fetched at runtime, so the page is fully static once built.

---
layout: base
title: City weights
---
<div id="deck" class="map-panel"></div>

<script type="application/json" id="deck-spec">
  {{ deck | dump | safe }}
</script>

<script src="/assets/deckgl-json.js"></script>
<script>
  const spec = JSON.parse(document.getElementById('deck-spec').textContent);
  const { JSONConverter } = deck;
  const converter = new JSONConverter({ configuration: {} });
  new deck.DeckGL({
    container: 'deck',
    ...converter.convert(spec),
  });
</script>

Alternative Variants

Passthrough copy for a pre-rendered standalone file

When the Python step already produced a complete standalone HTML export, skip the data plumbing entirely and publish the file unchanged. Register a passthrough copy in the 11ty config so the build emits the artifact into the output directory verbatim, then reference it from any page with an <iframe>.

// eleventy.config.js
export default function (eleventyConfig) {
  // Copy Python-generated map bundles straight into the built site.
  eleventyConfig.addPassthroughCopy({ "build/maps": "maps" });
}

Emit pattern reference

Python output 11ty mechanism Template use Best when
to_json() string _data/deck.json global inject into <script> You control page markup
Standalone .html passthrough copy <iframe src> Deck is complete as-is
Many decks _data/decks/*.json loop / pagination One page per dataset
Deck + metadata _data/deck.js (returns object) mix data + computed fields Titles, timestamps needed

A .js data file in _data/ can require the JSON spec and attach build metadata like a generation timestamp or a content hash, which downstream pages can print. Regenerating these files on a schedule is the job of serving output through automated rebuilds, and shipping them reproducibly is the job of deploying generated map bundles with GitHub Actions CI/CD.

Verification Steps

Confirm the handoff before wiring it into a deploy:

  • Data file is valid JSON — run the spec through a JSON parser; 11ty silently skips a _data file it cannot parse, so a typo yields an empty deck with no error.
  • Global key resolves — add {{ deck.initialViewState | dump }} temporarily to a page and confirm the view state prints, proving the filename mapped to the expected data key.
  • Spec injected, not fetched — open the built page’s Network panel; there should be no request for the deck data, since it is inlined in the <script type="application/json"> block.
  • Deck renders from the spec — load the page and confirm layers appear; a blank canvas with a valid spec usually means the Deck.gl JSON runtime script did not load.
  • Rebuild is deterministic — run the Python step twice and diff the two deck.json files; identical inputs must produce identical output for the build to be reproducible.

Common Errors & Fixes

The deck global is undefined in the template

11ty keys global data by filename, so _data/deck.json becomes deck. If the file lives elsewhere, is named differently, or contains invalid JSON, the key is absent and templating fails silently. Confirm the file sits directly under the configured _data directory, has a .json (or .js) extension, and parses cleanly.

The page shows the raw spec text instead of a map

The Nunjucks output was HTML-escaped, so the JSON printed as visible text rather than machine-readable script content. Pipe the value through | dump | safe when writing it into a <script type="application/json"> block so quotes are preserved and the markup is not escaped. Read it back with JSON.parse(...textContent).

Deck.gl reports an unknown layer type

The to_json() spec references a layer the browser bundle does not include, or the JSON runtime configuration omits it. Ensure the Deck.gl script you load is the JSON-capable build and that the configuration passed to JSONConverter registers any non-core layers. Core layers like ScatterplotLayer work with the default configuration.

Stale map after data changes

The _data file was not regenerated before the 11ty build ran, so the site baked an old spec. Order the pipeline so the Python step always completes before 11ty starts, and treat the _data/deck.json file as a build artifact to be recreated on every run, never a checked-in source of truth.