Adding a Source
Adding a source is the single highest-impact contribution you can make. Most sources take 30–90 minutes end to end.
Two paths, depending on how standard the API is:
- Manifest-only — for ordinary REST APIs that fit the generic adapter. Just drop a JSON file in
insposearch/sources/and you're done - Manifest + custom adapter — for APIs that need auth quirks, two-step fetches, response mangling, or CORS proxying. Add a
fetch*function insrc/fetchers.jsand register it in theADAPTERSmap
Both paths share the same manifest schema and test protocol.
1. The manifest
One JSON file per source, dropped in insposearch/sources/. File name matches the id field: example-museum.json.
{
"id": "example-museum",
"name": "Example Museum",
"category": "museums",
"homepage": "https://example.org",
"apiBase": "https://api.example.org/v1",
"keyRequired": false,
"keyParam": "",
"searchEndpoint": "/search",
"queryParam": "q",
"perPage": 40,
"pageParam": "page",
"resultsPath": "data.items",
"mapping": {
"title": "title",
"imageUrl": "images.web",
"thumbUrl": "images.thumbnail",
"sourceUrl": "links.self",
"artist": "artist_display",
"date": "date_display",
"medium": "medium_display"
}
}Required fields
| Field | Type | Notes |
|---|---|---|
id | string | Unique, kebab-case. Used as the filename and the registry key |
name | string | Human-readable display name |
category | string | One of: museums, historical, art-design, photography, nature, maps, fashion, science |
homepage | string | Institution homepage URL |
apiBase | string | Base URL for API calls |
searchEndpoint | string | Path appended to apiBase |
queryParam | string | Name of the search query parameter |
resultsPath | string | Dotted path to the results array in the response |
mapping | object | Maps response fields → InspoSearch's normalised schema |
Optional fields
| Field | Default | Notes |
|---|---|---|
keyRequired | false | Set to true and list the source on API Keys in your PR |
keyParam | "" | Query parameter name for the key (or leave empty and use a header) |
perPage | 40 | Results per page |
pageParam | "page" | Pagination parameter name |
headers | {} | Custom headers — use this for Authorization: Bearer … style keys |
rateLimit | 0 | Minimum milliseconds between requests. Use for APIs that throttle aggressively |
The normalised schema
Every result in InspoSearch's grid conforms to this shape. Your mapping just tells the generic adapter how to get there from the source's response.
{
"id": "sourceId-recordId",
"sourceId": "example-museum",
"title": "Artwork title",
"imageUrl": "https://full-resolution-url",
"thumbUrl": "https://thumbnail-url",
"sourceUrl": "https://link-to-original-record",
"artist": "Creator name",
"date": "Date or range",
"medium": "Medium / materials",
"description": "Longer description if available"
}Anything you can't map, omit. Empty fields are fine; wrong fields aren't.
2. If you need a custom adapter
Go this route when:
- The API requires a two-step fetch (search returns IDs, then one call per record — like the Met)
- Response structure has arrays-of-single-element weirdness you need to flatten
- CORS requires proxying via our image proxy worker
- Rate limiting needs custom backoff
- Image URLs are pattern-constructed (
{server}/{id}_{secret}.jpg— Flickr)
Write a fetchExampleMuseum(query, opts) function in src/fetchers.js and register it in ADAPTERS. Look at fetchMet, fetchFlickr, or fetchEuropeana as templates. If you also add display metadata, update BADGE_META, SOURCE_META, SOURCE_GROUPS, and SOURCE_DOMAINS in src/state.js.
3. The 3-term test protocol
Every source must pass these three queries before it ships.
| Term | What it tests |
|---|---|
landscape | Common term — should return many diverse results with thumbnails |
vermeer | Specific artist — tests relevance, artist metadata, and diacritic handling |
quilt | Niche term — tests empty / low-result edge cases |
For each query, verify:
- Results load without console errors
- Thumbnails render (not 404, not broken CORS)
- Clicking a result opens a valid source URL
title,artist,datepopulate when the source has them- Empty or sparse results don't crash the grid or spin forever
Capture screenshots of all three. Attach them to your PR.
4. Test the adapter in isolation
node scripts/test-source.js example-museumRuns the 3-term protocol programmatically and prints per-term counts plus a sample record. Useful for fast iteration before opening the app.
5. Validate
npm run validateCI runs this on every PR. It checks manifest structure, category validity, URL formatting, and duplicate IDs. Fix any errors before pushing.
6. PR checklist
- [ ] Manifest file at
insposearch/sources/<id>.json - [ ]
idis unique and kebab-case - [ ] Category is one of the eight valid values
- [ ]
apiBaseuses HTTPS - [ ] Mapping tested with all three terms
- [ ] Thumbnails render, source URLs open
- [ ]
npm run validatepasses locally - [ ] Screenshots of the 3-term test attached to the PR description
- [ ] Link to the API documentation in the PR description
- [ ] No API keys committed — if the source needs one, set
keyRequired: trueand add it to API Keys in the same PR - [ ] If custom adapter:
fetch*function registered inADAPTERS, metadata added toBADGE_META/SOURCE_META/SOURCE_GROUPS/SOURCE_DOMAINS
Tips
- Understand the API first. Use your browser's Network tab or
curlwith pretty-printed JSON. Read their docs on pagination, rate limits, and image URL construction - Favour the highest-quality image URL you can get. Some APIs return both a thumbnail and a full-res — always map the full-res to
imageUrl - IIIF sources get deep-zoom for free. If the source has IIIF support, prefer the IIIF endpoint — users get pan/zoom with no extra work from you
- If a source throttles, set
rateLimit. Better to queue requests than get the source temporarily disabled by our health tracker - Mention CORS issues in your PR. If the API refuses browser requests, flag it — we may need to fetch server-side via the nightly job instead