The Markdown Content Engine
The DevIndex application isn't just a data grid; it's a fully-featured documentation portal. The "Learn" tab you're reading right now is powered by a sophisticated Markdown Content Engine that dynamically fetches, parses, and renders documentation files—all while executing entirely off the Main Thread.
This guide explores the architecture behind Neo.mjs's content rendering system: how tree.json provides the navigation structure, how Neo.component.Markdown transforms raw Markdown into interactive components, and how the routing system ties everything together for a seamless single-page application experience.
The Three Pillars of the Content Engine
The Markdown Content Engine is built on three core concepts:
- The Navigation Tree (
tree.json): A hierarchical JSON manifest that defines the structure and relationships of all documentation pages. - The Markdown Component (
Neo.component.Markdown): A specialized renderer that transforms Markdown into interactive HTML, complete with syntax-highlighted code blocks, live previews, and embedded diagrams. - The Routing System: A hash-based router that maps URLs to specific documentation records, enabling deep linking and browser history integration.
The Navigation Manifest: tree.json
Every documentation section in Neo.mjs is defined by a tree.json file. This JSON manifest describes the hierarchical structure of the content, including parent-child relationships, display names, and unique identifiers.
Structure of tree.json
{"data": [
{"name": "Introduction & Overview", "parentId": null, "id": "Introduction"},
{"name": "Methodology", "parentId": null, "id": "Methodology"},
{"name": "The Data Factory", "parentId": null, "isLeaf": false, "id": "DataFactory"},
{"name": "Spider Engine", "parentId": "DataFactory", "id": "data-factory/Engine"},
{"name": "Updater Engine", "parentId": "DataFactory", "id": "data-factory/Updater"},
{"name": "Frontend Architecture", "parentId": null, "isLeaf": false, "id": "Frontend"},
{"name": "App Shell & MVVM", "parentId": "Frontend", "id": "frontend/Architecture"}
]}
Each record contains:
| Property | Description |
|---|---|
id |
The unique identifier, which also maps to the file path (e.g., frontend/Architecture → frontend/Architecture.md) |
name |
The display label shown in the navigation tree |
parentId |
Reference to the parent node (or null for root items) |
isLeaf |
When false, indicates a folder/category with children |
collapsed |
Optional: Whether the node starts collapsed in the TreeList |
hidden |
Optional: Hides the item from navigation while keeping it routable |
How tree.json is Loaded
The Neo.app.content.TreeList component is responsible for fetching and populating the navigation. When the contentPath config is set, it automatically triggers a store load:
// src/app/content/TreeList.mjs
doLoadStore() {
if (Neo.isFunction(this.store?.load)) {
this.store.load({url: `${this.contentPath}tree.json`})
}
}
The contentPath is defined centrally in the StateProvider, which tells the TreeList where to fetch the JSON from:
// apps/devindex/view/learn/MainContainerStateProvider.mjs
data: {
contentPath: Neo.config.basePath + 'learn/guides/devindex/',
// ...
}
By decoupling the path from the component, the rendering engine remains highly reusable. (In fact, the main Neo.mjs Portal app uses this exact same architecture to dynamically serve multiple different "decks" of documentation).
The Markdown Renderer: Neo.component.Markdown
At the heart of the Content Engine is Neo.component.Markdown—a sophisticated component that transforms raw Markdown text into fully interactive HTML. Unlike simple Markdown parsers, this component implements a multi-pass compilation strategy to handle dynamic content.
The Rendering Pipeline
When the value config of a Markdown component changes, it triggers a complex rendering pipeline:
Pass 1: Extraction
Before parsing, the Markdown content is scanned for special code blocks that need custom handling. These are extracted and replaced with placeholder <div> elements:
// src/component/Markdown.mjs
processNeoComponentsBlocks(contentString, map) {
return contentString.replace(regexNeoComponent, (match, code) => {
const key = IdGenerator.getId('learn-content-component');
map[key] = JSON.parse(code);
return `<div id="${key}"></div>`
})
}
This protects dynamic content from being mangled by the standard Markdown parser.
Pass 2: Syntax Highlighting
Standard code blocks (marked with readonly) are processed asynchronously using HighlightJS:
// Asynchronous highlighting with fallback
HighlightJs.highlight(code, lang, windowId)
.catch(err => {
console.warn(`Highlighting failed for '${lang}', falling back to plaintext.`);
return HighlightJs.highlight(code, 'plaintext', windowId);
})
Pass 3: Markdown Parsing
The modified content (with placeholders) is then parsed using the marked library:
html = marked.parse(content);
Pass 4: Component Injection
Finally, the renderer iterates over the extracted component maps and instantiates actual Neo.mjs components into the placeholder DIVs:
// Lazy-load and instantiate LivePreview components
if (Object.keys(neoDivs).length > 0) {
const LivePreviewModule = await import('../code/LivePreview.mjs');
const LivePreviewClass = LivePreviewModule.default;
Object.keys(neoDivs).forEach(key => {
instance = Neo.create({
...baseConfigs,
module : LivePreviewClass,
parentId: key,
value : neoDivs[key].code
});
me.activeComponents.push(instance);
});
}
Lazy Loading Optimization: Heavy dependencies like Mermaid diagrams and LivePreview editors are imported dynamically via await import() only when their blocks are detected. This minimizes the initial bundle size.
Special Markdown Features
The Markdown component supports several enhanced block types beyond standard Markdown:
1. Syntax-Highlighted Code Blocks
Standard fenced code blocks with a language identifier are automatically highlighted:
```javascript readonly
const greeting = 'Hello, Neo.mjs!';
console.log(greeting);
```
2. Live Preview Blocks
Code blocks marked with live-preview render an interactive code editor with real-time output:
```javascript live-preview
Neo.create({
module: Neo.button.Base,
text : 'Click Me!'
})
```
3. Neo Component Blocks
Embed any Neo.mjs component directly in documentation using neo-component:
```json neo-component
{
"module": "Neo.button.Base",
"text": "I'm a real button!"
}
```
4. Mermaid Diagrams
Create flowcharts, sequence diagrams, and more using Mermaid syntax:
```mermaid
flowchart LR
A[Start] --> B[End]
```
5. Ticket References
When replaceTicketIds is enabled, references like #1234 are automatically converted to clickable links:
This feature was added in #9262.
Routing and Deep Linking
The Content Engine integrates seamlessly with Neo.mjs's hash-based routing system, enabling bookmarkable URLs and browser history navigation.
Route Configuration
The MainContainerController defines route patterns that map URL hashes to handler methods:
// apps/devindex/view/learn/MainContainerController.mjs
static config = {
routes: {
'/learn' : 'onRouteDefault',
'/learn/{*itemId}': 'onRouteLearnItem'
}
}
The {*itemId} wildcard captures the entire path after /learn/, allowing nested paths like /learn/frontend/Architecture.
Route Handling
When a route matches, the handler updates the StateProvider, which triggers a cascade of reactive updates:
async onRouteLearnItem({itemId}, value, oldValue) {
let stateProvider = this.getStateProvider(),
store = stateProvider.getStore('tree');
// Update the current page record in state
stateProvider.data.currentPageRecord = store.get(itemId);
// Expand parent nodes and scroll to the item in the tree
if (!oldValue?.hashString?.startsWith('/learn')) {
await tree.expandAndScrollToItem(itemId)
} else {
tree.expandParents(itemId)
}
}
Content Fetching
The Content component is bound to currentPageRecord via the StateProvider. When this record changes, it triggers a fetch of the corresponding Markdown file:
// src/app/content/Component.mjs
async doFetchContent(record) {
let path = me.getContentPath(record);
if (record.isLeaf && path) {
data = await fetch(path);
content = await data.text();
me.value = content; // Triggers the Markdown rendering pipeline
}
}
The getContentPath method constructs the full URL from the record's id:
// apps/devindex/view/learn/Component.mjs
getContentPath(record) {
let path = this.getStateProvider().getData('contentPath');
return path + `${record.id}.md`
}
For example, a record with id: "data-factory/Engine" and contentPath: ".../learn/guides/devindex/" resolves to .../learn/guides/devindex/data-factory/Engine.md.
The StateProvider: Binding It All Together
The MainContainerStateProvider acts as the central nervous system, managing shared state and coordinating between the TreeList, Content component, and navigation controls:
// Key state properties
data: {
contentPath : null, // Base path for content files
currentPageRecord : null, // The currently displayed page
nextPageRecord : null, // For "Next Page" navigation
previousPageRecord: null, // For "Previous Page" navigation
countPages : null // Total page count for navigation
}
When currentPageRecord changes, the StateProvider automatically calculates the previous and next pages by iterating through the tree store:
case 'currentPageRecord': {
let store = me.getStore('tree'),
index = store.indexOf(value);
// Find previous leaf node
for (i = index - 1; i >= 0; i--) {
record = store.getAt(i);
if (record.isLeaf && !me.recordIsHidden(record, store)) {
previousPageRecord = record;
break
}
}
// Find next leaf node
for (i = index + 1; i < countPages; i++) {
record = store.getAt(i);
if (record.isLeaf && !me.recordIsHidden(record, store)) {
nextPageRecord = record;
break
}
}
me.setData({previousPageRecord, nextPageRecord});
}
This enables the "Previous" and "Next" navigation buttons to work correctly, even when some pages are hidden.
Sections Panel: Auto-Generated Table of Contents
As the Markdown is parsed, the Content component extracts headline information (## and ### tags) and populates a sections store:
// src/app/content/Component.mjs
onHeadline(tag, text, index) {
const sideNavTitle = text.replaceAll('`', '');
// Push headline data to the sections store
this.headlineData.push({
id : index,
name : sideNavTitle,
sourceId: this.id,
tag
});
return `<${tag} class="neo-${tag}" data-record-id="${index}">${headline}</${tag}>`
}
The SectionsContainer binds to this store and renders an interactive table of contents. Combined with an IntersectionObserver, the current section is automatically highlighted as you scroll through the content.
Summary
The Markdown Content Engine demonstrates Neo.mjs's power to build sophisticated, interactive documentation portals using the same component architecture as data-intensive applications:
tree.jsonprovides a declarative navigation manifest, loaded into a reactive TreeList componentNeo.component.Markdownimplements a multi-pass rendering pipeline, supporting syntax highlighting, live previews, embedded components, and diagrams- Hash-based routing enables deep linking and browser history integration
- StateProvider coordinates all components through declarative data binding
- All logic executes in the App Worker, keeping the Main Thread free for smooth scrolling and interactions
This architecture makes it trivial to add new documentation sections: simply create a Markdown file and add an entry to tree.json. The Content Engine handles the rest.