/portaljs-connect-ckan
Wire a scaffolded PortalJS portal to a CKAN backend over its API. Generates a tiny server-side fetch client (no runtime dependency) and feeds the /search catalog and /@<namespace>/<slug> showcases from CKAN instead of datasets.json.
> /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-connect-ckan
Context preview
What this command does when you run it.
Wire a scaffolded PortalJS portal to a CKAN backend over its API. Generates a tiny server-side fetch client (no runtime dependency) and feeds the /search catalog and /@<namespace>/<slug> showcases from CKAN instead of datasets.json.
Command definition
portaljs-connect-ckan.mddescription: Wire a scaffolded PortalJS portal to a CKAN backend over its API. Generates a tiny server-side fetch client (no runtime dependency) and feeds the /search catalog and /@<namespace>/<slug> showcases from CKAN instead of datasets.json.
allowed-tools: Read, Write, Edit, Bash, WebFetch
/portaljs-connect-ckan
Connect an existing `portaljs-catalog` portal to a live CKAN backend. The portal stops reading the static `datasets.json` manifest (and files in `/public/data/`) and instead feeds its two data surfaces — the **`/search` catalog** and the **`/@<namespace>/<slug>` showcases** — straight from a CKAN instance's REST API (`package_search` / `package_show`) through a tiny generated fetch client. Output is plain, editable Next.js code with **no runtime dependency** — no opaque framework wiring.
> **Why a fetch client, not `@portaljs/ckan`?** That package's bundle wires React UI > components to React 18 internals (`ReactCurrentDispatcher`) and crashes at import under > the template's React 19 — even server-side, where this skill imports it. The CKAN client > class is locked inside that same module, so it can't be used in isolation. A ~40-line > `fetch` wrapper covers the two REST actions this skill needs (`package_search`, > `package_show`), is server-side only, ships zero client bytes, and has no React coupling.
Use this for the "decoupled / any backend" path: the user has a CKAN data management system (their own or a public one) and wants a browseable portal in front of it.
**You can run this right after `/portaljs-new-portal`.** A fresh portal ships with a few sample datasets so it builds and renders immediately — you do NOT need to hand-author a static dataset list first. If the user says up front they want a CKAN backend, scaffold with the sample data, then run `/portaljs-connect-ckan` to swap the source over to CKAN. Only a CKAN base URL is required.
The generated pages fetch CKAN **server-side** in `getStaticProps`/`getStaticPaths`, so the catalog is pre-rendered at build time and the site can be statically deployed.
Required input — ask, don't error
- **CKAN base URL** (required) — e.g. `https://demo.dev.datopian.com`. The root of the
CKAN instance; the skill appends `/api/3/action/...` itself. Must be publicly reachable.
- **Org filter** (optional) — one or more CKAN organization names to restrict the catalog to.
- **Group filter** (optional) — one or more CKAN group names to restrict the catalog to.
- **Portal directory** (optional) — path to the portal project (default: current directory).
**If the CKAN base URL is missing, ask for it (and the optional org/group filter) — never dead-end with a missing-input error.**
Steps
1. Gather input from `$ARGUMENTS` (interview if thin)
Extract:
- `CKAN_URL` — base URL, with any trailing slash stripped.
- `ORG_FILTER` — list of org names (default: empty = all orgs).
- `GROUP_FILTER` — list of group names (default: empty = all groups).
- `PORTAL_DIR` — portal directory (default: `.`).
If the CKAN base URL is missing, **ask** and wait for the answer:
To connect a CKAN backend I need:
1. CKAN base URL (e.g. https://demo.dev.datopian.com) — required
2. Optional org filter (Enter for all organizations)
3. Optional group filter (Enter for all groups)
4. Portal directory (Enter for current directory)
2. Validate the portal directory
The target must be a `portaljs-catalog` portal. Confirm `PORTAL_DIR/package.json` and `PORTAL_DIR/pages` exist (the catalog template also has `datasets.json`, `pages/search.tsx`, and `pages/[owner]/[slug].tsx`, which this skill rewires). If it isn't a portal, tell the user and suggest running `/portaljs-new-portal` first rather than failing silently.
3. Verify the CKAN backend is reachable
Hit `package_search` with a tiny page to confirm the URL is a working CKAN API:
curl -s -m 20 "CKAN_URL/api/3/action/package_search?rows=1"
The response must be JSON with `"success": true`. If the request fails, times out, or `success` is not `true`, tell the user the URL didn't resolve to a working CKAN API and ask them to confirm it's a CKAN root that's publicly accessible, then retry — don't dead-end.
If `ORG_FILTER` is set, validate each org exists:
curl -s -m 20 "CKAN_URL/api/3/action/organization_show?id=ORG"
If any returns `success: false`, tell the user that org wasn't found, list the valid orgs from `organization_list`, and ask which one they meant (or to drop the filter) before continuing.
4. Generate the CKAN client module (a tiny fetch wrapper)
Write `PORTAL_DIR/lib/ckan.ts` — a self-contained server-side client over the CKAN REST API. **No package install, no `tsconfig` changes**: it uses the built-in `fetch`, so there is nothing to add to `package.json` and no `paths` entry to maintain. The provided URL becomes the default, overridable at deploy time via the `DMS` env var. Org/group filters and the build-time page cap live here as plain editable constants.
// Minimal server-side CKAN client — plain fetch, no dependency, no React coupling.
// Used ONLY in getStaticProps/getStaticPaths, so it never reaches the browser bundle.
// CKAN backend base URL. Override at deploy time with the DMS env var.
export const DMS = (process.env.DMS || 'CKAN_URL').replace(/\/+$/, '')
// Filters baked in by /portaljs-connect-ckan. Empty array = no filter.
export const ORG_FILTER: string[] = [/* ORG_FILTER */]
export const GROUP_FILTER: string[] = [/* GROUP_FILTER */]
// Max datasets to pre-render at build time (SSG). Raise for larger catalogs;
// note every dataset becomes one statically generated page.
export const MAX_DATASETS = 200
// CKAN REST shapes — only the fields the pages read.
type CkanResource = { id: string; name?: string; format?: string; url?: string }
type CkanOrganization = { name?: string; title?: string }
export type CkanPackage = {
name: string
title?: string
notes?: string
organization?: CkanOrganization
resRead more
description: Wire a scaffolded PortalJS portal to a CKAN backend over its API. Generates a tiny server-side fetch client (no runtime dependency) and feeds the /search catalog and /@<namespace>/<slug> showcases from CKAN instead of datasets.json. allowed-tools: Read, Write, Edit, Bash, WebFetch
/portaljs-connect-ckan
Connect an existing `portaljs-catalog` portal to a live CKAN backend. The portal stops reading the static `datasets.json` manifest (and files in `/public/data/`) and instead feeds its two data surfaces — the **`/search` catalog** and the **`/@<namespace>/<slug>` showcases** — straight from a CKAN instance's REST API (`package_search` / `package_show`) through a tiny generated fetch client. Output is plain, editable Next.js code with **no runtime dependency** — no opaque framework wiring.
> **Why a fetch client, not `@portaljs/ckan`?** That package's bundle wires React UI > components to React 18 internals (`ReactCurrentDispatcher`) and crashes at import under > the template's React 19 — even server-side, where this skill imports it. The CKAN client > class is locked inside that same module, so it can't be used in isolation. A ~40-line > `fetch` wrapper covers the two REST actions this skill needs (`package_search`, > `package_show`), is server-side only, ships zero client bytes, and has no React coupling.
Use this for the "decoupled / any backend" path: the user has a CKAN data management system (their own or a public one) and wants a browseable portal in front of it.
**You can run this right after `/portaljs-new-portal`.** A fresh portal ships with a few sample datasets so it builds and renders immediately — you do NOT need to hand-author a static dataset list first. If the user says up front they want a CKAN backend, scaffold with the sample data, then run `/portaljs-connect-ckan` to swap the source over to CKAN. Only a CKAN base URL is required.
The generated pages fetch CKAN **server-side** in `getStaticProps`/`getStaticPaths`, so the catalog is pre-rendered at build time and the site can be statically deployed.
Required input — ask, don't error
- **CKAN base URL** (required) — e.g. `https://demo.dev.datopian.com`. The root of the
CKAN instance; the skill appends `/api/3/action/...` itself. Must be publicly reachable.
- **Org filter** (optional) — one or more CKAN organization names to restrict the catalog to.
- **Group filter** (optional) — one or more CKAN group names to restrict the catalog to.
- **Portal directory** (optional) — path to the portal project (default: current directory).
**If the CKAN base URL is missing, ask for it (and the optional org/group filter) — never dead-end with a missing-input error.**
Steps
1. Gather input from `$ARGUMENTS` (interview if thin)
Extract:
- `CKAN_URL` — base URL, with any trailing slash stripped.
- `ORG_FILTER` — list of org names (default: empty = all orgs).
- `GROUP_FILTER` — list of group names (default: empty = all groups).
- `PORTAL_DIR` — portal directory (default: `.`).
If the CKAN base URL is missing, **ask** and wait for the answer:
To connect a CKAN backend I need: 1. CKAN base URL (e.g. https://demo.dev.datopian.com) — required 2. Optional org filter (Enter for all organizations) 3. Optional group filter (Enter for all groups) 4. Portal directory (Enter for current directory)
2. Validate the portal directory
The target must be a `portaljs-catalog` portal. Confirm `PORTAL_DIR/package.json` and `PORTAL_DIR/pages` exist (the catalog template also has `datasets.json`, `pages/search.tsx`, and `pages/[owner]/[slug].tsx`, which this skill rewires). If it isn't a portal, tell the user and suggest running `/portaljs-new-portal` first rather than failing silently.
3. Verify the CKAN backend is reachable
Hit `package_search` with a tiny page to confirm the URL is a working CKAN API:
curl -s -m 20 "CKAN_URL/api/3/action/package_search?rows=1"
The response must be JSON with `"success": true`. If the request fails, times out, or `success` is not `true`, tell the user the URL didn't resolve to a working CKAN API and ask them to confirm it's a CKAN root that's publicly accessible, then retry — don't dead-end.
If `ORG_FILTER` is set, validate each org exists:
curl -s -m 20 "CKAN_URL/api/3/action/organization_show?id=ORG"
If any returns `success: false`, tell the user that org wasn't found, list the valid orgs from `organization_list`, and ask which one they meant (or to drop the filter) before continuing.
4. Generate the CKAN client module (a tiny fetch wrapper)
Write `PORTAL_DIR/lib/ckan.ts` — a self-contained server-side client over the CKAN REST API. **No package install, no `tsconfig` changes**: it uses the built-in `fetch`, so there is nothing to add to `package.json` and no `paths` entry to maintain. The provided URL becomes the default, overridable at deploy time via the `DMS` env var. Org/group filters and the build-time page cap live here as plain editable constants.
// Minimal server-side CKAN client — plain fetch, no dependency, no React coupling.
// Used ONLY in getStaticProps/getStaticPaths, so it never reaches the browser bundle.
// CKAN backend base URL. Override at deploy time with the DMS env var.
export const DMS = (process.env.DMS || 'CKAN_URL').replace(/\/+$/, '')
// Filters baked in by /portaljs-connect-ckan. Empty array = no filter.
export const ORG_FILTER: string[] = [/* ORG_FILTER */]
export const GROUP_FILTER: string[] = [/* GROUP_FILTER */]
// Max datasets to pre-render at build time (SSG). Raise for larger catalogs;
// note every dataset becomes one statically generated page.
export const MAX_DATASETS = 200
// CKAN REST shapes — only the fields the pages read.
type CkanResource = { id: string; name?: string; format?: string; url?: string }
type CkanOrganization = { name?: string; title?: string }
export type CkanPackage = {
name: string
title?: string
notes?: string
organization?: CkanOrganization
res🌀 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

