Sanity Studio comes with a powerful Portable Text editor by default, but sometimes you or your content creators might prefer working with Markdown. This guide shows you how to add Markdown support alongside Portable Text, giving your content team flexibility in how they create content.
Installing Dependencies
First, install the necessary packages:
bash
# For Sanity Studio
npm install sanity-plugin-markdown easymde@2
# For Next.js frontend
npm install react-markdown remark-gfm rehype-raw rehype-sanitize rehype-highlight highlight.js
These packages provide:
sanity-plugin-markdown: The Sanity plugin for Markdown editing
easymde: The Markdown editor used by the plugin
react-markdown: For rendering Markdown in React
remark-gfm: Adds GitHub Flavored Markdown support
rehype-raw: Allows rendering of HTML within Markdown
rehype-sanitize: Sanitizes HTML to prevent XSS attacks
rehype-highlight: Adds syntax highlighting for code blocks
highlight.js: The syntax highlighter used by rehype-highlight
Configuring Sanity Studio
Adding the Markdown Schema
First, update your sanity.config.ts file to include the Markdown schema:
typescript
import {markdownSchema} from'sanity-plugin-markdown'exportdefaultdefineConfig({
// ... other configplugins: [
markdownSchema(),
// ... other plugins
],
})
Creating a Custom Markdown Input Component
Create a custom component to enhance the Markdown editor experience:
When using Markdown content, it's important to ensure that your meta fields and SEO information are correctly generated. This section covers how to extract meta information from Markdown content and set up Open Graph tags and JSON-LD structured data.
Extracting Meta Information from Markdown
For SEO purposes, you might want to extract the first paragraph or heading from your Markdown content to use as a description or excerpt. Here's a utility function to help with that:
typescript
// src/utils/markdown-utils.tsimport { remark } from'remark';
import strip from'strip-markdown';
import { unified } from'unified';
exportfunctionextractExcerptFromMarkdown(markdown: string, maxLength = 160): string {
if (!markdown) return'';
// Process the markdown to extract plain textconst plainText = unified()
.use(remark)
.use(strip)
.processSync(markdown)
.toString();
// Get the first paragraph (or a portion of it)const firstParagraph = plainText.split('\n\n')[0].trim();
// Truncate to maxLength if neededif (firstParagraph.length <= maxLength) {
return firstParagraph;
}
// Find a good breaking pointconst truncated = firstParagraph.substring(0, maxLength);
const lastSpace = truncated.lastIndexOf(' ');
return truncated.substring(0, lastSpace) + '...';
}
exportfunctionextractFirstHeadingFromMarkdown(markdown: string): string | null {
if (!markdown) returnnull;
// Look for the first heading (# Title)const headingMatch = markdown.match(/^#\s+(.+)$/m);
return headingMatch ? headingMatch[1].trim() : null;
}
Install the required dependencies:
bash
npm install remark strip-markdown unified
Setting Up Open Graph Tags
Update your blog post page to include Open Graph tags based on either the explicit meta fields or extracted from Markdown:
// In your BlogPostPage componentconst jsonLd = generateJsonLd(post);
return (
<><scripttype="application/ld+json"dangerouslySetInnerHTML={{__html:JSON.stringify(jsonLd) }}
/>
{/* Rest of your component */}
</>
);
Updating the Schema for Better SEO
Enhance your post schema to include dedicated SEO fields:
typescript
// src/sanity/schemaTypes/postType.ts// ... existing importsexportconst postType = defineType({
// ... existing configurationfields: [
// ... existing fieldsdefineField({
name: 'metaDescription',
title: 'Meta Description',
type: 'text',
description: 'This description appears in search engines and social media shares (max 155 characters)',
validation: (Rule) =>Rule.max(155),
}),
defineField({
name: 'openGraphImage',
title: 'Open Graph Image',
type: 'image',
description: 'Image used for social media sharing. If not provided, the main image will be used.',
options: {
hotspot: true,
},
fields: [
{
name: 'alt',
type: 'string',
title: 'Alternative text',
description: 'Important for SEO and accessibility',
}
]
}),
// ... other fields
],
// ... rest of the configuration
});
With these additions, your blog posts will have proper meta tags and structured data regardless of whether you're using Portable Text or Markdown content.
Advanced Configuration
Making Content Fields Optional
The validation rules we added to the schema make the body and markdownContent fields optional, but require at least one of them to be present. This gives content creators flexibility to use either Portable Text or Markdown.
Enhancing the Editor Experience
You can further enhance the Markdown editor experience by:
Adding more toolbar buttons: Customize the toolbar in the CustomMarkdownInput component.
Changing the theme: Import a different highlight.js theme for syntax highlighting.
Customizing the preview: Modify the previewRender function to change how Markdown is rendered in the preview.
Troubleshooting
Common Issues
CSS not loading: Make sure you've imported the CSS files in the correct layout files.
TypeScript errors: Use @ts-ignore comments for any TypeScript errors related to third-party libraries.
Markdown not rendering: Check that you're including the markdownContent field in your GROQ queries.
Code blocks not highlighting: Ensure you've installed and configured rehype-highlight correctly.
Debugging Tips
Check the browser console for any errors.
Use the React DevTools to inspect the components and props.
Test with simple Markdown content first, then add more complex elements.
Conclusion
You now have a fully functional Markdown editor in your Sanity Studio, with proper rendering in your Next.js frontend. This setup gives your content team the flexibility to use either Portable Text or Markdown, depending on their preferences and needs.