gatsby-transformer-remark, react-markdown, react-remark, and remarkable all handle Markdown, but they serve different architectural roles. gatsby-transformer-remark is a Gatsby-specific plugin that converts Markdown files into GraphQL nodes during the build process, enabling static site generation. react-markdown is a pure React component that parses and renders Markdown strings directly in the browser or server, prioritizing security and extensibility via the unified ecosystem. react-remark acts as a bridge, allowing developers to use remark plugins (like syntax highlighting or math support) within the react-markdown component pipeline. remarkable is a standalone, high-speed Markdown parser from an earlier era of JavaScript development that outputs HTML strings rather than React components, and is currently unmaintained.
Handling Markdown in the React ecosystem isn't a one-size-fits-all task. The right tool depends entirely on your framework, your need for security, and how much you need to customize the rendering pipeline. Let's break down gatsby-transformer-remark, react-markdown, react-remark, and remarkable to see where each fits in a professional architecture.
The most critical distinction is when and how the Markdown is processed.
gatsby-transformer-remark operates at build time. It is not a React component you render; it is a Gatsby plugin that reads .md files from your disk, parses them, and injects them into your GraphQL data layer. You query the data, and Gatsby gives you pre-processed HTML or AST nodes.
// gatsby-transformer-remark: Usage in a Gatsby Page Component
import { graphql } from 'gatsby';
export const query = graphql`
query {
markdownRemark(frontmatter: { slug: { eq: "/blog-post" } }) {
html // Pre-processed HTML string generated at build time
frontmatter {
title
}
}
}
`;
export default function BlogPost({ data }) {
// Directly injecting HTML (Gatsby handles sanitization internally for this plugin)
return <div dangerouslySetInnerHTML={{ __html: data.markdownRemark.html }} />;
}
react-markdown operates at runtime (or server-render time) as a React Component. It takes a Markdown string as a prop, parses it safely, and returns a tree of React elements. It does not output HTML strings, avoiding the need for dangerouslySetInnerHTML.
// react-markdown: Usage in any React App
import ReactMarkdown from 'react-markdown';
const content = "# Hello\nThis is **bold** text.";
export default function Article() {
// Renders as React components, not raw HTML
return <ReactMarkdown>{content}</ReactMarkdown>;
}
react-remark is a hybrid. It is a wrapper around react-markdown that allows you to inject remark plugins into the parsing pipeline before rendering to React. It gives you the customization of the remark ecosystem with the safety of react-markdown.
// react-remark: Usage with plugins
import { ReactMarkdown } from 'react-markdown';
import remarkGfm from 'remark-gfm'; // GitHub Flavored Markdown
import remarkMath from 'remark-math';
export default function TechnicalDoc({ content }) {
return (
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
children={content}
/>
);
}
remarkable is a legacy parser. It runs at runtime but outputs a raw HTML string. To use it in React, you must force React to render raw HTML, which bypasses React's security protections.
// remarkable: Legacy approach (Not Recommended)
import Remarkable from 'remarkable';
const md = new Remarkable();
const content = "# Hello\n[Link](http://evil.com)";
const htmlString = md.render(content);
export default function LegacyView() {
// DANGEROUS: Requires dangerouslySetInnerHTML
// Vulnerable to XSS if content is user-generated
return <div dangerouslySetInnerHTML={{ __html: htmlString }} />;
}
Security is the biggest differentiator between modern and legacy approaches.
react-markdown and react-remark are secure by default. They parse Markdown into an Abstract Syntax Tree (AST) and then render it as React elements. They automatically strip dangerous HTML tags (like <script>) unless you explicitly configure them to allow specific HTML. This makes them safe for rendering user-generated content.
// react-markdown: Safe by default
// Even if content contains <script>, it will be escaped or ignored
<ReactMarkdown>{userSubmittedContent}</ReactMarkdown>
// Customizing allowed components safely
<ReactMarkdown
components={{
h1: ({node, ...props}) => <h1 className="text-3xl" {...props} />
}}
>
{content}
</ReactMarkdown>
gatsby-transformer-remark handles security at the build step. Since the content is usually authored by developers or trusted editors in the CMS/filesystem, the risk is lower. However, it outputs an HTML string, so if you modify that string before rendering, you must be careful.
// gatsby-transformer-remark: Trusted content flow
// Content is sanitized during the Gatsby build process
const { html } = data.markdownRemark;
// Safe to render if content source is trusted
<div dangerouslySetInnerHTML={{ __html: html }} />
remarkable has no built-in React security. It blindly converts Markdown to HTML. If a user inputs ), remarkable might render it as an image tag with an executable script unless you manually configure a sanitizer. This extra step is error-prone and why this package is risky for modern apps.
// remarkable: Manual sanitization required (Error Prone)
const md = new Remarkable({ html: true, linkify: true });
// You MUST add a separate sanitizer library to prevent XSS
// const clean = DOMPurify.sanitize(md.render(content));
<div dangerouslySetInnerHTML={{ __html: clean }} />
How easy is it to add features like tables, math, or custom heading IDs?
react-remark shines here. Because it exposes the remarkPlugins prop, you can drop in any plugin from the massive unified ecosystem. Need GitHub tables? Math formulas? Autolinks? Just install the plugin and pass it in.
// react-remark: Easy plugin integration
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex'; // For rendering math
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
>
{content}
</ReactMarkdown>
react-markdown supports plugins too (in newer versions), but react-remark was historically the dedicated bridge. Now, react-markdown accepts remarkPlugins directly, making the distinction thinner, but react-remark documentation often highlights complex pipeline configurations.
// react-markdown (v6+): Direct plugin support
import remarkGfm from 'remark-gfm';
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{content}
</ReactMarkdown>
gatsby-transformer-remark uses a Gatsby-specific plugin system. You configure plugins in gatsby-config.js. It's powerful but locked into the Gatsby ecosystem. You cannot use these plugins outside of Gatsby.
// gatsby-config.js: Plugin configuration
module.exports = {
plugins: [
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
`gatsby-remark-prismjs`, // Syntax highlighting
`gatsby-remark-images`, // Image processing
],
},
},
],
};
remarkable has limited extensibility. It allows some rule customization, but it lacks the rich ecosystem of plugins available for remark/rehype. Adding complex features often requires writing custom parser rules, which is difficult and brittle.
// remarkable: Limited custom rules
const md = new Remarkable();
md.use(function (md) {
// Hard to write custom rules compared to remark plugins
md.inline.ruler.enable(['ins', 'mark']);
});
This is the dealbreaker for remarkable.
react-markdown and react-remark: Actively maintained. They follow modern React patterns (hooks, functional components) and keep up with the unified ecosystem updates.gatsby-transformer-remark: Actively maintained only if you are on Gatsby. If Gatsby evolves, this plugin evolves. It is stable for Gatsby v4/v5.remarkable: Unmaintained. The last significant updates were years ago. It does not support modern ES modules well out of the box without bundler tweaks, and it has known security issues that are not being patched. Do not start new projects with this.You are building a marketing site or blog with Gatsby. Content lives in src/pages/blog.
gatsby-transformer-remarkgatsby-remark-images, and generates static HTML for maximum performance.// Gatsby Page
export const query = graphql`{ markdownRemark { html } }`;
<div dangerouslySetInnerHTML={{ __html: data.markdownRemark.html }} />
You have a Next.js app where users can post comments in Markdown.
react-markdown// Next.js Component
<ReactMarkdown>{userComment}</ReactMarkdown>
You need to render complex docs with tables, math equations, and code highlighting.
react-markdown (with remark-gfm and rehype-highlight)<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex, rehypeHighlight]}
>
{docContent}
</ReactMarkdown>
You are maintaining an old app using remarkable.
react-markdown.remarkable is a security liability. The migration involves replacing md.render() with the <ReactMarkdown> component and removing dangerouslySetInnerHTML.| Feature | gatsby-transformer-remark | react-markdown | react-remark | remarkable |
|---|---|---|---|---|
| Primary Use | Gatsby Static Sites | General React Apps | React + Advanced Plugins | Legacy HTML Generation |
| Output | HTML String (via GraphQL) | React Components | React Components | HTML String |
| Security | Build-time Sanitization | Secure by Default | Secure by Default | โ Unsafe (XSS Risk) |
| Extensibility | Gatsby Plugins | Remark/Rehype Plugins | Remark/Rehype Plugins | Limited Custom Rules |
| Maintenance | Active (Gatsby Ecosystem) | Active | Active | โ Unmaintained |
| React Integration | Indirect (via Data Layer) | Native Component | Native Component | Manual (dangerouslySetInnerHTML) |
If you are using Gatsby, gatsby-transformer-remark is the correct architectural choice for your content layer. It leverages Gatsby's strengths in static generation.
For all other React projects, react-markdown is the industry standard. It is secure, fast, and flexible. If you need advanced features like math or tables, simply add the relevant remark plugins to react-markdown (or use react-remark if you prefer its specific API for complex pipelines).
Avoid remarkable entirely. It represents an older pattern of rendering HTML strings that conflicts with modern React's component model and security best practices. The cost of migrating away from it later is far higher than starting with react-markdown today.
Choose gatsby-transformer-remark if you are building a Gatsby site and need to source Markdown files (like blog posts) directly from the filesystem into your GraphQL data layer. It is the standard choice for Gatsby projects where content is compiled at build time into static HTML, offering deep integration with Gatsby's image processing and routing systems. Do not use this package if you are not using Gatsby, as it relies entirely on Gatsby's build pipeline and GraphQL schema.
Choose react-markdown if you need a secure, flexible, and maintained way to render Markdown strings as React components in any React environment (Next.js, Vite, Create React App). It is ideal when you want to avoid dangerously setting inner HTML and need to customize how specific elements (like headings or code blocks) are rendered using standard React props. This is the best default choice for most modern React applications requiring dynamic or static Markdown rendering.
Choose react-remark if your project requires advanced Markdown processing features provided by the remark ecosystem, such as GitHub Flavored Markdown tables, footnotes, or math equations, while still rendering to React components. It allows you to plug remark plugins directly into the react-markdown pipeline without writing custom parsers. Use this when react-markdown's default behavior is insufficient and you need the power of the unified processor stack.
Do NOT choose remarkable for new projects. It is an older library that outputs raw HTML strings, requiring you to use dangerouslySetInnerHTML, which introduces significant security risks (XSS) if content is not strictly sanitized. The package is no longer actively maintained, lacks support for modern React patterns, and has been superseded by safer, component-based alternatives like react-markdown.
Parses Markdown files using remark.
Install the plugin to your site:
npm install gatsby-transformer-remark
Add it to your gatsby-config:
module.exports = {
plugins: [
{
resolve: `gatsby-transformer-remark`,
options: {},
},
],
}
module.exports = {
plugins: [
{
resolve: `gatsby-transformer-remark`,
options: {
// Footnotes mode (default: true)
footnotes: true,
// GitHub Flavored Markdown mode (default: true)
gfm: true,
// Add your gatsby-remark-* plugins here
plugins: [],
// Enable JS for https://github.com/jonschlinkert/gray-matter#optionsengines (default: false)
// It's not advised to set this to "true" and this option will likely be removed in the future
jsFrontmatterEngine: false,
},
},
],
}
The following parts of options enable the remark-footnotes and remark-gfm
plugins:
options.footnotesoptions.gfmA full explanation of how to use markdown in Gatsby can be found here: Adding Markdown Pages
There are many gatsby-remark-* plugins which you can install to customize how Markdown is processed. Check out the source code for using-remark as an example.
gray-matter optionsgatsby-transformer-remark uses gray-matter to parse Markdown frontmatter, so you can specify any of the options mentioned in its README in the options key of the plugin.
Example: Excerpts
If you don't want to use pruneLength for excerpts but a custom separator, you can specify an excerpt_separator:
module.exports = {
plugins: [
{
resolve: `gatsby-transformer-remark`,
options: {
excerpt_separator: `<!-- end -->`
}
},
],
}
It recognizes files with the following extensions as Markdown:
mdmarkdownEach Markdown file is parsed into a node of type MarkdownRemark.
All frontmatter fields are converted into GraphQL fields through inference.
This plugin adds additional fields to the MarkdownRemark GraphQL type
including html, excerpt, headings, etc. Other Gatsby plugins can also add
additional fields.
A sample GraphQL query to get MarkdownRemark nodes:
{
allMarkdownRemark {
edges {
node {
html
headings {
depth
value
}
frontmatter {
# Assumes you're using title in your frontmatter.
title
}
}
}
}
}
Using the following GraphQL query you'll be able to get the table of contents
{
allMarkdownRemark {
edges {
node {
html
tableOfContents
}
}
}
}
tableOfContentsBy default, absolute is set to false, generating a relative path. If you'd like to generate an absolute path, pass absolute: true. In that case, be sure to pass the pathToSlugField parameter, often fields.slug, to create absolute URLs. Note that providing a non-existent field will cause the result to be null. To alter the default values for tableOfContents generation, include values for heading (string) and/or maxDepth (number 1 to 6) in GraphQL query. If a value for heading is given, the first heading that matches will be omitted and the ToC is generated from the next heading of the same depth onwards. Value for maxDepth sets the maximum depth of the toc (i.e. if a maxDepth of 3 is set, only h1 to h3 headings will appear in the toc).
{
allMarkdownRemark {
edges {
node {
html
tableOfContents(
absolute: true
pathToSlugField: "frontmatter.path"
heading: "only show toc from this heading onwards"
maxDepth: 2
)
frontmatter {
# Assumes you're using path in your frontmatter.
path
}
}
}
}
}
To pass default options to the plugin generating the tableOfContents, configure it in gatsby-config.js as shown below. The options shown below are the defaults used by the plugin.
module.exports = {
plugins: [
{
resolve: `gatsby-transformer-remark`,
options: {
tableOfContents: {
heading: null,
maxDepth: 6,
},
},
},
],
}
By default, excerpts have a maximum length of 140 characters. You can change the default using the pruneLength argument. For example, if you need 500 characters, you can specify:
{
allMarkdownRemark {
edges {
node {
html
excerpt(pruneLength: 500)
}
}
}
}
By default, Gatsby will return excerpts as plain text. This might be useful for populating opengraph HTML tags for SEO reasons. You can also explicitly specify a PLAIN format like so:
{
allMarkdownRemark {
edges {
node {
excerpt(format: PLAIN)
}
}
}
}
It's also possible to ask Gatsby to return excerpts formatted as HTML. You might use this if you have a blog post whose excerpt contains markdown content -- e.g. header, link, etc. -- and you want these links to render as HTML.
{
allMarkdownRemark {
edges {
node {
excerpt(format: HTML)
}
}
}
}
You can also get excerpts in Markdown format.
{
allMarkdownRemark {
edges {
node {
excerpt(format: MARKDOWN)
}
}
}
}
Any file that does not have the given excerpt_separator will fall back to the default pruning method.
By default, excerpt uses underscore.string/prune which doesn't handle non-latin characters (https://github.com/epeli/underscore.string/issues/418).
If that is the case, you can set truncate option on excerpt field, like:
{
markdownRemark {
excerpt(truncate: true)
}
}
If your Markdown file contains HTML, excerpt will not return a value.
In that case, you can set an excerpt_separator in the gatsby-config:
module.exports = {
plugins: [
{
resolve: `gatsby-transformer-remark`,
options: {
excerpt_separator: `<!-- endexcerpt -->`
},
},
],
}
Edit your Markdown files to include that HTML tag after the text you'd like to appear in the excerpt:
---
title: "my little pony"
date: "2017-09-18T23:19:51.246Z"
---
<p>Where oh where is that pony?</p>
<!-- endexcerpt -->
<p>Is he in the stable or down by the stream?</p>
Then specify MARKDOWN as the format in your GraphQL query:
{
markdownRemark {
excerpt(format: MARKDOWN)
}
}