Exportar un hilo de Perplexity a PDF, Word o Markdown

Guarda un hilo de Perplexity como Markdown, Word o PDF con cada pregunta, respuesta, tabla y bloque de código, y las fuentes de cada respuesta como enlaces numerados.

export-perplexity.js · 192 líneaspégalo en la consola (F12) de www.perplexity.ai
// 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 = 'Exportar hilo de Perplexity';
  const NAMES = { user: 'Tú', ai: 'Perplexity' };   // headings used in the file

  if (!/(^|\.)perplexity\.ai$/.test(location.hostname) || !/^\/search\//.test(location.pathname)) {
    alert('Abre primero el hilo de Perplexity que quieres exportar (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} ${n === 1 ? word : ({ canal: 'canales', 'canción': 'canciones', 'publicación': 'publicaciones', 'elemento guardado': 'elementos guardados' })[word] || word + 's'}`;   // 1 vídeo, 2 vídeos
  const panel = (() => {
    const box = document.createElement('div');
    box.style.cssText = '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('Cancelar', false, () => { row.replaceChildren(); r(false); });
          button(yes, true, () => { row.replaceChildren(); r(true); });
        });
      },
      stoppable(onStop) { row.replaceChildren(); button('Parar', false, onStop); },
      actions(t, list) {
        msg.textContent = t;
        row.replaceChildren();
        list.forEach(([label, primary, run]) => button(label, primary, run));
      },
      finish(t) { this.actions(t, [['Cerrar', 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 ? `![${node.alt}](${node.src})` : '';
      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_Exportado el ${date} desde ${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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    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>Exportado el ${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(`Guardado como ${file}.${ext} (mira tu carpeta de Descargas).`);
    };
    const pdf = () => {
      const w = window.open('', '_blank');
      if (!w) { panel.text('El navegador ha bloqueado la ventana nueva. Permite las ventanas emergentes en esta web y vuelve a probar.'); return; }
      w.document.write(html);
      w.document.close();
      w.focus();
      w.print();
      panel.text('Elige «Guardar como PDF» como impresora en la ventana que se ha abierto.');
    };
    const copy = () => navigator.clipboard.writeText(md)
      .then(() => panel.text('Copiado en Markdown. Pégalo en Notion, Obsidian o cualquier editor.'))
      .catch(() => panel.text('El navegador ha bloqueado el portapapeles: usa Markdown.'));
    panel.actions(`Listo: ${many(messages.length, 'mensaje')}.${note ? '\n' + note : ''}`, [
      ['Copiar', 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('Leyendo el hilo…');
    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**Fuentes**\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 se han encontrado respuestas. Puede que Perplexity haya cambiado su diseño: avísanos en responsive-muse.com.');
    return 0;
  }
  const title = messages.find(m => m.role === 'user')?.markdown.split('\n')[0].slice(0, 80) || 'Hilo de Perplexity';
  deliverChat(title, messages, NAMES);
  return messages.length;
})();

Marcador: arrastra el botón del medio a tu barra de marcadores, abre www.perplexity.ai y pulsa el marcador. Ejecuta el mismo script con las opciones de arriba.

Por qué es seguro ejecutarloComprobación automática superada 23 oct 2026
  • No envía nada a ningún sitio. Sin fetch, XHR ni scripts externos. La única actividad de red es la de www.perplexity.ai cargando tu lista.
  • No toca tu sesión. No lee cookies, contraseñas, tokens ni el almacenamiento del navegador.
  • Solo funciona en www.perplexity.ai. En cualquier otra web se detiene antes de hacer nada.
  • Corto y legible. 192 líneas comentadas, sin minificar ni ofuscar.
  • Muestra su progreso en la página. Un pequeño recuadro te dice qué está haciendo y al final te ofrece Descargar o Copiar.
  • El mismo código para todos. SHA-256 de la versión por defecto: 650acd95350bc07d…
Compruébalo tú mismo en 30 segundos
  1. Léelo: todas las líneas están arriba, con comentarios.
  2. Busca en el código fetch, XMLHttpRequest, cookie, localStorage o eval. No aparece ninguna.
  3. Abre la pestaña Red (Network) antes de ejecutarlo: solo verás peticiones a www.perplexity.ai.
¿Te ha funcionado?

Cómo usarlo

  1. Abre el hilo en perplexity.ai.
  2. Abre la consola: F12 o Ctrl+Mayús+J (Cmd+Opción+J en Mac). Si Chrome te lo pide, escribe permitir pegar (o allow pasting si lo tienes en inglés) y pulsa Enter.
  3. Copia el script, pégalo en la consola y pulsa Enter. Aparece un recuadro: pulsa Markdown, Word o PDF (o Copiar para pegarlo en Notion u Obsidian).

Las fuentes se quedan con la respuesta

Lo valioso de Perplexity son sus citas, y al copiar el hilo se pierden o se quedan en botones «+1». Este exportador numera cada página citada, la enlaza en el texto y lista todas las fuentes debajo de cada respuesta.

Preguntas frecuentes

¿Se incluyen las fuentes?

Sí. Cada cita se convierte en un enlace numerado como [1] y cada respuesta termina con su lista de fuentes, así el documento conserva de dónde sale cada dato.

¿Funciona sin cuenta de Perplexity?

Sí. Exporta el hilo que tengas abierto, con sesión o sin ella, incluidas tablas y bloques de código.

¿Perplexity no tiene botón de exportar?

Algunos planes permiten exportar un hilo a PDF o Markdown, pero depende del plan y de la app. Esto funciona igual en todas partes y además te da un archivo de Word.

Más herramientas gratis

Exportar y hacer copia