Extensible Markdown

Markdown has quietly become one of the most important interface formats of the AI era.

It was already the natural choice for documentation, knowledge bases, and web publishing. Then large language models arrived and gave Markdown a second life: today it is the de facto format for prompts, responses, generated reports, and context passed between people, agents, and tools.

The reasons are obvious. Markdown is compact, readable before rendering, easy to generate, and simple to transform into HTML, PDF, or structured text. It is one of the few formats that works equally well as source code, published content, and a conversational medium.

Its limitations are equally obvious. Markdown has no native vocabulary for interactive maps, diagrams, image comparisons, live data, media galleries, or any other application-specific controls. Even responsive images require capabilities such as srcset, sizes, and loading hints that the basic image syntax cannot express.

The interesting question, then, is not how to replace Markdown. It is how to extend it without destroying the qualities that made it successful.

Start with the platform

Before inventing another syntax, it is worth remembering that most Markdown parsers can preserve raw HTML. The exact behavior depends on the parser and its security configuration, but in a controlled publishing pipeline HTML is already a powerful extension layer.

If Markdown’s image syntax is too limited, for example, we do not need a framework component. The web platform already has <picture>:

<picture>
  <source srcset="diagram.avif" type="image/avif">
  <source srcset="diagram.webp" type="image/webp">
  <img
    src="diagram.png"
    srcset="diagram-small.png 480w, diagram.png 960w"
    sizes="(max-width: 600px) 100vw, 960px"
    width="960"
    height="540"
    loading="lazy"
    alt="The document flows through a parser, renderer, and component layer">
</picture>

The same principle applies elsewhere. Native <video> supports multiple sources and captions. <details> and <summary> provide an accessible disclosure widget without JavaScript. <figure> and <figcaption> express the relationship between an illustration and its explanation.

This gives us the first rule of extensible Markdown:

Use standard Markdown when it is sufficient, and standard HTML when the platform has already solved the problem.

The best custom component is the one we do not need to create.

What should an extension preserve?

Adding JSX or arbitrary JavaScript to a document is easy. Designing an extension model that keeps the document useful outside one application is harder.

In my view, a good Markdown extension should preserve six properties:

  • Raw readability. A person or language model should understand the document without executing it.
  • Useful fallback content. If the interactive layer is unavailable, the reader should still receive information or a path to it.
  • Declarative intent. The document should describe what a component represents, not the sequence of operations required to construct it.
  • Runtime independence. The content contract should not assume that it will be processed only by React, only in a browser, or only on a server.
  • Controlled execution. Opening a document should not implicitly grant arbitrary code execution to its author.
  • Stable semantics. Tags and attributes should behave like a public API that renderers, agents, and other tools can depend on.

These properties matter for traditional publishing, but they become even more important when documents are read and written by AI agents. Agents benefit from explicit structure; they do not benefit from reconstructing meaning out of framework internals.

MDX

MDX is the best-known attempt to extend Markdown. It combines Markdown with JSX, allowing authors to import components, evaluate JavaScript expressions, and compose a document as part of an application.

Its real advantage is direct access to a React component library. In a React product whose content is written exclusively by developers, that can be a convenient shortcut: the application already has the runtime, compiler, components, and conventions that MDX expects.

But convenience inside an existing React codebase should not be confused with a general solution for extensible Markdown. Outside that ecosystem, I see no compelling reason to introduce JSX compilation and its associated runtime model. Everything MDX adds can be expressed through Custom Elements and JSDA while keeping the resulting document based on web standards.

This is not merely a question of replacing one syntax with another. It is about putting responsibilities in the right layer. Content should remain declarative. Application logic should live in modules. Interactive behavior should live in components. The renderer should decide which implementations are trusted and available.

MDX collapses those layers into an application-specific source format. Custom tags plus JSDA separate them cleanly without sacrificing capability.

Even inside React, MDX is therefore a pragmatic integration choice rather than an architectural advantage. Outside React, the trade-off makes little sense: we accept framework coupling to obtain features already available through HTML, Custom Elements, ESM, and ordinary JavaScript.

Custom Elements as semantic extension points

HTML gives us another option through the Custom Elements standard. A custom tag can act as a semantic boundary in the document and be upgraded by the browser when its implementation becomes available.

Consider an image comparison:

<image-compare label="Restoration result">
  <figure data-view="before">
    <img
      src="sculpture-before.webp"
      width="1200"
      height="800"
      loading="lazy"
      alt="Damaged sculpture before digital restoration">
    <figcaption>Before restoration</figcaption>
  </figure>

  <figure data-view="after">
    <img
      src="sculpture-after.webp"
      width="1200"
      height="800"
      loading="lazy"
      alt="Reconstructed sculpture after digital restoration">
    <figcaption>After restoration</figcaption>
  </figure>
</image-compare>

Without JavaScript, the reader still sees two properly described images. With JavaScript, <image-compare> can turn them into a draggable comparison. A server renderer can recognize the same tag and produce a static preview. An AI agent can infer the purpose of the block from its tag, label, images, and captions without knowing anything about its implementation.

The document contains the meaning; the component adds the behavior.

The same pattern works for a map:

<interactive-map
  latitude="51.5072"
  longitude="-0.1276"
  zoom="12"
  label="Central London">
  <p>
    <a href="https://www.openstreetmap.org/#map=12/51.5072/-0.1276">
      Open Central London on a map
    </a>
  </p>
</interactive-map>

The attributes form a small, machine-readable configuration contract. The link is the fallback. In a browser, the element can initialize a full map through its native connectedCallback() lifecycle. On the server, another implementation can render a static map image or preserve the link.

Configuration does not have to be squeezed into attributes. A useful convention is:

  • use attributes for small scalar values such as coordinates, labels, modes, and identifiers;
  • use semantic child HTML when the data has a natural document representation;
  • use an inner JSON block for large structured datasets when the parser and sanitizer explicitly permit it;
  • keep important human-readable information outside the configuration so that it survives without the component.

This is more verbose than hiding everything behind a JSX property, but the verbosity is productive: it makes the contract visible to people, parsers, and agents.

A custom tag is a public API

Custom Elements provide the mechanism, not the architecture. A document full of undocumented tags can be just as proprietary as a document full of framework components.

To keep the format portable, extension tags should be treated as public APIs:

  • Give a tag one clear semantic responsibility.
  • Keep its name and attribute meanings stable.
  • Validate attribute values instead of trusting them.
  • Preserve meaningful child content rather than replacing it unconditionally.
  • Make network access and other side effects predictable.
  • Provide accessible names and keyboard behavior for interactive controls.
  • Sanitize untrusted HTML, SVG, URLs, and structured data before inserting or executing anything.
  • Define what happens when the implementation is missing or fails to load.

This is an important difference from allowing arbitrary JavaScript inside the document. The renderer decides which components are trusted and registers their implementations. The document can request a known capability, but it does not automatically gain permission to execute arbitrary code.

That boundary makes the model suitable for content coming from an AI agent as well. An agent can safely produce a known <interactive-map> contract; the application remains responsible for validating it and deciding how it is rendered.

JSDA: generate context without inventing a document language

Custom Elements solve the presentation and interaction layer. A different problem appears when the document itself must be assembled from files, APIs, databases, or computed data.

This is where JavaScript Distributed Assets (JSDA) fits naturally. JSDA treats a standard ESM module as an endpoint that generates a text asset. A module named build-report.md.js, for example, exports the Markdown that becomes build-report.md.

import getBundleMetrics from './getBundleMetrics.js';

const metrics = await getBundleMetrics();
const kb = (bytes) => (bytes / 1024).toFixed(1);

export default /*md*/ `
## Build report

<bundle-chart
  js-kb="${kb(metrics.js)}"
  css-kb="${kb(metrics.css)}"
  total-kb="${kb(metrics.total)}">
  <dl>
    <dt>JavaScript</dt><dd>${kb(metrics.js)} KB</dd>
    <dt>CSS</dt><dd>${kb(metrics.css)} KB</dd>
    <dt>Total</dt><dd>${kb(metrics.total)} KB</dd>
  </dl>
</bundle-chart>
`;

This example has several useful properties at once:

  • the data can come from any asynchronous JavaScript source;
  • the generated file is still ordinary Markdown with ordinary HTML;
  • the definition list remains readable without the chart;
  • the browser can enhance the block when <bundle-chart> is registered;
  • an agent can read either the numeric attributes or the fallback content;
  • no new template language is required beyond ESM and template literals.

