/portaljs-add-geo
Auto-ingest a geospatial file (GeoJSON, Shapefile, GeoPackage, KML/KMZ, FlatGeobuf, CSV-with-geometry) into a PortalJS portal — on your own machine, no server. Normalizes CRS to EPSG:4326, derives a PMTiles render tier + a GeoParquet query tier, pushes all three to R2 via Git
> /plugin marketplace add datopian/portaljs > /plugin install portaljs@datopian-portaljs
How it fires
How this command gets triggered: by you, by Claude, or both.
- Fires itselfClaude auto-loads it when your prompt matches the work.
- You can call itInvoke it directly when you want it.
- Slash command
/portaljs-add-geo
Context preview
What this command does when you run it.
Auto-ingest a geospatial file (GeoJSON, Shapefile, GeoPackage, KML/KMZ, FlatGeobuf, CSV-with-geometry) into a PortalJS portal — on your own machine, no server. Normalizes CRS to EPSG:4326, derives a PMTiles render tier + a GeoParquet query tier, pushes all three to R2 via Git
Command definition
portaljs-add-geo.mddescription: Auto-ingest a geospatial file (GeoJSON, Shapefile, GeoPackage, KML/KMZ, FlatGeobuf, CSV-with-geometry) into a PortalJS portal — on your own machine, no server. Normalizes CRS to EPSG:4326, derives a PMTiles render tier + a GeoParquet query tier, pushes all three to R2 via Git LFS, and emits one dual-tier datasets.json entry the showcase renders with <MapPreview> + <GeoQuery>.
allowed-tools: Read, Write, Edit, Bash, WebFetch
/portaljs-add-geo
Turn one geospatial upload into a **dual-tier** PortalJS dataset entirely on the user's machine — **no server-side container compute**, keeping the zero-backend bet end-to-end. From a single source file this produces two derivatives plus the preserved original:
- **PMTiles** — the **render tier**. A single vector-tile archive `<MapPreview>` renders
with MapLibre GL over HTTP range requests: any dataset size pans/zooms with no tile server.
- **GeoParquet** — the **query tier**. A GeoParquet 1.1 file (covering `bbox` column,
Hilbert-sorted) `<GeoQuery>` runs spatial SQL over in place via DuckDB-Wasm.
- **Original** — the untouched upload in its native CRS, kept downloadable as the source
artifact.
All three land on Cloudflare R2 (via Git LFS → Giftless), and the skill appends **one** `datasets.json` entry whose `resources[]` are exactly the shape the showcase auto-renders (PR #1647 / the `@reference/world-boundaries` demo): a `pmtiles` resource → `<MapPreview>`, a `geoparquet` resource → `<GeoQuery>`, and the original as a download. No page edits — the dynamic showcase route `pages/[owner]/[slug].tsx` renders each resource by its `format`.
This automates the manual `tippecanoe` / `duckdb` recipes documented in the template README (the interim ingest path). Use it whenever the source is a **vector geo format**; for plain tabular data use `/portaljs-add-dataset`, and for a Leaflet view over small GeoJSON use `/portaljs-add-map`.
Runtime — native shell-out, detect-or-instruct
This runs **native CLIs on the user's machine** (locked direction, 2026-07-08 — overrides po-6sr §7's server-compute sketch). No container, no cloud job. tippecanoe has no mature WASM build, so the PMTiles tier fundamentally needs the native binary; skills already shell out to git-lfs and npm, so native geo deps fit the model. Three tools:
| Tool | Role | Why this tool | |------|------|---------------| | **GDAL** (`ogr2ogr`, `ogrinfo`) | Read **every** vector input; reproject → EPSG:4326; emit the normalized GeoJSON intermediate | Widest driver set (SHP, GPKG, KML/KMZ, FGB, CSV-geom) | | **tippecanoe** | normalized GeoJSON → PMTiles (render tier) | The de-facto tiler; no WASM equivalent | | **duckdb** (+ `spatial`) | normalized GeoJSON → GeoParquet 1.1 with covering `bbox` + Hilbert sort (query tier) | The **proven** writer — PR #1647 verified `<GeoQuery>` against duckdb output; ogr2ogr's Parquet driver won't cleanly emit the covering-bbox column + Hilbert ordering the bbox-first query needs to prune |
Required input — ask, don't error
- **Source** — a local geo file path or a public URL. Supported: `.geojson`/`.json`
(GeoJSON), `.zip` (zipped Shapefile), `.gpkg`, `.kml`/`.kmz`, `.fgb`, `.csv` with a geometry/lat-lon column. Raster (`.tif`/`.tiff`) is **out of scope** (COG is a later phase).
- **Portal directory** — path to the portal project (defaults to current directory).
- **Namespace** — the dataset's namespace value (subject for `'theme'` portals, publisher
for `'owner'` portals).
**If the source is missing, ask for it — never dead-end.** The user can say "use defaults".
Steps
1. Gather input from `$ARGUMENTS` (interview if thin)
Extract:
- `SOURCE` — geo file path or URL
- `PORTAL_DIR` — portal directory (default: `.`)
- `DATASET_NAME` — human name (default: derived from filename)
- `DATASET_SLUG` — URL slug (default: lowercase hyphenated filename without extension)
- `DESCRIPTION` — optional one-line description
- `NAMESPACE` — namespace value (default: read the first existing entry's `namespace`
from `datasets.json`, else `reference`)
If `SOURCE` is missing, ask (one focused prompt) and wait:
To add a geospatial dataset I need:
1. Source: a local geo file path or public URL (required) —
GeoJSON, zipped Shapefile (.zip), GeoPackage (.gpkg), KML/KMZ, FlatGeobuf (.fgb),
or a CSV with a geometry/lat-lon column.
2. Portal directory (Enter for current directory)
3. Dataset name (Enter to use the filename)
4. Namespace value — the group this dataset belongs to
(subject if the portal is "theme" mode, publisher if "owner" mode; Enter to reuse the catalog's existing namespace)
Read `NAMESPACE_TYPE` from `PORTAL_DIR/lib/datasets.ts` so you can phrase the namespace question correctly ("subject" vs "publisher").
2. Check the native tools (detect-or-instruct — hard gate)
Do this **before touching data**. Missing tools stop the run with a one-line install — never silently skip a tier.
MISSING=""
for t in ogr2ogr tippecanoe duckdb; do
command -v "$t" >/dev/null 2>&1 || MISSING="$MISSING $t"
done
If `MISSING` is non-empty, print the install for the user's OS and STOP:
ERROR: [add-geo] MISSING_TOOLS Missing:<MISSING>. Install then re-run:
macOS: brew install gdal tippecanoe duckdb
Debian/Ubuntu: sudo apt-get install -y gdal-bin duckdb # tippecanoe: apt where available,
else build from source — https://github.com/felt/tippecanoe
Windows: not supported natively — use WSL (Ubuntu) and follow the Debian steps.Detect Windows-native (`$OSTYPE` = `msys`/`win32`, or `uname` shows MINGW/MSYS) and print the WSL note prominently — this is a documented first-cut limitation, not a bug.
Verify the duckdb `spatial` extension can load (it auto-installs on first use, needs network once): `duckdb -c "INSTALL spatial; LOAD spatial; SELECT 1;"` — if this fails, report it as `MISSING_TOOLS` (duckdb spatial) with the same stop behavior.
3. Validate the po
Read more
description: Auto-ingest a geospatial file (GeoJSON, Shapefile, GeoPackage, KML/KMZ, FlatGeobuf, CSV-with-geometry) into a PortalJS portal — on your own machine, no server. Normalizes CRS to EPSG:4326, derives a PMTiles render tier + a GeoParquet query tier, pushes all three to R2 via Git LFS, and emits one dual-tier datasets.json entry the showcase renders with <MapPreview> + <GeoQuery>. allowed-tools: Read, Write, Edit, Bash, WebFetch
/portaljs-add-geo
Turn one geospatial upload into a **dual-tier** PortalJS dataset entirely on the user's machine — **no server-side container compute**, keeping the zero-backend bet end-to-end. From a single source file this produces two derivatives plus the preserved original:
- **PMTiles** — the **render tier**. A single vector-tile archive `<MapPreview>` renders
with MapLibre GL over HTTP range requests: any dataset size pans/zooms with no tile server.
- **GeoParquet** — the **query tier**. A GeoParquet 1.1 file (covering `bbox` column,
Hilbert-sorted) `<GeoQuery>` runs spatial SQL over in place via DuckDB-Wasm.
- **Original** — the untouched upload in its native CRS, kept downloadable as the source
artifact.
All three land on Cloudflare R2 (via Git LFS → Giftless), and the skill appends **one** `datasets.json` entry whose `resources[]` are exactly the shape the showcase auto-renders (PR #1647 / the `@reference/world-boundaries` demo): a `pmtiles` resource → `<MapPreview>`, a `geoparquet` resource → `<GeoQuery>`, and the original as a download. No page edits — the dynamic showcase route `pages/[owner]/[slug].tsx` renders each resource by its `format`.
This automates the manual `tippecanoe` / `duckdb` recipes documented in the template README (the interim ingest path). Use it whenever the source is a **vector geo format**; for plain tabular data use `/portaljs-add-dataset`, and for a Leaflet view over small GeoJSON use `/portaljs-add-map`.
Runtime — native shell-out, detect-or-instruct
This runs **native CLIs on the user's machine** (locked direction, 2026-07-08 — overrides po-6sr §7's server-compute sketch). No container, no cloud job. tippecanoe has no mature WASM build, so the PMTiles tier fundamentally needs the native binary; skills already shell out to git-lfs and npm, so native geo deps fit the model. Three tools:
| Tool | Role | Why this tool | |------|------|---------------| | **GDAL** (`ogr2ogr`, `ogrinfo`) | Read **every** vector input; reproject → EPSG:4326; emit the normalized GeoJSON intermediate | Widest driver set (SHP, GPKG, KML/KMZ, FGB, CSV-geom) | | **tippecanoe** | normalized GeoJSON → PMTiles (render tier) | The de-facto tiler; no WASM equivalent | | **duckdb** (+ `spatial`) | normalized GeoJSON → GeoParquet 1.1 with covering `bbox` + Hilbert sort (query tier) | The **proven** writer — PR #1647 verified `<GeoQuery>` against duckdb output; ogr2ogr's Parquet driver won't cleanly emit the covering-bbox column + Hilbert ordering the bbox-first query needs to prune |
Required input — ask, don't error
- **Source** — a local geo file path or a public URL. Supported: `.geojson`/`.json`
(GeoJSON), `.zip` (zipped Shapefile), `.gpkg`, `.kml`/`.kmz`, `.fgb`, `.csv` with a geometry/lat-lon column. Raster (`.tif`/`.tiff`) is **out of scope** (COG is a later phase).
- **Portal directory** — path to the portal project (defaults to current directory).
- **Namespace** — the dataset's namespace value (subject for `'theme'` portals, publisher
for `'owner'` portals).
**If the source is missing, ask for it — never dead-end.** The user can say "use defaults".
Steps
1. Gather input from `$ARGUMENTS` (interview if thin)
Extract:
- `SOURCE` — geo file path or URL
- `PORTAL_DIR` — portal directory (default: `.`)
- `DATASET_NAME` — human name (default: derived from filename)
- `DATASET_SLUG` — URL slug (default: lowercase hyphenated filename without extension)
- `DESCRIPTION` — optional one-line description
- `NAMESPACE` — namespace value (default: read the first existing entry's `namespace`
from `datasets.json`, else `reference`)
If `SOURCE` is missing, ask (one focused prompt) and wait:
To add a geospatial dataset I need: 1. Source: a local geo file path or public URL (required) — GeoJSON, zipped Shapefile (.zip), GeoPackage (.gpkg), KML/KMZ, FlatGeobuf (.fgb), or a CSV with a geometry/lat-lon column. 2. Portal directory (Enter for current directory) 3. Dataset name (Enter to use the filename) 4. Namespace value — the group this dataset belongs to (subject if the portal is "theme" mode, publisher if "owner" mode; Enter to reuse the catalog's existing namespace)
Read `NAMESPACE_TYPE` from `PORTAL_DIR/lib/datasets.ts` so you can phrase the namespace question correctly ("subject" vs "publisher").
2. Check the native tools (detect-or-instruct — hard gate)
Do this **before touching data**. Missing tools stop the run with a one-line install — never silently skip a tier.
MISSING="" for t in ogr2ogr tippecanoe duckdb; do command -v "$t" >/dev/null 2>&1 || MISSING="$MISSING $t" done
If `MISSING` is non-empty, print the install for the user's OS and STOP:
ERROR: [add-geo] MISSING_TOOLS Missing:<MISSING>. Install then re-run:
macOS: brew install gdal tippecanoe duckdb
Debian/Ubuntu: sudo apt-get install -y gdal-bin duckdb # tippecanoe: apt where available,
else build from source — https://github.com/felt/tippecanoe
Windows: not supported natively — use WSL (Ubuntu) and follow the Debian steps.Detect Windows-native (`$OSTYPE` = `msys`/`win32`, or `uname` shows MINGW/MSYS) and print the WSL note prominently — this is a documented first-cut limitation, not a bug.
Verify the duckdb `spatial` extension can load (it auto-installs on first use, needs network once): `duckdb -c "INSTALL spatial; LOAD spatial; SELECT 1;"` — if this fails, report it as `MISSING_TOOLS` (duckdb spatial) with the same stop behavior.
3. Validate the po
🌀 AI-native framework for building data portals. Scaffold a full portal from a brief and load datasets in minutes with agentic skills — any backend (CKAN, GitHub, Frictionless).
Repo: datopian/portaljs
Other commands on portaljs.
- /add-chart
Renamed → /portaljs-add-chart. This alias will be removed next minor release.
Open command - /add-dataset
Renamed → /portaljs-add-dataset. This alias will be removed next minor release.
Open command - /add-map
Renamed → /portaljs-add-map. This alias will be removed next minor release.
Open command - /add-resource
Renamed → /portaljs-add-resource. This alias will be removed next minor release.
Open command - /arcgis-to-portaljs
Migrate a whole ArcGIS Hub site (opendata.arcgis.com or a Hub Premium custom domain) into a PortalJS Arc portal end-to-end. Harvests the Hub /data.json (DCAT-US) inventory, exports every FeatureService layer through the ArcGIS REST query API (resultOffset paging), converts each
Open command - /architect
Renamed → /portaljs-architect. This alias will be removed next minor release.
Open command

