Skip to main content
Glama

paperpress

Eine API, die die Marke eines Unternehmens anhand seiner Live-URL erkennt – Logo, Primärfarbe, Schriftart – und Markdown in einem einzigen HTTP-Aufruf in ein PDF rendert, das im Stil dieser Marke gestaltet ist. Erhältlich als reine REST-API und als MCP-Server.

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

Über dieses Projekt

Dies wurde als echter, bereitgestellter Dienst gebaut und kurz betrieben, bevor ich mir den Markt genau ansah, in dem es konkurrieren müsste: Claude bietet jetzt native PDF/PPTX/DOCX-Erzeugung an, und Brandfetch verkauft bereits eine Brand Context API, die speziell für die Verankerung von KI-Agenten entwickelt wurde, mit echten zahlenden Kunden. Beide Hälften dessen, was dies tut – eine Marke erkennen, ein Dokument rendern – sind inzwischen quasi Commodity oder bereits im Besitz eines finanzierten Konkurrenten. Ich verfolge dies nicht als Produkt.

Es ist öffentlich als Portfolio-Stück / Referenzimplementierung: ein funktionierender, auf Playwright basierender Markendetektor (CSS-Custom-Properties, CTA-Farbabtastung, bewertete Logo-Kandidatenextraktion, WCAG-Kontrastschutz), eine synchrone Fastify-Render-Pipeline, ein SSRF-gehärteter URL-Fetcher und ein MCP-Server, der dies umschließt. Lies den Code, forke ihn, führe ihn aus – er ist MIT-lizenziert. Er wird nicht als Produkt gepflegt: Es ist keine Zahlungsabwicklung eingerichtet und das MCP-Paket (mcp/) ist nicht auf npm veröffentlicht.

Related MCP server: Markitdown Universal MCP Server

So funktioniert es

Ein einzelner Fastify-Prozess erledigt drei Dinge:

  1. Erkennen (src/render/detect.ts) – navigiert zur Ziel-URL mit einer gepoolten Playwright/Chromium-Instanz, liest theme-color, Marken-CSS-Custom-Properties, CTA-Button-Farben, Header-<img>-Kandidaten, die nach Position/Größe/Format bewertet werden, und berechnete Font-Stacks. Filtert nahezu weißes/schwarzes/graues Rauschen, wendet einen WCAG-Luminanzschutz an, damit eine zu helle Markenfarbe den Textkontrast nicht sprengt.

  2. Rendern (src/render/) – wandelt Markdown über unified/remark/rehype in HTML um (mit allowDangerousHtml: false), wendet eines von fünf Themes plus das erkannte/übergebene Brand Kit an und druckt mit Playwright als PDF.

  3. Bereitstellen – PDFs werden auf die lokale Festplatte (oder ein eingebundenes Volume) gelegt, hinter HMAC-signierten, zeitlich begrenzten URLs.

Keine Warteschlange, kein Worker-Prozess, kein Redis. Renderings sind synchron und dauern typischerweise 100–400 ms, sobald Chromium warm ist; die Erkennung wird 24 Stunden pro Host zwischengespeichert.

Was hier enthalten ist

.
├── 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-Oberfläche (v1)

Authentifizierung

Methode

Pfad

Auth

Was es tut

POST

/auth/register

-

Schlüssel per E-Mail anfordern. Immer 202; Schlüssel wird an das Postfach gesendet. Beim ersten Mal = neuer Benutzer + kostenlose Credits. Bei späteren Aufrufen = Rotation (alte Schlüssel 24h gültig, dann widerrufen).

Rendern

Methode

Pfad

Auth

Was es tut

POST

/v1/documents

Bearer key

Markdown → PDF. Akzeptiert theme, brandKit (gespeicherter Name oder inline), brandFromUrl (Verknüpfung zum Erkennen und Anwenden), css, format, landscape, title. Gibt signierte URL zurück. 413, wenn gerenderte Seiten > MAX_PAGES_PER_RENDER (Standard 200).

GET

/pdf/:id?exp=&sig=

signierte URL

PDF streamen

Brand Kits

Methode

Pfad

Auth

Was es tut

POST

/v1/brand-kits

Bearer key

Erstellt/aktualisiert ein gespeichertes Kit nach Namen (eines pro Benutzer und Name)

GET

/v1/brand-kits

Bearer key

Listet deine Kits auf

GET

