Skip to content

Instantly share code, notes, and snippets.

@andywerner
Last active March 7, 2025 11:50
Show Gist options
  • Save andywerner/bad051c75f841763f7f67bfc2c258dac to your computer and use it in GitHub Desktop.
Save andywerner/bad051c75f841763f7f67bfc2c258dac to your computer and use it in GitHub Desktop.
JS to screenshot details page of a JobNinja job details page (liquid only) - Fast n dirty ChatGPT session. But quite cool!
/**
* 📌 JobNinja Job Screenshot Script
* ✅ Based on `dom-to-image`: https://github.com/tsayen/dom-to-image
* 📸 Captures job details and downloads them as a PNG screenshot.
* 🛠️ How to use:
* 1️⃣ Open a JobNinja job listing (e.g., https://jobninja.com/jobs/information-manager-legal-german-language-required-f-m-d--10980089)
* 2️⃣ Open Developer Tools (`F12` or `Ctrl+Shift+I`) → Console tab.
* 3️⃣ Paste this script into the console and press Enter.
* 4️⃣ Wait a few seconds ⏳. The screenshot appears on the page and downloads automatically.
*/
(async () => {
// ✅ Step 1: Load dom-to-image dynamically if not already available
const script = document.createElement("script");
script.src = "https://cdnjs.cloudflare.com/ajax/libs/dom-to-image/2.6.0/dom-to-image.min.js";
script.onload = async () => {
console.log("✅ dom-to-image loaded!");
// ✅ Step 2: Select the job detail container(s)
const targetElements = document.querySelectorAll('div[class^="JobSearch_jobDetail_"]');
if (!targetElements.length) {
console.error("❌ No matching elements found!");
return;
}
// ✅ Step 3: Create a wrapper for selected elements
const wrapper = document.createElement("div");
wrapper.style.backgroundColor = "white"; // Ensures a white background
wrapper.style.padding = "10px"; // Adds padding for clean visuals
wrapper.style.display = "inline-block"; // Prevents full-page expansion
// ✅ Step 4: Clone and copy styles for the job elements
targetElements.forEach(el => {
const clonedEl = el.cloneNode(true);
copyComputedStyles(el, clonedEl);
wrapper.appendChild(clonedEl);
});
// Temporarily add wrapper to the page
document.body.appendChild(wrapper);
console.log("📌 Cloned Elements:", wrapper.innerHTML);
// ✅ Step 5: Load and replace images before rendering
await loadAndReplaceImages(wrapper);
try {
console.log("⏳ Converting elements to PNG...");
// ✅ Step 6: Convert the wrapper to a PNG image
const dataUrl = await domtoimage.toPng(wrapper, {
bgcolor: "white", // White background in the PNG
filter: node => {
// ✅ Keep text nodes
if (node.nodeType === 3) return true;
// ✅ Ignore non-elements (e.g., comments)
if (!(node instanceof Element)) return false;
// ❌ Remove unnecessary elements (iframes, scripts, tracking pixels)
if (["IFRAME", "SCRIPT", "LINK"].includes(node.tagName)) return false;
// ❌ Skip tiny tracking pixels
if (node.tagName === "IMG" && (node.width <= 2 && node.height <= 2)) {
console.warn(`⚠️ Skipping tiny tracking pixel: ${node.src}`);
return false;
}
// ❌ Skip cross-origin images (to avoid CORS errors)
if (node.tagName === "IMG") {
try {
const imageURL = new URL(node.src);
if (imageURL.origin !== location.origin) {
console.warn(`⚠️ Skipping CORS image: ${node.src}`);
return false;
}
} catch (e) {
console.warn(`⚠️ Skipping invalid image URL: ${node.src}`, e);
return false;
}
}
return true; // ✅ Keep all other content
}
});
console.log("✅ Image successfully created!");
// ✅ Step 7: Remove the wrapper after capturing
document.body.removeChild(wrapper);
// ✅ Step 8: Display the image on the page
const img = new Image();
img.src = dataUrl;
document.body.appendChild(img);
console.log("🖼️ Image added to the page!");
// ✅ Step 9: Download the image automatically
const link = document.createElement("a");
link.href = dataUrl;
link.download = "screenshot.png";
link.click();
console.log("📥 Download triggered!");
} catch (error) {
console.error("❌ Error converting elements:", error);
}
};
document.body.appendChild(script);
// ✅ Function: Copy Computed Styles
function copyComputedStyles(source, target) {
const computedStyle = window.getComputedStyle(source);
for (let i = 0; i < computedStyle.length; i++) {
const prop = computedStyle[i];
target.style[prop] = computedStyle.getPropertyValue(prop);
}
[...source.children].forEach((child, index) => {
if (target.children[index]) {
copyComputedStyles(child, target.children[index]);
}
});
}
// ✅ Function: Load and Replace Images
async function loadAndReplaceImages(container) {
const images = container.querySelectorAll("img");
for (let img of images) {
const originalSrc = img.src;
if (!originalSrc) continue;
// ✅ Skip Tracking Pixels
if ((img.width <= 2 && img.height <= 2) || img.src.includes("tracking") || img.src.includes("beacon")) {
console.warn(`⚠️ Skipping tracking pixel: ${originalSrc}`);
continue;
}
// ✅ Skip CORS-restricted images
try {
const imageURL = new URL(originalSrc);
if (imageURL.origin !== location.origin) {
console.warn(`⚠️ Skipping external image due to CORS: ${originalSrc}`);
continue;
}
} catch (e) {
console.warn(`⚠️ Skipping invalid image URL: ${originalSrc}`, e);
continue;
}
try {
// ✅ Load image first
const loadedImg = await loadImage(originalSrc);
// ✅ Replace with simple `<img>` element
const simpleImg = document.createElement("img");
simpleImg.src = loadedImg.src;
simpleImg.width = img.width;
simpleImg.height = img.height;
simpleImg.style = img.style.cssText;
img.replaceWith(simpleImg);
console.log(`🔄 Replaced image: ${originalSrc}`);
} catch (error) {
console.warn(`⚠️ Failed to load image: ${originalSrc}`, error);
}
}
}
// ✅ Helper Function: Load an Image
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous"; // Allow cross-origin images to be used in canvas
img.src = url;
img.onload = () => resolve(img);
img.onerror = () => reject(`Image failed to load: ${url}`);
});
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment