Одностраничный просмотрщик Markdown (.md) и JSON (.json) файлов по URL или из локального файла. Поддерживает подсветку синтаксиса, переключение светлой/тёмной темы, компактный режим, а также рендеринг диаграмм Mermaid, PlantUML, Pikchr и внешних SVG (draw.io).
Открывается по адресу вида:
https://<viewer-domain>/?url=https://<any-host>/path/file.md
- Markdown (через
marked) с таблицами GFM, якорями заголовков и кнопкой «копировать» у блоков кода. - LaTeX-формулы (через KaTeX): блочные
$$...$$и инлайн$...$. - JSON-viewer с фильтром, сворачиванием узлов и подсчётом ключей.
- Диаграммы из fenced code blocks:
- Mermaid —
```mermaid/```mmd - PlantUML —
```plantuml/```puml - Pikchr —
```pikchr
- Mermaid —
- Изображения (включая draw.io SVG) — через обычный
, с автоматическим резолвингом относительных URL относительно источника Markdown. - Декодирование URL GitHub/GitLab
blob/→raw/для прямой загрузки. - Подсветка синтаксиса кода через highlight.js с переключением тем.
- Light/Dark тема с сохранением через
prefers-color-scheme, плюс ручной переключатель. - Компактный режим для широкого контента (таблиц, длинных листингов).
Все библиотеки самохостятся в каталоге vendor/ — приложение не зависит от внешних CDN и продолжает работать, даже если CDN заблокирован.
| Файл | Назначение | Версия | Источник |
|---|---|---|---|
marked.min.js |
Парсинг Markdown | 12 | https://cdn.jsdelivr.net/npm/marked@12/marked.min.js |
highlight.min.js |
Подсветка кода | 11.9.0 | https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/ |
hljs-dark.css |
Тема highlight.js (тёмная) | 11.9.0 | …/styles/github-dark.min.css |
hljs-light.css |
Тема highlight.js (светлая) | 11.9.0 | …/styles/github.min.css |
mermaid.min.js |
Рендеринг Mermaid-диаграмм | 11 | https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js |
pikchr.js |
Рендеринг Pikchr-диаграмм (WASM) | 0.1.4 | https://cdn.jsdelivr.net/npm/pikchr-js/pikchr.js |
katex/ |
Рендеринг LaTeX-формул (js+css+woff2) | 0.16.11 | https://registry.npmjs.org/katex/-/katex-0.16.11.tgz |
Обновление любой библиотеки:
curl -sL "<URL из таблицы выше>" -o vendor/<file>
# задеплоить vendor/ на сервер (см. ниже)Шрифты Satoshi и JetBrains Mono подключаются через <link> с fontshare.com и fonts.googleapis.com. Если эти домены недоступны, браузер автоматически откатывается на системные шрифты — функциональность не страдает.
Браузер по умолчанию блокирует fetch() к доменам, которые не отдают Access-Control-Allow-Origin. Чтобы просмотрщик работал с любыми http(s)-источниками (объектные storages, внутренние HTTP-серверы, GitHub/GitLab raw, произвольные сайты без настроенного CORS), на бэкенде работает SSRF-safe прокси-эндпоинт GET /api/fetch?url=<encoded>.
Фронтенд всегда идёт через /api/fetch (single source of truth) — больше никаких внешних публичных проксей (cors.eu.org, allorigins.win), никакой захардкоженной привязки к конкретному storage-домену.
- Frontend (
index.html): все запросы к внешним URL идут через единственный proxy'/api/fetch?url=' + encodeURIComponent(url)в массивеPROXIES. Изображения в Markdown () грузятся браузером напрямую — для них нуженimg-srcв CSP. - Backend (
backend/server.js, тот же процесс, что и DOCX-экспорт): endpointGET /api/fetchрезолвит upstream, проверяет IP, делает запрос и отдаёт тело сAccess-Control-Allow-Origin: *. - Nginx:
location /api/проксирует на127.0.0.1:3001(см.md.mtsa-next.ru.conf).
Прокси не является open-relay — он отвергает запросы к внутренним/приватным адресам, чтобы предотвратить SSRF-атаки на хостинг-инфраструктуру:
| Проверка | Реализация |
|---|---|
| Протокол | Только http: / https:. file:, ftp:, data: и пр. → 400. |
| Приватные/зарезервированные IP | net.BlockList покрывает RFC1918 (10/8, 172.16/12, 192.168/16), loopback 127/8, link-local 169.254/16 (включая cloud-metadata 169.254.169.254), CGNAT 100.64/10, TEST-NET, multicast, reserved, IPv6 ::1, fc00::/7, fe80::/10. |
| DNS-резолв | Все A/AAAA записи проверяются до соединения. Если хоть одна запись приватная → 400. |
| IP literal в URL | Проверяется напрямую через net.isIP (Node обходит lookup для numeric hostname). |
| Redirects | До 3 редиректов, каждый пере-валидируется (URL + DNS + IP). |
| Размер ответа | До 10 MB (FETCH_MAX_BYTES), сверх — 413. |
| Таймаут | 10 с на upstream-запрос (FETCH_TIMEOUT_MS). |
| Rate limit | 30 запросов/мин на клиентский IP (X-Real-IP или socket), in-memory bucket. |
Код集中在 функциях resolveAndCheck, validateFetchUrl, fetchUpstream в backend/server.js.
- Прокси обязателен. Без backend-сервиса просмотрщик не сможет загружать никакие внешние файлы (только локальные через «Открыть»).
- Backend один на оба endpoint'а:
/api/fetch(GET) и/api/export-docx(POST). Systemd-юнитmdviewer-export.service. - Менять лимиты — константы
FETCH_MAX_BYTES,FETCH_TIMEOUT_MS,FETCH_MAX_REDIRECTS,RATE_LIMIT_WINDOW_MS,RATE_LIMIT_MAXв начале файла. - Расширить blocklist — объект
ipBlockList(например, добавить публичные DNS-серверы провайдера, если они в приватном диапазоне). - Разрешить конкретный приватный хост (например, внутренний storage за VPN): добавьте bypass-проверку в
resolveAndCheckпо whitelist имён хостов. - CSP:
connect-srcдолжен включать'self'(запросы на/api/fetchsame-origin). Для изображений:img-srcдолжен явно перечислять разрешённые источники картинок. - После правок бэкенда:
systemctl restart mdviewer-export, для nginx —nginx -t && systemctl reload nginx.
См. раздел «Экспорт в DOCX» ниже — это тот же Node.js-сервис на 127.0.0.1:3001. Обновление кода:
sudo cp backend/server.js /home/admin/web/<domain>/private/backend/server.js
sudo systemctl restart mdviewer-export
curl -sI "https://<viewer-domain>/api/fetch?url=https://example.com/" | head -1 # HTTP/2 200В шапку добавлена кнопка «.docx» (рядом с переключателем темы). Она активна только когда:
- загружен Markdown-файл,
- настроен и запущен backend-эндпоинт
/api/export-docx.
- Frontend (
index.html): перед отправкой исходный Markdown проходит front-end препроцессинг:- относительные URL изображений резолвятся в абсолютные,
- блоки
\`\`\`mermaidи\`\`\`pikchrзаменяются на встроенные SVG в Base64, - блоки
\`\`\`plantumlзаменяются на.
- Backend (
private/backend/server.js): лёгкий Node.js HTTP-сервер (zero-зависимостей). Принимает JSON{ markdown, filename }, запускаетpandoc -f gfm -t docx --wrap=none, обрабатываетdata:URI изображений (пишет во временные файлы) и отдаёт готовый.docx. - Nginx (
nginx.ssl.conf_export_docx): location/api/проксируется на127.0.0.1:3001.
pandoc>= 3.x (или 2.9.2+). Рекомендуется свежий.debс GitHub Releases.node>= 18.- systemd (для запуска backend-как-сервиса).
# 1. Установить pandoc
wget https://github.com/jgm/pandoc/releases/download/3.10/pandoc-3.10-1-amd64.deb
sudo dpkg -i pandoc-3.10-1-amd64.deb
# 2. Скопировать backend
sudo mkdir -p /home/admin/web/report.insightpilot.ru/private/backend
sudo cp private/backend/* /home/admin/web/report.insightpilot.ru/private/backend/
# 3. Systemd
sudo cp /path/to/mdviewer-export.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now mdviewer-export
# 4. Nginx include
sudo cp /path/to/nginx.ssl.conf_export_docx /home/admin/conf/web/report.insightpilot.ru/
sudo nginx -t && sudo systemctl reload nginxПроект раздаётся nginx напрямую (статика) с двух доменов:
| Домен | Document root | Nginx-конфиг |
|---|---|---|
md.mtsa-next.ru |
/home/admin/web/md.mtsa-next.ru/public_html |
/etc/nginx/conf.d/md.mtsa-next.ru.conf |
report.insightpilot.ru |
/home/admin/web/report.insightpilot.ru/public_html |
/etc/nginx/conf.d/report.insightpilot.ru.conf |
Деплой frontend (index.html + vendor/) на оба домена с этой машины:
for d in md.mtsa-next.ru report.insightpilot.ru; do
cp /root/md-viewer/index.html /home/admin/web/$d/public_html/index.html
rsync -a --delete /root/md-viewer/vendor/ /home/admin/web/$d/public_html/vendor/
chown -R admin:admin /home/admin/web/$d/public_html
done
# проверка
curl -sI https://md.mtsa-next.ru/ | head -1
curl -sI https://report.insightpilot.ru/ | head -1Владелец файлов — admin:admin. Backend (/api/) проксируется nginx'ом на 127.0.0.1:3001 (см. «Деплой backend» выше).
ВНИМАНИЕ: Hestia-конфиги
/etc/nginx/conf.d/domains/report.insightpilot.ru*.conf(симлинки на/home/admin/conf/web/...) в основнойnginx.confне подключаются — они проксируют на apache:8443 безHost-заголовка и отдают 421. Используются самостоятельные server-блоки в/etc/nginx/conf.d/*.conf, не редактируйте Hestia-файлы (они перезаписываются при rebuild domain).
Приложение — это статический index.html + каталог vendor/:
# 1. Создать document root и скопировать статику
sudo mkdir -p /var/www/file-viewer
sudo cp index.html vendor -t /var/www/file-viewer/
sudo chown -R <web-server-user>:<web-server-user> /var/www/file-viewer
# 2. Настроить server block (см. раздел для вашей системы выше)
# 3. Проверить и перезагрузить веб-сервер
sudo <web-server> -t && sudo systemctl reload <web-server>
# 4. Проверить, что index.html и vendor/ отдаются
curl -sI "https://<viewer-domain>/index.html" | head -1 # HTTP/2 200
curl -sI "https://<viewer-domain>/vendor/marked.min.js" | head -1 # HTTP/2 200Особенности для LLM-агента:
- Не кладите
index.htmlв подкаталог — приложение использует относительные пути кvendor/. - Проверьте, что MIME-тип для
.jsотдаётся какapplication/javascript(илиtext/javascript), иначе браузер заблокирует исполнение. - Если страница грузится пустой — откройте DevTools → Console. Самые частые причины: 404 на
vendor/*.js(неправильный document root) или CSP-заголовок, блокирующийscript-src.
.
├── index.html # одностраничное приложение (HTML + CSS + JS в одном файле)
├── backend/
│ ├── server.js # Node.js: /api/fetch (SSRF-safe proxy) + /api/export-docx (pandoc)
│ └── package.json
├── vendor/ # самохостящиеся библиотеки (см. таблицу выше)
│ ├── marked.min.js
│ ├── highlight.min.js
│ ├── hljs-dark.css
│ ├── hljs-light.css
│ ├── mermaid.min.js
│ ├── pikchr.js
│ └── katex/ # KaTeX: katex.min.js, katex.min.css, fonts/*.woff2
├── mdviewer-export.service # systemd-юнит для backend
└── README.md