How the Best Practices Content System Is Organized and Rendered Using Tiptap in Developer Roadmap

The Developer Roadmap repository organizes best practices as Markdown files with front-matter metadata and renders them statically by parsing the content through Tiptap to generate a JSONContent AST, which is then transformed into interactive React JSX via the GuideRenderer class.

The kamranahmedse/developer-roadmap project manages its educational best practices collection through a static-first architecture that combines file-based content storage with the Tiptap editor framework. This system leverages Markdown for authoring while using Tiptap as the canonical AST format to power rich, interactive rendering across the site.

Content Storage and File Structure

Each best practice lives in its own folder under src/data/best-practices/. The system uses a hierarchical file structure where a top-level .md file holds global metadata and a nested content/ directory contains individual topic files.

The file layout follows this pattern:

  • src/data/best-practices/<practice-id>/<practice-id>.md — Contains front-matter metadata including the title, SEO description, JSON URL for interactive diagrams, and dimension settings
  • src/data/best-practices/<practice-id>/content/*.md — Individual Markdown files for each specific topic within the practice

This organization allows the build system to import entire best practice collections as typed objects while maintaining clear separation between configuration metadata and actual content.

Data Loading and Type Safety

The project uses Vite’s import.meta.glob to import all Markdown files at build time. Helper functions in src/lib/best-practice.ts and src/lib/best-practice-topic.ts handle the conversion from file system paths to typed data structures.

The getAllBestPractices() function in src/lib/best-practice.ts globs all practice definitions and returns an array of BestPracticeFileType objects. Similarly, getAllBestPracticeTopicFiles() handles the individual topic content files. These loaders strip file extensions to generate IDs and prepare the content for rendering.

// src/lib/best-practice.ts
export async function getBestPracticeIds() {
  const files = await import.meta.glob<BestPracticeFileType>(
    '/src/data/best-practices/*/*.md',
    { eager: true },
  );
  // Convert file path → ID (strip .md)
  return Object.keys(files).map((p) => p.split('/').pop()!.replace('.md', ''));
}

Tiptap Rendering Architecture

The rendering layer converts raw Markdown into interactive UI components through a three-stage pipeline: parsing to AST, sanitization, and JSX generation.

From Markdown to TipTap AST

The system uses the tiptap-markdown extension (version 0.8.10) to parse raw Markdown into a TipTap JSONContent AST. This AST serves as the canonical intermediate representation, enabling consistent handling of complex nodes like tables, code blocks, and custom qaSection elements.

Astro treats each imported .md file as a component exposing a <File.Content /> export. When rendered, these components feed their Markdown into the Tiptap parser, producing a structured AST that represents the document’s semantic structure rather than just its string content.

Pre-processing and Sanitization

Because tiptap-markdown does not natively understand escaped brackets (\[ and \]), the system runs a sanitization step before parsing. The sanitizeMarkdown() function in src/lib/markdown.ts uses a regex to convert escaped link syntax into standard Markdown format that the parser can handle.

// src/lib/markdown.ts
export function sanitizeMarkdown(markdown: string) {
  // Turn \[link\](url) → [link](url) so tiptap-markdown can parse it
  return markdown.replace(/\\\[([^\\]+)\\\]\(([^\\]+)\)/g, '[$1]($2)');
}

JSX Generation Strategies

Two renderer classes walk the TipTap AST to produce output:

  • GuideRenderer (src/lib/guide-renderer.tsx) — Builds a full-featured React element tree, handling custom nodes like qaSection, tables, and code blocks with specific UI components
  • MarkdownRenderer (src/lib/markdown-renderer.tsx) — Produces plain Markdown strings for copy-to-clipboard functionality or lightweight server-side generation

Both renderers rely on an ordered marksOrder array to correctly nest marks (underline → bold → italic → textStyle → link), ensuring the final output preserves the original author’s formatting intent.

The GuideRenderer uses dynamic dispatch to handle different node types through a unified renderNode() method:

// src/lib/guide-renderer.tsx
private renderNode(node: JSONContent): JSX.Element | null {
  const type = node.type || '';
  if (type in this) {
    // Dynamic dispatch to the method named after the node type
    // e.g. this.paragraph(node), this.heading(node), etc.
    return (this as any)[type](node);
  }
  console.warn(`Node type "${type}" is not supported.`);
  return null;
}

