Skip to main content
Glama

paperpress

一个API,可从公司的实时URL检测其品牌——标志、主色、字体——并将markdown渲染成以该品牌风格排版的PDF,一次HTTP调用即可完成。以纯REST API和MCP服务器两种形式提供。

POST /v1/documents
{ "markdown": "# Q4 report\n...", "brandFromUrl": "stripe.com" }
→ ~1s → signed URL to a PDF in Stripe's brand

关于本项目

这个项目曾被实际构建并短暂作为部署服务运行,之后我仔细审视了它需要竞争的领域:Claude现在原生支持PDF/PPTX/DOCX生成,而Brandfetch已经在销售专门用于为AI代理提供上下文的Brand Context API,并且拥有真实的付费客户。这个项目所做的两件事——检测品牌、渲染文档——如今要么已接近商品化,要么已被有资金支持的竞争对手占据。我不再将其作为产品推进。

它作为作品集/参考实现公开:一个基于Playwright的品牌检测器(CSS自定义属性、CTA按钮颜色采样、评分式logo候选提取、WCAG对比度保护)、一个同步Fastify渲染流水线、一个经过SSRF加固的URL抓取器,以及一个包装它的MCP服务器。可阅读代码、fork、运行——采用MIT许可证。它不作为产品维护:未接入支付处理,mcp/包也未发布到npm。

Related MCP server: docjet-mcp

工作原理

一个Fastify进程完成三件事:

  1. 检测(src/render/detect.ts)——使用池化的Playwright/Chromium实例导航目标URL,读取theme-color、品牌CSS自定义属性、CTA按钮颜色、按位置/大小/格式评分的<img>候选元素,以及计算出的字体栈。过滤接近白色/接近黑色/接近灰色的噪声,应用WCAG亮度保护,避免过浅的品牌色破坏文字对比度。

  2. 渲染(src/render/)——通过unified/remark/rehype(allowDangerousHtml: false)将markdown转换为HTML,应用五种主题之一以及检测到/提供的品牌套件,并使用Playwright打印为PDF。

  3. 服务——PDF输出到本地磁盘(或挂载的卷),通过HMAC签名、限时URL访问。

无队列、无工作进程、无Redis。渲染为同步操作,Chromium预热后通常耗时100–400ms;检测结果按主机缓存24小时。

项目内容

.
├── src/                       Fastify API (single process)
│   ├── index.ts               Bootstrap, route registration
│   ├── env.ts                 Env validation (zod)
│   ├── lib/                   prisma, auth, billing, storage, email, url-fetch (SSRF guard), inline-image
│   ├── render/                markdown → HTML → PDF (themes/, detect.ts)
│   └── routes/                auth, documents, demo, account, pdf, brand-kits, admin
├── prisma/schema.prisma       5 models: User, ApiKey, Document, CreditTransaction, BrandKit
├── mcp/                       MCP server (unpublished — see mcp/README.md)
├── samples/                   Example output (see Examples below) + input markdown used to generate it
├── scripts/                   preview.ts / detect.ts — regenerate the samples/ output locally
├── Dockerfile                 Single-image deploy (Playwright base)
└── railway.json               Railway config (healthcheck only — start cmd is in Dockerfile)

API接口(v1)

认证

方法

路径

认证

功能

POST

/auth/register

-

通过邮箱请求密钥。始终返回202;密钥发送至收件箱。首次调用 = 新用户 + 免费额度。后续调用 = 轮换(旧密钥有效24小时,之后撤销)。

渲染

方法

路径

认证

说明

POST

/v1/documents

Bearer密钥

Markdown → PDF。接受theme、brandKit(保存的名称或内联)、brandFromUrl(检测并应用的快捷方式)、css、format、landscape、title。返回签名URL。若渲染页数超过MAX_PAGES_PER_RENDER(默认200)则返回413。

GET

/pdf/:id?exp=&sig=

签名URL

流式传输PDF

品牌套件

方法

路径

认证

说明

POST

/v1/brand-kits

Bearer密钥

按名称创建/更新保存的套件(每个用户每个名称一个)

GET

/v1/brand-kits

Bearer密钥

列出你的套件

GET

/v1/brand-kits/:id

Bearer密钥

读取单个套件

DELETE

/v1/brand-kits/:id

Bearer密钥

删除套件

POST

/v1/brand-kits/detect

Bearer密钥

传入URL,返回primaryColor、logoUrl、favicon、fontFamily、fontStyle。缓存24小时。传入{ refresh: true }可绕过缓存。

POST

/v1/brand-kits/detect-batch

Bearer密钥

最多20个URL并行通过现有Playwright池。单项错误内联返回。

账户 / 健康检查

方法

路径

认证

说明

GET

/account

Bearer密钥

邮箱、额度、活动API密钥列表(明文,便于用户找回)

GET

/health

-

{ status: 'ok' }

演示(匿名,受限)

无需认证,不消耗额度。在全局60次/分钟限制之上,另有严格的每IP速率限制(30次/小时)。

方法

路径

说明

POST

/v1/demo

{ url } → 检测品牌(缓存)+ 将内置的samples/demo-q4-review.md渲染为PDF。返回套件 + 签名URL。

管理(只读)

由X-Admin-Token请求头控制。当ADMIN_TOKEN未设置时,所有/admin/*路由返回404——无表面、无发现。

方法

路径

说明

GET

/admin/stats

总计:用户、活动密钥、文档、页数、字节数、额度、最近24小时/7天的文档数

GET

/admin/users

分页列表,含每个用户的文档和密钥数量

GET

/admin/documents

分页列表,关联用户邮箱。可按userId、status过滤。

GET

/admin/documents/:id/pdf

无需签名URL即可流式传输任意PDF

安全态势

  • API密钥:192位随机,明文存储(以便/account显示);撤销使用revokedAt时间戳,并设有宽限期。

  • 签名URL:HMAC-SHA256,exp + sig查询参数,默认TTL为7天。

  • SSRF防护:assertPublicUrl解析DNS,并拒绝RFC1918、环回、链路本地、IPv6 ULA地址。应用于brandKit.logoUrl和/v1/brand-kits/detect。提交的主机为公网并不能保证每一跳都是公网——公网主机可重定向到私有地址——因此Playwright导航路径(src/render/detect.ts)和图片内联抓取(src/lib/inline-image.ts)都会在每次重定向跳转前重新验证地址,并在最终导航后再次验证。这缩小了攻击窗口,但并未完全消除:对重定向目标的初始连接发生在重新检查拒绝之前,因此即使被拒绝的目标从未被提取或渲染任何页面内容,有决心的攻击者仍可导致一次盲出站请求(不会向攻击者返回任何响应数据)。完全闭合需要在网络层进行IP固定。

  • CSS注入防护:css字段拒绝<style>、</style>、<script>、</script>——否则在<style>${css}</style>内部的原始嵌入会让攻击者逃逸并在Chromium池中运行JS。

  • Markdown净化:remark-rehype以allowDangerousHtml: false运行,因此markdown正文中的<script>会被剥离。

  • 限制:markdown ≤ 500KB,css ≤ 50KB,正文 ≤ 2MB,渲染 ≤ 30s,渲染页数 ≤ MAX_PAGES_PER_RENDER(默认200),速率 ≤ 60次/分钟/密钥。

  • 管理端点:恒定时间令牌比较;当令牌错误或未设置时,路由返回404(而非401)。

已知限制

不是产品,因此这些是披露而非作为待办事项跟踪:

  • 无自动化测试套件。 上述所有内容均通过手动验证运行实例;没有回归测试网络。

  • 无已提交的Prisma迁移。 容器启动命令运行prisma db push --skip-generate --accept-data-loss。

  • 内存速率限制和检测缓存。 单实例没问题;若需多副本,需将两者迁移到共享存储。

  • 未接入支付处理。 额度系统存在于schema和API中;没有信用卡计费。

  • MCP包(mcp/)未发布到npm,也不打算发布——见mcp/README.md。

本地开发

# 1. Postgres running locally on 5432
# 2. Env
cp .env.example .env
# (set SIGNING_SECRET to `openssl rand -base64 32`)

# 3. Install + migrate
npm install
npx prisma migrate dev

# 4. Run
npm run dev

冒烟测试:

# Request a key. Response is { "sent": true } - the key arrives by email.
# In dev (RESEND_API_KEY unset) the server logs the email to stdout; grab the
# key from there.
curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com"}'

export PP_KEY="pp_live_..."

# Render
curl -X POST http://localhost:3000/v1/documents \
  -H "Authorization: Bearer $PP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"markdown":"# Hello\n\nWorld.","title":"Test"}'

MCP

见mcp/README.md。一个基于REST API的轻量MCP客户端。未发布到npm——作为参考代码而非可安装工具。

部署(Railway示例)

实际部署此项目时使用的确切步骤——此处作为文档保留,而非邀请在生产环境中运行。

# 1. Create project with a Postgres database
railway init --name paperpress
railway add --database postgres

# 2. Create the app service. DATABASE_URL is wired via service reference.
railway add --service paperpress \
  --variables "DATABASE_URL=\${{Postgres.DATABASE_URL}}" \
  --variables "SIGNING_SECRET=$(openssl rand -base64 32)" \
  --variables "PUBLIC_BASE_URL=https://your-app.up.railway.app" \
  --variables "STORAGE_DIR=/data/storage" \
  --variables "NODE_ENV=production" \
  --variables "FREE_TIER_CREDITS=100" \
  --variables "PLAYWRIGHT_MAX_CONTEXTS=2" \
  --variables "RENDER_TIMEOUT_MS=30000" \
  --variables "KEY_GRACE_PERIOD_HOURS=24" \
  --variables "MAX_PAGES_PER_RENDER=200" \
  --variables "ADMIN_TOKEN=$(openssl rand -base64 36 | tr -d '\n')"

# 3. Attach a volume so PDFs survive container restarts
railway service paperpress
railway volume add --mount-path /data/storage

# 4. Domain (auto-detects the container port)
railway domain --port 3000

# 5. Ship
railway up --detach -c

注意事项:

  • Dockerfile使用mcr.microsoft.com/playwright:vX.Y-jammy作为基础镜像。保持该版本与playwright npm包同步——不匹配会导致浏览器二进制文件不存在,渲染失败。

  • railway.json中的startCommand有意省略:Railway将其解析为argv(而非shell),因此链式&&命令会失败。Dockerfile中的CMD使用sh -c包装并运行完整启动序列。

  • 邮件:在设置RESEND_API_KEY之前,注册密钥会记录到stdout。搜索[email:console]。

示例

所有这些都已提交到samples/——由scripts/preview.ts和scripts/detect.ts生成,可使用npx tsx scripts/preview.ts / npx tsx scripts/detect.ts <url>自行重新生成。

同一markdown,五种主题(下方展示clean——完整PDF):

clean主题示例

从实时URL自动检测品牌(brandFromUrl: "stripe.com"——完整PDF):

stripe品牌检测示例

来源 URL

检测 + 渲染

github.com

detect-github-com.pdf

railway.com

detect-railway-com.pdf

vercel.com

detect-vercel-com.pdf

内联品牌套件(无 URL,字段直接在请求中传递)— forest、mono-coral、stripe-colors。

上面使用的输入 markdown:sample.md、demo-q4-review.md(/v1/demo 使用的那份)。

状态

已构建:markdown → PDF,支持 5 种主题;从 URL 自动检测品牌套件(真实的字体族栈,而不仅仅是一个 serif|sans|mono 类别);24 小时检测缓存;brandFromUrl 单次调用快捷方式;批量检测(并行 20 个 URL);WCAG 亮度校验;基于电子邮件的密钥签发,带轮换宽限期;一个 MCP 服务器;只读管理界面;签名分享 URL;预渲染文档。

刻意未构建:支付处理、MCP 包的 npm 发布、自动化测试、真正的 Prisma 迁移。这不是一个活跃的待办事项列表 — 它作为作品集项目已经完成,并不是在朝着 1.0 版本开发。

许可证

MIT — 参见 LICENSE。

Related MCP Connectors

Related MCP Servers