Save a Perplexity thread as Markdown, Word or PDF with every question, answer, table and code block, and the sources of each answer as numbered links. Free, in your browser, no extension.
// Responsive Muse · Perplexity Thread Exporter · v1.0 · 2026-10-22
// Saves the Perplexity thread you have open as Markdown, Word (.doc) or PDF: every question and
// answer, with tables, code blocks and the source links of each citation. Runs only on
// perplexity.ai. It makes no network requests of its own and never reads cookies, passwords
// or tokens.
(async () => {
const TOOL = 'Export Perplexity thread';
const NAMES = { user: 'You', ai: 'Perplexity' }; // headings used in the file
if (!/(^|\.)perplexity\.ai$/.test(location.hostname) || !/^\/search\//.test(location.pathname)) {
alert('Open the Perplexity thread you want to export first (perplexity.ai/search/…).');
return;
}
// A small box on the page shows progress and asks before changing anything,
// so you never need to read or edit the console.
const many = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`; // 1 video, 2 videos
const panel = (() => {
const box = document.createElement('div');
box.style.cssText = 'display:block!important;position:fixed;right:16px;bottom:16px;z-index:2147483647;width:340px;max-width:calc(100vw - 32px);' +
'background:#111318;color:#e6e8ee;font:14px/1.5 system-ui,sans-serif;border-radius:12px;padding:16px;' +
'box-shadow:0 8px 30px rgba(0,0,0,.4);text-align:left';
const head = document.createElement('strong');
head.textContent = TOOL;
const msg = document.createElement('p');
msg.style.cssText = 'margin:8px 0 12px;white-space:pre-line';
const row = document.createElement('div');
row.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end';
box.append(head, msg, row);
document.body.append(box);
const button = (label, primary, onClick) => {
const b = document.createElement('button');
b.textContent = label;
b.style.cssText = 'font:600 14px system-ui,sans-serif;padding:8px 14px;border-radius:8px;cursor:pointer;' +
(primary ? 'background:#0f766e;color:#fff;border:1px solid #0f766e' : 'background:none;color:#e6e8ee;border:1px solid #3a3f4b');
b.onclick = onClick;
row.append(b);
};
return {
text(t) { msg.textContent = t; },
ask(t, yes) {
msg.textContent = t;
row.replaceChildren();
return new Promise(r => {
button('Cancel', false, () => { row.replaceChildren(); r(false); });
button(yes, true, () => { row.replaceChildren(); r(true); });
});
},
stoppable(onStop) { row.replaceChildren(); button('Stop', false, onStop); },
actions(t, list) {
msg.textContent = t;
row.replaceChildren();
list.forEach(([label, primary, run]) => button(label, primary, run));
},
finish(t) { this.actions(t, [['Close', true, () => box.remove()]]); },
close() { box.remove(); },
};
})();
// Turn a rendered message (HTML) back into Markdown: headings, lists, code, tables, links
const toMarkdown = root => {
const skip = el => el.matches('button, svg, script, style, [aria-hidden="true"], .sr-only, [data-markdown-copy="exclude"], ' +
'.code-block-decoration, [class*="code-block-header"]');
const walk = (node, depth) => {
if (node.nodeType === 3) return node.textContent;
if (node.nodeType !== 1 || skip(node)) return '';
const kids = () => [...node.childNodes].map(n => walk(n, depth)).join('');
const tag = node.tagName.toLowerCase();
// Math (KaTeX) is on the page three times; keep only its TeX source
if (node.classList.contains('katex')) {
const tex = node.querySelector('annotation[encoding="application/x-tex"]')?.textContent.trim();
if (tex) return node.closest('.katex-display') ? `\n\n$$${tex}$$\n\n` : `$${tex}$`;
}
if (/^h[1-6]$/.test(tag)) return `\n\n${'#'.repeat(+tag[1])} ${kids().trim()}\n\n`;
if (tag === 'p') return `\n\n${kids().trim()}\n\n`;
if (tag === 'br') return '\n';
if (tag === 'hr') return '\n\n---\n\n';
if (tag === 'strong' || tag === 'b') return `**${kids()}**`;
if (tag === 'em' || tag === 'i') return `*${kids()}*`;
if (tag === 'a') return `[${kids()}](${node.href})`;
if (tag === 'img') return node.alt ? `` : '';
if (tag === 'pre') {
const code = node.querySelector('code') || node;
const lang = ((code.className || '').match(/language-([\w+-]+)/) || [])[1] || '';
return `\n\n\`\`\`${lang}\n${code.textContent.replace(/\n$/, '')}\n\`\`\`\n\n`;
}
if (tag === 'code') return '`' + node.textContent + '`';
if (tag === 'blockquote') return '\n\n' + kids().trim().split('\n').map(l => '> ' + l).join('\n') + '\n\n';
if (tag === 'ul' || tag === 'ol') {
const items = [...node.children].filter(li => li.tagName === 'LI').map((li, i) => {
const body = [...li.childNodes].map(n => walk(n, depth + 1)).join('').trim().replace(/\n{2,}/g, '\n');
return ' '.repeat(depth) + (tag === 'ol' ? `${i + 1}. ` : '- ') + body;
});
return '\n' + items.join('\n') + '\n';
}
if (tag === 'table') {
const rows = [...node.querySelectorAll('tr')].map(tr =>
'| ' + [...tr.children].map(c => walk(c, depth).trim().replace(/\|/g, '\\|').replace(/\n/g, ' ')).join(' | ') + ' |');
if (rows.length) rows.splice(1, 0, '|' + ' --- |'.repeat(rows[0].split(' | ').length));
return '\n\n' + rows.join('\n') + '\n\n';
}
return kids();
};
return walk(root, 0).replace(/\n{3,}/g, '\n\n').trim();
};
// Offer the chat as Markdown, Word or PDF (all built here, in your browser)
const deliverChat = (title, messages, names, note = '') => {
const date = new Date().toISOString().slice(0, 10);
const md = `# ${title}\n\n_Exported ${date} from ${location.href}_\n\n` + messages.map(m =>
`## ${m.role === 'user' ? names.user : names.ai}\n\n${m.markdown}`).join('\n\n---\n\n') + '\n';
const esc = s => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
const html = '<!doctype html><html><head><meta charset="utf-8"><title>' + esc(title) + '</title><style>' +
'body{font:15px/1.6 Georgia,serif;max-width:760px;margin:40px auto;padding:0 20px;color:#111}' +
'h1{font:700 26px system-ui,sans-serif}h2{font:600 15px system-ui,sans-serif;text-transform:uppercase;letter-spacing:.05em;color:#555;margin-top:32px}' +
'.msg{white-space:pre-wrap}.user{background:#f3f4f6;padding:12px 16px;border-radius:8px}' +
'pre,code{font:13px Consolas,monospace}hr{border:0;border-top:1px solid #ddd}</style></head><body>' +
`<h1>${esc(title)}</h1><p><em>Exported ${date}</em></p>` + messages.map(m =>
`<h2>${esc(m.role === 'user' ? names.user : names.ai)}</h2><div class="msg ${m.role}">${esc(m.markdown)}</div>`).join('<hr>') +
'</body></html>';
const file = (title || 'chat').replace(/[\\/:*?"<>|]+/g, '_').slice(0, 80);
const save = (body, ext, type) => {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([body], { type }));
a.download = `${file}.${ext}`;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
panel.text(`Saved as ${file}.${ext} (check your Downloads).`);
};
const pdf = () => {
const w = window.open('', '_blank');
if (!w) { panel.text('Your browser blocked the new window. Allow pop-ups for this site and try again.'); return; }
w.document.write(html);
w.document.close();
w.focus();
w.print();
panel.text('Choose "Save as PDF" as the printer in the window that opened.');
};
const copy = () => navigator.clipboard.writeText(md)
.then(() => panel.text('Copied as Markdown. Paste it into Notion, Obsidian or any editor.'))
.catch(() => panel.text('The browser blocked the clipboard: use Markdown instead.'));
panel.actions(`Found ${many(messages.length, 'message')}.${note ? '\n' + note : ''}`, [
['Copy', false, copy],
['Markdown', false, () => save(md, 'md', 'text/markdown;charset=utf-8')],
['Word', false, () => save('\ufeff' + html, 'doc', 'application/msword')],
['PDF', true, pdf],
]);
};
const sleep = ms => new Promise(r => setTimeout(r, ms));
// Questions are the plain-text bubbles of the thread; answers are the formatted ".prose" blocks
const main = document.querySelector('main') || document.body;
const QUESTION = 'div.whitespace-pre-wrap.break-words';
// 1. Scroll through the thread once so every answer is rendered
window.scrollTo(0, 0);
for (let y = 0; y < document.documentElement.scrollHeight; y += window.innerHeight * 0.8) {
window.scrollTo(0, y);
panel.text('Reading the thread…');
await sleep(300);
}
// Citations become numbered links [1], [2]… plus a list of sources under each answer
// (worked on a copy, so the page itself is not changed)
const answer = el => {
const copy = el.cloneNode(true);
const urls = [];
for (const c of [...copy.querySelectorAll('span.citation')].filter(s => !s.parentElement.closest('span.citation'))) {
const url = c.getAttribute('data-pplx-citation-url') || c.querySelector('a')?.href || '';
if (!url) { c.remove(); continue; }
const n = urls.includes(url) ? urls.indexOf(url) + 1 : urls.push(url);
const a = document.createElement('a');
a.href = url;
a.textContent = `[${n}]`;
c.replaceWith(' ', a);
}
return toMarkdown(copy) + (urls.length ? '\n\n**Sources**\n\n' + urls.map((u, i) => `${i + 1}. ${u}`).join('\n') : '');
};
// 2. Collect questions and answers in page order
const parts = [...main.querySelectorAll(`${QUESTION}, .prose`)]
.filter(el => !el.closest('nav, aside, [data-testid="login-modal"]'))
.filter(el => !el.parentElement.closest(`${QUESTION}, .prose`)); // skip nested matches
const messages = parts.map(el => el.matches('.prose')
? { role: 'ai', markdown: answer(el) }
: { role: 'user', markdown: el.innerText.trim() })
.filter(m => m.markdown);
if (!messages.some(m => m.role === 'ai')) {
panel.finish('No answers found. Perplexity may have changed its layout: please report it on responsive-muse.com.');
return 0;
}
const title = messages.find(m => m.role === 'user')?.markdown.split('\n')[0].slice(0, 80) || 'Perplexity thread';
deliverChat(title, messages, NAMES);
return messages.length;
})();
🛡️ Chrome says “Don't paste code…”? That's normal. Type allow pasting in the console, press Enter and paste again. You only need to do it once.
Why does Chrome warn me?
Chrome shows this the first time anyone pastes into the console, to protect people from scams that ask them to paste unknown code. It's right to warn you, and that's why this script is fully visible above: 192 commented lines that send nothing anywhere and never touch your passwords or cookies. Why it's safe.
Rather not use the console? Drag the Export Perplexity thread button to your bookmarks bar and click it on www.perplexity.ai. No warning that way.
Bookmarklet: drag the middle button to your bookmarks bar, open www.perplexity.ai and click the bookmark. It runs the same script with the options above.
- Sends nothing anywhere. No fetch, XHR or external scripts. The only network activity is www.perplexity.ai itself loading your list.
- Never touches your login. It doesn't read cookies, passwords, tokens or browser storage.
- Only runs on www.perplexity.ai. Anywhere else it stops before doing anything.
- Short and readable. 192 commented lines, not minified or obfuscated.
- Shows its progress on the page. A small box tells you what it's doing and offers Download or Copy at the end.
- Same code for everyone. SHA-256 of the default version:
e7462d03a3a1ebd8…
Verify it yourself in 30 seconds
- Read it: every line is above, with comments.
- Search the code for
fetch,XMLHttpRequest,cookie,localStorageoreval. None of them appear. - Open the Network tab before running it: you'll only see requests to www.perplexity.ai.
How to use it
- Open the thread on perplexity.ai.
- Open the console: F12 or Ctrl+Shift+J (Cmd+Option+J on Mac). If Chrome asks, type
allow pastingand press Enter. - Copy the script, paste it in the console and press Enter. A box appears: click Markdown, Word or PDF (or Copy to paste it into Notion or Obsidian).
Sources stay with the answer
Perplexity’s value is in its citations, and copy-pasting a thread loses them or turns them into “+1” chips. This exporter numbers every cited page, links it inline and lists all the sources under each answer, so the document still works as a research note.
FAQ
Are the sources included?
Yes. Each citation becomes a numbered link like [1], and every answer ends with its list of sources, so the document keeps where each fact came from.
Does it work without a Perplexity account?
Yes. It exports whatever thread you have open, signed in or not, including tables and code blocks.
Isn't there an export button in Perplexity?
Some plans offer a PDF or Markdown export per thread, but it varies by plan and app. This works the same everywhere and also gives you a Word file.