/v1/brand-kits/:id

Bearer key

Liest ein Kit

DELETE

/v1/brand-kits/:id

Bearer key

Löscht ein Kit

POST

/v1/brand-kits/detect

Bearer key

Übergib eine URL, erhalte primaryColor, logoUrl, favicon, fontFamily, fontStyle zurück. 24h gecacht. Übergib { refresh: true }, um zu umgehen.

POST

/v1/brand-kits/detect-batch

Bearer key

Bis zu 20 URLs parallel über den vorhandenen Playwright-Pool. Fehler pro Element werden inline zurückgegeben.

Konto / Gesundheit

Methode

Pfad

Auth

Was es tut

GET

/account

Bearer key

E-Mail, Credits, Liste der aktiven API-Schlüssel (Klartext, damit Benutzer sie wiederherstellen können)

GET

/health

-

{ status: 'ok' }

Demo (anonym, eingeschränkt)

Keine Authentifizierung, keine Credits werden berechnet. Strenge Pro-IP-Ratenbegrenzung (30/Stunde) zusätzlich zur globalen 60/min.

Methode

Pfad

Was es tut

POST

/v1/demo

{ url } → Marke erkennen (gecacht) + das gebündelte samples/demo-q4-review.md als PDF rendern. Gibt Kit + signierte URL zurück.

Admin (nur lesen)

