DEV Community

Cover image for Using Markdown as a Reviewed Runtime Dataset in TypeScript
Tom Ricky
Tom Ricky

Posted on

Using Markdown as a Reviewed Runtime Dataset in TypeScript

Your application needs a content pool. Not configuration, not copy—structured data that drives runtime behavior. A quiz needs questions. A generator needs prompts. A flashcard deck needs cards. Each entry has typed fields, belongs to a category, and must pass editorial review before users see it.

The obvious options are a database, a headless CMS, or a JSON file. But for a bounded dataset that ships with the code, is reviewed by humans, and changes at editorial pace rather than user pace, there is another option: treat a constrained Markdown file as both the human-readable review artifact and the runtime data source.

This is not a pitch for Markdown as a universal data layer. It works under specific conditions: the dataset is small enough to bundle, reviews happen through version-controlled changes, and you control the schema. Here is how the pipeline works, what it actually enforces, and where it does not.

The problem: reviewable data that also has types

JSON is straightforward for TypeScript to consume, but the type information is compile-time only unless you add runtime validation. Asking a reviewer to scan hundreds of entries in a JSON file for a misclassified difficulty rating or a missing license note is still a poor review experience. Fields blur together. Metadata disappears into nesting.

A CMS can provide forms and workflows, but may also introduce authentication, hosting, synchronization, and a separate content dependency. For a client-only application with no server functions and infrequent editorial updates, that machinery may not be justified.

What you want is a format where:

  1. A reviewer can read a batch of entries, see their metadata, and inspect the change in a version-control diff.
  2. The application can parse that same file into typed objects when the data module is evaluated.
  3. Entries outside the approved status are excluded when the runtime pool is created.
  4. Explicit validation commands can catch format violations, duplicates, and missing fields before release.

A constrained Markdown contract

The approach is not "parse arbitrary Markdown." It is closer to: define a rigid document structure using Markdown syntax, then write a parser that validates the recognized data-bearing structure against that contract.

a clear left-to-right visual pipeline from a Markdown table to typed runtime objects

A simplified example of what one section looks like (not production code):

## Batch: imaginary-objects-easy-v1

- Status: approved
- Difficulty: easy
- Primary category: objects
- Source: Original editorial draft
- License: Internal project content
- Generated by: Editorial team
- Imported on: 2026-07-31
- Reviewed by: Project owner
- Reviewed on: 2026-07-31
- Review version: v1

| ID | Prompt | Drawability | Guessability | Aliases | Extra categories | Contexts | Notes |
|---|---|---:|---:|---|---|---|---|
| paper-moon | paper moon | 4 | 4 | — | concepts | — | Simplified example |
| clockwork-fish | clockwork fish | 4 | 3 | — | animals | — | Simplified example |
Enter fullscreen mode Exit fullscreen mode

Each ## Batch: heading starts an independently reviewable section. The parser expects a defined set of required metadata fields and a fixed table column order. It treats contract violations such as wrong column counts, missing required metadata, unknown enum values, and duplicate IDs as hard errors. It does not attempt to validate unrelated Markdown outside the recognized batch structure.

This is not a general-purpose Markdown parser. It splits lines, matches heading prefixes, extracts metadata key-value pairs, and walks table rows cell by cell. It knows nothing about bold text, links, or nested lists. The format contract is narrow enough that the parser can be strict.

Three content lanes showing only approved data entering the browser runtime pool

The validation boundary

After parsing, every entry is a fully typed object. But the typing alone does not enforce content rules. The parser applies layered checks:

Structural rules the parser rejects immediately:

  • A batch heading that is not a valid kebab-case slug
  • A duplicate batch ID or prompt ID within the file
  • A table row with the wrong number of columns
  • A difficulty or category value not in the defined enum
  • A numeric rating outside the 1–5 range
  • A prompt longer than four words
  • A display term that collides with another prompt's term or alias after normalization

Status-gated rules:

  • A batch marked approved must have Reviewed by and Reviewed on fields. The parser throws if they are absent.
  • Only approved batches pass through the status filter into the runtime pool. The contract also supports draft and retired; if those statuses are present, their batches are parsed and validated, but their prompts do not reach the application. The current checked library snapshot contains only approved batches.

What the parser does not verify:

  • Whether the named reviewer is a real person
  • Whether the review date is truthful
  • Whether the source URL actually responds
  • Whether the license claim is legally accurate
  • Whether the difficulty assignment is editorially correct

