// Constants const CONFIG = { ENDPOINTS: { DELIVER: '/_d', ADULT: '/_a', CONTACT: '/_c', EVENT: '/_e', FALLBACK: '/_o' }, AFD_SCRIPT: 'https://www.google.com/adsense/domains/caf.js?abp=1&abpgo=true', ERROR_MESSAGES: { FETCH_UNSUPPORTED: 'This browser is not supported. Please use a modern browser.', SERVER_ERROR: 'Error communicating with server. Please try again.', INITIALIZATION_ERROR: 'Failed to initialize page. If you have an ad blocker enabled, please disable it and refresh the page.', DOMAIN_NOT_FOUND: 'The domain has not been provisioned on our platform.', NO_CHANNELS: 'No valid delivery channels available for this domain.', DNS_MISCONFIGURED: 'Domain appears to be misconfigured. Please check the DNS settings.', UNDER_DEVELOPMENT: 'This domain is under development and coming soon.', AD_BLOCK_DETECTED: 'Ad blocker detected. Please disable ad blockers to view this page.' } }; // Error Handler class DeliveryError extends Error { constructor(message, code, details = {}) { super(message); this.name = 'DeliveryError'; this.code = code; this.details = details; } } // UI Manager class UIManager { constructor() { this.container = document.getElementById('container'); this.messageEl = document.getElementById('message'); this.headerEl = document.querySelector('header'); this.contactHeader = document.getElementById('banner'); } configureContactMessage(contact) { // If no contact info or it's empty, don't show any contact message if (!contact || Object.keys(contact).length === 0) { return; } const contactEl = contact.location === 'bottom' ? this.ensureContactFooter() : this.contactHeader; if (!contactEl) return; // Create contact link const link = document.createElement('a'); link.href = CONFIG.ENDPOINTS.CONTACT; link.textContent = contact.banner_text; link.style.textDecoration = 'none'; // Apply styles based on contact style if (contact.style === 'aggressive') { contactEl.style.background = '#ff6b6b'; link.style.color = '#ffffff'; link.style.fontWeight = 'bold'; } else { contactEl.style.background = '#646464'; link.style.color = '#eeeeee'; } const p = document.createElement('p'); p.appendChild(link); contactEl.innerHTML = ''; contactEl.appendChild(p); contactEl.style.opacity = '1'; } ensureContactFooter() { let contactFooter = document.getElementById('contact-footer'); if (!contactFooter) { contactFooter = document.createElement('div'); contactFooter.id = 'contact-footer'; contactFooter.style.cssText = 'position: fixed; bottom: 0; left: 0; width: 100%; opacity: 0; transition: opacity 0.3s ease; padding: 10px 0; text-align: center;'; document.body.appendChild(contactFooter); } return contactFooter; } showError(message) { if (this.messageEl) { this.messageEl.textContent = message; } this.showContainer(); } showContainer() { if (this.container) { this.container.style.visibility = 'visible'; } } setPageTitle(display_domain, show_title) { if (!this.headerEl) return; if (show_title) { document.title = display_domain; const titleElement = document.createElement('h1'); titleElement.textContent = display_domain; this.headerEl.appendChild(titleElement); } else { const spacerElement = document.createElement('div'); spacerElement.style.height = '2em'; this.headerEl.appendChild(spacerElement); } } } // API Client class APIClient { async fetchPageData() { try { // Get current redirect count from URL parameters const currentUrl = new URL(window.location.href); const redirectCount = parseInt(currentUrl.searchParams.get('rc') || '0'); const response = await fetch(CONFIG.ENDPOINTS.DELIVER, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ referrer: document.referrer, current_location: window.location.href, redirect_count: redirectCount, user_agent: navigator.userAgent, window_info: { href: window.location.href, hostname: window.location.hostname, pathname: window.location.pathname } }) }); if (!response.ok) { const errorText = await response.text(); console.debug('Error response:', errorText); let errorData; try { errorData = JSON.parse(errorText); console.debug('Parsed error data:', errorData); } catch (e) { console.error('Error parsing response:', e); throw new DeliveryError(CONFIG.ERROR_MESSAGES.SERVER_ERROR, 'SERVER_ERROR', { status: response.status, statusText: response.statusText, errorText }); } // If we have a code and a corresponding message, use it if (errorData.code && CONFIG.ERROR_MESSAGES[errorData.code]) { console.debug('Using error message for code:', errorData.code); // Pass through all error data to make it available to the UI throw new DeliveryError(CONFIG.ERROR_MESSAGES[errorData.code], errorData.code, errorData); } // If we get here, we didn't recognize the error code console.debug('Unrecognized error code:', errorData.code); throw new DeliveryError(CONFIG.ERROR_MESSAGES.SERVER_ERROR, 'SERVER_ERROR', { status: response.status, statusText: response.statusText, errorText }); } const data = await response.json(); if (!data) throw new DeliveryError('Empty response received', 'EMPTY_RESPONSE'); return data; } catch (error) { // Only log API level errors if they're not DeliveryErrors if (!(error instanceof DeliveryError)) { console.error('API Error:', error); } throw error; } } } // Main Application class DeliveryApp { constructor() { this.ui = new UIManager(); this.api = new APIClient(); this.validateEnvironment(); this.domain_settings = null; // Store domain settings for event logging this.terms = null; // Store terms for event logging this.fallback_urls = []; // Store fallback URLs } validateEnvironment() { if (!window.fetch) { throw new DeliveryError(CONFIG.ERROR_MESSAGES.FETCH_UNSUPPORTED, 'FETCH_UNSUPPORTED'); } } loadAFDScript() { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = CONFIG.AFD_SCRIPT; script.type = 'text/javascript'; script.onload = resolve; script.onerror = () => { console.error('Failed to load AFD script'); // Log script load failure this.reportEvent('error:browser:caf:script:load_failed', { script: CONFIG.AFD_SCRIPT }); // Attempt fallback delivery // this.attemptFallbackDelivery(); // reject(new Error('Failed to load AFD script')); reject(new DeliveryError(CONFIG.ERROR_MESSAGES.AD_BLOCK_DETECTED, 'AD_BLOCK_DETECTED')); }; document.head.appendChild(script); }); } // Event logging client reportEvent = async (eventName, context) => { try { const response = await fetch(CONFIG.ENDPOINTS.EVENT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify({ event: eventName, event_data: { context, domain_settings: this.domain_settings } }) }); if (!response.ok) throw new Error('Event reporting failed'); console.debug('Event logged:', eventName); } catch (error) { console.error('Event logging error:', error); } } attemptFallbackDelivery = () => { if (this.fallback_urls && this.fallback_urls.length > 0) { const fallbackUrl = this.fallback_urls[0]; console.log('Attempting fallback delivery to:', fallbackUrl); // Log the fallback attempt this.reportEvent('event:browser:fallback:attempt', { fallback_url: fallbackUrl }); // Redirect to the fallback URL window.location.href = fallbackUrl; } } handlePageLoadedCallback = (requestAccepted, status) => { console.log('AFD Page Loaded:', { requestAccepted, status }); if (!requestAccepted) { console.error('AFD request not accepted:', status); if (status.error_code) { console.error('AFD Error Code:', status.error_code); this.reportEvent('error:browser:caf:page', { error_code: status.error_code, status }); } if (status.faillisted) { console.error('Domain Faillisted. Reason:', status.faillistReason); this.reportEvent('error:browser:caf:page:faillisted', { reason: status.faillistReason, status }); } if (status.adult) { this.reportEvent('error:browser:caf:page:adult', { status }); // Navigate directly to /_a for adult domain window.location.pathname = CONFIG.ENDPOINTS.ADULT; return; } // For non-adult failures, redirect to fallback endpoint window.location.pathname = CONFIG.ENDPOINTS.FALLBACK; } else { // Log successful page load this.reportEvent('event:browser:caf:page:loaded', { status, client: status.client, feed: status.feed, user_search: status.user_search, query: status.query }); } } handleBlockLoadedCallback = (containerName, adsLoaded, isExperimentVariant, callbackOptions) => { console.log('AFD Block Loaded:', { container: containerName, adsLoaded, isExperimentVariant, options: callbackOptions }); if (!adsLoaded) { console.warn('No ads loaded for container:', containerName); // Log failed block load with specific reasons if available if (callbackOptions?.cafStatus?.adult) { this.reportEvent('error:browser:caf:block:adult', { container: containerName, isExperimentVariant, terms: this.terms, callbackOptions }); // Navigate directly to /_a for adult domain window.location.pathname = CONFIG.ENDPOINTS.ADULT; return; } else if (callbackOptions?.cafStatus?.faillisted) { this.reportEvent('error:browser:caf:block:faillisted', { container: containerName, isExperimentVariant, terms: this.terms, callbackOptions }); } else { this.reportEvent('error:browser:caf:block:failed', { container: containerName, isExperimentVariant, terms: this.terms, callbackOptions }); } // For non-adult failures, redirect to fallback endpoint window.location.pathname = CONFIG.ENDPOINTS.FALLBACK; } else { // Transform termPositions format and include terms in event data if (callbackOptions?.termPositions) { callbackOptions.termPositions = Object.entries(callbackOptions.termPositions) .map(([term, position]) => ({ term, position })); } this.reportEvent('event:browser:caf:block:loaded', { container: containerName, isExperimentVariant, terms: this.terms, callbackOptions }); if (containerName === 'rs') { this.reportEvent('event:browser:caf:rs:loaded', { container: containerName, isExperimentVariant, terms: this.terms, callbackOptions }); } } } applyAFDStyles(colors) { if (!colors) { console.warn('No AFD colors provided'); return; } console.log('Applying AFD theme colors:', colors); // Create or update AFD stylesheet let styleEl = document.getElementById('afd-theme'); if (!styleEl) { styleEl = document.createElement('style'); styleEl.id = 'afd-theme'; document.head.appendChild(styleEl); } // Build CSS rules to match AFD's styling const cssRules = ` :root { ${Object.entries(colors).map(([key, value]) => `${key}: ${value};`).join('\n ')} } body { background-color: var(--main-bg-color); color: var(--main-text-color); font-family: var(--font-family); } a:not([class*="afd"]) { color: var(--link-color); } header h1 { color: var(--main-text-color); } #custom-text { color: var(--main-text-color); } `; styleEl.textContent = cssRules; console.log('Applied AFD theme', { bgColor: getComputedStyle(document.body).backgroundColor, textColor: getComputedStyle(document.body).color, fontFamily: getComputedStyle(document.body).fontFamily }); } initializeAFD(data) { if (!data.afd || !data.afd.client_id || !data.afd.drid || !data.afd.style_id) { throw new DeliveryError('Missing AFD configuration', 'AFD_CONFIG_ERROR'); } // Apply theme colors first so page looks consistent before AFD loads if (data.afd.colors) { this.applyAFDStyles(data.afd.colors); } console.log('Initializing AFD:', data.afd); // Store terms for event logging this.terms = data.afd.related_searches; // Construct base URL for results page (without AFD token and query) const url = new URL(window.location.href); url.searchParams.append('afd', '1'); const pageOptions = { 'pubId': data.afd.client_id, 'domainRegistrant': data.afd.drid, 'adtest': 'off', 'adsafe': 'low', 'channel': 'ch1', 'maxTermLength': 50, 'styleId': data.afd.style_id, // Use the predefined AFD style ID 'resultsPageBaseUrl': url.href, 'domainName': data.domain, 'hl': 'en', 'personalizedAds': false, 'ivt': data.afd?.ivt ?? false, 'terms': data.afd.related_searches, 'kw': data.afd.related_searches, 'pageLoadedCallback': this.handlePageLoadedCallback, 'clicktrackUrl': window.location.origin + '/_t?session_id=' + data.settings.session_id + '&type=' + (new URLSearchParams(window.location.search).get('afd') === '1' ? 'click' : 'query') + (new URLSearchParams(window.location.search).get('afd') === '1' ? '&query=' + encodeURIComponent(new URLSearchParams(window.location.search).get('query') || '') : '') }; console.log('AFD Page Options:', pageOptions); // Define shared variables const isSearchResultPage = new URLSearchParams(window.location.search).get('afd') === '1'; const isSpecialUser = data.settings?.user_id === '4cd64db9-4e2a-4fbc-a46b-e463e0dcd5f0'; const hasRelatedTerms = !!data.afd?.related_searches; const stage = isSearchResultPage ? 'ads' : 'rs'; const containerId = stage === 'ads' ? 'ads' : 'rs'; // Create containers based on user and terms status if (isSpecialUser && !hasRelatedTerms) { if (!document.getElementById('search')) { const searchContainer = document.createElement('div'); searchContainer.id = 'search'; document.getElementById('container').appendChild(searchContainer); } // Create ads container for search results if (isSearchResultPage && !document.getElementById('ads')) { const adsContainer = document.createElement('div'); adsContainer.id = 'ads'; document.getElementById('container').appendChild(adsContainer); } } else if (!document.getElementById(containerId)) { const container = document.createElement('div'); container.id = containerId; document.getElementById('container').appendChild(container); } // Initialize AFD blocks based on settings const blocks = []; // For user_id 32 domains without related terms if (isSpecialUser && !hasRelatedTerms) { // Always add search box blocks.push({ 'container': 'search', 'type': 'searchbox', 'linkTarget': '_blank', 'adLoadedCallback': this.handleBlockLoadedCallback, // Modern search box styling with hex colors matching our theme 'colorSearchButton': '#76ABAE', 'colorSearchButtonText': '#FFFFFF', 'fontFamily': 'Arial, sans-serif', 'heightSearchButton': 45, 'heightSearchInput': 45, 'radiusSearchInputBorder': 8, 'hideSearchButtonBorder': true, 'hideSearchInputBorder': true, 'colorSearchButtonBorder': '#76ABAE', 'fontSizeSearchButton': 16, 'fontSizeSearchInput': 24, // 'widthSearchInput': 400, // 'widthSearchButton': 120 }); // Add ads block if this is a search result page if (isSearchResultPage) { blocks.push({ 'container': 'ads', 'type': 'ads', 'number': 3, 'linkTarget': '_blank', 'adLoadedCallback': this.handleBlockLoadedCallback }); } } else { // Add normal stage-specific block (ads or relatedsearch) blocks.push({ 'container': containerId, 'type': stage === 'ads' ? 'ads' : 'relatedsearch', 'number': 3, 'linkTarget': '_blank', 'adLoadedCallback': this.handleBlockLoadedCallback }); } // Initialize AFD with configured blocks new google.ads.domains.Caf(pageOptions, blocks); } async initialize() { try { const data = await this.api.fetchPageData(); // Check for contact.only flag and redirect if true if (data.contact && data.contact.only === true) { window.location.pathname = CONFIG.ENDPOINTS.CONTACT; return; } // Initialize UI elements regardless of delivery status if (data.settings?.display) { this.ui.setPageTitle( data.settings.display.title, data.settings.display.show_title ); // Apply any custom CSS if (data.settings.display.custom_css) { const styleEl = document.createElement('style'); styleEl.id = 'custom-styles'; styleEl.textContent = data.settings.display.custom_css; document.head.appendChild(styleEl); } // Apply any custom javascript from settings if (data.settings.display.custom_js) { const scriptEl = document.createElement('script'); scriptEl.textContent = data.settings.display.custom_js; document.body.appendChild(scriptEl); } // Apply any custom text content if (data.settings.display.custom_text) { const textEl = document.createElement('div'); textEl.id = 'custom-text'; textEl.innerHTML = data.settings.display.custom_text; textEl.style.position = data.settings.display.custom_text_position || 'static'; document.body.appendChild(textEl); } } // Configure contact message if present if (data.contact) { this.ui.configureContactMessage(data.contact); } // Store domain settings for event logging if available if (data.domain_id && data.settings?.user_id) { this.domain_settings = { domain_id: data.domain_id, user_uuid4: data.settings.user_id, sets: [{ delivery: { can_tier1: true, tier1: { drid: data.afd?.drid } } }] }; } // Store fallback URLs from delivery response this.fallback_urls = data.delivery.fallback_list || []; // Handle special display settings for user_id 32 const isSpecialUser = data.settings?.user_id === '4cd64db9-4e2a-4fbc-a46b-e463e0dcd5f0'; const hasRelatedTerms = !!data.afd?.related_searches; // Hide related search and show search box only for special user without terms if (isSpecialUser && !hasRelatedTerms) { const rsDiv = document.getElementById('rs'); if (rsDiv) rsDiv.style.display = 'none'; const searchDiv = document.getElementById('search'); if (searchDiv) searchDiv.style.display = 'block'; } // Handle delivery method if (data.delivery.method === 'redirect') { // For redirect type, simply navigate to the provided destination window.location.href = data.delivery.destination; return; // Stop further processing since we're redirecting } else if (data.delivery.method === 'afd') { await this.loadAFDScript(); await this.initializeAFD(data); } else if (data.delivery.destination) { // Get current redirect count from URL parameters const urlParams = new URLSearchParams(window.location.search); const redirectCount = parseInt(urlParams.get('rc') || '0'); // Check for potential redirect loop if (redirectCount >= 1) { throw new DeliveryError('Domain appears to be misconfigured. Please check the DNS settings.', 'REDIRECT_LOOP_ERROR'); } // Add or increment redirect count parameter const destinationUrl = new URL(data.delivery.destination); destinationUrl.searchParams.set('rc', (redirectCount + 1).toString()); // Redirect to the destination with updated count console.log('Redirecting to:', destinationUrl.href); window.location.href = destinationUrl.href; return; // Stop further processing since we're redirecting } else { // No suitable destination found throw new DeliveryError('Unable to find a suitable destination for this domain.', 'NO_DESTINATION_ERROR'); } // Show the container after successful initialization this.ui.showContainer(); } catch (error) { // Only log full error details if it's not a DeliveryError if (!(error instanceof DeliveryError)) { console.error('Initialization error:', error); } // Try to extract settings from error response if available if (error instanceof DeliveryError && error.details && error.code === 'NO_CHANNELS') { const errorData = error.details; if (errorData.settings?.display) { this.ui.setPageTitle( errorData.settings.display.title, errorData.settings.display.show_title ); // Apply any custom CSS if (errorData.settings.display.custom_css) { const styleEl = document.createElement('style'); styleEl.id = 'custom-styles'; styleEl.textContent = errorData.settings.display.custom_css; document.head.appendChild(styleEl); } // Apply any custom javascript from settings if (errorData.settings.display.custom_js) { const scriptEl = document.createElement('script'); scriptEl.textContent = errorData.settings.display.custom_js; document.body.appendChild(scriptEl); } // Apply any custom text content if (errorData.settings.display.custom_text) { const textEl = document.createElement('div'); textEl.id = 'custom-text'; textEl.innerHTML = errorData.settings.display.custom_text; textEl.style.position = errorData.settings.display.custom_text_position || 'static'; document.body.appendChild(textEl); } } // Configure contact message if present in error response if (errorData.contact) { this.ui.configureContactMessage(errorData.contact); } } const errorMessage = error instanceof DeliveryError ? error.message : CONFIG.ERROR_MESSAGES.INITIALIZATION_ERROR; this.ui.showError(errorMessage); // Make sure container is visible even on error this.ui.showContainer(); } } } // Initialize the application when DOM is ready document.addEventListener('DOMContentLoaded', () => { const app = new DeliveryApp(); app.initialize().catch(error => { console.error('Application initialization failed:', error); const errorMessage = error instanceof DeliveryError ? error.message : CONFIG.ERROR_MESSAGES.INITIALIZATION_ERROR; app.ui.showError(errorMessage); }); });