Skip to main content
Glama

paperpress

회사의 브랜드를 라이브 URL에서 감지하는 API — 로고, 기본 색상, 폰트 — 그리고 마크다운을 해당 브랜드 스타일의 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 색상 샘플링, 점수 기반 로고 후보 추출, WCAG 대비 가드), 동기식 Fastify 렌더 파이프라인, SSRF 강화 URL 페처, 그리고 이를 감싸는 MCP 서버. 코드를 읽고, 포크하고, 실행하세요 — MIT 라이선스입니다. 제품으로 유지보수되지는 않습니다: 결제 처리는 연결되어 있지 않으며 MCP 패키지(mcp/)는 npm에 게시되지 않았습니다.

Related MCP server: Markitdown Universal MCP Server

작동 방식

단일 Fastify 프로세스가 세 가지 작업을 수행합니다:

  1. 감지 (src/render/detect.ts) — 풀링된 Playwright/Chromium 인스턴스로 대상 URL을 탐색하고, theme-color, 브랜드 CSS 커스텀 속성, CTA 버튼 색상, 위치/크기/형식으로 점수를 매긴 헤더 <img> 후보, 그리고 계산된 폰트 스택을 읽습니다. 흰색에 가까운/검은색에 가까운/회색에 가까운 노이즈를 필터링하고, WCAG 휘도 가드를 적용하여 너무 밝은 브랜드 색상이 텍스트 대비를 망치지 않도록 합니다.

  2. 렌더링 (src/render/) — unified/remark/rehype(allowDangerousHtml: false 사용)로 마크다운을 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 키

마크다운 → 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를 해석하고 호출자가 제출한 URL에서 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를 실행할 수 있습니다.

  • 마크다운 살균: remark-rehypeallowDangerousHtml: false로 실행되므로 마크다운 본문의 <script>는 제거됩니다.

  • 제한: 마크다운 ≤ 500KB, css ≤ 50KB, 본문 ≤ 2MB, 렌더링 ≤ 30초, 렌더링된 페이지 ≤ MAX_PAGES_PER_RENDER(기본 200), 요율 ≤ 60 요청/분/키.

  • 관리자 엔드포인트: 상수 시간 토큰 비교; 토큰이 잘못되었거나 설정되지 않은 경우 경로는 401이 아닌 404를 반환합니다.

알려진 제한 사항

제품이 아니므로 백로그로 추적하기보다 공개합니다:

  • 자동화된 테스트 스위트 없음. 위의 모든 것은 실행 중인 인스턴스에 대해 수동으로 검증되었습니다. 회귀 테스트 네트가 없습니다.

  • 커밋된 Prisma 마이그레이션 없음. 컨테이너 시작 명령은 prisma db push --skip-generate --accept-data-loss를 실행합니다.

  • 인메모리 요율 제한 및 감지 캐시. 단일 인스턴스에서는 문제없음; 둘 다 공유 저장소로 옮기지 않으면 여러 복제본에서 유지되지 않습니다.

  • 결제 처리 연결 없음. 크레딧 시스템은 스키마와 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.jsonstartCommand는 의도적으로 없습니다: Railway는 이를 argv(셸 아님)로 파싱하므로 연결된 && 명령이 실패합니다. DockerfileCMDsh -c로 감싸 전체 시작 시퀀스를 실행합니다.

  • 이메일: RESEND_API_KEY가 설정될 때까지 등록 키는 stdout에 기록됩니다. [email:console]을 검색하세요.

예시

이 모든 것은 samples/에 체크인되어 있습니다 — scripts/preview.tsscripts/detect.ts로 생성되었으며, npx tsx scripts/preview.ts / npx tsx scripts/detect.ts <url>로 직접 재생성할 수 있습니다.

동일한 마크다운, 다섯 가지 테마 (아래는 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.

위에서 사용된 입력 마크다운: sample.md, demo-q4-review.md(/v1/demo에서 사용하는 것).

상태

구축됨: 5가지 테마에서 markdown → PDF, URL에서 브랜드 키트 자동 감지(단순한 serif|sans|mono 버킷이 아닌 실제 font-family 스택), 24시간 감지 캐시, brandFromUrl 원콜 단축키, 배치 감지(20개 URL 병렬), WCAG 휘도 가드, 이메일 기반 키 발급(교체 유예 포함), MCP 서버, 읽기 전용 관리자 표면, 서명된 공유 URL, 사전 렌더링된 문서.

의도적으로 구축하지 않음: 결제 처리, MCP 패키지의 npm 게시, 자동화된 테스트, 실제 Prisma 마이그레이션. 이것은 라이브 백로그가 아닙니다 — 포트폴리오 작품으로 완성된 것이며, 1.0을 향해 개발 중이 아닙니다.

라이선스

MIT — LICENSE 참조.

A
license - permissive license
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    Turn markdown into designed PDFs with cover page, table of contents, and code blocks that hold across pages. One command from Claude Desktop, Claude Code, Cursor, Cline, Zed, or any MCP-capable client.
    2
    38
    1
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides a standardized interface for interacting with Markitdown's tools and services through a unified API, compatible with MCP-compliant services.
    MIT

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rozetyp/paperpress'

If you have feedback or need assistance with the MCP directory API, please join our Discord server