Skip to content

Instantly share code, notes, and snippets.

@max-sym
Last active October 22, 2025 20:02
Show Gist options
  • Select an option

  • Save max-sym/78875aaefac46546d9e924b1961fff9d to your computer and use it in GitHub Desktop.

Select an option

Save max-sym/78875aaefac46546d9e924b1961fff9d to your computer and use it in GitHub Desktop.
GTM: Personalized Landing Page Generator
---
const config = {
// n8n webhook url to get the data from the n8n data table
webhookUrl: ''
}
// Get the search parameters from the URL
const searchParams = Astro.url.searchParams
// Get a specific parameter
const slug = searchParams.get('slug')
const response = await fetch(webhookUrl, {
method: 'POST',
body: JSON.stringify({ slug }),
headers: { 'Content-Type': 'application/json' }
})
if (!response.ok) {
console.error('Failed to fetch dynamic data:', response.statusText)
return
}
const raw = await response.json()
const data = JSON.parse(raw.data)
---
<head>
<script define:vars={{ data }} type="application/json">
function processNode(node) {
// We only care about element and text nodes
if (node.nodeType !== 1 && node.nodeType !== 3) {
return
}
// For text nodes, perform replacement
if (node.nodeType === 3) {
const text = node.nodeValue
const replacedText = text.replace(/\{\{(.*?)\}\}/g, (match, key) => {
const trimmedKey = key.trim()
return data[trimmedKey] !== undefined ? data[trimmedKey] : match
})
if (replacedText !== text) {
node.nodeValue = replacedText
}
return
}
// For element nodes, check attributes and recursively process child nodes
if (node.nodeType === 1) {
// Handle <img> src attribute
if (node.tagName === 'IMG') {
const srcAttr = node.getAttribute('src')
if (srcAttr && /\{\{(.*?)\}\}/.test(srcAttr)) {
const key = srcAttr.replace('{{', '').replace('}}', '').trim()
node.setAttribute('src', data[key] || srcAttr)
}
}
}
// For element nodes, recursively process child nodes
for (const child of node.childNodes) {
processNode(child)
}
}
async function initialize() {
try {
if (data.color) {
const style = document.createElement('style')
style.textContent = `
:root {
--color-primary: ${data.color};
}
`
document.head.appendChild(style)
}
if (data.sections && Array.isArray(data.sections)) {
// Hide all sections first
const allSections = document.querySelectorAll('[class*="abm-lp-"]')
allSections.forEach((section) => {
if (section instanceof HTMLElement) {
section.style.display = 'none'
}
})
// Show only the sections from data
data.sections.forEach((sectionName) => {
const sectionToShow = document.querySelector(`.abm-lp-${sectionName}`)
if (sectionToShow && sectionToShow instanceof HTMLElement) {
sectionToShow.style.display = ''
}
})
}
// Run on initial content
processNode(document.body)
// Hide loader
setTimeout(() => {
const loader = document.getElementById('page-loader')
if (loader) {
loader.style.opacity = '0'
setTimeout(() => {
loader.style.display = 'none'
}, 300) // Corresponds to duration-300
}
}, 300)
// Observe and run on future DOM changes
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
processNode(node)
}
}
})
observer.observe(document.body, { childList: true, subtree: true })
} catch (error) {
console.error('Error initializing dynamic content:', error)
}
}
document.addEventListener('DOMContentLoaded', initialize)
</script>
</head>
<script>
// ----------------------------------------------------------------
// 1. CONFIGURATION
// ----------------------------------------------------------------
// !! IMPORTANT: Set your webhook URL here
const config = {
webhookUrl: '' // e.g., 'https://n8n.yourdomain.com/webhook/...'
};
// ----------------------------------------------------------------
// 2. HELPER FUNCTIONS
// ----------------------------------------------------------------
/**
* Recursively traverses a DOM node to find and replace
* {{variable}} placeholders with values from the data object.
* @param {Node} node - The DOM node to process
* @param {Object} data - The data object with key-value pairs
*/
function processNode(node, data) {
// We only care about element (1) and text (3) nodes
if (node.nodeType !== 1 && node.nodeType !== 3) {
return;
}
// --- For text nodes, perform replacement ---
if (node.nodeType === 3) {
const text = node.nodeValue;
const replacedText = text.replace(/\{\{(.*?)\}\}/g, (match, key) => {
const trimmedKey = key.trim();
return data[trimmedKey] !== undefined ? data[trimmedKey] : match;
});
if (replacedText !== text) {
node.nodeValue = replacedText;
}
return;
}
// --- For element nodes, check attributes and recurse ---
if (node.nodeType === 1) {
// Handle <img> src attribute
if (node.tagName === 'IMG') {
const srcAttr = node.getAttribute('src');
if (srcAttr && /\{\{(.*?)\}\}/.test(srcAttr)) {
const key = srcAttr.replace('{{', '').replace('}}', '').trim();
if (data[key]) {
node.setAttribute('src', data[key]);
}
}
}
// Note: You could add similar logic for other attributes like 'href'
}
// Recursively process child nodes
for (const child of node.childNodes) {
processNode(child, data);
}
}
/**
* Hides the page loader element after a short delay
*/
function hideLoader() {
setTimeout(() => {
const loader = document.getElementById('page-loader');
if (loader) {
loader.style.opacity = '0';
setTimeout(() => {
loader.style.display = 'none';
}, 300); // Corresponds to transition duration
}
}, 300); // Initial delay
}
// ----------------------------------------------------------------
// 3. MAIN INITIALIZATION FUNCTION
// ----------------------------------------------------------------
/**
* Fetches data and initializes all dynamic content on the page.
*/
async function initializeDynamicContent() {
try {
// 1. Get 'slug' from the URL query parameters
const searchParams = new URLSearchParams(window.location.search);
const slug = searchParams.get('slug');
// 2. Fetch data from the webhook
if (!config.webhookUrl) {
console.warn('Dynamic Content: webhookUrl is not set. Skipping fetch.');
hideLoader();
return;
}
const response = await fetch(config.webhookUrl, {
method: 'POST',
body: JSON.stringify({ slug }),
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
console.error('Failed to fetch dynamic data:', response.statusText);
hideLoader(); // Hide loader even if fetch fails
return;
}
const raw = await response.json();
// Expecting data to be a JSON string inside the 'data' property
const data = JSON.parse(raw.data);
if (!data) {
console.error('Dynamic data is empty or invalid.');
hideLoader();
return;
}
// 3. Inject dynamic color styles
if (data.color) {
const style = document.createElement('style');
style.textContent = `
:root {
--color-primary: ${data.color};
}
`;
document.head.appendChild(style);
}
// 4. Toggle dynamic sections
if (data.sections && Array.isArray(data.sections)) {
// Hide all sections with the base class
const allSections = document.querySelectorAll('[class*="abm-lp-"]');
allSections.forEach((section) => {
if (section instanceof HTMLElement) {
section.style.display = 'none';
}
});
// Show only the sections specified in the data
data.sections.forEach((sectionName) => {
const sectionToShow = document.querySelector(`.abm-lp-${sectionName}`);
if (sectionToShow && sectionToShow instanceof HTMLElement) {
sectionToShow.style.display = ''; // Reset to default (e.g., 'block', 'flex')
}
});
}
// 5. Process all content already on the page
processNode(document.body, data);
// 6. Set up a MutationObserver to process new content
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
processNode(node, data); // Pass the fetched data
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
} catch (error) {
console.error('Error initializing dynamic content:', error);
} finally {
// 7. Hide the loader regardless of success or failure
hideLoader();
}
}
// ----------------------------------------------------------------
// 4. EXECUTION
// ----------------------------------------------------------------
// Run the initialization logic once the DOM is ready
document.addEventListener('DOMContentLoaded', initializeDynamicContent);
</script>
<style>
/* CSS replacement for the Tailwind-styled page loader
*/
/* 1. Keyframes for the spinning animation */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 2. Main loader backdrop
(from: fixed inset-0 z-[9999] flex items-center justify-center bg-white transition-opacity duration-300 dark:bg-black)
*/
#page-loader {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background-color: #ffffff;
/* This matches the 'transition-opacity' and 'duration-300' */
transition: opacity 300ms ease-in-out;
/* The JS from your previous question will control this */
opacity: 1;
}
/* 3. Inner content wrapper
(from: flex flex-col items-center justify-center)
*/
#page-loader .page-loader-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
/* Set default text color */
color: #000000;
}
/* 4. Pure CSS spinner
(replaces: <Icon ... class="w-12 h-12 text-black animate-spin dark:text-white" />)
*/
#page-loader .spinner {
width: 3rem; /* w-12 */
height: 3rem; /* h-12 */
border: 4px solid rgba(0, 0, 0, 0.1); /* Light ring */
border-left-color: #000000; /* text-black */
border-radius: 50%;
animation: spin 1s linear infinite; /* animate-spin */
}
/* 5. "Loading" text
(from: mt-2 text-sm uppercase)
*/
#page-loader span {
margin-top: 0.5rem; /* mt-2 */
font-size: 0.875rem; /* text-sm */
line-height: 1.25rem;
text-transform: uppercase;
/* Added sensible defaults for cross-browser compatibility */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
/* 6. Dark mode styles
(from: dark:bg-black and dark:text-white)
*/
@media (prefers-color-scheme: dark) {
#page-loader {
background-color: #000000; /* dark:bg-black */
}
/* Apply dark mode text color to the content wrapper */
#page-loader .page-loader-content {
color: #ffffff; /* dark:text-white */
}
/* Update spinner color for dark mode */
#page-loader .spinner {
border-color: rgba(255, 255, 255, 0.1);
border-left-color: #ffffff; /* dark:text-white */
}
}
</style>
<div id="page-loader">
<div class="page-loader-content">
<div class="spinner"></div>
<span>Loading</span>
</div>
</div>
<div
id="page-loader"
class="fixed inset-0 z-[9999] flex items-center justify-center bg-white transition-opacity duration-300 dark:bg-black"
>
<div class="flex flex-col items-center justify-center">
<Icon name="mdi:loading" class="w-12 h-12 text-black animate-spin dark:text-white" />
<span class="mt-2 text-sm uppercase">Loading</span>
</div>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment