Short answer
Getting a QGIS layer onto a web map is mostly a question of choosing the right delivery format and the right projection, then trimming the data so the browser is not asked to download megabytes it cannot draw. For a handful of features, export GeoJSON in EPSG:4326. For large or richly styled vector layers, generate Mapbox Vector Tiles (MVT) with per-zoom simplification. For rasters such as hillshade or scanned geology, generate XYZ/WMTS raster tiles in EPSG:3857. The mistakes that cost the most are leaving data in a metric projected CRS that web libraries do not expect, exporting full-precision coordinates, and shipping every attribute column to the client.
The two projections you cannot mix up
Web mapping uses two coordinate reference systems, and confusing them is the most common failure:
- EPSG:4326 (WGS 84, lon/lat in degrees) is what the GeoJSON specification (RFC 7946) requires: coordinates must be longitude, latitude, in decimal degrees. Store and ship GeoJSON in 4326.
- EPSG:3857 (Web Mercator) is the rendering and tiling CRS. Leaflet, MapLibre GL, OpenLayers, and every slippy-map basemap draw in 3857. Tile pyramids (XYZ, MBTiles, WMTS) are cut in 3857.
The clean rule: vector data travels as 4326 GeoJSON and is reprojected for display by the library; tiles are baked in 3857. If your source is in a national grid such as EPSG:27700 (British National Grid) or a UTM zone, reproject before export with Processing > native:reprojectlayer, or with GDAL:
ogr2ogr -f GeoJSON faults.geojson faults.gpkg \
-t_srs EPSG:4326 -lco COORDINATE_PRECISION=6 \
-select unit_code,fault_type,confidence
-t_srs EPSG:4326 reprojects, COORDINATE_PRECISION=6 rounds to ~0.1 m, and -select keeps only the columns the web map needs.
Choosing the format
GeoJSON is human-readable, universally supported, and ideal for small layers — say, fewer than a few hundred features, or a few thousand simple points. It does not scale: a national fault network at full precision can be tens of megabytes, all of which the browser must download and parse before drawing a single line.
Mapbox Vector Tiles (MVT / .pbf) are the right answer for large styled vectors. The data is sliced into a tile pyramid, simplified per zoom level, and only the visible tiles are fetched. QGIS exports MVT through Processing > native:writevectortiles ("Write Vector Tiles (MBTiles)" / "Write Vector Tiles (Directory)"). You set a minimum and maximum zoom (e.g. 4–14) and QGIS handles per-zoom geometry simplification.
Raster tiles (XYZ / WMTS) suit cartography that is expensive to render live: hillshade, geological raster mosaics, georeferenced scans. Generate them from Processing > native:tilesxyzdirectory / native:tilesxyzmbtiles ("Generate XYZ tiles") or with gdal2tiles.py:
gdal2tiles.py -p mercator -z 6-13 --xyz hillshade_3857.tif tiles/
-p mercator produces a 3857 pyramid and --xyz writes the standard top-left tile origin that Leaflet/MapLibre expect (rather than the TMS bottom-left origin).
Worked example: a geology layer for a project viewer
You have bedrock.gpkg in EPSG:32630, ~12,000 polygons, 18 attribute columns. You want an interactive, styled web layer.
- Reproject and trim. Vector tiles are cut in 3857; keep only
map_unit,lithology, andagefor the client. Drop internal QA columns — they bloat every tile. - Simplify per scale. Coastline-level detail is wasted at zoom 5. The MVT writer applies tolerance automatically, but you can pre-simplify with
native:simplifygeometries(Douglas–Peucker, tolerance ~10 m for a regional layer) before tiling to control the result. - Write tiles. Run
native:writevectortileswith zoom 5–14, outputbedrock.mbtiles. - Carry styling. Export the QGIS symbology as a MapLibre/Mapbox GL style JSON (the qgis2web plugin or the built-in style export can help), so the unit colours match the desktop map rather than being re-invented in the front end.
- Serve. Host the
.mbtilesbehind a tile server (tileserver-gl, or unpack to a directory served statically) and point MapLibre at the tile URL.
For a quick, fully static alternative, the qgis2web plugin exports a complete Leaflet or OpenLayers page (HTML + GeoJSON + style) directly from your current QGIS project — useful for a one-off shareable map, less so for a maintained application.
Common pitfalls and why they happen
- Layer is invisible or in the ocean. Cause: exported in a projected CRS but the library assumes 4326, so coordinates like 500000, 5600000 are read as degrees. Fix: always
-t_srs EPSG:4326for GeoJSON. - File too large to load. Cause: full coordinate precision (15 decimals) plus every attribute. Fix:
COORDINATE_PRECISION=6,-selectthe needed columns, or switch to vector tiles. - Tiles upside-down or offset. Cause: TMS vs XYZ tile origin mismatch. Fix: use
--xyzingdal2tiles.pyor set the scheme correctly in your tile config. - Polygons with holes render wrong. Cause: invalid ring winding after simplification. Fix: run
native:fixgeometriesbefore export; RFC 7946 expects right-hand-rule winding. - Labels and styling lost. Cause: GeoJSON/MVT carry geometry and attributes, not symbology. Fix: export a separate GL style or re-create styling in the web library from attributes.
Validation
Before publishing: open the exported GeoJSON in a fresh QGIS project (it should land in the correct place over a basemap), check the file size against your performance budget, and load the tiles in a throwaway MapLibre page across the full zoom range to confirm features appear and disappear sensibly. Verify attribute values survived the -select, and confirm coordinate precision did not visibly distort contacts at the largest intended zoom.
Bathyl perspective
Web delivery is where careful desktop work most often degrades. We reproject deliberately to 4326 for vectors and 3857 for tiles, budget file size against zoom, and export styling as a portable GL spec so the published map reads the same as the analyst's. The format choice is driven by feature count and interactivity, not habit.
Related reading
- QGIS QA Checklist Before Publication
- QGIS Reproject Layer Workflow
- QGIS Package Project for Delivery
- GIS and spatial analysis