PortalJS is a Next.js framework for building data portals and catalogs. This file teaches AI assistants the conventions, patterns, and idioms used across this repo.
packages/
components/ β data viz React components (@portaljs/components)
core/ β layout/UI components (@portaljs/core)
ckan/ β CKAN backend integration (@portaljs/ckan)
remark-*/ β remark plugins for markdown processing
examples/ β reference implementations (read these before building)
.claude/
commands/ β Claude Code slash commands (OSS skills)
datopian/ β Datopian-internal skills (require API keys)
AUTHORING.md β how to write new skills
The template ships its own lightweight components in components/. Do not add @portaljs/components β it bundles leaflet, vega, ag-grid, and pdf.js into a single non-tree-shakeable bundle and is not kept up to date with the local source.
| Need | Where |
|---|---|
| Show tabular data (CSV/TSV/JSON) | components/Table.tsx β uses papaparse + @tanstack/react-table |
| Charts | Add a chart library directly (e.g. recharts, victory) β do not use @portaljs/components |
| Map (GeoJSON) | Add react-leaflet + leaflet directly if needed |
| Page layout, nav | Plain Tailwind β no layout package needed |
| CKAN catalog | @portaljs/ckan only if connecting to a CKAN backend |
CSV or TSV from a local file:
- Place the file in
/public/data/filename.csv - In the page:
<Table url="/data/filename.csv" />
No server-side code needed. Next.js serves /public/ statically. The Table component fetches via url prop using the browser's fetch.
CSV string (inline data):
<Table csv={csvString} />JSON array:
Pass as data prop:
<Table data={rows} cols={[{ key: 'name', name: 'Name' }, ...]} />Remote URL (CORS-enabled):
<Table url="https://example.com/data.csv" />CKAN datastore:
Use datastoreConfig prop β requires a running CKAN backend.
Large files (>5MB): Add a note in the page that loading may be slow. Consider server-side pagination via datastoreConfig.
Standard data portal page:
// pages/datasets/[slug].tsx
import { Table } from '../../components/Table'
import Head from 'next/head'
export default function DatasetPage() {
return (
<>
<Head><title>Dataset Name</title></Head>
<main className="max-w-5xl mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Dataset Name</h1>
<Table url="/data/filename.csv" />
</main>
</>
)
}_app.tsx:
import '../styles/globals.css'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}styles/globals.css β Tailwind directives:
@tailwind base;
@tailwind components;
@tailwind utilities;/β catalog/home (list of datasets)/datasets/[slug]β individual dataset page/api/β server-side API routes (avoid for simple portals; use static data in/public/)
Dataset slug: lowercase, hyphenated. Derived from filename: country-codes.csv β /datasets/country-codes.
- Tailwind CSS everywhere. Always include
@tailwindcss/typographyfor prose content. - Do not use inline styles. Do not use CSS modules unless the project already uses them.
Standard tailwind.config.js:
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: { extend: {} },
plugins: [require('@tailwindcss/typography')],
}Minimal portal:
{
"dependencies": {
"next": "^14.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"@tanstack/react-table": "^8.0.0",
"papaparse": "^5.0.0",
"@heroicons/react": "^2.0.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.0",
"autoprefixer": "^10.0.0",
"postcss": "^8.0.0",
"tailwindcss": "^3.0.0",
"typescript": "^5.0.0",
"@types/node": "^20.0.0",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/papaparse": "^5.0.0"
}
}Add @portaljs/ckan only if connecting to a CKAN backend.
| Variable | Required | Purpose |
|---|---|---|
DMS |
Only for CKAN portals | CKAN backend URL (e.g. https://demo.dev.datopian.com) |
GITHUB_PAT |
Only for GitHub-backed catalogs | GitHub personal access token |
Simple CSV portals need no environment variables.
All components live in components/ inside the portal project (copied from examples/portaljs-template/components/).
import { Table } from '../components/Table'
// From CSV file in /public/
<Table url="/data/file.csv" />
// From CSV string
<Table csv="name,age\nAlice,30" />
// From data array
<Table data={[{name:'Alice',age:30}]} cols={[{key:'name',name:'Name'},{key:'age',name:'Age'}]} />
// Full-width layout
<Table url="/data/file.csv" fullWidth />Add a chart library directly β e.g. npm install recharts. No chart component is bundled in the template.
Add npm install react-leaflet leaflet @types/leaflet and import directly. No map component is bundled in the template.
- Importing from
@portaljs/components: do not install or import from this package. Usecomponents/Table.tsxfrom the local template. The package bundles leaflet, vega, ag-grid, and pdf.js β 1.9 MB compressed. - Wrong import path for Table: use
import { Table } from '../../components/Table'(relative path), not a package import. - Using
/public/paths ingetStaticProps:fs.readFiledoes not work in client components. For server-side data loading, usegetStaticProps. For client-side, use<Table url="..." />. - Tailwind content array too narrow: if custom component classes are missing in production, ensure
'./components/**/*.{js,ts,jsx,tsx}'is intailwind.config.jscontent.
See .claude/commands/ for available slash commands:
/new-portalβ scaffold a new PortalJS data portal from a brief/add-datasetβ add a dataset (CSV/TSV/JSON/GeoJSON) to an existing portal/add-chartβ add a chart (line/bar/area/pie/scatter) to a dataset page via recharts/add-mapβ render a GeoJSON dataset on an interactive Leaflet map/deployβ one-shot deploy to Vercel or static hosting/connect-ckanβ wire a portal to a CKAN backend over its API (decoupled / any backend)
These skills run from any project, not just a clone of this repo β see
.claude/INSTALL.md for the three install paths (run-from-clone,
personal ~/.claude/commands/, or Claude Code plugin).
See .claude/AUTHORING.md to write new skills.