Geschützt durch den X-Admin-Token-Header. Wenn ADMIN_TOKEN nicht gesetzt ist, gibt jede /admin/*-Route 404 zurück – keine Oberfläche, keine Entdeckung.

Methode

Pfad

Was es tut

GET

/admin/stats

Summen: Benutzer, aktive Schlüssel, Dokumente, Seiten, Bytes, Credits, Dokumentanzahl für letzte 24h / 7d

GET

/admin/users

Paginiert Liste mit Dokument- und Schlüsselanzahl pro Benutzer

GET

/admin/documents

Paginiert Liste mit verknüpfter Benutzer-E-Mail. Filter nach userId, status.

GET

/admin/documents/:id/pdf

Beliebiges PDF ohne signierte URL streamen

Sicherheitslage

  • API-Schlüssel: 192-Bit zufällig, als Klartext gespeichert (damit /account sie anzeigen kann); Widerruf verwendet einen revokedAt-Zeitstempel mit einem Gnadenfenster.

  • Signierte URLs: HMAC-SHA256, exp + sig-Abfrageparameter, Standard-TTL 7 Tage.

  • SSRF-Schutz: assertPublicUrl löst DNS auf und lehnt RFC1918, Loopback, Link-Local, IPv6-ULA auf der vom Aufrufer übermittelten URL ab. Angewendet auf brandKit.logoUrl und /v1/brand-kits/detect. Dass der übermittelte Host öffentlich ist, garantiert nicht, dass jeder Hop es ist – ein öffentlicher Host kann auf eine private Adresse umleiten – daher validieren sowohl der Playwright-Navigationspfad (src/render/detect.ts) als auch der Bild-Inlining-Fetch (src/lib/inline-image.ts) die Adresse bei jedem Redirect-Hop erneut, bevor sie ihr folgen, und erneut nach der endgültigen Navigation. Dies verengt das Fenster, beseitigt es aber nicht vollständig: Die anfängliche Verbindung zu einem Redirect-Ziel erfolgt, bevor die erneute Prüfung es ablehnen kann, sodass ein entschlossener Angreifer immer noch eine blinde ausgehende Anfrage verursachen kann (keine Antwortdaten werden an ihn zurückgegeben), obwohl niemals Seiteninhalt aus einem abgelehnten Ziel extrahiert oder gerendert wird. Eine vollständige Schließung würde IP-Pinning auf Netzwerkebene erfordern.

  • CSS-Injektionsschutz: Das css-Feld lehnt <style>, </style>, <script>, </script> ab – andernfalls würde das rohe Einbetten innerhalb von <style>${css}</style> einem Angreifer ermöglichen, auszubrechen und JS im Chromium-Pool auszuführen.

  • Markdown-Bereinigung: remark-rehype läuft mit allowDangerousHtml: false, sodass <script> in Markdown-Textkörpern entfernt wird.

  • Grenzen: Markdown ≤ 500KB, CSS ≤ 50KB, Body ≤ 2MB, Rendern ≤ 30s, gerenderte Seiten ≤ MAX_PAGES_PER_RENDER (Standard 200), Rate ≤ 60 Anfragen/min/Schlüssel.

  • Admin-Endpunkt: Konstantzeit-Token-Vergleich; Routen geben 404 (nicht 401) zurück, wenn Token falsch oder nicht gesetzt.

Bekannte Einschränkungen

Kein Produkt, daher werden diese offengelegt und nicht als Backlog verfolgt:

  • Keine automatisierte Testsuite. Alles oben Genannte wurde von Hand gegen eine laufende Instanz verifiziert; es gibt kein Regressionsnetz.

  • Keine eingecheckten Prisma-Migrationen. Der Container-Startbefehl führt prisma db push --skip-generate --accept-data-loss aus.

  • In-Memory-Ratenbegrenzung und Erkennungs-Cache. Für eine einzelne Instanz in Ordnung; würde mehrere Replikate nicht überleben, ohne beide auf etwas Gemeinsames zu verschieben.

  • Keine Zahlungsabwicklung eingerichtet. Das Creditsystem existiert im Schema und in der API; nichts belastet eine Karte.

  • Das MCP-Paket (mcp/) ist nicht auf npm veröffentlicht und wird es auch nicht – siehe mcp/README.md.

Lokale Entwicklung

# 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

Smoke-Test:

# 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

Siehe mcp/README.md. Ein dünner MCP-Client über die REST-API. Nicht auf npm veröffentlicht – als Referenzcode enthalten, nicht als installierbares Werkzeug.

Bereitstellung (Railway-Beispiel)

Die genauen Schritte, die für die echte Bereitstellung verwendet wurden, unter der dies lief – hier als Dokumentation aufbewahrt, nicht als Einladung, dies in Produktion zu betreiben.

# 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

Hinweise:

  • Das Dockerfile verwendet mcr.microsoft.com/playwright:vX.Y-jammy als Basis. Halte diese Version im Gleichschritt mit dem playwright-npm-Paket – eine Abweichung bedeutet, dass die Browser-Binärdatei nicht existiert und Renderings fehlschlagen.

  • Der startCommand in railway.json ist absichtlich nicht vorhanden: Railway parst ihn als argv (nicht als Shell), daher schlagen verkettete &&-Befehle fehl. Das CMD im Dockerfile umschließt mit sh -c und führt die vollständige Startsequenz aus.

  • E-Mails: Bis RESEND_API_KEY gesetzt ist, werden Registrierungsschlüssel auf stdout protokolliert. Grep nach [email:console].

Beispiele

Alle diese sind in samples/ eingecheckt – generiert von scripts/preview.ts und scripts/detect.ts, generiere sie selbst mit npx tsx scripts/preview.ts / npx tsx scripts/detect.ts <url>.

Gleiches Markdown, fünf Themes (clean unten gezeigt – vollständiges PDF):

clean theme example

Marke automatisch von einer Live-URL erkannt (brandFromUrl: "stripe.com"vollständiges PDF):

stripe brand-detected example

Quell-URL

Erkannt + gerendert

github.com

detect-github-com.pdf

railway.com

detect-railway-com.pdf

vercel.com

detect-vercel-com.pdf

Inline-Brand-Kits (keine URL, Felder werden direkt in der Anfrage übergeben) — forest, mono-coral, stripe-colors.

Eingabe-Markdown oben verwendet: sample.md, demo-q4-review.md (das von /v1/demo verwendete).

Status

Gebaut: Markdown → PDF in 5 Themes, Brand-Kit-Autoerkennung von einer URL (echter font-family-Stack, nicht nur ein serif|sans|mono-Bucket), 24h-Erkennungscache, brandFromUrl-Ein-Aufruf-Verknüpfung, Batch-Erkennung (20 URLs parallel), WCAG-Luminanzschutz, E-Mail-basierte Schlüsselausstellung mit Rotationsfrist, ein MCP-Server, eine schreibgeschützte Admin-Oberfläche, signierte Freigabe-URLs, vorgerenderte Dokumentation.

Bewusst nicht gebaut: Zahlungsabwicklung, npm-Publish des MCP-Pakets, automatisierte Tests, echte Prisma-Migrationen. Das ist kein aktives Backlog — es ist als Portfolio-Stück abgeschlossen und wird nicht auf eine 1.0 hin entwickelt.

Lizenz

MIT — siehe 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