Skip to content

Repository files navigation

snapdf

Convert JavaScript-rendered web pages to PDF or plain text. Built for sites with virtual scrolling (ChatGPT, Claude, etc.) where the browser's native print fails because content isn't all in the DOM at once.

Instead of screenshotting, snapdf scrolls through the page collecting DOM nodes as virtual scroll renders them, reassembles them into a static document, and prints it with Chrome's native PDF engine — giving you a real text-based, searchable PDF.

Installation

npm install -g snapdf

CLI

snapdf <url> [output]

Options

FlagDescription
-t, --txtSave as plain text file instead of PDF
-p, --page-sizeletter or a4 (default: letter)
-m, --marginMargin in points, 72pt = 1in (default: 36)
-l, --landscapeLandscape orientation
-T, --timeoutPage load timeout in milliseconds (default: 60000)
-s, --selectorCSS selector for message elements (auto-detected for ChatGPT and Claude)
-H, --hide-user-inputOmit user messages from the output
-A, --hide-assistant-outputOmit assistant messages from the output

Examples

# Save a ChatGPT shared conversation to PDF
snapdf https://chatgpt.com/share/abc123
# Save a Claude shared conversation to PDF
snapdf https://claude.ai/share/abc123
# Custom output path
snapdf https://chatgpt.com/share/abc123 conversation.pdf
# Plain text
snapdf https://chatgpt.com/share/abc123 -t
# A4, 0.5in margins, landscape
snapdf https://chatgpt.com/share/abc123 -p a4 -m 36 -l
# Custom selector for other sites
snapdf https://example.com/thread -s "article.message"# Longer timeout for slow pages
snapdf https://chatgpt.com/share/abc123 -T 120000

If no output path is given, the filename is derived from the page title (e.g. crumby-app-discussion.pdf). If a name is given without an extension, the correct one is added automatically (conversationconversation.pdf). Existing files are never overwritten — snapdf increments the filename (output-1.pdf, output-2.pdf, etc.).

Programmatic API

npm install snapdf
import{fetchPdf,fetchTxt,typeFetchResult}from'snapdf'

fetchPdf(url, options?): Promise<FetchResult>

Returns { buffer: Buffer, title: string }.

const{ buffer, title }=awaitfetchPdf('https://chatgpt.com/share/abc123',{pageSize: 'letter',// 'letter' | 'a4'margin: 36,// points, 72pt = 1inlandscape: false,timeout: 60000,// mscookies: [{name: 'session',value: '...',domain: 'chatgpt.com'}],executablePath: '/path/to/chrome',args: ['--no-sandbox'],onProgress: (msg)=>console.log(msg),})awaitfs.writeFile(`${title}.pdf`,buffer)

fetchTxt(url, options?): Promise<string>

consttext=awaitfetchTxt('https://chatgpt.com/share/abc123',{timeout: 60000,cookies: [...],executablePath: '/path/to/chrome',args: ['--no-sandbox'],onProgress: (msg)=>console.log(msg),})

Types

interfaceFetchOptions{pageSize?: 'letter'|'a4'// PDF onlymargin?: number// PDF only, pointslandscape?: boolean// PDF onlytimeout?: number// ms, applies to page loadselector?: string// CSS selector for content nodescookies?: CookieParam[]// Puppeteer cookie objectsexecutablePath?: string// Path to Chrome binaryargs?: string[]// Puppeteer launch args (e.g. ['--no-sandbox'])onProgress?: (msg: string)=>void// Progress callbackhideUserInput?: boolean// Omit user messages from outputhideAssistantOutput?: boolean// Omit assistant messages from output}interfaceFetchResult{buffer: Buffer// PDF bytestitle: string// Page title, sanitized for use as a filename}

Express example

importexpressfrom'express'import{fetchPdf}from'snapdf'constapp=express()app.get('/pdf',async(req,res)=>{const{ url }=req.queryconst{ buffer }=awaitfetchPdf(String(url))res.setHeader('Content-Type','application/pdf')res.send(buffer)})

Cookies / Private pages

Pass session cookies to access pages behind a login:

const{ buffer }=awaitfetchPdf('https://chatgpt.com/c/private-thread',{cookies: [{name: '__Secure-next-auth.session-token',value: '...',domain: 'chatgpt.com'}]})

Serverless / Custom Chrome

Puppeteer bundles its own Chrome, which works on standard servers and locally. For serverless environments (Lambda, Vercel, etc.) use @sparticuz/chromium and pass its executable path:

importchromiumfrom'@sparticuz/chromium'import{fetchPdf}from'snapdf'const{ buffer }=awaitfetchPdf(url,{executablePath: awaitchromium.executablePath(),args: chromium.args,})

Other sites

ChatGPT and Claude share URLs are auto-detected — no selector needed. For other sites, pass a CSS selector that matches the repeating content nodes you want captured:

// Generic blog/articleawaitfetchPdf(url,{selector: 'article'})// Any custom formatawaitfetchPdf(url,{selector: '.message-bubble'})

About

Snap ChatGPT conversations to searchable PDF or plain text. Handles virtual scrolling

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages