HomeBlog

A Practical Guide to Rendering Markdown to HTML in Reflex

8 min read
An illustration showing a Markdown file being transformed into a styled web page, with the Reflex.
By David Muraya • October 4, 2025

Reflex is a pure Python web framework that lets you build and deploy web apps without writing any JavaScript. This website is built with Reflex. This blog page is written in Markdown and rendered to HTML using Reflex’s rx.markdown component. If you're building a blog, documentation, or any site with rich text content in Reflex, you'll need an efficient way to handle Markdown. The framework provides a built-in rx.markdown component that converts Markdown text into styled HTML components.

This guide covers how to use the rx.markdown component, from basic rendering to advanced customization for a production-ready blog.

Basic Markdown Rendering

The rx.markdown component renders any GitHub Flavored Markdown string into corresponding Reflex components.

For example, you can render headings, links, and bold text directly.

import reflex as rx

def simple_markdown_example():
    return rx.vstack(
        rx.markdown("# Main Heading"),
        rx.markdown("This is a paragraph with a [link](https://davidmuraya.com)."),
        rx.markdown("Use `code` for inline snippets and **bold** for emphasis."),
        align_items="start"
    )

Reflex also supports syntax highlighting for code blocks, tables, and even LaTeX for math equations right out of the box.

# Example of a code block with syntax highlighting
rx.markdown(
    r"""
\```python
import reflex as rx

def my_app():
    return rx.text("Hello, World!")
\```
"""
)

Customizing Output with `component_map`

The real power of rx.markdown comes from the component_map prop. It allows you to replace the default HTML elements with your own custom Reflex components.

The component_map is a dictionary where each key is a Markdown element tag (like "h1", "p", or "a") and the value is a function that defines how to render it. This function receives the text content as an argument and should return a Reflex component.

Let's say you want to style all headings and paragraphs to match your site's theme.

import reflex as rx

# Define a custom map for markdown components
custom_component_map = {
    "h1": lambda text: rx.heading(text, size="7", color="blue", margin_y="1em"),
    "p": lambda text: rx.text(text, color="gray", margin_y="1em"),
    "a": lambda text, **props: rx.link(
        text, **props, color="green", _hover={"color": "red"}
    ),
}

def custom_markdown_example():
    return rx.markdown(
        """
# This Heading is Now Blue
This paragraph is now gray.

And this [link is green](/).
        """,
        component_map=custom_component_map,
    )

This approach gives you complete control over the look and feel of your rendered content, ensuring it integrates seamlessly with your application's design system.

A Real-World Example: The Blog Post View

On this blog, I use component_map to render every article. The setup allows for consistent styling and functionality across all posts.

Here is a more detailed version of the component_map used on this site, which includes a "copy" button for code blocks and custom styling for images.

# From app/views/blog/blog_content.py
component_map = {
    "h2": lambda text: rx.heading(text, size="4", color=GRAY, margin_y="1em", as_="h2"),
    "p": lambda text: rx.text(text, color=GRAY, margin_y="1em"),
    "ul": lambda *children: rx.unordered_list(*children, color=GRAY, margin_y="1em"),
    "li": lambda *children: rx.list_item(*children, color=GRAY, margin_y="0.6em"),
    "codeblock": lambda text, **props: rx.box(
        rx.code_block(
            text,
            **props,
            theme=rx.code_block.themes.dark,
            margin_y="1em",
            font_size="0.9em",
        ),
        rx.button(
            rx.icon(tag="copy", size=15),
            on_click=[rx.set_clipboard(text), rx.toast.info("Copied to clipboard")],
            position="absolute",
            top="0.8em",
            right="1em",
            color="white",
            background_color="transparent",
            _hover={"background_color": rx.color("slate", 9)},
        ),
        position="relative",
    ),
    "a": lambda text, **props: rx.link(
        text, **props, color="grass", _hover={"color": "red"}
    ),
    "img": lambda src, **props: rx.image(
        src=src,
        width="100%",
        height="auto",
        aspect_ratio="16 / 9",
        border_radius="12px",
        object_fit="cover",
        margin_y="1em",
        **props,
    ),
}

By defining this map, every article automatically gets:

  • Styled headings, paragraphs, and lists.
  • A custom code block component with a copy button.
  • Uniformly styled images that fit the site's design.

Advanced Use: Pre-processing Markdown

Sometimes, you need more than just styling. For this blog's Table of Contents, each h2 and h3 heading needs a unique id so it can be targeted by anchor links. The component_map doesn't provide a direct way to add attributes like an id to a heading.

The solution is to pre-process the Markdown string before passing it to rx.markdown.

I created a Python function that reads the Markdown content, finds all heading lines, and replaces them with raw HTML that includes the id. This function also returns the list of headings to build the TOC.

# Simplified from app/views/blog/blog_content.py
import re

