Add 'Share' button to your Pickaxe chat that exports current thread to a PDF (advanced)

Hi everyone!! :jack_o_lantern:

Here’s how you can add a Share to PDF button to your Pickaxe. This exports the visible chat thread to a PDF using custom javascript. Inspired by our dynamic-loader so it won’t clash with your page!!

To do this, you’ll:

  1. Use a script embed (not an iframe)

  2. Add one more <script> that loads html2pdf.js, mounts a floating button, and captures the chat DOM to PDF

  3. Everything is namespaced and guarded to avoid duplicate injections

Example

Your Pickaxe script embed →

<script src="https://studio.pickaxe.co/api/embed/bundle.js" defer></script>

Script begins here! →

<script>
(() => {
  // prevent dupe loads if snippet appears more than once
  if (window.__PICKAXE_PDF_LOADER__) return;
  window.__PICKAXE_PDF_LOADER__ = true;
  // ---- config (edit these!!) ----
  const NS = 'pa-pdf';
  const CONFIG = {
    buttonText: 'Share',
    fileName: 'chat.pdf',
    // if you customized your chat DOM, update this selector
    chatSelector: '[data-pickaxe-embed] section, [data-pickaxe-embed] .chat, [data-pickaxe-embed]',
    position: { bottom: '24px', right: '24px' }
  };
  // load a script exactly once by URL
  const loadOnce = (src) => new Promise((res, rej) => {
    const id = `${NS}-script-${btoa(src)}`;
    if (document.getElementById(id)) return res();
    const s = document.createElement('script');
    s.id = id; s.src = src; s.defer = true;
    s.onload = res; s.onerror = rej;
    document.head.appendChild(s);
  });
  // wait for the Pickaxe embed to exist in the DOM
  const waitForEmbed = () => new Promise((resolve) => {
    if (document.querySelector('[data-pickaxe-embed]')) return resolve();
    const obs = new MutationObserver(() => {
      if (document.querySelector('[data-pickaxe-embed]')) { obs.disconnect(); resolve(); }
    });
    obs.observe(document.documentElement || document.body, { childList: true, subtree: true });
  });
  // find the chat container
  const getChatRoot = () => {
    const root = document.querySelector('[data-pickaxe-embed]');
    if (root) {
      const inner = root.querySelector('section, .chat, .messages, [role="log"]');
      return inner || root;
    }
    return document.querySelector(CONFIG.chatSelector) || document.body;
  };
  // build a copy w/ stripped inputs/toolbars
  const buildPrintable = (chatEl) => {
    const clone = chatEl.cloneNode(true);
    clone.querySelectorAll('textarea, input, button, [contenteditable], .composer, .toolbar, [data-action]')
      .forEach(n => n.remove());
    // optional: hide reasoning / system blocks (uncomment if you want them omitted!!)
    // clone.querySelectorAll('.reasoning, think, [data-reasoning]').forEach(n => n.remove());
    const wrap = document.createElement('div');
    wrap.style.padding = '16px';
    wrap.style.background = 'white';
    wrap.style.color = '#111';
    wrap.style.maxWidth = '920px';
    wrap.style.margin = '0 auto';
    wrap.appendChild(clone);
    return wrap;
  };
  // create the floating Share button
  const mountButton = () => {
    if (document.querySelector(`.${NS}-btn`)) return null;
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = `${NS}-btn`;
    btn.textContent = CONFIG.buttonText;
    Object.assign(btn.style, {
      position: 'fixed',
      zIndex: '2147483646',
      bottom: CONFIG.position.bottom,
      right: CONFIG.position.right,
      padding: '10px 14px',
      borderRadius: '10px',
      border: '1px solid rgba(0,0,0,.12)',
      background: 'white',
      boxShadow: '0 8px 24px rgba(0,0,0,.15)',
      font: '500 14px/1.2 system-ui, -apple-system, Segoe UI, Roboto, sans-serif',
      cursor: 'pointer'
    });
    btn.setAttribute('aria-label', 'Share chat as PDF');
    document.body.appendChild(btn);
    return btn;
  };
  // wire everything up
  const init = async () => {
    await waitForEmbed();
    await loadOnce('https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js');
    const btn = mountButton();
    if (!btn) return;
    btn.addEventListener('click', () => {
      try {
        const chatEl = getChatRoot();
        const node = buildPrintable(chatEl);
        window.html2pdf().set({
          margin: [10,10,10,10],
          filename: CONFIG.fileName,
          image: { type: 'jpeg', quality: 0.98 },
          html2canvas: { scale: 2, useCORS: true },
          jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
        }).from(node).save();
      } catch (e) {
        console.error('[Pickaxe PDF] export failed:', e);
        alert('Sorry, there was a problem exporting your PDF.');
      }
    }, { passive: true });
  };
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init, { once: true });
  } else {
    init();
  }
})();
</script>

CUSTOMIZATION

You can tweak the filename, button position, or which elements are included/excluded in the PDF (see the comments in the code above)

Notes

  • Use script embed for this (iframes isolate content and can’t be captured from the parent page).

  • The snippet is namespaced (pa-pdf) and guarded with window.__PICKAXE_PDF_LOADER__ so you won’t get duplicates if it’s included in both a layout and a page

  • If your chat wrapper is customized, update chatSelector to match your DOM

If any of this is super duper confusing but you still want to try: we’ll reply below with a short glossary defining some of the logic talked about above

Happy Building and special shoutout to one of our users @leoreche for this inspiration on this one! :construction_worker:

4 Likes

This is so cool, thanks for sharing Carson! :tada:

2 Likes

Quick Reference (2-min read)

DOM (Document Object Model)

In plain English: Your webpage as a tree of elements that JavaScript can grab and manipulate.

Why you care: We find your chat container, make a copy, and feed it to the PDF library.

CSS Selector

In plain English: A search pattern for finding stuff in the DOM (#id, .class, [data-something]).

Why you care: If the PDF is blank, you probably need to tweak chatSelector to match your theme’s chat wrapper.

Script Embed vs. Iframe

In plain English: Script embeds inject directly into your page. Iframes are locked boxes your page can’t peek into.

Why you care: PDFs only work with script embeds. We can’t see inside iframes!!

Namespacing

In plain English: Adding prefixes like pa-pdf- to avoid stepping on other code’s toes.

Why you care: Keeps our button and styles from breaking your site (or vice versa).

Duplicate-Load Guard

In plain English: A “has this run already?” check using something like window.__PICKAXE_PDF_LOADER__.

Why you care: Page builders love running the same code twice. This prevents double buttons and double downloads.

MutationObserver

In plain English: A browser API that yells “Hey, something changed!” when the DOM updates.

Why you care: Your chatbot loads async, we wait for it to appear before adding the button.

CDN (Content Delivery Network)

In plain English: Fast servers around the world hosting libraries.

Why you care: We lazy-load html2pdf.js from a CDN only when you click Share, it keeps your page light.

html2pdf.js

In plain English: Takes HTML, turns it into pixels, wraps it in a PDF.

Why you care: This is what actually generates your chat.pdf.

Rasterization / html2canvas

In plain English: Converting your rendered page to pixels before PDFing it.

Why you care: Custom fonts and wild CSS might look slightly off. Try not to get too crazy and keep your chat area simple for best results!

CSP (Content Security Policy)

In plain English: Your site’s bouncer that blocks sketchy scripts.

Why you care: If inline <script> tags don’t work, host the code in a .js file or tweak your CSP settings.

Idempotent

In plain English: Run it once, run it ten times and get the same result. Like the old school definition for insanity.

Why you care: Our code is safe to run multiple times (SPA navigation, accidental double-pastes, etc.).

Event Listener

In plain English: “When user does X, run Y.”

Why you care: The Share button’s click listener kicks off the PDF export.

Deep Clone

In plain English: Copying a DOM chunk and all its children.

Why you care: We clone the chat, strip out textboxes and toolbars, then hand the clean copy to the PDF generator.


The Mental Model

Find the chat → clone it → clean it → PDFify it.

Everything else (guards, observers, namespaces) just makes this stronger across different platforms.


Quick 30-Second Test to Run Through

:white_check_mark: Using script embed (not iframe)?

:white_check_mark: See one Share button (not duplicates)?

:white_check_mark: Click → downloads chat.pdf with visible messages?

:cross_mark: Blank PDF? → Fix your chatSelector to target the actual chat wrapper

:locked: Hide internal stuff? → Uncomment the line that strips reasoning blocks

1 Like

Will try, sounds amazing

1 Like

whenever you do get a chance to try we’re here to help if ya run into any hiccups!

1 Like

Nice one @carsondev :raising_hands: This script is a handy UX win!

Quick clarity for everyone on where this shines and where a webhook-to-PDF pipeline is a better fit.

“DOM” (“Document Object Model”) = the live page your browser renders. This script can only export what exists in that live page.

Topic Share button script (client-side) Webhook to Make. com (server-side)
What it exports Only what is currently in the “DOM” Full transcript you send or fetch (not limited to what’s visible)
PDF method html2pdf.js captures the chat area; output is rasterized Server renderer from an HTML template; higher layout fidelity
Branding Light CSS tweaks and optional element hiding; no standard header/footer template Fully branded templates with headers, footers, page numbers, watermarks, logos
Delivery Instant download to the user’s device Automatic email to user and team, plus storage to Drive/S3 and CRM logging
Range control Exports the on-page thread; no built-in range inference Rules like “last N messages,” between message IDs, or redaction are easy to implement
Configuration Selector for chat container, filename pattern, button position Page size/orientation, naming conventions, merge rules, redaction, analytics
Reliability Works when the script can access the chat container; breaks inside strict iframes Runs regardless of embed; pipeline executes even if the user closes the page
Setup effort Simple script embed Webhook + Make scenario + PDF renderer + email/storage modules
Best for Quick share of what you’re seeing right now Production-grade branded summaries and records at scale

Guidance

  • If you need fast, user-initiated exports of the current view, this script is great. Remember it’s limited to the live “DOM,” uses rasterized PDFs, and has no built-in email or storage.
  • If you need consistent brand output, automated delivery, archival, and range/redaction rules, use a Pickaxe Action that posts to a Make. com webhook, render a branded HTML template to PDF, email it, and store it.

Who should use which

  • Builders who want a quick “save what I see” for a client or thread ➝ Use the Share button script.
  • Builders/Teams that need consistent branding, audit trails, and automated distribution ➝ Use the webhook-to-PDF flow.

For branded PDFs and team workflows, the webhook route remains the right tool.

1 Like