Skip to content

IDEMSInternational/cimh-website

Repository files navigation

CIMH Catalogue

A minimal, static catalogue of entries — a framework you can populate later. Each entry is an abstract "item" (it will eventually hold reports, but nothing in the code assumes that). Items can link out to an externally hosted page or document; this app is purely a discovery / index layer.

Built with Astro + TypeScript, fully static, client-side search and filtering, no backend, no database, no external APIs.

Status: scaffold. The structure, catalogue mechanics and a handful of dummy items + placeholder pages are in place. The copy (home, about, get-involved, contact) and the dummy data under src/content/items/ are meant to be replaced.


Quick start

Requires Node 22+ (it also runs on Node 20.3+).

npm install
npm run dev          # local dev server
npm run build        # static build → dist/
npm run preview      # serve the built site
npm run check        # astro + TypeScript type check
npm run test:logic   # verify the pure filtering/URL logic
npm run test:data    # validate every item JSON against the schema
npm run test         # both of the above

What it does

  • Home (/) — short description + a live item count.
  • Catalogue (/catalogue) — the primary feature:
    • Multi-select filters for category, topic, type, derived dynamically from the data (never hardcoded). These three facet names are generic placeholders — rename them in one place (see below).
    • Case-insensitive search across title, description, keywords and authors.
    • Filters + search combine as (category AND topic AND type) AND search, with OR within a facet and AND across facets.
    • All filter/search state lives in the URL (?category=Research&type=Brief&search=health) so views are shareable and browser back/forward works.
  • Markdown pages (/about, /get-involved, /contact, …) — driven entirely by files in src/content/pages/. Add a page by dropping in a Markdown file; no routing changes.

Architecture

Two-layer content architecture:

