A statically deployable terminology browser. Install as an npm package, configure via YAML, build, deploy.
# 1. Create your project
mkdir my-dictionary && cd my-dictionary
npm init -y
# 2. Install
npm install @glossarist/concept-browser
# 3. Create site-config.yml
cat > site-config.yml << 'EOF'
id: my-dictionary
title: My Dictionary
uri_base: https://example.com
datasets:
- id: my-vocab
title: My Vocabulary
local_path: datasets/my-vocab # or gcr_package: https://...
EOF
# 4. Build
npx concept-browser build
# 5. Deploy
# Upload dist/ to any static host (GitHub Pages, Netlify, S3, etc.)Your project (CWD) Package (read-only)
├── site-config.yml ──┐
├── datasets/ ─┤
│ └── my-vocab/ ─┤ ┌──────────────────────────┐
│ └── concepts/*.yaml ─┤ │ @glossarist/concept-browser │
├── public/ ─┤ ──> │ │
│ └── (logos, favicons) ─┤ │ generate-data.ts │
├── .cb-content/ ← generated │ │ bridge-to-astro.ts │
└── dist/ ← output │ │ astro build │
──┘ └──────────────────────────┘
The package reads your config and data, generates .cb-content/ (content collections) and dist/ (static site) in your project. The package itself (node_modules/) is never modified.
There is a strict separation between dataset authors and deployers:
| Role | Owns | Does NOT touch |
|---|---|---|
| Dataset author | YAML concept files, bibliography, figures/tables | site-config.yml, deployment URLs, other datasets |
| Deployer | site-config.yml, URI patterns, branding, fonts | dataset content, inline references, concept IDs |
| concept-browser | Resolves every citation at runtime | — |
The same dataset YAML works in any deployment. A citation like {{cite:iso704}} resolves differently depending on which datasets are co-deployed — the deployer's site-config.yml:uriPatterns decides.
All inline references in concept text use the unified {{kind:target}} notation:
| Kind | Syntax | Example | Description |
|---|---|---|---|
| cite | {{cite:sourceId}} |
{{cite:iso7301}} |
Cite a ConceptSource from this concept's sources[] |
| cite+label | {{cite:id, label}} |
{{cite:iso7301, rice}} |
Same, with explicit display label |
| urn | {{urn:URN}} |
{{urn:iso:std:iso:704}} |
Reference via URN routing |
| fig | {{fig:id}} |
{{fig:diagram_3}} |
Reference a figure entity |
| table | {{table:id}} |
{{table:unit_list}} |
Reference a table entity |
| formula | {{formula:id}} |
{{formula:ohm_law}} |
Reference a formula entity |
| bib | {{bib:id}} |
{{bib:ref_1}} |
Bibliography entry (no underlying concept) |
| link | {{link:URL}} |
{{link:https://example.com}} |
External URL |
| link+label | {{link:URL, label}} |
{{link:https://example.com, click here}} |
External URL with label |
| image | {{image:src}} |
{{image:diagram.png}} |
Inline image embed |
| image+alt | {{image:src, alt}} |
{{image:diagram.png, The diagram}} |
Image with alt text |
| designation | {{designation}} |
{{measurement unit}} |
Reference by designation text |
| numeric | {{numeric_id}} |
{{112-01-10}} |
Reference by numeric ID |
The deprecated <<ref,title>> AsciiDoc xref syntax still works but emits a console warning. Migrate to the unified syntax.
When concept-browser encounters {{cite:sourceId}}, it walks this cascade:
- uriPatterns — Is there a co-deployed dataset matching this URI? → internal link
- routing[] — Is there a routing entry in site-config.yml? → external link
- citation.link — Does the source have a canonical link? → flat bibliography record
- unresolved — No match → plain text
Everything is in site-config.yml. See site-config.example.yml for all options.
Each dataset specifies where its source data lives. Three options:
datasets:
# Option A: Local directory with concepts/*.yaml
- id: my-vocab
local_path: datasets/my-vocab # relative to project root
# Option B: GCR package (auto-downloaded at build time)
- id: iev
gcr_package: https://github.com/org/repo/releases/download/v1.0/my-vocab.gcr
# Option C: Git repository (cloned at build time)
- id: iso-terms
source_repo: https://github.com/org/iso-termsEach dataset can have a color — single hex or light/dark pair:
datasets:
- id: my-vocab
color: "#2563eb" # single color (both modes)
- id: legacy-vocab
color:
light: "#004996" # light mode
dark: "#3B82F6" # dark modeColors appear in the sidebar (color dot per dataset), sphere cards (top bar + tint), badges, and relationship edges. If omitted, a palette color is assigned automatically.
Group colors work the same way:
dataset_groups:
- id: my-vocab-series
color: "#d97706"Place logo SVGs/PNGs in public/ and reference them in branding:
branding:
logo:
alt: My Dictionary
light: /images/logo-light.svg # shown in light mode
dark: /images/logo-dark.svg # shown in dark modeIf no logo is configured, the Glossarist logo is shown by default. The footer always shows the Glossarist logo ("Powered by Glossarist").
Brand typography is fully slot-based and category-agnostic. There are four slots — title, heading, body, mono — and each slot accepts any category (serif, sans-serif, monospace). Nothing dictates that "headings must be serif" or "body must be sans-serif". Pick whatever combination matches your brand.
| Slot | Applies to | Default family | Default category |
|---|---|---|---|
title |
The single most-prominent text on each page (home hero, concept name on detail page, dataset/group title) | DM Serif Display | serif |
heading |
h2–h6 section headings | DM Serif Display | serif |
body |
Paragraph text, lists, table cells | DM Sans | sans-serif |
mono |
Code blocks, inline code, kbd | JetBrains Mono | monospace |
The defaults preserve the Glossarist visual identity — override any of them to match your brand.
Set category on any slot to control the fallback chain (the browser uses it when the primary family fails to load or is still loading):
branding:
fonts:
title:
family: Inter
source: google
category: sans-serif # sans-serif title (was serif by default)
weights: [400, 600, 700]
heading:
family: Inter
source: google
category: sans-serif
weights: [600, 700]
body:
family: Merriweather
source: google
category: serif # serif body (was sans-serif by default)
weights: [400, 700]
mono:
family: Fira Code
source: google
category: monospace
weights: [400, 500]This produces:
| Slot | CSS variable | Stack |
|---|---|---|
| title | --font-title |
'Inter', system-ui, sans-serif |
| heading | --font-heading (and --font-header for backward compat) |
'Inter', system-ui, sans-serif |
| body | --font-body |
'Merriweather', Georgia, serif |
| mono | --font-mono |
'Fira Code', ui-monospace, "JetBrains Mono", Menlo, Monaco, monospace |
source |
Behavior |
|---|---|
google |
Build emits a Google Fonts CSS @import for the declared family + weights. |
url |
Build emits a @font-face block loading from url. |
local |
Consumer ships the font files in public/; no build-time fetch. |
The legacy branding.fonts.header slot still works — it's a deprecated alias for branding.fonts.heading. Existing configs don't break. The CSS variable --font-header is still emitted (as an alias for --font-heading) so existing stylesheets that reference it continue to work.
branding:
fonts:
title: { family: Inter, source: google, category: sans-serif }
heading: { family: Inter, source: google, category: sans-serif }
body: { family: Inter, source: google, category: sans-serif }
mono: { family: JetBrains Mono, source: google, category: monospace }branding:
fonts:
title: { family: Playfair Display, source: google, category: serif }
heading: { family: Lora, source: google, category: serif }
body: { family: Source Sans Pro, source: google, category: sans-serif }
mono: { family: Source Code Pro, source: google, category: monospace }branding.favicon accepts two shapes — a legacy string form and an object form that lets consumers provide their own canonical favicon set without writing a post-build script.
Path to a single source SVG/PNG. The CLI generates the full variant set (favicon.ico, apple-touch-icon-*.png, etc.) from it using the favicons package:
branding:
favicon: assets/my-brand.svgThe object form lets you provide your own canonical favicon set (typically RealFaviconGenerator output) and have the CLI install it without a post-build script.
A canonical favicon set is multiple files — typically favicon.svg, favicon.ico, favicon-96x96.png, apple-touch-icon.png, web-app-manifest-192x192.png, web-app-manifest-512x512.png, and site.webmanifest. Put all of them in a directory (e.g. assets/favicons/) and reference it via source_dir:
my-deployment/
├── site-config.yml
└── assets/
└── favicons/ ← branding.favicon.source_dir points here
├── favicon.svg
├── favicon.ico
├── favicon-96x96.png
├── apple-touch-icon.png
├── web-app-manifest-192x192.png
├── web-app-manifest-512x512.png
└── site.webmanifest ← optional; CLI regenerates with BASE_PATH
Then in site-config.yml:
branding:
favicon:
base_path: / # URL prefix (BASE_PATH-aware; default '/')
source_dir: assets/favicons # canonical files (all of them, in one directory)
icons: # DATA — declare each icon, not HTML
- rel: icon
type: image/svg+xml
href: favicon.svg
- rel: icon
type: image/png
sizes: 96x96
href: favicon-96x96.png
- rel: shortcut icon
href: favicon.ico
- rel: apple-touch-icon
sizes: 180x180
href: apple-touch-icon.png
- rel: manifest
href: site.webmanifestThe CLI copies every file from source_dir/ into public/ (also removing the default cruft), then renders one <link> tag per icon entry. The href is a filename — the system applies the correct base_path prefix automatically. Absolute URLs (https://cdn.example.com/x.png) and root-relative paths (/x.png) are emitted unchanged.
| Field | Type | Effect |
|---|---|---|
source_dir |
string | Path (relative to cwd) to a directory containing all canonical favicon files. The CLI copies every file in this directory into public/, overriding any defaults. Also removes the default-generated cruft (apple-touch-icon-57x57.png, favicon-16x16.png, etc.) so it doesn't linger. |
icons |
FaviconIcon[] |
DATA — recommended. Array of { rel, href, type?, sizes? } entries. Each renders to one <link> tag with BASE_PATH-aware href. Replaces the default 16-link set. |
skip_default_links |
boolean | When true, the CLI does NOT call the favicons package and does NOT emit the default <link> tags. Pair with icons and source_dir for fully custom branding. |
base_path |
string | URL prefix prepended to every emitted link. Useful for BASE_PATH-scoped deployments (e.g. /vocab/). |
links_html |
string | @deprecated — use icons instead. Raw HTML emitted verbatim. Impossible to validate or safely BASE_PATH-rewrite; kept for backward compat with a console warning. |
The object form exists so consumers with a RealFaviconGenerator favicon set (or any other canonical brand favicon bundle) can install it without a post-build script. Previously this required workarounds like glossarist/cie-eilv/scripts/install-favicons.mjs (149 lines) and glossarist/iala-vocab/scripts/install-favicons.mjs (175 lines) — both are now unnecessary.
If neither form is set, the Glossarist default favicon is used.
Place JSON files in public/pages/:
public/
└── pages/
├── about.json → /about
└── my-vocab-about.json → /my-vocab-about
Each file:
{
"title": "About This Dictionary",
"html": "<p>Content here. Supports full HTML.</p>"
}The page appears in the sidebar navigation automatically.
See site-config.example.yml for all options including UI languages, fonts, features, and dataset groups.
Concept-browser enforces a strict separation between data (authored by dataset authors) and deployment (configured by deployers):
- Dataset authors write concepts, sources, bibliography, and inline mentions. They don't know where their dataset will be deployed or what other datasets will be co-deployed.
- Deployers write
site-config.ymlto register datasets, declare URI patterns, and optionally add routing entries for external datasets. They never edit dataset content. - Concept-browser resolves every cross-reference at runtime via a fixed cascade, making multiple datasets behave as one coherent whole.
Every {{cite:...}} and {{urn:...}} mention walks this cascade at render time:
uriPatterns— is there a co-deployed dataset that matches? → internal link (case 1)routing[]— is there a routing entry for the URI? → external link (case 2)citation.link— does the source have a canonical link? → flat bib record (case 3)- Unresolved — plain text
The same YAML renders differently in different deployments. The data never changes.
All inline references in concept text use the unified {{kind:target}} notation:
| Kind | Example | What it does |
|---|---|---|
cite |
{{cite:sourceId}} |
Cite a ConceptSource from this concept's sources[] — walks the full resolution cascade |
urn |
{{urn:iso:std:iso:704}} |
Reference a concept via URN routing |
fig |
{{fig:diagram_3}} |
Reference a figure entity in the same dataset |
table |
{{table:units}} |
Reference a table entity |
formula |
{{formula:ohm_law}} |
Reference a formula entity |
bib |
{{bib:ref_1}} |
Reference a bibliography entry (case-3-only, no underlying concept) |
link |
{{link:https://example.com}} |
External URL (canonical, deployment-independent) |
image |
{{image:src, alt}} |
Inline image embed |
| (none) | {{measurement unit}} |
Designation match in same dataset |
| (none) | {{112-01-10}} |
Numeric ID match in same dataset |
Each kind accepts an optional label: {{kind:target, label}}.
Deprecated: <<ref,title>> (AsciiDoc xref syntax) emits a deprecation warning. Migrate to {{kind:target}}.
See /learn/inline-content on any deployed site for a full interactive reference.
npx concept-browser fetch # Download datasets from GCR/repos
npx concept-browser generate # Convert YAML → JSON-LD + RDF artifacts
npx concept-browser build # Full pipeline: fetch + generate + build site
Add these to your project's .gitignore:
.cb-content/
dist/
.datasets/
.gcr/
public/data/
Each concept is a YAML file in datasets/{id}/concepts/:
---
data:
identifier: "1-1-1"
localized_concepts:
eng: <uuid>
sources:
- type: authoritative
origin:
ref:
source: "ISO 9000"
definition:
- content: "The concept definition text."
terms:
- type: expression
normative_status: preferred
designation: "quality"
language_code: eng
---Or use a Glossarist GCR package — set gcr_package in your dataset config and the CLI downloads it automatically.
The build outputs a static site to dist/. Deploy it anywhere:
- GitHub Pages: Push
dist/togh-pagesbranch - Netlify/Vercel: Set build command to
npx concept-browser build, publish directory todist/ - S3/CloudFront:
aws s3 sync dist/ s3://my-bucket/
No server required. All data is pre-built into static JSON files.