Skip to main content

Overview

An entry is the core content unit in Dex DSL. Each entry represents a single recording/performance with associated metadata, credits, downloads, and a generated HTML page.
Entries live in the entries/ directory, with each entry in its own folder named by slug (e.g., entries/john-doe-piano/).

Entry Folder Structure

Each entry folder contains five tracked files:
entries/john-doe-piano/
├── entry.json           # Core metadata and configuration
├── description.txt      # Human-readable description
├── description.html     # [Legacy] HTML description
├── manifest.json        # Download file mappings
└── index.html          # Generated static HTML page
All five files are tracked. Modifying any file updates the entry’s updatedAt timestamp.

entry.json Schema

The entry.json file defines the entry’s structure:
{
  "slug": "john-doe-piano",
  "title": "John Doe performing Piano",
  "canonical": {
    "instrument": "Piano",
    "artistName": "John Doe"
  },
  "lifecycle": {
    "publishedAt": "2026-01-15T08:00:00.000Z",
    "updatedAt": "2026-03-07T10:30:00.000Z"
  },
  "video": {
    "mode": "url",
    "dataUrl": "https://cdn.example.com/video.mp4",
    "dataHtml": ""
  },
  "sidebarPageConfig": {
    "lookupNumber": "A.Pl S4 01",
    "buckets": ["A", "B", "C"],
    "specialEventImage": "/assets/series/dex.png",
    "attributionSentence": "Performed by John Doe",
    "credits": { /* ... */ },
    "fileSpecs": { /* ... */ },
    "metadata": { /* ... */ },
    "downloads": {
      "recordingIndexPdfRef": "lookup:A.Pl_S4_01",
      "recordingIndexBundleRef": "bundle:john-doe-piano-stems",
      "recordingIndexSourceUrl": "https://docs.google.com/spreadsheets/...",
      "fileTree": { /* ... */ }
    }
  },
  "descriptionText": "Piano performance featuring...",
  "series": "dex",
  "selectedBuckets": ["A", "B", "C"]
}

Core Fields

slug
string
required
URL-safe identifier (lowercase, alphanumeric, hyphens). Must match folder name.
title
string
required
Display title shown in UI and metadata.
canonical
object
Derived canonical metadata for SEO and search:
  • instrument: Primary instrument name
  • artistName: Primary artist/performer name
lifecycle
object
Timestamps managed automatically:
  • publishedAt: First write timestamp (immutable after creation)
  • updatedAt: Last modification timestamp (auto-updated on every write)
video
object
required
Video configuration:
  • mode: Either "url" (direct video URL) or "embed" (iframe HTML)
  • dataUrl: Video CDN URL (required if mode is url)
  • dataHtml: Embed iframe HTML (required if mode is embed)
The sidebarPageConfig contains download panel and credits:
lookupNumber
string
required
Catalog lookup code (e.g., "A.Pl S4 01"). Used for asset resolution.
buckets
array
required
Download bucket identifiers. Valid values: ["A", "B", "C", "D", "E", "X"].
credits
object
required
Credits structure:
{
  "artist": ["John Doe"],
  "artistAlt": null,
  "instruments": ["Piano", "Synthesizer"],
  "instrumentLinksEnabled": false,
  "linksByPerson": {
    "John Doe": [
      { "label": "Website", "href": "https://johndoe.com" }
    ]
  },
  "video": {
    "director": ["Jane Smith"],
    "cinematography": ["Alex Chen"],
    "editing": ["Sam Lee"]
  },
  "audio": {
    "recording": ["Studio A"],
    "mix": ["John Doe"],
    "master": ["Mastering Lab"]
  },
  "year": 2026,
  "season": "S4",
  "location": "Brooklyn, NY"
}
fileSpecs
object
required
Audio file specifications:
  • bitDepth: Bit depth (e.g., 24)
  • sampleRate: Sample rate in Hz (e.g., 48000)
  • channels: "mono", "stereo", or "multichannel"
  • staticSizes: Object mapping buckets to file sizes (e.g., {"A": "250 MB"})
downloads
object
Recording index references:
  • recordingIndexPdfRef: Asset token for PDF (e.g., lookup:A.Pl_S4_01 or asset:abc123)
  • recordingIndexBundleRef: Bundle token (e.g., bundle:stems-package)
  • recordingIndexSourceUrl: Google Sheets URL for recording index data
  • fileTree: Hierarchical download tree structure (see Download Tree section)

manifest.json Schema

