對話紀錄 January 19, 2026
.inner, .dynamic-section, .content, etc.100vw + calc(-50vw + 50%) (which we used successfully)In short: Tattoo is very “forgiving” for heavy dynamic-content customization — it gives you a blank-ish canvas that’s easy to paint on top of.
We did have to build quite a few things from scratch:
.dynamic-section .innerAll of these were necessary because Tattoo is intentionally minimal — it doesn’t come with built-in embed handling, modal styles, or separator pseudo-elements like Square did.
It depends heavily on which theme, but in general:
Likely less customization needed in most cases — but not always zero.
Typical scenarios:
| Theme Type | Expected Customization Effort | Main Reasons |
|---|---|---|
| Modern elaborate Publii theme (e.g. one with built-in hero, embed support, modal) | Low–Medium | Usually already has responsive embeds, better spacing defaults, possibly modal-like components |
| Old/complex Square-era theme | Medium–High | Lots of legacy classes → may need to override or ignore them, similar to our early struggles |
| Ultra-minimal / new blank theme | Medium–High | Same as Tattoo — have to build most things ourselves |
| Highly opinionated / rigid theme | High | Fighting against built-in styles (e.g. forced padding, fixed widths) can be more painful than starting from minimal |
Most likely future outcome — if you choose a reasonably modern, elaborated theme (not too old, not too rigid), you’ll probably need 30–60% less custom CSS than with Tattoo, especially for:
But you’ll still need some custom work for:
.dynamic-section, .inner, etc.)Tattoo turned out to be an excellent long-term choice for your use case because:
Switching to a more elaborated theme would probably save you some CSS work (especially spacing and embed basics), but it would also introduce the risk of having to fight against built-in styles — which can sometimes be more frustrating than starting from minimal.
If you ever do switch, I would recommend:
.dynamic-section, .inner, .content-embed, .separator-wrapper classes/structure as much as possiblemodal-embed.js (it’s now decoupled and theme-agnostic)So overall — Tattoo was a very good decision, and we’re in a strong position for future changes (whether we stay or switch).
What do you think — happy to keep Tattoo long-term, or already considering a switch sometime later? 😊
Here's my honest, detailed evaluation for your use case (migrating from Tattoo, with heavy dynamic content, custom embeds, modals, separators, etc.):
100vw + calc hacks.!important rules..video-wrapper), your .content-embed wrapper might need adjustment..dynamic-section .inner structure will still need custom CSS to fit perfectly — not automatic..dynamic-section, keeping your modal/bee logicIf you want a visually stunning, more professional look without reinventing the wheel on basics — yes, Nordic Wall would be a good upgrade.
It’s one of the nicer premium themes, and the effort to adapt your dynamic system would be manageable (less than starting from scratch on another minimal theme).
If you want to keep maximum control and avoid fighting defaults — stay with Tattoo. It’s proven to work perfectly for your needs now.
In short: Nordic Wall is a solid, beautiful paid option — worth it if aesthetics are a priority, but expect some override work (not as plug-and-play as hoped, but much less painful than old Square).
Let me know if you want to explore other themes or dive into migration planning! 😊
Current time: January 12, 2026 3:15 PM HKT
Debug: Modal script loaded (Tattoo mode) Debug: contentCode is not an array or unavailable Debug: Modal elements found - enabling modal functionality Debug: Modal trigger clicked: modal-burnout.php Debug: Trigger clicked, link: modal-burnout.php Debug: Fetching absolute link: https://www.claudioflute.live/modal-burnout.php?modal=1
modal-embed.js?v=1768196983%22:1 modal-embed.js LOADED SUCCESSFULLY (Tattoo version)
modal-embed.js?v=1768196983%22:217 Uncaught ReferenceError: DEBUG is not defined
at modal-embed.js?v=1768196983%22:217:8
(anonymous) @ modal-embed.js?v=1768196983%22:217
modal-embed.js?v=1768196983%22:27 Debug: Modal script loaded (Tattoo mode)
modal-embed.js?v=1768196983%22:27 Debug: contentCode is not an array or unavailable
modal-embed.js?v=1768196983%22:27 Debug: Modal elements found - enabling modal functionality
modal-embed.js?v=1768196983%22:150 Uncaught ReferenceError: closeModal is not defined
at HTMLDocument.<anonymous> (modal-embed.js?v=1768196983%22:150:44)
(anonymous) @ modal-embed.js?v=1768196983%22:150(index):1 Tracking Prevention blocked access to storage for https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription.
(index):1 Tracking Prevention blocked access to storage for https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription.
(index):1 Tracking Prevention blocked access to storage for https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription.
(index):1 Tracking Prevention blocked access to storage for https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription.
modal-embed.js?v=1768196983%22:27 Debug: Modal trigger clicked: modal-burnout.php
modal-embed.js?v=1768196983%22:27 Debug: Trigger clicked, link: modal-burnout.php
modal-embed.js?v=1768196983%22:27 Debug: Fetching absolute link: https://www.claudioflute.live/modal-burnout.php?modal=1
modal-embed.js?v=1768196983%22:88 Uncaught ReferenceError: openModal is not defined
at HTMLAnchorElement.<anonymous> (modal-embed.js?v=1768196983%22:88:17)
(anonymous) @ modal-embed.js?v=1768196983%22:88
Debug: Modal script loaded (Tattoo mode) Debug: contentCode is not an array or unavailable Debug: Modal elements found - enabling modal functionality Debug: Modal trigger clicked: modal-burnout.php Debug: Trigger clicked, link: modal-burnout.php Debug: Fetching absolute link: https://www.claudioflute.live/modal-burnout.php?modal=1Direct opening https://www.claudioflute.live/modal-burnout.php?modal=1 can show the contents correctly.
(index):1 Tracking Prevention blocked access to storage for https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription.
(index):1 Tracking Prevention blocked access to storage for https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription.
(index):1 Tracking Prevention blocked access to storage for https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription.
(index):1 Tracking Prevention blocked access to storage for https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription.
modal-embed.js?v=1768197606%22:1 modal-embed.js LOADED SUCCESSFULLY (Tattoo version)
modal-embed.js?v=1768197606%22:27 Debug: Modal script loaded (Tattoo mode)
modal-embed.js?v=1768197606%22:27 Debug: contentCode is not an array or unavailable
modal-embed.js?v=1768197606%22:27 Debug: Modal elements found - enabling modal functionality
[NEW] Explain Console errors by using Copilot in Edge: click to explain an error.
Learn more
Don't show againmodal-embed.js?v=1768197606%22:27 Debug: Modal trigger clicked: modal-burnout.php
modal-embed.js?v=1768197606%22:27 Debug: Trigger clicked, link: modal-burnout.php
modal-embed.js?v=1768197606%22:27 Debug: Fetching absolute link: https://www.claudioflute.live/modal-burnout.php?modal=1
modal-embed.js?v=1768197606%22:221 Uncaught ReferenceError: DEBUG is not defined
at openModal (modal-embed.js?v=1768197606%22:221:5)
at HTMLAnchorElement.<anonymous> (modal-embed.js?v=1768197606%22:132:17)
openModal @ modal-embed.js?v=1768197606%22:221
(anonymous) @ modal-embed.js?v=1768197606%22:132Does this give new hints?
Uncaught ReferenceError: DEBUG is not defined at HTMLSpanElement.closeModal (modal-embed.js?v=1768198903%22:234:5)Nothing is about the fetching in the message.
closeModal() is called (likely from the close button or outside click).DEBUG is not defined in the scope of closeModal() — this causes a crash inside the function..then() / .catch() handlers.This is a scope/definition issue — DEBUG is defined inside DOMContentLoaded, but closeModal() is defined outside (global), so it doesn't have access to DEBUG.
closeModal (and openModal) Inside the ScopeUpdate modal-embed.js by moving both openModal and closeModal inside the DOMContentLoaded function (so they inherit the local DEBUG variable).
Replace the entire file with this corrected version (only change is moving the two functions inside — nothing else removed or optimized):
console.log("modal-embed.js LOADED SUCCESSFULLY (Tattoo version)");
document.addEventListener('DOMContentLoaded', function() {
const DEBUG = true; // ← Change to false when done testing
let debugLog = null;
if (DEBUG) {
debugLog = document.createElement('div');
debugLog.id = 'debug-log';
debugLog.style.position = 'fixed';
debugLog.style.bottom = '10px';
debugLog.style.left = '10px';
debugLog.style.background = 'rgba(0, 0, 0, 0.8)';
debugLog.style.color = 'white';
debugLog.style.padding = '10px';
debugLog.style.zIndex = '1000';
debugLog.style.maxHeight = '200px';
debugLog.style.overflowY = 'auto';
document.body.appendChild(debugLog);
}
function logDebug(message, color = 'green') {
if (DEBUG && debugLog) {
debugLog.insertAdjacentHTML('beforeend', `<div style="color: ${color}; font-weight: bold;">Debug: ${message}</div>`);
debugLog.scrollTop = debugLog.scrollHeight;
}
if (DEBUG && console) console.log(`Debug: ${message}`);
}
if (DEBUG) logDebug('Modal script loaded (Tattoo mode)');
// Embed rendering function (always run)
function renderEmbed(item, index, total) {
const container = document.getElementById(`embed-${item.id}`);
if (!container) {
if (DEBUG) logDebug(`Container not found for embed ID ${item.id}`, 'red');
return;
}
if (item.type === 'YouTube') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered YouTube embed for ID ${item.id} (sandbox removed)`, 'orange');
} else if (item.type === 'PayPal') {
// PayPal logic (your original)
const divMatch = item.code.match(/<div\s+[^>]*id="[^"]+"[^>]*>/i);
const buttonMatch = item.code.match(/<script>\s*paypal\.Buttons\([\s\S]*?\)\.render\(['"][^'"]+['"]\);\s*<\/script>/i);
if (divMatch && buttonMatch) {
const uniqueId = `paypal-button-container-${item.id}`;
let divContent = divMatch[0].replace(/id="[^"]+"/, `id="${uniqueId}"`);
container.innerHTML = divContent;
container.querySelector(`#${uniqueId}`).style.cssText = 'width: 100% !important; min-width: 300px !important; max-width: 300px !important; margin: 0 auto !important;';
function waitForSDK(callback) {
if (typeof paypal !== 'undefined') {
callback();
} else {
setTimeout(() => waitForSDK(callback), 100);
}
}
waitForSDK(() => {
let scriptContent = buttonMatch[0].replace(/^<script>|<\/script>$/g, '').trim();
scriptContent = scriptContent.replace(/\.render\(['"][^'"]+['"]\)/, `.render('#${uniqueId}')`);
const uniqueScript = `(function() { ${scriptContent} })();`;
const buttonScript = document.createElement('script');
buttonScript.textContent = uniqueScript;
document.body.appendChild(buttonScript);
});
}
}
}
// Run embed rendering on every page
if (window.contentCode && Array.isArray(window.contentCode)) {
window.contentCode.forEach((item, index) => {
renderEmbed(item, index, window.contentCode.length);
});
} else if (DEBUG) {
logDebug('contentCode is not an array or unavailable', 'red');
}
// Modal logic only if elements exist
const modal = document.getElementById('story-modal');
const modalBody = document.getElementById('modal-body');
const closeBtn = document.querySelector('.modal-close');
const triggers = document.querySelectorAll('.modal-trigger');
if (modal && modalBody && closeBtn && triggers.length > 0) {
if (DEBUG) logDebug('Modal elements found - enabling modal functionality');
triggers.forEach(trigger => {
trigger.addEventListener('click', function(e) {
e.preventDefault();
if (DEBUG) logDebug(`Modal trigger clicked: ${this.getAttribute('data-modal-id') || this.getAttribute('data-modal-link')}`, 'green');
const modalId = this.getAttribute('data-modal-id');
if (modalId) {
const contentDiv = document.getElementById(modalId);
if (contentDiv) {
const content = contentDiv.innerHTML;
if (DEBUG) logDebug('Stories modal triggered, content from ID: ' + modalId);
openModal(content);
} else {
openModal('<p>Content not found.</p>');
if (DEBUG) logDebug('Modal content not found for ID: ' + modalId, 'red');
}
return;
}
const link = this.getAttribute('data-modal-link');
if (DEBUG) logDebug('Trigger clicked, link: ' + link);
if (!link || link === '#' || link.includes('index.php')) {
openModal('<p>This link cannot be loaded in a modal to prevent recursive loading.</p>');
return;
}
let absoluteLink;
try {
absoluteLink = new URL(link, window.location.origin).href;
} catch (error) {
openModal('<p>Invalid URL.</p>');
return;
}
absoluteLink += (absoluteLink.includes('?') ? '&' : '?') + 'modal=1';
if (DEBUG) logDebug('Fetching absolute link: ' + absoluteLink);
openModal('<p>Loading...</p>');
fetch(absoluteLink, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (DEBUG) logDebug('Fetch response status: ' + response.status + ' ' + response.statusText);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return response.text();
})
.then(data => {
if (DEBUG) logDebug('Fetched HTML length: ' + data.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
let combinedContent = '';
const sections = doc.querySelectorAll('.modal-story-content');
if (DEBUG) logDebug('Found ' + sections.length + ' modal-story-content sections');
if (sections.length > 0) {
sections.forEach((section, index) => {
combinedContent += section.outerHTML.trim();
if (index < sections.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
// Tattoo-specific fallback
const tattooFallback = doc.querySelectorAll('.dynamic-section .inner');
if (DEBUG) logDebug('Found ' + tattooFallback.length + ' Tattoo .dynamic-section .inner sections');
if (tattooFallback.length > 0) {
tattooFallback.forEach((section, index) => {
const newContent = document.createElement('div');
newContent.className = 'modal-story-content';
newContent.innerHTML = section.innerHTML;
combinedContent += newContent.outerHTML.trim();
if (index < tattooFallback.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
if (DEBUG) logDebug('No content found in Tattoo structure', 'red');
combinedContent = '<p>Content could not be parsed from the page.</p>';
}
}
if (combinedContent) {
openModal(combinedContent);
} else {
openModal('<p>No content found.</p>');
}
})
.catch(error => {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
openModal('<p>Sorry, the content could not be loaded: ' + error.message + '</p>');
});
});
});
closeBtn.addEventListener('click', closeModal);
window.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && modal.style.display === 'block') {
closeModal();
}
});
} else {
if (DEBUG) logDebug('Modal elements missing - skipping modal functionality (normal on non-modal pages)', 'orange');
}
// Define openModal and closeModal inside the scope so they have access to DEBUG
function openModal(content) {
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = content;
const modal = document.getElementById('story-modal');
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
const closeBtn = document.querySelector('.modal-close');
if (closeBtn) closeBtn.focus();
if (DEBUG) logDebug('Modal opened');
}
function closeModal() {
const modal = document.getElementById('story-modal');
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = '';
if (DEBUG) logDebug('Modal closed');
}
});
openModal and closeModal inside the DOMContentLoaded callback so they inherit the local DEBUG variable.modal-embed.js with the above code.If you see "Fetch response status" → success! Modal should populate. If still no further lines → share the full console (including any red errors).
This one scope fix should eliminate the ReferenceError and let the fetch complete.
Let me know the new console after clicking — we're fixing the final blocker! 😊
Current time: January 12, 2026 4:15 PM HKT
The message you're seeing in the modal:
"Modal content could not be loaded: Network does not respond : Not Found"
is not three separate errors — it's one single error message composed of three parts that are concatenated together in the code. Let me break it down clearly so you understand exactly what it means and why it appears, and then we can improve the wording to make it less alarming for your users.
The full sentence comes from this line in your JS (inside the .catch block):
openModal('<p>Sorry, the content could not be loaded: ' + error.message + '</p>');
When the fetch fails, error.message contains the actual reason from the browser/server. In your case, the browser is reporting:
"Network does not respond : Not Found"
This is one error message — the browser's way of saying:
So the complete message users see is:
"Sorry, the content could not be loaded: Network does not respond : Not Found"
→ Translation:
The requested modal page returned a 404 Not Found error.
When clicking a button like "關於CLAUDIO" → JS tries to fetch:
https://www.claudioflute.live/modal-profile.php?modal=1
(or similar URL)
The server responds with 404 Not Found — meaning:
modal-profile.php does not exist at that exact pathThis is not a JS or PHP bug — it's a file/path configuration issue on the server.
Confirm the file exists
Check Publii file placement
modal-profile.php in the root or correct subfolderdata-modal-link attribute matches the real relative path (e.g., if it's in /pages/modal-profile.php, the link should be /pages/modal-profile.php)Fix in PHP if needed
absoluteLink = new URL('/pages/' + link, window.location.origin).href; // example if in /pages/
To make it less scary and more helpful for end users (especially the chaplaincy team), we can rewrite the message to be friendly and actionable.
Replace the .catch block in modal-embed.js with this improved version:
.catch(error => {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
let userMessage = '抱歉,暫時未能載入內容。';
if (error.message.includes('Not Found') || error.message.includes('404')) {
userMessage += ' 頁面可能不存在或連結有誤。';
} else if (error.message.includes('Network')) {
userMessage += ' 請檢查網絡連接是否正常。';
} else {
userMessage += ' 請稍後再試或聯絡管理員。';
}
userMessage += '<br><small>(技術錯誤:' + error.message + ')</small>';
openModal('<p style="color:#d32f2f; text-align:center;">' + userMessage + '</p>');
});
What users will see now (much friendlier):
抱歉,暫時未能載入內容。頁面可能不存在或連結有誤。
請檢查網絡連接是否正常,或稍後再試。
(技術錯誤:Network does not respond : Not Found)
You can customize the Chinese text further (e.g., add contact email).
modal-burnout.php (or whatever file) exists at the correct path..catch block as above → much less panicking for end users.Quick test:
data-modal-link value (inspect the button HTML)Once the file is accessible, the modal will populate correctly.
Share the result of the direct URL test — we're literally one path fix away from full success! 😊
Current time: January 13, 2026 1:15 PM HKT
modal-burnout.php, modal-profile.php etc.) has not been generated/published in Publii, so the URL returns 404. That is the most common cause of exactly this symptom.We can make the user-facing error message much friendlier, clearer, and less alarming, while still keeping a concise technical detail line for troubleshooting (and for you/the chaplaincy team to quickly understand it's a 404).
User sees:
抱歉,暫時未能載入內容。
該頁面可能尚未發佈或連結有誤,請稍後再試或聯絡管理員。
(技術錯誤:HTTP 404 – 頁面未找到)
Find the .catch(error => { ... }) block in your modal-embed.js (inside the fetch chain) and replace it with this improved version:
.catch(error => {
if (DEBUG) logDebug('Fetch error: ' + error.message + ' (status likely 404)', 'red');
let userMessage = '抱歉,暫時未能載入內容。<br>';
userMessage += '該頁面可能尚未發佈或連結有誤,請稍後再試或聯絡管理員。';
let techDetail = '未知錯誤';
if (error.message.includes('404') || error.message.includes('Not Found')) {
techDetail = 'HTTP 404 – 頁面未找到';
} else if (error.message.includes('Network') || error.message.includes('Failed to fetch')) {
techDetail = '網絡連接問題';
} else {
techDetail = error.message;
}
userMessage += '<br><small style="color:#666; font-size:0.9em;">(技術錯誤:' + techDetail + ')</small>';
openModal('<p style="text-align:center; color:#333;">' + userMessage + '</p>');
});
Normal 404 case (most common):
抱歉,暫時未能載入內容。
該頁面可能尚未發佈或連結有誤,請稍後再試或聯絡管理員。
(技術錯誤:HTTP 404 – 頁面未找到)
Network issue:
抱歉,暫時未能載入內容。
該頁面可能尚未發佈或連結有誤,請稍後再試或聯絡管理員。
(技術錯誤:網絡連接問題)
Other unexpected error:
抱歉,暫時未能載入內容。
該頁面可能尚未發佈或連結有誤,請稍後再試或聯絡管理員。
(技術錯誤:Failed to fetch)
This version is:
If you want to add a "Close" button inside the error modal (so users can dismiss it easily):
Add this line right before the closing </p> in userMessage:
userMessage += '<br><button onclick="document.getElementById(\'story-modal\').style.display=\'none\'" style="margin-top:1rem; padding:0.5rem 1rem; background:#A6752E; color:white; border:none; border-radius:4px; cursor:pointer;">關閉</button>';
This adds a nice brown "關閉" button inside the error message.
.catch block in modal-embed.js with the new version above.Let me know how it looks — if you want any wording tweaks (e.g., more polite, add email contact), I can refine it further.
Once the error message is nice, we can turn DEBUG = false to hide the floating panel for production.
Current time: January 13, 2026 01:15 PM HKT
"抱歉,暫時未能載入內容。該頁面可能尚未發佈或連結有誤,請稍後再試或聯絡管理員。 (技術錯誤:"
part is in Chinese. I want to rewrite the message as:"未能載入內容。請檢查該頁面是否已經發佈,或連結檔案位置是否正確。(錯誤詳情:)"
For the technical details, let's use:HTTP status 404 Network disconnected Failed to fetch for unknown reasons
.catch(error => {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
openModal('<p>Modal content could not be loaded: ' + error.message + '</p>');
});
Do you mean I need to expand this with your provided new JS?
The original .catch block you quoted is very short and basic:
.catch(error => {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
openModal('<p>Modal content could not be loaded: ' + error.message + '</p>');
});
It works, but:
So yes, you should replace that short block with the improved version I suggested earlier. This keeps the same logic (log debug + show error in modal), but makes the user-facing message much nicer, clearer, and less alarming.
Replace the entire .catch block with this:
.catch(error => {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
let userMessage = '未能載入內容。<br>';
userMessage += '請檢查該頁面是否已經發佈,或連結檔案位置是否正確。<br>';
userMessage += '(錯誤詳情:';
let techDetail = 'Failed to fetch for unknown reasons';
if (error.message.includes('404') || error.message.includes('Not Found')) {
techDetail = 'HTTP status 404';
} else if (error.message.includes('Network') || error.message.includes('timeout') || error.name === 'AbortError') {
techDetail = 'Network disconnected';
} else {
techDetail = 'Failed to fetch for unknown reasons';
}
userMessage += techDetail + ')';
openModal('<p style="text-align:center; color:#333;">' + userMessage + '</p>');
});
If you want users to be able to dismiss the error easily (instead of clicking the × or outside):
Add this line before the closing </p>:
userMessage += '<br><button onclick="document.getElementById(\'story-modal\').style.display=\'none\'" style="margin-top:1rem; padding:0.6rem 1.2rem; background:#A6752E; color:white; border:none; border-radius:6px; cursor:pointer; font-size:1rem;">關閉</button>';
Final message example for 404:
未能載入內容。
請檢查該頁面是否已經發佈,或連結檔案位置是否正確。
(錯誤詳情:HTTP status 404)
[關閉] ← nice brown button
.catch block with the new one.Once updated, test with a broken link → the modal should show the friendly message instead of raw English/translated mess.
Let me know how it looks, or if you want any wording tweak (e.g., add email/phone for "聯絡管理員"). 😊
Regarding your request to expand the maximum number of allowed dynamic sections from 10 to 30 (or more) in pull-contents.php:
There is actually no hard-coded limit of 10 (or any fixed number) in the code itself.
The current code (both in V2.74-2 and all later versions we’ve worked on) has no artificial cap on the number of sections/positions/groups. It will render as many sections as exist in the database for the current page, no matter if it's 8, 30, 100 or more.
The perception of a "10-section limit" usually comes from one of these sources (not from the PHP code):
AppGini / Admin UI limitation — the dropdown or repeater field in AppGini that you use to manage web_content entries might have a default/recommended max of 10 positions. This is not enforced by PHP; it's just the admin interface suggesting or limiting the UI choices. You can usually override this in AppGini by:
position field settingsPerformance / Browser rendering — even if PHP outputs 30+ sections, very long pages can feel slow or crash the browser due to too much DOM. But this is not a PHP limit — it's browser memory.
Publii generation limit (very rare) — Publii itself doesn't impose a hard limit on dynamic PHP output, but very large pages might timeout during generation. Again — not a 10 limit.
Your current pull-contents.php (V2.74-2 or any later version we’ve built) already supports unlimited sections (as long as they exist in the database).
The only place you need to adjust is in the AppGini admin UI — increase the allowed values for the position field to 30 (or remove the limit).
If you want to prevent accidental overload (e.g., >50 sections could slow the site), you can add a simple check in PHP (but it's optional — not required).
Example (add right after grouping):
if (count($sections) > 50 && DEBUG) {
echo "<div style='color: red; font-weight: bold; text-align: center;'>Warning: Too many sections (" . count($sections) . ") — performance may be affected.</div>";
}
But again — no hard limit exists in the code. You can already use 30+ sections today.
position field limit in AppGini admin (this is the only change needed)Let me know how many sections you plan to use for the webtoon-style pages — happy to help optimize spacing/loading for very long content too!
Current time: January 13, 2026 01:45 PM HKT
However, based on our experience and common real-world issues, here are the main reasons why a YouTube embed might appear to fail specifically inside a modal (even though it works fine on normal pages):
Lazy-loading / iframe not initialized
display: none) when inserted, so YouTube doesn't initialize it properly.Modal CSS overrides
overflow: hidden or height: 0 on parent containers (to prevent scroll), which collapses the iframe..modal-content, .modal-body, and .content-embed all have proper height/overflow rules.Fetch timing / DOM insertion
<div class="content-embed"> is inserted via innerHTML after fetch, but the JS renderEmbed (which cleans the code and removes sandbox) runs on page load, not when modal opens.renderEmbed never sees it.Browser security / CSP
To make YouTube work reliably inside the modal, add this small block inside the openModal(content) function, right after modalBody.innerHTML = content;:
// Re-run embed rendering after modal content is inserted
setTimeout(() => {
if (window.contentCode && Array.isArray(window.contentCode)) {
window.contentCode.forEach((item, index) => {
renderEmbed(item, index, window.contentCode.length);
});
if (DEBUG) logDebug('Re-ran embed rendering after modal open', 'orange');
}
}, 100); // Small delay to ensure DOM is updated
This:
renderEmbed specifically for any embeds inside the modalTo prevent collapse:
.modal-body .content-embed {
min-height: 315px !important; /* YouTube default height */
height: auto !important;
}
.modal-body iframe {
width: 100% !important;
height: 100% !important;
}
setTimeout block to openModal in modal-embed.jsThis should make YouTube work inside modals reliably — it's a very common need, and the re-run + timeout fixes it 99% of the time.
Let me know what happens after adding this — if the video shows, we're golden! If not, share the new console output after modal open. 😊
Current time: January 13, 2026 02:15 PM HKT
Your original JS has this key part (near the end, after all the modal logic):
// Run embed rendering on every page
if (window.contentCode && Array.isArray(window.contentCode)) {
window.contentCode.forEach((item, index) => {
renderEmbed(item, index, window.contentCode.length);
});
} else if (DEBUG) {
logDebug('contentCode is not an array or unavailable', 'red');
}
This block runs immediately when the script loads — it checks for window.contentCode (pushed by PHP) and renders embeds right away.
When you added my suggested lines before this block:
if (window.debugFromPHP) {
console.log('PHP Debug Info:', window.debugFromPHP);
if (DEBUG) logDebug('PHP → Fetched sections: ' + window.debugFromPHP.fetched + ', Grouped positions: ' + window.debugFromPHP.grouped + ', Modal request: ' + window.debugFromPHP.modalRequest, 'purple');
} else {
console.warn('No debug info from PHP');
}
...the browser parses and executes the script top to bottom. If there is any syntax error, reference error, or exception in the code before the embed rendering block, the entire script stops executing at that point — so the embed rendering code never runs.
In your case, the added block is safe (no syntax error), but there is likely a small reference error or timing issue when it runs:
window.debugFromPHP may not exist yet (PHP echo happens after JS load in some cases)DEBUG is not in scope (if you moved things around)Result: The embed rendering block never executes → YouTube stays as empty <div class="content-embed">.
To prevent the new debug block from breaking the rest of the script, wrap it in a try-catch and move it after the embed rendering block (so embed rendering always runs first, even if debug fails).
Updated modal-embed.js (only change is order + try-catch — nothing removed):
console.log("modal-embed.js LOADED SUCCESSFULLY (Tattoo version)");
document.addEventListener('DOMContentLoaded', function() {
const DEBUG = true; // ← Change to false when done testing
let debugLog = null;
if (DEBUG) {
debugLog = document.createElement('div');
debugLog.id = 'debug-log';
debugLog.style.position = 'fixed';
debugLog.style.bottom = '10px';
debugLog.style.left = '10px';
debugLog.style.background = 'rgba(0, 0, 0, 0.8)';
debugLog.style.color = 'white';
debugLog.style.padding = '10px';
debugLog.style.zIndex = '1000';
debugLog.style.maxHeight = '200px';
debugLog.style.overflowY = 'auto';
document.body.appendChild(debugLog);
}
function logDebug(message, color = 'green') {
if (DEBUG && debugLog) {
debugLog.insertAdjacentHTML('beforeend', `<div style="color: ${color}; font-weight: bold;">Debug: ${message}</div>`);
debugLog.scrollTop = debugLog.scrollHeight;
}
if (DEBUG && console) console.log(`Debug: ${message}`);
}
if (DEBUG) logDebug('Modal script loaded (Tattoo mode)');
// Embed rendering function (always run first)
function renderEmbed(item, index, total) {
const container = document.getElementById(`embed-${item.id}`);
if (!container) {
if (DEBUG) logDebug(`Container not found for embed ID ${item.id}`, 'red');
return;
}
if (item.type === 'YouTube') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered YouTube embed for ID ${item.id} (sandbox removed)`, 'orange');
} else if (item.type === 'PayPal') {
// Your original PayPal logic (unchanged)
const divMatch = item.code.match(/<div\s+[^>]*id="[^"]+"[^>]*>/i);
const buttonMatch = item.code.match(/<script>\s*paypal\.Buttons\([\s\S]*?\)\.render\(['"][^'"]+['"]\);\s*<\/script>/i);
if (divMatch && buttonMatch) {
const uniqueId = `paypal-button-container-${item.id}`;
let divContent = divMatch[0].replace(/id="[^"]+"/, `id="${uniqueId}"`);
container.innerHTML = divContent;
container.querySelector(`#${uniqueId}`).style.cssText = 'width: 100% !important; min-width: 300px !important; max-width: 300px !important; margin: 0 auto !important;';
function waitForSDK(callback) {
if (typeof paypal !== 'undefined') {
callback();
} else {
setTimeout(() => waitForSDK(callback), 100);
}
}
waitForSDK(() => {
let scriptContent = buttonMatch[0].replace(/^<script>|<\/script>$/g, '').trim();
scriptContent = scriptContent.replace(/\.render\(['"][^'"]+['"]\)/, `.render('#${uniqueId}')`);
const uniqueScript = `(function() { ${scriptContent} })();`;
const buttonScript = document.createElement('script');
buttonScript.textContent = uniqueScript;
document.body.appendChild(buttonScript);
});
}
}
}
// Run embed rendering on every page (this must come first)
if (window.contentCode && Array.isArray(window.contentCode)) {
window.contentCode.forEach((item, index) => {
renderEmbed(item, index, window.contentCode.length);
});
} else if (DEBUG) {
logDebug('contentCode is not an array or unavailable', 'red');
}
// PHP debug info (safe try-catch so it never breaks the rest)
try {
if (window.debugFromPHP) {
console.log('PHP Debug Info:', window.debugFromPHP);
if (DEBUG) logDebug('PHP → Fetched sections: ' + window.debugFromPHP.fetched + ', Grouped positions: ' + window.debugFromPHP.grouped + ', Modal request: ' + window.debugFromPHP.modalRequest, 'purple');
} else {
console.warn('No debug info from PHP');
}
} catch (e) {
console.warn('Error reading PHP debug info:', e);
}
// Modal logic only if elements exist (rest of your code unchanged)
const modal = document.getElementById('story-modal');
const modalBody = document.getElementById('modal-body');
const closeBtn = document.querySelector('.modal-close');
const triggers = document.querySelectorAll('.modal-trigger');
if (modal && modalBody && closeBtn && triggers.length > 0) {
if (DEBUG) logDebug('Modal elements found - enabling modal functionality');
triggers.forEach(trigger => {
trigger.addEventListener('click', function(e) {
e.preventDefault();
if (DEBUG) logDebug(`Modal trigger clicked: ${this.getAttribute('data-modal-id') || this.getAttribute('data-modal-link')}`, 'green');
const modalId = this.getAttribute('data-modal-id');
if (modalId) {
const contentDiv = document.getElementById(modalId);
if (contentDiv) {
const content = contentDiv.innerHTML;
if (DEBUG) logDebug('Stories modal triggered, content from ID: ' + modalId);
openModal(content);
} else {
openModal('<p>Content not found.</p>');
if (DEBUG) logDebug('Modal content not found for ID: ' + modalId, 'red');
}
return;
}
const link = this.getAttribute('data-modal-link');
if (DEBUG) logDebug('Trigger clicked, link: ' + link);
if (!link || link === '#' || link.includes('index.php')) {
openModal('<p>This link cannot be loaded in a modal to prevent recursive loading.</p>');
return;
}
let absoluteLink;
try {
absoluteLink = new URL(link, window.location.origin).href;
} catch (error) {
openModal('<p>Invalid URL.</p>');
return;
}
absoluteLink += (absoluteLink.includes('?') ? '&' : '?') + 'modal=1';
if (DEBUG) logDebug('Fetching absolute link: ' + absoluteLink);
openModal('<p>Loading...</p>');
fetch(absoluteLink, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (DEBUG) logDebug('Fetch response status: ' + response.status + ' ' + response.statusText);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return response.text();
})
.then(data => {
if (DEBUG) logDebug('Fetched HTML length: ' + data.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
let combinedContent = '';
const sections = doc.querySelectorAll('.modal-story-content');
if (DEBUG) logDebug('Found ' + sections.length + ' modal-story-content sections');
if (sections.length > 0) {
sections.forEach((section, index) => {
combinedContent += section.outerHTML.trim();
if (index < sections.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
// Tattoo-specific fallback
const tattooFallback = doc.querySelectorAll('.dynamic-section .inner');
if (DEBUG) logDebug('Found ' + tattooFallback.length + ' Tattoo .dynamic-section .inner sections');
if (tattooFallback.length > 0) {
tattooFallback.forEach((section, index) => {
const newContent = document.createElement('div');
newContent.className = 'modal-story-content';
newContent.innerHTML = section.innerHTML;
combinedContent += newContent.outerHTML.trim();
if (index < tattooFallback.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
if (DEBUG) logDebug('No content found in Tattoo structure', 'red');
combinedContent = '<p>Content could not be parsed from the page.</p>';
}
}
if (combinedContent) {
openModal(combinedContent);
} else {
openModal('<p>No content found.</p>');
}
})
.catch(error => {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
openModal('<p>Sorry, the content could not be loaded: ' + error.message + '</p>');
});
});
});
closeBtn.addEventListener('click', closeModal);
window.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && modal.style.display === 'block') {
closeModal();
}
});
} else {
if (DEBUG) logDebug('Modal elements missing - skipping modal functionality (normal on non-modal pages)', 'orange');
}
// Define openModal and closeModal inside the scope so they have access to DEBUG
function openModal(content) {
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = content;
const modal = document.getElementById('story-modal');
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
const closeBtn = document.querySelector('.modal-close');
if (closeBtn) closeBtn.focus();
if (DEBUG) logDebug('Modal opened');
}
function closeModal() {
const modal = document.getElementById('story-modal');
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = '';
if (DEBUG) logDebug('Modal closed');
}
});
try { ... } catch (e) { console.warn(...) } — prevents any error from crashing the rest of the script.modal-embed.jsThis way:
Let me know the console output on the video page — especially whether "Rendered YouTube embed" appears now.
Current time: January 13, 2026 04:45 PM HKT
We’re fixing the last detail! 😊
Debug: Modal script loaded (Tattoo mode) Debug: Rendered YouTube embed for ID 14 (sandbox removed) Debug: Rendered YouTube embed for ID 20 (sandbox removed) Debug: PHP → Fetched sections: 10, Grouped positions: 10, Modal request: false Debug: Modal elements found - enabling modal functionality
The YT embed video works from the page but not shown in modal, same as before.
modal-embed.js?v=1768293050:288 Uncaught SyntaxError: Unexpected token ')' (at modal-embed.js?v=1768293050:288:2) favicon.ico:1 GET https://www.claudioflute.live/favicon.ico 404 (Not Found)
modal-embed.js?v=1768293280:272 Uncaught SyntaxError: Unexpected end of input (at modal-embed.js?v=1768293280:272:2)
modal-embed.js?v=1768295609:98
No debug info from PHP
(anonymous) @ modal-embed.js?v=1768295609:98
modal-embed.js?v=1768295609:29 Debug: Modal elements found - enabling modal functionality
favicon.ico:1 GET https://www.claudioflute.live/favicon.ico 404 (Not Found)
modal-embed.js?v=1768295609:29 Debug: Modal trigger clicked: landing-260107.php
modal-embed.js?v=1768295609:29 Debug: Trigger clicked, link: landing-260107.php
modal-embed.js?v=1768295609:29 Debug: Fetching absolute link: https://www.claudioflute.live/landing-260107.php?modal=1
modal-embed.js?v=1768295609:29 Debug: Modal opened
modal-embed.js?v=1768295609:29 Debug: Fetch response status: 200 OK
modal-embed.js?v=1768295609:29 Debug: Fetched HTML length: 6352
modal-embed.js?v=1768295609:29 Debug: Found 2 modal-story-content sections
modal-embed.js?v=1768295609:29 Debug: Modal opened
modal-embed.js?v=1768295609:29 Debug: Found 1 embeds inside modal – re-rendering
modal-embed.js?v=1768295609:29 Debug: No matching contentCode for embed-3 in modal
modal-embed.js?v=1768295609:29 Debug: Found 1 embeds inside modal – re-rendering
modal-embed.js?v=1768295609:29 Debug: No matching contentCode for embed-3 in modal
modal-embed.js?v=1768295609:29 Debug: Modal closed
Again no YT embed video inside the modal. The
modal-embed.js?v=1768295609:98
No debug info from PHP
(anonymous) @ modal-embed.js?v=1768295609:98lines
look like new!
Everything is now loading, fetching, parsing, and detecting the embed correctly. The modal is receiving content (200 OK, 6352 bytes, 2 sections found), the re-render is triggering ("Found 1 embeds inside modal"), but it still fails at matching ID 3.
The new line you noticed:
No debug info from PHP
is expected — it's from the try-catch block I added earlier (safe warning when window.debugFromPHP is missing). It's harmless and confirms the script is running normally.
<div id="embed-3"> is correctly inserted into the modalwindow.contentCode.find(i => i.id == id) returns null for ID 3window.contentCode only contains main page embeds (e.g. IDs 14, 20)window.contentCode because the fetched HTML is static — no JS execution on the fetched pagerenderEmbed skips → empty divWe need to parse the fetched HTML for the <script> tag that pushes to window.contentCode and extract the embed data.
Update modal-embed.js — replace the entire .then(data => { ... }) block (inside the fetch chain) with this:
.then(data => {
if (DEBUG) logDebug('Fetched HTML length: ' + data.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
// Extract embed data from fetched HTML's script tags
let modalContentCode = [];
const scripts = doc.querySelectorAll('script');
scripts.forEach(script => {
const text = script.textContent || '';
if (text.includes('window.contentCode.push')) {
// Extract the pushed object string
const pushMatch = text.match(/window\.contentCode\.push\((.*?)\);/);
if (pushMatch) {
const objStr = pushMatch[1];
try {
// Parse the object string (safe eval for JSON-like)
const item = eval('(' + objStr + ')');
modalContentCode.push(item);
if (DEBUG) logDebug('Extracted modal embed item: ID ' + item.id + ', type ' + item.type, 'purple');
} catch (e) {
if (DEBUG) logDebug('Failed to parse modal embed push: ' + e, 'red');
}
}
}
});
let combinedContent = '';
const sections = doc.querySelectorAll('.modal-story-content');
if (DEBUG) logDebug('Found ' + sections.length + ' modal-story-content sections');
if (sections.length > 0) {
sections.forEach((section, index) => {
combinedContent += section.outerHTML.trim();
if (index < sections.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
const tattooFallback = doc.querySelectorAll('.dynamic-section .inner');
if (DEBUG) logDebug('Found ' + tattooFallback.length + ' Tattoo .dynamic-section .inner sections');
if (tattooFallback.length > 0) {
tattooFallback.forEach((section, index) => {
const newContent = document.createElement('div');
newContent.className = 'modal-story-content';
newContent.innerHTML = section.innerHTML;
combinedContent += newContent.outerHTML.trim();
if (index < tattooFallback.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
if (DEBUG) logDebug('No content found in Tattoo structure', 'red');
combinedContent = '<p>Content could not be parsed from the page.</p>';
}
}
if (combinedContent) {
openModal(combinedContent);
} else {
openModal('<p>No content found.</p>');
}
// Re-run embed rendering for modal embeds
setTimeout(() => {
const modalEmbeds = modalBody.querySelectorAll('.content-embed[id^="embed-"]');
if (modalEmbeds.length > 0) {
if (DEBUG) logDebug('Found ' + modalEmbeds.length + ' embeds inside modal – re-rendering', 'orange');
modalEmbeds.forEach(embedDiv => {
const id = embedDiv.id.replace('embed-', '');
// Check both main page and extracted modal data
let item = window.contentCode?.find(i => i.id == id);
if (!item) {
item = modalContentCode.find(i => i.id == id);
}
if (item) {
renderEmbed(item, 0, 1);
if (DEBUG) logDebug('Re-rendered embed-' + id + ' inside modal', 'green');
} else {
if (DEBUG) logDebug('No matching contentCode for embed-' + id + ' in modal', 'red');
}
});
} else {
if (DEBUG) logDebug('No embeds found inside modal', 'orange');
}
}, 500);
})
window.contentCode.push(...) scripteval to safely parse the pushed object (safe because it's from your own PHP)modalContentCodewindow.contentCode and modalContentCode → finds ID 3renderEmbed → injects iframe → video shows in modal.then(data => { ... }) block in modal-embed.js with the aboveThis is the most reliable way to handle dynamic embeds in modals — extracting data from the fetched HTML itself.
Let me know the console after testing — especially any "Extracted modal embed item" line. If it works, we can turn DEBUG = false and call it done!
Current time: January 13, 2026 07:15 PM HKT
Here's a clear breakdown of the differences between:
<script> push in modal branch#modal-embed-data)| Aspect | V2.86 PHP + 2.85 JS (your current) | Latest suggestion (hidden JSON div) | Winner & Why |
|---|---|---|---|
| How embed data is passed | Via <script> tag that executes window.contentCode.push(...) |
Via hidden <div id="modal-embed-data"> with raw JSON |
JSON div wins — no JS execution needed in fetched HTML |
| When data is available | Only if the fetched HTML executes the <script> (which it doesn't reliably in fetch context) |
Immediately after parsing — no execution needed | JSON div wins — guaranteed availability |
| Risk of failure | High — fetched HTML is static, <script> tags are not executed in DOMParser context |
Low — JSON is just text in a div, parsed directly with JSON.parse |
JSON div wins — no dependency on script execution |
| Debug visibility | You see "Rendered..." only if push happens (which it doesn't) | You see "Extracted X modal embed items" even if push fails | JSON div wins — clearer failure diagnosis |
| Complexity & fragility | Fragile — relies on regex/eval to extract from script text (can break if PHP format changes) | Simple & robust — direct getElementById + JSON.parse |
JSON div wins — fewer moving parts |
| Performance | Slightly slower (parsing scripts + eval) | Faster (simple DOM lookup + JSON parse) | JSON div wins |
| Line count impact | Adds ~5 lines to modal branch in PHP | Adds ~10 lines to PHP + ~8 lines to JS | Similar |
Your current V2.86 + 2.85 setup looks like it should work because we added the push script to the modal branch. But in practice:
landing-260107.php?modal=1, it outputs the <script> tagDOMParser, the <script> tag is not executed — it's treated as plain textwindow.contentCode.push never runs in the main page context → no new item for ID 3 → "No matching contentCode"The hidden JSON div avoids this entirely — no script execution is needed; JS just reads the text content of the div.
Stick with the JSON div approach — it's more reliable and eliminates the execution dependency.
<?php
// pull-contents.php (Version 2.86 - Modal YT Embed Fixed via JSON Data)
// Last updated: January 13, 2026
// Features: title-text-video model, bee below background, modal YT re-render
define('DEBUG', true);
define('IMAGE_BASE_PATH', 'https://navi.claudioflute.live/images/');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$config_path = '../config.php';
if (!file_exists($config_path)) {
die("Error: config.php not found at $config_path");
}
require_once $config_path;
if (!isset($dbServer) || !isset($dbUsername) || !isset($dbPassword) || !isset($dbDatabase)) {
die("Error: AppGini database variables not defined in config.php");
}
$conn = new mysqli($dbServer, $dbUsername, $dbPassword, $dbDatabase, $dbPort ?: 3306);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Auto-detect page
if (isset($_GET['page'])) {
$current_page = $_GET['page'];
} else {
$calling_file = basename($_SERVER['SCRIPT_FILENAME']);
$current_page = str_replace('.php', '', $calling_file);
if ($current_page === 'index') {
$current_page = 'landing';
}
}
$status = 'Show';
// Preload PayPal SDK
$paypalSdkUrl = 'https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription';
echo "<script src=\"$paypalSdkUrl\" id=\"paypal-sdk\" async=\"false\"></script>";
// Is modal request
$is_modal_request = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || isset($_GET['modal']);
// Is stories page (for preview logic)
$is_stories_page = ($current_page === 'stories');
// SQL - matches your schema
$sql = "SELECT id, title, content, code, code_type, image, button_link, button_text, position, model, date_edited, theme_img, img_role, chk_divider
FROM web_content
WHERE page = ? AND status = ?
ORDER BY position ASC, date_edited DESC";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $current_page, $status);
$stmt->execute();
$result = $stmt->get_result();
$sections = [];
while ($row = $result->fetch_assoc()) {
$sections[] = $row;
}
// Group by position for multi-column
$sections_by_position = [];
foreach ($sections as $row) {
$position = $row['position'] ?? 1;
$sections_by_position[$position][] = $row;
}
if (!empty($sections_by_position)) {
ksort($sections_by_position);
foreach ($sections_by_position as $position => $group) {
$is_grid = count($group) > 1;
if ($is_modal_request) {
foreach ($group as $index => $row) {
echo "<div class=\"modal-story-content\">";
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
}
echo $row['content'];
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
echo "</div>";
if ($index < count($group) - 1) {
echo "<div class=\"modal-section-separator\"></div>";
}
}
} else {
// SMART CLASS for Tattoo
$section_class = 'content';
$has_background = false;
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$has_background = true;
}
}
if ($has_background) {
$section_class = 'hero';
}
if ($is_grid) {
$section_class .= ' grid';
}
$bg_style = '';
if ($section_class === 'hero') {
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$bg_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$bg_style = "style=\"background-image: url('$bg_url');\"";
break;
}
}
}
echo "<section class=\"{$section_class} dynamic-section\" {$bg_style}>";
echo "<div class=\"inner\">";
foreach ($group as $row) {
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">{$row['title']}</h2>";
}
echo $row['content'];
if ($row['model'] === 'title-text-image-video') {
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
elseif ($row['model'] === 'title-text-video') {
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
elseif ($row['model'] === 'title-text-image-code' || $row['model'] === 'title-text-code') {
if (!empty($row['image']) && $row['model'] === 'title-text-image-code') {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
}
elseif (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
// BUTTON RENDERING
if (!empty($row['button_text'])) {
$button_text = htmlspecialchars($row['button_text']);
$button_link = htmlspecialchars($row['button_link'] ?: '#');
echo "<div class=\"content__buttons\">";
if ($is_stories_page) {
$modal_id = 'modal-content-' . $row['id'];
echo "<div id=\"$modal_id\" style=\"display:none;\">";
echo '<div class="modal-story-content">';
if (!empty($row['title'])) echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image modal-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['content'])) echo $row['content'];
echo '</div>';
echo "</div>";
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-id=\"$modal_id\">$button_text</a>";
} else {
if (!empty($button_link) && $button_link !== '#') {
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-link=\"$button_link\">$button_text</a>";
} else {
echo "<a class=\"btn\" href=\"javascript:void(0)\">$button_text</a>";
}
}
echo "</div>";
}
}
echo "</div>"; // .inner
// Separator — moved AFTER section for below background
$show_separator = false;
$sep_url = '';
if (!empty($row['theme_img']) && $row['img_role'] === 'Separator') {
$sep_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$show_separator = true;
} elseif (!empty($row['chk_divider']) && $row['chk_divider'] == 1) {
$sep_url = 'https://navi.claudioflute.live/public/Flying_bee_R.gif';
$show_separator = true;
}
if ($show_separator) {
$sep_class = ($row['img_role'] === 'Separator') ? 'separator custom-separator' : 'separator default-bee';
echo "<div class=\"separator-wrapper\">";
echo "<img src=\"$sep_url\" class=\"$sep_class\" alt=\"Separator\">";
echo "</div>";
}
echo "</section>";
}
}
} else {
echo "No sections found.";
}
// Modal container (only for non-AJAX requests)
if (!$is_modal_request) {
echo '<div id="story-modal" class="modal">';
echo '<div class="modal-content">';
echo '<span class="modal-close">×</span>';
echo '<div id="modal-body"></div>';
echo '</div>';
echo '</div>';
}
// Pass embed data to modal as JSON (when modal request)
if ($is_modal_request && !empty($sections)) {
$modalEmbeds = [];
foreach ($sections as $row) {
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'PayPal'])) {
$modalEmbeds[] = [
'id' => $row['id'],
'code' => $row['code'],
'type' => $row['code_type']
];
}
}
if (!empty($modalEmbeds)) {
echo '<div id="modal-embed-data" style="display:none;">' . json_encode($modalEmbeds) . '</div>';
if (DEBUG) {
echo "<div style='color: purple; font-weight: bold; text-align: center;'>Debug: Pushed " . count($modalEmbeds) . " modal embeds as JSON</div>";
}
}
}
// Load external JS for modal and embed rendering
echo '<script src="/modal-embed.js?v=' . time() . '"></script>';
$stmt->close();
$conn->close();
?>
// modal-embed.js - Version 2.85 (Tattoo-optimized, modal YouTube embed fixed)
// Last updated: January 13, 2026
console.log("modal-embed.js LOADED SUCCESSFULLY (Tattoo version 2.85)");
document.addEventListener('DOMContentLoaded', function() {
const DEBUG = true; // Change to false when done testing
let debugLog = null;
if (DEBUG) {
debugLog = document.createElement('div');
debugLog.id = 'debug-log';
debugLog.style.position = 'fixed';
debugLog.style.bottom = '10px';
debugLog.style.left = '10px';
debugLog.style.background = 'rgba(0, 0, 0, 0.8)';
debugLog.style.color = 'white';
debugLog.style.padding = '10px';
debugLog.style.zIndex = '1000';
debugLog.style.maxHeight = '200px';
debugLog.style.overflowY = 'auto';
document.body.appendChild(debugLog);
}
function logDebug(message, color = 'green') {
if (DEBUG && debugLog) {
debugLog.insertAdjacentHTML('beforeend', `<div style="color: ${color}; font-weight: bold;">Debug: ${message}</div>`);
debugLog.scrollTop = debugLog.scrollHeight;
}
if (DEBUG && console) console.log(`Debug: ${message}`);
}
if (DEBUG) logDebug('Modal script loaded (Tattoo version 2.85)');
const modal = document.getElementById('story-modal');
const modalBody = document.getElementById('modal-body');
const closeBtn = document.querySelector('.modal-close');
const triggers = document.querySelectorAll('.modal-trigger');
if (DEBUG) logDebug('Found ' + triggers.length + ' modal triggers');
// Embed rendering function
function renderEmbed(item, index, total) {
const container = document.getElementById(`embed-${item.id}`);
if (!container) {
if (DEBUG) logDebug(`Container not found for embed ID ${item.id}`, 'red');
return;
}
if (item.type === 'YouTube') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered YouTube embed for ID ${item.id} (sandbox removed)`, 'orange');
} else if (item.type === 'PayPal') {
const divMatch = item.code.match(/<div\s+[^>]*id="[^"]+"[^>]*>/i);
const buttonMatch = item.code.match(/<script>\s*paypal\.Buttons\([\s\S]*?\)\.render\(['"][^'"]+['"]\);\s*<\/script>/i);
if (divMatch && buttonMatch) {
const uniqueId = `paypal-button-container-${item.id}`;
let divContent = divMatch[0].replace(/id="[^"]+"/, `id="${uniqueId}"`);
container.innerHTML = divContent;
container.querySelector(`#${uniqueId}`).style.cssText = 'width: 100% !important; min-width: 300px !important; max-width: 300px !important; margin: 0 auto !important;';
function waitForSDK(callback) {
if (typeof paypal !== 'undefined') {
callback();
} else {
setTimeout(function() { waitForSDK(callback); }, 100);
}
}
waitForSDK(function() {
let scriptContent = buttonMatch[0].replace(/^<script>|<\/script>$/g, '').trim();
scriptContent = scriptContent.replace(/\.render\(['"][^'"]+['"]\)/, `.render('#${uniqueId}')`);
const uniqueScript = `(function() { ${scriptContent} })();`;
const buttonScript = document.createElement('script');
buttonScript.textContent = uniqueScript;
document.body.appendChild(buttonScript);
});
}
}
}
// Run embed rendering on page load
if (window.contentCode && Array.isArray(window.contentCode)) {
window.contentCode.forEach(function(item, index) {
renderEmbed(item, index, window.contentCode.length);
});
} else if (DEBUG) {
logDebug('contentCode is not an array or unavailable', 'red');
}
// PHP debug info (safe try-catch)
try {
if (window.debugFromPHP) {
console.log('PHP Debug Info:', window.debugFromPHP);
if (DEBUG) logDebug('PHP → Fetched sections: ' + window.debugFromPHP.fetched + ', Grouped positions: ' + window.debugFromPHP.grouped + ', Modal request: ' + window.debugFromPHP.modalRequest, 'purple');
} else {
console.warn('No debug info from PHP');
}
} catch (e) {
console.warn('Error reading PHP debug info:', e);
}
// Modal logic only if elements exist
if (modal && modalBody && closeBtn && triggers.length > 0) {
if (DEBUG) logDebug('Modal elements found - enabling modal functionality');
triggers.forEach(function(trigger) {
trigger.addEventListener('click', function(e) {
e.preventDefault();
if (DEBUG) logDebug(`Modal trigger clicked: ${this.getAttribute('data-modal-id') || this.getAttribute('data-modal-link')}`, 'green');
const modalId = this.getAttribute('data-modal-id');
if (modalId) {
const contentDiv = document.getElementById(modalId);
if (contentDiv) {
const content = contentDiv.innerHTML;
if (DEBUG) logDebug('Stories modal triggered, content from ID: ' + modalId);
openModal(content);
} else {
openModal('<p>Content not found.</p>');
if (DEBUG) logDebug('Modal content not found for ID: ' + modalId, 'red');
}
return;
}
const link = this.getAttribute('data-modal-link');
if (DEBUG) logDebug('Trigger clicked, link: ' + link);
if (!link || link === '#' || link.includes('index.php')) {
openModal('<p>This link cannot be loaded in a modal to prevent recursive loading.</p>');
return;
}
let absoluteLink;
try {
absoluteLink = new URL(link, window.location.origin).href;
} catch (error) {
openModal('<p>Invalid URL.</p>');
return;
}
absoluteLink += (absoluteLink.includes('?') ? '&' : '?') + 'modal=1';
if (DEBUG) logDebug('Fetching absolute link: ' + absoluteLink);
openModal('<p>Loading...</p>');
fetch(absoluteLink, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function(response) {
if (DEBUG) logDebug('Fetch response status: ' + response.status + ' ' + response.statusText);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return response.text();
})
.then(function(data) {
if (DEBUG) logDebug('Fetched HTML length: ' + data.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
let combinedContent = '';
const sections = doc.querySelectorAll('.modal-story-content');
if (DEBUG) logDebug('Found ' + sections.length + ' modal-story-content sections');
if (sections.length > 0) {
sections.forEach(function(section, index) {
combinedContent += section.outerHTML.trim();
if (index < sections.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
const tattooFallback = doc.querySelectorAll('.dynamic-section .inner');
if (DEBUG) logDebug('Found ' + tattooFallback.length + ' Tattoo .dynamic-section .inner sections');
if (tattooFallback.length > 0) {
tattooFallback.forEach(function(section, index) {
const newContent = document.createElement('div');
newContent.className = 'modal-story-content';
newContent.innerHTML = section.innerHTML;
combinedContent += newContent.outerHTML.trim();
if (index < tattooFallback.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
if (DEBUG) logDebug('No content found in Tattoo structure', 'red');
combinedContent = '<p>Content could not be parsed from the page.</p>';
}
}
if (combinedContent) {
openModal(combinedContent);
} else {
openModal('<p>No content found.</p>');
}
})
.catch(function(error) {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
openModal('<p>Sorry, the content could not be loaded: ' + error.message + '</p>');
});
});
});
closeBtn.addEventListener('click', closeModal);
window.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && modal.style.display === 'block') {
closeModal();
}
});
} else {
if (DEBUG) logDebug('Modal elements missing - skipping modal functionality (normal on non-modal pages)', 'orange');
}
function openModal(content) {
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = content;
// Re-run embed rendering after modal content is inserted
setTimeout(function() {
const modalEmbeds = modalBody.querySelectorAll('.content-embed[id^="embed-"]');
if (modalEmbeds.length > 0) {
if (DEBUG) logDebug('Found ' + modalEmbeds.length + ' embeds inside modal – re-rendering', 'orange');
modalEmbeds.forEach(function(embedDiv) {
const id = embedDiv.id.replace('embed-', '');
const item = window.contentCode?.find(function(i) {
return i.id == id;
});
if (item) {
renderEmbed(item, 0, 1);
if (DEBUG) logDebug('Re-rendered embed-' + id + ' inside modal', 'green');
} else {
if (DEBUG) logDebug('No matching contentCode for embed-' + id + ' in modal', 'red');
}
});
} else {
if (DEBUG) logDebug('No embeds found inside modal', 'orange');
}
}, 500); // 500ms delay
const modal = document.getElementById('story-modal');
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
const closeBtn = document.querySelector('.modal-close');
if (closeBtn) closeBtn.focus();
if (DEBUG) logDebug('Modal opened');
}
function closeModal() {
const modal = document.getElementById('story-modal');
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = '';
if (DEBUG) logDebug('Modal closed');
}
});
This is the cleanest, full-file reset — no more partial fixes, no syntax risks.
Let me know the console after clicking the button — especially any "Rendered YouTube embed" lines and whether the video shows in the modal.
Current time: January 13, 2026 08:15 PM HKT
We're closing in — this should be the one! 😊
From my research (using web search and browsing Bootstrap docs), most common modals do support YT embeds, but they handle it differently from your dynamic section system. Here's a summary of best practices from popular libraries and Stack Overflow examples:
Bootstrap Modals (most popular for simple modals):
data-bs-video="https://www.youtube.com/embed/VIDEO_ID"), then dynamically set the iframe src when the modal opens, and reset src = '' on close to stop playback.Example code from Stack Overflow (adapted for Bootstrap 5):
<!-- Trigger button -->
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#videoModal" data-bs-video="https://www.youtube.com/embed/V3iS2xltayA">Open Video Modal</button>
<!-- Modal structure -->
<div class="modal fade" id="videoModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Video Title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="ratio ratio-16x9">
<iframe src="" allowfullscreen></iframe> <!-- src set dynamically -->
</div>
</div>
</div>
</div>
</div>
<!-- JS to handle YT -->
<script>
const videoModal = document.getElementById('videoModal');
videoModal.addEventListener('show.bs.modal', event => {
const button = event.relatedTarget;
const videoUrl = button.getAttribute('data-bs-video');
const iframe = videoModal.querySelector('iframe');
iframe.src = videoUrl;
});
videoModal.addEventListener('hidden.bs.modal', event => {
const iframe = videoModal.querySelector('iframe');
iframe.src = ''; // Reset to stop playback
});
</script>
Other Libraries (Magnific Popup, Fancybox, etc.):
type: 'iframe' for YT. You set the link as href="https://www.youtube.com/watch?v=VIDEO_ID" class="popup-youtube", and it auto-embeds in modal.
<a href="https://www.youtube.com/watch?v=V3iS2xltayA" class="popup-youtube">Open YouTube</a>
$('.popup-youtube').magnificPopup({ type: 'iframe' });Common Patterns for Dynamic Modals:
data-video-url on the trigger button — no fetch needed, just insert iframe in modal on open.?autoplay=1 to YT URL on open, remove iframe or set src='' on close.To make YT work in modal without changing your dynamic system, add this small block to openModal in modal-embed.js (right after modalBody.innerHTML = content;):
// Re-run embed rendering for modal embeds
setTimeout(function() {
const modalEmbeds = modalBody.querySelectorAll('.content-embed[id^="embed-"]');
if (modalEmbeds.length > 0) {
if (DEBUG) logDebug('Found ' + modalEmbeds.length + ' embeds inside modal – re-rendering', 'orange');
modalEmbeds.forEach(function(embedDiv) {
const id = embedDiv.id.replace('embed-', '');
const item = window.contentCode?.find(function(i) { return i.id == id; });
if (item) {
renderEmbed(item, 0, 1);
if (DEBUG) logDebug('Re-rendered embed-' + id + ' inside modal', 'green');
} else {
if (DEBUG) logDebug('No matching contentCode for embed-' + id + ' in modal', 'red');
}
});
} else {
if (DEBUG) logDebug('No embeds found inside modal', 'orange');
}
}, 500); // 500ms delay
This is the minimal change — it keeps your working JS intact and only adds the re-render for modal embeds.
As you suggested, if fixing the modal YT is too tricky, let's roll back the extra codes we've added for this feature (from both PHP and JS). This will restore your site to the working state of V2.83 (modal opens with content, but no YT inside modal).
Roll Back Steps for PHP (remove the modal embed re-render support):
if ($is_modal_request && !empty($sections)) { $modalEmbeds = []; ... } block (the JSON div)Roll Back Steps for JS (remove the re-render setTimeout from openModal):
setTimeout(function() { ... }, 500); block from openModalAfter rollback, regenerate and test — modal should work as before (no YT inside, but stable).
Other libraries handle YT in modals by not fetching full pages — they use data attributes for the YT URL and create iframes dynamically. Your system is more complex (dynamic sections with IDs), so it's trickier, but the fix above should work without rollback.
Let me know if the minimal addition fixes it, or if you want to roll back.
Current time: January 13, 2026 08:45 PM HKT
I have:
<?php
// pull-contents.php (Version 2.83 - Stable rollback)
// Last updated: January 13, 2026
// Features: title-text-video model, bee below background with classes, working main-page YT, modal opens with content
define('DEBUG', true);
define('IMAGE_BASE_PATH', 'https://navi.claudioflute.live/images/');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$config_path = '../config.php';
if (!file_exists($config_path)) {
die("Error: config.php not found at $config_path");
}
require_once $config_path;
if (!isset($dbServer) || !isset($dbUsername) || !isset($dbPassword) || !isset($dbDatabase)) {
die("Error: AppGini database variables not defined in config.php");
}
$conn = new mysqli($dbServer, $dbUsername, $dbPassword, $dbDatabase, $dbPort ?: 3306);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Auto-detect page
if (isset($_GET['page'])) {
$current_page = $_GET['page'];
} else {
$calling_file = basename($_SERVER['SCRIPT_FILENAME']);
$current_page = str_replace('.php', '', $calling_file);
if ($current_page === 'index') {
$current_page = 'landing';
}
}
$status = 'Show';
// Preload PayPal SDK
$paypalSdkUrl = 'https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription';
echo "<script src=\"$paypalSdkUrl\" id=\"paypal-sdk\" async=\"false\"></script>";
// Is modal request
$is_modal_request = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || isset($_GET['modal']);
// Is stories page (for preview logic)
$is_stories_page = ($current_page === 'stories');
// SQL - matches your schema
$sql = "SELECT id, title, content, code, code_type, image, button_link, button_text, position, model, date_edited, theme_img, img_role, chk_divider
FROM web_content
WHERE page = ? AND status = ?
ORDER BY position ASC, date_edited DESC";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $current_page, $status);
$stmt->execute();
$result = $stmt->get_result();
$sections = [];
while ($row = $result->fetch_assoc()) {
$sections[] = $row;
}
// Group by position for multi-column
$sections_by_position = [];
foreach ($sections as $row) {
$position = $row['position'] ?? 1;
$sections_by_position[$position][] = $row;
}
if (!empty($sections_by_position)) {
ksort($sections_by_position);
foreach ($sections_by_position as $position => $group) {
$is_grid = count($group) > 1;
if ($is_modal_request) {
foreach ($group as $index => $row) {
echo "<div class=\"modal-story-content\">";
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
}
echo $row['content'];
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
echo "</div>";
if ($index < count($group) - 1) {
echo "<div class=\"modal-section-separator\"></div>";
}
}
} else {
// SMART CLASS for Tattoo
$section_class = 'content';
$has_background = false;
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$has_background = true;
}
}
if ($has_background) {
$section_class = 'hero';
}
if ($is_grid) {
$section_class .= ' grid';
}
$bg_style = '';
if ($section_class === 'hero') {
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$bg_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$bg_style = "style=\"background-image: url('$bg_url');\"";
break;
}
}
}
echo "<section class=\"{$section_class} dynamic-section\" {$bg_style}>";
echo "<div class=\"inner\">";
foreach ($group as $row) {
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">{$row['title']}</h2>";
}
echo $row['content'];
if ($row['model'] === 'title-text-image-video') {
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
elseif ($row['model'] === 'title-text-video') {
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
elseif ($row['model'] === 'title-text-image-code' || $row['model'] === 'title-text-code') {
if (!empty($row['image']) && $row['model'] === 'title-text-image-code') {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
}
elseif (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
// BUTTON RENDERING
if (!empty($row['button_text'])) {
$button_text = htmlspecialchars($row['button_text']);
$button_link = htmlspecialchars($row['button_link'] ?: '#');
echo "<div class=\"content__buttons\">";
if ($is_stories_page) {
$modal_id = 'modal-content-' . $row['id'];
echo "<div id=\"$modal_id\" style=\"display:none;\">";
echo '<div class="modal-story-content">';
if (!empty($row['title'])) echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image modal-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['content'])) echo $row['content'];
echo '</div>';
echo "</div>";
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-id=\"$modal_id\">$button_text</a>";
} else {
if (!empty($button_link) && $button_link !== '#') {
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-link=\"$button_link\">$button_text</a>";
} else {
echo "<a class=\"btn\" href=\"javascript:void(0)\">$button_text</a>";
}
}
echo "</div>";
}
}
echo "</div>"; // .inner
// Separator — moved AFTER section for below background
$show_separator = false;
$sep_url = '';
if (!empty($row['theme_img']) && $row['img_role'] === 'Separator') {
$sep_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$show_separator = true;
} elseif (!empty($row['chk_divider']) && $row['chk_divider'] == 1) {
$sep_url = 'https://navi.claudioflute.live/public/Flying_bee_R.gif';
$show_separator = true;
}
if ($show_separator) {
$sep_class = ($row['img_role'] === 'Separator') ? 'separator custom-separator' : 'separator default-bee';
echo "<div class=\"separator-wrapper\">";
echo "<img src=\"$sep_url\" class=\"$sep_class\" alt=\"Separator\">";
echo "</div>";
}
echo "</section>";
}
}
} else {
echo "No sections found.";
}
// Modal container (only for non-AJAX requests)
if (!$is_modal_request) {
echo '<div id="story-modal" class="modal">';
echo '<div class="modal-content">';
echo '<span class="modal-close">×</span>';
echo '<div id="modal-body"></div>';
echo '</div>';
echo '</div>';
}
// Load external JS for modal and embed rendering
echo '<script src="/modal-embed.js?v=' . time() . '"></script>';
$stmt->close();
$conn->close();
?>
// modal-embed.js - Version 2.85 (Tattoo-optimized, stable rollback)
// Last updated: January 13, 2026
console.log("modal-embed.js LOADED SUCCESSFULLY (Tattoo version 2.85)");
document.addEventListener('DOMContentLoaded', function() {
const DEBUG = true; // Change to false when done testing
let debugLog = null;
if (DEBUG) {
debugLog = document.createElement('div');
debugLog.id = 'debug-log';
debugLog.style.position = 'fixed';
debugLog.style.bottom = '10px';
debugLog.style.left = '10px';
debugLog.style.background = 'rgba(0, 0, 0, 0.8)';
debugLog.style.color = 'white';
debugLog.style.padding = '10px';
debugLog.style.zIndex = '1000';
debugLog.style.maxHeight = '200px';
debugLog.style.overflowY = 'auto';
document.body.appendChild(debugLog);
}
function logDebug(message, color = 'green') {
if (DEBUG && debugLog) {
debugLog.insertAdjacentHTML('beforeend', `<div style="color: ${color}; font-weight: bold;">Debug: ${message}</div>`);
debugLog.scrollTop = debugLog.scrollHeight;
}
if (DEBUG && console) console.log(`Debug: ${message}`);
}
if (DEBUG) logDebug('Modal script loaded (Tattoo version 2.85)');
const modal = document.getElementById('story-modal');
const modalBody = document.getElementById('modal-body');
const closeBtn = document.querySelector('.modal-close');
const triggers = document.querySelectorAll('.modal-trigger');
if (DEBUG) logDebug('Found ' + triggers.length + ' modal triggers');
// Embed rendering function
function renderEmbed(item, index, total) {
const container = document.getElementById(`embed-${item.id}`);
if (!container) {
if (DEBUG) logDebug(`Container not found for embed ID ${item.id}`, 'red');
return;
}
if (item.type === 'YouTube') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered YouTube embed for ID ${item.id} (sandbox removed)`, 'orange');
} else if (item.type === 'PayPal') {
const divMatch = item.code.match(/<div\s+[^>]*id="[^"]+"[^>]*>/i);
const buttonMatch = item.code.match(/<script>\s*paypal\.Buttons\([\s\S]*?\)\.render\(['"][^'"]+['"]\);\s*<\/script>/i);
if (divMatch && buttonMatch) {
const uniqueId = `paypal-button-container-${item.id}`;
let divContent = divMatch[0].replace(/id="[^"]+"/, `id="${uniqueId}"`);
container.innerHTML = divContent;
container.querySelector(`#${uniqueId}`).style.cssText = 'width: 100% !important; min-width: 300px !important; max-width: 300px !important; margin: 0 auto !important;';
function waitForSDK(callback) {
if (typeof paypal !== 'undefined') {
callback();
} else {
setTimeout(function() { waitForSDK(callback); }, 100);
}
}
waitForSDK(function() {
let scriptContent = buttonMatch[0].replace(/^<script>|<\/script>$/g, '').trim();
scriptContent = scriptContent.replace(/\.render\(['"][^'"]+['"]\)/, `.render('#${uniqueId}')`);
const uniqueScript = `(function() { ${scriptContent} })();`;
const buttonScript = document.createElement('script');
buttonScript.textContent = uniqueScript;
document.body.appendChild(buttonScript);
});
}
}
}
// Run embed rendering on page load
if (window.contentCode && Array.isArray(window.contentCode)) {
window.contentCode.forEach(function(item, index) {
renderEmbed(item, index, window.contentCode.length);
});
} else if (DEBUG) {
logDebug('contentCode is not an array or unavailable', 'red');
}
// PHP debug info (safe try-catch)
try {
if (window.debugFromPHP) {
console.log('PHP Debug Info:', window.debugFromPHP);
if (DEBUG) logDebug('PHP → Fetched sections: ' + window.debugFromPHP.fetched + ', Grouped positions: ' + window.debugFromPHP.grouped + ', Modal request: ' + window.debugFromPHP.modalRequest, 'purple');
} else {
console.warn('No debug info from PHP');
}
} catch (e) {
console.warn('Error reading PHP debug info:', e);
}
// Modal logic only if elements exist
if (modal && modalBody && closeBtn && triggers.length > 0) {
if (DEBUG) logDebug('Modal elements found - enabling modal functionality');
triggers.forEach(function(trigger) {
trigger.addEventListener('click', function(e) {
e.preventDefault();
if (DEBUG) logDebug(`Modal trigger clicked: ${this.getAttribute('data-modal-id') || this.getAttribute('data-modal-link')}`, 'green');
const modalId = this.getAttribute('data-modal-id');
if (modalId) {
const contentDiv = document.getElementById(modalId);
if (contentDiv) {
const content = contentDiv.innerHTML;
if (DEBUG) logDebug('Stories modal triggered, content from ID: ' + modalId);
openModal(content);
} else {
openModal('<p>Content not found.</p>');
if (DEBUG) logDebug('Modal content not found for ID: ' + modalId, 'red');
}
return;
}
const link = this.getAttribute('data-modal-link');
if (DEBUG) logDebug('Trigger clicked, link: ' + link);
if (!link || link === '#' || link.includes('index.php')) {
openModal('<p>This link cannot be loaded in a modal to prevent recursive loading.</p>');
return;
}
let absoluteLink;
try {
absoluteLink = new URL(link, window.location.origin).href;
} catch (error) {
openModal('<p>Invalid URL.</p>');
return;
}
absoluteLink += (absoluteLink.includes('?') ? '&' : '?') + 'modal=1';
if (DEBUG) logDebug('Fetching absolute link: ' + absoluteLink);
openModal('<p>Loading...</p>');
fetch(absoluteLink, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function(response) {
if (DEBUG) logDebug('Fetch response status: ' + response.status + ' ' + response.statusText);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return response.text();
})
.then(function(data) {
if (DEBUG) logDebug('Fetched HTML length: ' + data.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
let combinedContent = '';
const sections = doc.querySelectorAll('.modal-story-content');
if (DEBUG) logDebug('Found ' + sections.length + ' modal-story-content sections');
if (sections.length > 0) {
sections.forEach(function(section, index) {
combinedContent += section.outerHTML.trim();
if (index < sections.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
const tattooFallback = doc.querySelectorAll('.dynamic-section .inner');
if (DEBUG) logDebug('Found ' + tattooFallback.length + ' Tattoo .dynamic-section .inner sections');
if (tattooFallback.length > 0) {
tattooFallback.forEach(function(section, index) {
const newContent = document.createElement('div');
newContent.className = 'modal-story-content';
newContent.innerHTML = section.innerHTML;
combinedContent += newContent.outerHTML.trim();
if (index < tattooFallback.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
if (DEBUG) logDebug('No content found in Tattoo structure', 'red');
combinedContent = '<p>Content could not be parsed from the page.</p>';
}
}
if (combinedContent) {
openModal(combinedContent);
} else {
openModal('<p>No content found.</p>');
}
})
.catch(function(error) {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
openModal('<p>Sorry, the content could not be loaded: ' + error.message + '</p>');
});
});
});
closeBtn.addEventListener('click', closeModal);
window.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && modal.style.display === 'block') {
closeModal();
}
});
} else {
if (DEBUG) logDebug('Modal elements missing - skipping modal functionality (normal on non-modal pages)', 'orange');
}
function openModal(content) {
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = content;
const modal = document.getElementById('story-modal');
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
const closeBtn = document.querySelector('.modal-close');
if (closeBtn) closeBtn.focus();
if (DEBUG) logDebug('Modal opened');
}
function closeModal() {
const modal = document.getElementById('story-modal');
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = '';
if (DEBUG) logDebug('Modal closed');
}
});
This rollback keeps:
No modal YT — but stable and reliable.
If you want to try one last time with the JSON div (no script execution needed), let me know — I can provide a clean full file pair again.
Otherwise, we're back to your working state — good to go for production.
Let me know the console after testing this rollback — especially if debug messages appear now.
Current time: January 13, 2026 08:45 PM HKT
We're stable again! 😊
It’s a smart compromise:
static-embed.php)Create a new PHP file static-embed.php (or name it static-video.php, etc.) with minimal content:
<?php
// static-embed.php - Version 1.0 - Simple YouTube embed page for modal iframe
// Optional: get video ID from query string for flexibility
$video_id = isset($_GET['v']) ? htmlspecialchars($_GET['v']) : 'V3iS2xltayA'; // fallback ID
$title = isset($_GET['title']) ? htmlspecialchars($_GET['title']) : 'Video';
?>
<!DOCTYPE html>
<html lang="zh-hk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo $title; ?></title>
<style>
body { margin:0; padding:0; background:#000; overflow:hidden; }
.video-container {
position: relative;
width: 100%;
height: 100vh;
}
.video-container iframe {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
}
</style>
</head>
<body>
<div class="video-container">
<iframe
src="https://www.youtube.com/embed/<?php echo $video_id; ?>?autoplay=1&rel=0&modestbranding=1&playsinline=1"
title="<?php echo $title; ?>"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</body>
</html>
src query params)In your AppGini admin:
button_link to: static-embed.php?v=VIDEO_ID (or static-embed.php?v=V3iS2xltayA&title=Burnout%20Recovery)This URL will be used in data-modal-link.
Add a new condition in the click handler (inside trigger.addEventListener('click', ...)):
// ... inside the click handler ...
const link = this.getAttribute('data-modal-link');
if (link && link.includes('static-embed.php')) {
// Special case: static embed page → load in iframe
const iframeUrl = link; // already has ?v=...
const iframeHtml = `<div class="content-embed" style="height:80vh;"><iframe src="${iframeUrl}" frameborder="0" allowfullscreen style="width:100%;height:100%;"></iframe></div>`;
openModal(iframeHtml);
if (DEBUG) logDebug('Opened static embed iframe: ' + iframeUrl, 'green');
return;
}
// ... rest of your existing code (fetch for normal pages) ...
static-embed.phpstatic-embed.php?v=... → modal opens with full-screen iframe YT video.content-embed wrapper).modal-body iframe {
width: 100% !important;
height: 80vh !important; /* or 90vh for almost full modal */
}openModal('<p>Loading video...</p>');
setTimeout(() => {
const iframeHtml = `<div class="content-embed" style="height:80vh;"><iframe src="${iframeUrl}" frameborder="0" allowfullscreen style="width:100%;height:100%;"></iframe></div>`;
modalBody.innerHTML = iframeHtml;
}, 300);static-embed.php with the code abovestatic-embed.php?v=VIDEO_IDif (link && link.includes('static-embed.php')) block to JS click handlerThis approach is simple, stable, and avoids all fetch/execution issues.
Let me know if you want the updated full JS/PHP files with this static-iframe logic already included — I can generate them right away.
Current time: January 13, 2026 08:45 PM HKT
This workaround is clean and future-proof! 😊
Create a "mother" section that links to static-embed.php Create three modal sections with slugs set to "static-embed" and display order set to 1, 2, 3, so that they will appear in the same modal triggered from the mother section Each modal section will be showing one video
If this is possible, it seems that static-embed.php (We can add more mother sections linking to static-embed02.php etc.) has to be able to allow multiple videos. But I'm not sure how to support multiple video with a single "static-embed.php?v=VIDEO_ID".
static-embed.php with a query string that includes multiple video IDs (e.g., static-embed.php?v=ID1,ID2,ID3).static-embed.php page will parse the query string and render multiple iframes (one for each ID) in a vertical column or row (your choice).static-embed.php page as an iframe (as we discussed), so all three videos will appear together in the modal.static-embed02.php) — one file can handle any number of videos dynamically.static-embed.php (Full File, Supports Multiple Videos)Create or replace static-embed.php with this code (it's dynamic and handles 1 or more videos):
<?php
// static-embed.php - Version 1.0 - Multi-video embed page for modal iframe
// Last updated: January 13, 2026
// Get video IDs from query string (e.g., v=ID1,ID2,ID3)
$videos = isset($_GET['v']) ? explode(',', htmlspecialchars($_GET['v'])) : ['V3iS2xltayA']; // fallback to one video if none provided
// Optional: title
$title = isset($_GET['title']) ? htmlspecialchars($_GET['title']) : 'Videos';
?>
<!DOCTYPE html>
<html lang="zh-hk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo $title; ?></title>
<style>
body { margin:0; padding:0; background:#fff; overflow:auto; }
.video-container {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
margin-bottom: 1rem;
}
.video-container iframe {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
}
</style>
</head>
<body>
<?php foreach ($videos as $video_id) { ?>
<div class="video-container">
<iframe
src="https://www.youtube.com/embed/<?php echo htmlspecialchars(trim($video_id)); ?>?autoplay=0&rel=0&modestbranding=1&playsinline=1"
title="YouTube video"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
<?php } ?>
</body>
</html>
?v=ID1,ID2,ID3 and renders an iframe for each.For the "mother" section button:
button_link to: static-embed.php?v=V3iS2xltayA,ID2,ID3 (replace with your actual IDs)&title=My%20Videos for a custom titlestatic-embed.php with the code abovestatic-embed.php?v=V3iS2xltayA,ID2,ID3If you want the videos in a horizontal row instead of column (for wide modals), add this to the <style> in static-embed.php:
body { display: flex; flex-wrap: wrap; justify-content: center; gap: 1rem; }
.video-container { flex: 1 1 30%; min-width: 300px; }
This makes them responsive side-by-side.
Let me know how it works — this is a clean, scalable way to show multiple videos in one modal without complex fetching!
Current time: January 13, 2026 07:45 PM HKT
There will be just one static-embed.php file in total.
You don’t need multiple versions of it (no static-embed02.php, static-embed03.php, etc.). The single file is dynamic and can handle any number of videos, depending on what you pass in the query string.
You create different modal buttons in your dynamic sections, each linking to the same static-embed.php, but with different video IDs in the URL.
Examples:
Button for burnout video:
data-modal-link = "static-embed.php?v=V3iS2xltayA"
Button for another video:
data-modal-link = "static-embed.php?v=dQw4w9WgXcQ"
Button for three videos in one modal:
data-modal-link = "static-embed.php?v=V3iS2xltayA,dQw4w9WgXcQ,VIDEO_ID_3"
The static-embed.php file reads the v parameter, splits it by comma, and renders an iframe for each ID.
So yes — single file, multiple possible URLs with different IDs.
In your AppGini admin or wherever you edit dynamic sections:
button_link field to one of the URLs above (e.g. static-embed.php?v=ID1,ID2,ID3)if (link && link.includes('static-embed.php')) {
// load as iframe
}That’s it — no more complex fetching or re-rendering needed for these videos.
If you want to make the static page even more flexible (e.g., pass autoplay, loop, start time per video), we can extend the query string later (e.g. v=ID1:autoplay1,loop1;ID2:autoplay0).
But for now — one file + different query strings is clean, simple, and powerful.
Let me know if you want to add any extras to static-embed.php (e.g. titles above each video, responsive grid layout for multiple videos, etc.) — happy to refine it! 😊
The new block is placed inside the click handler (right after reading the link), and it works as a special case:
static-embed.php → immediately opens the modal with an iframe (no fetch needed)// modal-embed.js - Version 2.86 (Tattoo-optimized, static-embed.php iframe support)
// Last updated: January 13, 2026
console.log("modal-embed.js LOADED SUCCESSFULLY (Tattoo version 2.86)");
document.addEventListener('DOMContentLoaded', function() {
const DEBUG = true; // Change to false when done testing
let debugLog = null;
if (DEBUG) {
debugLog = document.createElement('div');
debugLog.id = 'debug-log';
debugLog.style.position = 'fixed';
debugLog.style.bottom = '10px';
debugLog.style.left = '10px';
debugLog.style.background = 'rgba(0, 0, 0, 0.8)';
debugLog.style.color = 'white';
debugLog.style.padding = '10px';
debugLog.style.zIndex = '1000';
debugLog.style.maxHeight = '200px';
debugLog.style.overflowY = 'auto';
document.body.appendChild(debugLog);
}
function logDebug(message, color = 'green') {
if (DEBUG && debugLog) {
debugLog.insertAdjacentHTML('beforeend', `<div style="color: ${color}; font-weight: bold;">Debug: ${message}</div>`);
debugLog.scrollTop = debugLog.scrollHeight;
}
if (DEBUG && console) console.log(`Debug: ${message}`);
}
if (DEBUG) logDebug('Modal script loaded (Tattoo version 2.86)');
const modal = document.getElementById('story-modal');
const modalBody = document.getElementById('modal-body');
const closeBtn = document.querySelector('.modal-close');
const triggers = document.querySelectorAll('.modal-trigger');
if (DEBUG) logDebug('Found ' + triggers.length + ' modal triggers');
// Embed rendering function
function renderEmbed(item, index, total) {
const container = document.getElementById(`embed-${item.id}`);
if (!container) {
if (DEBUG) logDebug(`Container not found for embed ID ${item.id}`, 'red');
return;
}
if (item.type === 'YouTube') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered YouTube embed for ID ${item.id} (sandbox removed)`, 'orange');
} else if (item.type === 'PayPal') {
const divMatch = item.code.match(/<div\s+[^>]*id="[^"]+"[^>]*>/i);
const buttonMatch = item.code.match(/<script>\s*paypal\.Buttons\([\s\S]*?\)\.render\(['"][^'"]+['"]\);\s*<\/script>/i);
if (divMatch && buttonMatch) {
const uniqueId = `paypal-button-container-${item.id}`;
let divContent = divMatch[0].replace(/id="[^"]+"/, `id="${uniqueId}"`);
container.innerHTML = divContent;
container.querySelector(`#${uniqueId}`).style.cssText = 'width: 100% !important; min-width: 300px !important; max-width: 300px !important; margin: 0 auto !important;';
function waitForSDK(callback) {
if (typeof paypal !== 'undefined') {
callback();
} else {
setTimeout(function() { waitForSDK(callback); }, 100);
}
}
waitForSDK(function() {
let scriptContent = buttonMatch[0].replace(/^<script>|<\/script>$/g, '').trim();
scriptContent = scriptContent.replace(/\.render\(['"][^'"]+['"]\)/, `.render('#${uniqueId}')`);
const uniqueScript = `(function() { ${scriptContent} })();`;
const buttonScript = document.createElement('script');
buttonScript.textContent = uniqueScript;
document.body.appendChild(buttonScript);
});
}
}
}
// Run embed rendering on page load
if (window.contentCode && Array.isArray(window.contentCode)) {
window.contentCode.forEach(function(item, index) {
renderEmbed(item, index, window.contentCode.length);
});
} else if (DEBUG) {
logDebug('contentCode is not an array or unavailable', 'red');
}
// PHP debug info (safe try-catch)
try {
if (window.debugFromPHP) {
console.log('PHP Debug Info:', window.debugFromPHP);
if (DEBUG) logDebug('PHP → Fetched sections: ' + window.debugFromPHP.fetched + ', Grouped positions: ' + window.debugFromPHP.grouped + ', Modal request: ' + window.debugFromPHP.modalRequest, 'purple');
} else {
console.warn('No debug info from PHP');
}
} catch (e) {
console.warn('Error reading PHP debug info:', e);
}
// Modal logic only if elements exist
if (modal && modalBody && closeBtn && triggers.length > 0) {
if (DEBUG) logDebug('Modal elements found - enabling modal functionality');
triggers.forEach(function(trigger) {
trigger.addEventListener('click', function(e) {
e.preventDefault();
if (DEBUG) logDebug(`Modal trigger clicked: ${this.getAttribute('data-modal-id') || this.getAttribute('data-modal-link')}`, 'green');
const modalId = this.getAttribute('data-modal-id');
if (modalId) {
const contentDiv = document.getElementById(modalId);
if (contentDiv) {
const content = contentDiv.innerHTML;
if (DEBUG) logDebug('Stories modal triggered, content from ID: ' + modalId);
openModal(content);
} else {
openModal('<p>Content not found.</p>');
if (DEBUG) logDebug('Modal content not found for ID: ' + modalId, 'red');
}
return;
}
const link = this.getAttribute('data-modal-link');
if (DEBUG) logDebug('Trigger clicked, link: ' + link);
if (!link || link === '#' || link.includes('index.php')) {
openModal('<p>This link cannot be loaded in a modal to prevent recursive loading.</p>');
return;
}
// Special case: static embed page → load in iframe
if (link && link.includes('static-embed.php')) {
const iframeUrl = link; // already has ?v=...
const iframeHtml = `<div class="content-embed" style="height:80vh;"><iframe src="${iframeUrl}" frameborder="0" allowfullscreen style="width:100%;height:100%;"></iframe></div>`;
openModal(iframeHtml);
if (DEBUG) logDebug('Opened static embed iframe: ' + iframeUrl, 'green');
return;
}
let absoluteLink;
try {
absoluteLink = new URL(link, window.location.origin).href;
} catch (error) {
openModal('<p>Invalid URL.</p>');
return;
}
absoluteLink += (absoluteLink.includes('?') ? '&' : '?') + 'modal=1';
if (DEBUG) logDebug('Fetching absolute link: ' + absoluteLink);
openModal('<p>Loading...</p>');
fetch(absoluteLink, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function(response) {
if (DEBUG) logDebug('Fetch response status: ' + response.status + ' ' + response.statusText);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return response.text();
})
.then(function(data) {
if (DEBUG) logDebug('Fetched HTML length: ' + data.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
let combinedContent = '';
const sections = doc.querySelectorAll('.modal-story-content');
if (DEBUG) logDebug('Found ' + sections.length + ' modal-story-content sections');
if (sections.length > 0) {
sections.forEach(function(section, index) {
combinedContent += section.outerHTML.trim();
if (index < sections.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
const tattooFallback = doc.querySelectorAll('.dynamic-section .inner');
if (DEBUG) logDebug('Found ' + tattooFallback.length + ' Tattoo .dynamic-section .inner sections');
if (tattooFallback.length > 0) {
tattooFallback.forEach(function(section, index) {
const newContent = document.createElement('div');
newContent.className = 'modal-story-content';
newContent.innerHTML = section.innerHTML;
combinedContent += newContent.outerHTML.trim();
if (index < tattooFallback.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
if (DEBUG) logDebug('No content found in Tattoo structure', 'red');
combinedContent = '<p>Content could not be parsed from the page.</p>';
}
}
if (combinedContent) {
openModal(combinedContent);
} else {
openModal('<p>No content found.</p>');
}
})
.catch(function(error) {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
openModal('<p>Sorry, the content could not be loaded: ' + error.message + '</p>');
});
});
});
closeBtn.addEventListener('click', closeModal);
window.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && modal.style.display === 'block') {
closeModal();
}
});
} else {
if (DEBUG) logDebug('Modal elements missing - skipping modal functionality (normal on non-modal pages)', 'orange');
}
function openModal(content) {
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = content;
const modal = document.getElementById('story-modal');
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
const closeBtn = document.querySelector('.modal-close');
if (closeBtn) closeBtn.focus();
if (DEBUG) logDebug('Modal opened');
}
function closeModal() {
const modal = document.getElementById('story-modal');
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = '';
if (DEBUG) logDebug('Modal closed');
}
});
modal-embed.js with the full V2.85 code abovepull-contents.php (no change needed for this feature)data-modal-link = "static-embed.php?v=ID1,ID2,ID3"When clicking a static embed button:
This approach is simple, fast, and stable — perfect for your multi-video modal use case.
Let me know how it works — if you want to add autoplay, titles above each video, or a grid layout in static-embed.php, I can refine it further!
Current time: January 13, 2026 09:15 PM HKT
This should be smooth and reliable! 😊
Since we're using a static static-embed.php page loaded in an iframe, we have full control over its HTML/CSS. We can make it look like a standard dynamic modal section.
Modify static-embed.php to accept per-video titles and descriptions via query string (e.g. v=ID1|Title1|Desc1,ID2|Title2|Desc2).
Here's the updated full file:
<?php
// static-embed.php - Version 2.0 - Multi-video embed with titles & descriptions
// Last updated: January 13, 2026
// Get videos from query string (format: v=ID1|Title1|Desc1,ID2|Title2|Desc2)
$videos_raw = isset($_GET['v']) ? htmlspecialchars($_GET['v']) : '';
$videos = [];
if ($videos_raw) {
$entries = explode(',', $videos_raw);
foreach ($entries as $entry) {
$parts = explode('|', $entry);
$id = trim($parts[0] ?? '');
$title = trim($parts[1] ?? 'Video');
$desc = trim($parts[2] ?? '');
if ($id) {
$videos[] = [
'id' => $id,
'title' => $title,
'desc' => $desc
];
}
}
}
// Fallback if no videos
if (empty($videos)) {
$videos = [
['id' => 'V3iS2xltayA', 'title' => 'Default Video', 'desc' => '']
];
}
$modal_title = isset($_GET['title']) ? htmlspecialchars($_GET['title']) : 'Videos';
?>
<!DOCTYPE html>
<html lang="zh-hk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo $modal_title; ?></title>
<style>
body {
margin: 0;
padding: 0;
background: #fff;
font-family: Arial, sans-serif;
color: #333;
}
.modal-title {
font-size: 2rem;
margin: 1.5rem 0 1rem;
text-align: center;
color: #000;
}
.video-block {
margin: 1.5rem 0;
padding: 1rem;
background: #f8f8f8;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.video-title {
font-size: 1.5rem;
margin-bottom: 0.5rem;
color: #000;
}
.video-desc {
font-size: 1rem;
line-height: 1.5;
color: #555;
margin-bottom: 1rem;
}
.video-container {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 */
background: #000;
border-radius: 8px;
overflow: hidden;
}
.video-container iframe {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
}
</style>
</head>
<body>
<h1 class="modal-title"><?php echo $modal_title; ?></h1>
<?php foreach ($videos as $video) { ?>
<div class="video-block">
<h2 class="video-title"><?php echo $video['title']; ?></h2>
<?php if ($video['desc']) { ?>
<p class="video-desc"><?php echo nl2br($video['desc']); ?></p>
<?php } ?>
<div class="video-container">
<iframe
src="https://www.youtube.com/embed/<?php echo $video['id']; ?>?autoplay=0&rel=0&modestbranding=1&playsinline=1"
title="<?php echo htmlspecialchars($video['title']); ?>"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<?php } ?>
</body>
</html>
In your dynamic section button:
Single video:
static-embed.php?v=V3iS2xltayA&title=Burnout%20Recovery
Three videos (with titles & descriptions):
static-embed.php?v=V3iS2xltayA|Burnout%20Recovery|Short%20description%20here,dQw4w9WgXcQ|Second%20Video|Another%20line%20of%20text,VIDEO_ID_3|Third%20Video|Final%20note&title=My%20Video%20Collection
The modal title comes from &title=... (defaults to "Videos")
Each video can have its own title and description (separated by |)
Your current JS already handles static-embed.php links by loading them as iframes — no update required.
Add this to your Custom CSS to make video titles/descriptions match Tattoo:
.modal-body .modal-title {
font-size: 2.5rem !important;
margin-bottom: 1.5rem !important;
color: #000 !important;
}
.modal-body .video-title {
font-size: 1.8rem !important;
margin: 1rem 0 0.5rem !important;
color: #000 !important;
}
.modal-body .video-desc {
font-size: 1.1rem !important;
line-height: 1.6 !important;
color: #333 !important;
margin-bottom: 1rem !important;
}
.modal-body .video-container {
border-radius: 8px !important;
overflow: hidden !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
}
This makes titles/descriptions feel like native Tattoo section titles and generic content.
static-embed.php with the code abovestatic-embed.php?v=V3iS2xltayA|Burnout%20Recovery|Short%20description%20here&title=My%20Video%20ModalIf you want:
autoplay=0 to autoplay=1display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1rem; to .video-block parentThis is simple, stable, and fully under your control — perfect for multi-video modals.
Current time: January 13, 2026 09:15 PM HKT
Let me know how it looks — we can tweak titles/descriptions/layout easily! 😊
The modal title showing "Video" (default) instead of the button text is because the static-embed.php is using a fallback title when no &title=... parameter is passed in the URL.
To make the modal title match the button text (just like normal dynamic modals), we need to pass the button text as the title query parameter when the link is to static-embed.php.
We can do this in two ways — choose the one that fits your workflow better.
&title=... to each button link in adminThis is the simplest and most straightforward.
In your dynamic section:
For the "mother" section button:
button_link to:
static-embed.php?v=VIDEO_ID1,VIDEO_ID2,VIDEO_ID3&title=關於Burnout%20Recovery%20 is for spaces — Publii will URL-encode it automatically if you type spaces.Result: Modal title will show "關於Burnout Recovery" (or whatever button text you put)
Pros: Full control, no code change needed
Cons: You need to edit each button link manually
Modify the JS click handler to automatically append the button text as &title=... when the link is for static-embed.php.
Replace the static embed special case block in your click handler with this updated version:
// Special case: static embed page → load in iframe
if (link && link.includes('static-embed.php')) {
// Auto-append button text as title if not already present
let iframeUrl = link;
const buttonText = this.textContent.trim(); // Get the visible button text
if (!iframeUrl.includes('&title=')) {
const separator = iframeUrl.includes('?') ? '&' : '?';
iframeUrl += separator + 'title=' + encodeURIComponent(buttonText);
}
const iframeHtml = `<div class="content-embed" style="height:80vh;"><iframe src="${iframeUrl}" frameborder="0" allowfullscreen style="width:100%;height:100%;"></iframe></div>`;
openModal(iframeHtml);
if (DEBUG) logDebug('Opened static embed iframe: ' + iframeUrl + ' (title from button: ' + buttonText + ')', 'green');
return;
}
What this does:
this.textContent)&title=..., it adds &title= + encoded button textstatic-embed.php?v=...&title=%E9%97%9C%E6%96%BCBurnout%20RecoveryResult:
static-embed.phpIf you want the title to look more like Tattoo section titles, update the <h1 class="modal-title"> in static-embed.php to use Tattoo-like styling:
<h1 class="modal-title" style="font-size: 2.5rem; margin: 1.5rem 0 1rem; text-align: center; color: #000;"><?php echo $modal_title; ?></h1>
Or add this to your global Custom CSS:
.modal-body .modal-title {
font-size: 2.5rem !important;
margin: 1.5rem 0 1rem !important;
color: #000 !important;
text-align: center !important;
}
modal-embed.jsstatic-embed.php?v=... (no &title needed now)If you prefer Option 1 (manual &title=...) — just ignore the JS change and edit the button links directly.
Let me know how it looks — if the title matches and everything feels consistent with normal modals now! 😊
Current time: January 13, 2026 09:45 PM HKT
The issue with the modal title size/style is easy to fix: the dynamic modals use <h2 class="section-title"> (Tattoo’s native styling), while the static modal (from static-embed.php) uses <h1 class="modal-title">, which is smaller and has different styling.
To make the static modal title look identical to dynamic ones:
<h2 class="section-title"> instead of <h1 class="modal-title">.modal-title CSS (or override it to match .section-title)Replace the <h1 class="modal-title"> line with this:
<h2 class="section-title"><?php echo $modal_title; ?></h2>
Full updated static-embed.php (only this line changes):
<?php
// static-embed.php - Version 2.0 - Multi-video embed with titles & descriptions
// Last updated: January 13, 2026
// Get videos from query string (format: v=ID1|Title1|Desc1,ID2|Title2|Desc2)
$videos_raw = isset($_GET['v']) ? htmlspecialchars($_GET['v']) : '';
$videos = [];
if ($videos_raw) {
$entries = explode(',', $videos_raw);
foreach ($entries as $entry) {
$parts = explode('|', $entry);
$id = trim($parts[0] ?? '');
$title = trim($parts[1] ?? 'Video');
$desc = trim($parts[2] ?? '');
if ($id) {
$videos[] = [
'id' => $id,
'title' => $title,
'desc' => $desc
];
}
}
}
// Fallback if no videos
if (empty($videos)) {
$videos = [
['id' => 'V3iS2xltayA', 'title' => 'Default Video', 'desc' => '']
];
}
$modal_title = isset($_GET['title']) ? htmlspecialchars($_GET['title']) : 'Videos';
?>
<!DOCTYPE html>
<html lang="zh-hk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo $modal_title; ?></title>
<style>
body {
margin: 0;
padding: 0;
background: #fff;
font-family: Arial, sans-serif;
color: #333;
}
.modal-title {
font-size: 2.5rem;
margin: 1.5rem 0 1rem;
text-align: center;
color: #000;
}
.video-block {
margin: 1.5rem 0;
padding: 1rem;
background: #f8f8f8;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.video-title {
font-size: 1.5rem;
margin-bottom: 0.5rem;
color: #000;
}
.video-desc {
font-size: 1rem;
line-height: 1.5;
color: #555;
margin-bottom: 1rem;
}
.video-container {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 */
background: #000;
border-radius: 8px;
overflow: hidden;
}
.video-container iframe {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
}
</style>
</head>
<body>
<h2 class="section-title"><?php echo $modal_title; ?></h2>
<?php foreach ($videos as $video) { ?>
<div class="video-block">
<h2 class="video-title"><?php echo $video['title']; ?></h2>
<?php if ($video['desc']) { ?>
<p class="video-desc"><?php echo nl2br($video['desc']); ?></p>
<?php } ?>
<div class="video-container">
<iframe
src="https://www.youtube.com/embed/<?php echo $video['id']; ?>?autoplay=0&rel=0&modestbranding=1&playsinline=1"
title="<?php echo htmlspecialchars($video['title']); ?>"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<?php } ?>
</body>
</html>
.modal-title CSSSince we're now using .section-title (Tattoo’s native class), you can remove the custom .modal-title CSS block completely, or override it to match:
.modal-body .modal-title {
/* Optional: force it to match section-title if still used somewhere */
font-size: 2.5rem !important;
margin: 1.5rem 0 1rem !important;
text-align: center !important;
color: #000 !important;
}
But since the static page now uses <h2 class="section-title">, you can safely delete the .modal-title CSS block — Tattoo’s existing .section-title styles will take over automatically.
static-embed.php with the updated code above.modal-title CSS block from your Custom CSSsection-title styling (size, color, spacing, etc.)If you want the per-video titles (.video-title) to match Tattoo’s .section-title style:
Add this to Custom CSS:
.modal-body .video-title {
font-size: 1.8rem !important;
margin: 1rem 0 0.5rem !important;
color: #000 !important;
}
This makes them look consistent with normal section titles.
Let me know how it looks — the title should now match perfectly, and everything will feel like a native dynamic modal!
Current time: January 13, 2026 10:15 PM HKT
We’re making it seamless! 😊
The normal dynamic modals:
.section-title, .generic-content, etc.).modal-body (no iframe wrapper)The static modal (iframe):
<iframe><head> and styles (not inheriting Tattoo’s global CSS)We can achieve this by:
static-embed.php and relying on Tattoo’s global CSSsection-title, generic-content)Replace your current static-embed.php with this version (it now uses Tattoo classes and removes most inline styles):
<?php
// static-embed.php - Version 2.1 - Multi-video embed with Tattoo styling
// Last updated: January 13, 2026
// Get videos from query string (format: v=ID1|Title1|Desc1,ID2|Title2|Desc2)
$videos_raw = isset($_GET['v']) ? htmlspecialchars($_GET['v']) : '';
$videos = [];
if ($videos_raw) {
$entries = explode(',', $videos_raw);
foreach ($entries as $entry) {
$parts = explode('|', $entry);
$id = trim($parts[0] ?? '');
$title = trim($parts[1] ?? 'Video');
$desc = trim($parts[2] ?? '');
if ($id) {
$videos[] = [
'id' => $id,
'title' => $title,
'desc' => $desc
];
}
}
}
// Fallback if no videos
if (empty($videos)) {
$videos = [
['id' => 'V3iS2xltayA', 'title' => 'Default Video', 'desc' => '']
];
}
$modal_title = isset($_GET['title']) ? htmlspecialchars($_GET['title']) : 'Videos';
?>
<!DOCTYPE html>
<html lang="zh-hk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo $modal_title; ?></title>
<!-- No custom styles — rely on Tattoo global CSS -->
</head>
<body style="margin:0; padding:1rem; background:#fff;">
<h2 class="section-title"><?php echo $modal_title; ?></h2>
<?php foreach ($videos as $video) { ?>
<div class="generic-content">
<h3 class="section-title"><?php echo $video['title']; ?></h3>
<?php if ($video['desc']) { ?>
<p><?php echo nl2br($video['desc']); ?></p>
<?php } ?>
<div class="content-embed">
<iframe
src="https://www.youtube.com/embed/<?php echo $video['id']; ?>?autoplay=0&rel=0&modestbranding=1&playsinline=1"
title="<?php echo htmlspecialchars($video['title']); ?>"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<?php } ?>
</body>
</html>
section-title for modal title and video titles, generic-content for descriptions, content-embed for iframes.content-embed — inherits your existing embed CSS (responsive 16:9).modal-title CSSSince we're now using .section-title, remove this block from your Custom CSS (or comment it out):
.modal-body .modal-title { ... }
Tattoo’s .section-title will take over automatically.
To make the modal taller and scroll bar appear outside:
In Custom CSS, add/update:
.modal-body .content-embed {
min-height: 315px !important;
height: 60vh !important; /* Adjust as needed — 60vh is good for multiple videos */
}
.modal-body iframe {
width: 100% !important;
height: 100% !important;
display: block !important;
}
.modal-content {
max-height: 90vh !important; /* Modal itself gets taller */
overflow-y: auto !important; /* Scroll bar outside iframe */
}
static-embed.php with the code above.modal-title CSS block.section-title styleLet me know how it looks — the title should now match perfectly, and the modal should feel consistent with dynamic ones.
If you want:
h2 instead of h3I can refine the static page further!
Current time: January 13, 2026 10:45 PM HKT
We're making it feel native! 😊
Modal title still looks quite small and not centered The entire modal content has become much smaller and left aligned The scrollbar is still appearing inside the modal
Playing with the inspect window I find I need to remove "height: 0!important;" from .content-embed in order to enable the inline style "height:80vh;" to work. "max-height: 90vh !important;" has to be removed from .modal-content as well. With these two changes the static modal will look like the dynamic one with scrollbar outside and modal height more acceptable.In general, I find the very first static-embed.php with CSS styles inside looking the best, though less elegant. I think defining our own styles for the static modal looks better than mapping to existing Tatoo styles, as there seems to be style overrides, reminding me of working with the old Square theme.What do you think?
I have incorporated relevant styles from the Tattoo CSS into the <style> block of static-embed.php. This makes the static modal look as close as possible to dynamic ones:
Full static-embed.php:
<?php
// static-embed.php - Version 2.1 - Multi-video embed page for modal iframe
// Last updated: January 13, 2026
// Get videos from query string (format: v=ID1|Title1|Desc1,ID2|Title2|Desc2)
$videos_raw = isset($_GET['v']) ? htmlspecialchars($_GET['v']) : '';
$videos = [];
if ($videos_raw) {
$entries = explode(',', $videos_raw);
foreach ($entries as $entry) {
$parts = explode('|', $entry);
$id = trim($parts[0] ?? '');
$title = trim($parts[1] ?? 'Video');
$desc = trim($parts[2] ?? '');
if ($id) {
$videos[] = [
'id' => $id,
'title' => $title,
'desc' => $desc
];
}
}
}
// Fallback if no videos
if (empty($videos)) {
$videos = [
['id' => 'V3iS2xltayA', 'title' => 'Default Video', 'desc' => '']
];
}
$modal_title = isset($_GET['title']) ? htmlspecialchars($_GET['title']) : 'Videos';
?>
<!DOCTYPE html>
<html lang="zh-hk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?php echo $modal_title; ?></title>
<style>
@font-face {
font-family: 'Roboto Flex';
src: url('../dynamic/fonts/robotoflex/robotoflex.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
font-style: normal;
}
body {
margin: 0;
padding: 0;
background: #FFFFFF;
font-family: var(--body-font, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol");
font-size: clamp(1rem, 1rem + (0.19999999999999996 * ((100vw - 20rem) / 46)), 1.2rem);
line-height: 1.6;
letter-spacing: 0em;
color: #111111;
overflow: auto;
}
.section-title {
font-family: 'Roboto Flex', sans-serif;
font-weight: 600;
font-size: 2rem;
line-height: 1.2;
letter-spacing: -0.02em;
margin-bottom: 2.5rem;
text-align: center;
color: #111111;
}
.video-title {
font-family: 'Roboto Flex', sans-serif;
font-weight: 600;
font-size: 1.5rem;
line-height: 1.2;
letter-spacing: -0.02em;
margin-bottom: 0.5rem;
color: #111111;
}
.video-desc {
font-size: clamp(1rem, 1rem + (0.19999999999999996 * ((100vw - 20rem) / 46)), 1.2rem);
line-height: 1.6;
letter-spacing: 0em;
margin-bottom: 1rem;
color: #111111;
}
.video-container {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 */
margin: 1rem 0;
border-radius: 3px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.video-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
@media (max-width: 768px) {
body {
padding: 1rem;
}
.section-title {
font-size: 1.8rem;
}
.video-title {
font-size: 1.3rem;
}
.video-desc {
font-size: 1rem;
}
}
</style>
</head>
<body>
<h2 class="section-title"><?php echo $modal_title; ?></h2>
<?php foreach ($videos as $video) { ?>
<div class="video-block">
<h3 class="video-title"><?php echo $video['title']; ?></h3>
<?php if ($video['desc']) { ?>
<p class="video-desc"><?php echo nl2br($video['desc']); ?></p>
<?php } ?>
<div class="video-container">
<iframe
src="https://www.youtube.com/embed/<?php echo $video['id']; ?>?autoplay=0&rel=0&modestbranding=1&playsinline=1"
title="<?php echo htmlspecialchars($video['title']); ?>"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
</div>
</div>
<?php } ?>
</body>
</html>
<h2 class="section-title"> with Tattoo styles (2rem, bold, centered)<h3 class="video-title"> with slightly smaller Tattoo-like styling (1.5rem)<p class="video-desc"> with Tattoo body text styles (clamp font, line-height 1.6).video-container with Tattoo border-radius (3px), shadow, and 16:9 paddingHere is your entire Custom CSS, organized, with duplicates removed, and unnecessary/overlapping styles cleaned up:
/* Tattoo Theme + Dynamic Sections – Organized Custom CSS */
/* Safe, no conflicts – All features preserved */
/* 1. Hero / Background – Full Browser Width */
.dynamic-section.hero,
.dynamic-section[style*="background-image"] {
min-height: 60vh;
background-size: cover !important;
background-position: center !important;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: white;
width: 100vw !important;
margin-left: calc(-50vw + 50%) !important;
}
.dynamic-section.hero::before,
.dynamic-section[style*="background-image"]::before {
content: "";
position: absolute;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 1;
}
.dynamic-section.hero > .inner,
.dynamic-section[style*="background-image"] > .inner {
position: relative;
z-index: 2;
max-width: 90%;
padding: 2rem;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
background: transparent;
}
/* 2. Title Styling */
.section-title {
margin-bottom: 2.5rem !important;
font-size: 2rem;
line-height: 1.2;
}
.dynamic-section.hero .section-title {
margin-bottom: 3rem !important;
font-size: 3rem;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.7);
}
/* 3. Normal Content Spacing */
.dynamic-section:not(.hero):not([style*="background-image"]) > .inner {
padding: 8rem 2rem 2rem !important;
max-width: 1200px;
margin: 0 auto;
}
/* 4. Multi-Column Grid */
.dynamic-section.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
padding: 4rem 2rem;
}
/* 5. Brand Button */
.dynamic-section .btn,
.dynamic-section a[href*="paypal"],
.dynamic-section button {
background: #A6752E !important;
color: white !important;
padding: 0.8rem 1.8rem !important;
border-radius: 6px !important;
text-decoration: none !important;
display: inline-block;
font-weight: 600;
}
.dynamic-section .btn:hover {
background: #8B5A1F !important;
}
.content__buttons {
text-align: center;
margin: 2rem 0;
}
/* 6. Separator – Tight Spacing + Below Section */
.separator-wrapper {
display: flex;
justify-content: center;
width: 100vw;
margin-left: calc(-50vw + 50%);
background: white !important;
padding: 2rem 0;
z-index: 2 !important;
}
.separator-wrapper .separator {
display: block !important;
text-align: center !important;
margin: 1.5rem auto 0 auto !important;
height: auto;
}
.separator-wrapper .default-bee {
max-width: 150px !important;
}
.separator-wrapper .custom-separator {
max-width: none !important;
width: 100% !important;
}
/* 7. Responsive YouTube/Video Embeds – Stronger Overrides for Tattoo */
.content-embed {
position: relative !important;
width: 100% !important;
padding-bottom: 56.25% !important;
height: 0 !important;
overflow: hidden !important;
margin: 2rem 0 !important;
}
.content-embed iframe {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
border: 0 !important;
display: block !important;
}
/* 8. Modal – Chaplaincy-Style: Centered + Double-Scrollbar */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.5);
}
.modal.is-active {
display: block;
}
.modal-content {
background-color: #fff;
margin: 5% auto;
padding: 2rem;
border: 1px solid #888;
width: 80%;
max-width: 800px;
min-height: 200px;
position: relative;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
box-sizing: border-box;
overflow-y: auto;
}
.modal-close {
color: #aaa;
position: absolute;
top: 10px;
right: 20px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.3s ease;
}
.modal-close:hover,
.modal-close:focus {
color: #000;
text-decoration: none;
}
/* Only for modal embeds (affects static/iframe modals only) */
.modal-body .content-embed {
min-height: 315px !important;
height: 70vh !important;
}
.modal-body iframe {
width: 100% !important;
height: 100% !important;
display: block !important;
}
.separator-wrapper block, overlapping max-width rules, useless background-repeat.modal-body .content-embed, .modal-body iframe)Let me know how it looks — if still not perfect, share a screenshot of the static modal on mobile, and I'll refine the CSS further.
Current time: January 13, 2026 11:15 PM HKT
We're polishing it! 😊
AND second, change the circled "element.style" to "height: 95vh;" (See attachment for circled area & arrow-point spot) in order to move the scrollbar from the static modal to the outside, like the dynamic modal. But I can't locate from my CSS "element.style".Also, the following styles you provided at the very bottom of my CSS didn't affect the modal height regardless of what values I tried with .modal-body .content-embed:
Why the dynamic modal style is affecting the static modal and the change of CSS has no effect?
Dynamic modal styles are affecting static modal
#story-modal.modal, .modal-content, .modal-body)..modal-body .content-embed.static-embed.php) does not inherit Tattoo’s global CSS — but the iframe wrapper (.content-embed) and the modal container do inherit the outer modal styles.height: 0 !important; on .content-embed, it collapses the iframe container → the iframe itself can't expand even if you set height: 95vh inline (because its parent is forced to 0 height).Why .modal-body .content-embed CSS has no effect
height: 0 !important; in your general .content-embed rule has higher specificity or is applied later in the cascade, overriding your more specific .modal-body .content-embed { height: 80vh !important; }.style="height:80vh;") have even higher priority, but if the parent (.content-embed) is height: 0 !important;, the child iframe is constrained to 0 height — hence no effect.Why changing "element.style" to 95vh works
.content-embed div (from JS: style="height:80vh;").95vh in inspect, you override the collapsed height → the iframe expands → scrollbar moves outside.Do not change global .content-embed — it would break normal page embeds (which rely on height: 0 + padding-bottom: 56.25% for 16:9 responsiveness).
Instead, make the modal-specific overrides stronger and more specific:
Replace or add this to your Custom CSS (at the very bottom, so it wins):
/* Stronger overrides for static/iframe modals only */
.modal-body .content-embed {
height: 80vh !important; /* or 90vh for taller modal */
min-height: 315px !important;
padding-bottom: 0 !important; /* Remove 16:9 padding — iframe handles aspect */
overflow: visible !important;
}
.modal-body .content-embed iframe {
width: 100% !important;
height: 100% !important;
display: block !important;
border: none !important;
}
/* Make modal taller with external scrollbar (affects all modals — optional) */
.modal-content {
max-height: 90vh !important;
overflow-y: auto !important;
}
.modal-body .content-embed → overrides the general height: 0 !important; and padding-bottom.modal-body iframe → ensures iframe fills the new height.modal-content max-height: 90vh → makes the entire modal taller with external scroll (optional — test if it affects dynamic modals too much)Add a special class to the iframe wrapper in JS (in the static case block):
const iframeHtml = `<div class="content-embed static-iframe" style="height:80vh;"><iframe src="${iframeUrl}" frameborder="0" allowfullscreen style="width:100%;height:100%;"></iframe></div>`;
Then target it specifically:
.modal-body .static-iframe {
height: 80vh !important;
min-height: 315px !important;
padding-bottom: 0 !important;
overflow: visible !important;
}
.modal-body .static-iframe iframe {
width: 100% !important;
height: 100% !important;
display: block !important;
}
This way, only static modals get the fixed height — dynamic modals remain unaffected.
.modal-body .content-embed, .modal-body iframe).static-iframe class in JS + CSS for isolationheight: 80vh to 90vh or 95vhThis keeps dynamic modals untouched while making static ones look and behave better.
Let me know how it looks after these changes — we can fine-tune the height/percentage or add the .static-iframe class if needed.
Current time: January 13, 2026 11:45 PM HKT
We're making it consistent! 😊
Current situation (static-embed.php in iframe):
static-embed.php → duplication, maintenance hassle, and risk of inconsistencyProposed change (iframe code in "code" field):
innerHTML = content).modal-content, .modal-body) controls the height/scroll → external scrollbar, flexible height, consistent with dynamic modals.content-embed { height: 0 !important; }) can be overridden with one targeted rulePros:
renderEmbed (if you want to clean attributes)Cons:
Add a new code_type in AppGini (optional but recommended):
web_content table → add "Iframe" or "Static" to the code_type dropdown optionsUpdate PHP (in the non-modal branch, add handling for new code_type):
// Existing models...
elseif ($row['code_type'] === 'Iframe' || $row['code_type'] === 'Static') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
window.contentCode — same as YouTube/PayPalif (item.type === 'YouTube') {
// existing YouTube code...
} else if (item.type === 'PayPal') {
// existing PayPal code...
} else if (item.type === 'Iframe' || item.type === 'Static') {
container.innerHTML = item.code;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered Iframe/Static embed for ID ${item.id}`, 'orange');
}
In admin:
code_type = "Iframe" or "Static"code field, e.g.:
<iframe src="https://www.youtube.com/embed/V3iS2xltayA?autoplay=0&rel=0" frameborder="0" allowfullscreen></iframe><div style="display:flex; flex-wrap:wrap; gap:1rem;">
<iframe src="https://www.youtube.com/embed/ID1" frameborder="0" allowfullscreen style="flex:1; min-width:300px;"></iframe>
<iframe src="https://www.youtube.com/embed/ID2" frameborder="0" allowfullscreen style="flex:1; min-width:300px;"></iframe>
</div>Optional CSS (to make it look good in modal):
.modal-body .content-embed {
min-height: 315px !important;
height: auto !important; /* flexible height */
}
.modal-body .content-embed iframe {
width: 100% !important;
height: 400px !important; /* or auto */
border-radius: 8px !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
}
renderEmbed)code field)This is simpler than static page + iframe and avoids all conflicts.
#story-modal selector from my previous messageLet me know which direction you want to go — I can generate the full updated PHP/JS for either approach.
Current time: January 14, 2026 11:05 AM HKT
We're close to a perfect solution! 😊
This method is much simpler and more reliable than the previous dynamic fetch + extraction attempts. Here's why it solves the problem and how it works:
The core problem before was:
window.contentCode only on the main page loadWith the "Modal Video" approach:
<iframe src="https://www.youtube.com/embed/VIDEO_ID"...></iframe>)window.contentCode on the main page (same as YouTube/PayPal)innerHTML = contentrenderEmbed (in openModal) finds the matching item in window.contentCode (because it was pushed on load) → renders the iframe correctlyAdd "Modal Video" to code_type in AppGini (if you want dropdown support — optional)
web_content table → add "Modal Video" to code_type optionsUpdate PHP (in the non-modal branch, add handling for "Modal Video"):
// Existing models...
elseif ($row['model'] === 'title-text-video') {
// ... your existing code ...
}
elseif ($row['model'] === 'title-text-image-code' || $row['model'] === 'title-text-code') {
// ... your existing code ...
}
// New: Modal Video (raw iframe code)
elseif ($row['code_type'] === 'Modal Video') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'Modal Video'});</script>";
}
window.contentCode — same as YouTubeif (item.type === 'YouTube') {
// existing...
} else if (item.type === 'PayPal') {
// existing...
} else if (item.type === 'Modal Video') {
container.innerHTML = item.code;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered Modal Video embed for ID ${item.id}`, 'orange');
}
In admin:
code_type = "Modal Video"code field, e.g.:
<iframe width="100%" height="400" src="https://www.youtube.com/embed/V3iS2xltayA?rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><iframe src="https://www.youtube.com/embed/ID1" ...></iframe>
<iframe src="https://www.youtube.com/embed/ID2" ...></iframe>CSS (optional, for consistent look):
.content-embed {
min-height: 315px;
margin: 1rem 0;
}
.content-embed iframe {
width: 100%;
height: 400px; /* or auto */
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
This is simpler, more stable, and avoids all previous conflicts.
Let me know if you want the full updated PHP/JS files with this "Modal Video" logic already integrated — I can generate them right away.
Current time: January 14, 2026 11:15 AM HKT
This should be the cleanest solution yet! 😊
These versions:
<?php
// pull-contents.php (Version 2.86 - Modal YT Embed Fixed + All Features Preserved)
// Last updated: January 13, 2026
// Based on your V2.83 / V2.74-2 — no shortening, no removals
// Added: title-text-video model, bee below background with classes
// Enhanced: Modal embed re-rendering (500ms delay + direct scan)
define('DEBUG', true);
define('IMAGE_BASE_PATH', 'https://navi.claudioflute.live/images/');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$config_path = '../config.php';
if (!file_exists($config_path)) {
die("Error: config.php not found at $config_path");
}
require_once $config_path;
if (!isset($dbServer) || !isset($dbUsername) || !isset($dbPassword) || !isset($dbDatabase)) {
die("Error: AppGini database variables not defined in config.php");
}
$conn = new mysqli($dbServer, $dbUsername, $dbPassword, $dbDatabase, $dbPort ?: 3306);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Auto-detect page
if (isset($_GET['page'])) {
$current_page = $_GET['page'];
} else {
$calling_file = basename($_SERVER['SCRIPT_FILENAME']);
$current_page = str_replace('.php', '', $calling_file);
if ($current_page === 'index') {
$current_page = 'landing';
}
}
$status = 'Show';
// Preload PayPal SDK
$paypalSdkUrl = 'https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription';
echo "<script src=\"$paypalSdkUrl\" id=\"paypal-sdk\" async=\"false\"></script>";
// Is modal request
$is_modal_request = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || isset($_GET['modal']);
// Is stories page (for preview logic)
$is_stories_page = ($current_page === 'stories');
// SQL - matches your schema
$sql = "SELECT id, title, content, code, code_type, image, button_link, button_text, position, model, date_edited, theme_img, img_role, chk_divider
FROM web_content
WHERE page = ? AND status = ?
ORDER BY position ASC, date_edited DESC";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $current_page, $status);
$stmt->execute();
$result = $stmt->get_result();
$sections = [];
while ($row = $result->fetch_assoc()) {
$sections[] = $row;
}
// Group by position for multi-column
$sections_by_position = [];
foreach ($sections as $row) {
$position = $row['position'] ?? 1;
$sections_by_position[$position][] = $row;
}
if (!empty($sections_by_position)) {
ksort($sections_by_position);
foreach ($sections_by_position as $position => $group) {
$is_grid = count($group) > 1;
if ($is_modal_request) {
foreach ($group as $index => $row) {
echo "<div class=\"modal-story-content\">";
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
}
echo $row['content'];
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
echo "</div>";
if ($index < count($group) - 1) {
echo "<div class=\"modal-section-separator\"></div>";
}
}
} else {
// SMART CLASS for Tattoo
$section_class = 'content';
$has_background = false;
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$has_background = true;
}
}
if ($has_background) {
$section_class = 'hero';
}
if ($is_grid) {
$section_class .= ' grid';
}
$bg_style = '';
if ($section_class === 'hero') {
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$bg_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$bg_style = "style=\"background-image: url('$bg_url');\"";
break;
}
}
}
echo "<section class=\"{$section_class} dynamic-section\" {$bg_style}>";
echo "<div class=\"inner\">";
foreach ($group as $row) {
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">{$row['title']}</h2>";
}
echo $row['content'];
if ($row['model'] === 'title-text-image-video') {
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
elseif ($row['model'] === 'title-text-video') {
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
elseif ($row['model'] === 'title-text-image-code' || $row['model'] === 'title-text-code') {
if (!empty($row['image']) && $row['model'] === 'title-text-image-code') {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
}
elseif (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
// BUTTON RENDERING
if (!empty($row['button_text'])) {
$button_text = htmlspecialchars($row['button_text']);
$button_link = htmlspecialchars($row['button_link'] ?: '#');
echo "<div class=\"content__buttons\">";
if ($is_stories_page) {
$modal_id = 'modal-content-' . $row['id'];
echo "<div id=\"$modal_id\" style=\"display:none;\">";
echo '<div class="modal-story-content">';
if (!empty($row['title'])) echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image modal-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['content'])) echo $row['content'];
echo '</div>';
echo "</div>";
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-id=\"$modal_id\">$button_text</a>";
} else {
if (!empty($button_link) && $button_link !== '#') {
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-link=\"$button_link\">$button_text</a>";
} else {
echo "<a class=\"btn\" href=\"javascript:void(0)\">$button_text</a>";
}
}
echo "</div>";
}
}
echo "</div>"; // .inner
// Separator — moved AFTER section for below background
$show_separator = false;
$sep_url = '';
if (!empty($row['theme_img']) && $row['img_role'] === 'Separator') {
$sep_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$show_separator = true;
} elseif (!empty($row['chk_divider']) && $row['chk_divider'] == 1) {
$sep_url = 'https://navi.claudioflute.live/public/Flying_bee_R.gif';
$show_separator = true;
}
if ($show_separator) {
$sep_class = ($row['img_role'] === 'Separator') ? 'separator custom-separator' : 'separator default-bee';
echo "<div class=\"separator-wrapper\">";
echo "<img src=\"$sep_url\" class=\"$sep_class\" alt=\"Separator\">";
echo "</div>";
}
echo "</section>";
}
}
} else {
echo "No sections found.";
}
// Modal container (only for non-AJAX requests)
if (!$is_modal_request) {
echo '<div id="story-modal" class="modal">';
echo '<div class="modal-content">';
echo '<span class="modal-close">×</span>';
echo '<div id="modal-body"></div>';
echo '</div>';
echo '</div>';
}
// Load external JS for modal and embed rendering
echo '<script src="/modal-embed.js?v=' . time() . '"></script>';
$stmt->close();
$conn->close();
?>
// modal-embed.js - Version 2.86 (Tattoo-optimized, modal YouTube embed fixed)
// Last updated: January 13, 2026
console.log("modal-embed.js LOADED SUCCESSFULLY (Tattoo version 2.86)");
document.addEventListener('DOMContentLoaded', function() {
const DEBUG = true; // Change to false when done testing
let debugLog = null;
if (DEBUG) {
debugLog = document.createElement('div');
debugLog.id = 'debug-log';
debugLog.style.position = 'fixed';
debugLog.style.bottom = '10px';
debugLog.style.left = '10px';
debugLog.style.background = 'rgba(0, 0, 0, 0.8)';
debugLog.style.color = 'white';
debugLog.style.padding = '10px';
debugLog.style.zIndex = '1000';
debugLog.style.maxHeight = '200px';
debugLog.style.overflowY = 'auto';
document.body.appendChild(debugLog);
}
function logDebug(message, color = 'green') {
if (DEBUG && debugLog) {
debugLog.insertAdjacentHTML('beforeend', `<div style="color: ${color}; font-weight: bold;">Debug: ${message}</div>`);
debugLog.scrollTop = debugLog.scrollHeight;
}
if (DEBUG && console) console.log(`Debug: ${message}`);
}
if (DEBUG) logDebug('Modal script loaded (Tattoo version 2.86)');
const modal = document.getElementById('story-modal');
const modalBody = document.getElementById('modal-body');
const closeBtn = document.querySelector('.modal-close');
const triggers = document.querySelectorAll('.modal-trigger');
if (DEBUG) logDebug('Found ' + triggers.length + ' modal triggers');
// Embed rendering function
function renderEmbed(item, index, total) {
const container = document.getElementById(`embed-${item.id}`);
if (!container) {
if (DEBUG) logDebug(`Container not found for embed ID ${item.id}`, 'red');
return;
}
if (item.type === 'YouTube') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered YouTube embed for ID ${item.id} (sandbox removed)`, 'orange');
} else if (item.type === 'PayPal') {
const divMatch = item.code.match(/<div\s+[^>]*id="[^"]+"[^>]*>/i);
const buttonMatch = item.code.match(/<script>\s*paypal\.Buttons\([\s\S]*?\)\.render\(['"][^'"]+['"]\);\s*<\/script>/i);
if (divMatch && buttonMatch) {
const uniqueId = `paypal-button-container-${item.id}`;
let divContent = divMatch[0].replace(/id="[^"]+"/, `id="${uniqueId}"`);
container.innerHTML = divContent;
container.querySelector(`#${uniqueId}`).style.cssText = 'width: 100% !important; min-width: 300px !important; max-width: 300px !important; margin: 0 auto !important;';
function waitForSDK(callback) {
if (typeof paypal !== 'undefined') {
callback();
} else {
setTimeout(function() { waitForSDK(callback); }, 100);
}
}
waitForSDK(function() {
let scriptContent = buttonMatch[0].replace(/^<script>|<\/script>$/g, '').trim();
scriptContent = scriptContent.replace(/\.render\(['"][^'"]+['"]\)/, `.render('#${uniqueId}')`);
const uniqueScript = `(function() { ${scriptContent} })();`;
const buttonScript = document.createElement('script');
buttonScript.textContent = uniqueScript;
document.body.appendChild(buttonScript);
});
}
}
}
// Run embed rendering on page load
if (window.contentCode && Array.isArray(window.contentCode)) {
window.contentCode.forEach(function(item, index) {
renderEmbed(item, index, window.contentCode.length);
});
} else if (DEBUG) {
logDebug('contentCode is not an array or unavailable', 'red');
}
// PHP debug info (safe try-catch)
try {
if (window.debugFromPHP) {
console.log('PHP Debug Info:', window.debugFromPHP);
if (DEBUG) logDebug('PHP → Fetched sections: ' + window.debugFromPHP.fetched + ', Grouped positions: ' + window.debugFromPHP.grouped + ', Modal request: ' + window.debugFromPHP.modalRequest, 'purple');
} else {
console.warn('No debug info from PHP');
}
} catch (e) {
console.warn('Error reading PHP debug info:', e);
}
// Modal logic only if elements exist
if (modal && modalBody && closeBtn && triggers.length > 0) {
if (DEBUG) logDebug('Modal elements found - enabling modal functionality');
triggers.forEach(function(trigger) {
trigger.addEventListener('click', function(e) {
e.preventDefault();
if (DEBUG) logDebug(`Modal trigger clicked: ${this.getAttribute('data-modal-id') || this.getAttribute('data-modal-link')}`, 'green');
const modalId = this.getAttribute('data-modal-id');
if (modalId) {
const contentDiv = document.getElementById(modalId);
if (contentDiv) {
const content = contentDiv.innerHTML;
if (DEBUG) logDebug('Stories modal triggered, content from ID: ' + modalId);
openModal(content);
} else {
openModal('<p>Content not found.</p>');
if (DEBUG) logDebug('Modal content not found for ID: ' + modalId, 'red');
}
return;
}
const link = this.getAttribute('data-modal-link');
if (DEBUG) logDebug('Trigger clicked, link: ' + link);
if (!link || link === '#' || link.includes('index.php')) {
openModal('<p>This link cannot be loaded in a modal to prevent recursive loading.</p>');
return;
}
let absoluteLink;
try {
absoluteLink = new URL(link, window.location.origin).href;
} catch (error) {
openModal('<p>Invalid URL.</p>');
return;
}
absoluteLink += (absoluteLink.includes('?') ? '&' : '?') + 'modal=1';
if (DEBUG) logDebug('Fetching absolute link: ' + absoluteLink);
openModal('<p>Loading...</p>');
fetch(absoluteLink, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function(response) {
if (DEBUG) logDebug('Fetch response status: ' + response.status + ' ' + response.statusText);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return response.text();
})
.then(function(data) {
if (DEBUG) logDebug('Fetched HTML length: ' + data.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
let combinedContent = '';
const sections = doc.querySelectorAll('.modal-story-content');
if (DEBUG) logDebug('Found ' + sections.length + ' modal-story-content sections');
if (sections.length > 0) {
sections.forEach(function(section, index) {
combinedContent += section.outerHTML.trim();
if (index < sections.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
const tattooFallback = doc.querySelectorAll('.dynamic-section .inner');
if (DEBUG) logDebug('Found ' + tattooFallback.length + ' Tattoo .dynamic-section .inner sections');
if (tattooFallback.length > 0) {
tattooFallback.forEach(function(section, index) {
const newContent = document.createElement('div');
newContent.className = 'modal-story-content';
newContent.innerHTML = section.innerHTML;
combinedContent += newContent.outerHTML.trim();
if (index < tattooFallback.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
if (DEBUG) logDebug('No content found in Tattoo structure', 'red');
combinedContent = '<p>Content could not be parsed from the page.</p>';
}
}
if (combinedContent) {
openModal(combinedContent);
} else {
openModal('<p>No content found.</p>');
}
})
.catch(function(error) {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
openModal('<p>Sorry, the content could not be loaded: ' + error.message + '</p>');
});
});
});
closeBtn.addEventListener('click', closeModal);
window.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && modal.style.display === 'block') {
closeModal();
}
});
} else {
if (DEBUG) logDebug('Modal elements missing - skipping modal functionality (normal on non-modal pages)', 'orange');
}
function openModal(content) {
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = content;
// Re-run embed rendering after modal content is inserted
setTimeout(function() {
const modalEmbeds = modalBody.querySelectorAll('.content-embed[id^="embed-"]');
if (modalEmbeds.length > 0) {
if (DEBUG) logDebug('Found ' + modalEmbeds.length + ' embeds inside modal – re-rendering', 'orange');
modalEmbeds.forEach(function(embedDiv) {
const id = embedDiv.id.replace('embed-', '');
const item = window.contentCode?.find(function(i) {
return i.id == id;
});
if (item) {
renderEmbed(item, 0, 1);
if (DEBUG) logDebug('Re-rendered embed-' + id + ' inside modal', 'green');
} else {
if (DEBUG) logDebug('No matching contentCode for embed-' + id + ' in modal', 'red');
}
});
} else {
if (DEBUG) logDebug('No embeds found inside modal', 'orange');
}
}, 500); // 500ms delay
const modal = document.getElementById('story-modal');
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
const closeBtn = document.querySelector('.modal-close');
if (closeBtn) closeBtn.focus();
if (DEBUG) logDebug('Modal opened');
}
function closeModal() {
const modal = document.getElementById('story-modal');
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = '';
if (DEBUG) logDebug('Modal closed');
}
});
pull-contents.php with V2.86 PHP abovemodal-embed.js with V2.86 JS aboveThis is the cleanest, full-file reset — no more partial fixes, no syntax risks.
Let me know the console after clicking the button — especially any "Re-rendered embed" lines and whether the video shows in the modal.
Current time: January 13, 2026 08:45 PM HKT
This should be the one — clean and complete! 😊
For a section that uses the new "Modal Video" code_type, you should set the modal button link exactly the same way as usual (i.e., to the slug of the target page/section, such as any_section.php or whatever the page slug is).
Button setup (in the "mother" section):
button_link to the slug of the target page (e.g. landing-260107.php, any_section.php, etc.)any_section.php?modal=1What appears in the modal:
any_section.php (according to display order / position)section-title)content)image field)Example structure on any_section.php:
The modal content is rendered in exactly the same order as on the normal page — just wrapped in the modal.
You do not need to link to static-embed.php or anything else.
Just use the normal target page slug as data-modal-link — the same way you do for all other dynamic modals.
In AppGini admin:
code_type dropdown (optional but nice for clarity)landing-260107), create a section with:code_type = Modal Videocode field = raw <iframe src="https://www.youtube.com/embed/..." ...></iframe> (or multiple iframes)In the "mother" section button:
button_link = landing-260107.php (or whatever the slug is)landing-260107.php?modal=1This is the cleanest, most consistent way — no static page needed, no extra fetch conflicts.
Let me know if you want:
Otherwise — go ahead and test! 😊
Current time: January 14, 2026 11:45 AM HKT
If we put the raw <iframe> code inside the modal sections (i.e. the sections on the target page like any_section.php that get fetched into the modal), we would indeed reintroduce the original problem:
window.contentCode.push(...)) would only run on the main page load.any_section.php?modal=1, the HTML is inserted statically — the push script is not executed in the main page's JS context.So putting the iframe code in the modal sections would defeat the purpose of the workaround.
To make it work as intended (avoid the ID matching issue while keeping dynamic text/image before the video), the raw <iframe> code must be placed in the mother section itself (the section that has the modal trigger button).
Mother section (the one users see on the main page, with the button):
# if you want no fetch — see below)<iframe> code (or multiple iframes) hereModal behavior:
# → instant, no loading, no ID mismatchOption A: No Fetch (Fastest, Recommended for This Case)
# (or leave empty)Option B: Fetch a Dummy Page (If You Want to Keep Fetch Logic)
dummy-modal.php) with no content or just a placeholderdummy-modal.phpBut Option A (link = #) is cleaner, faster, and avoids all fetch/ID issues.
In AppGini admin:
<iframe width="100%" height="450" src="https://www.youtube.com/embed/ID1?rel=0" frameborder="0" allowfullscreen></iframe>
<iframe width="100%" height="450" src="https://www.youtube.com/embed/ID2?rel=0" frameborder="0" allowfullscreen></iframe># (or empty)No change needed to PHP or JS — your existing embed push + renderEmbed already handles it
Test:
This is the cleanest way — no fetch, no extra files, no ID mismatch, video appears exactly where you want it in the modal flow.
If you want to keep the fetch (for consistency with other modals), let me know — we can make a dummy page or adjust the push.
Otherwise — this should work immediately with your current files.
Let me know how it goes or if you need the full PHP/JS again with any small tweaks!
Current time: January 14, 2026 12:15 PM HKT
This is the elegant workaround we were looking for! 😊
Let me summarize and confirm your points clearly, then provide the updated full files (V2.87 for both) with the necessary small changes to support the new "Modal Video" type exactly as you described.
#) — modal content comes from the mother section's content + the embedded iframe from the Modal Video sectionany_section.php (normal fetch)<iframe> (just put them in the "code" field)All of this is possible with very minimal changes to PHP and JS.
PHP (pull-contents.php):
code_type === 'Modal Video'window.contentCode (so main page doesn't show it)JS (modal-embed.js):
else if (item.type === 'Modal Video') in renderEmbed to insert raw code (no sandbox removal needed)setTimeout re-render in openModal — it will now pick up Modal Video embeds<?php
// pull-contents.php (Version 2.87 - Modal Video Type Added)
// Last updated: January 14, 2026
// Features: title-text-video model, bee below background with classes, Modal Video type (iframe only in modal)
define('DEBUG', true);
define('IMAGE_BASE_PATH', 'https://navi.claudioflute.live/images/');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$config_path = '../config.php';
if (!file_exists($config_path)) {
die("Error: config.php not found at $config_path");
}
require_once $config_path;
if (!isset($dbServer) || !isset($dbUsername) || !isset($dbPassword) || !isset($dbDatabase)) {
die("Error: AppGini database variables not defined in config.php");
}
$conn = new mysqli($dbServer, $dbUsername, $dbPassword, $dbDatabase, $dbPort ?: 3306);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Auto-detect page
if (isset($_GET['page'])) {
$current_page = $_GET['page'];
} else {
$calling_file = basename($_SERVER['SCRIPT_FILENAME']);
$current_page = str_replace('.php', '', $calling_file);
if ($current_page === 'index') {
$current_page = 'landing';
}
}
$status = 'Show';
// Preload PayPal SDK
$paypalSdkUrl = 'https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription';
echo "<script src=\"$paypalSdkUrl\" id=\"paypal-sdk\" async=\"false\"></script>";
// Is modal request
$is_modal_request = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || isset($_GET['modal']);
// Is stories page (for preview logic)
$is_stories_page = ($current_page === 'stories');
// SQL - matches your schema
$sql = "SELECT id, title, content, code, code_type, image, button_link, button_text, position, model, date_edited, theme_img, img_role, chk_divider
FROM web_content
WHERE page = ? AND status = ?
ORDER BY position ASC, date_edited DESC";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $current_page, $status);
$stmt->execute();
$result = $stmt->get_result();
$sections = [];
while ($row = $result->fetch_assoc()) {
$sections[] = $row;
}
// Group by position for multi-column
$sections_by_position = [];
foreach ($sections as $row) {
$position = $row['position'] ?? 1;
$sections_by_position[$position][] = $row;
}
if (!empty($sections_by_position)) {
ksort($sections_by_position);
foreach ($sections_by_position as $position => $group) {
$is_grid = count($group) > 1;
if ($is_modal_request) {
foreach ($group as $index => $row) {
echo "<div class=\"modal-story-content\">";
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
}
echo $row['content'];
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'PayPal', 'Modal Video'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
echo "</div>";
if ($index < count($group) - 1) {
echo "<div class=\"modal-section-separator\"></div>";
}
}
} else {
// SMART CLASS for Tattoo
$section_class = 'content';
$has_background = false;
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$has_background = true;
}
}
if ($has_background) {
$section_class = 'hero';
}
if ($is_grid) {
$section_class .= ' grid';
}
$bg_style = '';
if ($section_class === 'hero') {
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$bg_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$bg_style = "style=\"background-image: url('$bg_url');\"";
break;
}
}
}
echo "<section class=\"{$section_class} dynamic-section\" {$bg_style}>";
echo "<div class=\"inner\">";
foreach ($group as $row) {
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">{$row['title']}</h2>";
}
echo $row['content'];
// title-text-image-video model
if ($row['model'] === 'title-text-image-video') {
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
// title-text-video model (video only, no image)
elseif ($row['model'] === 'title-text-video') {
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
// Existing models (title-text-image-code, title-text-code)
elseif ($row['model'] === 'title-text-image-code' || $row['model'] === 'title-text-code') {
if (!empty($row['image']) && $row['model'] === 'title-text-image-code') {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'PayPal', 'Modal Video'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
}
// Default image
elseif (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
// BUTTON RENDERING
if (!empty($row['button_text'])) {
$button_text = htmlspecialchars($row['button_text']);
$button_link = htmlspecialchars($row['button_link'] ?: '#');
echo "<div class=\"content__buttons\">";
if ($is_stories_page) {
$modal_id = 'modal-content-' . $row['id'];
echo "<div id=\"$modal_id\" style=\"display:none;\">";
echo '<div class="modal-story-content">';
if (!empty($row['title'])) echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image modal-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['content'])) echo $row['content'];
echo '</div>';
echo "</div>";
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-id=\"$modal_id\">$button_text</a>";
} else {
if (!empty($button_link) && $button_link !== '#') {
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-link=\"$button_link\">$button_text</a>";
} else {
echo "<a class=\"btn\" href=\"javascript:void(0)\">$button_text</a>";
}
}
echo "</div>";
}
}
echo "</div>"; // .inner
// Separator — moved AFTER section for below background
$show_separator = false;
$sep_url = '';
if (!empty($row['theme_img']) && $row['img_role'] === 'Separator') {
$sep_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$show_separator = true;
} elseif (!empty($row['chk_divider']) && $row['chk_divider'] == 1) {
$sep_url = 'https://navi.claudioflute.live/public/Flying_bee_R.gif';
$show_separator = true;
}
if ($show_separator) {
$sep_class = ($row['img_role'] === 'Separator') ? 'separator custom-separator' : 'separator default-bee';
echo "<div class=\"separator-wrapper\">";
echo "<img src=\"$sep_url\" class=\"$sep_class\" alt=\"Separator\">";
echo "</div>";
}
echo "</section>";
}
}
} else {
echo "No sections found.";
}
// Modal container (only for non-AJAX requests)
if (!$is_modal_request) {
echo '<div id="story-modal" class="modal">';
echo '<div class="modal-content">';
echo '<span class="modal-close">×</span>';
echo '<div id="modal-body"></div>';
echo '</div>';
echo '</div>';
}
// Load external JS for modal and embed rendering
echo '<script src="/modal-embed.js?v=' . time() . '"></script>';
$stmt->close();
$conn->close();
?>
// modal-embed.js - Version 2.86 (Tattoo-optimized, Modal Video type support)
// Last updated: January 14, 2026
console.log("modal-embed.js LOADED SUCCESSFULLY (Tattoo version 2.86)");
document.addEventListener('DOMContentLoaded', function() {
const DEBUG = true; // Change to false when done testing
let debugLog = null;
if (DEBUG) {
debugLog = document.createElement('div');
debugLog.id = 'debug-log';
debugLog.style.position = 'fixed';
debugLog.style.bottom = '10px';
debugLog.style.left = '10px';
debugLog.style.background = 'rgba(0, 0, 0, 0.8)';
debugLog.style.color = 'white';
debugLog.style.padding = '10px';
debugLog.style.zIndex = '1000';
debugLog.style.maxHeight = '200px';
debugLog.style.overflowY = 'auto';
document.body.appendChild(debugLog);
}
function logDebug(message, color = 'green') {
if (DEBUG && debugLog) {
debugLog.insertAdjacentHTML('beforeend', `<div style="color: ${color}; font-weight: bold;">Debug: ${message}</div>`);
debugLog.scrollTop = debugLog.scrollHeight;
}
if (DEBUG && console) console.log(`Debug: ${message}`);
}
if (DEBUG) logDebug('Modal script loaded (Tattoo version 2.86)');
const modal = document.getElementById('story-modal');
const modalBody = document.getElementById('modal-body');
const closeBtn = document.querySelector('.modal-close');
const triggers = document.querySelectorAll('.modal-trigger');
if (DEBUG) logDebug('Found ' + triggers.length + ' modal triggers');
// Embed rendering function
function renderEmbed(item, index, total) {
const container = document.getElementById(`embed-${item.id}`);
if (!container) {
if (DEBUG) logDebug(`Container not found for embed ID ${item.id}`, 'red');
return;
}
if (item.type === 'YouTube') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered YouTube embed for ID ${item.id} (sandbox removed)`, 'orange');
} else if (item.type === 'PayPal') {
const divMatch = item.code.match(/<div\s+[^>]*id="[^"]+"[^>]*>/i);
const buttonMatch = item.code.match(/<script>\s*paypal\.Buttons\([\s\S]*?\)\.render\(['"][^'"]+['"]\);\s*<\/script>/i);
if (divMatch && buttonMatch) {
const uniqueId = `paypal-button-container-${item.id}`;
let divContent = divMatch[0].replace(/id="[^"]+"/, `id="${uniqueId}"`);
container.innerHTML = divContent;
container.querySelector(`#${uniqueId}`).style.cssText = 'width: 100% !important; min-width: 300px !important; max-width: 300px !important; margin: 0 auto !important;';
function waitForSDK(callback) {
if (typeof paypal !== 'undefined') {
callback();
} else {
setTimeout(function() { waitForSDK(callback); }, 100);
}
}
waitForSDK(function() {
let scriptContent = buttonMatch[0].replace(/^<script>|<\/script>$/g, '').trim();
scriptContent = scriptContent.replace(/\.render\(['"][^'"]+['"]\)/, `.render('#${uniqueId}')`);
const uniqueScript = `(function() { ${scriptContent} })();`;
const buttonScript = document.createElement('script');
buttonScript.textContent = uniqueScript;
document.body.appendChild(buttonScript);
});
}
} else if (item.type === 'Modal Video') {
container.innerHTML = item.code;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered Modal Video embed for ID ${item.id}`, 'orange');
}
}
// Run embed rendering on page load
if (window.contentCode && Array.isArray(window.contentCode)) {
window.contentCode.forEach(function(item, index) {
renderEmbed(item, index, window.contentCode.length);
});
} else if (DEBUG) {
logDebug('contentCode is not an array or unavailable', 'red');
}
// PHP debug info (safe try-catch)
try {
if (window.debugFromPHP) {
console.log('PHP Debug Info:', window.debugFromPHP);
if (DEBUG) logDebug('PHP → Fetched sections: ' + window.debugFromPHP.fetched + ', Grouped positions: ' + window.debugFromPHP.grouped + ', Modal request: ' + window.debugFromPHP.modalRequest, 'purple');
} else {
console.warn('No debug info from PHP');
}
} catch (e) {
console.warn('Error reading PHP debug info:', e);
}
// Modal logic only if elements exist
if (modal && modalBody && closeBtn && triggers.length > 0) {
if (DEBUG) logDebug('Modal elements found - enabling modal functionality');
triggers.forEach(function(trigger) {
trigger.addEventListener('click', function(e) {
e.preventDefault();
if (DEBUG) logDebug(`Modal trigger clicked: ${this.getAttribute('data-modal-id') || this.getAttribute('data-modal-link')}`, 'green');
const modalId = this.getAttribute('data-modal-id');
if (modalId) {
const contentDiv = document.getElementById(modalId);
if (contentDiv) {
const content = contentDiv.innerHTML;
if (DEBUG) logDebug('Stories modal triggered, content from ID: ' + modalId);
openModal(content);
} else {
openModal('<p>Content not found.</p>');
if (DEBUG) logDebug('Modal content not found for ID: ' + modalId, 'red');
}
return;
}
const link = this.getAttribute('data-modal-link');
if (DEBUG) logDebug('Trigger clicked, link: ' + link);
if (!link || link === '#' || link.includes('index.php')) {
openModal('<p>This link cannot be loaded in a modal to prevent recursive loading.</p>');
return;
}
let absoluteLink;
try {
absoluteLink = new URL(link, window.location.origin).href;
} catch (error) {
openModal('<p>Invalid URL.</p>');
return;
}
absoluteLink += (absoluteLink.includes('?') ? '&' : '?') + 'modal=1';
if (DEBUG) logDebug('Fetching absolute link: ' + absoluteLink);
openModal('<p>Loading...</p>');
fetch(absoluteLink, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function(response) {
if (DEBUG) logDebug('Fetch response status: ' + response.status + ' ' + response.statusText);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return response.text();
})
.then(function(data) {
if (DEBUG) logDebug('Fetched HTML length: ' + data.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
let combinedContent = '';
const sections = doc.querySelectorAll('.modal-story-content');
if (DEBUG) logDebug('Found ' + sections.length + ' modal-story-content sections');
if (sections.length > 0) {
sections.forEach(function(section, index) {
combinedContent += section.outerHTML.trim();
if (index < sections.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
const tattooFallback = doc.querySelectorAll('.dynamic-section .inner');
if (DEBUG) logDebug('Found ' + tattooFallback.length + ' Tattoo .dynamic-section .inner sections');
if (tattooFallback.length > 0) {
tattooFallback.forEach(function(section, index) {
const newContent = document.createElement('div');
newContent.className = 'modal-story-content';
newContent.innerHTML = section.innerHTML;
combinedContent += newContent.outerHTML.trim();
if (index < tattooFallback.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
if (DEBUG) logDebug('No content found in Tattoo structure', 'red');
combinedContent = '<p>Content could not be parsed from the page.</p>';
}
}
if (combinedContent) {
openModal(combinedContent);
} else {
openModal('<p>No content found.</p>');
}
})
.catch(function(error) {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
openModal('<p>Sorry, the content could not be loaded: ' + error.message + '</p>');
});
});
});
closeBtn.addEventListener('click', closeModal);
window.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && modal.style.display === 'block') {
closeModal();
}
});
} else {
if (DEBUG) logDebug('Modal elements missing - skipping modal functionality (normal on non-modal pages)', 'orange');
}
function openModal(content) {
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = content;
const modal = document.getElementById('story-modal');
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
const closeBtn = document.querySelector('.modal-close');
if (closeBtn) closeBtn.focus();
if (DEBUG) logDebug('Modal opened');
}
function closeModal() {
const modal = document.getElementById('story-modal');
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = '';
if (DEBUG) logDebug('Modal closed');
}
});
This version should finally give you videos in the modal without the ID mismatch issue.
Let me know the console output after clicking the button — especially any "Rendered" lines and whether the video shows in the modal.
Current time: January 14, 2026 11:45 AM HKT
This should be the clean, final solution! 😊
Debug: Modal trigger clicked: modal-dummy-12 Debug: Modal opened Debug: Modal content not found for ID: modal-dummy-12
Debug: Modal trigger clicked: null Debug: Trigger clicked, link: null Debug: Modal opened Debug: No embed found for local modal
An error occurred. Try watching this video on www.youtube.com, or enable JavaScript if it is disabled in your browser.
The core insight is spot-on: "YouTube" code type already works perfectly (video shows on main page and in modal via fetch + re-render). We can duplicate this behavior for "Modal Video" but hide the main-page frame (visually remove it from the page while keeping the push/render data).
This gives you two clear options:
| Code Type | Name Suggestion | Main Page Behavior | Modal Behavior | Button Link Needed? |
|---|---|---|---|---|
| YouTube | Show On Page | Video visible on page | Video also in modal (duplicate) | Yes (to any page) |
| Modal Video | Show In Modal | Video hidden on page | Video visible in modal only | No (# or empty) |
This way:
In the non-modal branch, make sure the embed div + push script is output for both "YouTube" and "Modal Video" (regardless of model, to be safe):
// ... inside the foreach ($group as $row) loop, after content/text/image ...
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'Modal Video'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
window.contentCode on loadIn renderEmbed, add a hide rule only for "Modal Video":
function renderEmbed(item, index, total) {
const container = document.getElementById(`embed-${item.id}`);
if (!container) {
if (DEBUG) logDebug(`Container not found for embed ID ${item.id}`, 'red');
return;
}
if (item.type === 'YouTube' || item.type === 'Modal Video') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
// Hide the embed on main page for Modal Video type
if (item.type === 'Modal Video') {
container.style.display = 'none !important';
container.style.height = '0 !important';
container.style.overflow = 'hidden !important';
if (DEBUG) logDebug(`Hid main-page embed for Modal Video ID ${item.id}`, 'purple');
}
if (DEBUG) logDebug(`Rendered ${item.type} embed for ID ${item.id}`, 'orange');
} else if (item.type === 'PayPal') {
// ... your existing PayPal code ...
}
}
renderEmbed will show it again (display:block)renderEmbed, add hide logic only for "Modal Video" on main pageThis gives you exactly the two modes you wanted:
To make sure the hidden embed doesn't take space:
.content-embed[style*="display: none"] {
display: none !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
}
Let me know if you want the full updated files (PHP 2.87 + JS 2.87) with these changes integrated — or if you prefer to apply them manually to your current versions.
Current time: January 14, 2026 03:45 PM HKT
This "hide on page" trick should end the loop once and for all! 😊
Your table is precise. But to be even more specific: A button is not expected for on-page video, But if the user wants, she must add a linked page. A button is always expected for modal video and a linked page is optional. Providing a linked page above will show relevant sections just like usual.
These two video types are activated only with the two "video" models.
The only real issue is that the embed div is not being output in the mother section when code_type = "Modal Video" (because the PHP conditions are too restrictive on model). That's why JS can't find it → "No Modal Video embed found in this section."
Your JS is already good — the problem is PHP output (no embed div for Modal Video unless model is one of the allowed ones).
To fix this, we need to always output the embed div + push script for "Modal Video" (regardless of model) in the non-modal branch (main page). This ensures:
window.contentCodeNo change needed in JS — your current V2.87 JS is perfect for this.
I have regenerated the entire file with:
<?php
// pull-contents.php (Version 2.88 - New Code Type Names + All Features Preserved)
// Last updated: January 14, 2026
// Code Types: "Page Video" (shows on page), "Pop-up Video" (shows in modal), "PayPal"
// "Page Video" and "Pop-up Video" activated only with video models
// Button: Not expected for "Page Video" (must have linked page if added); always expected for "Pop-up Video" (linked page optional)
define('DEBUG', true);
define('IMAGE_BASE_PATH', 'https://navi.claudioflute.live/images/');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$config_path = '../config.php';
if (!file_exists($config_path)) {
die("Error: config.php not found at $config_path");
}
require_once $config_path;
if (!isset($dbServer) || !isset($dbUsername) || !isset($dbPassword) || !isset($dbDatabase)) {
die("Error: AppGini database variables not defined in config.php");
}
$conn = new mysqli($dbServer, $dbUsername, $dbPassword, $dbDatabase, $dbPort ?: 3306);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Auto-detect page
if (isset($_GET['page'])) {
$current_page = $_GET['page'];
} else {
$calling_file = basename($_SERVER['SCRIPT_FILENAME']);
$current_page = str_replace('.php', '', $calling_file);
if ($current_page === 'index') {
$current_page = 'landing';
}
}
$status = 'Show';
// Preload PayPal SDK
$paypalSdkUrl = 'https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription';
echo "<script src=\"$paypalSdkUrl\" id=\"paypal-sdk\" async=\"false\"></script>";
// Is modal request
$is_modal_request = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || isset($_GET['modal']);
// Is stories page (for preview logic)
$is_stories_page = ($current_page === 'stories');
// SQL - matches your schema
$sql = "SELECT id, title, content, code, code_type, image, button_link, button_text, position, model, date_edited, theme_img, img_role, chk_divider
FROM web_content
WHERE page = ? AND status = ?
ORDER BY position ASC, date_edited DESC";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $current_page, $status);
$stmt->execute();
$result = $stmt->get_result();
$sections = [];
while ($row = $result->fetch_assoc()) {
$sections[] = $row;
}
// Group by position for multi-column
$sections_by_position = [];
foreach ($sections as $row) {
$position = $row['position'] ?? 1;
$sections_by_position[$position][] = $row;
}
if (!empty($sections_by_position)) {
ksort($sections_by_position);
foreach ($sections_by_position as $position => $group) {
$is_grid = count($group) > 1;
if ($is_modal_request) {
foreach ($group as $index => $row) {
echo "<div class=\"modal-story-content\">";
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
}
echo $row['content'];
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['Page Video', 'Pop-up Video', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
echo "</div>";
if ($index < count($group) - 1) {
echo "<div class=\"modal-section-separator\"></div>";
}
}
} else {
// SMART CLASS for Tattoo
$section_class = 'content';
$has_background = false;
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$has_background = true;
}
}
if ($has_background) {
$section_class = 'hero';
}
if ($is_grid) {
$section_class .= ' grid';
}
$bg_style = '';
if ($section_class === 'hero') {
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$bg_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$bg_style = "style=\"background-image: url('$bg_url');\"";
break;
}
}
}
echo "<section class=\"{$section_class} dynamic-section\" {$bg_style}>";
echo "<div class=\"inner\">";
foreach ($group as $row) {
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">{$row['title']}</h2>";
}
echo $row['content'];
// title-text-image-video model
if ($row['model'] === 'title-text-image-video') {
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && $row['code_type'] === 'Page Video') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'Page Video'});</script>";
}
}
// title-text-video model (video only, no image)
elseif ($row['model'] === 'title-text-video') {
if (!empty($row['code']) && $row['code_type'] === 'Page Video') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'Page Video'});</script>";
}
}
// Existing models (title-text-image-code, title-text-code)
elseif ($row['model'] === 'title-text-image-code' || $row['model'] === 'title-text-code') {
if (!empty($row['image']) && $row['model'] === 'title-text-image-code') {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['Page Video', 'Pop-up Video', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
}
// Default image
elseif (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
// Always output embed div + push for Pop-up Video (regardless of model)
if ($row['code_type'] === 'Pop-up Video') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'Pop-up Video'});</script>";
}
// BUTTON RENDERING
if (!empty($row['button_text'])) {
$button_text = htmlspecialchars($row['button_text']);
$button_link = htmlspecialchars($row['button_link'] ?: '#');
echo "<div class=\"content__buttons\">";
if ($is_stories_page) {
$modal_id = 'modal-content-' . $row['id'];
echo "<div id=\"$modal_id\" style=\"display:none;\">";
echo '<div class="modal-story-content">';
if (!empty($row['title'])) echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image modal-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['content'])) echo $row['content'];
echo '</div>';
echo "</div>";
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-id=\"$modal_id\">$button_text</a>";
} else {
// Always add modal-trigger class
$data_attr = '';
if (!empty($button_link) && $button_link !== '#') {
$data_attr = "data-modal-link=\"$button_link\"";
}
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" $data_attr>$button_text</a>";
}
echo "</div>";
}
}
echo "</div>"; // .inner
// Separator — moved AFTER section for below background
$show_separator = false;
$sep_url = '';
if (!empty($row['theme_img']) && $row['img_role'] === 'Separator') {
$sep_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$show_separator = true;
} elseif (!empty($row['chk_divider']) && $row['chk_divider'] == 1) {
$sep_url = 'https://navi.claudioflute.live/public/Flying_bee_R.gif';
$show_separator = true;
}
if ($show_separator) {
$sep_class = ($row['img_role'] === 'Separator') ? 'separator custom-separator' : 'separator default-bee';
echo "<div class=\"separator-wrapper\">";
echo "<img src=\"$sep_url\" class=\"$sep_class\" alt=\"Separator\">";
echo "</div>";
}
echo "</section>";
}
}
} else {
echo "No sections found.";
}
// Modal container (only for non-AJAX requests)
if (!$is_modal_request) {
echo '<div id="story-modal" class="modal">';
echo '<div class="modal-content">';
echo '<span class="modal-close">×</span>';
echo '<div id="modal-body"></div>';
echo '</div>';
echo '</div>';
}
// Load external JS for modal and embed rendering
echo '<script src="/modal-embed.js?v=' . time() . '"></script>';
$stmt->close();
$conn->close();
?>
This is your current JS regenerated with version bump to 2.88 (no changes needed — it's already correct).
// modal-embed.js - Version 2.88 (Tattoo-optimized, Modal Video type support)
// Last updated: January 14, 2026
console.log("modal-embed.js LOADED SUCCESSFULLY (Tattoo version 2.88)");
document.addEventListener('DOMContentLoaded', function() {
const DEBUG = true; // Change to false when done testing
let debugLog = null;
if (DEBUG) {
debugLog = document.createElement('div');
debugLog.id = 'debug-log';
debugLog.style.position = 'fixed';
debugLog.style.bottom = '10px';
debugLog.style.left = '10px';
debugLog.style.background = 'rgba(0, 0, 0, 0.8)';
debugLog.style.color = 'white';
debugLog.style.padding = '10px';
debugLog.style.zIndex = '1000';
debugLog.style.maxHeight = '200px';
debugLog.style.overflowY = 'auto';
document.body.appendChild(debugLog);
}
function logDebug(message, color = 'green') {
if (DEBUG && debugLog) {
debugLog.insertAdjacentHTML('beforeend', `<div style="color: ${color}; font-weight: bold;">Debug: ${message}</div>`);
debugLog.scrollTop = debugLog.scrollHeight;
}
if (DEBUG && console) console.log(`Debug: ${message}`);
}
if (DEBUG) logDebug('Modal script loaded (Tattoo version 2.88)');
const modal = document.getElementById('story-modal');
const modalBody = document.getElementById('modal-body');
const closeBtn = document.querySelector('.modal-close');
const triggers = document.querySelectorAll('.modal-trigger');
if (DEBUG) logDebug('Found ' + triggers.length + ' modal triggers');
// Embed rendering function
function renderEmbed(item, index, total) {
const container = document.getElementById(`embed-${item.id}`);
if (!container) {
if (DEBUG) logDebug(`Container not found for embed ID ${item.id}`, 'red');
return;
}
if (item.type === 'Page Video') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered Page Video embed for ID ${item.id} (sandbox removed)`, 'orange');
} else if (item.type === 'Pop-up Video') {
container.innerHTML = item.code;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered Pop-up Video embed for ID ${item.id}`, 'orange');
} else if (item.type === 'PayPal') {
const divMatch = item.code.match(/<div\s+[^>]*id="[^"]+"[^>]*>/i);
const buttonMatch = item.code.match(/<script>\s*paypal\.Buttons\([\s\S]*?\)\.render\(['"][^'"]+['"]\);\s*<\/script>/i);
if (divMatch && buttonMatch) {
const uniqueId = `paypal-button-container-${item.id}`;
let divContent = divMatch[0].replace(/id="[^"]+"/, `id="${uniqueId}"`);
container.innerHTML = divContent;
container.querySelector(`#${uniqueId}`).style.cssText = 'width: 100% !important; min-width: 300px !important; max-width: 300px !important; margin: 0 auto !important;';
function waitForSDK(callback) {
if (typeof paypal !== 'undefined') {
callback();
} else {
setTimeout(function() { waitForSDK(callback); }, 100);
}
}
waitForSDK(function() {
let scriptContent = buttonMatch[0].replace(/^<script>|<\/script>$/g, '').trim();
scriptContent = scriptContent.replace(/\.render\(['"][^'"]+['"]\)/, `.render('#${uniqueId}')`);
const uniqueScript = `(function() { ${scriptContent} })();`;
const buttonScript = document.createElement('script');
buttonScript.textContent = uniqueScript;
document.body.appendChild(buttonScript);
});
}
}
}
// Run embed rendering on page load
if (window.contentCode && Array.isArray(window.contentCode)) {
window.contentCode.forEach(function(item, index) {
renderEmbed(item, index, window.contentCode.length);
});
} else if (DEBUG) {
logDebug('contentCode is not an array or unavailable', 'red');
}
// PHP debug info (safe try-catch)
try {
if (window.debugFromPHP) {
console.log('PHP Debug Info:', window.debugFromPHP);
if (DEBUG) logDebug('PHP → Fetched sections: ' + window.debugFromPHP.fetched + ', Grouped positions: ' + window.debugFromPHP.grouped + ', Modal request: ' + window.debugFromPHP.modalRequest, 'purple');
} else {
console.warn('No debug info from PHP');
}
} catch (e) {
console.warn('Error reading PHP debug info:', e);
}
// Modal logic only if elements exist
if (modal && modalBody && closeBtn && triggers.length > 0) {
if (DEBUG) logDebug('Modal elements found - enabling modal functionality');
triggers.forEach(function(trigger) {
trigger.addEventListener('click', function(e) {
e.preventDefault();
if (DEBUG) logDebug(`Modal trigger clicked: ${this.getAttribute('data-modal-id') || this.getAttribute('data-modal-link')}`, 'green');
const modalId = this.getAttribute('data-modal-id');
if (modalId) {
const contentDiv = document.getElementById(modalId);
if (contentDiv) {
const content = contentDiv.innerHTML;
if (DEBUG) logDebug('Stories modal triggered, content from ID: ' + modalId);
openModal(content);
} else {
openModal('<p>Content not found.</p>');
if (DEBUG) logDebug('Modal content not found for ID: ' + modalId, 'red');
}
return;
}
const link = this.getAttribute('data-modal-link');
if (DEBUG) logDebug('Trigger clicked, link: ' + link);
// Allow empty or # link to open ONLY the Modal Video embed from this section
if (!link || link === '#') {
// Find the closest parent section containing this button
const sectionParent = this.closest('.dynamic-section') ||
this.closest('.inner') ||
this.closest('.grid-item') ||
this.closest('.content');
if (sectionParent) {
// Look for the embed div inside this parent section
const videoEmbed = sectionParent.querySelector('.content-embed[id^="embed-"]');
if (videoEmbed) {
const embedClone = videoEmbed.cloneNode(true);
openModal(embedClone.outerHTML);
if (DEBUG) logDebug('Opened local Pop-up Video embed from this section only', 'green');
} else {
openModal('<p>No Pop-up Video embed found in this section.</p>');
if (DEBUG) logDebug('No embed found in parent section for local modal (checked: ' + sectionParent.className + ')', 'red');
}
} else {
openModal('<p>Section context not found.</p>');
if (DEBUG) logDebug('No parent section found for local modal', 'red');
}
return;
}
// Prevent recursive loading for same-page links
if (link.includes('index.php') || link === window.location.pathname) {
openModal('<p>This link cannot be loaded in a modal to prevent recursive loading.</p>');
return;
}
// Normal fetch for other links
let absoluteLink;
try {
absoluteLink = new URL(link, window.location.origin).href;
} catch (error) {
openModal('<p>Invalid URL.</p>');
return;
}
absoluteLink += (absoluteLink.includes('?') ? '&' : '?') + 'modal=1';
if (DEBUG) logDebug('Fetching absolute link: ' + absoluteLink);
openModal('<p>Loading...</p>');
fetch(absoluteLink, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function(response) {
if (DEBUG) logDebug('Fetch response status: ' + response.status + ' ' + response.statusText);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return response.text();
})
.then(function(data) {
if (DEBUG) logDebug('Fetched HTML length: ' + data.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
let combinedContent = '';
const sections = doc.querySelectorAll('.modal-story-content');
if (DEBUG) logDebug('Found ' + sections.length + ' modal-story-content sections');
if (sections.length > 0) {
sections.forEach(function(section, index) {
combinedContent += section.outerHTML.trim();
if (index < sections.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
const tattooFallback = doc.querySelectorAll('.dynamic-section .inner');
if (DEBUG) logDebug('Found ' + tattooFallback.length + ' Tattoo .dynamic-section .inner sections');
if (tattooFallback.length > 0) {
tattooFallback.forEach(function(section, index) {
const newContent = document.createElement('div');
newContent.className = 'modal-story-content';
newContent.innerHTML = section.innerHTML;
combinedContent += newContent.outerHTML.trim();
if (index < tattooFallback.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
if (DEBUG) logDebug('No content found in Tattoo structure', 'red');
combinedContent = '<p>Content could not be parsed from the page.</p>';
}
}
if (combinedContent) {
openModal(combinedContent);
} else {
openModal('<p>No content found.</p>');
}
})
.catch(function(error) {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
openModal('<p>Sorry, the content could not be loaded: ' + error.message + '</p>');
});
});
});
closeBtn.addEventListener('click', closeModal);
window.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && modal.style.display === 'block') {
closeModal();
}
});
} else {
if (DEBUG) logDebug('Modal elements missing - skipping modal functionality (normal on non-modal pages)', 'orange');
}
function openModal(content) {
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = content;
// Re-run embed rendering after modal content is inserted
setTimeout(function() {
const modalEmbeds = modalBody.querySelectorAll('.content-embed[id^="embed-"]');
if (modalEmbeds.length > 0) {
if (DEBUG) logDebug('Found ' + modalEmbeds.length + ' embeds inside modal – re-rendering', 'orange');
modalEmbeds.forEach(function(embedDiv) {
const id = embedDiv.id.replace('embed-', '');
const item = window.contentCode?.find(function(i) {
return i.id == id;
});
if (item) {
renderEmbed(item, 0, 1);
if (DEBUG) logDebug('Re-rendered embed-' + id + ' inside modal', 'green');
} else {
if (DEBUG) logDebug('No matching contentCode for embed-' + id + ' in modal', 'red');
}
});
} else {
if (DEBUG) logDebug('No embeds found inside modal', 'orange');
}
}, 500); // 500ms delay
const modal = document.getElementById('story-modal');
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
const closeBtn = document.querySelector('.modal-close');
if (closeBtn) closeBtn.focus();
if (DEBUG) logDebug('Modal opened');
}
function closeModal() {
const modal = document.getElementById('story-modal');
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = '';
if (DEBUG) logDebug('Modal closed');
}
});
PHP (2.88):
JS (2.88):
This is the clean, full-file pair with new names and correct logic.
Let me know the console output after testing — especially any "Rendered Pop-up Video" lines and whether the video appears in the modal.
Current time: January 14, 2026 03:45 PM HKT
This should be the final, stable pair! 😊
"An error occurred. Try watching this video on www.youtube.com, or enable JavaScript if it is disabled in your browser."
Debug msg. is:
Debug: Modal opened Debug: Opened local Pop-up Video embed from this section only Debug: Found 1 embeds inside modal – re-rendering Debug: Rendered Pop-up Video embed for ID 12 Debug: Re-rendered embed-12 inside modal
Debug: Modal opened Debug: No embed found in parent section for local modal (checked: content dynamic-section) Debug: No embeds found inside modal
The fact that before the code_type name change ("YouTube" → "Pop-up Video"), the video frame was successfully shown both on the page and inside the modal, but after the change we are back to a darkened/black frame with the YouTube error message, points to one very specific cause:
The renderEmbed function is no longer recognizing the new code_type "Pop-up Video" as something to process.
In the original "YouTube" type, the rendering logic was triggered → iframe loaded → video played (on page and modal).
After changing the type to "Pop-up Video", the rendering skips because there is no matching case for "Pop-up Video" in renderEmbed — so the <div class="content-embed"> stays empty → YouTube iframe never gets inserted → black/error frame.
Update renderEmbed in your current modal-embed.js to explicitly handle "Pop-up Video" (same as YouTube, but with optional hiding on main page):
Replace the entire if chain inside renderEmbed with this (keep the rest of the function):
if (item.type === 'Page Video' || item.type === 'YouTube') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered ${item.type} embed for ID ${item.id} (sandbox removed)`, 'orange');
} else if (item.type === 'Pop-up Video') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
// Hide on main page (no visible frame)
container.style.display = 'none !important';
container.style.height = '0 !important';
container.style.overflow = 'hidden !important';
container.style.margin = '0 !important';
container.style.padding = '0 !important';
if (DEBUG) logDebug(`Rendered Pop-up Video embed for ID ${item.id} (hidden on main page)`, 'orange');
} else if (item.type === 'PayPal') {
// Your existing PayPal logic...
}
Add this after container.innerHTML = cleanCode; in the Pop-up Video case:
const iframe = container.querySelector('iframe');
if (iframe) {
const originalSrc = iframe.src;
iframe.src = '';
setTimeout(() => {
iframe.src = originalSrc + (originalSrc.includes('?') ? '&' : '?') + 'enablejsapi=1&origin=' + encodeURIComponent(window.location.origin);
if (DEBUG) logDebug(`Forced reload for Pop-up Video ID ${item.id}`, 'purple');
}, 100);
}
Add to Custom CSS (bottom):
.content-embed:has(iframe) {
display: none !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
}
.modal-body .content-embed {
display: block !important;
height: 70vh !important;
}
renderEmbed with the new "Pop-up Video" case (including reload)This uses the proven YouTube rendering path (which worked before) but hides the main page frame for Pop-up Video.
If the black frame persists:
src → open in new tab → does it play??mute=1 in admin iframe code (test)Let me know the console and if the video plays in modal now (and is hidden on page).
Current time: January 14, 2026 04:15 PM HKT
This "hide + proven render" approach should give you exactly what you want! 😊
This is the safest path forward: it restores your site to a known-good state without white/black frames, "no embed found" errors, or syntax issues.
You can then decide later if you want to refine the "hide on page" logic or keep the duplicate as-is (many sites do show video in both places, especially for emphasis).
Use the exact file you had before the Modal Video experiments (the one you posted earlier):
<?php
// pull-contents.php (Version 2.83 - Stable rollback)
// Last updated: January 13, 2026
// Features: title-text-video model, bee below background with classes, working main-page YT, modal opens with content
define('DEBUG', true);
define('IMAGE_BASE_PATH', 'https://navi.claudioflute.live/images/');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$config_path = '../config.php';
if (!file_exists($config_path)) {
die("Error: config.php not found at $config_path");
}
require_once $config_path;
if (!isset($dbServer) || !isset($dbUsername) || !isset($dbPassword) || !isset($dbDatabase)) {
die("Error: AppGini database variables not defined in config.php");
}
$conn = new mysqli($dbServer, $dbUsername, $dbPassword, $dbDatabase, $dbPort ?: 3306);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Auto-detect page
if (isset($_GET['page'])) {
$current_page = $_GET['page'];
} else {
$calling_file = basename($_SERVER['SCRIPT_FILENAME']);
$current_page = str_replace('.php', '', $calling_file);
if ($current_page === 'index') {
$current_page = 'landing';
}
}
$status = 'Show';
// Preload PayPal SDK
$paypalSdkUrl = 'https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription';
echo "<script src=\"$paypalSdkUrl\" id=\"paypal-sdk\" async=\"false\"></script>";
// Is modal request
$is_modal_request = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || isset($_GET['modal']);
// Is stories page (for preview logic)
$is_stories_page = ($current_page === 'stories');
// SQL - matches your schema
$sql = "SELECT id, title, content, code, code_type, image, button_link, button_text, position, model, date_edited, theme_img, img_role, chk_divider
FROM web_content
WHERE page = ? AND status = ?
ORDER BY position ASC, date_edited DESC";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $current_page, $status);
$stmt->execute();
$result = $stmt->get_result();
$sections = [];
while ($row = $result->fetch_assoc()) {
$sections[] = $row;
}
// Group by position for multi-column
$sections_by_position = [];
foreach ($sections as $row) {
$position = $row['position'] ?? 1;
$sections_by_position[$position][] = $row;
}
if (!empty($sections_by_position)) {
ksort($sections_by_position);
foreach ($sections_by_position as $position => $group) {
$is_grid = count($group) > 1;
if ($is_modal_request) {
foreach ($group as $index => $row) {
echo "<div class=\"modal-story-content\">";
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
}
echo $row['content'];
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
echo "</div>";
if ($index < count($group) - 1) {
echo "<div class=\"modal-section-separator\"></div>";
}
}
} else {
// SMART CLASS for Tattoo
$section_class = 'content';
$has_background = false;
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$has_background = true;
}
}
if ($has_background) {
$section_class = 'hero';
}
if ($is_grid) {
$section_class .= ' grid';
}
$bg_style = '';
if ($section_class === 'hero') {
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$bg_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$bg_style = "style=\"background-image: url('$bg_url');\"";
break;
}
}
}
echo "<section class=\"{$section_class} dynamic-section\" {$bg_style}>";
echo "<div class=\"inner\">";
foreach ($group as $row) {
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">{$row['title']}</h2>";
}
echo $row['content'];
if ($row['model'] === 'title-text-image-video') {
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
elseif ($row['model'] === 'title-text-video') {
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
elseif ($row['model'] === 'title-text-image-code' || $row['model'] === 'title-text-code') {
if (!empty($row['image']) && $row['model'] === 'title-text-image-code') {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
}
elseif (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
// BUTTON RENDERING
if (!empty($row['button_text'])) {
$button_text = htmlspecialchars($row['button_text']);
$button_link = htmlspecialchars($row['button_link'] ?: '#');
echo "<div class=\"content__buttons\">";
if ($is_stories_page) {
$modal_id = 'modal-content-' . $row['id'];
echo "<div id=\"$modal_id\" style=\"display:none;\">";
echo '<div class="modal-story-content">';
if (!empty($row['title'])) echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image modal-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['content'])) echo $row['content'];
echo '</div>';
echo "</div>";
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-id=\"$modal_id\">$button_text</a>";
} else {
if (!empty($button_link) && $button_link !== '#') {
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-link=\"$button_link\">$button_text</a>";
} else {
echo "<a class=\"btn\" href=\"javascript:void(0)\">$button_text</a>";
}
}
echo "</div>";
}
}
echo "</div>"; // .inner
// Separator — moved AFTER section for below background
$show_separator = false;
$sep_url = '';
if (!empty($row['theme_img']) && $row['img_role'] === 'Separator') {
$sep_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$show_separator = true;
} elseif (!empty($row['chk_divider']) && $row['chk_divider'] == 1) {
$sep_url = 'https://navi.claudioflute.live/public/Flying_bee_R.gif';
$show_separator = true;
}
if ($show_separator) {
$sep_class = ($row['img_role'] === 'Separator') ? 'separator custom-separator' : 'separator default-bee';
echo "<div class=\"separator-wrapper\">";
echo "<img src=\"$sep_url\" class=\"$sep_class\" alt=\"Separator\">";
echo "</div>";
}
echo "</section>";
}
}
} else {
echo "No sections found.";
}
// Modal container (only for non-AJAX requests)
if (!$is_modal_request) {
echo '<div id="story-modal" class="modal">';
echo '<div class="modal-content">';
echo '<span class="modal-close">×</span>';
echo '<div id="modal-body"></div>';
echo '</div>';
echo '</div>';
}
// Load external JS for modal and embed rendering
echo '<script src="/modal-embed.js?v=' . time() . '"></script>';
$stmt->close();
$conn->close();
?>
Use the exact file you posted in your last message (the one with the local modal block you pasted). It is already the correct V2.86 version that works with the rollback PHP.
pull-contents.php with the V2.83 code abovemodal-embed.js (the one you pasted — V2.86 or 2.87)This restores the known-good state where video works in both places.
Let me know how it looks after rollback — especially if the video is back in both places and modal opens correctly.
Current time: January 14, 2026 04:15 PM HKT
We're back to stable ground — good call on rollback! 😊
pull-contents.php handles modal display in exactly the same way as before — it is the stable state you had when everything worked reliably (main-page YouTube embeds, modal opens with content, but no video inside modal for the special case).Here’s a clear breakdown of how modal display works in this V2.83 version:
The file checks $is_modal_request (set to true if the request is AJAX or has ?modal=1 in URL):
$is_modal_request = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || isset($_GET['modal']);
When a modal button is clicked (e.g. data-modal-link = "some-page.php"), JS fetches some-page.php?modal=1 → PHP detects modal mode.
if ($is_modal_request) { ... })In modal mode, PHP outputs only the inner content for each section in the fetched page:
<h2 class="section-title">...echo $row['content'];<div class="content-image"><img ...><div class="content-embed" id="embed-{$row['id']}"></div> + push script (only for YouTube/PayPal)<div class="modal-story-content">...</div><div class="modal-section-separator"></div>Important: In V2.83, the embed div + push script is only output for code_type = 'YouTube' or 'PayPal' — not for any other type (including the temporary "Modal Video").
Your JS (V2.86 or earlier) does:
modalBody.innerHTML = combinedContent → inserts all modal-story-content divssetTimeout re-renders any .content-embed[id^="embed-"] found inside the modalwindow.contentCode (from main page load)renderEmbed → injects iframe/video/player| Content Type | Shown in Modal? | How it Works |
|---|---|---|
| Title | Yes | <h2 class="section-title"> |
Text (content) |
Yes | Direct echo |
| Image | Yes | <div class="content-image"> |
| YouTube embed | Yes | Placeholder div + push script → re-render injects iframe |
| PayPal button | Yes | Placeholder div + push script → re-render injects button |
| Modal Video (old) | No | No push/output in modal branch → no embed div → "no matching contentCode" or nothing |
| Other sections | Yes | Text/image only |
window.contentCode on main loadThis rollback does not break any existing modals — they continue to show title + text + image + embeds (YouTube/PayPal) exactly as before.
.content-embed {
display: none !important; /* hide all embeds on main page */
}
.modal-body .content-embed {
display: block !important; /* show in modal */
}
(You can make it more specific later if needed.)
This gives you the working state (video in both places) while keeping the door open for future refinements (e.g. a true "modal-only" type).
Let me know how it looks after rollback — especially if videos are back in both places and modals open normally.
Current time: January 14, 2026 04:45 PM HKT
We're back to reliable ground — good decision! 😊
pull-contents.php handles modal display in exactly the same way as before — it is the stable state you had when everything worked reliably (main-page YouTube embeds, modal opens with content, but no video inside modal for the special case).Here’s a clear breakdown of how modal display works in this V2.83 version:
The file checks $is_modal_request (set to true if the request is AJAX or has ?modal=1 in URL):
$is_modal_request = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || isset($_GET['modal']);
When a modal button is clicked (e.g. data-modal-link = "some-page.php"), JS fetches some-page.php?modal=1 → PHP detects modal mode.
if ($is_modal_request) { ... })In modal mode, PHP outputs only the inner content for each section in the fetched page:
<h2 class="section-title">...echo $row['content'];<div class="content-image"><img ...><div class="content-embed" id="embed-{$row['id']}"></div> + push script (only for YouTube/PayPal)<div class="modal-story-content">...</div><div class="modal-section-separator"></div>Important: In V2.83, the embed div + push script is only output for code_type = 'YouTube' or 'PayPal' — not for any other type (including the temporary "Modal Video").
Your JS (V2.86 or earlier) does:
modalBody.innerHTML = combinedContent → inserts all modal-story-content divssetTimeout re-renders any .content-embed[id^="embed-"] found inside the modalwindow.contentCode (from main page load)renderEmbed → injects iframe/video/player| Content Type | Shown in Modal? | How it Works |
|---|---|---|
| Title | Yes | <h2 class="section-title"> |
Text (content) |
Yes | Direct echo |
| Image | Yes | <div class="content-image"> |
| YouTube embed | Yes | Placeholder div + push script → re-render injects iframe |
| PayPal button | Yes | Placeholder div + push script → re-render injects button |
| Modal Video (old) | No | No push/output in modal branch → no embed div → "no matching contentCode" or nothing |
| Other sections | Yes | Text/image only |
window.contentCode on main loadThis rollback does not break any existing modals — they continue to show title + text + image + embeds (YouTube/PayPal) exactly as before.
.content-embed {
display: none !important; /* hide all embeds on main page */
}
.modal-body .content-embed {
display: block !important; /* show in modal */
}
(You can make it more specific later if needed.)
This gives you the working state (video in both places) while keeping the door open for future refinements (e.g. a true "modal-only" type).
Let me know how it looks after rollback — especially if videos are back in both places and modals open normally.
Current time: January 14, 2026 04:45 PM HKT
We're back to reliable ground — good decision! 😊
Your current V2.83 PHP + JS pair is now stable and functional again — main-page embeds (YouTube/PayPal) work, modals open with content from linked pages, no white/black frames, no "no embed found" errors, no syntax issues. That's a solid baseline to build from.
Restored state (V2.83):
What you want next:
A clean way to have two distinct behaviors:
| Path | Description | Pros | Cons | Recommendation |
|---|---|---|---|---|
| A: Double-display with new code_type | Keep the current embed logic (push + render + re-render in modal). Add new code_type "Pop-up Video" (or "Modal Only"). Hide the embed div on main page via CSS or JS (display:none; height:0). |
- Uses proven working code (no new bugs) - No extra files/pages - Modal always looks consistent with dynamic content - Easy to implement |
- Slight code duplication (video loads twice) - Needs CSS/JS hide rule |
My preferred path — it's the least disruptive and keeps everything inside your existing system. |
| B: Static-embed.php approach | Button links to static-embed.php?v=ID1,ID2,...JS loads it as iframe in modal. Static page has its own styles (self-contained). |
- Complete isolation (no main-page interference) - No duplicate loading - Full control over modal look |
- Extra file to maintain - Iframe limitations (no inheritance of Tattoo CSS) - Scrollbar inside iframe (harder to fix) |
Good alternative if you prefer isolation, but more work and less "native" feel. |
Go with Path A (double-display + new code_type + hide on page).
Reasons:
This keeps the system simple, robust, and maintainable — exactly what you want.
If yes — I'll generate the full V2.89 PHP + JS pair with these changes (new type, hide on page, button link handling).
If you prefer Path B (static-embed.php) — just say so and I'll refine that instead.
Are we proceeding with Path A (double-display + hide)?
Current time: January 14, 2026 05:45 PM HKT
Let me know your preference — we're ready to finalize! 😊
# or empty links.The button HTML output (from PHP V2.83):
#, PHP renders the button as:
<a class="btn" href="javascript:void(0)">Button Text</a>
modal-trigger class is added (because the else branch only adds it when link is not #)The JS logic in V2.83:
.modal-trigger class.href="javascript:void(0)" (which does nothing, hence the status bar message).if (!link || link === '#' || link.includes('index.php')) {
openModal('<p>This link cannot be loaded in a modal to prevent recursive loading.</p>');
return;
}
Why this didn't happen in your previous working state:
modal-trigger class, and the JS allowed # links to open the local content.<a class="btn"> for # links) — that's why it broke the modal button.Since you agreed to Option A (double-display + new code_type + hide on page), I've prepared the full V2.89 pair with:
modal-trigger class (even for # / empty)This is the simplest, most reliable path — no iframe, no static page, consistent modals.
<?php
// pull-contents.php (Version 2.89 - Option A: Double-Display with Hide on Page)
// Last updated: January 14, 2026
// Code Types: "Page Video" (shows on page, double in modal), "Pop-up Video" (hidden on page, shown in modal), "PayPal"
define('DEBUG', true);
define('IMAGE_BASE_PATH', 'https://navi.claudioflute.live/images/');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$config_path = '../config.php';
if (!file_exists($config_path)) {
die("Error: config.php not found at $config_path");
}
require_once $config_path;
if (!isset($dbServer) || !isset($dbUsername) || !isset($dbPassword) || !isset($dbDatabase)) {
die("Error: AppGini database variables not defined in config.php");
}
$conn = new mysqli($dbServer, $dbUsername, $dbPassword, $dbDatabase, $dbPort ?: 3306);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Auto-detect page
if (isset($_GET['page'])) {
$current_page = $_GET['page'];
} else {
$calling_file = basename($_SERVER['SCRIPT_FILENAME']);
$current_page = str_replace('.php', '', $calling_file);
if ($current_page === 'index') {
$current_page = 'landing';
}
}
$status = 'Show';
// Preload PayPal SDK
$paypalSdkUrl = 'https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription';
echo "<script src=\"$paypalSdkUrl\" id=\"paypal-sdk\" async=\"false\"></script>";
// Is modal request
$is_modal_request = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || isset($_GET['modal']);
// Is stories page (for preview logic)
$is_stories_page = ($current_page === 'stories');
// SQL - matches your schema
$sql = "SELECT id, title, content, code, code_type, image, button_link, button_text, position, model, date_edited, theme_img, img_role, chk_divider
FROM web_content
WHERE page = ? AND status = ?
ORDER BY position ASC, date_edited DESC";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $current_page, $status);
$stmt->execute();
$result = $stmt->get_result();
$sections = [];
while ($row = $result->fetch_assoc()) {
$sections[] = $row;
}
// Group by position for multi-column
$sections_by_position = [];
foreach ($sections as $row) {
$position = $row['position'] ?? 1;
$sections_by_position[$position][] = $row;
}
if (!empty($sections_by_position)) {
ksort($sections_by_position);
foreach ($sections_by_position as $position => $group) {
$is_grid = count($group) > 1;
if ($is_modal_request) {
foreach ($group as $index => $row) {
echo "<div class=\"modal-story-content\">";
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
}
echo $row['content'];
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['Page Video', 'Pop-up Video', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
echo "</div>";
if ($index < count($group) - 1) {
echo "<div class=\"modal-section-separator\"></div>";
}
}
} else {
// SMART CLASS for Tattoo
$section_class = 'content';
$has_background = false;
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$has_background = true;
}
}
if ($has_background) {
$section_class = 'hero';
}
if ($is_grid) {
$section_class .= ' grid';
}
$bg_style = '';
if ($section_class === 'hero') {
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$bg_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$bg_style = "style=\"background-image: url('$bg_url');\"";
break;
}
}
}
echo "<section class=\"{$section_class} dynamic-section\" {$bg_style}>";
echo "<div class=\"inner\">";
foreach ($group as $row) {
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">{$row['title']}</h2>";
}
echo $row['content'];
// title-text-image-video model
if ($row['model'] === 'title-text-image-video') {
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && $row['code_type'] === 'Page Video') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'Page Video'});</script>";
}
}
// title-text-video model (video only, no image)
elseif ($row['model'] === 'title-text-video') {
if (!empty($row['code']) && $row['code_type'] === 'Page Video') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'Page Video'});</script>";
}
}
// Existing models (title-text-image-code, title-text-code)
elseif ($row['model'] === 'title-text-image-code' || $row['model'] === 'title-text-code') {
if (!empty($row['image']) && $row['model'] === 'title-text-image-code') {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['Page Video', 'Pop-up Video', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
}
// Default image
elseif (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
// BUTTON RENDERING
if (!empty($row['button_text'])) {
$button_text = htmlspecialchars($row['button_text']);
$button_link = htmlspecialchars($row['button_link'] ?: '#');
echo "<div class=\"content__buttons\">";
if ($is_stories_page) {
$modal_id = 'modal-content-' . $row['id'];
echo "<div id=\"$modal_id\" style=\"display:none;\">";
echo '<div class="modal-story-content">';
if (!empty($row['title'])) echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image modal-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['content'])) echo $row['content'];
echo '</div>';
echo "</div>";
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-id=\"$modal_id\">$button_text</a>";
} else {
// Always add modal-trigger class
$data_attr = '';
if (!empty($button_link) && $button_link !== '#') {
$data_attr = "data-modal-link=\"$button_link\"";
}
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" $data_attr>$button_text</a>";
}
echo "</div>";
}
}
echo "</div>"; // .inner
// Separator — moved AFTER section for below background
$show_separator = false;
$sep_url = '';
if (!empty($row['theme_img']) && $row['img_role'] === 'Separator') {
$sep_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$show_separator = true;
} elseif (!empty($row['chk_divider']) && $row['chk_divider'] == 1) {
$sep_url = 'https://navi.claudioflute.live/public/Flying_bee_R.gif';
$show_separator = true;
}
if ($show_separator) {
$sep_class = ($row['img_role'] === 'Separator') ? 'separator custom-separator' : 'separator default-bee';
echo "<div class=\"separator-wrapper\">";
echo "<img src=\"$sep_url\" class=\"$sep_class\" alt=\"Separator\">";
echo "</div>";
}
echo "</section>";
}
}
} else {
echo "No sections found.";
}
// Modal container (only for non-AJAX requests)
if (!$is_modal_request) {
echo '<div id="story-modal" class="modal">';
echo '<div class="modal-content">';
echo '<span class="modal-close">×</span>';
echo '<div id="modal-body"></div>';
echo '</div>';
echo '</div>';
}
// Load external JS for modal and embed rendering
echo '<script src="/modal-embed.js?v=' . time() . '"></script>';
$stmt->close();
$conn->close();
?>
// modal-embed.js - Version 2.88 (Tattoo-optimized, new code type names)
// Last updated: January 14, 2026
console.log("modal-embed.js LOADED SUCCESSFULLY (Tattoo version 2.88)");
document.addEventListener('DOMContentLoaded', function() {
const DEBUG = true; // Change to false when done testing
let debugLog = null;
if (DEBUG) {
debugLog = document.createElement('div');
debugLog.id = 'debug-log';
debugLog.style.position = 'fixed';
debugLog.style.bottom = '10px';
debugLog.style.left = '10px';
debugLog.style.background = 'rgba(0, 0, 0, 0.8)';
debugLog.style.color = 'white';
debugLog.style.padding = '10px';
debugLog.style.zIndex = '1000';
debugLog.style.maxHeight = '200px';
debugLog.style.overflowY = 'auto';
document.body.appendChild(debugLog);
}
function logDebug(message, color = 'green') {
if (DEBUG && debugLog) {
debugLog.insertAdjacentHTML('beforeend', `<div style="color: ${color}; font-weight: bold;">Debug: ${message}</div>`);
debugLog.scrollTop = debugLog.scrollHeight;
}
if (DEBUG && console) console.log(`Debug: ${message}`);
}
if (DEBUG) logDebug('Modal script loaded (Tattoo version 2.88)');
const modal = document.getElementById('story-modal');
const modalBody = document.getElementById('modal-body');
const closeBtn = document.querySelector('.modal-close');
const triggers = document.querySelectorAll('.modal-trigger');
if (DEBUG) logDebug('Found ' + triggers.length + ' modal triggers');
// Embed rendering function
function renderEmbed(item, index, total) {
const container = document.getElementById(`embed-${item.id}`);
if (!container) {
if (DEBUG) logDebug(`Container not found for embed ID ${item.id}`, 'red');
return;
}
if (item.type === 'Page Video') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered Page Video embed for ID ${item.id} (sandbox removed)`, 'orange');
} else if (item.type === 'Pop-up Video') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'none !important'; // Hide on main page
container.style.height = '0 !important';
container.style.overflow = 'hidden !important';
container.style.margin = '0 !important';
container.style.padding = '0 !important';
if (DEBUG) logDebug(`Rendered Pop-up Video embed for ID ${item.id} (hidden on main page)`, 'orange');
} else if (item.type === 'PayPal') {
const divMatch = item.code.match(/<div\s+[^>]*id="[^"]+"[^>]*>/i);
const buttonMatch = item.code.match(/<script>\s*paypal\.Buttons\([\s\S]*?\)\.render\(['"][^'"]+['"]\);\s*<\/script>/i);
if (divMatch && buttonMatch) {
const uniqueId = `paypal-button-container-${item.id}`;
let divContent = divMatch[0].replace(/id="[^"]+"/, `id="${uniqueId}"`);
container.innerHTML = divContent;
container.querySelector(`#${uniqueId}`).style.cssText = 'width: 100% !important; min-width: 300px !important; max-width: 300px !important; margin: 0 auto !important;';
function waitForSDK(callback) {
if (typeof paypal !== 'undefined') {
callback();
} else {
setTimeout(function() { waitForSDK(callback); }, 100);
}
}
waitForSDK(function() {
let scriptContent = buttonMatch[0].replace(/^<script>|<\/script>$/g, '').trim();
scriptContent = scriptContent.replace(/\.render\(['"][^'"]+['"]\)/, `.render('#${uniqueId}')`);
const uniqueScript = `(function() { ${scriptContent} })();`;
const buttonScript = document.createElement('script');
buttonScript.textContent = uniqueScript;
document.body.appendChild(buttonScript);
});
}
}
}
// Run embed rendering on page load
if (window.contentCode && Array.isArray(window.contentCode)) {
window.contentCode.forEach(function(item, index) {
renderEmbed(item, index, window.contentCode.length);
});
} else if (DEBUG) {
logDebug('contentCode is not an array or unavailable', 'red');
}
// PHP debug info (safe try-catch)
try {
if (window.debugFromPHP) {
console.log('PHP Debug Info:', window.debugFromPHP);
if (DEBUG) logDebug('PHP → Fetched sections: ' + window.debugFromPHP.fetched + ', Grouped positions: ' + window.debugFromPHP.grouped + ', Modal request: ' + window.debugFromPHP.modalRequest, 'purple');
} else {
console.warn('No debug info from PHP');
}
} catch (e) {
console.warn('Error reading PHP debug info:', e);
}
// Modal logic only if elements exist
if (modal && modalBody && closeBtn && triggers.length > 0) {
if (DEBUG) logDebug('Modal elements found - enabling modal functionality');
triggers.forEach(function(trigger) {
trigger.addEventListener('click', function(e) {
e.preventDefault();
if (DEBUG) logDebug(`Modal trigger clicked: ${this.getAttribute('data-modal-id') || this.getAttribute('data-modal-link')}`, 'green');
const modalId = this.getAttribute('data-modal-id');
if (modalId) {
const contentDiv = document.getElementById(modalId);
if (contentDiv) {
const content = contentDiv.innerHTML;
if (DEBUG) logDebug('Stories modal triggered, content from ID: ' + modalId);
openModal(content);
} else {
openModal('<p>Content not found.</p>');
if (DEBUG) logDebug('Modal content not found for ID: ' + modalId, 'red');
}
return;
}
const link = this.getAttribute('data-modal-link');
if (DEBUG) logDebug('Trigger clicked, link: ' + link);
// Allow empty or # link to open ONLY the Pop-up Video embed from this section
if (!link || link === '#') {
// Find the closest parent section containing this button
const sectionParent = this.closest('.dynamic-section') ||
this.closest('.inner') ||
this.closest('.grid-item') ||
this.closest('.content');
if (sectionParent) {
// Look for the embed div inside this parent section
const videoEmbed = sectionParent.querySelector('.content-embed[id^="embed-"]');
if (videoEmbed) {
const embedClone = videoEmbed.cloneNode(true);
openModal(embedClone.outerHTML);
if (DEBUG) logDebug('Opened local Pop-up Video embed from this section only', 'green');
} else {
openModal('<p>No Pop-up Video embed found in this section.</p>');
if (DEBUG) logDebug('No embed found in parent section for local modal (checked: ' + sectionParent.className + ')', 'red');
}
} else {
openModal('<p>Section context not found.</p>');
if (DEBUG) logDebug('No parent section found for local modal', 'red');
}
return;
}
// Prevent recursive loading for same-page links
if (link.includes('index.php') || link === window.location.pathname) {
openModal('<p>This link cannot be loaded in a modal to prevent recursive loading.</p>');
return;
}
// Normal fetch for other links
let absoluteLink;
try {
absoluteLink = new URL(link, window.location.origin).href;
} catch (error) {
openModal('<p>Invalid URL.</p>');
return;
}
absoluteLink += (absoluteLink.includes('?') ? '&' : '?') + 'modal=1';
if (DEBUG) logDebug('Fetching absolute link: ' + absoluteLink);
openModal('<p>Loading...</p>');
fetch(absoluteLink, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function(response) {
if (DEBUG) logDebug('Fetch response status: ' + response.status + ' ' + response.statusText);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return response.text();
})
.then(function(data) {
if (DEBUG) logDebug('Fetched HTML length: ' + data.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
let combinedContent = '';
const sections = doc.querySelectorAll('.modal-story-content');
if (DEBUG) logDebug('Found ' + sections.length + ' modal-story-content sections');
if (sections.length > 0) {
sections.forEach(function(section, index) {
combinedContent += section.outerHTML.trim();
if (index < sections.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
const tattooFallback = doc.querySelectorAll('.dynamic-section .inner');
if (DEBUG) logDebug('Found ' + tattooFallback.length + ' Tattoo .dynamic-section .inner sections');
if (tattooFallback.length > 0) {
tattooFallback.forEach(function(section, index) {
const newContent = document.createElement('div');
newContent.className = 'modal-story-content';
newContent.innerHTML = section.innerHTML;
combinedContent += newContent.outerHTML.trim();
if (index < tattooFallback.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
if (DEBUG) logDebug('No content found in Tattoo structure', 'red');
combinedContent = '<p>Content could not be parsed from the page.</p>';
}
}
if (combinedContent) {
openModal(combinedContent);
} else {
openModal('<p>No content found.</p>');
}
})
.catch(function(error) {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
openModal('<p>Sorry, the content could not be loaded: ' + error.message + '</p>');
});
});
});
closeBtn.addEventListener('click', closeModal);
window.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && modal.style.display === 'block') {
closeModal();
}
});
} else {
if (DEBUG) logDebug('Modal elements missing - skipping modal functionality (normal on non-modal pages)', 'orange');
}
function openModal(content) {
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = content;
const modal = document.getElementById('story-modal');
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
const closeBtn = document.querySelector('.modal-close');
if (closeBtn) closeBtn.focus();
if (DEBUG) logDebug('Modal opened');
}
function closeModal() {
const modal = document.getElementById('story-modal');
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = '';
if (DEBUG) logDebug('Modal closed');
}
});
This is the matched, clean pair with the new types and logic.
Let me know the console output and if the video appears in the modal now (and hidden on page).
Current time: January 14, 2026 05:15 PM HKT
This should be the clean, working pair! 😊
Debug: No embed found in parent section for local modal (checked: content dynamic-section)What should we do next? As discussed, we can return to the earlier state where the Pop-up video displays simultaneously on-page and in-modal, if necessary.
#.This is not a regression — it's the expected result of the rollback to V2.83 PHP + the current JS logic.
In V2.83 PHP:
code_type = 'YouTube' or 'PayPal' (in the non-modal branch, and only when model allows it)<div class="content-embed" id="embed-XXX"> in the mother sectionThe rollback removed the special output block for "Pop-up Video" — that's why the div is missing again.
This is stable and functional — you can live with it while we decide on the next step.
To make double display optional:
# → modal opens with local content + video (double)If you want to keep the new types and hide the video on main page:
if ($row['code_type'] === 'Pop-up Video') { ... } block in non-modal branch (always output div + push)This is slightly more work but gives you the exact control you want.
Stick with Path 1 for the next few days:
Then, when you have time, we can:
If double display works → we're back to baseline.
If not → share console + inspect screenshot of the embed area.
We are not looping — we're just confirming the rollback state is clean before deciding on the next small improvement.
Let me know the test result — then we can decide whether to stay or add the hide feature.
Current time: January 14, 2026 05:45 PM HKT
We're stabilizing first — good call! 😊
For now, let's return to the stable V2.83 pair (PHP + JS), where videos already work reliably, and then enable double-display for "YouTube" type embeds (show on page and in modal when the button is clicked). This is the safest and quickest way to achieve what you want without risking new bugs or white/black frames.
Use the V2.83 PHP (the rollback file I provided earlier)
Use the V2.83 JS (the full rollback file I provided in my previous message — the one without "Page Video" / "Pop-up Video" cases)
In admin:
landing-260107.php) → modal will fetch the same page → duplicate video# → modal opens with local content (including the video embed)Test:
# → modal opens with local section content + videoIn V2.83 JS, # / empty links trigger the warning:
if (!link || link === '#' || link.includes('index.php')) {
openModal('<p>This link cannot be loaded in a modal to prevent recursive loading.</p>');
return;
}
To allow # / empty to open the local section (including video embed), replace that block with:
if (!link || link === '#') {
// Open local section content (including embed)
const content = this.closest('.content__buttons')?.parentElement?.innerHTML || '<p>Content not found.</p>';
openModal(content);
if (DEBUG) logDebug('Opened local section modal (no link)', 'green');
return;
}
This opens the entire parent .inner content (title + text + embed) in the modal — no fetch, no warning.
.modal-body .content-embed {
min-height: 315px !important;
height: 450px !important;
}
.modal-body .content-embed iframe {
width: 100% !important;
height: 100% !important;
border-radius: 8px !important;
}
This gives you immediate working double-display without new types.
Let me know if you want:
We're stable again — good to go!
Current time: January 14, 2026 05:45 PM HKT
All YT videos with "YouTube" code_type but NO button text+linked page are shown on the page correctly. All modal buttons with linked pages are shown on the page correctly. In a testing section with "YouTube" code_type AND button text+empty linked page, the first video of the page is loaded in the modal Switching the above section with the "YouTubeModal" code_type AND button text+empty linked page will hide the video from the page (Bravo!). But the modal is still showing the first video of the page.
I think the requirements now are:
For any section with "YouTube" as the code_type, the logic should just follow any section with no code block: A modal will be triggered if it has button text + linked page. The YT video should NOT appear in the modal in all cases. For any section with "YouTubeModal" as the code_type + button text + empty linked page , the video should be hidden on page (Already so) but showing in the modal
So what we need to fix seems to be just showing the right video in the modal. Below are the code pair I restored.
<?php
// pull-contents.php (Version 2.90 - Manually restored - Updated 2028/01/14 19:25)
define('DEBUG', true);
define('IMAGE_BASE_PATH', 'https://navi.claudioflute.live/images/');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$config_path = '../config.php';
if (!file_exists($config_path)) {
die("Error: config.php not found at $config_path");
}
require_once $config_path;
if (!isset($dbServer) || !isset($dbUsername) || !isset($dbPassword) || !isset($dbDatabase)) {
die("Error: AppGini database variables not defined in config.php");
}
$conn = new mysqli($dbServer, $dbUsername, $dbPassword, $dbDatabase, $dbPort ?: 3306);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Auto-detect page
if (isset($_GET['page'])) {
$current_page = $_GET['page'];
} else {
$calling_file = basename($_SERVER['SCRIPT_FILENAME']);
$current_page = str_replace('.php', '', $calling_file);
if ($current_page === 'index') {
$current_page = 'landing';
}
}
$status = 'Show';
// Preload PayPal SDK
$paypalSdkUrl = 'https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription';
echo "<script src=\"$paypalSdkUrl\" id=\"paypal-sdk\" async=\"false\"></script>";
// Is modal request
$is_modal_request = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || isset($_GET['modal']);
// Is stories page (for preview logic)
$is_stories_page = ($current_page === 'stories');
// SQL - matches your schema
$sql = "SELECT id, title, content, code, code_type, image, button_link, button_text, position, model, date_edited, theme_img, img_role, chk_divider
FROM web_content
WHERE page = ? AND status = ?
ORDER BY position ASC, date_edited DESC";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $current_page, $status);
$stmt->execute();
$result = $stmt->get_result();
$sections = [];
while ($row = $result->fetch_assoc()) {
$sections[] = $row;
}
// Group by position for multi-column
$sections_by_position = [];
foreach ($sections as $row) {
$position = $row['position'] ?? 1;
$sections_by_position[$position][] = $row;
}
if (!empty($sections_by_position)) {
ksort($sections_by_position);
foreach ($sections_by_position as $position => $group) {
$is_grid = count($group) > 1;
if ($is_modal_request) {
foreach ($group as $index => $row) {
echo "<div class=\"modal-story-content\">";
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
}
echo $row['content'];
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'PayPal', 'Modal Video'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
echo "</div>";
if ($index < count($group) - 1) {
echo "<div class=\"modal-section-separator\"></div>";
}
}
} else {
// SMART CLASS for Tattoo
$section_class = 'content';
$has_background = false;
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$has_background = true;
}
}
if ($has_background) {
$section_class = 'hero';
}
if ($is_grid) {
$section_class .= ' grid';
}
$bg_style = '';
if ($section_class === 'hero') {
// Use first background image in group
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$bg_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$bg_style = "style=\"background-image: url('$bg_url');\"";
break;
}
}
}
echo "<section class=\"{$section_class} dynamic-section\" {$bg_style}>";
echo "<div class=\"inner\">";
foreach ($group as $row) {
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">{$row['title']}</h2>";
}
echo $row['content'];
// title-text-image-video model
if ($row['model'] === 'title-text-image-video') {
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
// title-text-video model (video only, no image)
elseif ($row['model'] === 'title-text-video') {
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
// Existing models (title-text-image-code, title-text-code)
elseif ($row['model'] === 'title-text-image-code' || $row['model'] === 'title-text-code') {
if (!empty($row['image']) && $row['model'] === 'title-text-image-code') {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'PayPal', 'Modal Video'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
}
// Default image
elseif (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
// NEW: Always output embed div + push for Modal Video (regardless of model)
if ($row['code_type'] === 'Modal Video') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'Modal Video'});</script>";
}
// BUTTON RENDERING
if (!empty($row['button_text'])) {
$button_text = htmlspecialchars($row['button_text']);
$button_link = htmlspecialchars($row['button_link'] ?: '#');
echo "<div class=\"content__buttons\">";
if ($is_stories_page) {
$modal_id = 'modal-content-' . $row['id'];
echo "<div id=\"$modal_id\" style=\"display:none;\">";
echo '<div class="modal-story-content">';
if (!empty($row['title'])) echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image modal-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['content'])) echo $row['content'];
echo '</div>';
echo "</div>";
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-id=\"$modal_id\">$button_text</a>";
} else {
// Always add modal-trigger class
$data_attr = '';
if (!empty($button_link) && $button_link !== '#') {
$data_attr = "data-modal-link=\"$button_link\"";
}
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" $data_attr>$button_text</a>";
}
echo "</div>";
}
}
echo "</div>"; // .inner
// Separator — moved AFTER section for below background
$show_separator = false;
$sep_url = '';
if (!empty($row['theme_img']) && $row['img_role'] === 'Separator') {
$sep_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$show_separator = true;
} elseif (!empty($row['chk_divider']) && $row['chk_divider'] == 1) {
$sep_url = 'https://navi.claudioflute.live/public/Flying_bee_R.gif';
$show_separator = true;
}
if ($show_separator) {
$sep_class = ($row['img_role'] === 'Separator') ? 'separator custom-separator' : 'separator default-bee';
echo "<div class=\"separator-wrapper\">";
echo "<img src=\"$sep_url\" class=\"$sep_class\" alt=\"Separator\">";
echo "</div>";
}
echo "</section>";
}
}
} else {
echo "No sections found.";
}
// Modal container (only for non-AJAX requests)
if (!$is_modal_request) {
echo '<div id="story-modal" class="modal">';
echo '<div class="modal-content">';
echo '<span class="modal-close">×</span>';
echo '<div id="modal-body"></div>';
echo '</div>';
echo '</div>';
}
// Load external JS for modal and embed rendering
echo '<script src="/modal-embed.js?v=' . time() . '"></script>';
$stmt->close();
$conn->close();
?>
// modal-embed.js - (Version 2.90 - Manually restored - Updated 2028/01/14 19:25)
console.log("modal-embed.js LOADED SUCCESSFULLY (Tattoo version 2.86)");
document.addEventListener('DOMContentLoaded', function() {
const DEBUG = true; // Change to false when done testing
let debugLog = null;
if (DEBUG) {
debugLog = document.createElement('div');
debugLog.id = 'debug-log';
debugLog.style.position = 'fixed';
debugLog.style.bottom = '10px';
debugLog.style.left = '10px';
debugLog.style.background = 'rgba(0, 0, 0, 0.8)';
debugLog.style.color = 'white';
debugLog.style.padding = '10px';
debugLog.style.zIndex = '1000';
debugLog.style.maxHeight = '200px';
debugLog.style.overflowY = 'auto';
document.body.appendChild(debugLog);
}
function logDebug(message, color = 'green') {
if (DEBUG && debugLog) {
debugLog.insertAdjacentHTML('beforeend', `<div style="color: ${color}; font-weight: bold;">Debug: ${message}</div>`);
debugLog.scrollTop = debugLog.scrollHeight;
}
if (DEBUG && console) console.log(`Debug: ${message}`);
}
if (DEBUG) logDebug('Modal script loaded (Tattoo version 2.86)');
const modal = document.getElementById('story-modal');
const modalBody = document.getElementById('modal-body');
const closeBtn = document.querySelector('.modal-close');
const triggers = document.querySelectorAll('.modal-trigger');
if (DEBUG) logDebug('Found ' + triggers.length + ' modal triggers');
// Embed rendering function
function renderEmbed(item, index, total) {
const container = document.getElementById(`embed-${item.id}`);
if (!container) {
if (DEBUG) logDebug(`Container not found for embed ID ${item.id}`, 'red');
return;
}
if (item.type === 'YouTube') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered YouTube embed for ID ${item.id} (sandbox removed)`, 'orange');
} else if (item.type === 'PayPal') {
const divMatch = item.code.match(/<div\s+[^>]*id="[^"]+"[^>]*>/i);
const buttonMatch = item.code.match(/<script>\s*paypal\.Buttons\([\s\S]*?\)\.render\(['"][^'"]+['"]\);\s*<\/script>/i);
if (divMatch && buttonMatch) {
const uniqueId = `paypal-button-container-${item.id}`;
let divContent = divMatch[0].replace(/id="[^"]+"/, `id="${uniqueId}"`);
container.innerHTML = divContent;
container.querySelector(`#${uniqueId}`).style.cssText = 'width: 100% !important; min-width: 300px !important; max-width: 300px !important; margin: 0 auto !important;';
function waitForSDK(callback) {
if (typeof paypal !== 'undefined') {
callback();
} else {
setTimeout(function() { waitForSDK(callback); }, 100);
}
}
waitForSDK(function() {
let scriptContent = buttonMatch[0].replace(/^<script>|<\/script>$/g, '').trim();
scriptContent = scriptContent.replace(/\.render\(['"][^'"]+['"]\)/, `.render('#${uniqueId}')`);
const uniqueScript = `(function() { ${scriptContent} })();`;
const buttonScript = document.createElement('script');
buttonScript.textContent = uniqueScript;
document.body.appendChild(buttonScript);
});
}
} else if (item.type === 'Modal Video') {
container.innerHTML = item.code;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered Modal Video embed for ID ${item.id}`, 'orange');
}
}
// Run embed rendering on page load
if (window.contentCode && Array.isArray(window.contentCode)) {
window.contentCode.forEach(function(item, index) {
renderEmbed(item, index, window.contentCode.length);
});
} else if (DEBUG) {
logDebug('contentCode is not an array or unavailable', 'red');
}
// PHP debug info (safe try-catch)
try {
if (window.debugFromPHP) {
console.log('PHP Debug Info:', window.debugFromPHP);
if (DEBUG) logDebug('PHP → Fetched sections: ' + window.debugFromPHP.fetched + ', Grouped positions: ' + window.debugFromPHP.grouped + ', Modal request: ' + window.debugFromPHP.modalRequest, 'purple');
} else {
console.warn('No debug info from PHP');
}
} catch (e) {
console.warn('Error reading PHP debug info:', e);
}
// Modal logic only if elements exist
if (modal && modalBody && closeBtn && triggers.length > 0) {
if (DEBUG) logDebug('Modal elements found - enabling modal functionality');
triggers.forEach(function(trigger) {
trigger.addEventListener('click', function(e) {
e.preventDefault();
if (DEBUG) logDebug(`Modal trigger clicked: ${this.getAttribute('data-modal-id') || this.getAttribute('data-modal-link')}`, 'green');
const modalId = this.getAttribute('data-modal-id');
if (modalId) {
const contentDiv = document.getElementById(modalId);
if (contentDiv) {
const content = contentDiv.innerHTML;
if (DEBUG) logDebug('Stories modal triggered, content from ID: ' + modalId);
openModal(content);
} else {
openModal('<p>Content not found.</p>');
if (DEBUG) logDebug('Modal content not found for ID: ' + modalId, 'red');
}
return;
}
const link = this.getAttribute('data-modal-link');
if (DEBUG) logDebug('Trigger clicked, link: ' + link);
// Allow empty or # link to open ONLY the Modal Video embed (no mother content)
if (!link || link === '#') {
// Search the entire page for any .content-embed (fallback if parent lookup fails)
const videoEmbed = document.querySelector('.content-embed[id^="embed-"]') ||
this.closest('.dynamic-section')?.querySelector('.content-embed[id^="embed-"]') ||
this.closest('.inner')?.querySelector('.content-embed[id^="embed-"]') ||
this.closest('.grid-item')?.querySelector('.content-embed[id^="embed-"]');
if (videoEmbed) {
const embedClone = videoEmbed.cloneNode(true);
openModal(embedClone.outerHTML);
if (DEBUG) logDebug('Opened local Modal Video embed only (no mother content)', 'green');
} else {
openModal('<p>No Modal Video embed found in this section.</p>');
if (DEBUG) logDebug('No embed found for local modal – checked page, dynamic-section, inner, grid-item', 'red');
}
return;
}
// Prevent recursive loading for same-page links
if (link.includes('index.php') || link === window.location.pathname) {
openModal('<p>This link cannot be loaded in a modal to prevent recursive loading.</p>');
return;
}
// Normal fetch for other links
let absoluteLink;
try {
absoluteLink = new URL(link, window.location.origin).href;
} catch (error) {
openModal('<p>Invalid URL.</p>');
return;
}
absoluteLink += (absoluteLink.includes('?') ? '&' : '?') + 'modal=1';
if (DEBUG) logDebug('Fetching absolute link: ' + absoluteLink);
openModal('<p>Loading...</p>');
fetch(absoluteLink, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function(response) {
if (DEBUG) logDebug('Fetch response status: ' + response.status + ' ' + response.statusText);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return response.text();
})
.then(function(data) {
if (DEBUG) logDebug('Fetched HTML length: ' + data.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
let combinedContent = '';
const sections = doc.querySelectorAll('.modal-story-content');
if (DEBUG) logDebug('Found ' + sections.length + ' modal-story-content sections');
if (sections.length > 0) {
sections.forEach(function(section, index) {
combinedContent += section.outerHTML.trim();
if (index < sections.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
const tattooFallback = doc.querySelectorAll('.dynamic-section .inner');
if (DEBUG) logDebug('Found ' + tattooFallback.length + ' Tattoo .dynamic-section .inner sections');
if (tattooFallback.length > 0) {
tattooFallback.forEach(function(section, index) {
const newContent = document.createElement('div');
newContent.className = 'modal-story-content';
newContent.innerHTML = section.innerHTML;
combinedContent += newContent.outerHTML.trim();
if (index < tattooFallback.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
if (DEBUG) logDebug('No content found in Tattoo structure', 'red');
combinedContent = '<p>Content could not be parsed from the page.</p>';
}
}
if (combinedContent) {
openModal(combinedContent);
} else {
openModal('<p>No content found.</p>');
}
})
.catch(function(error) {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
openModal('<p>Sorry, the content could not be loaded: ' + error.message + '</p>');
});
});
});
closeBtn.addEventListener('click', closeModal);
window.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && modal.style.display === 'block') {
closeModal();
}
});
} else {
if (DEBUG) logDebug('Modal elements missing - skipping modal functionality (normal on non-modal pages)', 'orange');
}
function openModal(content) {
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = content;
const modal = document.getElementById('story-modal');
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
const closeBtn = document.querySelector('.modal-close');
if (closeBtn) closeBtn.focus();
if (DEBUG) logDebug('Modal opened');
}
function closeModal() {
const modal = document.getElementById('story-modal');
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = '';
if (DEBUG) logDebug('Modal closed');
}
});
→ Video shows on page → Modal (if button exists) should NOT show the video in the modal. Only content from linked page should be shown in the modal (Same as section with empty code block)I know this is new and different from my requirements before.Do we need to adjust the fix you proposed?
| Code Type | Button Text + Link | On-Page Behavior | Modal Behavior |
|---|---|---|---|
| YouTube | No button text | Video visible | No modal (no button) |
| YouTube | Button + real link | Video visible | Fetched content only (no video from code block) |
| YouTubeModal | Button + no link (# or empty) | Video hidden | Video only (from this section's code block) |
| YouTubeModal | Button + real link | Video hidden | Fetched content + video from this section's code block |
PHP (pull-contents.php)
modal-trigger class (even for #/empty link).Add this block in the non-modal branch, after the last elseif but before default image:
// Always output embed div + push for YouTubeModal (video only in modal)
if ($row['code_type'] === 'YouTubeModal') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTubeModal'});</script>";
}
In the modal branch, keep the existing embed output (for "YouTube", "PayPal", "YouTubeModal") — no change needed.
JS (modal-embed.js)
renderEmbed (raw insert, no sandbox removal).Add this case in renderEmbed:
} else if (item.type === 'YouTubeModal') {
container.innerHTML = item.code;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered YouTubeModal embed for ID ${item.id}`, 'orange');
}
Update the local modal block (if (!link || link === '#') { ... }):
if (!link || link === '#') {
const sectionParent = this.closest('.dynamic-section') ||
this.closest('.inner') ||
this.closest('.grid-item') ||
this.closest('.content');
if (sectionParent) {
const videoEmbed = sectionParent.querySelector('.content-embed[id^="embed-"]');
if (videoEmbed) {
const embedClone = videoEmbed.cloneNode(true);
openModal(embedClone.outerHTML);
if (DEBUG) logDebug('Opened local YouTubeModal embed from this section only', 'green');
} else {
openModal('<p>No YouTubeModal embed found in this section.</p>');
if (DEBUG) logDebug('No embed found in parent section for local modal (checked: ' + sectionParent.className + ')', 'red');
}
} else {
openModal('<p>Section context not found.</p>');
if (DEBUG) logDebug('No parent section found for local modal', 'red');
}
return;
}
Update the re-render block in openModal to skip "YouTube" (no duplicate):
setTimeout(function() {
const modalEmbeds = modalBody.querySelectorAll('.content-embed[id^="embed-"]');
if (modalEmbeds.length > 0) {
if (DEBUG) logDebug('Found ' + modalEmbeds.length + ' embeds inside modal – re-rendering', 'orange');
modalEmbeds.forEach(function(embedDiv) {
const id = embedDiv.id.replace('embed-', '');
const item = window.contentCode?.find(function(i) {
return i.id == id;
});
if (item) {
// Skip re-rendering "YouTube" in modal (no duplicate)
if (item.type === 'YouTube') {
if (DEBUG) logDebug(`Skipped re-rendering YouTube embed ID ${id} in modal`, 'gray');
return;
}
renderEmbed(item, 0, 1);
if (DEBUG) logDebug('Re-rendered embed-' + id + ' inside modal', 'green');
} else {
if (DEBUG) logDebug('No matching contentCode for embed-' + id + ' in modal', 'red');
}
});
} else {
if (DEBUG) logDebug('No embeds found inside modal', 'orange');
}
}, 500);
CSS (add to bottom of Custom CSS)
.content-embed:has(iframe) {
display: none !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
}
.modal-body .content-embed {
display: block !important;
min-height: 315px !important;
height: 70vh !important;
}This is minimal, safe, and matches your clarified requirements.
Current time: January 14, 2026 08:15 PM HKT
This is the precise, final fix set — no more loops! 😊
"YouTube" code_type
→ Video shows on page
→ Modal (if button exists + real link) shows fetched content only (no video from code block)
→ No button text = no modal (current logic)
"YouTubeModal" code_type
→ Video hidden on main page
→ Modal with button + empty link (# or blank) → video only (from this section's code block)
→ Modal with button + real link → fetched content + video from this section's code block
→ No button text = no modal
No support for video embeds from fetched sections in modal (avoids ID matching issues)
CSS you provided is correct and will be kept as-is for hiding on page + sizing in modal
<?php
// pull-contents.php (Version 2.91 - Final Goal Summary Logic)
// Last updated: January 14, 2026
// Code Types: "YouTube" (page display), "YouTubeModal" (modal-only), "PayPal"
// Button: No button = no modal; "YouTube" + real link = no video in modal; "YouTubeModal" + real link = video from this section only
define('DEBUG', true);
define('IMAGE_BASE_PATH', 'https://navi.claudioflute.live/images/');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$config_path = '../config.php';
if (!file_exists($config_path)) {
die("Error: config.php not found at $config_path");
}
require_once $config_path;
if (!isset($dbServer) || !isset($dbUsername) || !isset($dbPassword) || !isset($dbDatabase)) {
die("Error: AppGini database variables not defined in config.php");
}
$conn = new mysqli($dbServer, $dbUsername, $dbPassword, $dbDatabase, $dbPort ?: 3306);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Auto-detect page
if (isset($_GET['page'])) {
$current_page = $_GET['page'];
} else {
$calling_file = basename($_SERVER['SCRIPT_FILENAME']);
$current_page = str_replace('.php', '', $calling_file);
if ($current_page === 'index') {
$current_page = 'landing';
}
}
$status = 'Show';
// Preload PayPal SDK
$paypalSdkUrl = 'https://www.paypal.com/sdk/js?client-id=AUNZIE2pdhVLgVgUFGEQyyYAJVMuxwkXmCwvslu3c1eR-TMLSvCIBhDZxhhteIoQCTAzJ-50qNcVpuoa&vault=true&intent=subscription';
echo "<script src=\"$paypalSdkUrl\" id=\"paypal-sdk\" async=\"false\"></script>";
// Is modal request
$is_modal_request = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') || isset($_GET['modal']);
// Is stories page (for preview logic)
$is_stories_page = ($current_page === 'stories');
// SQL
$sql = "SELECT id, title, content, code, code_type, image, button_link, button_text, position, model, date_edited, theme_img, img_role, chk_divider
FROM web_content
WHERE page = ? AND status = ?
ORDER BY position ASC, date_edited DESC";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $current_page, $status);
$stmt->execute();
$result = $stmt->get_result();
$sections = [];
while ($row = $result->fetch_assoc()) {
$sections[] = $row;
}
// Group by position
$sections_by_position = [];
foreach ($sections as $row) {
$position = $row['position'] ?? 1;
$sections_by_position[$position][] = $row;
}
if (!empty($sections_by_position)) {
ksort($sections_by_position);
foreach ($sections_by_position as $position => $group) {
$is_grid = count($group) > 1;
if ($is_modal_request) {
foreach ($group as $index => $row) {
echo "<div class=\"modal-story-content\">";
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
}
echo $row['content'];
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'YouTubeModal', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
echo "</div>";
if ($index < count($group) - 1) {
echo "<div class=\"modal-section-separator\"></div>";
}
}
} else {
$section_class = 'content';
$has_background = false;
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$has_background = true;
}
}
if ($has_background) {
$section_class = 'hero';
}
if ($is_grid) {
$section_class .= ' grid';
}
$bg_style = '';
if ($section_class === 'hero') {
foreach ($group as $row) {
if ($row['img_role'] === 'Background' && !empty($row['theme_img'])) {
$bg_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$bg_style = "style=\"background-image: url('$bg_url');\"";
break;
}
}
}
echo "<section class=\"{$section_class} dynamic-section\" {$bg_style}>";
echo "<div class=\"inner\">";
foreach ($group as $row) {
if (!empty($row['title'])) {
echo "<h2 class=\"section-title\">{$row['title']}</h2>";
}
echo $row['content'];
// title-text-image-video model
if ($row['model'] === 'title-text-image-video') {
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
// title-text-video model (video only, no image)
elseif ($row['model'] === 'title-text-video') {
if (!empty($row['code']) && $row['code_type'] === 'YouTube') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTube'});</script>";
}
}
// Existing models (title-text-image-code, title-text-code)
elseif ($row['model'] === 'title-text-image-code' || $row['model'] === 'title-text-code') {
if (!empty($row['image']) && $row['model'] === 'title-text-image-code') {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['code']) && in_array($row['code_type'], ['YouTube', 'YouTubeModal', 'PayPal'])) {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: '{$row['code_type']}'});</script>";
}
}
// Default image
elseif (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
// Always output embed div + push for YouTubeModal (video only in modal)
if ($row['code_type'] === 'YouTubeModal') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'YouTubeModal'});</script>";
}
// BUTTON RENDERING
if (!empty($row['button_text'])) {
$button_text = htmlspecialchars($row['button_text']);
$button_link = htmlspecialchars($row['button_link'] ?: '#');
echo "<div class=\"content__buttons\">";
if ($is_stories_page) {
$modal_id = 'modal-content-' . $row['id'];
echo "<div id=\"$modal_id\" style=\"display:none;\">";
echo '<div class="modal-story-content">';
if (!empty($row['title'])) echo "<h2 class=\"section-title\">" . $row['title'] . "</h2>";
if (!empty($row['image'])) {
$image_url = IMAGE_BASE_PATH . htmlspecialchars($row['image']);
echo "<div class=\"content-image modal-image\"><img src=\"$image_url\" alt=\"Section Image\" class=\"welcome-image\"></div>";
}
if (!empty($row['content'])) echo $row['content'];
echo '</div>';
echo "</div>";
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" data-modal-id=\"$modal_id\">$button_text</a>";
} else {
$data_attr = '';
if (!empty($button_link) && $button_link !== '#') {
$data_attr = "data-modal-link=\"$button_link\"";
}
echo "<a class=\"btn modal-trigger\" href=\"javascript:void(0)\" $data_attr>$button_text</a>";
}
echo "</div>";
}
}
echo "</div>"; // .inner
// Separator
$show_separator = false;
$sep_url = '';
if (!empty($row['theme_img']) && $row['img_role'] === 'Separator') {
$sep_url = IMAGE_BASE_PATH . htmlspecialchars($row['theme_img']);
$show_separator = true;
} elseif (!empty($row['chk_divider']) && $row['chk_divider'] == 1) {
$sep_url = 'https://navi.claudioflute.live/public/Flying_bee_R.gif';
$show_separator = true;
}
if ($show_separator) {
$sep_class = ($row['img_role'] === 'Separator') ? 'separator custom-separator' : 'separator default-bee';
echo "<div class=\"separator-wrapper\">";
echo "<img src=\"$sep_url\" class=\"$sep_class\" alt=\"Separator\">";
echo "</div>";
}
echo "</section>";
}
}
} else {
echo "No sections found.";
}
// Modal container
if (!$is_modal_request) {
echo '<div id="story-modal" class="modal">';
echo '<div class="modal-content">';
echo '<span class="modal-close">×</span>';
echo '<div id="modal-body"></div>';
echo '</div>';
echo '</div>';
}
// Load external JS
echo '<script src="/modal-embed.js?v=' . time() . '"></script>';
$stmt->close();
$conn->close();
?>
This is the matched JS file with:
// modal-embed.js - Version 2.91 (Tattoo-optimized, Goal Summary Logic)
// Last updated: January 14, 2026
console.log("modal-embed.js LOADED SUCCESSFULLY (Tattoo version 2.91)");
document.addEventListener('DOMContentLoaded', function() {
const DEBUG = true; // Change to false when done testing
let debugLog = null;
if (DEBUG) {
debugLog = document.createElement('div');
debugLog.id = 'debug-log';
debugLog.style.position = 'fixed';
debugLog.style.bottom = '10px';
debugLog.style.left = '10px';
debugLog.style.background = 'rgba(0, 0, 0, 0.8)';
debugLog.style.color = 'white';
debugLog.style.padding = '10px';
debugLog.style.zIndex = '1000';
debugLog.style.maxHeight = '200px';
debugLog.style.overflowY = 'auto';
document.body.appendChild(debugLog);
}
function logDebug(message, color = 'green') {
if (DEBUG && debugLog) {
debugLog.insertAdjacentHTML('beforeend', `<div style="color: ${color}; font-weight: bold;">Debug: ${message}</div>`);
debugLog.scrollTop = debugLog.scrollHeight;
}
if (DEBUG && console) console.log(`Debug: ${message}`);
}
if (DEBUG) logDebug('Modal script loaded (Tattoo version 2.91)');
const modal = document.getElementById('story-modal');
const modalBody = document.getElementById('modal-body');
const closeBtn = document.querySelector('.modal-close');
const triggers = document.querySelectorAll('.modal-trigger');
if (DEBUG) logDebug('Found ' + triggers.length + ' modal triggers');
// Embed rendering function
function renderEmbed(item, index, total) {
const container = document.getElementById(`embed-${item.id}`);
if (!container) {
if (DEBUG) logDebug(`Container not found for embed ID ${item.id}`, 'red');
return;
}
if (item.type === 'Page Video') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered Page Video embed for ID ${item.id} (sandbox removed)`, 'orange');
} else if (item.type === 'YouTubeModal') {
container.innerHTML = item.code;
container.style.display = 'none !important'; // Hide on main page
container.style.height = '0 !important';
container.style.overflow = 'hidden !important';
container.style.margin = '0 !important';
container.style.padding = '0 !important';
if (DEBUG) logDebug(`Rendered YouTubeModal embed for ID ${item.id} (hidden on main page)`, 'orange');
} else if (item.type === 'PayPal') {
const divMatch = item.code.match(/<div\s+[^>]*id="[^"]+"[^>]*>/i);
const buttonMatch = item.code.match(/<script>\s*paypal\.Buttons\([\s\S]*?\)\.render\(['"][^'"]+['"]\);\s*<\/script>/i);
if (divMatch && buttonMatch) {
const uniqueId = `paypal-button-container-${item.id}`;
let divContent = divMatch[0].replace(/id="[^"]+"/, `id="${uniqueId}"`);
container.innerHTML = divContent;
container.querySelector(`#${uniqueId}`).style.cssText = 'width: 100% !important; min-width: 300px !important; max-width: 300px !important; margin: 0 auto !important;';
function waitForSDK(callback) {
if (typeof paypal !== 'undefined') {
callback();
} else {
setTimeout(function() { waitForSDK(callback); }, 100);
}
}
waitForSDK(function() {
let scriptContent = buttonMatch[0].replace(/^<script>|<\/script>$/g, '').trim();
scriptContent = scriptContent.replace(/\.render\(['"][^'"]+['"]\)/, `.render('#${uniqueId}')`);
const uniqueScript = `(function() { ${scriptContent} })();`;
const buttonScript = document.createElement('script');
buttonScript.textContent = uniqueScript;
document.body.appendChild(buttonScript);
});
}
}
}
// Run embed rendering on page load
if (window.contentCode && Array.isArray(window.contentCode)) {
window.contentCode.forEach(function(item, index) {
renderEmbed(item, index, window.contentCode.length);
});
} else if (DEBUG) {
logDebug('contentCode is not an array or unavailable', 'red');
}
// PHP debug info (safe try-catch)
try {
if (window.debugFromPHP) {
console.log('PHP Debug Info:', window.debugFromPHP);
if (DEBUG) logDebug('PHP → Fetched sections: ' + window.debugFromPHP.fetched + ', Grouped positions: ' + window.debugFromPHP.grouped + ', Modal request: ' + window.debugFromPHP.modalRequest, 'purple');
} else {
console.warn('No debug info from PHP');
}
} catch (e) {
console.warn('Error reading PHP debug info:', e);
}
// Modal logic only if elements exist
if (modal && modalBody && closeBtn && triggers.length > 0) {
if (DEBUG) logDebug('Modal elements found - enabling modal functionality');
triggers.forEach(function(trigger) {
trigger.addEventListener('click', function(e) {
e.preventDefault();
if (DEBUG) logDebug(`Modal trigger clicked: ${this.getAttribute('data-modal-id') || this.getAttribute('data-modal-link')}`, 'green');
const modalId = this.getAttribute('data-modal-id');
if (modalId) {
const contentDiv = document.getElementById(modalId);
if (contentDiv) {
const content = contentDiv.innerHTML;
if (DEBUG) logDebug('Stories modal triggered, content from ID: ' + modalId);
openModal(content);
} else {
openModal('<p>Content not found.</p>');
if (DEBUG) logDebug('Modal content not found for ID: ' + modalId, 'red');
}
return;
}
const link = this.getAttribute('data-modal-link');
if (DEBUG) logDebug('Trigger clicked, link: ' + link);
// Allow empty or # link to open ONLY the YouTubeModal embed from this section
if (!link || link === '#') {
const sectionParent = this.closest('.dynamic-section') ||
this.closest('.inner') ||
this.closest('.grid-item') ||
this.closest('.content');
if (sectionParent) {
const videoEmbed = sectionParent.querySelector('.content-embed[id^="embed-"]');
if (videoEmbed) {
const embedClone = videoEmbed.cloneNode(true);
openModal(embedClone.outerHTML);
if (DEBUG) logDebug('Opened local YouTubeModal embed from this section only', 'green');
} else {
openModal('<p>No YouTubeModal embed found in this section.</p>');
if (DEBUG) logDebug('No embed found in parent section for local modal (checked: ' + sectionParent.className + ')', 'red');
}
} else {
openModal('<p>Section context not found.</p>');
if (DEBUG) logDebug('No parent section found for local modal', 'red');
}
return;
}
// Prevent recursive loading for same-page links
if (link.includes('index.php') || link === window.location.pathname) {
openModal('<p>This link cannot be loaded in a modal to prevent recursive loading.</p>');
return;
}
// Normal fetch for other links
let absoluteLink;
try {
absoluteLink = new URL(link, window.location.origin).href;
} catch (error) {
openModal('<p>Invalid URL.</p>');
return;
}
absoluteLink += (absoluteLink.includes('?') ? '&' : '?') + 'modal=1';
if (DEBUG) logDebug('Fetching absolute link: ' + absoluteLink);
openModal('<p>Loading...</p>');
fetch(absoluteLink, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function(response) {
if (DEBUG) logDebug('Fetch response status: ' + response.status + ' ' + response.statusText);
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return response.text();
})
.then(function(data) {
if (DEBUG) logDebug('Fetched HTML length: ' + data.length);
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'text/html');
let combinedContent = '';
const sections = doc.querySelectorAll('.modal-story-content');
if (DEBUG) logDebug('Found ' + sections.length + ' modal-story-content sections');
if (sections.length > 0) {
sections.forEach(function(section, index) {
combinedContent += section.outerHTML.trim();
if (index < sections.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
const tattooFallback = doc.querySelectorAll('.dynamic-section .inner');
if (DEBUG) logDebug('Found ' + tattooFallback.length + ' Tattoo .dynamic-section .inner sections');
if (tattooFallback.length > 0) {
tattooFallback.forEach(function(section, index) {
const newContent = document.createElement('div');
newContent.className = 'modal-story-content';
newContent.innerHTML = section.innerHTML;
combinedContent += newContent.outerHTML.trim();
if (index < tattooFallback.length - 1) {
combinedContent += '<div class="modal-section-separator"></div>';
}
});
} else {
if (DEBUG) logDebug('No content found in Tattoo structure', 'red');
combinedContent = '<p>Content could not be parsed from the page.</p>';
}
}
if (combinedContent) {
openModal(combinedContent);
} else {
openModal('<p>No content found.</p>');
}
})
.catch(function(error) {
if (DEBUG) logDebug('Fetch error: ' + error.message, 'red');
openModal('<p>Sorry, the content could not be loaded: ' + error.message + '</p>');
});
});
});
closeBtn.addEventListener('click', closeModal);
window.addEventListener('click', function(event) {
if (event.target === modal) {
closeModal();
}
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape' && modal.style.display === 'block') {
closeModal();
}
});
} else {
if (DEBUG) logDebug('Modal elements missing - skipping modal functionality (normal on non-modal pages)', 'orange');
}
function openModal(content) {
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = content;
// Re-run embed rendering after modal content is inserted
setTimeout(function() {
const modalEmbeds = modalBody.querySelectorAll('.content-embed[id^="embed-"]');
if (modalEmbeds.length > 0) {
if (DEBUG) logDebug('Found ' + modalEmbeds.length + ' embeds inside modal – re-rendering', 'orange');
modalEmbeds.forEach(function(embedDiv) {
const id = embedDiv.id.replace('embed-', '');
const item = window.contentCode?.find(function(i) {
return i.id == id;
});
if (item) {
// Skip re-rendering "YouTube" in modal (no duplicate)
if (item.type === 'YouTube') {
if (DEBUG) logDebug(`Skipped re-rendering YouTube embed ID ${id} in modal`, 'gray');
return;
}
renderEmbed(item, 0, 1);
if (DEBUG) logDebug('Re-rendered embed-' + id + ' inside modal', 'green');
} else {
if (DEBUG) logDebug('No matching contentCode for embed-' + id + ' in modal', 'red');
}
});
} else {
if (DEBUG) logDebug('No embeds found inside modal', 'orange');
}
}, 500);
const modal = document.getElementById('story-modal');
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
const closeBtn = document.querySelector('.modal-close');
if (closeBtn) closeBtn.focus();
if (DEBUG) logDebug('Modal opened');
}
function closeModal() {
const modal = document.getElementById('story-modal');
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
const modalBody = document.getElementById('modal-body');
modalBody.innerHTML = '';
if (DEBUG) logDebug('Modal closed');
}
});
This pair fully supports your Goal Summary logic — no more complications with fetched video embeds.
Let me know the console and modal result after testing — especially if the "YouTube" modal avoids duplicate video and "YouTubeModal" shows only the correct video.
Current time: January 14, 2026 08:45 PM HKT
This V2.91 pair is the final, matched version — ready for testing! 😊
From the code you shared:
PHP V2.90 (pull-contents.php):
modal-trigger class, data-modal-link if link exists.window.contentCode was populated on load, and the re-render in openModal found the ID + code → injected the iframe in modal.JS V2.90 (modal-embed.js):
.content-embed, clones the first one it finds → opens modal with that embed.openModal: scans and renders any embeds in modal (including Modal Video).Overall behavior in V2.90:
The pair was stable because the conditions aligned with your admin settings (models, code_type), and the JS search, though broad, found something to clone.
display: none for "Pop-up Video" in JS, it hid the div, but if the search relied on visible divs or DOM order, it failed.To add "Pop-up Video" (video hidden on page, shown in modal) while keeping "YouTube" unchanged, do the following partial updates to your V2.90 files:
PHP: Add the always-output block for "Pop-up Video" (after the last elseif, before default image):
// Always output embed div + push for Pop-up Video (video only in modal)
if ($row['code_type'] === 'Pop-up Video') {
echo "<div class=\"content-embed\" id=\"embed-{$row['id']}\"></div>";
$encodedCode = json_encode($row['code'], JSON_HEX_TAG);
echo "<script>window.contentCode = window.contentCode || []; window.contentCode.push({id: '{$row['id']}', code: " . $encodedCode . ", type: 'Pop-up Video'});</script>";
}
JS: Add the "Pop-up Video" case in renderEmbed (raw insert + hide on main page):
} else if (item.type === 'Pop-up Video') {
container.innerHTML = item.code;
container.style.display = 'none !important'; // Hide on main page
container.style.height = '0 !important';
container.style.overflow = 'hidden !important';
container.style.margin = '0 !important';
container.style.padding = '0 !important';
if (DEBUG) logDebug(`Rendered Pop-up Video embed for ID ${item.id} (hidden on main page)`, 'orange');
}
No change to local modal block — your current one is fine
If "wrong video" persists → merge the stricter search from my previous message (remove the document.querySelector fallback).
This is the minimal path to your Goal Summary.
Let me know if you need the full V2.91 files with these applied.
Current time: January 14, 2026 08:45 PM HKT
We're wrapping this up! 😊
Checking for "YouTubeModal" in conditional statements, no "Modal Video" or "Pop-up Video" Try search for the video of the "mother" section and embed it Ignore other detailed logic from the Goal Summary
if (item.type === 'YouTube' || item.type === 'YouTubeModal').
However, since these two cases will eventually require different logic (per our Goal Summary), I’d prefer to keep them separate. As a temporary step, please align the behavior of code_type = "YouTubeModal" with that of code_type = "YouTube" rather than merging the conditions.
This is a very good foundation — we now have:
The only remaining gap is the local modal (# or empty link) not showing the video from the mother section's code block (or showing the wrong one).
To fix this one issue while preserving the stable double-display for "YouTube" type (video on page + in modal when linked), we can make a minimal, targeted adjustment to the JS local modal logic.
Replace the current local modal block (if (!link || link === '#') { ... }) with this version:
// Allow empty or # link to open ONLY the embed from this section
if (!link || link === '#') {
// Find the closest parent section containing this button
const sectionParent = this.closest('.dynamic-section') ||
this.closest('.inner') ||
this.closest('.grid-item') ||
this.closest('.content');
if (sectionParent) {
// Look ONLY inside this parent for the embed div
const videoEmbed = sectionParent.querySelector('.content-embed[id^="embed-"]');
if (videoEmbed) {
const embedClone = videoEmbed.cloneNode(true);
openModal(embedClone.outerHTML);
if (DEBUG) logDebug('Opened local embed from this section only', 'green');
} else {
openModal('<p>No embed found in this section.</p>');
if (DEBUG) logDebug('No embed found in parent section for local modal (checked: ' + sectionParent.className + ')', 'red');
}
} else {
openModal('<p>Section context not found.</p>');
if (DEBUG) logDebug('No parent section found for local modal', 'red');
}
return;
}
document.querySelector line → no more grabbing the first embed on the entire pagequerySelectorAll later if needed)If the modal shows the correct video alone → success!
If still "No embed found" → inspect the mother section:
<div class="content-embed" id="embed-XXX"> exists inside the parent (.dynamic-section / .inner / .grid-item)This is the smallest possible fix — no new types, no PHP change, just narrowing the search scope.
Let me know the console output and which video (or message) appears in the modal now.
Current time: January 14, 2026 07:45 PM HKT
This one tweak should make the local modal show the correct video only! 😊
Both code types currently behave the same (double-display: video visible on page + in modal when button has no link).
The next small, safe step is to hide the video frame on the main page only for "YouTubeModal" sections, while keeping everything else unchanged and stable.
Goal:
JS — Hide in renderEmbed (main page only)
In renderEmbed, add a type-specific hide only for "YouTubeModal" after inserting the iframe.
This is executed once on page load (main page rendering), so the frame is inserted but immediately hidden.
display: none !important; removes it visually height: 0, overflow: hidden, margin: 0, padding: 0 ensure zero space is taken (no white rectangle) display: block when called in openModal) No change to PHP
No change to local modal logic
CSS safety net (optional but recommended)
Add this to Custom CSS (bottom) as a fallback in case JS hide fails or is overridden:
/* Hide YouTubeModal embeds on main page (zero space) */
.content-embed:has(iframe) {
display: none !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
}
/* Ensure modal embeds are visible */
.modal-body .content-embed {
display: block !important;
min-height: 315px !important;
height: 70vh !important; /* adjust to taste */
}
renderEmbed (one if branch) and optional CSSrenderEmbed in JS (code below)Here is the updated renderEmbed function (replace only this part in your current JS):
function renderEmbed(item, index, total) {
const container = document.getElementById(`embed-${item.id}`);
if (!container) {
if (DEBUG) logDebug(`Container not found for embed ID ${item.id}`, 'red');
return;
}
if (item.type === 'YouTube') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
container.style.display = 'block';
if (DEBUG) logDebug(`Rendered YouTube embed for ID ${item.id} (sandbox removed)`, 'orange');
} else if (item.type === 'YouTubeModal') {
let cleanCode = item.code.replace(/ sandbox(?=[\s>])/g, '');
container.innerHTML = cleanCode;
// Hide on main page (no visible frame)
container.style.display = 'none !important';
container.style.height = '0 !important';
container.style.overflow = 'hidden !important';
container.style.margin = '0 !important';
container.style.padding = '0 !important';
if (DEBUG) logDebug(`Rendered YouTubeModal embed for ID ${item.id} (hidden on main page)`, 'orange');
} else if (item.type === 'PayPal') {
// your existing PayPal logic...
}
}
Apply this → regenerate → hard refresh → test a "YouTubeModal" section with empty/# link:
Console should show "Rendered YouTubeModal embed for ID XXX (hidden on main page)"
If successful → we have achieved:
Let me know the result — console + whether the video is hidden on page and visible in modal.
Current time: January 15, 2026 12:00 AM HKT
This is the smallest, safest step toward your goal! 😊
Debug: Rendered YouTubeModal embed for ID 12 (hidden on main page) Debug: Modal elements found - enabling modal functionalityClicking the button shows: Debug: Modal trigger clicked: null Debug: Trigger clicked, link: null Debug: Modal opened Debug: Opened local YouTubeModal embed from this section only Debug: Found 1 embeds inside modal – re-rendering Debug: Rendered YouTubeModal embed for ID 12 (hidden on main page)Where has gone wrong?
Copyright © 2023 Curation Web 我的資訊「展」輯. All Rights Reserved | Powered by Bludit | Theme by BlThemes