2025-09-07
计算机
0

目录

$\S$ 1 Upmath项目简介
项目功能架构梳理
使用流程梳理
技术优势
$\S$ 2 部署步骤
1. 准备VPS环境+域名
1.1 所需工具
1.2 安装基础工具
1.3 安装Docker
1.3 域名准备
2. 修改源码(跳过)
3. 两种部署方式:Docker部署或源码部署
A. 从GHCR直接Docker部署(推荐)
B. 手动安装
(1) 依赖要求
(2) 安装依赖
(3) 拉代码
(4) 安装依赖+构建前端
(4) 生成并配置 config.php
(5) 配置 Nginx:仅监听本机 8080
(a) 把 listen 改成仅本机端口,例如:
(b) 把站点根目录 root 指向你的项目 www 目录,例如:
(6) 启动 SVGO HTTP 服务(用于 SVG 优化缓存)
(7) 本机验证
4. 添加域名解析
5. Nginx反代

§\S 1 Upmath项目简介

Upmath是俄罗斯粒子物理学家Dr. Роман Парпалак主导完成的一个开源前端项目——一个基于Markdown+LaTeX的在线编辑器,见它的项目Github主页。它可以实现在Web上显示复杂的数学公式图形(比如LaTeX Tikz所画的图)的网页内容。

它是将Markdown文本与LaTeX数学表达式转换成HTML页面,并嵌入公式图像(SVG格式)。

项目功能架构梳理

项目组件技术栈与职责
前端 (upmath.me)JavaScript + HTML/CSS 前端编辑器,处理 Markdown + LaTeX 转 HTML,并嵌入公式图像。使用 grunt 构建流程。
后端渲染服务 (i.upmath.me)PHP + TeX Live + nginx + Node.js + Grunt + SVG 工具链:渲染公式为 SVG,并提供 API 服务给前端调用。
图像渲染方式使用 TeX Live 和工具链(如 dvisvgm)将 LaTeX 公式渲染为 SVG 矢量图,支持复杂图形(如 TikZ)。(i.upmath.me)

使用流程梳理

  1. 用户在前端编辑器中输入 Markdown 文本及 LaTeX 公式。

  2. 编辑器将 Markdown 转为 HTML。LaTeX 公式部分则使用 的方式,调用后端渲染服务。

  3. 后端服务渲染 LaTeX 为 SVG 图片,返回给前端展示。

  4. 最终用户在浏览器中看到的是 HTML 页面嵌入的 SVG 数学公式,可复制、分享或发布。

技术优势

  1. 双模块架构:清晰分离编辑与渲染职责,前端专注编辑与呈现,后端专注渲染服务。

  2. 技术栈丰富:前端为 JavaScript + Grunt 构建,后端融合 TeX、PHP、SVG 渲染工具,也支持 Docker 部署,便于复现环境。

  3. 灵活应用:可嵌入到博客、论坛等任何支持 HTML 的平台,只需贴入一段脚本即可动态渲染数学内容。

§\S 2 部署步骤

1. 准备VPS环境+域名

在VPS购买的网站购买一个按需要求的VPS,并且获得了公网IPv4:<你的IP地址>,我们这里使用的Debian 12发行版。

1.1 所需工具

  • git
  • curl
  • ca-certificates
  • gnupg
  • lsb-release
  • Docker

1.2 安装基础工具

bash
sudo apt update sudo apt install -y git curl ca-certificates gnupg lsb-release

1.3 安装Docker

添加 Docker 官方 GPG key:

bash
sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg \ | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

添加 Docker 官方 apt 源

bash
echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") \ $(lsb_release -cs) stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

安装 Docker 引擎

bash
sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io

测试

bash
sudo docker run hello-world

这样我们就准备好了基本的VPS环境。

1.3 域名准备

为了实现后面的域名访问,可以在一些域名购买网站上购买一个域名:<你的域名>。

2. 修改源码(跳过)

由于我已经Fork一个修改好的项目,可以跳过!!!

Fork项目在:https://github.com/X2M7/i.upmath.me

下面是在原来代码基础上的修改内容,仅作历史保存:

使用中文方案

我们之所以选择源码部署,而不直接选择官方Docker部署的原因是因为我们需要在编辑器有使用中文的需求,需要对源代码进行一定改动。

首先,我们从Github下载源码

Terminal
git clone https://github.com/parpalak/upmath.me.git

并进入源码所在的文件夹

Terminal
cd i.upmath.me

进入i.upmath.me项目源码文件夹下的tpl文件夹

Terminal
cd tpl

tpl文件夹里面有三个文件,可以通过ls查看

Terminal
ls

我们需要更改document.php这个文件,选择你喜欢的文本编辑器来编辑document.php

PHP
<?php /** @var bool $hasDvisvgmOption */ /** @var string $documentContent */ /** @var \S2\Tex\Tpl\PackageCollection $extraPackages */ /** * \documentclass[11pt,dvisvgm]{standalone} * %\usepackage[paperwidth=180in,paperheight=180in]{geometry} * \usepackage[paperwidth=180in, paperheight=180in,margin=0in]{geometry} * %\usepackage[a4paper, total={6in, 8in}]{geometry} * \standaloneconfig{crop=false} */ ?> \documentclass[11pt<?php if ($hasDvisvgmOption) { ?>,dvisvgm<?php } ?>]{article} \usepackage[paperwidth=180in,paperheight=180in]{geometry} \batchmode % 注意我们添加了这两行 \usepackage[utf8]{inputenc} \usepackage{CJKutf8} \usepackage{amsmath} \usepackage{amssymb} \usepackage{stmaryrd} \newcommand{\R}{\mathbb{R}} \newcommand{\lt}{<} \newcommand{\gt}{>} % Conditional definitions \providecommand{\tg}{\operatorname{tg}} \providecommand{\ctg}{\operatorname{ctg}} \providecommand{\arctg}{\operatorname{arctg}} \providecommand{\arcctg}{\operatorname{arcctg}} \usepackage[verbose]{newunicodechar} \newunicodechar{¬}{\ensuremath{\neg}} \newunicodechar{Γ}{\ensuremath{\Gamma}} \newunicodechar{γ}{\ensuremath{\gamma}} \newunicodechar{λ}{\ensuremath{\lambda}} \newunicodechar{φ}{\ensuremath{\varphi}} \newunicodechar{ψ}{\ensuremath{\psi}} \newunicodechar{ϕ}{\ensuremath{\varphi}} \newunicodechar{ᵢ}{\ensuremath{{}_{i}}} \newunicodechar{₀}{\ensuremath{{}_{0}}} \newunicodechar{₁}{\ensuremath{{}_{1}}} \newunicodechar{₂}{\ensuremath{{}_{2}}} \newunicodechar{₃}{\ensuremath{{}_{3}}} \newunicodechar{₄}{\ensuremath{{}_{4}}} \newunicodechar{₅}{\ensuremath{{}_{5}}} \newunicodechar{₆}{\ensuremath{{}_{6}}} \newunicodechar{₇}{\ensuremath{{}_{7}}} \newunicodechar{₈}{\ensuremath{{}_{8}}} \newunicodechar{₉}{\ensuremath{{}_{9}}} \newunicodechar{ₙ}{\ensuremath{{}_{n}}} \newunicodechar{ℓ}{\ensuremath{\ell}} \newunicodechar{→}{\ensuremath{\rightarrow}} \newunicodechar{⇒}{\ensuremath{\supset}} \newunicodechar{⇔}{\ensuremath{\Leftrightarrow}} \newunicodechar{∅}{\ensuremath{\emptyset}} \newunicodechar{∈}{\ensuremath{\in}} \newunicodechar{∘}{\ensuremath{\circ}} \newunicodechar{∙}{\ensuremath{\bullet}} \newunicodechar{∧}{\ensuremath{\wedge}} \newunicodechar{∨}{\ensuremath{\vee}} \newunicodechar{∼}{\ensuremath{\sim}} \newunicodechar{≠}{\ensuremath{\neq}} \newunicodechar{≡}{\ensuremath{\equiv}} \newunicodechar{⊃}{\ensuremath{\supset}} \newunicodechar{⊕}{\ensuremath{\oplus}} \newunicodechar{⊖}{\ensuremath{\ominus}} \newunicodechar{⊢}{\ensuremath{\vdash}} \newunicodechar{⊤}{\ensuremath{\top}} \newunicodechar{⊥}{\ensuremath{\bot}} \newunicodechar{⊻}{\ensuremath{\veebar}} \newunicodechar{⟝}{\ensuremath{\vdash}} \newunicodechar{⬓}{\ensuremath{\square}} \newunicodechar{Σ}{\ensuremath{\sum}} \newunicodechar{Π}{\ensuremath{\prod}} \newunicodechar{ⱼ}{\ensuremath{{}_{j}}} <?php echo $extraPackages->getCode(); ?> \pagestyle{empty} \setlength{\topskip}{0pt} \setlength{\parindent}{0pt} \setlength{\abovedisplayskip}{0pt} \setlength{\belowdisplayskip}{0pt} \begin{document} \begin{CJK}{UTF8}{gbsn} % 注意我们添加了这一行 <?php foreach (['newwrite', 'openout'] as $disabledCommand) { echo '\\renewcommand{\\' . $disabledCommand . '}{\\errmessage{Command \\noexpand\\' . $disabledCommand . ' is disabled}}', "\n"; } ?> <?php echo $documentContent; ?> \end{CJK}% 注意我们添加了这一行 \end{document}

