Реалізація Markdown-редактора на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Markdown-редактора на сайті
Середня
~3-5 робочих днів
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація Markdown-редактора

Markdown-редактор дозволяє користувачам створювати форматований контент без складності WYSIWYG. Популярні варіанти: CodeMirror + marked.js, TipTap з розширеннями Markdown, @uiw/react-md-editor.

@uiw/react-md-editor: швидкий старт

import MDEditor from '@uiw/react-md-editor';
import { useState } from 'react';

function MarkdownEditor({ initialValue = '', onChange }: EditorProps) {
  const [value, setValue] = useState(initialValue);

  const handleChange = (val?: string) => {
    const markdown = val ?? '';
    setValue(markdown);
    onChange?.(markdown);
  };

  return (
    <MDEditor
      value={value}
      onChange={handleChange}
      height={400}
      preview="live"
      hideToolbar={false}
      commands={[
        MDEditor.commands.bold,
        MDEditor.commands.italic,
        MDEditor.commands.title,
        MDEditor.commands.divider,
        MDEditor.commands.link,
        MDEditor.commands.image,
        MDEditor.commands.code,
        MDEditor.commands.codeBlock,
        MDEditor.commands.divider,
        MDEditor.commands.fullscreen,
      ]}
    />
  );
}

CodeMirror 6 + marked.js: кастомна реалізація

import { EditorView, basicSetup } from 'codemirror';
import { markdown } from '@codemirror/lang-markdown';
import { oneDark } from '@codemirror/theme-one-dark';
import { marked } from 'marked';
import DOMPurify from 'dompurify';

function createMarkdownEditor(container: HTMLElement, previewContainer: HTMLElement) {
  const view = new EditorView({
    doc: '',
    extensions: [
      basicSetup,
      markdown(),
      oneDark,
      EditorView.updateListener.of(update => {
        if (update.docChanged) {
          const markdown = update.state.doc.toString();
          const html = marked(markdown, { breaks: true, gfm: true });
          previewContainer.innerHTML = DOMPurify.sanitize(html as string);
        }
      }),
    ],
    parent: container,
  });

  return view;
}

Завантаження зображень з редактора

// Кастомна команда для вставки зображення з завантаженням
import * as commands from '@uiw/react-md-editor/commands';

const imageUploadCommand: commands.ICommand = {
  name: 'upload-image',
  keyCommand: 'upload-image',
  buttonProps: { 'aria-label': 'Завантажити зображення' },
  icon: <ImageIcon />,
  execute: async (state, api) => {
    const file = await openFilePicker(['image/jpeg', 'image/png', 'image/webp']);
    if (!file) return;

    const formData = new FormData();
    formData.append('file', file);

    const { data } = await api.post('/api/media/upload', formData);

    const imageMarkdown = `![${file.name}](${data.url})`;
    api.replaceSelection(imageMarkdown);
  },
};

async function openFilePicker(accept: string[]): Promise<File | null> {
  return new Promise(resolve => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = accept.join(',');
    input.onchange = () => resolve(input.files?.[0] ?? null);
    input.click();
  });
}

Серверна обробка: sanitization

use League\CommonMark\CommonMarkConverter;
use League\HTMLToMarkdown\HtmlConverter;

class MarkdownService
{
    private CommonMarkConverter $converter;

    public function __construct()
    {
        $this->converter = new CommonMarkConverter([
            'html_input'         => 'strip',   // видалити сирий HTML
            'allow_unsafe_links' => false,
            'max_nesting_level'  => 5,
        ]);
    }

    public function toHtml(string $markdown): string
    {
        return $this->converter->convert($markdown)->getContent();
    }

    public function sanitize(string $markdown): string
    {
        // Видалити потенційно небезпечні конструкції
        $markdown = preg_replace('/\[([^\]]+)\]\(javascript:[^)]+\)/', '[$1]', $markdown);
        return strip_tags($markdown);  // очистити якщо попав HTML
    }
}

Зберігання: Markdown vs HTML

Зберігати оригінальний Markdown (для редагування) та рендерити в HTML при відображенні. Кешувати HTML у полі body_html або в Redis.

// Модель статті
class Article extends Model
{
    protected static function booted(): void
    {
        static::saving(function (Article $article) {
            if ($article->isDirty('body_markdown')) {
                $article->body_html = app(MarkdownService::class)
                    ->toHtml($article->body_markdown);
            }
        });
    }
}

Терміни реалізації

@uiw/react-md-editor з завантаженням зображень та серверним sanitization: 2–3 дні. Кастомний редактор на CodeMirror з live-preview та синтаксичними розширеннями: 3–5 днів.