src/
├─ content/
│  ├─ items/*.json            # STRUCTURED data layer — one JSON document per item
│  └─ pages/*.md              # CONTENT layer (static markdown pages)
├─ content.config.ts          # defines the `pages` collection (+ stops auto-collection)
├─ lib/
│  ├─ types.ts                # Item type, facet/state types (single source of truth)
│  ├─ items.ts                # DATA ABSTRACTION LAYER — the only place that knows the source
│  └─ filtering.ts            # pure filter + URL (de)serialisation logic (no DOM, testable)
├─ components/
│  ├─ Layout.astro            # site shell (head, nav, footer)
│  ├─ SearchBar.astro
│  ├─ FilterPanel.astro       # renders facets derived from the data
│  └─ ItemCard.astro          # depends only on core fields; everything else optional
├─ scripts/
│  └─ catalogue.ts            # the only client JS: filtering + URL state (~2KB)
├─ styles/global.css          # design tokens + base + prose styles
└─ pages/
   ├─ index.astro
   ├─ catalogue.astro
   └─ [slug].astro            # generic route for every markdown page

Clear separation of concerns:

  • Data layer (lib/items.ts) is the only module that reads the data source. Today it bundles a folder of per-item JSON documents (src/content/items/*.json) via import.meta.glob; tomorrow it can fetch() a remote source or read generated data — the UI never changes because the returned shape stays Item[]. It also normalises records defensively, sorts them by title and derives facets.
  • Filtering logic (lib/filtering.ts) is pure and framework-free, so the same rules are testable (npm run test:logic) and mirrored by the client.
  • UI components depend only on the three guaranteed fields and render optional metadata only when present.

Schema-flexible but UI-stable

Only id, title, description are guaranteed. Everything else (url, authors, category, topic, type, keywords) is optional, and the meta bag holds arbitrary future fields (date, status, source, region, …). Unknown fields are ignored safely and never break rendering — so new metadata can be added over time without schema migrations: add it under meta, which is an open object. (The JSON Schema validates the known fields and steers genuinely new data into meta rather than new top-level keys, keeping the contract stable.)

Renaming / changing the facets

The three facets (category, topic, type) are intentionally generic. To rename one (e.g. topictheme) or add one (e.g. region):

  1. Edit the FACET_KEYS tuple and add/rename the optional string field on Item in src/lib/types.ts.
  2. Add the matching label in FACET_LABELS in src/lib/items.ts.
  3. Update the empty-state shape in emptyState() in src/lib/filtering.ts.
  4. Update the facet's entry in schemas/item.schema.json.

The filter panel, card chips and client filtering all read from FACET_KEYS, so nothing else needs to change.


Adding content

Add an item

Drop a new JSON document into src/content/items/, one file per entry. The id is a stable, globally-unique kebab-case slug; the filename must be <id>.json (e.g. id example-item-01src/content/items/example-item-01.json):

{
  "id": "example-item-01",
  "title": "Title",
  "description": "One-paragraph summary.",
  "url": "https://example.com/items/01",
  "authors": ["Author One"],
  "category": "Research",
  "topic": "Health",
  "type": "Summary",
  "keywords": ["topic", "topic"],
  "meta": { "status": "draft" }
}

Only id, title and description are required. The data layer picks the file up automatically (no central list to edit), and new facet values appear in the filters automatically. Catalogue order is by title.

Each document is validated against a JSON Schema (schemas/item.schema.json):

  • In your editor: .vscode/settings.json maps the schema onto src/content/items/*.json, so VS Code flags missing/mistyped fields as you type.
  • In CI / locally: npm run test:data validates every document against the same schema and checks catalogue-wide invariants (unique ids; filename derived from id). The runtime data layer stays lenient and skips malformed records, so this check is what fails loudly on an authoring mistake before an item silently drops out of the catalogue.

Add a static page

Drop a Markdown file into src/content/pages/:

---
title: FAQ
slug: faq
template: page
description: Frequently asked questions.
# optional nav controls:
nav: true      # default true — set false to keep it routable but out of the menu
order: 4       # optional sort key for the header nav (lower first)
---

# FAQ

It is served at /faq and is added to the header nav automatically (the nav is built from the pages collection, after Home and Catalogue). No routing or layout changes needed. Use nav: false to hide a page from the menu, and order to position it (ties fall back to alphabetical by title).

Note on layout vs template: Astro 5 reserves the layout frontmatter key for its legacy markdown layout feature (it tries to resolve the value as a component path, which breaks the build). We use template instead as a neutral layout hint; the generic [slug].astro route applies the site layout. slug defaults to the filename when omitted.


Keeping the catalogue up to date

The catalogue is just the src/content/items/ folder. Updating it — now or in the future — means adding, editing or removing one <id>.json file per entry. Nothing else has to change: the data layer (src/lib/items.ts) picks files up via import.meta.glob, facet values re-derive from the data, and the catalogue rebuilds. Whatever produces those files, every one is validated against schemas/item.schema.json by npm run test:data in CI, so malformed data is caught before it ships.

Any change merged to main triggers a rebuild + redeploy via .github/workflows/deploy.yml. So the question is only ever "how do the JSON files get written?" — and the design keeps that question open. A few patterns, none of which touch the UI:

  • Hand-authored PRs. A maintainer drops or edits a file in src/content/items/ and opens a pull request. CI validates it; merge deploys. This works today with no extra setup.

  • Push from each item's source (PR-based automation). If each entry maps to its own upstream repo, that repo can carry a small metadata file and run a GitHub Action that opens/updates a PR against this repo, writing or refreshing its own <id>.json. Maintainers review and merge; the deploy workflow does the rest. This keeps each source the owner of its own entry while this repo stays the catalogue/index.

  • Pull from external sources (scheduled, within this repo). A scheduled workflow here (on: schedule) fetches metadata from the external sources, regenerates the affected <id>.json files, and either commits to main directly or opens a PR for review. Good when the upstream sources can't (or shouldn't) push into this repo themselves.

  • Skip committing data entirely. Because src/lib/items.ts is the single data seam, getItems() could instead fetch() a remote source (an API, a published JSON index) at build time. The returned shape stays Item[], so the components never change — only that one function does.

None of this automation is implemented yet — the repo is deliberately set up so it can be added later without a structural rewrite: the data layer is the one swap point, the JSON Schema is the shared contract (additive via the open meta bag, so new fields need no migration), and facets/filters derive from the data so new values appear on their own.


Styling choice

Plain CSS with design tokens, not a utility framework. The design is deliberately minimal and the surface area is small, so a handful of CSS custom properties (spacing scale, type scale, colour) plus per-component scoped styles are easier to read and audit than a framework — and ship near-zero CSS overhead. Tokens live in src/styles/global.css; everything references them. Change the accent by editing the --color-accent* tokens there.

Search choice

Plain case-insensitive substring matching over a precomputed searchable string (title + description + keywords + authors). It fully meets the requirements with zero dependencies. If the dataset grows large enough to need ranked/fuzzy search, lib/filtering.ts is the single seam to swap in Fuse.js or FlexSearch (client-side only — no external search service).


Deployment

Fully static (dist/), so it works as-is on GitHub Pages and Cloudflare Pages.

  • Cloudflare Pages / custom domain / root: remove base and set site to the real host in astro.config.mjs.
  • GitHub Pages project subpath: site and base are already set as placeholders (base: '/cimh-website'). All internal links use import.meta.env.BASE_URL, so they respect base. Update site to the deploying account/org before going live.

What's intentionally left to do

This is a framework, not a finished site. Before launch you'll want to:

  • replace the placeholder copy in src/pages/index.astro, the footer in Layout.astro, and the markdown pages under src/content/pages/;
  • replace the dummy entries in src/content/items/ with real data;
  • rename the facets if category / topic / type aren't the right axes;
  • set the real site (and brand name siteName in Layout.astro);
  • decide how entries will be kept up to date and, if automating, wire up the workflow (see Keeping the catalogue up to date).

About

A minimal, static, filterable catalogue framework (Astro + TypeScript).

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors