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.
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;
11tysilently skips a_datafile it cannot parse, so a typo yields an emptydeckwith 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.jsonfiles; 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.
Related
- Static vs Dynamic Export Methods — parent guide framing when to bake maps at build time versus serve them live
- Exporting PyDeck Visualizations to Standalone HTML — producing the standalone artifact you pass through unchanged
- Deploying Generated Map Bundles with GitHub Actions CI/CD — running the Python and 11ty stages together in a reproducible pipeline
- Exporting Folium Maps to Static HTML with Embedded Assets — the Leaflet-based sibling of this build-time export pattern