Enhancing Our Writing Workflow: Adding Markdown Support to Sanity CMS

How we added a Markdown editor to our Sanity setup and other improvements

May 15, 2025

In 2023, we transitioned from our GitHub-based blog posting system to Sanity, a powerful content operating system. This move significantly accelerated our content creation process and improved our media asset management. However, our engineering team quickly discovered they missed the familiarity of writing in Markdown, as Sanity Studio ships with a Rich Text editor by default.

Image

Image

During our recent Milan hackathon/offsite, I tackled this challenge by implementing Markdown support in our Sanity dashboard. This post walks through how we enhanced our content creation workflow to accommodate both rich text and Markdown enthusiasts.

Why we chose Sanity?

Sanity describes itself as a "Content Operating System", which is essentially a fully customizable, code-first backend for content-driven websites and applications. We made the switch primarily for:

  • Faster writing process - streamlined content creation workflows
  • Better media asset management - no more checking image files into our GitHub repository.
  • Flexible content modeling - adapting to our specific publishing needs.

Despite these advantages, our engineering team wasn't entirely comfortable with Sanity's default Rich Text editor. It turns out engineers really do love their Markdown!

Implementing Markdown Support

Modifying the Post Schema

The first step was updating our Post schema to accommodate Markdown content alongside the existing Rich Text option:

// schemas/post.ts
import {defineField, defineType} from 'sanity';

export default defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    // ... other fields ...
    defineField({
      name: 'markdownBody',
      title: 'Markdown Body',
      description: 'Use this field for Markdown content',
      type: 'markdown',
      options: {
        imageUrl: (imageAsset: any) => `${imageAsset.url}?w=800`,
      },
      validation: rule =>
        rule.custom((currentValue: any, context: any) => {
          const richTextBody = context.document?.body;
          if (!currentValue && !richTextBody) {
            return 'Either Rich Text Body or Markdown Body must be provided';
          }
          return true;
        }),
    }),
    // ... other fields ...
  ],
});

This modification above does the following:

  • Adds a dedicated markdownBody field.
  • Implements validation ensuring at least one content format is used.
  • Gives writers the freedom to use their preferred writing method.

Creating a Custom Markdown Editor

Next, we built a custom Markdown input component that delivers the familiar editing experience our team was looking for:

// components/CustomMarkdownInput/index.tsx
import React, {useMemo} from 'react';
import {MarkdownInput} from 'sanity-plugin-markdown';
import DOMPurify from 'dompurify';
import {marked} from 'marked';
import hljs from 'highlight.js';
import 'highlight.js/styles/github-dark.css';
import 'easymde/dist/easymde.min.css';

export function CustomMarkdownInput(props) {
  const reactMdeProps = useMemo(() => {
    return {
      options: {
        toolbar: [
          'heading',
          'bold',
          'italic',
          '|',
          'code',
          'strikethrough',
          'quote',
          '|',
          'unordered-list',
          'ordered-list',
          '|',
          'link',
          'image',
          '|',
          'preview',
          'side-by-side',
          '|',
          'guide',
        ],
        autofocus: false,
        spellChecker: true,
        status: ['lines', 'words', 'cursor'],
        previewRender: markdownText => {
          const html = marked.parse(markdownText, {
            async: false,
            highlight: (code, language) => {
              if (language && hljs.getLanguage(language)) {
                return hljs.highlight(code, {language}).value;
              }
              return hljs.highlightAuto(code).value;
            },
          });

          return DOMPurify.sanitize(html);
        },
        renderingConfig: {
          singleLineBreaks: false,
          codeSyntaxHighlighting: true,
        },
        uploadImage: true,
        placeholder: 'Write your content in Markdown...\n\nTip: For code blocks, use ```language\ncode here\n```',
        tabSize: 2,
        minHeight: '300px',
      },
    };
  }, []);

  return <MarkdownInput {...props} reactMdeProps={reactMdeProps} />;
}

Our custom editor integrates:

  • The sanity-plugin-markdown base editor
  • marked for Markdown parsing
  • highlight.js for code syntax highlighting
  • DOMPurify for HTML sanitization

Key features include:

  • A comprehensive toolbar with common formatting options
  • Real-time preview and side-by-side editing
  • Code syntax highlighting with language detection
  • Spell checking
  • Status bar showing lines, words, and cursor position
  • Image upload support

Styling the Editor

We also created a custom CSS file to ensure the editor fits our design system:

/* styles/global.css */
.sanity-studio .EasyMDEContainer {
  font-size: 16px;
}

