Production-ready prompts, scripts, frameworks and AI agents for Google Ads professionals. No payment required.
Quality Score punishes mismatched ad-to-LP promises and never tells you which ones. I built this to find the leaks before the platform downgrades the CTR.
Quality Score quietly punishes ads that promise something the landing page does not deliver. The platform never tells you which ads are mismatched, just that your QS dropped. This script extracts themes from every RSA, compares them to the final URL path, flags the misses, and sorts the result by spend so the most expensive leaks come first.
SPREADSHEET_URL as 'CREATE_NEW' for a fresh sheet, set MINIMUM_IMPRESSIONS (default 100) and MINIMUM_COST (default $10) to filter low-traffic noise, and tune MIN_ALIGNMENT_SCORE (default 0.3, lower means stricter). The big lever is THEME_KEYWORDS, add your industry’s product or service words so the matcher knows what to look for./******************************************************************************
* LANDING PAGE AD MISMATCH DETECTOR
*
* Generated by PPC.io Script Engine
* https://ppc.io
*
* Purpose: Flag where final URLs don't match ad copy themes
* Author: PPC.io
* Version: 2.0
* Updated: 2025-01-13
*
* SETUP INSTRUCTIONS:
* 1. Set SPREADSHEET_URL to 'CREATE_NEW' or paste existing URL
* 2. Customize THEME_KEYWORDS if needed for your industry
* 3. Run in Preview mode first to verify
* 4. Schedule: Weekly recommended
*
* USE CASE: "Which ad/landing page mismatches are hurting Quality Score most?"
*
* HOW IT WORKS:
* 1. Extracts key themes from ad headlines and descriptions
* 2. Analyzes landing page URLs for matching themes
* 3. Flags mismatches where ad themes don't appear in URL path
* 4. Prioritizes by spend to focus on high-impact fixes
*
* CHANGELOG:
* v2.0 - Added numbered sheets, alignment score, enhanced AI prompts
* v1.0 - Initial release
*
******************************************************************************/
/******************************************************************************
* CONFIGURATION - Adjust these values for your account
******************************************************************************/
var CONFIG = {
// ═══════════════════════════════════════════════════════════════════════════
// OUTPUT SETTINGS
// ═══════════════════════════════════════════════════════════════════════════
SPREADSHEET_URL: 'CREATE_NEW', // Or paste existing spreadsheet URL
// Email alerts (leave empty array to disable)
EMAIL_RECIPIENTS: [],
// Slack webhook (leave empty to disable)
SLACK_WEBHOOK_URL: '',
// ═══════════════════════════════════════════════════════════════════════════
// DATE RANGE
// ═══════════════════════════════════════════════════════════════════════════
DATE_RANGE: 'LAST_30_DAYS',
// ═══════════════════════════════════════════════════════════════════════════
// FILTERS
// ═══════════════════════════════════════════════════════════════════════════
CAMPAIGN_NAME_CONTAINS: '', // Filter campaigns (empty = all)
CAMPAIGN_NAME_EXCLUDES: '', // Exclude campaigns containing this
INCLUDE_PAUSED_ADS: false,
MINIMUM_IMPRESSIONS: 100, // Only check ads with significant traffic
MINIMUM_COST: 10, // Focus on ads with real spend
// ═══════════════════════════════════════════════════════════════════════════
// MISMATCH DETECTION SETTINGS
// ═══════════════════════════════════════════════════════════════════════════
// Minimum theme match score (0-1) to consider aligned
// Lower = stricter (more mismatches flagged)
MIN_ALIGNMENT_SCORE: 0.3,
// Check if landing page is homepage (often a red flag)
FLAG_HOMEPAGE_URLS: true,
// Industry-specific theme keywords to look for
// Add your own product/service keywords
THEME_KEYWORDS: {
// Service-based
'emergency': ['emergency', 'urgent', '24-7', '24/7', 'same-day'],
'free': ['free', 'no-cost', 'complimentary'],
'quote': ['quote', 'estimate', 'pricing', 'cost'],
// E-commerce
'sale': ['sale', 'discount', 'deal', 'offer', 'save'],
'shipping': ['shipping', 'delivery', 'ship'],
// B2B
'demo': ['demo', 'trial', 'test'],
'enterprise': ['enterprise', 'business', 'corporate'],
// General
'buy': ['buy', 'purchase', 'order', 'shop'],
'compare': ['compare', 'vs', 'versus', 'best']
},
// ═══════════════════════════════════════════════════════════════════════════
// EXECUTION SETTINGS
// ═══════════════════════════════════════════════════════════════════════════
LOG_LEVEL: 'INFO', // DEBUG, INFO, WARN, ERROR
TIME_LIMIT_MINUTES: 25, // Exit gracefully before this (max 30)
BATCH_SIZE: 500 // Rows per spreadsheet write
};
/******************************************************************************
* MAIN EXECUTION
******************************************************************************/
function main() {
var startTime = new Date();
log('INFO', 'Landing Page Mismatch Detector started: ' + startTime.toISOString());
try {
var ss = initializeSpreadsheet();
var results = detectMismatches(startTime);
writeAllSheets(ss, results);
sendNotifications(results, ss.getUrl(), startTime);
logSummary(results, startTime);
} catch (error) {
handleFatalError(error, startTime);
}
}
/******************************************************************************
* MISMATCH DETECTION
******************************************************************************/
function detectMismatches(startTime) {
var results = {
mismatches: [],
aligned: [],
homepageUrls: [],
allAds: [],
summary: {
totalAds: 0,
mismatchCount: 0,
alignedCount: 0,
homepageCount: 0,
mismatchSpend: 0,
topMismatchThemes: {}
}
};
// Get all ads with their landing pages
var adsData = collectAdData(startTime);
for (var i = 0; i < adsData.length; i++) {
var ad = adsData[i];
results.summary.totalAds++;
// Analyze alignment
var analysis = analyzeAlignment(ad);
var adResult = {
'Campaign': ad.campaign,
'Ad Group': ad.adGroup,
'Ad ID': ad.adId,
'Final URL': ad.finalUrl,
'URL Path': getUrlPath(ad.finalUrl),
'Headlines': ad.headlines.join(' | '),
'Descriptions': ad.descriptions.slice(0, 2).join(' | '),
'Ad Themes': analysis.adThemes.join(', '),
'URL Themes': analysis.urlThemes.join(', '),
'Alignment Score': analysis.score,
'Is Homepage': analysis.isHomepage,
'Mismatch Type': analysis.mismatchType,
'Impressions': ad.impressions,
'Clicks': ad.clicks,
'Cost': ad.cost,
'Conversions': ad.conversions,
'CTR': ad.ctr,
'Conv Rate': ad.convRate,
'Quality Score Impact': getQualityScoreImpact(analysis),
'Recommendation': getRecommendation(analysis, ad)
};
results.allAds.push(adResult);
// Categorize
if (analysis.isHomepage && CONFIG.FLAG_HOMEPAGE_URLS) {
results.homepageUrls.push(adResult);
results.summary.homepageCount++;
}
if (analysis.score < CONFIG.MIN_ALIGNMENT_SCORE) {
results.mismatches.push(adResult);
results.summary.mismatchCount++;
results.summary.mismatchSpend += ad.cost;
// Track mismatch themes
for (var t = 0; t < analysis.adThemes.length; t++) {
var theme = analysis.adThemes[t];
if (!results.summary.topMismatchThemes[theme]) {
results.summary.topMismatchThemes[theme] = 0;
}
results.summary.topMismatchThemes[theme]++;
}
} else {
results.aligned.push(adResult);
results.summary.alignedCount++;
}
if ((i + 1) % 100 === 0) {
log('DEBUG', 'Analyzed ' + (i + 1) + ' ads');
checkTimeLimit(startTime);
}
}
// Sort mismatches by cost (highest first - biggest impact)
results.mismatches.sort(function(a, b) { return b.Cost - a.Cost; });
results.homepageUrls.sort(function(a, b) { return b.Cost - a.Cost; });
log('INFO', 'Found ' + results.summary.mismatchCount + ' mismatches out of ' +
results.summary.totalAds + ' ads');
return results;
}
function collectAdData(startTime) {
var ads = [];
// Use GAQL to get RSA data with final URLs
var query = "SELECT " +
"campaign.name, " +
"ad_group.name, " +
"ad_group_ad.ad.id, " +
"ad_group_ad.ad.final_urls, " +
"ad_group_ad.ad.responsive_search_ad.headlines, " +
"ad_group_ad.ad.responsive_search_ad.descriptions, " +
"metrics.impressions, " +
"metrics.clicks, " +
"metrics.cost_micros, " +
"metrics.conversions, " +
"metrics.ctr, " +
"metrics.conversions_from_interactions_rate " +
"FROM ad_group_ad " +
"WHERE ad_group_ad.ad.type = 'RESPONSIVE_SEARCH_AD' " +
"AND campaign.advertising_channel_type = 'SEARCH' " +
"AND metrics.impressions >= " + CONFIG.MINIMUM_IMPRESSIONS + " " +
"AND metrics.cost_micros >= " + (CONFIG.MINIMUM_COST * 1000000) + " " +
"AND segments.date DURING " + CONFIG.DATE_RANGE;
if (!CONFIG.INCLUDE_PAUSED_ADS) {
query += " AND ad_group_ad.status = 'ENABLED'";
query += " AND campaign.status = 'ENABLED'";
query += " AND ad_group.status = 'ENABLED'";
}
try {
var report = AdsApp.report(query);
var rows = report.rows();
while (rows.hasNext()) {
var row = rows.next();
var campaignName = row['campaign.name'];
// Apply filters
if (CONFIG.CAMPAIGN_NAME_CONTAINS &&
campaignName.toLowerCase().indexOf(CONFIG.CAMPAIGN_NAME_CONTAINS.toLowerCase()) === -1) {
continue;
}
if (CONFIG.CAMPAIGN_NAME_EXCLUDES &&
campaignName.toLowerCase().indexOf(CONFIG.CAMPAIGN_NAME_EXCLUDES.toLowerCase()) !== -1) {
continue;
}
var finalUrls = row['ad_group_ad.ad.final_urls'];
var finalUrl = Array.isArray(finalUrls) ? finalUrls[0] : (finalUrls || '');
var headlines = parseAssetText(row['ad_group_ad.ad.responsive_search_ad.headlines']);
var descriptions = parseAssetText(row['ad_group_ad.ad.responsive_search_ad.descriptions']);
var costMicros = parseInt(row['metrics.cost_micros'], 10) || 0;
ads.push({
campaign: campaignName,
adGroup: row['ad_group.name'],
adId: row['ad_group_ad.ad.id'],
finalUrl: finalUrl,
headlines: headlines,
descriptions: descriptions,
impressions: parseInt(row['metrics.impressions'], 10) || 0,
clicks: parseInt(row['metrics.clicks'], 10) || 0,
cost: costMicros / 1000000,
conversions: parseFloat(row['metrics.conversions']) || 0,
ctr: parseFloat(row['metrics.ctr']) || 0,
convRate: parseFloat(row['metrics.conversions_from_interactions_rate']) || 0
});
}
} catch (e) {
log('ERROR', 'GAQL query failed: ' + e.message);
// Fallback to standard API
collectAdDataFallback(ads, startTime);
}
log('INFO', 'Collected ' + ads.length + ' ads for analysis');
return ads;
}
function collectAdDataFallback(ads, startTime) {
log('INFO', 'Using fallback method for ad collection');
var adIterator = AdsApp.ads()
.withCondition('Type = RESPONSIVE_SEARCH_AD')
.withCondition('Status = ENABLED')
.withCondition('CampaignStatus = ENABLED')
.withCondition('AdGroupStatus = ENABLED')
.withCondition('Impressions >= ' + CONFIG.MINIMUM_IMPRESSIONS)
.forDateRange(CONFIG.DATE_RANGE)
.get();
while (adIterator.hasNext()) {
var ad = adIterator.next();
var campaign = ad.getCampaign();
var campaignName = campaign.getName();
if (CONFIG.CAMPAIGN_NAME_CONTAINS &&
campaignName.toLowerCase().indexOf(CONFIG.CAMPAIGN_NAME_CONTAINS.toLowerCase()) === -1) {
continue;
}
var stats = ad.getStatsFor(CONFIG.DATE_RANGE);
if (stats.getCost() < CONFIG.MINIMUM_COST) continue;
var urls = ad.urls();
var finalUrl = urls.getFinalUrl() || '';
// Get RSA headlines and descriptions
var headlines = [];
var descriptions = [];
try {
var rsa = ad.asType().responsiveSearchAd();
headlines = rsa.getHeadlines ? rsa.getHeadlines() : [];
descriptions = rsa.getDescriptions ? rsa.getDescriptions() : [];
// Convert to strings if needed
headlines = headlines.map(function(h) { return typeof h === 'string' ? h : h.text || ''; });
descriptions = descriptions.map(function(d) { return typeof d === 'string' ? d : d.text || ''; });
} catch (e) {
log('DEBUG', 'Could not get RSA assets: ' + e.message);
}
ads.push({
campaign: campaignName,
adGroup: ad.getAdGroup().getName(),
adId: ad.getId(),
finalUrl: finalUrl,
headlines: headlines,
descriptions: descriptions,
impressions: stats.getImpressions(),
clicks: stats.getClicks(),
cost: stats.getCost(),
conversions: stats.getConversions(),
ctr: stats.getCtr(),
convRate: stats.getConversionRate()
});
checkTimeLimit(startTime);
}
}
function parseAssetText(assetsRaw) {
var texts = [];
if (!assetsRaw) return texts;
if (typeof assetsRaw === 'string') {
try {
var parsed = JSON.parse(assetsRaw);
if (Array.isArray(parsed)) {
for (var i = 0; i < parsed.length; i++) {
if (parsed[i].text) {
texts.push(parsed[i].text);
}
}
}
} catch (e) {
// Try regex extraction
var matches = assetsRaw.match(/text:\s*"([^"]+)"/g);
if (matches) {
for (var j = 0; j < matches.length; j++) {
var text = matches[j].replace(/text:\s*"/, '').replace(/"$/, '');
texts.push(text);
}
}
}
} else if (Array.isArray(assetsRaw)) {
for (var k = 0; k < assetsRaw.length; k++) {
if (assetsRaw[k].text) {
texts.push(assetsRaw[k].text);
} else if (typeof assetsRaw[k] === 'string') {
texts.push(assetsRaw[k]);
}
}
}
return texts;
}
function analyzeAlignment(ad) {
var analysis = {
adThemes: [],
urlThemes: [],
score: 0,
isHomepage: false,
mismatchType: 'ALIGNED'
};
// Check if homepage
var urlPath = getUrlPath(ad.finalUrl);
analysis.isHomepage = isHomepageUrl(ad.finalUrl);
// Extract themes from ad copy
var adText = (ad.headlines.join(' ') + ' ' + ad.descriptions.join(' ')).toLowerCase();
analysis.adThemes = extractThemes(adText);
// Extract themes from URL
var urlText = urlPath.toLowerCase().replace(/[-_\/]/g, ' ');
analysis.urlThemes = extractThemes(urlText);
// Also check URL for direct keyword matches
var urlKeywords = urlText.split(/\s+/).filter(function(w) { return w.length > 2; });
for (var i = 0; i < urlKeywords.length; i++) {
if (analysis.urlThemes.indexOf(urlKeywords[i]) === -1) {
// Check if it's a meaningful word in the ad
if (adText.indexOf(urlKeywords[i]) !== -1) {
analysis.urlThemes.push(urlKeywords[i]);
}
}
}
// Calculate alignment score
if (analysis.adThemes.length === 0) {
analysis.score = 0.5; // No strong themes detected
analysis.mismatchType = 'NO_THEMES';
} else if (analysis.isHomepage) {
// Homepage gets lower score unless very generic ad
analysis.score = 0.2;
analysis.mismatchType = 'HOMEPAGE';
} else {
// Calculate overlap
var matchCount = 0;
for (var j = 0; j < analysis.adThemes.length; j++) {
var theme = analysis.adThemes[j];
// Check if theme or related words appear in URL
if (urlText.indexOf(theme) !== -1) {
matchCount++;
} else {
// Check theme keywords
var related = CONFIG.THEME_KEYWORDS[theme];
if (related) {
for (var k = 0; k < related.length; k++) {
if (urlText.indexOf(related[k]) !== -1) {
matchCount += 0.5;
break;
}
}
}
}
}
analysis.score = analysis.adThemes.length > 0 ? matchCount / analysis.adThemes.length : 0.5;
if (analysis.score < CONFIG.MIN_ALIGNMENT_SCORE) {
analysis.mismatchType = 'THEME_MISMATCH';
}
}
return analysis;
}
function extractThemes(text) {
var themes = [];
var textLower = text.toLowerCase();
// Check for configured theme keywords
for (var theme in CONFIG.THEME_KEYWORDS) {
var keywords = CONFIG.THEME_KEYWORDS[theme];
for (var i = 0; i < keywords.length; i++) {
if (textLower.indexOf(keywords[i]) !== -1) {
if (themes.indexOf(theme) === -1) {
themes.push(theme);
}
break;
}
}
}
// Extract significant words (potential themes)
var words = textLower.replace(/[^a-z0-9\s]/g, ' ').split(/\s+/);
var stopWords = ['a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
'of', 'with', 'by', 'from', 'your', 'our', 'we', 'you', 'get', 'now',
'today', 'best', 'top', 'new', 'more', 'all', 'any', 'call', 'click'];
for (var j = 0; j < words.length; j++) {
var word = words[j];
if (word.length > 4 && stopWords.indexOf(word) === -1 && themes.indexOf(word) === -1) {
themes.push(word);
}
}
return themes.slice(0, 5); // Limit to top 5 themes
}
function getUrlPath(url) {
if (!url) return '';
try {
var match = url.match(/https?:\/\/[^\/]+(\/[^?#]*)?/);
return match && match[1] ? match[1] : '/';
} catch (e) {
return '/';
}
}
function isHomepageUrl(url) {
if (!url) return true;
var path = getUrlPath(url);
return path === '/' || path === '' || path === '/index' || path === '/home';
}
function getQualityScoreImpact(analysis) {
if (analysis.score >= 0.7) return 'Positive';
if (analysis.score >= 0.4) return 'Neutral';
if (analysis.score >= 0.2) return 'Negative';
return 'Severe';
}
function getRecommendation(analysis, ad) {
if (analysis.mismatchType === 'ALIGNED') {
return 'Good alignment - no action needed';
}
if (analysis.mismatchType === 'HOMEPAGE') {
return 'Create dedicated landing page for: ' + analysis.adThemes.slice(0, 2).join(', ');
}
if (analysis.mismatchType === 'THEME_MISMATCH') {
return 'Update URL to include: ' + analysis.adThemes.slice(0, 2).join(', ') +
' OR update ad copy to match: ' + analysis.urlThemes.slice(0, 2).join(', ');
}
if (analysis.mismatchType === 'NO_THEMES') {
return 'Add specific themes to ad copy and ensure landing page matches';
}
return 'Review manually';
}
/******************************************************************************
* OUTPUT FUNCTIONS
******************************************************************************/
function initializeSpreadsheet() {
var ss;
if (!CONFIG.SPREADSHEET_URL || CONFIG.SPREADSHEET_URL === 'YOUR_SPREADSHEET_URL_HERE' || CONFIG.SPREADSHEET_URL === 'CREATE_NEW') {
ss = SpreadsheetApp.create('PPC.io LP Mismatch - ' +
AdsApp.currentAccount().getName() + ' - ' +
formatDate(new Date()));
log('INFO', 'Created spreadsheet: ' + ss.getUrl());
} else {
ss = SpreadsheetApp.openByUrl(CONFIG.SPREADSHEET_URL);
}
return ss;
}
function writeAllSheets(ss, results) {
// Summary first (position 0)
writeSummarySheet(ss, results);
// Numbered sheets for clear navigation
if (results.mismatches.length > 0) {
writeSheet(ss, '2. Mismatches', results.mismatches,
['Campaign', 'Ad Group', 'Final URL', 'Headlines', 'Ad Themes', 'URL Themes',
'Alignment Score', 'Mismatch Type', 'Cost', 'Conversions', 'Quality Score Impact', 'Recommendation']);
}
if (results.homepageUrls.length > 0) {
writeSheet(ss, '3. Homepage URLs', results.homepageUrls,
['Campaign', 'Ad Group', 'Final URL', 'Headlines', 'Ad Themes',
'Cost', 'Conversions', 'Recommendation']);
}
if (results.allAds.length > 0) {
writeSheet(ss, '4. All Ads', results.allAds,
['Campaign', 'Ad Group', 'Final URL', 'URL Path', 'Headlines',
'Ad Themes', 'URL Themes', 'Alignment Score', 'Is Homepage',
'Impressions', 'Clicks', 'Cost', 'CTR']);
}
}
function writeSummarySheet(ss, results) {
var sheet = ss.getSheetByName('1. Summary');
if (!sheet) {
sheet = ss.insertSheet('1. Summary', 0);
} else {
sheet.clear();
}
var alignmentRate = results.summary.totalAds > 0 ?
((results.summary.alignedCount / results.summary.totalAds) * 100).toFixed(1) : 0;
var data = [
['LANDING PAGE MISMATCH DETECTOR', ''],
['Generated by PPC.io Script Engine', ''],
['https://ppc.io', ''],
['', ''],
['Account: ' + AdsApp.currentAccount().getName(), ''],
['Date Range: ' + CONFIG.DATE_RANGE, ''],
['Export Date: ' + new Date().toISOString(), ''],
['', ''],
['═══════════════════════════════════════════════════════════════', ''],
['OVERVIEW', ''],
['═══════════════════════════════════════════════════════════════', ''],
['Total Ads Analyzed', results.summary.totalAds],
['Aligned (Good)', results.summary.alignedCount],
['Mismatched (Action Needed)', results.summary.mismatchCount],
['Homepage URLs', results.summary.homepageCount],
['Alignment Rate', alignmentRate + '%'],
['', ''],
['═══════════════════════════════════════════════════════════════', ''],
['IMPACT', ''],
['═══════════════════════════════════════════════════════════════', ''],
['Spend on Mismatched Ads', '$' + results.summary.mismatchSpend.toFixed(2)],
['Estimated QS Impact', results.summary.mismatchCount > 10 ? 'Significant' :
results.summary.mismatchCount > 5 ? 'Moderate' : 'Minor'],
['', ''],
['═══════════════════════════════════════════════════════════════', ''],
['TOP MISMATCHED THEMES (ad themes not found in URLs)', ''],
['═══════════════════════════════════════════════════════════════', '']
];
// Sort themes by count
var themeEntries = [];
for (var theme in results.summary.topMismatchThemes) {
themeEntries.push({ theme: theme, count: results.summary.topMismatchThemes[theme] });
}
themeEntries.sort(function(a, b) { return b.count - a.count; });
if (themeEntries.length === 0) {
data.push(['No significant theme mismatches found', '']);
} else {
for (var i = 0; i < Math.min(10, themeEntries.length); i++) {
data.push([themeEntries[i].theme, themeEntries[i].count + ' occurrences']);
}
}
data.push(['', '']);
data.push(['═══════════════════════════════════════════════════════════════', '']);
data.push(['HOW TO FIX', '']);
data.push(['═══════════════════════════════════════════════════════════════', '']);
data.push(['1. Review "Mismatches" sheet, sorted by spend (highest impact first)', '']);
data.push(['2. For homepage URLs: Create dedicated landing pages', '']);
data.push(['3. For theme mismatches: Either update landing page URL/content OR update ad copy', '']);
data.push(['4. Re-run weekly to track improvements', '']);
data.push(['', '']);
data.push(['═══════════════════════════════════════════════════════════════', '']);
data.push(['AI ANALYSIS PROMPTS', '']);
data.push(['═══════════════════════════════════════════════════════════════', '']);
data.push(['1. "Which mismatches are costing us the most in Quality Score?"', '']);
data.push(['2. "Suggest landing page URL structures for these ad themes."', '']);
data.push(['3. "Prioritize fixes by estimated CPC savings."', '']);
sheet.getRange(1, 1, data.length, 2).setValues(data);
sheet.getRange(1, 1).setFontWeight('bold').setFontSize(14);
sheet.setColumnWidth(1, 450);
sheet.setColumnWidth(2, 250);
}
function writeSheet(ss, sheetName, data, columns) {
if (!data || data.length === 0) return;
var sheet = ss.getSheetByName(sheetName);
if (!sheet) {
sheet = ss.insertSheet(sheetName);
} else {
sheet.clear();
}
sheet.getRange(1, 1, 1, columns.length).setValues([columns]).setFontWeight('bold');
sheet.setFrozenRows(1);
var rows = data.map(function(row) {
return columns.map(function(col) {
var val = row[col];
return val !== null && val !== undefined ? val : '';
});
});
for (var i = 0; i < rows.length; i += CONFIG.BATCH_SIZE) {
var batch = rows.slice(i, Math.min(i + CONFIG.BATCH_SIZE, rows.length));
sheet.getRange(2 + i, 1, batch.length, columns.length).setValues(batch);
}
// Format
formatColumn(sheet, columns, 'Alignment Score', '0.00', rows.length);
formatColumn(sheet, columns, 'Cost', '$#,##0.00', rows.length);
formatColumn(sheet, columns, 'CTR', '0.00%', rows.length);
// Color code mismatch types
applyMismatchFormatting(sheet, columns, rows.length);
for (var col = 1; col <= Math.min(columns.length, 10); col++) {
sheet.autoResizeColumn(col);
}
}
function applyMismatchFormatting(sheet, headers, numRows) {
var scoreCol = headers.indexOf('Alignment Score') + 1;
if (scoreCol === 0) return;
var range = sheet.getRange(2, scoreCol, numRows, 1);
var values = range.getValues();
var colors = values.map(function(row) {
var score = parseFloat(row[0]) || 0;
if (score >= 0.7) return ['#d4edda'];
if (score >= 0.4) return ['#fff3cd'];
return ['#f8d7da'];
});
range.setBackgrounds(colors);
}
function formatColumn(sheet, headers, columnName, format, numRows) {
var colIndex = headers.indexOf(columnName);
if (colIndex !== -1) {
sheet.getRange(2, colIndex + 1, numRows, 1).setNumberFormat(format);
}
}
/******************************************************************************
* NOTIFICATION FUNCTIONS
******************************************************************************/
function sendNotifications(results, spreadsheetUrl, startTime) {
var duration = ((new Date() - startTime) / 1000).toFixed(1);
var message = [
'Landing Page Mismatch Analysis Complete',
'',
'Account: ' + AdsApp.currentAccount().getName(),
'Duration: ' + duration + 's',
'',
'Summary:',
'- Ads Analyzed: ' + results.summary.totalAds,
'- Mismatches Found: ' + results.summary.mismatchCount,
'- Homepage URLs: ' + results.summary.homepageCount,
'- Spend on Mismatches: $' + results.summary.mismatchSpend.toFixed(2),
'',
'Report: ' + spreadsheetUrl,
'',
'--',
'Generated by PPC.io Script Engine'
].join('\n');
if (CONFIG.EMAIL_RECIPIENTS && CONFIG.EMAIL_RECIPIENTS.length > 0) {
try {
MailApp.sendEmail({
to: CONFIG.EMAIL_RECIPIENTS.join(','),
subject: '[PPC.io] LP Mismatches - ' + results.summary.mismatchCount + ' found',
body: message
});
log('INFO', 'Email sent');
} catch (e) {
log('ERROR', 'Failed to send email: ' + e.message);
}
}
if (CONFIG.SLACK_WEBHOOK_URL) {
var emoji = results.summary.mismatchCount > 10 ? ':warning:' : ':white_check_mark:';
try {
UrlFetchApp.fetch(CONFIG.SLACK_WEBHOOK_URL, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({
text: emoji + ' *PPC.io LP Mismatch Detector*\n```' + message + '```'
})
});
log('INFO', 'Slack sent');
} catch (e) {
log('ERROR', 'Failed to send Slack: ' + e.message);
}
}
}
/******************************************************************************
* UTILITY FUNCTIONS
******************************************************************************/
function checkTimeLimit(startTime) {
var elapsed = (new Date() - startTime) / 1000 / 60;
if (elapsed > CONFIG.TIME_LIMIT_MINUTES) {
throw new Error('TIME_LIMIT: Stopped after ' + elapsed.toFixed(1) + ' minutes.');
}
}
function log(level, message) {
var levels = { 'DEBUG': 0, 'INFO': 1, 'WARN': 2, 'ERROR': 3 };
if (levels[level] >= levels[CONFIG.LOG_LEVEL]) {
Logger.log('[' + level + '] ' + message);
}
}
function logSummary(results, startTime) {
var duration = ((new Date() - startTime) / 1000).toFixed(1);
log('INFO', '════════════════════════════════════════');
log('INFO', 'LP MISMATCH DETECTION COMPLETE');
log('INFO', 'Duration: ' + duration + ' seconds');
log('INFO', 'Ads Analyzed: ' + results.summary.totalAds);
log('INFO', 'Mismatches: ' + results.summary.mismatchCount);
log('INFO', 'Homepage URLs: ' + results.summary.homepageCount);
log('INFO', '════════════════════════════════════════');
}
function handleFatalError(error, startTime) {
log('ERROR', '════════════════════════════════════════');
log('ERROR', 'FATAL ERROR: ' + error.message);
log('ERROR', 'Stack: ' + error.stack);
log('ERROR', '════════════════════════════════════════');
if (CONFIG.EMAIL_RECIPIENTS && CONFIG.EMAIL_RECIPIENTS.length > 0) {
try {
MailApp.sendEmail({
to: CONFIG.EMAIL_RECIPIENTS.join(','),
subject: '[PPC.io ERROR] LP Mismatch Detector Failed',
body: 'Error: ' + error.message + '\n\nStack:\n' + error.stack
});
} catch (e) {
log('ERROR', 'Could not send error email: ' + e.message);
}
}
}
function formatDate(date) {
return Utilities.formatDate(date, AdsApp.currentAccount().getTimeZone(), 'yyyy-MM-dd');
}
Skip the 1. Summary tab on the first read and go straight to 2. Mismatches. It is sorted by Cost descending. The top rows are the most expensive ads pointing at the wrong page. Look at Ad Themes vs URL Themes: where ad copy promises “free quote” but the URL path says /about-us, you have either a copy fix or a landing page fix. The Recommendation column tells you which. The 3. Homepage URLs tab is the laziest fix list, every ad pointing at / is a dedicated landing page waiting to be built.