Интеграция с Express

Для работы с Markdown в приложении на Express часто используют библиотеку Marked. Установка выполняется через npm:

npm install marked

После установки подключение в коде выглядит следующим образом:

const express = require('express');
const { marked } = require('marked');
const fs = require('fs');
const path = require('path');

const app = express();
const PORT = 3000;

Чтение Markdown-файлов и генерация HTML

Основная задача — преобразовать содержимое Markdown-файла в HTML, который можно отдать клиенту. Для этого удобно использовать встроенный модуль fs:

app.get('/page/:name', (req, res) => {
    const filePath = path.join(__dirname, 'markdown', `${req.params.name}.md`);
    
    fs.readFile(filePath, 'utf8', (err, data) => {
        if (err) {
            res.status(404).send('Файл не найден');
            return;
        }
        const htmlContent = marked(data);
        res.send(`
            <!DOCTYPE html>
            <html lang="ru">
            <head>
                <meta charset="UTF-8">
                <title>${req.params.name}</title>
            </head>
            <body>
                ${htmlContent}
            </body>
            </html>
        `);
    });
});

В этом примере:

  • Используется параметр маршрута :name для динамического определения файла Markdown.
  • Содержимое файла читается асинхронно, чтобы не блокировать основной поток Node.js.
  • Функция marked(data) конвертирует Markdown в HTML.

Настройка опций Marked

Библиотека Marked позволяет тонко настраивать процесс парсинга. Примеры наиболее часто используемых опций:

marked.setOptions({
    renderer: new marked.Renderer(),  // Пользовательский рендерер
    gfm: true,                        // Поддержка GitHub Flavored Markdown
    breaks: true,                     // Разрывы строк как в GitHub
    sanitize: false,                  // Отключение автоматической очистки HTML
    smartLists: true,                 // Улучшенные списки
    smartypants: true                 // "Умные" кавычки и тире
});

Примечания к опциям:

  • renderer позволяет переопределять способ генерации отдельных элементов, таких как ссылки, изображения, заголовки.
  • sanitize больше не является обязательной мерой безопасности — для безопасного рендеринга рекомендуется использовать внешние библиотеки вроде DOMPurify.

Пользовательские рендереры

Можно создавать кастомные рендереры для изменения HTML-вывода:

const renderer = new marked.Renderer();

renderer.heading = (text, level) => {
    return `<h${level} class="custom-heading">${text}</h${level}>`;
};

renderer.link = (href, title, text) => {
    return `<a href="${href}" target="_blank" rel="noopener">${text}</a>`;
};

marked.setOptions({ renderer });

Такой подход позволяет:

  • Добавлять классы к элементам для стилизации через CSS.
  • Принудительно открывать ссылки в новой вкладке.
  • Контролировать генерацию любых HTML-элементов Markdown.

Асинхронная генерация HTML

Marked поддерживает промисы, что удобно при интеграции с асинхронными источниками данных:

app.get('/async/:name', async (req, res) => {
    const filePath = path.join(__dirname, 'markdown', `${req.params.name}.md`);
    
    try {
        const data = await fs.promises.readFile(filePath, 'utf8');
        const htmlContent = await marked.parse(data);
        res.send(htmlContent);
    } catch (err) {
        res.status(404).send('Ошибка при чтении файла');
    }
});

Использование marked.parse вместо синхронного marked(data) позволяет интегрировать библиотеку в асинхронные цепочки кода без блокировки сервера.

Интеграция с шаблонизаторами Express

Marked хорошо сочетается с шаблонизаторами вроде EJS, Pug, Handlebars. Пример с EJS:

app.set('view engine', 'ejs');

app.get('/ejs/:name', async (req, res) => {
    const filePath = path.join(__dirname, 'markdown', `${req.params.name}.md`);
    try {
        const data = await fs.promises.readFile(filePath, 'utf8');
        const htmlContent = marked(data);
        res.render('template', { content: htmlContent });
    } catch (err) {
        res.status(404).send('Файл не найден');
    }
});

В шаблоне template.ejs:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Документ</title>
</head>
<body>
    <%- content %>
</body>
</html>

Использование <%- %> гарантирует, что HTML от Marked не будет экранирован и корректно отобразится на странице.

Безопасность при рендеринге Markdown

Markdown-файлы могут содержать вредоносный HTML. Чтобы защитить приложение:

  • Применять библиотеку DOMPurify или аналог для очистки HTML перед отправкой клиенту.
  • Не доверять пользовательскому Markdown, если он приходит из внешнего источника.
  • Использовать опцию sanitize с осторожностью, так как она не всегда полностью блокирует XSS.

Кеширование с Express

Если приложение работает с большим количеством статических Markdown-файлов, имеет смысл кешировать HTML:

const cache = {};

app.get('/cached/:name', async (req, res) => {
    const name = req.params.name;
    if (cache[name]) {
        res.send(cache[name]);
        return;
    }

    const filePath = path.join(__dirname, 'markdown', `${name}.md`);
    try {
        const data = await fs.promises.readFile(filePath, 'utf8');
        const htmlContent = marked(data);
        cache[name] = htmlContent;
        res.send(htmlContent);
    } catch (err) {
        res.status(404).send('Файл не найден');
    }
});

Кеширование значительно уменьшает нагрузку на сервер при частых запросах к одним и тем же страницам.

Интеграция с маршрутизаторами Express

Для больших приложений удобно выделять отдельный роутер для работы с Markdown:

const router = express.Router();

router.get('/:name', async (req, res) => {
    const filePath = path.join(__dirname, 'markdown', `${req.params.name}.md`);
    try {
        const data = await fs.promises.readFile(filePath, 'utf8');
        res.send(marked(data));
    } catch {
        res.status(404).send('Файл не найден');
    }
});

app.use('/docs', router);

Такой подход структурирует проект, облегчает поддержку и масштабирование, отделяя логику рендеринга Markdown от основной бизнес-логики.