The manifest.json maps download formats to CDN URLs:
{
  "audio": {
    "A": {
      "mp3": "https://cdn.example.com/audio/A.mp3",
      "wav": "https://cdn.example.com/audio/A.wav"
    },
    "B": {
      "mp3": "https://cdn.example.com/audio/B.mp3",
      "wav": "https://cdn.example.com/audio/B.wav"
    },
    "C": { /* ... */ }
  },
  "video": {
    "A": {
      "1080p": "https://cdn.example.com/video/A-1080p.mp4",
      "4K": "https://cdn.example.com/video/A-4K.mp4"
    },
    "B": { /* ... */ },
    "C": { /* ... */ }
  }
}
Format keys (like mp3, wav, 1080p, 4K) are extracted from the HTML template and validated during entry creation.

Entry Lifecycle

Creation (Init)

When creating an entry with dex init: The publishedAt timestamp is set on first write and never changes:
// From scripts/lib/entry-lifecycle.mjs:14
export function resolveLifecycleForInit(now = Date.now()) {
  const isoNow = toIsoDateTime(now) || new Date().toISOString();
  return { publishedAt: isoNow, updatedAt: isoNow };
}

Updates (Update)

When updating an entry with dex update:
  1. Read existing entry.json
  2. Preserve publishedAt from existing lifecycle
  3. Fall back to oldest file mtime if publishedAt is missing
  4. Set updatedAt to current timestamp
  5. Regenerate index.html with updated data
// From scripts/lib/entry-lifecycle.mjs:38
export async function resolveLifecycleForWrite({ existingLifecycle, entryFolder, now = Date.now() } = {}) {
  const isoNow = toIsoDateTime(now) || new Date().toISOString();
  const publishedAt = toIsoDateTime(existingLifecycle?.publishedAt)
    || await resolvePublishedFromEntryFiles(entryFolder)
    || isoNow;
  return { publishedAt, updatedAt: isoNow };
}

Health Checks (Doctor)

The dex doctor command checks for:
  • Drift: Differences between entry.json and generated HTML
  • Missing files: Any of the 5 tracked files absent
  • Invalid JSON: Malformed entry.json or manifest.json
  • Schema violations: Fields not matching Zod schemas
  • Broken asset references: Invalid lookup:, asset:, or bundle: tokens
Doctor can auto-repair drift by regenerating index.html, but this may overwrite manual edits.

Canonical Metadata

Canonical fields are derived from credits and sidebar config:
// From scripts/lib/entry-html.mjs (deriveCanonicalEntry)
canonical: {
  instrument: firstInstrument(credits) || existingCanonical?.instrument || '',
  artistName: firstArtist(credits) || existingCanonical?.artistName || ''
}
This ensures search and SEO metadata stay synchronized with credits.

Download Tree Structure

The fileTree in downloads supports hierarchical file organization:
{
  "lookup": "A.Pl_S4_01",
  "buckets": [
    {
      "bucket": "A",
      "types": [
        {
          "mediaType": "audio",
          "variants": [
            {
              "variantKey": "stems",
              "label": "Individual Stems",
              "files": [
                {
                  "fileId": "stem-piano-left",
                  "label": "Piano Left",
                  "filename": "piano-left.wav",
                  "extension": "wav"
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}
This structure is validated and used to generate the download UI.

Slugification Rules

Slugs are normalized using:
// From scripts/lib/entry-schema.mjs:206
export function slugify(input) {
  return String(input)
    .toLowerCase()
    .replace(/[\s_]+/g, '-')      // spaces/underscores → hyphens
    .replace(/[^a-z0-9-]/g, '')   // remove non-alphanumeric
    .replace(/-+/g, '-')          // collapse multiple hyphens
    .replace(/^-|-$/g, '');       // trim leading/trailing hyphens
}
Examples:
  • "John Doe Piano""john-doe-piano"
  • "A.Pl S4 01!!""apl-s4-01"
  • "test___entry""test-entry"

Validation and Schemas

Entries are validated using Zod schemas:
// From scripts/lib/entry-schema.mjs:152
export const entrySchema = z.object({
  slug: z.string().min(1),
  title: z.string().min(1),
  canonical: z.object({
    instrument: z.string().default(''),
    artistName: z.string().default(''),
  }).optional(),
  lifecycle: z.object({
    publishedAt: z.string().min(1),
    updatedAt: z.string().min(1),
  }).optional(),
  video: z.object({
    mode: z.enum(['url', 'embed']),
    dataUrl: z.string().default(''),
    dataHtml: z.string().default('')
  }),
  sidebarPageConfig: sidebarConfigSchema,
  // ...
});
See scripts/lib/entry-schema.mjs for the complete schema definitions including credits, download tree, and manifest validation.

Best Practices

Stable Slugs

Never change an entry’s slug after publishing. URLs depend on slug permanence.

Validate Before Commit

Run dex doctor before committing entry changes to catch drift and schema issues.

Use Asset Tokens

Prefer lookup: and bundle: tokens over hardcoded URLs for maintainability.

Track File Size

Keep staticSizes updated so users know download sizes before clicking.

Build docs developers (and LLMs) love