/portaljs-add-dcat
Make a PortalJS portal harvestable by national/EU/US open-data portals — emit standards-compliant DCAT catalog feeds (DCAT 2/3, DCAT-AP, DCAT-US, national profiles) in JSON-LD, Turtle, and RDF/XML at build, with autodiscovery and per-profile conformance checking.
> /plugin marketplace add datopian/portaljs> /plugin install portaljs@datopian-portaljs
Command definition
portaljs-add-dcat.mddescription: Make a PortalJS portal harvestable by national/EU/US open-data portals — emit standards-compliant DCAT catalog feeds (DCAT 2/3, DCAT-AP, DCAT-US, national profiles) in JSON-LD, Turtle, and RDF/XML at build, with autodiscovery and per-profile conformance checking.
allowed-tools: Read, Write, Edit, Bash, WebFetch
/portaljs-add-dcat
Turn an existing PortalJS (`portaljs-catalog`) portal into a **harvestable** data catalog: emit standards-compliant **DCAT** metadata feeds so external catalogs and government data portals (data.europa.eu, data.gov, national portals) can harvest its datasets automatically.
PortalJS is Frictionless-native (a dataset is a Data Package — see `/portaljs-define-schema`); **DCAT is the serialization + harvest layer on top** (`lib/metadata/dcat.ts` + `lib/metadata/dcat-profiles.ts`). This skill selects one or more DCAT **application profiles**, maps every dataset's metadata to them, and writes static feed files at build so they harvest on **any** host (static Cloudflare Pages, a CDN, a Worker) — no runtime.
What it produces
Wired into `predev`/`prebuild` via `scripts/generate-dcat.ts`, so the feeds are always fresh:
| File | What | |------|------| | `public/catalog.jsonld` · `catalog.ttl` · `catalog.rdf` | **Canonical** feed (first profile) in JSON-LD, Turtle, RDF/XML — the stable autodiscovery targets | | `public/catalog.<profile>.{jsonld,ttl,rdf}` | One feed per configured profile × serialization (e.g. `catalog.dcat-ap.ttl`) | | `public/catalog-feeds.json` | Machine-readable index of every emitted feed | | `<link rel="alternate" type="application/ld+json" href="/catalog.jsonld">` in `_document.tsx` | Autodiscovery — how harvesters find the feed |
Config lives in **`dcat.config.json`** at the portal root (created/updated by this skill). All feeds regenerate from `datasets.json` + this config.
Supported profiles
Profiles are a **pluggable registry** (`lib/metadata/dcat-profiles.ts`) — national profiles are config/data, not hardcoded, and multiple can be emitted at once.
| id | Profile | Notes | |----|---------|-------| | `dcat-3` | DCAT 3 (W3C) | Default. `conformsTo` DCAT-3. | | `dcat-2` | DCAT 2 (W3C) | Same core subset stamped as DCAT-2. | | `dcat-ap` | DCAT-AP (data.europa.eu) | EU profile: adds foaf/vcard, publisher, contactPoint, `dcat:theme` (EU data-theme vocab). | | `dcat-us` | DCAT-US 3.0 (data.gov) | US federal / Project Open Data: adds publisher, contactPoint, `dct:accessLevel`. | | `geodcat-ap` | GeoDCAT-AP (spatial / INSPIRE) | DCAT-AP + spatial coverage: `dct:spatial` (bbox/geometry as `gsp:wktLiteral`) + `dcat:spatialResolutionInMeters` from each dataset's `dcat.spatial` (or the catalog-wide `spatial` config). | | `croissant` | Croissant (MLCommons / schema.org) | ML-dataset metadata: a schema.org `DataCatalog` of Croissant `Dataset`s with `cr:FileObject` distributions + `cr:RecordSet`/`cr:Field` mapped from the Frictionless Table Schema. **JSON-LD only.** | | `dcat-ap-se` · `dcat-ap-ch` · `dcat-ap-de` | National (Sweden / Switzerland / Germany) | DCAT-AP + national `conformsTo`; examples of the plug mechanism. | | `dcat-ap-it` | DCAT-AP_IT (Italy — AgID / dati.gov.it) | DCAT-AP + `dcatapit:*` types, agents with `dct:identifier` (IPA code), `dct:rightsHolder` + `dct:accrualPeriodicity` on every dataset, licence on every distribution. Needs `publisher.identifier`. |
**Add another national profile** without code: `registerDcatProfile(makeNationalProfile({ id, label, conformsTo, context }))` in a small module the app loads, then list its id in `dcat.config.json`. See `lib/metadata/README.md`.
**GeoDCAT-AP spatial input.** Per dataset, add `"dcat": { "spatial": { "bbox": "POLYGON((…))" } }` (or `"geometry"`, `"uri"`, `"spatialResolutionInMeters"`) to `datasets.json`; or set a catalog-wide `"spatial"` in `dcat.config.json`. WKT strings are serialized as `gsp:wktLiteral`. A dataset with no spatial coverage is still GeoDCAT-AP-conformant.
**DCAT-AP_IT input.** Set `publisher.identifier` in `dcat.config.json` to the IPA code (Indice della Pubblica Amministrazione) of the publishing administration — the VAT number for a private body — plus EU `themes` and a licence (dataset `licenses`, or `license` in config). Dataset identifiers become `<IPA code>:<namespace>/<slug>`. Per dataset, `datasets.json` can add `"dcat": { "rightsHolder": { "name": "Comune di Palermo", "identifier": "c_g273" }, "accrualPeriodicity": "ANNUAL" }` (an EU frequency token or IRI) and an explicit `"identifier"` (e.g. `"c_g273:D.12"`). Defaults: the rights holder is the dataset's publisher, else the catalog publisher; the frequency is `UNKNOWN` (AgID allows it); `dct:modified` falls back to the catalog's. Other profiles ignore these fields.
**Croissant** describes datasets for ML tooling (schema.org JSON-LD). It reads the Frictionless Table Schema to emit `cr:RecordSet`/`cr:Field`, and stamps a `sha256` on each `cr:FileObject` from the local `public/data/<file>` bytes (mlcroissant requires a checksum on a hosted file; remote-only files are emitted without one). Because a Croissant document describes a single dataset, the feed is a `DataCatalog` whose `dataset[]` entries are each a standalone Croissant `Dataset`.
Required input — ask, don't error
- **Portal directory** — path to the portal project (defaults to current directory).
- **Profiles** — which to emit (default: `dcat-3`). For national-portal harvesting the
user wants `dcat-ap` (EU) or `dcat-us` (US) + optionally a national profile.
- **Publisher + contact** — DCAT-AP and DCAT-US **require** `dct:publisher` and
`dcat:contactPoint`. If the user picks one of those profiles, ask for the publishing organization (name + homepage) and a contact (name + email). Without them the feed is emitted but **fails conformance** — the generator will say what's missing.
- **Site URL** — the portal's public origin (e.g. `https://data.example.org`), so feed
links are absolute. Without it links are
Read more
description: Make a PortalJS portal harvestable by national/EU/US open-data portals — emit standards-compliant DCAT catalog feeds (DCAT 2/3, DCAT-AP, DCAT-US, national profiles) in JSON-LD, Turtle, and RDF/XML at build, with autodiscovery and per-profile conformance checking. allowed-tools: Read, Write, Edit, Bash, WebFetch
/portaljs-add-dcat
Turn an existing PortalJS (`portaljs-catalog`) portal into a **harvestable** data catalog: emit standards-compliant **DCAT** metadata feeds so external catalogs and government data portals (data.europa.eu, data.gov, national portals) can harvest its datasets automatically.
PortalJS is Frictionless-native (a dataset is a Data Package — see `/portaljs-define-schema`); **DCAT is the serialization + harvest layer on top** (`lib/metadata/dcat.ts` + `lib/metadata/dcat-profiles.ts`). This skill selects one or more DCAT **application profiles**, maps every dataset's metadata to them, and writes static feed files at build so they harvest on **any** host (static Cloudflare Pages, a CDN, a Worker) — no runtime.
What it produces
Wired into `predev`/`prebuild` via `scripts/generate-dcat.ts`, so the feeds are always fresh:
| File | What | |------|------| | `public/catalog.jsonld` · `catalog.ttl` · `catalog.rdf` | **Canonical** feed (first profile) in JSON-LD, Turtle, RDF/XML — the stable autodiscovery targets | | `public/catalog.<profile>.{jsonld,ttl,rdf}` | One feed per configured profile × serialization (e.g. `catalog.dcat-ap.ttl`) | | `public/catalog-feeds.json` | Machine-readable index of every emitted feed | | `<link rel="alternate" type="application/ld+json" href="/catalog.jsonld">` in `_document.tsx` | Autodiscovery — how harvesters find the feed |
Config lives in **`dcat.config.json`** at the portal root (created/updated by this skill). All feeds regenerate from `datasets.json` + this config.
Supported profiles
Profiles are a **pluggable registry** (`lib/metadata/dcat-profiles.ts`) — national profiles are config/data, not hardcoded, and multiple can be emitted at once.
| id | Profile | Notes | |----|---------|-------| | `dcat-3` | DCAT 3 (W3C) | Default. `conformsTo` DCAT-3. | | `dcat-2` | DCAT 2 (W3C) | Same core subset stamped as DCAT-2. | | `dcat-ap` | DCAT-AP (data.europa.eu) | EU profile: adds foaf/vcard, publisher, contactPoint, `dcat:theme` (EU data-theme vocab). | | `dcat-us` | DCAT-US 3.0 (data.gov) | US federal / Project Open Data: adds publisher, contactPoint, `dct:accessLevel`. | | `geodcat-ap` | GeoDCAT-AP (spatial / INSPIRE) | DCAT-AP + spatial coverage: `dct:spatial` (bbox/geometry as `gsp:wktLiteral`) + `dcat:spatialResolutionInMeters` from each dataset's `dcat.spatial` (or the catalog-wide `spatial` config). | | `croissant` | Croissant (MLCommons / schema.org) | ML-dataset metadata: a schema.org `DataCatalog` of Croissant `Dataset`s with `cr:FileObject` distributions + `cr:RecordSet`/`cr:Field` mapped from the Frictionless Table Schema. **JSON-LD only.** | | `dcat-ap-se` · `dcat-ap-ch` · `dcat-ap-de` | National (Sweden / Switzerland / Germany) | DCAT-AP + national `conformsTo`; examples of the plug mechanism. | | `dcat-ap-it` | DCAT-AP_IT (Italy — AgID / dati.gov.it) | DCAT-AP + `dcatapit:*` types, agents with `dct:identifier` (IPA code), `dct:rightsHolder` + `dct:accrualPeriodicity` on every dataset, licence on every distribution. Needs `publisher.identifier`. |
**Add another national profile** without code: `registerDcatProfile(makeNationalProfile({ id, label, conformsTo, context }))` in a small module the app loads, then list its id in `dcat.config.json`. See `lib/metadata/README.md`.
**GeoDCAT-AP spatial input.** Per dataset, add `"dcat": { "spatial": { "bbox": "POLYGON((…))" } }` (or `"geometry"`, `"uri"`, `"spatialResolutionInMeters"`) to `datasets.json`; or set a catalog-wide `"spatial"` in `dcat.config.json`. WKT strings are serialized as `gsp:wktLiteral`. A dataset with no spatial coverage is still GeoDCAT-AP-conformant.
**DCAT-AP_IT input.** Set `publisher.identifier` in `dcat.config.json` to the IPA code (Indice della Pubblica Amministrazione) of the publishing administration — the VAT number for a private body — plus EU `themes` and a licence (dataset `licenses`, or `license` in config). Dataset identifiers become `<IPA code>:<namespace>/<slug>`. Per dataset, `datasets.json` can add `"dcat": { "rightsHolder": { "name": "Comune di Palermo", "identifier": "c_g273" }, "accrualPeriodicity": "ANNUAL" }` (an EU frequency token or IRI) and an explicit `"identifier"` (e.g. `"c_g273:D.12"`). Defaults: the rights holder is the dataset's publisher, else the catalog publisher; the frequency is `UNKNOWN` (AgID allows it); `dct:modified` falls back to the catalog's. Other profiles ignore these fields.
**Croissant** describes datasets for ML tooling (schema.org JSON-LD). It reads the Frictionless Table Schema to emit `cr:RecordSet`/`cr:Field`, and stamps a `sha256` on each `cr:FileObject` from the local `public/data/<file>` bytes (mlcroissant requires a checksum on a hosted file; remote-only files are emitted without one). Because a Croissant document describes a single dataset, the feed is a `DataCatalog` whose `dataset[]` entries are each a standalone Croissant `Dataset`.
Required input — ask, don't error
- **Portal directory** — path to the portal project (defaults to current directory).
- **Profiles** — which to emit (default: `dcat-3`). For national-portal harvesting the
user wants `dcat-ap` (EU) or `dcat-us` (US) + optionally a national profile.
- **Publisher + contact** — DCAT-AP and DCAT-US **require** `dct:publisher` and
`dcat:contactPoint`. If the user picks one of those profiles, ask for the publishing organization (name + homepage) and a contact (name + email). Without them the feed is emitted but **fails conformance** — the generator will say what's missing.
- **Site URL** — the portal's public origin (e.g. `https://data.example.org`), so feed
links are absolute. Without it links are
🌀 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).
Other commands on portaljs.
More commands from other plugins.
build
CommandImplement tasks incrementally — build, test, verify, commit. Add "auto" to run the whole plan in one approved pass.

