serpbear/utils/generateEmail.ts
2024-11-09 21:24:27 +06:00

241 lines
11 KiB
TypeScript

import dayjs from 'dayjs';
import { readFile } from 'fs/promises';
import path from 'path';
import { getKeywordsInsight, getPagesInsight } from './insight';
import { readLocalSCData } from './searchConsole';
const serpBearLogo = 'https://erevanto.sirv.com/Images/serpbear/ikAdjQq.png';
const mobileIcon = 'https://erevanto.sirv.com/Images/serpbear/SqXD9rd.png';
const desktopIcon = 'https://erevanto.sirv.com/Images/serpbear/Dx3u0XD.png';
const googleIcon = 'https://erevanto.sirv.com/Images/serpbear/Sx3u0X9.png';
type SCStatsObject = {
[key:string]: {
html: string,
label: string,
clicks?: number,
impressions?: number
},
}
/**
* Generate Human readable Time string.
* @param {number} date - Keywords to scrape
* @returns {string}
*/
const timeSince = (date:number) : string => {
const seconds = Math.floor(((new Date().getTime() / 1000) - date));
let interval = Math.floor(seconds / 31536000);
if (interval > 1) return `${interval} years ago`;
interval = Math.floor(seconds / 2592000);
if (interval > 1) return `${interval} months ago`;
interval = Math.floor(seconds / 86400);
if (interval >= 1) return `${interval} days ago`;
interval = Math.floor(seconds / 3600);
if (interval >= 1) return `${interval} hrs ago`;
interval = Math.floor(seconds / 60);
if (interval > 1) return `${interval} mins ago`;
return `${Math.floor(seconds)} secs ago`;
};
/**
* Returns a Keyword's position change value by comparing the current position with previous position.
* @param {KeywordHistory} history - Keywords to scrape
* @param {number} position - Keywords to scrape
* @returns {number}
*/
const getPositionChange = (history:KeywordHistory, position:number) : number => {
let status = 0;
if (Object.keys(history).length >= 2) {
const historyArray = Object.keys(history).map((dateKey) => ({
date: new Date(dateKey).getTime(),
dateRaw: dateKey,
position: history[dateKey],
}));
const historySorted = historyArray.sort((a, b) => a.date - b.date);
const previousPos = historySorted[historySorted.length - 2].position;
status = previousPos === 0 ? position : previousPos - position;
if (position === 0 && previousPos > 0) {
status = previousPos - 100;
}
}
return status;
};
const getBestKeywordPosition = (history: KeywordHistory) => {
let bestPos;
if (Object.keys(history).length > 0) {
const historyArray = Object.keys(history).map((itemID) => ({ date: itemID, position: history[itemID] }))
.sort((a, b) => a.position - b.position).filter((el) => (el.position > 0));
if (historyArray[0]) {
bestPos = { ...historyArray[0] };
}
}
return bestPos?.position || '-';
};
/**
* Generate the Email HTML based on given domain name and its keywords
* @param {string} domainName - Keywords to scrape
* @param {keywords[]} keywords - Keywords to scrape
* @returns {Promise}
*/
const generateEmail = async (domainName:string, keywords:KeywordType[], settings: SettingsType) : Promise<string> => {
const emailTemplate = await readFile(path.join(__dirname, '..', '..', '..', '..', 'email', 'email.html'), { encoding: 'utf-8' });
const currentDate = dayjs(new Date()).format('MMMM D, YYYY');
const keywordsCount = keywords.length;
let improved = 0; let declined = 0;
let keywordsTable = '';
keywords.forEach((keyword) => {
let positionChangeIcon = '';
const positionChange = getPositionChange(keyword.history, keyword.position);
const deviceIconImg = keyword.device === 'desktop' ? desktopIcon : mobileIcon;
const countryFlag = `<img class="flag" src="https://flagcdn.com/w20/${keyword.country.toLowerCase()}.png" alt="${keyword.country}" title="${keyword.country}" />`;
const deviceIcon = `<img class="device" src="${deviceIconImg}" alt="${keyword.device}" title="${keyword.device}" width="18" height="18" />`;
if (positionChange > 0) { positionChangeIcon = '<span style="color:#5ed7c3;">▲</span>'; improved += 1; }
if (positionChange < 0) { positionChangeIcon = '<span style="color:#fca5a5;">▼</span>'; declined += 1; }
const posChangeIcon = positionChange ? `<span class="pos_change">${positionChangeIcon} ${positionChange}</span>` : '';
keywordsTable += `<tr class="keyword">
<td>${countryFlag} ${deviceIcon} ${keyword.keyword}</td>
<td>${keyword.position}${posChangeIcon}</td>
<td>${getBestKeywordPosition(keyword.history)}</td>
<td>${timeSince(new Date(keyword.lastUpdated).getTime() / 1000)}</td>
</tr>`;
});
const stat = `${improved > 0 ? `${improved} Improved` : ''}
${improved > 0 && declined > 0 ? ', ' : ''} ${declined > 0 ? `${declined} Declined` : ''}`;
const updatedEmail = emailTemplate
.replace('{{logo}}', `<img class="logo_img" src="${serpBearLogo}" alt="SerpBear" width="24" height="24" />`)
.replace('{{currentDate}}', currentDate)
.replace('{{domainName}}', domainName)
.replace('{{keywordsCount}}', keywordsCount.toString())
.replace('{{keywordsTable}}', keywordsTable)
.replace('{{appURL}}', process.env.NEXT_PUBLIC_APP_URL || '')
.replace('{{stat}}', stat)
.replace('{{preheader}}', stat);
const isConsoleIntegrated = !!(process.env.SEARCH_CONSOLE_PRIVATE_KEY && process.env.SEARCH_CONSOLE_CLIENT_EMAIL)
|| (settings.search_console_client_email && settings.search_console_private_key);
const htmlWithSCStats = isConsoleIntegrated ? await generateGoogeleConsoleStats(domainName) : '';
const emailHTML = updatedEmail.replace('{{SCStatsTable}}', htmlWithSCStats);
// await writeFile('testemail.html', emailHTML, { encoding: 'utf-8' });
return emailHTML;
};
/**
* Generate the Email HTML for Google Search Console Data.
* @param {string} domainName - The Domain name for which to generate the HTML.
* @returns {Promise<string>}
*/
const generateGoogeleConsoleStats = async (domainName:string): Promise<string> => {
if (!domainName) return '';
const localSCData = await readLocalSCData(domainName);
if (!localSCData || !localSCData.stats || !localSCData.stats.length) {
return ''; // IF No SC Data Found, Abot the process.
}
const scData:SCStatsObject = {
stats: { html: '', label: 'Performance for Last 7 Days', clicks: 0, impressions: 0 },
keywords: { html: '', label: 'Top 5 Keywords' },
pages: { html: '', label: 'Top 5 Pages' },
};
const SCStats = localSCData && localSCData.stats && Array.isArray(localSCData.stats) ? localSCData.stats.reverse().slice(0, 7) : [];
const keywords = getKeywordsInsight(localSCData, 'clicks', 'sevenDays');
const pages = getPagesInsight(localSCData, 'clicks', 'sevenDays');
const genColumn = (item:SCInsightItem, firstColumKey:string):string => {
return `<tr class="keyword">
<td>${item[firstColumKey as keyof SCInsightItem]}</td>
<td>${item.clicks}</td>
<td>${item.impressions}</td>
<td>${Math.round(item.position)}</td>
</tr>`;
};
if (SCStats.length > 0) {
scData.stats.html = SCStats.reduce((acc, item) => acc + genColumn(item, 'date'), '');
}
if (keywords.length > 0) {
scData.keywords.html = keywords.slice(0, 5).reduce((acc, item) => acc + genColumn(item, 'keyword'), '');
}
if (pages.length > 0) {
scData.pages.html = pages.slice(0, 5).reduce((acc, item) => acc + genColumn(item, 'page'), '');
}
scData.stats.clicks = SCStats.reduce((acc, item) => acc + item.clicks, 0);
scData.stats.impressions = SCStats.reduce((acc, item) => acc + item.impressions, 0);
// Create Stats Start, End Date
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const endDate = new Date(SCStats[0].date);
const startDate = new Date(SCStats[SCStats.length - 1].date);
// Add the SC header Title
let htmlWithSCStats = `<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="console_table">
<tr>
<td style="font-weight:bold;">
<img class="google_icon" src="${googleIcon}" alt="Google" width="13" height="13"> Google Search Console Stats</h3>
</td>
<td class="stat" align="right" style="font-size: 12px;">
${startDate.getDate()} ${months[startDate.getMonth()]} - ${endDate.getDate()} ${months[endDate.getMonth()]}
(Last 7 Days)
</td>
</tr>
</table>
`;
// Add the SC Data Tables
Object.keys(scData).forEach((itemKey) => {
const scItem = scData[itemKey as keyof SCStatsObject];
const scItemFirstColName = itemKey === 'stats' ? 'Date' : `${itemKey[0].toUpperCase()}${itemKey.slice(1)}`;
htmlWithSCStats += `<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="subhead">
<tr>
<td style="font-weight:bold;">${scItem.label}</h3></td>
${scItem.clicks && scItem.impressions ? (
`<td class="stat" align="right">
<strong>${scItem.clicks}</strong> Clicks | <strong>${scItem.impressions}</strong> Views
</td>`
)
: ''
}
</tr>
</table>
<table role="presentation" class="main" style="margin-bottom:20px">
<tbody>
<tr>
<td class="wrapper">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="keyword_table keyword_table--sc">
<tbody>
<tr align="left">
<th>${scItemFirstColName}</th>
<th>Clicks</th>
<th>Views</th>
<th>Position</th>
</tr>
${scItem.html}
</tbody>
</table>
</td>
</tr>
</tbody>
</table>`;
});
return htmlWithSCStats;
};
export default generateEmail;