请注意在原来代码中,我们添加了4行,为了尽量减少修改,我们仍然使用pdfLaTeX编译,因此,我们增加了对CJK包的支持

... \usepackage[utf8]{inputenc} \usepackage{CJKutf8} ...

注:如果你需要更多LaTeX的包的支持,就可以在这里的后面添加\usepackage{<你需要的LaTeX包>}

并在\begin{document}和\end{document}之内,添加了CJK的使用,UTF8是文本格式,而gbsn是宋体(你可以按需更改)

... \begin{document} \begin{CJK}{UTF8}{gbsn} % 注意我们添加了这一行 ... \end{CJK}% 注意我们添加了这一行 \end{document}

保存并退出,这样我们就完成了这个包的修改。回到上一级i.upmath.me/

Terminal
cd ..

为了保证有中文字体的支持,我们在生成Docker容器前写一个Dockerfile,使用你喜欢的编辑器

Terminal
nano Dockerfile

并在其中输入

Dockerfile
FROM ghcr.io/parpalak/upmath-texlive-docker:2025.0.1 EXPOSE 80 WORKDIR /var/www/i.upmath.me RUN apt-get update && apt-get install -y --no-install-recommends \ fonts-noto-cjk \ latex-cjk-all \ && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get -y --no-install-recommends install \ nginx-extras lua-zlib \ zip unzip \ php8.2-fpm \ php8.2-curl \ php8.2-xml \ php8.2-gd \ composer \ librsvg2-bin \ optipng \ supervisor \ curl gnupg && \ mkdir -p /etc/apt/keyrings && \ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \ apt-get update && \ apt-get install -y nodejs && \ apt-get remove -y curl gnupg && \ apt-get autoremove -y && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ rm -rf /var/cache/apt/ COPY . . RUN mkdir -p logs RUN composer install --no-dev RUN npm install -g yarn grunt-cli && \ yarn install && \ grunt && \ yarn install --prod && \ npm uninstall -g yarn grunt-cli RUN mkdir -p /var/run/php-fpm/ RUN cp config.php.dist config.php \ && tlversion=$(cat /usr/local/texlive/20*/release-texlive.txt | head -n 1 | awk '{ print $5 }') \ && sed -i "s/\${tlversion}/${tlversion}/g" config.php RUN cp docker/nginx.conf /etc/nginx/nginx.conf RUN cp docker/www.conf /etc/php/8.2/fpm/pool.d/www.conf && \ cp docker/www-tex.conf /etc/php/8.2/fpm/pool.d/www-tex.conf RUN cp docker/superv.conf /etc/superv.conf ENTRYPOINT [ "/var/www/i.upmath.me/docker/entrypoint.sh" ]

保存并退出即可。

暗色模式附加方案

为了实现“默认不变+?c=暗色改色+不串缓存”,你一共需要改动这些文件:

(1) 颜色改写(只在带?c=时生效)

(a) lib/Renderer/SvgHelper.php

php
<?php namespace S2\Tex\Renderer; class SvgHelper { private const POINTS_IN_PIXEL = 0.75; private const TOP_SHIFT_RATIO = 0.5; private static function normalizeHexColor(?string $c): ?string { if ($c === null) return null; $c = strtolower(trim($c)); $c = ltrim($c, '#'); if (preg_match('/^[0-9a-f]{3}$/', $c)) { return '#' . $c[0].$c[0] . $c[1].$c[1] . $c[2].$c[2]; } if (preg_match('/^[0-9a-f]{6}$/', $c)) { return '#' . $c; } return null; } /** * Only apply recolor when ?c=xxxxxx (or ?color=xxxxxx) exists. * - Root fill="currentColor" * - Root style="color:#xxxxxx" * - Replace explicit pure-black representations to currentColor * - Do NOT set root stroke (avoid bold glyphs) */ private static function applyColorParam(string $svg): string { $color = self::normalizeHexColor($_GET['c'] ?? ($_GET['color'] ?? null)); if ($color === null) { return $svg; // default output unchanged } // 1) Root svg: fill=currentColor + color=<hex> $svg = preg_replace_callback('/<svg\b([^>]*)>/', static function (array $m) use ($color): string { $attrs = $m[1]; // Ensure default fill follows currentColor if (stripos($attrs, ' fill=') === false) { $attrs .= ' fill="currentColor"'; } // Update or append style="color:..." if (preg_match('/\sstyle=("|\')([^"\']*)\1/i', $attrs, $sm)) { $quote = $sm[1]; $style = $sm[2]; // remove existing color:... $style = preg_replace('/(^|;)\s*color\s*:\s*[^;]+/i', '$1', $style); $style = trim($style); if ($style !== '' && substr($style, -1) !== ';') $style .= ';'; $style .= 'color:' . $color . ';'; $attrs = preg_replace('/\sstyle=("|\')([^"\']*)\1/i', ' style=' . $quote . $style . $quote, $attrs, 1); } else { $attrs .= ' style="color:' . $color . ';"'; } return '<svg' . $attrs . '>'; }, $svg, 1); // 2) Replace pure black in attributes -> currentColor // Support BOTH single and double quotes. $svg = preg_replace( '/\b(fill|stroke|stop-color)\s*=\s*(["\'])(#000000|#000|black)\2/i', '$1=$2currentColor$2', $svg ); // rgb(0,0,0) or rgb(0%,0%,0%) or rgb(0.0%,0.0%,0.0%) $svg = preg_replace( '/\b(fill|stroke|stop-color)\s*=\s*(["\'])rgb\(\s*0(?:\.0+)?%?\s*,\s*0(?:\.0+)?%?\s*,\s*0(?:\.0+)?%?\s*\)\2/i', '$1=$2currentColor$2', $svg ); // rgb(0 0 0) space-separated variant $svg = preg_replace( '/\b(fill|stroke|stop-color)\s*=\s*(["\'])rgb\(\s*0(?:\.0+)?%?\s+0(?:\.0+)?%?\s+0(?:\.0+)?%?\s*\)\2/i', '$1=$2currentColor$2', $svg ); // rgba(0,0,0,1) $svg = preg_replace( '/\b(fill|stroke|stop-color)\s*=\s*(["\'])rgba\(\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*1(?:\.0+)?\s*\)\2/i', '$1=$2currentColor$2', $svg ); // 3) Replace pure black in inline style -> currentColor $svg = preg_replace('/\b(fill|stroke|stop-color)\s*:\s*#000000\b/i', '$1:currentColor', $svg); $svg = preg_replace('/\b(fill|stroke|stop-color)\s*:\s*#000\b/i', '$1:currentColor', $svg); $svg = preg_replace('/\b(fill|stroke|stop-color)\s*:\s*black\b/i', '$1:currentColor', $svg); $svg = preg_replace('/\b(fill|stroke|stop-color)\s*:\s*rgb\(\s*0(?:\.0+)?%?\s*,\s*0(?:\.0+)?%?\s*,\s*0(?:\.0+)?%?\s*\)\b/i', '$1:currentColor', $svg); $svg = preg_replace('/\b(fill|stroke|stop-color)\s*:\s*rgb\(\s*0(?:\.0+)?%?\s+0(?:\.0+)?%?\s+0(?:\.0+)?%?\s*\)\b/i', '$1:currentColor', $svg); $svg = preg_replace('/\b(fill|stroke|stop-color)\s*:\s*rgba\(\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*1(?:\.0+)?\s*\)\b/i', '$1:currentColor', $svg); return $svg; } public static function processSvgContent(string $svg, bool $useBaseline): string { $startPattern = '#<!--start (-?[\d.]+) (-?[\d.]+) -->#'; if (!preg_match($startPattern, $svg, $matchBaseline)) { return self::applyColorParam($svg); } $viewBoxPattern = '#viewBox=["\'](-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)["\']#'; if (!preg_match($viewBoxPattern, $svg, $matchViewBox)) { return self::applyColorParam($svg); } [, $userStartX, $userStartY, $userWidth, $userHeight] = $matchViewBox; if ($userWidth < 0.000001 || $userHeight < 0.000001) { return self::applyColorParam($svg); } $userBaselineY = $matchBaseline[2]; $userFromTopToBaseline = max(0, $userBaselineY - $userStartY); $userFromBottomToBaseline = $useBaseline ? max($userHeight - $userFromTopToBaseline, 0) : $userHeight * 0.5; $multiplier = OUTER_SCALE / self::POINTS_IN_PIXEL; $viewportFromBottomToBaseline = $multiplier * $userFromBottomToBaseline; $viewportHeight = $multiplier * $userHeight; $viewportWidth = $multiplier * $userWidth; $extendedViewportHeight = ceil($viewportHeight); $extendedViewportWidth = ceil($viewportWidth); $extendedViewportFromBottomToBaseline = $viewportFromBottomToBaseline + (1 - self::TOP_SHIFT_RATIO) * ($extendedViewportHeight - $viewportHeight); $extendedUserHeight = $userHeight * $extendedViewportHeight / $viewportHeight; $extendedUserWidth = $userWidth * $extendedViewportWidth / $viewportWidth; $svg = preg_replace( '#<svg\b[^>]*>#', sprintf( '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%s" height="%s" viewBox="%s %s %s %s">', round($extendedViewportWidth, 6), round($extendedViewportHeight, 6), $userStartX, round($userStartY - self::TOP_SHIFT_RATIO * ($extendedUserHeight - $userHeight), 6), round($extendedUserWidth, 6), round($extendedUserHeight, 6) ), $svg, 1 ); $script = sprintf( '<script type="text/ecmascript">if(window.parent.postMessage)window.parent.postMessage("%s|%s|%s|"+window.location,"*");</script>', round($extendedViewportFromBottomToBaseline * self::POINTS_IN_PIXEL, 5), round($extendedViewportWidth * self::POINTS_IN_PIXEL, 5), round($extendedViewportHeight * self::POINTS_IN_PIXEL, 5) ); $svg = str_replace('</svg>', $script . "\n" . '</svg>', $svg); return self::applyColorParam($svg); } }

(b) lib/Cache/CacheProvider.php

php
<?php /** * @copyright 2020-2022 Roman Parpalak * @license http://www.opensource.org/licenses/mit-license.php MIT * @package Upmath Latex Renderer * @link https://i.upmath.me */ namespace S2\Tex\Cache; use S2\Tex\Processor\Request; class CacheProvider { protected string $cacheFailDir; protected string $cacheSuccessDir; public function __construct(string $cacheSuccessDir, string $cacheFailDir) { $this->cacheFailDir = $cacheFailDir; $this->cacheSuccessDir = $cacheSuccessDir; } public function getCacheState(Request $request): CacheState { $cacheName = $this->cachePathFromRequest($request, false); return new CacheState($cacheName, file_exists($cacheName)); } /** * Returns the cached path. * This algorithm should be used by a web-server to process the cache files as a static content. */ public function cachePathFromRequest(Request $request, bool $hasError): string { // IMPORTANT: // old code: md5(formula) -> will mix /svg/x and /svg/x?c=... // new code: include extension + variant, and bump schema with "cache-v2" $hash = md5( $request->getFormula() . "\n" . $request->getExtension() . "\n" . $request->getVariant() . "\n" . 'cache-v2' ); $prefixDir = $hasError ? $this->cacheFailDir : $this->cacheSuccessDir; return $prefixDir . substr($hash, 0, 2) . '/' . substr($hash, 2, 2) . '/' . substr($hash, 4) . '.' . $request->getExtension(); } }

(2) 让Request能拿到query(否则 variant 永远为空),修改www/render.php如下:

php
<?php /** * Entry point for rendering. * * @copyright 2014-2020 Roman Parpalak * @license http://www.opensource.org/licenses/mit-license.php MIT * @package Upmath Latex Renderer * @link https://i.upmath.me */ use hollodotme\FastCGI\Client; use hollodotme\FastCGI\Requests\PostRequest; use hollodotme\FastCGI\SocketConnections\UnixDomainSocket; use Katzgrau\KLogger\Logger; use S2\Tex\Cache\CacheProvider; use S2\Tex\Processor\CachedResponse; use S2\Tex\Processor\PostProcessor; use S2\Tex\Processor\Processor; use S2\Tex\Processor\Request; use S2\Tex\Renderer\PngConverter; use S2\Tex\Renderer\Renderer; use S2\Tex\Templater; require '../vendor/autoload.php'; require '../config.php'; $isDebug = defined('DEBUG') && DEBUG; error_reporting($isDebug ? E_ALL : -1); // Setting up external commands define('LATEX_COMMAND', TEX_PATH . 'latex -output-directory=' . TMP_DIR); define('DVISVG_COMMAND', TEX_PATH . 'dvisvgm %1$s -o %1$s.svg -n --exact -v0 --relative --zoom=' . OUTER_SCALE); // define('DVIPNG_COMMAND', TEX_PATH . 'dvipng -T tight %1$s -o %1$s.png -D ' . (96 * OUTER_SCALE)); // outdated define('SVG2PNG_COMMAND', 'rsvg-convert %1$s -d 96 -p 96 -b white'); // stdout function error400($error = 'Invalid formula') { header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); include '400.php'; } //ignore_user_abort(); ini_set('max_execution_time', 10); header('X-Powered-By: Upmath Latex Renderer'); $templater = new Templater(TPL_DIR); $pngConverter = new PngConverter(SVG2PNG_COMMAND); $renderer = new Renderer($templater, TMP_DIR, TEX_PATH, LATEX_COMMAND, DVISVG_COMMAND); $renderer ->setPngConverter($pngConverter) ->setIsDebug($isDebug); if (defined('LOG_DIR')) { $renderer->setLogger(new Logger(LOG_DIR)); } $cacheProvider = new CacheProvider(CACHE_SUCCESS_DIR, CACHE_FAIL_DIR); $processor = new Processor($renderer, $cacheProvider, $pngConverter); try { // IMPORTANT: use full REQUEST_URI so Request can see query (?c=...) $request = Request::createFromUri($_SERVER['REQUEST_URI']); } catch (Exception $e) { error400($isDebug ? $e->getMessage() : 'Invalid formula'); die; } $response = $processor->process($request); if (!$response->hasError()) { $response->echoContent(); } else { error400($isDebug ? $response->getError() : 'Invalid formula'); } if (!$isDebug && !($response instanceof CachedResponse)) { // Disconnecting from web-server flush(); fastcgi_finish_request(); $postProc = new PostProcessor($cacheProvider); $asyncRequest = $postProc->cacheResponseAndGetAsyncRequest($response, $_SERVER['HTTP_REFERER'] ?? 'no referer'); if ($asyncRequest !== null) { $connection = new UnixDomainSocket(FASTCGI_SOCKET, 1000, 1000); $client = new Client(); // IMPORTANT: pass color param to async processor too $content = http_build_query([ 'formula' => $asyncRequest->getFormula(), 'extension' => $asyncRequest->getExtension(), 'c' => $_GET['c'] ?? ($_GET['color'] ?? ''), ]); $request = new PostRequest(realpath('../cache_processor.php'), $content); $client->sendAsyncRequest($connection, $request); } }

(3) 异步缓存处理也必须带上 variant(否则异步优化会写错缓存),修改cache_processor.php如下:

php
<?php /** * Entry point for async cache optimizer. * * @copyright 2020-2022 Roman Parpalak * @license http://www.opensource.org/licenses/mit-license.php MIT * @package Upmath Latex Renderer * @link https://i.upmath.me */ require 'vendor/autoload.php'; require 'config.php'; // Fallback commands, now HTTP service is used. define('SVGO', realpath(SVGO_PATH) . '/svgo -i %1$s -o %1$s.new; rm %1$s; mv %1$s.new %1$s'); define('GZIP', 'gzip -cn6 %1$s > %1$s.gz.new; rm %1$s.gz; mv %1$s.gz.new %1$s.gz; rm %1$s'); // outdated, disabled due to SVG is now well-supported in browsers. define('OPTIPNG', 'optipng %1$s'); define('PNGOUT', 'pngout %1$s'); use S2\Tex\Cache\CacheProvider; use S2\Tex\Processor\DelayedProcessor; use S2\Tex\Processor\Request; $delayedProcessor = new DelayedProcessor( new CacheProvider(CACHE_SUCCESS_DIR, CACHE_FAIL_DIR), 'http://localhost:' . (defined('HTTP_SVGO_PORT') ? HTTP_SVGO_PORT : '8800') . '/' ); $delayedProcessor ->addSVGCommand(SVGO) ->addSVGCommand(GZIP) // ->addPNGCommand(OPTIPNG) // ->addPNGCommand(PNGOUT) ; $formula = $_POST['formula'] ?? ''; $ext = $_POST['extension'] ?? 'svg'; // IMPORTANT: same variant logic as render.php/request parsing $variant = Request::buildVariantFromParams($_POST); $request = new Request($formula, $ext, $variant); $delayedProcessor->process($request);

3. 两种部署方式:Docker部署或源码部署

A. 从GHCR直接Docker部署(推荐)

在服务器终端运行

bash
docker run -d --name upmath -p 8080:80 ghcr.io/x2m7/i.upmath.me:latest

本地验证

bash
curl -I http://127.0.0.1:8080

能返回 200/302 等正常响应,说明“内部服务”已启动完成。

B. 手动安装

(1) 依赖要求

  • php-fpm
  • Node.js + yarn + grunt-cli
  • TeX Live(建议 full)+ ghostscript + dvisvgm
  • librsvg2-bin
  • nginx(带 lua 模块)

(2) 安装依赖

bash
sudo apt update sudo apt install -y \ php-fpm php-curl php-xml php-gd \ nodejs npm yarn \ texlive-full ghostscript dvisvgm \ librsvg2-bin \ nginx-extras

(3) 拉代码

bash
git clone https://github.com/X2M7/i.upmath.me.git cd i.upmath.me

(4) 安装依赖+构建前端

bash
# PHP 依赖 composer install --no-dev # 前端依赖 + 构建 yarn install npx grunt yarn install --prod

(4) 生成并配置 config.php

bash
cp config.php.dist config.php nano config.php

重点检查/修改:

  • TEX_PATH:TeX 可执行文件目录(例如 /usr/bin/ 或 TeXLive 的 .../bin/x86_64-linux/
  • TMP_DIRCACHE_SUCCESS_DIRCACHE_FAIL_DIR:目录必须存在且 nginx/php-fpm 可读写 (建议你创建目录并赋权;以 /var/www/i.upmath.me 为例)
bash
sudo mkdir -p cache tmp sudo chown -R www-data:www-data cache tmp

(5) 配置 Nginx:仅监听本机 8080

创建一个“内部站点”配置文件:

bash
sudo cp nginx.conf.dist /etc/nginx/sites-available/i.upmath.internal sudo nano /etc/nginx/sites-available/i.upmath.internal

在该文件里做两类必要修改(按你的实际情况改):

(a) 把 listen 改成仅本机端口,例如:
nginx
listen 127.0.0.1:8080;
(b) 把站点根目录 root 指向你的项目 www 目录,例如:
nginx
root /path/to/i.upmath.me/www;

启用站点并重载 Nginx:

bash
sudo ln -s /etc/nginx/sites-available/i.upmath.internal /etc/nginx/sites-enabled/i.upmath.internal sudo nginx -t sudo systemctl reload nginx

(6) 启动 SVGO HTTP 服务(用于 SVG 优化缓存)

bash
sudo cp http-svgo.service.dist /etc/systemd/system/http-svgo.service sudo sed -i "s~@@DIR@@~$PWD~g" /etc/systemd/system/http-svgo.service sudo systemctl daemon-reload sudo systemctl enable --now http-svgo

(7) 本机验证

bash
curl -I http://127.0.0.1:8080

能返回 200/302 等正常响应,说明“内部服务”已启动完成。

4. 添加域名解析

打开购买域名的网站,在<你的域名>中添加两条域名解析

主机记录 记录类型 线路类型 记录值 TTL
www A 默认 <你的IP地址> 600
@ A 默认 <你的IP地址> 600

5. Nginx反代

编辑/etc/nginx/sites-available/下的default文件

Terminal
nano /etc/nginx/sites-available/default

在default最后添加

server { server_name <你的域名> www.<你的域名>; # 所有请求反代到内部服务 <你的IP地址>:8080 location / { proxy_pass http://<你的IP地址>:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/<你的域名>/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/<你的域名>/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { if ($host = www.<你的域名>) { return 301 https://$host$request_uri; } # managed by Certbot if ($host = <你的域名>) { return 301 https://$host$request_uri; } # managed by Certbot listen 80; server_name <你的域名> www.<你的域名>; return 404; # managed by Certbot }

注意替换所有的<你的IP地址>和<你的域名>为你所使用VPS的IPv4地址和你的域名。

测试Nginx配置

Terminal
sudo nginx -t

确保没有报错,那么重启Nginx

Terminal
sudo systemctl reload nginx

为了开启HTTPS,我们需要安装Certbot

Terminal
sudo apt install certbot python3-certbot-nginx -y

并且申请证书并自动修改Nginx

Terminal
sudo certbot --nginx -d <你的域名> -d www.<你的域名>

完成后, Nginx会自动生成443端口的配置,HTTP自动跳转到HTTPS。

这样你就可以通过<你的域名>访问搭建好的Upmath服务了!它还支持中文!大功告成!

当然也欢迎使用我目前搭建好的Upmath服务:https://tex.xumin.net