import { Router, Request, Response } from "express"; import rateLimit from "express-rate-limit"; import { renderPdf } from "../services/browser.js"; import { markdownToHtml, wrapHtml } from "../services/markdown.js"; import logger from "../services/logger.js"; import { sanitizeFilename } from "../utils/sanitize.js"; import { validatePdfOptions } from "../utils/pdf-options.js"; const router = Router(); const WATERMARK_SVG = `DEMO — docfast.dev`; const WATERMARK_BG = `data:image/svg+xml,${encodeURIComponent(WATERMARK_SVG)}`; const WATERMARK_HTML = `
Generated by DocFast — docfast.dev | Upgrade to Pro for clean PDFs
`; function injectWatermark(html: string): string { if (html.includes("")) { return html.replace("", WATERMARK_HTML + ""); } return html + WATERMARK_HTML; } // 5 requests per hour per IP const demoLimiter = rateLimit({ windowMs: 60 * 60 * 1000, max: 5, message: { error: "Demo limit reached (5/hour). Get a Pro API key at https://docfast.dev for unlimited access." }, standardHeaders: true, legacyHeaders: false, keyGenerator: (req) => req.ip || req.socket.remoteAddress || "unknown", }); router.use(demoLimiter); interface DemoBody { html?: string; markdown?: string; css?: string; format?: string; landscape?: boolean; margin?: { top?: string; right?: string; bottom?: string; left?: string }; printBackground?: boolean; filename?: string; } /** * @openapi * /v1/demo/html: * post: * tags: [Demo] * summary: Convert HTML to PDF (demo) * description: | * Public endpoint — no API key required. Rate limited to 5 requests per hour per IP. * Output PDFs include a DocFast watermark. Upgrade to Pro for clean output. * requestBody: * required: true * content: * application/json: * schema: * allOf: * - type: object * required: [html] * properties: * html: * type: string * description: HTML content to convert * example: '

Hello World

My first PDF

' * css: * type: string * description: Optional CSS to inject (used when html is a fragment) * - $ref: '#/components/schemas/PdfOptions' * responses: * 200: * description: Watermarked PDF document * content: * application/pdf: * schema: * type: string * format: binary * 400: * description: Missing html field * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 415: * description: Unsupported Content-Type * 429: * description: Demo rate limit exceeded (5/hour) * content: * application/json: * schema: * $ref: '#/components/schemas/Error' * 503: * description: Server busy * 504: * description: PDF generation timed out */ router.post("/html", async (req: Request, res: Response) => { let slotAcquired = false; try { const ct = req.headers["content-type"] || ""; if (!ct.includes("application/json")) { res.status(415).json({ error: "Unsupported Content-Type. Use application/json." }); return; } const body: DemoBody = typeof req.body === "string" ? { html: req.body } : req.body; if (!body.html) { res.status(400).json({ error: "Missing 'html' field" }); return; } const validation = validatePdfOptions(body); if (!validation.valid) { res.status(400).json({ error: validation.error }); return; } if (req.acquirePdfSlot) { await req.acquirePdfSlot(); slotAcquired = true; } const fullHtml = body.html.includes(" { let slotAcquired = false; try { const ct = req.headers["content-type"] || ""; if (!ct.includes("application/json")) { res.status(415).json({ error: "Unsupported Content-Type. Use application/json." }); return; } const body: DemoBody = typeof req.body === "string" ? { markdown: req.body } : req.body; if (!body.markdown) { res.status(400).json({ error: "Missing 'markdown' field" }); return; } const validation = validatePdfOptions(body); if (!validation.valid) { res.status(400).json({ error: validation.error }); return; } if (req.acquirePdfSlot) { await req.acquirePdfSlot(); slotAcquired = true; } const htmlContent = markdownToHtml(body.markdown); const fullHtml = injectWatermark(wrapHtml(htmlContent, body.css)); const defaultOpts = { format: "A4" as const, landscape: false, printBackground: true, margin: { top: "0", right: "0", bottom: "0", left: "0" }, }; const { pdf, durationMs } = await renderPdf(fullHtml, { ...defaultOpts, ...validation.sanitized, }); const filename = sanitizeFilename(body.filename || "demo.pdf"); res.setHeader("Content-Type", "application/pdf"); res.setHeader("Content-Disposition", `attachment; filename="${filename}"`); res.setHeader("Content-Length", pdf.length); res.setHeader("X-Render-Time", String(durationMs)); res.send(pdf); } catch (err: any) { if (err.message === "QUEUE_FULL") { res.status(503).json({ error: "Server busy. Please try again in a moment." }); } else if (err.message === "PDF_TIMEOUT") { res.status(504).json({ error: "PDF generation timed out." }); } else { logger.error({ err }, "Demo markdown conversion failed"); res.status(500).json({ error: "PDF generation failed." }); } } finally { if (slotAcquired && req.releasePdfSlot) req.releasePdfSlot(); } }); export { router as demoRouter };