Those are human judgment calls. The parser enforces that required metadata exists and, where implemented, satisfies basic shape checks. Its semantic accuracy depends on the review process, not the code.

Tests as the second enforcement layer

The parser throws when the data module is evaluated with structurally invalid Markdown. An explicit validation command is what turns that behavior into a pre-release gate in this project: repository policy requires pnpm words:check before a commit. That is not a claim that hosted CI runs the check or that every vite build necessarily evaluates the parser.

A test suite adds checks that the parser alone cannot express:

  • The exact number of approved prompts matches an expected count. A mismatch makes an inventory change explicit instead of proving by itself that the change was unauthorized.
  • Every difficulty-and-category combination has a minimum number of entries. This prevents shipping a filter combination that would return zero results at runtime.
  • Prompt terms and aliases are globally unique after Unicode normalization. The parser enforces uniqueness while traversing the full file, and the test independently asserts the property across the resulting pool.
  • Unit tests assert selected review versions, reviewer fields, source text, and license prefixes for imported batches. A separate Quick Draw check validates the pinned repository, source commit, recorded SHA-256, license identifier, row counts, and review decisions in the review artifact.

An optional networked command, words:quickdraw:verify-source, goes further: it re-fetches the pinned upstream files, verifies the categories file's SHA-256 hash, recomputes overlap with the existing library, and checks that the reviewed candidates still match the pinned snapshot. That network verification is separate from the default local check.

The default words:check command runs the local Quick Draw review validation and the prompt-library unit tests. It does not invoke the optional network verification. These are not integration or E2E tests; they operate on the review artifact and parsed Markdown data.

The approval filter in one line

After all validation passes, the runtime export is brief:

export const prompts = batches
  .filter((batch) => batch.status === 'approved')
  .flatMap((batch) => batch.prompts);
Enter fullscreen mode Exit fullscreen mode

If a future library snapshot includes draft or retired batches, they can remain reviewable without entering the pool the application consumes. The current checked library snapshot contains only approved batches. The runtime boundary is still a one-line status check because the parser has already required the approved-batch metadata and constructed each prompt with the expected fields.

Public claim consistency

One subtle problem: the application displays a prompt count to users ("900+ reviewed prompts"). That number should reflect the approved pool, not a separate marketing estimate. A utility function converts the loaded count to a floored magnitude (969 becomes "900+", 330 becomes "300+"). Dedicated tests verify the formatter, and the current homepage derives this display from prompts.length when it renders. For that surface, the floored magnitude does not exceed the loaded approved count.

Where this works and where it does not

This pipeline works when:

  • The dataset is bounded (hundreds to low thousands of entries)
  • Content changes at editorial pace, not user pace
  • Reviews happen through version-controlled changes
  • The data ships with the application bundle
  • You control the schema and can enforce it with a custom parser

It starts to break when:

  • The dataset exceeds what you want in a client bundle
  • Non-technical editors need a form-based interface
  • Content updates must deploy independently of code
  • Multiple writers need concurrent editing with conflict resolution
  • You need row-level permissions or approval workflows more complex than a batch status field

For those cases, a database or CMS is the right tool. The Markdown approach is not a general CMS replacement—it is a specific solution for datasets that live at the intersection of "needs human review" and "ships as application code."

Human editorial review working with automated validation on a structured dataset

Version history is a workflow layer

When content changes are committed, Git history can preserve who changed the file and when. Separate review documents can record reviewer decisions, exclusion reasons, and source attribution. Together they provide useful evidence, but only for changes the workflow actually records.

Project policy requires approved-content changes to remain auditable. Batch metadata can carry a Revisions entry recording who changed what, when, and why. The parser checks that revision entries are well-formed when present, but it does not require one on every edit. Git history and review discipline remain workflow controls rather than parser guarantees.

Live case

I'm building Pictionary Word Generator, a free browser-based prompt generator. The content pipeline described here is how its 900+ reviewed prompts—across three difficulty levels and seven categories—are maintained, reviewed, and delivered to the client without a database or CMS. The production repository is private due to licensing constraints; the examples above are simplified and independently written.

Two companion guides cover different parts of the system: one on building a filtered no-repeat random picker, and another on designing the round state machine that coordinates filters, history, and timer. This article covers what happens before the picker runs: how content enters the system and earns the right to be selected.


Disclosure: I built the Pictionary Word Generator used as the live case. This article was drafted with AI assistance and independently fact-checked against the current source code, tests, and live pages.

Top comments (0)