/portaljs-add-resource
Add another file (resource) to an EXISTING dataset in a PortalJS portal — a data dictionary, methodology, or an additional data file. Turns a single-file dataset into a multi-resource one; the showcase renders a section per resource.
> /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-resource
Context preview
What this command does when you run it.
Add another file (resource) to an EXISTING dataset in a PortalJS portal — a data dictionary, methodology, or an additional data file. Turns a single-file dataset into a multi-resource one; the showcase renders a section per resource.
Command definition
portaljs-add-resource.mddescription: Add another file (resource) to an EXISTING dataset in a PortalJS portal — a data dictionary, methodology, or an additional data file. Turns a single-file dataset into a multi-resource one; the showcase renders a section per resource.
allowed-tools: Read, Write, Edit, Bash, WebFetch
/portaljs-add-resource
Add a **resource** (an additional file) to a dataset that already exists in a `portaljs-catalog` portal. Use this when a dataset is more than one file — data + a data dictionary + a methodology doc, or quarterly/yearly files under one dataset. Where [`/portaljs-add-dataset`](/docs/skills/portaljs-add-dataset) creates a **new** dataset (one file), this adds a file to an **existing** one.
Mirrors the Frictionless Data Package model: a dataset holds a `resources[]` array, and the showcase at `/@<namespace>/<slug>` renders **one section per resource** (preview, schema, download). A single-file dataset is migrated to `resources[]` automatically the first time you add a second file — no data is lost.
Required input — ask, don't error
- **Target dataset** — which dataset to add to, by `slug` (or `namespace/slug`).
- **Source** — a local file path or a public URL for the new resource.
- **Portal directory** — defaults to the current directory.
- **Resource name/title** (optional) — a short id + human title for the resource.
Supported formats: **CSV, TSV, JSON (array), GeoJSON** (same as `/portaljs-add-dataset`).
**If the target dataset or source is missing, ask — never dead-end.** When the user doesn't know the slug, read `datasets.json` and list the datasets so they can pick.
Steps
1. Gather input from `$ARGUMENTS` (interview if thin)
Extract:
- `DATASET` — target dataset slug (or `namespace/slug`).
- `SOURCE` — file path or URL of the new resource.
- `PORTAL_DIR` — portal directory (default: `.`).
- `RESOURCE_NAME` — short id within the dataset (default: lowercase-hyphenated filename stem; must be unique among the dataset's resources).
- `RESOURCE_TITLE` — human title (default: derived from the filename).
- `DESCRIPTION` — optional one-line description of the resource.
If `DATASET` or `SOURCE` is missing, ask (listing existing datasets from `datasets.json` when the slug is unknown) and wait.
2. Validate the portal + locate the dataset
Confirm `PORTAL_DIR/datasets.json`, `PORTAL_DIR/package.json`, and `PORTAL_DIR/pages/[owner]/[slug].tsx` exist (the catalog template). Find the dataset entry by `slug` (and `namespace` if given). If it doesn't exist, tell the user and offer to create it with `/portaljs-add-dataset` instead.
3. Detect format and copy/fetch the file
Same as `/portaljs-add-dataset` step 3: detect format from extension/Content-Type (`csv`/`tsv`/`json`/`geojson`), fetch a URL (check HTTP status) or check a local path exists, then copy into the portal:
mkdir -p PORTAL_DIR/public/data
cp SOURCE PORTAL_DIR/public/data/RESOURCE_NAME.EXT
# or for URLs: curl -L SOURCE -o PORTAL_DIR/public/data/RESOURCE_NAME.EXT
Pick a filename that won't collide with an existing file in `/public/data`.
4. Add the resource to the dataset entry
Open `PORTAL_DIR/datasets.json` and update the target dataset, matching the `Dataset` / `Resource` shape in `lib/providers/types.ts`:
- **If the dataset has no `resources` yet** (it's a single-file dataset with top-level
`file`/`format`/`schema`), **migrate it first**: move the existing file into a `resources` array as the first resource, then remove the now-redundant top-level `file`/`format` (and top-level `schema`, which becomes that resource's schema). Preserve the existing data — this is a lossless transform:
// before
{ "slug": "orders", "name": "Orders", "file": "orders.csv", "format": "csv", "schema": { ... } }
// after (migrated) + new resource appended
{
"slug": "orders", "name": "Orders",
"resources": [
{ "name": "data", "path": "orders.csv", "format": "csv", "title": "Orders", "schema": { ... } },
{ "name": "RESOURCE_NAME", "path": "RESOURCE_NAME.EXT", "format": "<detected>", "title": "RESOURCE_TITLE", "description": "DESCRIPTION" }
]
}- **If the dataset already has `resources`**, just append the new resource object. Ensure
`name` is unique within the array (the showcase uses it as the section key/anchor).
Keep all package-level fields (`description`, `keywords`, `licenses`, `sources`, `version`, …) on the dataset. Drop `description` on the resource only if there genuinely is none.
That is the entire registration — `getResources()` (`lib/datasets.ts`) reads the `resources` array and the showcase renders a section per resource automatically.
5. Verify the build
cd PORTAL_DIR
npx next build > /tmp/portaljs-add-resource-build.log 2>&1
BUILD_EXIT=$?
tail -20 /tmp/portaljs-add-resource-build.log
The common failure is malformed JSON in `datasets.json`. Fix and rebuild before reporting success.
6. Report
✓ Resource added to DATASET: RESOURCE_TITLE (RESOURCE_NAME.EXT)
- Data file: public/data/RESOURCE_NAME.EXT
- Manifest: datasets.json (dataset now has <n> resources)
- Showcase: /@<namespace>/<slug> renders a section per resource
If this was the first migration to multi-resource, note that the dataset's single `file` was moved into `resources[]` (no data lost).
Notes
- **One source of truth.** Resources live on the dataset's `datasets.json` entry. No page
edits — the showcase loops `getResources(dataset)`.
- **Per-resource schema.** Describe a resource's fields with [`/portaljs-define-schema`](/docs/skills/portaljs-define-schema)
— its Frictionless Table Schema renders under that resource's section.
- **Single-file stays simple.** Datasets with one file keep the plain `file`/`format`
shape; migration to `resources[]` only happens when a second file is added.
Example
/portaljs-add-resource orders ./data/orders-data-dictionary.csv (title: "Data dictionary")
Adds
Read more
description: Add another file (resource) to an EXISTING dataset in a PortalJS portal — a data dictionary, methodology, or an additional data file. Turns a single-file dataset into a multi-resource one; the showcase renders a section per resource. allowed-tools: Read, Write, Edit, Bash, WebFetch
/portaljs-add-resource
Add a **resource** (an additional file) to a dataset that already exists in a `portaljs-catalog` portal. Use this when a dataset is more than one file — data + a data dictionary + a methodology doc, or quarterly/yearly files under one dataset. Where [`/portaljs-add-dataset`](/docs/skills/portaljs-add-dataset) creates a **new** dataset (one file), this adds a file to an **existing** one.
Mirrors the Frictionless Data Package model: a dataset holds a `resources[]` array, and the showcase at `/@<namespace>/<slug>` renders **one section per resource** (preview, schema, download). A single-file dataset is migrated to `resources[]` automatically the first time you add a second file — no data is lost.
Required input — ask, don't error
- **Target dataset** — which dataset to add to, by `slug` (or `namespace/slug`).
- **Source** — a local file path or a public URL for the new resource.
- **Portal directory** — defaults to the current directory.
- **Resource name/title** (optional) — a short id + human title for the resource.
Supported formats: **CSV, TSV, JSON (array), GeoJSON** (same as `/portaljs-add-dataset`).
**If the target dataset or source is missing, ask — never dead-end.** When the user doesn't know the slug, read `datasets.json` and list the datasets so they can pick.
Steps
1. Gather input from `$ARGUMENTS` (interview if thin)
Extract:
- `DATASET` — target dataset slug (or `namespace/slug`).
- `SOURCE` — file path or URL of the new resource.
- `PORTAL_DIR` — portal directory (default: `.`).
- `RESOURCE_NAME` — short id within the dataset (default: lowercase-hyphenated filename stem; must be unique among the dataset's resources).
- `RESOURCE_TITLE` — human title (default: derived from the filename).
- `DESCRIPTION` — optional one-line description of the resource.
If `DATASET` or `SOURCE` is missing, ask (listing existing datasets from `datasets.json` when the slug is unknown) and wait.
2. Validate the portal + locate the dataset
Confirm `PORTAL_DIR/datasets.json`, `PORTAL_DIR/package.json`, and `PORTAL_DIR/pages/[owner]/[slug].tsx` exist (the catalog template). Find the dataset entry by `slug` (and `namespace` if given). If it doesn't exist, tell the user and offer to create it with `/portaljs-add-dataset` instead.
3. Detect format and copy/fetch the file
Same as `/portaljs-add-dataset` step 3: detect format from extension/Content-Type (`csv`/`tsv`/`json`/`geojson`), fetch a URL (check HTTP status) or check a local path exists, then copy into the portal:
mkdir -p PORTAL_DIR/public/data cp SOURCE PORTAL_DIR/public/data/RESOURCE_NAME.EXT # or for URLs: curl -L SOURCE -o PORTAL_DIR/public/data/RESOURCE_NAME.EXT
Pick a filename that won't collide with an existing file in `/public/data`.
4. Add the resource to the dataset entry
Open `PORTAL_DIR/datasets.json` and update the target dataset, matching the `Dataset` / `Resource` shape in `lib/providers/types.ts`:
- **If the dataset has no `resources` yet** (it's a single-file dataset with top-level
`file`/`format`/`schema`), **migrate it first**: move the existing file into a `resources` array as the first resource, then remove the now-redundant top-level `file`/`format` (and top-level `schema`, which becomes that resource's schema). Preserve the existing data — this is a lossless transform:
// before
{ "slug": "orders", "name": "Orders", "file": "orders.csv", "format": "csv", "schema": { ... } }
// after (migrated) + new resource appended
{
"slug": "orders", "name": "Orders",
"resources": [
{ "name": "data", "path": "orders.csv", "format": "csv", "title": "Orders", "schema": { ... } },
{ "name": "RESOURCE_NAME", "path": "RESOURCE_NAME.EXT", "format": "<detected>", "title": "RESOURCE_TITLE", "description": "DESCRIPTION" }
]
}- **If the dataset already has `resources`**, just append the new resource object. Ensure
`name` is unique within the array (the showcase uses it as the section key/anchor).
Keep all package-level fields (`description`, `keywords`, `licenses`, `sources`, `version`, …) on the dataset. Drop `description` on the resource only if there genuinely is none.
That is the entire registration — `getResources()` (`lib/datasets.ts`) reads the `resources` array and the showcase renders a section per resource automatically.
5. Verify the build
cd PORTAL_DIR npx next build > /tmp/portaljs-add-resource-build.log 2>&1 BUILD_EXIT=$? tail -20 /tmp/portaljs-add-resource-build.log
The common failure is malformed JSON in `datasets.json`. Fix and rebuild before reporting success.
6. Report
✓ Resource added to DATASET: RESOURCE_TITLE (RESOURCE_NAME.EXT) - Data file: public/data/RESOURCE_NAME.EXT - Manifest: datasets.json (dataset now has <n> resources) - Showcase: /@<namespace>/<slug> renders a section per resource
If this was the first migration to multi-resource, note that the dataset's single `file` was moved into `resources[]` (no data lost).
Notes
- **One source of truth.** Resources live on the dataset's `datasets.json` entry. No page
edits — the showcase loops `getResources(dataset)`.
- **Per-resource schema.** Describe a resource's fields with [`/portaljs-define-schema`](/docs/skills/portaljs-define-schema)
— its Frictionless Table Schema renders under that resource's section.
- **Single-file stays simple.** Datasets with one file keep the plain `file`/`format`
shape; migration to `resources[]` only happens when a second file is added.
Example
/portaljs-add-resource orders ./data/orders-data-dictionary.csv (title: "Data dictionary")
Adds
🌀 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

