Convert URLs to PDFs using Cloudflare Browser Rendering.
┌──────────────────┐
│ Client Request │
│ GET /?url=... │
└────────┬─────────┘
│
▼
┌───────────────┐
│ Hono Router │
│ (src/index.ts) │
└───────┬───────┘
│
┌────────────┴────────────┐
│ │
(no url) (url present)
│ │
▼ ▼
┌──────────────┐ ┌───────────────┐
│ Metadata │ │ R2 Cache │
│ (JSON) │ │ Check │
└──────────────┘ └───┬───────┬───┘
│ │
(hit)│ │(miss)
│ ▼
│ ┌──────────────────────┐
│ │ WORKFLOW.create() │
│ │ Idempotent via md5 │
│ └──────────┬───────────┘
│ │
│ ▼
│ ┌──────────────────────┐
│ │ Url2PdfWorkflow │
│ │ (src/workflows/) │
│ │ │
│ │ Step 1: generate-pdf │
│ │ - BROWSER.fetch() │
│ │ - BUCKET.put() │
│ │ (3 retries) │
│ │ │
│ │ Step 2: verify-stored│
│ │ - BUCKET.head() │
│ └──────────┬───────────┘
│ │
│ ▼
│ ┌──────────────────────┐
│ │ R2 Store │
│ │ (cache for future) │
│ └──────────────────────┘
│
│ ┌──────────────────────────┐
│ │ Auto-polling HTML page │
│ │ polls /?url= every 2s │
│ └──────────┬───────────────┘
│ │
│ ┌───────┴────────┐
│ │ PDF ready? │
│ └───┬───┬────────┘
│ (no) │ │ (yes)
│ ┌────┘ │
│ ▼ │
│ poll again │ window.location.href
│ (loop) │ → PDF served from R2
│ │
◄──────────────┘
│
▼
┌───────────────┐
│ Response │
│ PDF bytes │
└───────────────┘
- Request:
GET /?url=https://example.com - Validation: URL is validated (must be http or https)
- Cache check: Worker looks up the URL key in R2 bucket
- Cache hit: Returns the PDF bytes directly from R2
- Cache miss: Worker creates a Workflow instance (idempotent via md5 URL hash)
- Workflow Step 1 (generate-pdf): Browser Rendering opens the URL, generates PDF, stores in R2 (with 3 retries, exponential backoff)
- Workflow Step 2 (verify-stored): Confirms the PDF exists in R2
- Auto-polling HTML: Returns an HTML page with a loading spinner and JavaScript that polls
/?url=<url>every 2 seconds - PDF delivered: When the poll detects a PDF response (content-type
application/pdf), the browser redirects to the same URL — no manual refresh needed
bun install
bun run devbun run deployDeploy automatically via CI on push to main — see deploy.yml.
bun run test # Run all tests
bun run test:watch # Watch modeSet DEPLOYED_URL to the worker URL and run the integration tests against it:
DEPLOYED_URL=https://url2pdf.rjoydip.workers.dev bun testThe smoke tests confirm:
GET /returns service metadata (200 + JSON body)GET /?url=<valid>returns 200 (PDF, or HTML processing page)GET /?url=<invalid>returns 400
# Check service metadata
curl https://url2pdf.rjoydip.workers.dev/# Generate a PDF (first call triggers workflow; poll until cached)
curl "https://url2pdf.rjoydip.workers.dev/?url=https://example.com" \
--output example.pdf# After the PDF is cached, subsequent calls return immediately:
curl "https://url2pdf.rjoydip.workers.dev/?url=https://example.com" \
--output example.pdfReturns service metadata as JSON when no url parameter is provided.
| Status | Description |
|---|---|
200 |
JSON object with service info |
Example:
curl https://url2pdf.rjoydip.workers.dev/{
"name": "url2pdf-workflow",
"version": "1.0.0",
"description": "Convert URLs to PDFs using Cloudflare Browser Rendering",
"source": "https://github.com/rjoydip/url2pdf-workflow",
"endpoints": {
"/": "Service metadata (this response)",
"/?url=<url>": "Convert a URL to PDF"
}
}Converts a URL to PDF.
| Param | Required | Description |
|---|---|---|
url |
Yes | The URL to convert (must be http or https) |
Responses
| Status | Description |
|---|---|
200 |
PDF body (cached) or HTML auto-polling page (workflow in progress) |
400 |
Missing, empty, or malformed URL |
500 |
Internal error (PDF generation failure, etc.) |
curl "https://url2pdf.rjoydip.workers.dev/?url=https://example.com" \
--output example.pdfGenerated PDFs are cached in an R2 bucket using a normalized URL as the key (trailing slashes stripped, query parameters sorted). Subsequent requests for the same URL first check the cache before creating a new Workflow instance.
- Cloudflare Workers account with a paid plan (Browser Rendering requires Workers Paid)
- R2 bucket named
url2pdf-bucket - Wrangler CLI installed and authenticated
- Bun >= 1.3
| Script | Description |
|---|---|
bun run dev |
Start local dev server |
bun run deploy |
Deploy to Cloudflare Workers |
bun run test |
Run tests with bun |
bun run test:watch |
Run tests in watch mode |
bun run lint |
Lint with oxlint |
bun run lint:fix |
Lint and fix + format |
bun run format |
Format with oxfmt |
bun run format:check |
Check formatting |
bun run typecheck |
Type-check with tsc --noEmit |
- Runtime: Cloudflare Workers
- Framework: Hono
- Linter: oxlint
- Formatter: oxfmt
- Testing: bun test
- Git hooks: simple-git-hooks
| Resource | Binding | Description |
|---|---|---|
| Browser Rendering | BROWSER |
Remote browser for PDF generation |
| R2 Bucket | BUCKET |
Cache for generated PDFs (url2pdf-bucket) |
| Workflow | WORKFLOW |
Durable PDF generation (Url2PdfWorkflow) |
├── src/
│ ├── index.ts # Worker entry point and request handler
│ ├── index.test.ts # Unit tests for HTTP handler
│ ├── integration.test.ts # Integration and deployment smoke tests
│ ├── env.d.ts # Global type declarations (Bindings)
│ └── workflows/
│ ├── url2pdf.ts # Workflow definition (Url2PdfWorkflow)
│ └── url2pdf.test.ts # Unit tests for workflow class
├── .github/
│ └── workflows/
│ ├── ci.yml # CI pipeline (lint, typecheck, format, test)
│ ├── deploy.yml # Deploy to Cloudflare Workers
│ ├── fallow.yml # Code analysis
│ └── pinact.yml # GitHub Actions pin verification
├── AGENTS.md # AI agent instructions
├── wrangler.jsonc # Cloudflare Worker configuration
├── tsconfig.json # TypeScript configuration
└── package.json # Dependencies and scripts