Last active
October 22, 2025 20:02
-
-
Save max-sym/78875aaefac46546d9e924b1961fff9d to your computer and use it in GitHub Desktop.
GTM: Personalized Landing Page Generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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