.sanity-studio .EasyMDEContainer .CodeMirror {
  border-radius: 4px;
  border-color: var(--card-border-color, #e2e8f0);
}

.sanity-studio .EasyMDEContainer .editor-toolbar {
  border-color: var(--card-border-color, #e2e8f0);
  border-top-left-radius: 4px;
  border-top-right-radius: 4px;
}

/* Dark mode support */
.dark .sanity-studio .EasyMDEContainer .CodeMirror {
  border-color: var(--card-border-color, #2d3748);
  background-color: var(--card-bg-color, #1a202c);
  color: var(--card-fg-color, #e2e8f0);
}

.dark .sanity-studio .EasyMDEContainer .editor-toolbar {
  border-color: var(--card-border-color, #2d3748);
  background-color: var(--card-bg-color, #1a202c);
}

Configuring Sanity

With our components ready, we updated the Sanity configuration to include the Markdown plugin:

// sanity.config.ts
import {defineConfig} from 'sanity';
import {markdownSchema} from 'sanity-plugin-markdown';
import {CustomMarkdownInput} from './components/CustomMarkdownInput';

export default defineConfig({
  // ... other config
  plugins: [
    // ... other plugins
    markdownSchema({input: CustomMarkdownInput}),
  ],
});

Rendering Markdown Content

To display the Markdown content on our blog, we created a specialized renderer:

// components/SanityPostRenderer/MarkdownRenderer.tsx
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import rehypeHighlight from 'rehype-highlight';

const sanitizeConfig = {
  tagNames: [
    'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
    'p', 'a', 'ul', 'ol', 'li', 'blockquote',
    'code', 'pre', 'img', 'table', 'thead',
    'tbody', 'tr', 'th', 'td', 'hr', 'iframe',
    'span', 'strong', 'em', 'del',
  ],
  attributes: {
    a: ['href', 'title', 'target', 'rel'],
    img: ['src', 'alt', 'title'],
    iframe: ['src', 'width', 'height', 'frameborder', 'allow', 'allowfullscreen', 'title'],
    code: ['class', 'className', 'node'],
    pre: ['class', 'className'],
    // ... other attributes
  },
  protocols: {
    src: ['http', 'https', 'data'],
    href: ['http', 'https', 'mailto', 'tel'],
  },
};

const MarkdownRenderer = ({post}) => {
  return (
    <ReactMarkdown
      remarkPlugins={[remarkGfm]}
      rehypePlugins={[rehypeRaw, [rehypeSanitize, sanitizeConfig], rehypeHighlight]}
      components={{
        h1: ({children}) => <h1 className="text-3xl font-bold mt-8 mb-4">{children}</h1>,
        h2: ({children}) => <h2 className="text-2xl font-bold mt-6 mb-3">{children}</h2>,
        // ... other component customizations
        iframe: ({node, ...props}) => (
          <IframePreview
            node={{
              url: props.src,
            }}
            {...props}
          />
        ),
      }}
    >
      {post}
    </ReactMarkdown>
  );
};

This renderer includes:

  • react-markdown as the base renderer
  • GitHub Flavored Markdown support via remark-gfm
  • Raw HTML support through rehype-raw
  • HTML sanitization with rehype-sanitize
  • Code syntax highlighting via rehype-highlight

Updating Sanity Queries

We also updated our Sanity queries to include the new Markdown field:

// utils/sanity.queries.ts
export const singlePostQuery = groq`
  *[_type == "post" && slug.current == $slugWithoutDates][0] {
    title,
    "authorLinks": author[]->slug,
    "authors": author[]->name,
    mainImage,
    description,
    publishedAt,
    "tags": tags[]->title,
    slug,
    body,
    markdownBody
  }
`;

With all the changes above, we were now able to write blog posts in Markdown and have them render nicely on the Polar Signals blog.

Image

This article was in fact written with the newly added Markdown editor!

One more thing

As a bonus enhancement to our workflow, we've integrated Vercel's Comments Toolbar, enabling real-time feedback on blog post drafts. Team members who are signed into our Vercel organization can easily add comments to drafts, streamlining our review process.

This was quite easy to do. We used the @vercel/toolbar package to enable the toolbar on the blog post pages.

// next.config.js
const withVercelToolbar = require('@vercel/toolbar/plugins/next')();

module.exports = withBundleAnalyzer(
  withMDX(
     withVercelToolbar({
		  // ... other next config here
	 })
  )
)

// src/components/BlogPostPreview/index.tsx
import {VercelToolbar} from '@vercel/toolbar/next';

export default function BlogPostPreview() {
  return (
    <>
      <BlogPost meta={meta} post={post} />
      <VercelToolbar />
    </>
  );
}

You can read more about how to implement this here.

Conclusion

By adding Markdown support to our Sanity CMS, we've created a more flexible content creation environment that caters to our team's preferences. Engineers can now work in the familiar Markdown syntax they love, while others can continue using the rich text editor.

The implementation was straightforward and has significantly improved our content creation experience. I hope this guide helps you in implementing a Markdown editor in your writing workflow.

Discuss:
Sign up for the latest Polar Signals news