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.
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!") \``` """ )
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.
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:
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.
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.
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.
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
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.
Enjoyed this blog post? Check out these related posts!
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
A Guide to Serving Your Frontend and Backend from a Single Application
Read More...
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
A step-by-step guide to building and serving Reflex static front-end files using Caddy in a Docker container
Read More...
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.