Export your ChatGPT and Google Gemini conversations as clean, readable Markdown, HTML, or print-ready PDF files — with faithful sender labels, code blocks, tables, math, and formatting.
No install, no server, no account: everything runs locally in your browser.
- 📝 Captures all messages with proper sender attribution
- 📜 Long conversations export fully: the exporter auto-scrolls through virtualized (lazy-loaded) conversations so messages ChatGPT removed from the page are still captured
- 🔗 Web-search references included: citation sources in a response are appended as a numbered References list, matching ChatGPT's own copy output
- 🖼️ Images are kept: Markdown with available image bytes downloads as a ZIP containing a readable
.mdfile and animages/folder. Repeated images share a file. HTML and PDF-ready exports embed their images. - 🕓 Per-turn context included: ChatGPT timestamps, uploaded/generated file references, download paths, and visible reasoning recaps are preserved when available
- 🔤 Faithful text: prompt line breaks, indentation, and backslashes are preserved exactly as written — no re-flowed whitespace, no doubled
\escapes - 🔧 Preserves code blocks (including CodeMirror), tables, MathJax/KaTeX equations, lists, links, media, and file/artifact cards
- 📄 Exports to Markdown, HTML, or printable PDF
- 🆕 Google Gemini conversation export support
- 🔒 Private by default: exports show the provider label without embedding your exact conversation URL
- 🧩 Integrates with ChatGPT's native conversation and Share menus — and falls back to a floating Export button when your account has no Share control (e.g. enterprise policies)
- 🚀 Works directly from the browser console — or install as a userscript
- 🛡️ One shared, tested extraction engine powers every exporter, with multiple selector fallbacks to survive UI changes
-
Install a userscript manager:
- Tampermonkey (Chrome, Firefox, Safari, Edge)
- Violentmonkey (Chrome, Firefox)
- Greasemonkey (Firefox)
-
Install either userscript. Both installers expose Markdown and PDF export actions, so existing installations keep working without needing both scripts:
From GitHub (canonical):
From GreasyFork (auto-synced from GitHub since v1.1.0):
Both sources serve the same code and update automatically. The GreasyFork listing was stale before v1.1.0 (issue #34); if an installed copy reports a version below 1.1.0, update it or reinstall from either link above.
-
Open a ChatGPT conversation, then either:
- Open the conversation's ••• menu and choose Export to Markdown or Export to PDF.
- Click the header Share button and choose Share…, Copy link, Export to Markdown, or Export to PDF.
- If your account has no Share control at all (sharing disabled by an enterprise policy, for example), a floating Export button appears in the bottom-right corner with the same options.
- Open a conversation in ChatGPT
- Open DevTools → Console (
F12orCmd+Option+J) - Paste the contents of the exporter you want and press Enter:
- Markdown (.md, or .zip with images): exporter-markdown.js
- HTML (.html): exporter-html.js
- PDF (print-ready HTML): exporter-pdf.js
- To omit ChatGPT's per-answer reasoning/progress blocks, change
const INCLUDE_REASONING = truetofalsebefore pasting.
- The file downloads automatically, named after your conversation title
For a Markdown ZIP, extract it and keep the .md file beside its images/ folder. Open the Markdown in a viewer that supports local images; every image is also a normal file you can open or copy. Text-only conversations still download as a single .md file. Images whose bytes are unavailable retain their remote link or a readable placeholder.
- Open your conversation at gemini.google.com
- Open DevTools → Console (
F12) - Paste the contents of gemini-exporter-markdown.js and press Enter
- Open the bookmarklet for the export you want and copy all of its text: ChatGPT → Markdown, HTML or PDF, or Gemini → Markdown.
- Create a bookmark (bookmark any page, then edit it), paste that text as its URL, and name it, for example, "Export chat".
- Open a conversation and click the bookmark.
With a downloaded copy of the repository you can instead open public/bookmarklets/index.html in your browser and drag the links to your bookmarks bar.
Each bookmark contains the whole exporter (over 100,000 characters) and never loads code from anywhere else, so it does not update itself: replace it to upgrade. Chromium-based browsers (Chrome, Edge, Brave, Arc) store it. Firefox cannot — it rejects bookmark URLs longer than 65,536 characters — so use the userscript or the console there.
Chromium, Firefox, and WebKit CI exercise every shipped console bundle, both userscripts, all four bookmarklets, real downloads, rich ChatGPT/Gemini fixtures, strict CSP/Trusted Types, app-backed research, and a 1,243-message payload. See coverage and limits. WebKit coverage does not verify Safari userscript managers or mobile installation.
| Markdown Output | PDF View |
|---|---|
![]() |
![]() |
- 🖼️ Markdown with images downloads as a ZIP. The readable
.mddocument links to individual files in animages/folder, with repeated images stored once. Extract the ZIP to view or copy the images. Text-only chats still download as.md. - 🔤 Code examples stay verbatim. Image syntax inside fenced or inline code and HTML reasoning recaps is preserved.
Tested live on a signed-in, 180-message conversation with 72 images.
- 🧭 Signed-in ChatGPT exports to HTML and PDF. Its page layout was not recognised, and it scrolls in reverse, loading older history in batches seconds apart. The export read 15 of 166 messages from the page and rebuilt the rest from ChatGPT's record as plain paragraphs. It now reads 171 of 180 from the page, with their formatting.
- 🖼️ Images are embedded as the original files. Redrawing them made each one larger, and 44 of 75 became placeholders; 4 still do, over the 50 MB cap. A gallery's images appear once each, not twice, and uploads are not added again.
- 🧹 Files ChatGPT read back are no longer exported as its replies ("Make sure to include in your response to cite this file…").
- ✅ No false "incomplete" warnings from empty records, and an interrupted reply leaves a note in every format.
📝 Previous updates
- ✅ No more false "may be incomplete" warnings. ChatGPT keeps replies it never finished — stopped, failed or abandoned — in its record for good, and one anywhere in a conversation flagged the whole export. Only a final answer still being written does now.
- 💬 Interrupted replies are shown, not dropped. One that saved no text appears as ChatGPT's turn saying so, instead of leaving two of your prompts in a row. One stopped part-way ends with a note, and a code block it left open is closed, so it no longer swallows the rest of the file.
- 📅 Exports are dated by your calendar day, not UTC's: evening exports in the Americas were dated tomorrow.
- 🖼️ Generated images are exported. ChatGPT stores an image it generates — and a chart from code execution — as a tool reply, and Markdown exports skipped every tool reply: in one real 142-message export, 18 requests had no answer and 6 answers were a caption with nothing under it.
- 📸 Every uploaded image is embedded, not just the first few. Images shared one 15-second budget, so 41 of 48 in that export became
[Image: …]placeholders. The budget now grows with the number of images, and the progress card counts them as they are embedded. - 🧹 Tool plumbing no longer appears as "Reasoning / progress" — including redaction notices such as "This code was redacted."
- 🔗 Newer
sediment://image pointers download,file-service://pointers no longer add a bogus[Image: file-service], and images that share a name are all kept.
- 🧭 ChatGPT's new page layout is supported. ChatGPT has begun serving a second transcript layout (verified live on 2026-09-25): each turn is an
li[data-message-role], your prompt sits inside a button, and web sources are buttons that carry their links as data. The exporter found no messages at all there. Every turn, prompt and source now exports, and the userscript's Export button appears on it. - 💬 Short prompts are no longer skipped (#43). "Hi", "OK" or 👍 fell below a minimum-length filter — and, on the new layout, disappeared with the button it sits in. Every non-empty turn is exported.
- 🖼️ Images in temporary chats are kept (#40). Clickable image previews used to be stripped along with their button.
- 📜 Long conversations export completely (#41). The primary read of ChatGPT's stored conversation now gets up to 60 seconds per request (120 seconds overall) instead of 5, so a 1,000-message chat no longer falls back to scrolling. HTML and PDF exports use the same budget to check a scrolled capture against that record and fill any turn the sweep missed — even when the sweep ran out of time.
- 🔬 Deep Research reports from the app-backed iframe, legacy results and completed task streams, with bounded reads; a report that cannot be read is flagged inside the export. Based on @zvictor's PR #38.
- 🔖 Bookmarklets (#39) — see Method 3.
- ✨ Gemini answers keep their formatting. Gemini styles its whole answer
white-space: pre-wrap, and the exporter read that as typed text: headings, lists and bold came out flat. Code fences also get their real language instead of the first line of the code. - 🔤 Fidelity: code keeps consecutive blank lines, ChatGPT's display math exports as
$$…$$, one blank line (not two) precedes a code fence, and a temporary chat is titled "Conversation with ChatGPT/Gemini" instead of the site's tagline. - 🛡️ Hardening: timeouts cover response bodies, authenticated requests refuse redirects, CDN image reads carry no credentials or referrer, streamed images are size-capped, exported HTML carries a restrictive CSP, and an incomplete export says so inside the file. CI runs Node 22/24/26 and a Chromium/Firefox/WebKit browser matrix with pinned actions and checksummed release artifacts.
(release notes · release audit · browser coverage)
- 📦 GitHub is now the canonical install source, and GreasyFork syncs from it (issue #34). The GreasyFork listing had gone stale at a version numbered 1.0.0, which made working GitHub installs look like downgrades. This release jumps past it, the userscripts carry
@downloadURL/@updateURLheaders so GitHub installs update themselves, and the GreasyFork listing now auto-syncs from this repo — existing installs from either source pick up updates automatically. (release notes) - 🏷️ ChatGPT exports use the conversation's tab title. Answer bodies can contain ordinary
<h1>headings, and treating the first mounted one as metadata produced unstable titles like "Final architecture". (#36, thanks @zvictor) - 🧠 Reasoning progress folds into its final answer. ChatGPT stores visible "Worked for…" progress as separate assistant records; they now render as a labelled
<small>block before the answer instead of masquerading as turns. Console users can setINCLUDE_REASONING = falseto omit them. (#37, thanks @zvictor) - 🧯 Fixed whitespace loss from bookkeeping references. A
content_referencesentry whosematched_textis a single space could glue every word of an answer together. Whitespace is content, never a citation marker. (#35, thanks @zvictor) - 🛡️ Citation markers strip in linear time — resolves the CodeQL polynomial-ReDoS finding on hostile payloads.
- 🧮 Gemini maths export as real TeX —
$$W_s = -C_s + \delta[\ldots]$$instead of the rendered glyph soup. Gemini keeps its source in adata-mathattribute the exporter wasn't reading. Verified live: 42 formulas, 42 recovered. (release notes) - 🛟 Fixes a v0.12.0/v0.11.0 regression that would have deleted every formula from a Gemini export. Gemini renders KaTeX with no accessible copy alongside, so the de-duplication introduced in v0.11.0 removed the only copy there was. It is now conditional on something surviving it.
ChatGPT Markdown exports now read ChatGPT's own record of the conversation instead of scraping the page. (release notes)
- ⚡ No scrolling. One request instead of 30–60 seconds of sweeping a long conversation — and no hidden-tab stall, because there is nothing to scroll.
- ✅ Complete by construction. "Messages the sweep never reached" cannot happen: the payload is the conversation, not a sample of whatever was on screen.
- 🔗 Better citations. Sources now carry their real titles — "PROV-O: The PROV Ontology" where the page only offered a pill reading "W3C+1".
- 🧼 No scraping artifacts, because nothing is scraped.
Unchanged and still reading the page: HTML and PDF (which need rendered HTML), Gemini, shared links, temporary chats, and any case where the record is unavailable. sourceFromPayload: false forces the old behaviour.
- 🧮 Math from every renderer, and never twice. TeX is now recovered from KaTeX, MathJax v3,
data-latexattributes, and MathJax v2 scripts — the last of which had never worked, because the source was being stripped before math was processed. And when no TeX exists at all, the renderer's visual duplicate is dropped so a formula stops exporting asf(x)f(x). (release notes) - 🔀 Earlier versions of regenerated or edited turns can be included. They live in the conversation as replaced branches and were invisible to every export. Pass
includeVariants: trueto append them, labelled and in place. Off by default so nobody's existing export changes shape; when a conversation has them, the export tells you.
- 🔗 Inline citations no longer export as a wall of base64. ChatGPT puts a favicon and a label inside one link; the exporter escaped the image markdown into the link text, producing literal backslashes and a base64 blob mid-sentence instead of a link. Citations now read as
[MetaMCP+1](url), and a link that is only an image keeps its image. (release notes)
- 🧹
#### ChatGPT said:no longer appears above every message. ChatGPT labels each turn with a 1×1px off-screen heading for screen readers; the exporter was reading it as content, so it opened every message in every export. Screen-reader-only elements are now stripped as the chrome they are — matched by class, so a heading that genuinely ends in "said:" survives. (release notes) - 🕓 Recovered messages now carry their timestamp — they were the only ones in a file without one, while the payload had it all along.
Two silent defects, found by auditing a real 32-message export against ChatGPT's own record of it. (release notes)
- 📉 The last two messages could be missing. A sweep that stalls gives up wherever it happens to be, and the final read ran at that position — one conversation stopped at 85% and shipped without its newest messages. The sweep now always finishes at the bottom.
- 🔀 An answer could appear before its own question. Ordering used scroll offsets measured at capture time, which a virtualizer can make incomparable. Where ChatGPT's record accounts for every message, it now decides the order.
- 🧩 Messages the sweep can't reach are recovered rather than reported as a hole — rendered from ChatGPT's own record and placed correctly. A ChatGPT export is now complete whenever the private API is reachable.
- 📊 Exports now show their work. A small card appears over the conversation while the export runs — phase, progress bar, how many messages and lines have been read, and a preview of the message it just captured. A long export no longer looks like a frozen tab. (release notes)
- 🟠 An incomplete export doesn't end on a green bar: if the sweep finishes short of the count ChatGPT's own payload reports, the card turns amber and says how many messages never loaded. It also tells you when a background tab has paused it.
- 🧷 Built so it can't break anything: no
innerHTML(Trusted Types safe), it can never be captured into your export, it never intercepts a click, and a progress card that fails to draw costs you a progress bar rather than your file. ▶️ Still one paste — nothing about running an export changes.
- ⏸️ A background tab no longer eats your export budget. The sweep waited for a hidden tab on its own clock, so a backgrounded tab burned the whole time limit doing nothing and saved a fragment. Worse, raising the limit to help a long conversation only bought a longer stall. The wait now has its own budget and gives the time back. (release notes)
- 📣 Long exports report progress every 5 seconds instead of going silent for minutes — silence is indistinguishable from a hang.
- ⚡ Exports are about 4× faster. The sweep slept a fixed 350ms per scroll step whether or not the page had finished rendering — on a long conversation that timer was the export. It now watches the DOM and continues the moment it settles, capped at the old delay, so it can only be faster and never less thorough. Measured 11086ms → 2848ms on a 40-message fixture, byte-identical output. (release notes)
- 🎯 One loop deliberately still waits: pinning to the top of a conversation waits on a network fetch of older history, which produces no DOM activity until it arrives. Speeding that up would start the sweep below the real top and drop your oldest messages.
- 🔕 Budget warning rebalanced to match the new speed — a long conversation that now finishes comfortably is no longer flagged.
- 📏 A short export no longer claims to be complete: the old check could pass while the sweep quietly stopped before the top of a long conversation. The export now compares against the message count ChatGPT's own payload reports — ground truth instead of a heuristic. (release notes)
- ⏱️ Long conversations warn you before you export: a 129,522px conversation needs ~184 scroll steps and can't finish inside the default 120s budget.
selector-doctor.jsnow measures that from the page and tells you themaxDurationto pass. - 🔕 Fewer, better warnings: two that fired on perfectly healthy pages were demoted to informational notes. A warning list that cries wolf stops being read.
- 🏷️ A Gemini chat could export titled "Flash-Lite" — the model name, not the conversation. Gemini renders its model picker under a class matching one of its own title selectors, and it outranked the tab title. That selector is removed, and Gemini now trusts the tab title, which is verified accurate. Found on the first real run of v0.9.4's own health check. (release notes)
- 🩺
selector-doctor.jsnow catches this whole class of bug: it warns when a title selector matched something the browser tab disagrees with — "one of them is page chrome." - 🔍 Lesson recorded in the repo docs: page chrome mounts after first paint, so a single point-in-time probe is not evidence that a selector is safe. v0.9.4 shipped on exactly that mistake.
Hardening pass over the provider layer — both ChatGPT and Gemini read against the live sites. (release notes)
- 🏷️ Gemini exports no longer carry Google's tab suffix: every Gemini title selector misses on the live site, so the title came from the tab — including its
" - Google Gemini"ending, which landed in the exported heading and the download filename. - 🧩 A "turn" is now a per-provider concept: the wrapper that owns a message was hardcoded to ChatGPT's, so the v0.9.2 fix for media rendered beside a message could never apply to Gemini. Gemini's own pair wrapper holds a question and its answer — adopting it would have prefixed every answer with its question, so Gemini's turn is correctly its message element. Pinned by a regression test.
- 🩺 New
selector-doctor.js: paste it into the console on either provider and it reports what each shipped selector actually matches, which one is carrying the page, and whether ChatGPT's private API authenticates. It warns when a cascade is still working but only on a fallback — what silent drift looks like one release before it breaks. - 🕓 Worth knowing: neither site exposes per-message timestamps in the DOM. ChatGPT's come from the conversation payload — so the v0.9.3 bug meant every export before it shipped with no timestamps at all.
- 🔑
404 (Not Found)onbackend-api/conversation/…is fixed: chatgpt.com's private API doesn't accept cookies alone — it wants the same bearer token the app itself uses — and it reports a request without one as a 404, not a 401. v0.9.2 only retried with the token after a 401/403, so the retry never ran: every export silently lost its metadata pass and left a red error in the console. The token is now read up front and attached to every request, so the failing call isn't made at all. (release notes) - 🏢 Team/Business/Enterprise conversations: a refused read now retries as each workspace you can act as, which a workspace-owned conversation requires.
- 🖼️ Attachment images can actually be embedded: the file endpoint was the one private-API call sending no credentials at all, and it reports failure as HTTP 200 with an error body — both now handled.
- 🔒 Your session can't follow a download link off-origin: signed file links point at a third-party CDN, so credentials are attached only on a same-origin hop.
- 💬 A skipped metadata pass explains itself — which cause, and that the conversation itself exported fine — instead of leaving you a bare network error.
- 🖼️ Image-only messages no longer disappear (#33): attachment media can live beside ChatGPT's otherwise empty role node. The exporter now reads the whole turn, embeds safe raster image data when possible, and falls back to the source image or an explicit placeholder instead of reporting the turn as unread.
- 🕓 Transcript context is preserved (#32): per-message timestamps, uploaded filenames, generated
sandbox:/mnt/datadownloads, and visible reasoning recaps are included when ChatGPT exposes them. - 📎 Button-backed file cards keep their labels and links: file controls are serialized before generic interface buttons are stripped.
- 🛟 Metadata remains best-effort: the normal DOM export still completes if ChatGPT's authenticated conversation or file endpoint is unavailable or changes.
- ✍️ Exporting mid-answer waits for the answer to finish: hit Export while ChatGPT is still writing and the file used to contain the reply cut off mid-sentence, with nothing to say so. The sweep now waits for the newest message to stop growing (bounded by the same deadline), and marks the export incomplete if it can't.
- 🧪 Gemini's auto-scroll path is now tested — it has shipped since v0.8.0 with no coverage of its custom-element markup.
An audit pass over the whole engine (see release notes). Two of these were silent data loss:
- 🧬 Messages sharing a long opening are no longer collapsed (critical): deduplication keyed on the first 160 characters, so a conversation of redrafts — every answer opening with the same letterhead or preamble — lost all but the first, without a word. Messages are now keyed by a hash of their whole text.
- 📣 Truncated exports say so: a sweep that couldn't read every turn used to save a short file that looked complete. The export now reports
complete/missedMessagesand puts a dialog in front of you. - ⏱️ An export can't hang the page: the sweep runs against a wall-clock budget (
maxDuration, default 120s) instead of an unbounded step count. - 🙈 A backgrounded tab is waited for, not fought — the sweep pauses while the tab is hidden and resumes when it returns (v0.8.3 only warned).
- ↩️ Your scroll position always comes back, even if the sweep throws midway; a container that disappears mid-sweep is re-resolved.
- ⏳ The launcher shows Exporting… while a sweep runs, and a second click won't start a competing one.
Follow-up fixes from live testing (see release notes):
- ⏳ Turns caught mid-render are no longer dropped: ChatGPT mounts a turn before its text renders, and the sweep was retiring those turns before it managed to read them. It now retries them in place, and only marks a turn done once it has actually been captured.
- 🙈 Hidden-tab warning: Chrome throttles timers and suspends rendering in a background tab, which truncates exports. Starting an export on a hidden tab now warns you to bring it to the front.
Fixes found by testing against a live ChatGPT conversation — each one ships with a regression test that fails on v0.8.1:
- 🔢 Long exports were out of order (critical): the auto-scroll sweep appended messages as it captured them, so a 12-turn conversation exported from the bottom came out as turns
1, 2, 8, 9, 10, 11, 12, 3, 4, 5, 6, 7— ChatGPT hadn't unmounted the bottom turns yet when the sweep jumped to the top. Messages now carry their offset in the conversation and are sorted into reading order. - ✂️ Sweeps could stop after the first screenful: a virtualizer swapping turns for placeholders can drag
scrollTopbackwards, which the old loop read as "reached the bottom" and exported a fragment. Progress is now judged by messages captured and by actually reaching the end. - 🧩 Export entries never appeared in the ••• menu on desktop: ChatGPT hides that menu's Share row (
sm:hidden) when the header Share button is present, and the entries were cloned from it. They now clone a row that actually renders. - 🔗 "Share prompt" on a message stays native: per-message share buttons were being intercepted by the export menu.
- 📋 Sidebar conversation menus are left alone — they belong to other chats, but an export always reads the open one.
The Export UI No Longer Depends on ChatGPT's Share Button (#31, thanks @n8henrie):
- 🎯 Floating Export button is back — as a fallback: v0.8.0 moved exports into ChatGPT's native menus, which left accounts with sharing disabled (enterprise policies) with no export control at all. A floating Export button now appears whenever no Share control exists on the page, and steps aside automatically when one does.
- 🧯 Console escape hatch: the userscript exposes
ChatExporter.markdown(),ChatExporter.pdf(), andChatExporter.showLauncher()so an export is always reachable, whatever ChatGPT's UI does next. - 🛡️ Strict-CSP safe: the UI builds its icons and menu items through DOM APIs instead of
innerHTML, so pages that enforce Trusted Types can't block it. No shipped code touches an HTML injection sink — covered by a regression test. - 🌍 Localized menus labelled correctly: export entries cloned into a non-English ••• menu now get their own label instead of repeating ChatGPT's.
- 🧹 Cloned menu entries no longer carry ChatGPT's own
data-testids, so its internal queries never pick up our copies.
Full Captures for Long Conversations (#28, #29):
- 📜 Lazy-loading handled: ChatGPT virtualizes long conversations — messages scrolled out of view are removed from the DOM, so a single DOM pass could only ever export a fragment. The engine now auto-scrolls the conversation container from top to bottom, serializing each message while it exists, then restores your scroll position. Manual pre-scrolling is no longer needed (and never worked, since the DOM forgets what you scrolled past).
- 🔗 References exported (#27): web-search citations in a response (recognized by their
utm_source=chatgpt.comlinks or citation containers) are appended to that message as a numbered References list in Markdown, HTML, and PDF exports. - 🧩 Native menu polish: the header Share menu now includes a Share… item that opens ChatGPT's real share dialog, and share controls are recognized by
data-testidso the integration works on non-English ChatGPT locales.
Markdown Fidelity Fixes (thanks @kbelleau23-byte for the detailed report and test case in #25):
- 🔤 Prompt whitespace preserved: user messages rendered with
white-space: pre-wrapkeep their line breaks, blank lines, and indentation instead of being collapsed onto one line - ➖ No more doubled backslashes: inline code and table cells keep
\n,C:\path, and other backslash sequences verbatim (backslash escapes are never processed inside code spans, so escaping them only corrupted the output) - 🧵 Code block line breaks: code lines separated by element boundaries or
<br>tags no longer run together (Line 1Line 2…) — extraction no longer trustsinnerTexton detached nodes - 💠 Correct backtick handling: inline code containing backticks now uses longer CommonMark delimiters (
ab`` ``) instead of invalid`` escapes - 📚 Nested lists indent properly and ordered-list continuation lines align with their markers
- 🔣 Entity-safe text: conversations that literally discuss
&-style HTML entities are no longer silently un-escaped - 🛡️ Hardening: randomized internal placeholders (no marker collisions/injection), URL escaping for spaces and parentheses in links, safer filenames, and delayed
revokeObjectURLso downloads can't be aborted in Firefox-based browsers
Security and Privacy Fixes:
- 🔒 Private Source Metadata by Default: Markdown, HTML, and PDF-ready exports now show the provider label without embedding the exact conversation URL unless
includeSourceUrl: trueis explicitly used. - 🧱 Safe Markdown Code Fences: Code blocks now choose a fence longer than any backtick run in the exported code, preventing premature fence closure when conversations contain triple backticks.
- 🧪 Regression Coverage: Tests cover source URL omission, explicit source URL opt-in, and Markdown fence-injection cases across generated exporters.
Live Compatibility Rewrite:
- 🧭 Shared Extraction Engine: ChatGPT console scripts, Gemini console script, and userscripts now embed one canonical extraction engine generated from
src/extraction-engine.js - 🧱 Provider Adapters: ChatGPT uses
data-message-author-rolefirst; Gemini uses currentuser-query/model-responsecustom elements first - 🧩 Modern Rich Content: Handles CodeMirror, custom
code-block, tables, links/citations, MathJax/KaTeX/TeX annotations, media placeholders, and basic file/artifact cards - 🧪 Expanded Synthetic Fixtures: Tests cover live-observed ChatGPT and Gemini DOM shapes without including private authenticated captures
- 🧱 Modern ChatGPT Code Blocks: Supports CodeMirror-based code blocks used by current
chatgpt.com - ∑ MathJax/KaTeX Support: Exports equations as inline
$...$or block$$...$$Markdown - 📊 Table Export: Converts rendered tables into Markdown tables and keeps tables in HTML/PDF exports
- ✨ Gemini Refresh: Updated Gemini selectors and rich-content handling
- 🎯 Smart File Naming: Exported files use conversation titles instead of generic names
- 📄 True PDF Support: PDF exporter creates print-optimized HTML that converts perfectly to PDF
- 🔍 Better Message Detection: Improved selectors to prevent duplicate messages
- 🛡️ CSP Compliant: All exporters work within ChatGPT's security restrictions
- 🆕 Google Gemini Support: Full conversation export for Gemini
- 🔧 Unified Codebase: Shared logic and improvements across platforms
All exporters are generated from a single, tested engine:
src/extraction-engine.js ← canonical source (edit this)
src/userscript-ui.js ← native ChatGPT menu integration
scripts/build-exporters.js ← generates the files below
├── exporter-markdown.js ChatGPT → Markdown (console)
├── exporter-html.js ChatGPT → HTML (console)
├── exporter-pdf.js ChatGPT → print-ready HTML (console)
├── gemini-exporter-markdown.js Gemini → Markdown (console)
├── chatgpt-markdown-exporter.user.js userscript + native export menus
└── chatgpt-pdf-exporter.user.js userscript + native export menus
The engine finds messages through a cascade of selector strategies (data attributes → ARIA → semantic HTML → content heuristics), so it keeps working across ChatGPT and Gemini UI revisions. Rich content — code, tables, math, links, media — is converted through a processing pipeline that protects verbatim regions (code, pre-wrap prompts) from whitespace cleanup. On ChatGPT, the async exporter also makes a bounded, best-effort request for the already-open conversation's metadata and attachment bytes so it can add timestamps and embed images; the DOM capture remains the fallback.
npm ci # Node 22.22.2+, 24.15.0+, or 26+; dev-only jsdom and Terser
npm run build # regenerate exporters and self-contained bookmarklets
npm test # verify generated files are current + run regression tests
npm run release:prepare # also prepare dist/ with SHA256SUMS; does not publishNever edit the generated exporter files directly — change src/extraction-engine.js, src/userscript-ui.js, or the build script and run npm run build.
See CONTRIBUTING.md and docs/EXPORTER_GUIDE.md for details.
- No conversation content is sent to this project. ChatGPT exports read the current conversation and available research/file records using your signed-in session. Signed image downloads may contact the provider’s CDN without forwarding ChatGPT credentials. Remote image fallbacks can contact their original hosts when an export is opened. See SECURITY.md.
- Exports omit your exact conversation URL by default (the engine supports
includeSourceUrl: truefor explicit opt-in) - HTML/PDF output escapes all conversation content; unsafe link schemes (
javascript:,data:) are never exported as links
- "No messages found" — the site's DOM may have changed. Update to the latest exporter version; if it persists, paste selector-doctor.js into the same console — it reports which selectors still match on that page — and open an issue with a redacted report, your browser, and a description of the page. Remove conversation titles, URLs, and other private details first.
- Long conversation exports only a fragment, or turns come out in the wrong order — update to v1.2.0 or later. ChatGPT Markdown first downloads the stored conversation, allowing up to 60 seconds per request; watch the progress card. If it falls back to scrolling, keep the tab visible. Advanced callers can set
conversationFetchTimeoutandconversationMaxDurationonChatExporterEngine.exportConversationFull(...); optional enrichment usesmetadataFetchTimeout/metadataMaxDuration. An incomplete file carries a warning inside the export. GET https://chatgpt.com/backend-api/conversation/… 404 (Not Found)in the console — fixed in v0.9.3; update your copy of the exporter. ChatGPT reports a request that lacks the app's bearer token as a 404 rather than a 401, and versions before v0.9.3 only retried with the token after a 401, so the retry never ran. The export itself was never affected — only the optional per-turn metadata (timestamps, attachment names, reasoning recaps) was lost. If v0.9.3 still can't read it, the console now says why: signed out, refused, or a conversation with no stored copy (a temporary chat, a shared link, or a deleted conversation).- An image is shown as a link or placeholder instead of embedded data — the exporter embeds safe raster images up to 20 MB each (50 MB total). If ChatGPT's authenticated file endpoint or the browser canvas cannot provide the bytes, the export keeps the HTTPS source or an explicit image placeholder rather than dropping the turn.
- Export actions don't appear — confirm the userscript is enabled for
chatgpt.com, reload the page, and open a conversation's ••• or header Share menu. On accounts with no Share control (v0.8.1+), look for the floating Export button in the bottom-right corner instead; you can also force it on from the console withChatExporter.showLauncher(), or export directly withChatExporter.markdown()/ChatExporter.pdf(). - Downloads blocked in the console — some browsers require you to allow downloads/popups triggered from DevTools; the userscript method avoids this.
- v1.2.3 (Current) - Signed-in ChatGPT in HTML and PDF: its page layout, reverse scrolling and lazily loaded history; original image files; tool file reads no longer exported as replies
- v1.2.2 - Interrupted replies shown instead of flagging the export incomplete; closed code fences; local dates
- v1.2.1 - Generated images and every uploaded image in Markdown exports; tool plumbing no longer shown as reasoning
- v1.2.0 - ChatGPT's new page layout, short prompts and temporary-chat images, complete long conversations, Deep Research, bookmarklets, Gemini formatting, and release hardening
- v1.1.0 - Canonical userscript updates, stable titles, reasoning progress, and citation whitespace fixes
- v0.12.1 - Gemini maths export as TeX; fixes a regression that would have deleted them entirely
- v0.12.0 - ChatGPT Markdown reads the conversation record instead of scraping the page: no scrolling, complete by construction, real citation titles
- v0.11.0 - Math recovered from every renderer and never duplicated; opt-in export of regenerated/edited turn history
- v0.10.3 - Inline citations export as their label instead of escaped base64
- v0.10.2 - Strips ChatGPT's screen-reader "said:" heading from every message; recovered messages keep their timestamp
- v0.10.1 - Sweep always finishes at the bottom; payload decides order and fills messages the sweep couldn't reach
- v0.10.0 - In-page progress card showing phase, messages, lines and captured content; amber warning for incomplete exports
- v0.9.8 - A hidden tab no longer consumes the export budget; long sweeps report progress
- v0.9.7 - Sweep ~4× faster: waits for the DOM to settle instead of a fixed delay per scroll step
- v0.9.6 - Export completeness checked against ChatGPT's own message count; sweep-budget warning for long conversations
- v0.9.5 - Fixes a Gemini chat exporting titled with the model name; doctor flags title selectors that disagree with the tab
- v0.9.4 - Provider-layer hardening: per-provider turn scope, Gemini title/filename suffix fixed, new
selector-doctor.jshealth check - v0.9.3 - Fixes the
404onbackend-api/conversation/…: private-API calls now carry the page's bearer token up front, with workspace-account and signed-file-link handling - v0.9.2 - Embedded image/image-only turn support (#33); per-turn timestamps, uploaded/generated files, and visible reasoning recaps (#32)
- v0.9.1 - Waits for a streaming answer to finish before exporting; Gemini sweep coverage
- v0.9.0 - Whole-text dedupe (redrafts no longer collapse), incomplete-export reporting, sweep deadline, hidden-tab wait, scroll restored on failure
- v0.8.3 - Turns caught mid-render are retried instead of dropped; hidden-tab warning
- v0.8.2 - Conversation order preserved in long exports, sweeps no longer stop early, ••• menu entries visible on desktop, per-message "Share prompt" left native
- v0.8.1 - Floating Export fallback for accounts with no Share control, console API, and Trusted-Types-safe UI (#31)
- v0.8.0 - Full export of virtualized long conversations via auto-scroll (#28, #29), References list for web-search citations (#27), native menu export integration with locale-safe share detection (#26)
- v0.7.2 - Markdown fidelity: preserved prompt whitespace, no doubled backslashes, correct code-span/list/entity handling (#25)
- v0.7.1 - Source URL privacy by default and safe Markdown code fences
- v0.7.0 - Shared extraction engine, live compatibility audit, current ChatGPT/Gemini provider adapters
- v0.6.0 - Modern ChatGPT code blocks, MathJax/KaTeX, tables, Gemini refresh
- v0.5.0 - Smart file naming, true PDF support, improved duplicate detection
- v0.4.0 - Added Google Gemini support, multi-platform architecture
- v0.3.0 - Major ChatGPT stability fixes, modern selectors, duplicate prevention
- v0.2.0 (Archived) - Original ChatGPT working version


