Skip to main content

Overview

The catalog system manages the curated index of all published entries. It provides a centralized manifest, featured spotlight content, and editorial controls for staging and publishing changes to test/production environments.
The catalog data lives in data/catalog.editorial.json (editorial staging) and data/catalog.entries.json (build artifacts).

Catalog Architecture

Catalog Editorial File

The catalog.editorial.json is the source of truth for staged catalog changes:
{
  "version": "catalog-editorial-v1",
  "updatedAt": "2026-03-07T10:30:00.000Z",
  "manifest": [
    {
      "entry_id": "john-doe-piano",
      "entry_href": "/entry/john-doe-piano/",
      "title_raw": "John Doe performing Piano",
      "lookup_number": "A.Pl S4 01",
      "season": "S4",
      "performer": "John Doe",
      "instrument": "Piano",
      "status": "active"
    }
  ],
  "spotlight": {
    "entry_id": "john-doe-piano",
    "headline_raw": "ARTIST SPOTLIGHT",
    "subhead_raw": "John Doe performing Piano",
    "body_raw": "Featuring virtuosic piano performance...",
    "cta_label_raw": "VIEW COLLECTION",
    "image_src": "/assets/spotlight/john-doe.jpg"
  }
}

Manifest Entry Schema

entry_id
string
required
Entry slug/identifier (must match entry folder name).
entry_href
string
required
Canonical entry path (e.g., /entry/john-doe-piano/). Must start with /entry/ and end with /.
title_raw
string
Display title for catalog listings.
lookup_number
string
Lookup code from entry (e.g., "A.Pl S4 01").
season
string
Season identifier (e.g., "S4"). Must match pattern S\d+.
performer
string
Primary artist/performer name.
instrument
string
Primary instrument name.
status
'active' | 'draft' | 'archived'
Entry status:
  • active: Visible in catalog
  • draft: Hidden from public catalog
  • archived: Retired but preserved in manifest

Spotlight Schema

entry_id
string
Entry to feature in spotlight. Must exist in manifest.
headline_raw
string
Spotlight headline (e.g., "ARTIST SPOTLIGHT").
subhead_raw
string
Subheading text (usually entry title).
body_raw
string
Spotlight body copy (up to 640 characters).
cta_label_raw
string
Call-to-action button label (e.g., "VIEW COLLECTION").
image_src
string
Spotlight image URL or asset path.

Catalog Entries File

The catalog.entries.json is generated from scraping the catalog HTML page:
{
  "source": "local",
  "generated_at": "2026-03-07T10:30:00.000Z",
  "anchors": {
    "performer": "#dex-performer",
    "instrument": "#dex-instrument",
    "lookup": "#dex-lookup",
    "how": "#dex-how",
    "symbols": "#list-of-identifiers"
  },
  "stats": {
    "entries_count": 142,
    "lookup_count": 138,
    "seasons": ["S5", "S4", "S3", "S2", "S1"],
    "instruments": ["Bass", "Drums", "Guitar", "Piano", "Vocals"],
    "protected_char_count": 0
  },
  "entries": [
    {
      "id": "john-doe-piano",
      "title_raw": "Piano",
      "performer_raw": "John Doe",
      "instrument_family": ["Keyboard"],
      "instrument_labels": ["Piano"],
      "lookup_raw": "A.Pl S4 01",
      "season": "S4",
      "entry_href": "/entry/john-doe-piano/",
      "image_src": "/assets/entries/john-doe.jpg",
      "image_alt_raw": "John Doe performing Piano",
      "featured": false,
      "sort_key": "S4::A.Pl S4 01",
      "title_norm": "piano",
      "performer_norm": "john doe",
      "lookup_norm": "apl s4 01",
      "instrument_norm": "piano keyboard"
    }
  ],
  "spotlight": { /* ... */ },
  "guide": { /* ... */ },
  "symbols": { /* ... */ }
}
This file is generated by scripts/lib/catalog-model.mjs from HTML scraping. It’s used for validation and as a staging source.

Catalog CLI Commands

Manifest Management

List Manifest

# Show staged entries
dex catalog manifest list

# Show all entries (staged + published)
dex catalog manifest list --all

Add Entry to Manifest

dex catalog manifest add \
  --entry john-doe-piano \
  --lookup "A.Pl S4 01" \
  --season S4 \
  --instrument Piano \
  --performer "John Doe"
The --entry flag accepts:
  • Entry slug: john-doe-piano
  • Entry href: /entry/john-doe-piano/
  • Entry ID: john-doe-piano
If the entry exists in catalog.entries.json, metadata is auto-populated from that source.

Edit Manifest Entry

dex catalog manifest edit \
  --entry john-doe-piano \
  --lookup "A.Pl S4 02" \
  --status archived
Partial updates are supported. Only specified fields are changed.

Retire Entry

dex catalog manifest retire --entry john-doe-piano
Sets status: "archived" without removing from manifest.

Remove Entry

dex catalog manifest remove --entry john-doe-piano
Removal is blocked if the entry page exists. Use retire instead, or pass --force-remove to override.

Staging Entries

The stage command adds an entry from catalog.entries.json to the editorial manifest:
dex catalog stage --entry john-doe-piano
This is equivalent to manifest add but pulls all metadata from the entries file.

Spotlight Management

dex catalog spotlight set \
  --entry john-doe-piano \
  --headline "FEATURED ARTIST" \
  --subhead "John Doe performing Piano" \
  --body "Experience virtuosic piano performance..." \
  --cta-label "WATCH NOW" \
  --image "/assets/spotlight/john-doe.jpg"
The spotlighted entry must exist in the manifest. Validation checks this constraint.

Validation

dex catalog validate
Validation checks:
  1. Schema compliance: All fields match Zod schemas
  2. Linkage: Every manifest entry has a corresponding entry page in entries/
  3. Spotlight reference: Spotlight entry_id exists in manifest
  4. Cross-manifest consistency: Home featured entries are in catalog manifest
  5. Duplicate detection: No duplicate entry_id or entry_href values

Diff and Publish

Diff Local vs Remote

# Diff against test environment
dex catalog diff --env test

# Diff against production
dex catalog diff --env prod
Output:
catalog:diff (test) local=a1b2c3d4e5f6 remote=f6e5d4c3b2a1
  manifest +3 -1 ~2
  spotlight changed=yes
  • +3: 3 entries added locally
  • -1: 1 entry removed locally
  • ~2: 2 entries modified locally

Publish to Environment

# Dry run (preview changes without publishing)
dex catalog publish --env test --dry-run

# Publish to test
dex catalog publish --env test

# Publish to production
dex catalog publish --env prod
Production publishes require confirmation. Always test in test environment first.

Pull from Environment

# Pull published catalog from test
dex catalog pull --env test
This overwrites local catalog.editorial.json with the remote version.

Catalog Model Extraction

The catalog model is built by scraping HTML from the live catalog page:
// From scripts/lib/catalog-model.mjs:423
export function buildCatalogModelFromHtml(html, sourceLabel = 'local') {
  const $ = load(html, { decodeEntities: false });
  const entriesByHref = new Map();
  const lookupMap = buildLookupMap($);
  
  // Extract entries from carousel slides
  $('li.user-items-list-carousel__slide.list-item').each((index, slide) => {
    const $slide = $(slide);
    const href = $slide.find('a.list-item-content__button').first().attr('href') || '';
    const entry = ensureEntry(entriesByHref, href, index);
    // ...
  });
  
  // Extract entries from accordion sections
  $('ul.accordion-items-container > li.accordion-item').each((_, item) => {
    // ...
  });
  
  return model;
}
This extracts:
  • Entry hrefs and titles from carousel slides
  • Instrument families from accordion sections
  • Lookup numbers from anchor text
  • Season identifiers from lookup patterns
  • Spotlight content from special sections
See scripts/lib/catalog-model.mjs for the full extraction logic including normalization and deduplication.

Season Management

Seasons group entries chronologically:
# List all seasons
dex catalog seasons list

# Add a new season
dex catalog seasons add --season S5 --title "Season 5" --year 2026

# Edit season metadata
dex catalog seasons edit --season S5 --title "Season Five"
Season data is stored separately in data/catalog.seasons.json.

Linkage Validation

The catalog enforces entry page linkage: every manifest entry must have a corresponding entry page:
// From scripts/lib/entry-catalog-linkage.mjs:25
export async function assertCatalogManifestRowLinkage(row, { rootDir } = {}) {
  const entryId = String(row?.entry_id || '').trim();
  if (!entryId) throw new Error('Catalog manifest row entry_id is required.');
  
  const href = canonicalEntryHrefFromId(entryId);
  const entryDir = path.join(rootDir, 'entries', entryId);
  const indexPath = path.join(entryDir, 'index.html');
  
  if (!await pathExists(indexPath)) {
    throw new Error(`Catalog manifest row linkage failed: entry page not found at ${indexPath}`);
  }
  
  return { entryId, href, entryDir, indexPath };
}
This prevents broken catalog links from being published.

Symbol Classification

The catalog includes a symbol legend with auto-classification:
// From scripts/lib/catalog-model.mjs:31
function classifySymbolGroup(keyRaw, descriptionRaw) {
  const key = normalizeSearch(keyRaw);
  const description = normalizeSearch(descriptionRaw);
  
  if (key.startsWith('[')) {
    if (/\b(4k|1080|720|mono|stereo|quad|wav|mp3)\b/.test(`${key} ${description}`)) {
      return 'quality';  // Format/quality indicators
    }
    return 'qualifier';  // Other bracketed symbols
  }
  
  if (/^[a-z]$/i.test(keyRaw)) {
    return 'instrument';  // Single-letter instrument codes
  }
  
  return 'qualifier';  // Default category
}
Symbols are grouped into:
  • Instrument: A, Pl, Dr, etc.
  • Collection: AV, A-E, etc.
  • Quality: [4K], [Stereo], etc.
  • Qualifier: [Loud], [Clean], etc.

Best Practices

Stage Before Publish

Always use dex catalog stage to add entries, ensuring metadata consistency with entry pages.

Validate Early

Run dex catalog validate before publishing to catch linkage and schema errors.

Test Environment First

Publish to --env test and verify in staging before promoting to production.

Retire vs Remove

Use retire to preserve history. Only remove --force-remove when absolutely necessary.

Troubleshooting

Linkage Validation Failed

If you see “entry page not found”:
  1. Verify the entry folder exists: ls entries/entry-slug
  2. Check for index.html: ls entries/entry-slug/index.html
  3. Regenerate if missing: dex update entry-slug

Spotlight Entry Not in Manifest

If spotlight validation fails:
# Add the entry to manifest first
dex catalog manifest add --entry spotlight-entry

# Then set spotlight
dex catalog spotlight set --entry spotlight-entry

Manifest Drift from Published

If local differs significantly from published:
# Pull published version
dex catalog pull --env test

# Review diff
dex catalog diff --env test

# Stage local changes incrementally
dex catalog stage --entry entry-1
dex catalog stage --entry entry-2

Build docs developers (and LLMs) love