def process_markdown_for_toc(markdown_content: str) -> tuple[str, list[dict]]:
    headings = []
    modified_lines = []
    for line in markdown_content.splitlines():
        # Use regex to find ## or ### headings
        match = re.match(r"^(##|###)\s+(.*)", line)
        if match:
            level = len(match.group(1))
            text = match.group(2).strip()
            # Create a URL-friendly slug
            slug = re.sub(r"[^\w-]", "", text.lower().replace(" ", "-")).strip()
            headings.append({"level": level, "text": text, "slug": slug})
            # Replace the markdown heading with a raw HTML heading
            modified_lines.append(
                f"<h{level} id='{slug}'>{text}</h{level}>"
            )
        else:
            modified_lines.append(line)
    return "\n".join(modified_lines), headings

# In the page component:
# modified_content, toc_headings = process_markdown_for_toc(original_markdown)
# toc_component = table_of_contents_component(toc_headings)
# return rx.markdown(modified_content, component_map=...)

This hybrid approach—pre-processing for structure and using component_map for styling—offers maximum flexibility.

Bonus: Adding SEO with Structured Data

While not directly related to rendering Markdown, a key part of a blog is making it discoverable by search engines. You can improve your blog's SEO by adding structured data using the JSON-LD format. This helps search engines understand the content and context of your page. Reflex also provides other SEO benefits, such as the automatic generation of sitemap.xml.

In the blog's page layout, a Python dictionary is created to define the article's properties according to the schema.org vocabulary for a BlogPosting.

# Simplified from app/views/blog/blog_content.py

nairobi_tz = timezone(timedelta(hours=3))
published_date = datetime.strptime(date, "%B %d, %Y").replace(tzinfo=nairobi_tz)
published_date_iso = published_date.isoformat()


schema = {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    "headline": blog["title"],
    "keywords": ", ".join(blog.get("tags", [])),
    "image": f"{BASE_URL}/blog/{blog['thumbnail']}",
    "author": {"@type": "Person", "name": blog["author"]},
    "publisher": {
        "@type": "Organization",
        "name": "David Muraya",
        "logo": {"@type": "ImageObject", "url": LOGO_URL},
    },
    "datePublished": published_date_iso,
}

This dictionary is then converted into a JSON string and embedded in a <script> tag using rx.html.

import json

# ... inside the page component
schema_script = rx.html(
    f'<script type="application/ld+json">{json.dumps(schema)}</script>'
)

# This script is then added to the page layout
return rx.box(
    # ... other components
    schema_script,
)

Adding this schema tells search engines that your page is a blog post, who wrote it, and when it was published. This can lead to enhanced search results, like rich snippets, which can improve click-through rates.

As you can see from the Google Rich Results Test, the structured data is valid and makes this page eligible for rich results.

A screenshot of the Google Rich Results Test showing that the 'BlogPosting' structured data is valid for this article.

Frequently Asked Questions

1. Can I use my own custom components in component_map? Yes. The value for any key in the map can be a function that returns any valid Reflex component. This is useful for creating complex components, like a code block with a copy button.

2. How do I style inline code differently from a codeblock? The component_map has separate keys for them. Use the "code" key for inline snippets and "codeblock" for fenced code blocks.

3. What if rx.markdown doesn't support a feature I need? If component_map isn't enough, you can pre-process the Markdown string into custom HTML and pass it to rx.markdown, as shown in the Table of Contents example. For full control, you can also use the rx.html component to render raw HTML directly.

Final Thoughts

The rx.markdown component is a powerful tool for handling content in a Reflex application. While its basic usage is straightforward, the component_map prop provides the deep customization needed for building polished, professional sites. By combining it with simple Python pre-processing, you can handle almost any content rendering requirement.

For next steps, learn how to deploy a Reflex frontend with Caddy or how to optimize its performance on Cloud Run

Share This Article

About the Author

David Muraya is a Solutions Architect specializing in Python, FastAPI, and Cloud Infrastructure. He is passionate about building scalable, production-ready applications and sharing his knowledge with the developer community. You can connect with him on LinkedIn.

Related Blog Posts

Enjoyed this blog post? Check out these related posts!

Reflex Makes SEO Easier: Automatic robots.txt and sitemap.xml Generation

Reflex Makes SEO Easier: Automatic robots.txt and sitemap.xml Generation

Discover how adding your deploy URL in Reflex automatically generates robots.txt and sitemap.xml for easier SEO.

Read More...

Serving a React Frontend Application with FastAPI

Serving a React Frontend Application with FastAPI

A Guide to Serving Your Frontend and Backend from a Single Application

Read More...

Optimizing Reflex Performance on Google Cloud Run

Optimizing Reflex Performance on Google Cloud Run

A Comparison of Gunicorn, Uvicorn, and Granian for Running Reflex Apps

Read More...

Deploying Reflex Front-End with Caddy in Docker

Deploying Reflex Front-End with Caddy in Docker

A step-by-step guide to building and serving Reflex static front-end files using Caddy in a Docker container

Read More...

On this page

Contact Me

Have a project in mind? Send me an email at hello@davidmuraya.com and let's bring your ideas to life. I am always available for exciting discussions.

© 2025 David Muraya. All rights reserved.