Skip to content

Large data: PMTiles, FlatGeobuf, GeoParquet & COG

“Cloud-native” files are made to be read in parts, straight from a web server: the browser asks for the bytes it needs (HTTP range requests), so a map can draw a city from a country-sized file without downloading all of it. Since 0.9, maplibre-yaml reads four of them with nothing but YAML — in plain HTML from the CDN, in Astro, and under any bundler:

FormatFileHoldsSource type:
PMTiles.pmtilesVector tiles, raster tiles or elevation, in one filevector, raster, raster-dem
FlatGeobuf.fgbFeatures (points, lines, polygons) with their propertiesgeojson
GeoParquet.parquet, .geoparquetFeatures in a columnar table: only the columns a map uses are readgeojson
Cloud Optimized GeoTIFF (COG).tif, .tiffOne image: imagery, elevation, or a band of values (in Web Mercator)raster, raster-dem

A Protomaps .pmtiles file can also be a whole basemap, from one line of YAML (Host your own basemap).

A PMTiles archive (imagery and outlines) and a FlatGeobuf file (the coloured districts). Click a district.

The rule: type: is what MapLibre draws, the URL is what the file is

Section titled “The rule: type: is what MapLibre draws, the URL is what the file is”

You keep writing the source type MapLibre knows — vector, raster, raster-dem, geojson — and point url: at the file. The file name tells maplibre-yaml how to read it.

type: map
id: cloud-native-minimal
config:
center: [10.2, 42.15]
zoom: 10
mapStyle: "https://tiles.openfreemap.org/styles/positron"
sources:
outlines:
type: vector
url: /gallery-assets/cloud-native/districts.pmtiles
districts:
type: geojson
url: /gallery-assets/cloud-native/districts.fgb
layers:
- id: districts
type: fill
source: districts
paint: { fill-color: "#e9c46a", fill-opacity: 0.6 }
- id: outlines
type: line
source: outlines
source-layer: districts
paint: { line-color: "#1d3557" }

That is the whole setup: no <script> to register anything, no extra import map entry. Layers, popups, the legend, fitTo and clustering work exactly as they do for any other source.

  • pmtiles:// in front of the URL works too (url: pmtiles://data/x.pmtiles or pmtiles://https://example.org/x.pmtiles) — it’s the form MapLibre’s own examples use.
  • format: names the format when the URL can’t — a signed link or an API endpoint with no file extension. It wins over the URL.
sources:
parcels:
type: geojson
url: "https://data.example.org/api/parcels?f=fgb"
format: flatgeobuf

One .pmtiles file holds a whole tile pyramid. Vector, raster and elevation archives all work:

sources:
buildings:
type: vector
url: data/buildings.pmtiles # layers say which source-layer to draw
imagery:
type: raster
url: data/imagery.pmtiles
tileSize: 256 # most raster archives use 256-pixel tiles
terrain:
type: raster-dem
url: data/terrain.pmtiles
encoding: terrarium
  • The archive’s own metadata (zoom range, bounds, attribution, vector layer names) reaches MapLibre, so you rarely need minzoom/maxzoom here.
  • Framing: a tile archive has no client-side extent to fit, so fitTo doesn’t apply to PMTiles sources in 0.9 — set center/zoom (or bounds) for where the data is.
  • Making one: tippecanoe -o buildings.pmtiles buildings.geojson (vector), ogr2ogr -f PMTiles out.pmtiles in.gpkg, or pmtiles convert in.mbtiles out.pmtiles for an existing MBTiles file (a browser can’t read .mbtiles directly).
  • A page that registers its own pmtiles protocol keeps it. If your page already does addProtocol("pmtiles", …) through @maplibre-yaml/core/maplibre (the 0.7 way, still supported — for custom headers or a custom Source), maplibre-yaml leaves that registration alone.

A basemap style loaded from a URL can use PMTiles too, with no page JavaScript: mapStyle: (or basemap: in format v2) can point at a style whose sources are pmtiles:// URLs, such as a Protomaps basemap or one you host yourself (below). maplibre-yaml registers the pmtiles:// protocol before the map is created, so the style’s first requests find it. A page that registered its own pmtiles protocol keeps it.

Relative URLs inside the style are relative to the style.json, the way a stylesheet’s url() is relative to the stylesheet. That applies to a source’s url (including the file inside pmtiles://), tiles, sprite and glyphs. MapLibre by itself would resolve them against the page, which breaks as soon as the page and the style live in different folders. base applies to the style URL itself, so a style under base="/my-repo/" finds its own files under /my-repo/ too. If the style names a file that isn’t there, the map’s notice says which one, instead of showing a blank basemap.

A basemap can be one PMTiles file next to your site, served by the same static host, with no tile server and no API key. Point mapStyle: at the file (mapStyle: data/nyc.pmtiles) and maplibre-yaml builds the whole Protomaps basemap style around it, in the page:

A Protomaps basemap from one 0.6 MB .pmtiles file served with these docs (DUMBO and Brooklyn Heights). Data © OpenStreetMap contributors.
  1. Cut a regional extract from the Protomaps daily planet build with the pmtiles command-line tool:

    Terminal window
    pmtiles extract https://build.protomaps.com/<date>.pmtiles nyc.pmtiles \
    --bbox=-74.26,40.48,-73.70,40.92 --maxzoom=13

    Pick <date> from build.protomaps.com. The tool reads only the part of the planet it needs. This extract is about 12 MB; without --maxzoom=13 (all zoom levels, to 15) it is about 95 MB. Zoom 13 is enough for a city map: MapLibre draws closer zooms from the zoom-13 tiles, with fewer minor street names (see Size below).

  2. Put the file in your site: public/data/nyc.pmtiles in an Astro or Vite project, or a data/ folder next to your HTML page.

  3. Use it from your document:

    config:
    center: [-73.98, 40.71]
    zoom: 12
    mapStyle: data/nyc.pmtiles

    That is the light flavor, with labels in the page’s language. To choose, write mapStyle: as a block:

    config:
    center: [-73.98, 40.71]
    zoom: 12
    mapStyle:
    pmtiles: data/nyc.pmtiles
    flavor: light # dark, white, grayscale or black
    lang: en # optional: label language

That’s the whole basemap. Details:

  • Paths. A relative pmtiles: path resolves against the page, and base applies to it like to every other path in the document, so the same YAML works on a GitHub Pages project site (<ml-map base="/my-repo/">; the Astro components set it from your site’s base). A full URL (https://…/nyc.pmtiles) or a pmtiles:// URL works too, from any server with range requests.

  • Labels and icons come from Protomaps’ hosted fonts and sprites (protomaps.github.io/basemaps-assets), so labels need that host to be reachable; when it isn’t, the map’s notice says so. To host everything yourself, copy the fonts and the sprite for your flavor from basemaps-assets and name them:

    mapStyle:
    pmtiles: data/nyc.pmtiles
    glyphs: fonts/{fontstack}/{range}.pbf
    sprite: sprites/v4/light
  • Label language. lang: picks the language labels are drawn in where OpenStreetMap has the name (en, es, fr, de, ja, zh-Hans, and about 40 more). Without it, labels follow the page’s lang when the basemap has that language, and English otherwise. Arabic, Hebrew, Persian and Urdu labels (ar, he, fa, ur) are written right to left: maplibre-gl 6.9 and later draw them as they are; with an older maplibre-gl, maplibre-yaml sets MapLibre’s right-to-left text plugin, which the browser downloads from unpkg.com the first time such a label is drawn.

  • Your layers draw on top of the basemap, as with any basemap. before: can name a basemap layer to slide one underneath, for example before: places_subplace to keep neighborhood names above your data, and a story chapter can restyle basemap layers by id (water, earth, landuse_park, roads_minor, buildings, …; map.getStyle().layers lists them all).

  • Attribution to OpenStreetMap and Protomaps is added to the map.

  • When something is wrong, the map shows a notice instead of a blank basemap: a file that isn’t there, a server without range requests, or a PMTiles file that isn’t a Protomaps basemap (your own tiles go in sources: with layers).

  • The style generator (@protomaps/basemaps) is part of maplibre-yaml and loaded only by maps that use this form.

Size. Each extra zoom level makes an extract roughly three times bigger: the New York extract above is about 12 MB at --maxzoom=13 and about 95 MB with every zoom level (to 15). GitHub’s web upload (“Add file → Upload files”) takes files up to 25 MB, git push warns above 50 MB and refuses files over 100 MB, and a GitHub Pages site is limited to 1 GB. To go smaller, cut a tighter --bbox around the area you show, or lower --maxzoom (12 still reads well for a whole city). For larger areas, use a host meant for big files, such as an object store.

Hosting. The file is read with HTTP range requests, like any PMTiles file. To preview locally, serve the folder with npx serve (or astro dev); published, GitHub Pages, Netlify, Cloudflare, S3 and R2 support them. python -m http.server does not (see Hosting).

To change the basemap’s design beyond a flavor, use a style file instead. Generate one for your file (Protomaps’ style generator, or mlym emit on a document using the form above), save it as style.json beside the file, edit it, and point its source at the file: "url": "pmtiles://nyc.pmtiles". Its glyphs and sprite can stay on Protomaps’ asset host, or be copied next to the style and named relatively. Then:

config:
mapStyle: basemap/style.json # public/basemap/ in an Astro or Vite site

Relative URLs inside the style resolve against the style (see PMTiles inside a basemap style).

A .fgb file is read into an ordinary GeoJSON source, so everything that works with a .geojson file works with it. Two ways to read it, with load::

load:What it readsNeeds
all (default)The whole file, onceAny web server
viewportOnly the features in view, re-read as the map moves; nothing below minzoom (default 10)A file with a spatial index, and a server with range requests
sources:
districts:
type: geojson
url: data/districts.fgb # small file: read whole
parcels:
type: geojson
url: https://data.example.org/parcels.fgb
load: { mode: viewport, minzoom: 14 } # big file: only what's in view

Sizes. Reading a file whole costs memory and time in the browser, so:

  • up to 10 MB: read whole, no remarks;
  • 10–50 MB: read whole, with a console note suggesting load: viewport or PMTiles;
  • over 50 MB: not read whole. A file with a spatial index is read by view instead (with a console note); one without is refused, and the map says so.

As a rule of thumb, whole-file data is comfortable up to about 100,000 simple features (~20 MB). Beyond that, use load: viewport, or PMTiles for anything large and static.

  • Coordinates: the file must be longitude/latitude (EPSG:4326). A file in another projection is refused with the ogr2ogr command that fixes it.
  • Compression: write the file uncompressed. A Zstandard-compressed FlatGeobuf is recognized and refused with a notice saying so (zstd -d decompresses it).
  • Making one: ogr2ogr -f FlatGeobuf -lco SPATIAL_INDEX=YES out.fgb in.geojson (the index is GDAL’s default; QGIS’s “Export → FlatGeobuf” works too).
  • Live data (refresh:, stream:) isn’t available for FlatGeobuf sources in 0.9 — validation says so. Serve GeoJSON for data that changes.
  • fitTo frames the decoded features; with load: viewport it frames the extent recorded in the file’s header.
  • classify: with computed classes (method:) needs the whole file. On a source read by view — load: viewport, or a file over 50 MB that switched to it — validation (for load: viewport) or the map (for the size switch) asks for explicit breaks: instead, which work either way. See Thematic maps.

A .parquet (or .geoparquet) file is read into an ordinary GeoJSON source, like a FlatGeobuf file: layers, popups, the legend, fitTo, clustering and classify: work as they do for a .geojson file.

type: map
id: geoparquet-wells
config:
center: [10.2, 42.15]
zoom: 10
mapStyle: "https://tiles.openfreemap.org/styles/positron"
fitTo: { source: wells, padding: 20 }
sources:
wells:
type: geojson
url: /gallery-assets/cloud-native/points.parquet
layers:
- id: wells
type: circle
source: wells
paint:
circle-radius: 4
circle-color: ["match", ["get", "kind"], "well", "#e63946", "station", "#457b9d", "#2a9d8f"]
interactive:
click:
popup:
- h3: [{ property: kind }]
- p: [{ str: "Value: " }, { property: value }]

Only the columns the map uses are read. GeoParquet stores each column separately, so maplibre-yaml reads the geometry plus the properties the document references for that source — here kind and value, not the rest of the file. It looks at ["get", …] and ["has", …] in paint, layout and filter, {name} tokens in text, popup and legend property: items, classify: field and normalize, the source’s promoteId, filter and clusterProperties, and in a story the chapters’ setFilter, setPaintProperty and setLayoutProperty actions. On a wide file (dozens of columns) that is most of the saving. To choose yourself, set columns::

sources:
wells:
type: geojson
url: data/wells.parquet
columns: [kind, value, owner] # or: columns: all

Two ways to read it, with load:, as for FlatGeobuf:

load:What it readsNeeds
all (default)The columns it uses, from every rowAny web server
viewportOnly the rows in view, re-read as the map moves; nothing below minzoom (default 10)A bbox covering column, row groups sorted by location, and a server with range requests

Reading by view. A GeoParquet 1.1 file can carry a bbox column (each row’s extent), and the file’s footer records each row group’s range of those values, so row groups outside the view are never downloaded. That only pays off when nearby rows sit in the same row groups — a file sorted by location. A file whose row groups each span most of the area is read whole instead, with a console note; one without a bbox column is refused with a notice that says how to add it. fitTo frames the extent recorded in the file’s metadata.

Sizes. The same limits as FlatGeobuf, counted on the columns the map reads (the footer gives their sizes before any data is downloaded): up to 10 MB read whole, 10–50 MB read whole with a console note, over 50 MB read by view if the file allows it, refused otherwise. A small file (under 512 KB) usually arrives in one request; a host that answers 416 for a small file’s end (GitHub Pages, npx serve) costs a second.

Without range requests (python -m http.server) the first request returns the whole file. That is fine for a small file; a file over 50 MB is refused before it downloads, and the notice explains why.

  • Geometry encoding: WKB, the default of GeoPandas, GDAL and DuckDB. GeoArrow-encoded geometry isn’t read yet; the notice gives the command that rewrites the file as WKB (ogr2ogr -f Parquet -lco GEOMETRY_ENCODING=WKB out.parquet in.parquet, or GeoPandas to_parquet(..., geometry_encoding="WKB")).
  • Coordinates: longitude/latitude (OGC:CRS84, the GeoParquet default, or EPSG:4326). A file in another projection is refused with the ogr2ogr command that fixes it.
  • Compression: snappy (the usual default), zstd, gzip, lz4 and brotli all work. Brotli’s decoder is large (about 68 KB), so it is downloaded only for a file that uses it.
  • A plain Parquet file — one without GeoParquet’s geometry metadata, such as a table with a WKB column written by hand — is named as such in the notice.
  • Live data (refresh:, stream:) isn’t available for GeoParquet sources in 0.9 — validation says so.
  • classify: with computed classes (method:) works on a file read whole. On a source read by view, validation (for load: viewport) or the map (for a file that switched to it) asks for explicit breaks: instead.

GDAL (ogr2ogr, also inside QGIS):

Terminal window
ogr2ogr -f Parquet out.parquet in.geojson -lco COMPRESSION=SNAPPY
# for load: viewport — a bbox column, rows sorted by location, smaller row groups
ogr2ogr -f Parquet out.parquet in.geojson -lco WRITE_COVERING_BBOX=YES \
-lco SORT_BY_BBOX=YES -lco ROW_GROUP_SIZE=10000

Add -t_srs EPSG:4326 when the source data is in a projected CRS.

GeoPandas (Python):

import geopandas as gpd
gdf = gpd.read_file("in.geojson").to_crs(4326)
gdf.to_parquet("out.parquet") # WKB geometry, snappy
# for load: viewport — sort by location, then write the bbox column
gdf = gdf.iloc[gdf.geometry.hilbert_distance().argsort()]
gdf.to_parquet("out.parquet", write_covering_bbox=True, row_group_size=10_000)

DuckDB (with its spatial extension):

INSTALL spatial; LOAD spatial;
COPY (SELECT * FROM ST_Read('in.geojson')) TO 'out.parquet' (FORMAT parquet);

Whatever writes it, keep the file to the area and columns you show: a smaller file loads faster for every visitor.

A COG is an ordinary GeoTIFF laid out for the web: its pixels are stored in tiles, with smaller copies (overviews) for zoomed-out views, so the browser reads only the parts it draws. Satellite and aerial imagery, elevation models and gridded measurements (vegetation, temperature) are often published this way.

Imagery from one COG, draped over 3D terrain and a hillshade from a second COG holding elevation.

A .tif URL on a raster source:

sources:
imagery:
type: raster
url: data/imagery.tif # or cog://data/imagery.tif
layers:
- { id: imagery, type: raster, source: imagery }

RGB, greyscale and paletted images draw in their own colours. maplibre-yaml sets tileSize: 256 (the size the COG reader draws), unless you set one yourself. A URL without a telling extension takes format: cog.

The same kind of file on a raster-dem source is read as elevation (in metres, from the first band). It drives hillshade layers and 3D terrain: exactly like any other elevation source:

type: map
id: cog-elevation
config:
center: [10.2, 42.15]
zoom: 10
pitch: 50
mapStyle: "https://tiles.openfreemap.org/styles/positron"
sources:
elevation:
type: raster-dem
url: /gallery-assets/cloud-native/elevation.cog.tif
terrain:
source: elevation
exaggeration: 1.5
layers:
- id: hills
type: hillshade
source: elevation
paint: { hillshade-exaggeration: 0.5 }

You don’t write encoding:: the COG reader converts the heights to MapLibre’s mapbox (Terrain-RGB) encoding, and maplibre-yaml sets that for you.

A COG with a single band of measurements (a vegetation index, a temperature, a probability) has no colours of its own. colorRamp: maps the values from min to max onto a palette:

type: map
id: cog-vegetation
config:
center: [10.2, 42.15]
zoom: 9.4
mapStyle: "https://tiles.openfreemap.org/styles/positron"
sources:
vegetation:
type: raster
url: /gallery-assets/cloud-native/ndvi.cog.tif
colorRamp: { palette: YlGn, min: 0, max: 0.85 }
layers:
- { id: vegetation, type: raster, source: vegetation }
One band of values (a vegetation index) coloured with colorRamp: YlGn from 0 to 0.85. Pixels with no value (the sea) stay clear.
KeyMeaning
paletteA palette name, the same ones classify: uses: viridis (the default), cividis, and the ColorBrewer names (YlGn, Blues, RdBu, Spectral …). A misspelt name gets a did-you-mean.
colorsYour own colours instead of a palette, low to high: colors: ["#2c7bb6", "#ffffbf", "#d7191c"]
min, maxThe values drawn in the first and last colour (required). Values outside the range take the end colours.
reversetrue runs the colours from high to low.
classesA number of flat classes (classes: 5) instead of a smooth ramp: the palette’s own set for that many classes, or that many colours spread along your colors:. Qualitative palettes (okabe-ito, Set1 …) need it: as a smooth ramp they blend unrelated colours.

Pixels the file marks as having no value (its NoData value, or NaN) are left transparent. colorRamp: applies to raster sources only; in a format v2 document it goes under the source’s runtime:.

GDAL (3.1 or later; it also ships with QGIS) writes COGs. One command reprojects any georeferenced raster to Web Mercator and writes it as a COG:

Terminal window
gdalwarp -t_srs EPSG:3857 -of COG -co COMPRESS=DEFLATE input.tif output.tif

A GeoTIFF that is already in Web Mercator but isn’t cloud-optimized (stored in strips, or without overviews) is converted with:

Terminal window
gdal_translate -of COG -co COMPRESS=DEFLATE input.tif output.tif
  • -co COMPRESS=DEFLATE is lossless; -co COMPRESS=JPEG makes imagery much smaller (lossy, for photos only, not for elevation or measurements).
  • -co TILING_SCHEME=GoogleMapsCompatible (with gdalwarp or gdal_translate) also aligns the file with web map tiles, which makes reads a little more efficient.
  • Check a file with gdalinfo output.tif: it should report Coordinate System is: … "WGS 84 / Pseudo-Mercator", LAYOUT=COG and overviews.

An ordinary GeoTIFF still draws, but the browser may have to download most of the file to show it, so while you develop the map’s notice says so, with the gdal_translate command.

A COG can be any size: the browser reads only the tiles in view, at the resolution the view needs, through the overviews. The limits are the host’s: GitHub refuses files over 100 MB in a repository and a GitHub Pages site is limited to 1 GB in total, so for a large mosaic use an object store (S3, R2, Google Cloud Storage) and point url: at it. Clip the file to the area you show (gdalwarp -te …), and pick the compression that suits the data (above), to keep it small.

  • Framing: fitTo: { source: imagery } frames the COG’s own extent, read from the file.
  • A page that registers its own cog protocol keeps it. If your page already calls addProtocol("cog", …) through @maplibre-yaml/core/maplibre (for example to send request headers), maplibre-yaml leaves it alone.
  • maplibre-gl 4.5 or later is needed for COGs (the COG reader’s own minimum); on an older one the map’s notice says so.
  • Content Security Policy: the COG reader decodes images in background workers it starts from blob: URLs. A site with a strict CSP needs worker-src 'self' blob:; without it, the map’s notice says the COG couldn’t be read and why.

PMTiles, COG and GeoParquet always, and FlatGeobuf with load: viewport, read files in parts with HTTP range requests. GitHub Pages, Netlify, Cloudflare Pages, Vercel, S3, R2, raw.githubusercontent.com and fastly.jsdelivr.net support them, and so do npx serve, astro dev and astro preview on your computer; python -m http.server doesn’t, and cdn.jsdelivr.net doesn’t reliably. A file on another site also needs CORS that allows the Range header and exposes Content-Range. On a GitHub Pages project site, set base and write paths as you would locally (data/x.pmtiles, pmtiles://data/x.pmtiles).

The full table of hosts, the CORS rule for S3 and R2, and base are in Publishing & subpaths.

Problems with one file never take down the map: the basemap and every other layer still draw, and the map’s notice (“Part of this map couldn’t load”) says what happened in plain words. The common cases:

What happenedWhat the notice says
The server doesn’t support range requests”…/x.pmtiles was served without range-request support (the server sent the whole file instead of the part asked for). PMTiles, FlatGeobuf, GeoParquet and COG files need a web server that supports HTTP range requests: GitHub Pages, Netlify, Cloudflare, S3 and R2 do; python -m http.server does not — preview locally with npx serve instead.”
A host that compresses the parts of files it sends (cdn.jsdelivr.net)“…/x.pmtiles comes from a server that compresses the parts of files it sends, so the PMTiles reader can’t read the file a part at a time. Put the file on your own site (GitHub Pages, Netlify and npx serve work); a file mirrored from GitHub works from fastly.jsdelivr.net, not cdn.jsdelivr.net.”
Another site refused (CORS), or it’s unreachable”The browser couldn’t read …: the server didn’t allow this page to load it (CORS), or it couldn’t be reached. A PMTiles file on another site needs that site to send Access-Control-Allow-Origin and to allow the Range header.”
A wrong path that returned a web page”…/x.pmtiles isn’t a PMTiles file. A wrong path often returns a web page instead — check the file name and folder.”
The file isn’t there”…/x.fgb didn’t load (404 Not Found)”, with where the browser looked and the base hint for project sites
Too large to read whole, no index”…/x.fgb is 180 MB, too large to load whole in a browser. Add load: viewport to the source (the file needs a spatial index), or convert it to PMTiles.”
The format’s reader couldn’t be downloaded”The PMTiles reader (…/chunks/pmtiles.js) couldn’t be downloaded. maplibre-yaml loads its file-format readers on demand from the folder its script came from: if you copied maplibre-yaml to your own site, copy the whole dist/ folder (not just register.js), or load it from the CDN. …”
A FlatGeobuf in the wrong projection”…/x.fgb is in EPSG:3857; MapLibre needs longitude/latitude (EPSG:4326). Reproject it: ogr2ogr -t_srs EPSG:4326 out.fgb x.fgb.”
A mapStyle: { pmtiles: … } file that isn’t a Protomaps basemap”…/x.pmtiles doesn’t look like a Protomaps basemap: its layers are …, while a Protomaps basemap has earth, water, roads and places.” A basemap file that fails to load starts with “The basemap didn’t load:” and the reason, as above.
A Protomaps file cut before the current tile schema (late 2024)“…/x.pmtiles is a Protomaps basemap built with an older tile schema (version 3.7.1), so only land and water draw … Cut a new extract with pmtiles extract from https://build.protomaps.com.”
A GeoParquet file with GeoArrow geometry”…/x.parquet stores its geometry as GeoArrow (point), which maplibre-yaml doesn’t read yet: it reads GeoParquet with WKB geometry. Rewrite it with WKB: ogr2ogr -f Parquet -lco GEOMETRY_ENCODING=WKB out.parquet x.parquet, or GeoPandas to_parquet(..., geometry_encoding="WKB").”
A Parquet file without geometry metadata”…/x.parquet is a Parquet file but not GeoParquet (it has no geometry metadata). Write it with GeoPandas to_parquet, ogr2ogr -f Parquet, or DuckDB’s spatial extension.”
A COG that isn’t in Web Mercator”…/x.tif is in EPSG:4326; a COG must be in Web Mercator (EPSG:3857) to be drawn. Reproject it: gdalwarp -t_srs EPSG:3857 -of COG -co COMPRESS=DEFLATE x.tif out.tif (GDAL 3.1 or later).”
A GeoTIFF that isn’t cloud-optimized (a hint while developing; it still draws)“…/x.tif is a GeoTIFF but not a Cloud Optimized one (it has no internal tiles or no overviews), so the browser may download most of the file to draw it. Convert it: gdal_translate -of COG -co COMPRESS=DEFLATE x.tif out.tif.”
A strict Content Security Policy”The Cloud Optimized GeoTIFF (COG) reader couldn’t be set up (this page’s Content Security Policy doesn’t allow the background workers COG images are decoded in; allow them with worker-src 'self' blob:) …”

The readers for these formats are not part of maplibre-yaml’s main script. A page downloads a reader from dist/chunks/, next to the script, only when a map uses that format: PMTiles about 8 KB gzip, FlatGeobuf about 12 KB, GeoParquet about 23 KB (plus about 68 KB for a brotli-compressed file), COG about 132 KB, and the Protomaps basemap style generator about 7 KB. From a CDN that is automatic; if you host maplibre-yaml’s files yourself, copy the whole dist/ folder (Installation).

A style.json can carry a URL but not a file reader, so:

  • PMTiles export as pmtiles:// URLs (a .pmtiles URL gains the scheme). The style then needs a renderer that reads pmtiles://, such as maplibre-gl JS with the protocol registered. With a bundler, after npm install pmtiles:

    import maplibregl from "maplibre-gl";
    import { Protocol } from "pmtiles";
    maplibregl.addProtocol("pmtiles", new Protocol().tile);

    In a plain HTML page, import addProtocol from the maplibre-gl your import map names and Protocol from https://esm.sh/pmtiles@4 (esm.sh resolves its dependencies; unpkg does not), before the map is created.

  • A Protomaps basemap (mapStyle: { pmtiles: … }) exports as the full style it generates: every layer, the file as pmtiles://…, and the glyph and sprite URLs. It needs the same pmtiles registration. A relative file path is written as it is, so it resolves against whichever page loads the style; give a full URL for a style used anywhere else. Labels are in lang: (English when unset: an exported style has no page language to follow).

  • FlatGeobuf and GeoParquet are converted: mlym emit reads the file (next to the YAML, in the current folder or its public/, or over http(s)) and writes its features into the style as GeoJSON — for GeoParquet, every column, or the columns: you listed. If it can’t read the file, the source is emitted empty and reported, and --strict refuses.

  • COG sources export as cog:// URLs, in the form the map itself uses: #dem on an elevation source, a colorRamp: written out as the COG protocol’s #color: fragment, and tileSize: 256. As with PMTiles, the style then needs the protocol registered — here the one from @geomatico/maplibre-cog-protocol, which maplibre-yaml uses itself. With a bundler, after npm install @geomatico/maplibre-cog-protocol:

    import maplibregl from "maplibre-gl";
    import { cogProtocol } from "@geomatico/maplibre-cog-protocol";
    maplibregl.addProtocol("cog", cogProtocol);

    In a plain HTML page, load the plugin’s own browser build (<script src="https://unpkg.com/@geomatico/maplibre-cog-protocol@0.10/dist/index.js"></script>, which defines MaplibreCOGProtocol) and call addProtocol("cog", MaplibreCOGProtocol.cogProtocol) on the maplibre-gl your import map names, before the map is created.

See Export classes for the full table.

In a format v2 document the URL stays in the source and format: / load: / columns: / colorRamp: move under the source’s runtime:. A Protomaps basemap is written under style: as basemap: data/nyc.pmtiles or basemap: { pmtiles: data/nyc.pmtiles, flavor: dark }, the same forms as mapStyle:.

version: 2
type: map
id: cloud-native-v2
style:
basemap: "https://tiles.openfreemap.org/styles/positron"
center: [10.2, 42.15]
zoom: 10
sources:
outlines: { type: vector, url: /gallery-assets/cloud-native/districts.pmtiles }
points:
type: geojson
url: /gallery-assets/cloud-native/points.fgb
runtime:
load: { mode: viewport, minzoom: 11 }
districts:
type: geojson
url: /gallery-assets/cloud-native/districts.parquet
runtime:
columns: [name, category]
vegetation:
type: raster
url: /gallery-assets/cloud-native/ndvi.cog.tif
runtime:
colorRamp: { palette: YlGn, min: 0, max: 0.85 }
layers:
- { id: vegetation, type: raster, source: vegetation }
- { id: districts, type: fill, source: districts, paint: { fill-color: "#e9c46a", fill-opacity: 0.5 } }
- { id: outlines, type: line, source: outlines, source-layer: districts }
- { id: points, type: circle, source: points }