Exportar una conversación de Claude a PDF, Word o Markdown

Guarda cualquier conversación de Claude como documento, en el orden correcto, con código, tablas y fórmulas. Gratis y sin extensiones.

Abre la conversación que quieras en claude.ai y ejecuta allí el script.

export-claude.js · 202 líneaspégalo en la consola (F12) de claude.ai
// Responsive Muse · Claude Conversation Exporter · v1.0 · 2026-10-22
// Saves the Claude conversation you have open as Markdown, Word (.doc) or PDF, with code
// blocks, lists and tables kept. Runs only on claude.ai. Claude only keeps part of a long
// chat on screen, so the script scrolls from the top and collects every message on the way.
// It makes no network requests of its own and never reads cookies, passwords or tokens.
(async () => {
  const TOOL = 'Exportar chat de Claude';
  const NAMES = { user: 'Tú', ai: 'Claude' };     // headings used in the file
  const DELAY = 900;                               // pause between scrolls, in milliseconds

  if (location.hostname !== 'claude.ai' || !location.pathname.startsWith('/chat/')) {
    alert('Abre primero la conversación de Claude que quieres exportar (claude.ai/chat/…).');
    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));
  const SEL = '[data-testid="user-message"], [data-testid="assistant-message"]';
  const first = document.querySelector(SEL);
  let box = first;
  while (box && !(box.scrollHeight > box.clientHeight + 50 && /(auto|scroll)/.test(getComputedStyle(box).overflowY))) box = box.parentElement;
  if (!first || !box) {
    panel.finish('No se han encontrado mensajes. Puede que Claude haya cambiado su diseño: avísanos en responsive-muse.com.');
    return 0;
  }

  // 1. Scroll to the very top until no older messages load
  let last = -1, idle = 0;
  while (idle < 3) {
    box.scrollTop = 0;
    await sleep(DELAY);
    const n = document.querySelectorAll(SEL).length;
    idle = n === last ? idle + 1 : 0;
    last = n;
    panel.text('Cargando el principio de la conversación…');
  }

  // 2. Scroll down, collecting each message once. Claude numbers every message (data-index),
  //    which keeps the right order even though it re-inserts rows while you scroll.
  const seen = new Map();
  const collect = () => {
    for (const m of document.querySelectorAll(SEL)) {
      const role = m.getAttribute('data-testid') === 'user-message' ? 'user' : 'ai';
      const index = Number(m.closest('[data-index]')?.getAttribute('data-index'));
      const key = Number.isFinite(index) ? index : (m.closest('[data-turn-key]')?.getAttribute('data-turn-key') || '') + role;
      if (seen.has(key)) continue;
      const body = role === 'ai' ? (m.querySelector('.standard-markdown') || m) : m;
      seen.set(key, { index, role, markdown: role === 'ai' ? toMarkdown(body) : body.textContent.trim() });
    }
  };
  idle = 0;
  while (idle < 3) {
    const before = seen.size;
    collect();
    const atEnd = box.scrollTop + box.clientHeight >= box.scrollHeight - 5;
    box.scrollTop += box.clientHeight * 0.8;
    await sleep(DELAY);
    idle = seen.size === before && atEnd ? idle + 1 : 0;
    panel.text(`Leyendo mensajes… ${many(seen.size, 'mensaje')}`);
  }
  const messages = [...seen.values()].sort((a, b) => a.index - b.index);
  const numbered = messages.filter(m => Number.isFinite(m.index));
  const missing = numbered.length ? numbered.at(-1).index + 1 - numbered.length : 0;
  const note = missing > 0 ? `${many(missing, 'mensaje')} no se han podido leer: recorre el chat una vez y vuelve a ejecutarlo.` : '';
  if (!messages.length) {
    panel.finish('No se han encontrado mensajes. Puede que Claude haya cambiado su diseño: avísanos en responsive-muse.com.');
    return 0;
  }
  deliverChat(document.title.replace(/\s*-\s*Claude$/, '') || 'Conversación de Claude', messages, NAMES, note);
  return messages.length;
})();

Marcador: arrastra el botón del medio a tu barra de marcadores, abre claude.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 · probado en una cuenta real el 22 oct 2026
  • No envía nada a ningún sitio. Sin fetch, XHR ni scripts externos. La única actividad de red es la de claude.ai cargando tu lista.
  • No toca tu sesión. No lee cookies, contraseñas, tokens ni el almacenamiento del navegador.
  • Solo funciona en claude.ai. En cualquier otra web se detiene antes de hacer nada.
  • Corto y legible. 202 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: cd2b5ee652c75b9f…
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 claude.ai.
¿Te ha funcionado?

Cómo usarlo

  1. Abre la conversación en claude.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).

En el orden correcto

Claude solo mantiene en pantalla parte de los chats largos y vuelve a colocar los mensajes al desplazarte. El script usa el número de cada mensaje, así que el documento sale completo y ordenado, y si alguno no se puede leer te avisa.

Preguntas frecuentes

¿Claude tiene un botón de exportar?

Claude puede exportar toda tu cuenta desde Ajustes, como un zip de archivos JSON que llega por email. No hay botón para guardar una conversación como documento; este script lo hace.

¿Se conservan el código, las tablas y las fórmulas?

Sí. El código mantiene su lenguaje, las tablas pasan a tablas Markdown y las fórmulas se exportan en TeX ($…$), que Notion, Obsidian y la mayoría de editores Markdown muestran como fórmulas.

¿Cómo saco un PDF?

Pulsa PDF en el recuadro: se abre una vista de impresión limpia. Elige «Guardar como PDF» como impresora. No se sube nada a ningún sitio.

¿Funciona con chats muy largos?

Sí. La web solo mantiene en pantalla parte de un chat largo, así que el script se desplaza desde el principio y recoge cada mensaje por el camino. Deja la pestaña abierta hasta que el recuadro muestre los botones.

¿Pueden salir los mensajes desordenados?

No. Claude numera cada mensaje y el script usa esos números. Si algún mensaje no se puede leer, el recuadro te avisa.

Más herramientas gratis

Exportar y hacer copia