I'll create a separate utility file with just the conversion function that you can reuse across your project.
I've created a reusable utility file with the following functions:
markdownToTiptap(markdown: string) - Converts markdown directly to Tiptap JSON schemamarkdownToHtml(markdown: string) - Converts markdown to HTML (intermediate step)getTiptapExtensions() - Returns the extensions used for conversionbatchMarkdownToTiptap(markdownArray: string[]) - Batch convert multiple markdown stringsmarkdownToTiptapDetailed(markdown: string) - Returns detailed conversion results with error handling// Simple conversion
const tiptapSchema = await markdownToTiptap("# Hello World")
// Get HTML intermediate
const html = await markdownToHtml("**Bold text**")
// Batch conversion
const schemas = await batchMarkdownToTiptap([markdown1, markdown2, markdown3])
// Use same extensions in your editor
const editor = useEditor({
extensions: getTiptapExtensions(),
content: tiptapSchema,
})You can now import these functions in any component and use them for markdown conversion without duplicating code!
this is our current editor component:'use client';import { useEditor, EditorContent, Editor, JSONContent } from '@tiptap/react';import StarterKit from '@tiptap/starter-kit';import Underline from '@tiptap/extension-underline';import TaskList from '@tiptap/extension-task-list';import TaskItem from '@tiptap/extension-task-item';import Link from '@tiptap/extension-link';import Image from '@tiptap/extension-image';import React from 'react';import {CodeXml,FileCode2,Link as LinkIcon,Image as ImageIcon,} from 'lucide-react';import {Tooltip,TooltipContent,TooltipProvider,TooltipTrigger,} from '@/components/ui/tooltip';import './rich-text-editor.css';import { cn } from '@/lib/utils';interface RichTextEditorProps {value: JSONContent;onChange: (value: Editor) => void;maxHeight?: string;}const ToolbarButton = ({onClick,isActive,children,label,shortcut,...props}: {onClick: () => void;isActive?: boolean;children: React.ReactNode;label: string;shortcut?: string;}) => (<Tooltip><TooltipTrigger asChild><button {...props} onClick={onClick} className={isActive ? 'active' : ''}>{children}</button></TooltipTrigger><TooltipContent side="top" className="bg-slate-800 text-xs text-slate-100">{label}{shortcut && <div className="text-slate-400">{shortcut}</div>}</TooltipContent></Tooltip>);export const RichTextEditor: React.FC<RichTextEditorProps> = ({value,onChange,maxHeight,}) => {const sanitizedContent = (() => {try {if (value?.type === 'doc' && (!value.content || value.content.length === 0) ) {return '<p></p>'; } } catch {// Not JSON, fall back to string content }return value || '<p></p>'; })();const editor = useEditor({extensions: [StarterKit,Underline,TaskList,TaskItem.configure({ nested: true }),Link.configure({openOnClick: false,HTMLAttributes: { class: 'editor-link' }, }),Image,],content: sanitizedContent,immediatelyRender: true,shouldRerenderOnTransaction: true,onUpdate: ({ editor }) => {onChange(editor); }, });if (!editor) return null;return (<div className="rounded-md border p-2"><TooltipProvider delayDuration={100}><div className="toolbar mb-2 flex gap-2 overflow-x-auto border-b pb-2"><ToolbarButtonkey={bold-${editor.isActive('bold')}}onClick={() => editor.chain().focus().toggleBold().run()}isActive={editor.isActive('bold')}label="Bold"shortcut="⌘+B"> B</ToolbarButton><ToolbarButtonkey={italic-${editor.isActive('italic')}}onClick={() => editor.chain().focus().toggleItalic().run()}isActive={editor.isActive('italic')}label="Italic"shortcut="⌘+I"> I</ToolbarButton><ToolbarButtonkey={underline-${editor.isActive('underline')}}onClick={() => editor.chain().focus().toggleUnderline().run()}isActive={editor.isActive('underline')}label="Underline"shortcut="⌘+U"> U</ToolbarButton><ToolbarButtonkey={h1-${editor.isActive('heading', { level: 1 })}}onClick={() =>editor.chain().focus().toggleHeading({ level: 1 }).run()}isActive={editor.isActive('heading', { level: 1 })}label="Heading 1"shortcut="#"> H1</ToolbarButton><ToolbarButtonkey={h2-${editor.isActive('heading', { level: 2 })}}onClick={() =>editor.chain().focus().toggleHeading({ level: 2 }).run()}isActive={editor.isActive('heading', { level: 2 })}label="Heading 2"shortcut="##"> H2</ToolbarButton><ToolbarButtonkey={h3-${editor.isActive('heading', { level: 3 })}}onClick={() =>editor.chain().focus().toggleHeading({ level: 3 }).run()}isActive={editor.isActive('heading', { level: 3 })}label="Heading 3"shortcut="###"> H3</ToolbarButton><ToolbarButtonkey={bullet-${editor.isActive('bulletList')}}onClick={() => editor.chain().focus().toggleBulletList().run()}isActive={editor.isActive('bulletList')}label="Bullet List"shortcut="-"> • List</ToolbarButton><ToolbarButtonkey={ordered-${editor.isActive('orderedList')}}onClick={() => editor.chain().focus().toggleOrderedList().run()}isActive={editor.isActive('orderedList')}label="Ordered List"shortcut="1."> 1. List</ToolbarButton><ToolbarButtonkey={task-${editor.isActive('taskList')}}onClick={() => editor.chain().focus().toggleTaskList().run()}isActive={editor.isActive('taskList')}label="Task List"shortcut="[ ]"> ☑ Task</ToolbarButton><ToolbarButtonkey={code-block-${editor.isActive('codeBlock')}}onClick={() => editor.chain().focus().toggleCodeBlock().run()}isActive={editor.isActive('codeBlock')}label="Code Block"shortcut="```"><FileCode2 className="h-4 w-4" /></ToolbarButton><ToolbarButtonkey={inline-code-${editor.isActive('code')}}onClick={() => editor.chain().focus().toggleCode().run()}isActive={editor.isActive('code')}label="Inline Code"shortcut=""><CodeXml className="h-4 w-4" /></ToolbarButton><ToolbarButtonkey={link-${editor.isActive('link')}}onClick={() => {const url = prompt('Enter URL');if (url) {editor.chain().focus().setLink({ href: url }).run(); } }}isActive={editor.isActive('link')}label="Link"><LinkIcon className="h-4 w-4" /></ToolbarButton><ToolbarButtononClick={() => {const url = prompt('Enter image URL');if (url) {editor.chain().focus().setImage({ src: url }).run(); } }}label="Image"><ImageIcon className="h-4 w-4" /></ToolbarButton><ToolbarButtononClick={() =>editor.chain().focus().unsetAllMarks().clearNodes().run()}label="Clear formatting"> Clear</ToolbarButton></div></TooltipProvider><EditorContenteditor={editor}className={cn(maxHeight && ${maxHeight} overflow-y-auto`)}/></div> );};will the converstion work for my editor? i need to have this convertion logic working on my backend so im not sure if i can install things like extentions in there, its a nodejs nestjs backend
Looking at your editor, the conversion will mostly work, but there are some differences in extensions. Your editor has additional extensions like Underline, TaskList, and TaskItem that aren't in the conversion utility.
For your Node.js/NestJS backend, you can't use Tiptap extensions directly since they're designed for the browser. Let me create a backend-compatible version and update the utility to match your editor extensions.