// Example: rendering a heading node with slug generation
private heading(node: JSONContent): JSX.Element {
  const level = node.attrs?.level || 1;
  const Tag = `h${level}` as keyof JSX.IntrinsicElements;
  const text = this.getText(node);
  const slug = slugify(text);
  return <Tag id={slug}>{this.content(node)}</Tag>;
}

Static Generation with Astro

The best practices pages are generated statically at build time through Astro’s file-based routing system.

The getStaticPaths() function in src/pages/best-practices/[bestPracticeId]/index.astro calls getAllBestPractices() to collect every best-practice markdown file and generate the route parameters. The page receives a bestPractice prop containing the front-matter and a virtual component (bestPractice.Content).

During rendering, the system decides between two display modes:

  1. Interactive Mode — If bestPracticeData.jsonUrl exists and the content is not upcoming, it renders the FrameRenderer component to display Balsamiq-style diagrams
  2. Markdown Mode — Otherwise, it wraps the Tiptap-rendered content in a <MarkdownFile> component
---
// src/pages/best-practices/[bestPracticeId]/index.astro
import { getAllBestPractices } from '../../../lib/best-practice';
export const prerender = true;

export async function getStaticPaths() {
  const bestPractices = await getAllBestPractices();
  return bestPractices.map(p => ({
    params: { bestPracticeId: p.id },
    props: { bestPractice: p },
  }));
}
---

<BestPracticeHeader title={bestPracticeData.title} ... />
{!bestPracticeData.isUpcoming && bestPracticeData.jsonUrl ? (
  <FrameRenderer resourceType="best-practice" resourceId={bestPracticeId} />
) : (
  <MarkdownFile>
    <bestPractice.Content />
  </MarkdownFile>
)}

Individual topic pages follow the same pattern in src/pages/best-practices/[bestPracticeId]/[...topicId].astro, using getAllBestPracticeTopicFiles() to load specific content files and rendering them via <file.Content />.

Summary

  • File-based storage: Best practices are stored as Markdown files with front-matter in src/data/best-practices/, with separate folders for metadata and topic content
  • Build-time loading: Vite’s import.meta.glob imports all files at build time, converting them to typed BestPracticeFileType objects via helper functions in src/lib/best-practice.ts
  • Tiptap parsing: The tiptap-markdown extension converts Markdown to a JSONContent AST, with src/lib/markdown.ts handling escaped bracket sanitization
  • Dual rendering strategy: GuideRenderer produces rich React JSX for the UI, while MarkdownRenderer generates plain strings for lightweight use cases
  • Static generation: Astro pre-renders all pages at build time using getStaticPaths(), dynamically choosing between interactive diagram rendering or Markdown content based on front-matter flags

Frequently Asked Questions

Why does the system use Tiptap instead of rendering Markdown directly?

The Tiptap AST (JSONContent) provides a structured, semantic representation of the content that enables complex UI features beyond basic HTML conversion. By parsing Markdown into this AST first, the system can handle custom nodes like qaSection, apply specific styling to code blocks, and manage mark nesting order (bold, italic, links) through the marksOrder array. This architecture separates content structure from presentation, allowing the same source Markdown to render differently depending on context.

How does the system handle escaped Markdown characters?

Before Tiptap parses the content, the sanitizeMarkdown() function in src/lib/markdown.ts runs a regex replacement to convert escaped brackets (\[ and \]) into standard Markdown link syntax. This workaround is necessary because tiptap-markdown@0.8.10 does not recognize escaped brackets as valid link delimiters, which would otherwise cause parsing failures for content containing literal bracket characters intended as links.

What is the difference between GuideRenderer and MarkdownRenderer?

GuideRenderer (src/lib/guide-renderer.tsx) is the full-featured renderer that walks the TipTap AST and produces React JSX elements, handling custom components like tables, headings with slugified IDs, and specialized qaSection layouts. MarkdownRenderer (src/lib/markdown-renderer.tsx) provides a lightweight alternative that converts the AST back to Markdown strings, useful for copy-to-clipboard features or generating plain text summaries where React component overhead is unnecessary.

How are best practice pages generated at build time?

During the Astro build process, getStaticPaths() in src/pages/best-practices/[bestPracticeId]/index.astro executes getAllBestPractices() to discover all available content. It returns a mapping of route parameters and props for each best practice. Astro then renders each page, executing the virtual <bestPractice.Content /> component which internally runs the Markdown through the Tiptap parser and GuideRenderer to produce the final static HTML.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →