[object Object]

← back to Designer Wallcoverings

Fix spec-sheet Download PDF: render via hidden iframe (fetch+DOMParser never ran the page JS), add layout none to spec-sheet template

a727c0942285607251971c42994ccdcd8977e999 · 2026-06-23 10:37:14 -0700 · Steve

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit a727c0942285607251971c42994ccdcd8977e999
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jun 23 10:37:14 2026 -0700

    Fix spec-sheet Download PDF: render via hidden iframe (fetch+DOMParser never ran the page JS), add layout none to spec-sheet template
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 shopify/_cwGRID/snippets/spec-sheet-button.liquid | 133 +++++++++++-----------
 shopify/_cwGRID/templates/page.spec-sheet.liquid  |   7 ++
 2 files changed, 74 insertions(+), 66 deletions(-)

diff --git a/shopify/_cwGRID/snippets/spec-sheet-button.liquid b/shopify/_cwGRID/snippets/spec-sheet-button.liquid
index c6030f30..22f6c7b2 100644
--- a/shopify/_cwGRID/snippets/spec-sheet-button.liquid
+++ b/shopify/_cwGRID/snippets/spec-sheet-button.liquid
@@ -1,97 +1,98 @@
 {% comment %}
-  Spec Sheet Button - With inline PDF download (no page navigation)
+  Spec Sheet Button - Inline PDF download (no page navigation)
+
+  FIX 2026-06-23: the previous version did fetch() + DOMParser on
+  /pages/spec-sheet, then ran html2canvas on the parsed #pdf-content. That
+  never worked: the spec-sheet page renders its product data via JavaScript
+  (loadProduct()), and DOMParser does NOT execute scripts, so #pdf-content was
+  always captured as the "Loading Product..." placeholder -> 0-dimension canvas
+  -> jsPDF.addImage() threw -> "Error" -> nothing saved.
+
+  This version spins up a hidden SAME-ORIGIN iframe pointed at the spec-sheet
+  page. Inside the iframe the page's OWN scripts run and render the product
+  (styles + image + specs all present), then we invoke the iframe's already-
+  proven downloadPDF(). One click, reuses the working generator.
 {% endcomment %}
 
 <div class="spec-sheet-controls">
   <button class="spec-sheet-btn pdf-download-btn"
-          onclick="downloadSpecSheetPDF(event, '{{ product.handle }}', '{{ product.title }}', '{{ product.featured_image.src }}');"
+          onclick="downloadSpecSheetPDF(event, '{{ product.handle }}', '{{ product.title }}');"
           style="vertical-align: top;">
     🖨️ Download PDF
   </button>
 </div>
 
 <script>
-async function downloadSpecSheetPDF(e, handle, title, imageUrl) {
+async function downloadSpecSheetPDF(e, handle, title) {
   e?.preventDefault?.();
   e?.stopPropagation?.();
 
-  const btn = e?.target;
-  if (btn) {
-    btn.textContent = '⏳ Generating...';
-    btn.disabled = true;
-  }
+  // currentTarget is the button itself (target can be a child node/emoji span)
+  const btn = e?.currentTarget || e?.target;
+  const reset = () => { if (btn) { btn.textContent = '🖨️ Download PDF'; btn.disabled = false; } };
+  if (btn) { btn.textContent = '⏳ Generating...'; btn.disabled = true; }
 
+  let iframe;
   try {
-    // Fetch the spec sheet HTML
-    const response = await fetch(`/pages/spec-sheet?product=${encodeURIComponent(handle)}`);
-    const html = await response.text();
-
-    // Extract the printable content
-    const parser = new DOMParser();
-    const doc = parser.parseFromString(html, 'text/html');
-    const pdfContent = doc.getElementById('pdf-content');
-
-    if (!pdfContent) {
-      throw new Error('Could not load spec sheet');
-    }
-
-    // Use html2pdf library (already loaded on spec sheet page)
-    const script1 = document.createElement('script');
-    script1.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js';
-    document.head.appendChild(script1);
-
-    const script2 = document.createElement('script');
-    script2.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
-    document.head.appendChild(script2);
-
-    // Wait for scripts to load
-    await new Promise(resolve => {
-      script1.onload = () => {
-        script2.onload = resolve;
-      };
-    });
-
-    // Generate PDF
-    const canvas = await html2canvas(pdfContent, {
-      scale: 2,
-      useCORS: true,
-      allowTaint: false,
-      backgroundColor: '#ffffff',
-      logging: false
+    // 1. Hidden, same-origin iframe so the spec-sheet page's scripts execute.
+    iframe = document.createElement('iframe');
+    iframe.setAttribute('aria-hidden', 'true');
+    iframe.style.cssText = 'position:fixed;left:-10000px;top:0;width:900px;height:1400px;border:0;visibility:hidden;';
+    iframe.src = '/pages/spec-sheet?product=' + encodeURIComponent(handle);
+    document.body.appendChild(iframe);
+
+    // 2. Wait for the iframe to render the product (its JS reveals #pdfBtn when ready).
+    await new Promise(function (resolve, reject) {
+      var t0 = Date.now();
+      iframe.addEventListener('load', function () {
+        var poll = setInterval(function () {
+          var idoc = iframe.contentDocument;
+          if (!idoc) return; // not accessible yet
+          var pdfBtn = idoc.getElementById('pdfBtn');
+          var content = idoc.getElementById('content');
+          var failed = content && /Product Not Found|No Product Specified/i.test(content.textContent || '');
+          var ready = pdfBtn && pdfBtn.style.display !== 'none';
+          if (ready) { clearInterval(poll); resolve(); }
+          else if (failed) { clearInterval(poll); reject(new Error('Product not found in spec sheet')); }
+          else if (Date.now() - t0 > 20000) { clearInterval(poll); reject(new Error('Spec sheet render timed out')); }
+        }, 200);
+      });
+      iframe.addEventListener('error', function () { reject(new Error('Could not load spec sheet')); });
     });
 
-    const { jsPDF } = window.jspdf;
-    const imgData = canvas.toDataURL('image/png');
-    const pdf = new jsPDF({
-      orientation: 'portrait',
-      unit: 'mm',
-      format: 'letter'
+    // 3. Invoke the iframe's own proven generator (libs + image + styles all in its context).
+    var iwin = iframe.contentWindow;
+    var idoc = iframe.contentDocument;
+    if (typeof iwin.downloadPDF !== 'function') {
+      throw new Error('Spec sheet generator unavailable');
+    }
+    iwin.downloadPDF();
+
+    // 4. Wait for the iframe's button to report success/failure before tearing it down.
+    await new Promise(function (resolve, reject) {
+      var t0 = Date.now();
+      var poll = setInterval(function () {
+        var pdfBtn = idoc.getElementById('pdfBtn');
+        var txt = pdfBtn ? pdfBtn.textContent : '';
+        if (/Downloaded/i.test(txt)) { clearInterval(poll); resolve(); }
+        else if (/Error/i.test(txt)) { clearInterval(poll); reject(new Error('Generator failed')); }
+        else if (Date.now() - t0 > 30000) { clearInterval(poll); reject(new Error('PDF generation timed out')); }
+      }, 250);
     });
 
-    const imgWidth = 216;
-    const imgHeight = (canvas.height * imgWidth) / canvas.width;
-    pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
-
-    // Download with product SKU in filename
-    const sku = title.replace(/[^a-z0-9]/gi, '-').toLowerCase();
-    pdf.save(`Designer-Wallcoverings-${sku}.pdf`);
-
     if (btn) {
       btn.textContent = '✓ Downloaded!';
-      setTimeout(() => {
-        btn.textContent = '🖨️ Download PDF';
-        btn.disabled = false;
-      }, 2000);
+      setTimeout(reset, 2000);
     }
   } catch (err) {
     console.error('PDF generation failed:', err);
     if (btn) {
       btn.textContent = '✗ Error - Try Again';
-      setTimeout(() => {
-        btn.textContent = '🖨️ Download PDF';
-        btn.disabled = false;
-      }, 2000);
+      setTimeout(reset, 2500);
     }
+  } finally {
+    // Give the browser a beat to commit the download, then remove the iframe.
+    if (iframe) setTimeout(function () { iframe.remove(); }, 1500);
   }
 }
 </script>
diff --git a/shopify/_cwGRID/templates/page.spec-sheet.liquid b/shopify/_cwGRID/templates/page.spec-sheet.liquid
index dfc7fef1..4fd5cb1d 100644
--- a/shopify/_cwGRID/templates/page.spec-sheet.liquid
+++ b/shopify/_cwGRID/templates/page.spec-sheet.liquid
@@ -1,8 +1,15 @@
+{% layout none %}
 {% comment %}
   Simple Spec Sheet - Refined version
   - Larger image (500x500px)
   - Single column if space available
   - Title case, not bold
+
+  layout none (2026-06-23): this template outputs its own complete <!DOCTYPE html>
+  document, so it must NOT be wrapped in theme.liquid. Without this, the rendered
+  page was ~900KB of nested/duplicate markup (theme chrome wrapping a full HTML
+  doc). Skipping the layout yields a lean standalone spec sheet -> faster, valid,
+  and ideal for the hidden-iframe PDF generator in spec-sheet-button.liquid.
 {% endcomment %}
 
 <!DOCTYPE html>

← f59d4ade Add Tres Tintas spec recrawl — fills width/repeat/length/wei  ·  back to Designer Wallcoverings  ·  cadence: honor newwall_catalog dedup_skip so Tres Tintas imp 0b67db27 →