JSDA is not required for extensible Markdown. A hand-written .md file works perfectly well. It simply adds an algorithmic generation layer when static authorship is no longer enough.

Our JSDA Kit implements this approach for static generation, server rendering, and dynamic output. This website is built with it, but the underlying idea does not depend on a particular toolkit.

One contract across server and browser

There is still a practical question: if the same custom tag can appear in server-generated and client-rendered content, do we need two unrelated component implementations?

Not necessarily. Symbiote.js is our isomorphic library for web components. With isoMode = true, a component can use the appropriate lifecycle for its environment. If its markup was rendered on the server, the browser attaches behavior to that existing structure without a separate diffing stage. If the component arrives empty on the client, it renders normally.

Isomorphism does not make browser-only and server-only APIs magically interchangeable. Runtime-specific dependencies still need explicit boundaries -- often a dynamic import inside try/catch is enough. What it does provide is a shared component contract, template model, and state model across both environments.

This matters for Markdown because the document should not care where enhancement happens. It declares <bundle-chart> or <interactive-map>; the rendering system chooses the appropriate implementation.

From readable structure to agent tools

Semantic HTML and Custom Elements help an AI agent understand what exists on a page. They do not, by themselves, define what the agent is allowed to do.

That distinction is useful:

  • the document exposes nouns and state through headings, links, attributes, and semantic tags;
  • a tool protocol exposes verbs and actions such as changing a map viewport, selecting a dataset, or exporting a report.

Symbiote.js includes support for WebMCP, which lets a component expose structured tools to an agent. Instead of asking an agent to find a visually positioned button and simulate a click, the component can provide a named action with a schema and a predictable result.

This produces a coherent chain of meaning:

Markdown section -> semantic custom element -> component state and UI -> agent-callable tools

People, browsers, server renderers, and agents encounter different layers of the same contract rather than four unrelated representations.

WebMCP is still experimental, and its APIs may change. The important architectural point does not depend on its current implementation: content structure and action interfaces should reinforce each other.

Know where the model stops

Markdown mixed with HTML is not universally portable. Some renderers disable raw HTML. Sanitizers may remove unknown tags, scripts, or attributes. Markdown parsing inside an HTML element differs between parsers. Messaging apps, email clients, and hosted publishing platforms may flatten the component or reject it entirely.

That does not invalidate the approach, but it defines its appropriate scope. Extensible Markdown works best when you control the rendering pipeline or can publish an explicit parser profile. For documents that must survive arbitrary third-party renderers, plain Markdown should remain the canonical fallback.

This is also why meaningful child content matters. A stripped <image-compare> should still leave two images. A missing <interactive-map> implementation should still leave a link. A chart should still expose its values as a table or definition list.

Progressive enhancement is not merely a browser compatibility technique here. It is a content portability strategy.

The extension ladder

The goal is not to make every Markdown document executable. It is to add the least powerful layer that solves the problem:

  1. Use Markdown for ordinary document structure.
  2. Use native HTML for capabilities already provided by the platform.
  3. Use Custom Elements for domain-specific semantics and progressive enhancement.
  4. Use ESM and JSDA when the document must be generated algorithmically.
  5. Add WebMCP-style tool contracts when agents need to act, not merely read.

Each step preserves the layers below it. The result can begin as a plain document, become a rich web experience, render on either side of the network, and expose structured capabilities to AI agents, without turning the source into a framework-specific application.

Markdown remains readable. HTML remains standard. Components remain replaceable. And complexity appears only where it earns its place.

06.07.2026
JSDA-Kit 1.6.x -- What's New?
Features for Richer Static Output
03.06.2026
Symbiote.js & WebMCP
The important part of modern web usability
28.05.2026
Symbiote.js v3.7.x
What's new?
10.05.2026
How to interview
A practical guide to technical interviews as a soft skill
03.05.2026
Symbiote VS Lit
David and Goliath: differences, pros and cons...
15.04.2026
R&D: How to?
The Art of Managing Uncertainty in Software Development
11.12.2025
JSDA is very simple
A new, simple, but powerful way to build modern web applications.
26.08.2024
AI as a Platform
New risk for our jobs or new opportunities?
10.05.2024
The path of Full Stack
How to be efficient in multiple development areas?
RND-PRO.com © 2026 | Built with JSDA-Kit