mirror of
https://github.com/towfiqi/serpbear
synced 2025-06-26 18:15:54 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54522b2261 | ||
|
|
495d872bb9 | ||
|
|
07eb4bd94f | ||
|
|
36ed4cf800 | ||
|
|
c8601ebb84 | ||
|
|
56d8b660c5 | ||
|
|
c34c8260c7 | ||
|
|
cab8f518bb | ||
|
|
6e47a6fba7 | ||
|
|
bf911b4e45 |
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
### [2.0.7](https://github.com/towfiqi/serpbear/compare/v2.0.6...v2.0.7) (2025-02-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Resolves AdWords integration issue. ([36ed4cf](https://github.com/towfiqi/serpbear/commit/36ed4cf800c1fd0e3df4e807faaa1fdb863df5e4))
|
||||||
|
* resolves broken CDN images. ([bf911b4](https://github.com/towfiqi/serpbear/commit/bf911b4e45b9007a05ce6399838da3276161c61d))
|
||||||
|
|
||||||
### [2.0.6](https://github.com/towfiqi/serpbear/compare/v2.0.5...v2.0.6) (2024-11-15)
|
### [2.0.6](https://github.com/towfiqi/serpbear/compare/v2.0.5...v2.0.6) (2024-11-15)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
# SerpBear
|
# SerpBear
|
||||||
|
|
||||||
   
|
    [](https://www.youtube.com/watch?v=bjtDsd0g468&rco=1)
|
||||||
|
|
||||||
#### [Documentation](https://docs.serpbear.com/) | [Changelog](https://github.com/towfiqi/serpbear/blob/main/CHANGELOG.md) | [Docker Image](https://hub.docker.com/r/towfiqi/serpbear)
|
#### [Documentation](https://docs.serpbear.com/) | [Changelog](https://github.com/towfiqi/serpbear/blob/main/CHANGELOG.md) | [Docker Image](https://hub.docker.com/r/towfiqi/serpbear)
|
||||||
|
|
||||||
SerpBear is an Open Source Search Engine Position Tracking and Keyword Research App. It allows you to track your website's keyword positions in Google and get notified of their position change.
|
SerpBear is an Open Source Search Engine Position Tracking and Keyword Research App. It allows you to track your website's keyword positions in Google and get notified of their position change.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Features
|
#### Features
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "serpbear",
|
"name": "serpbear",
|
||||||
"version": "2.0.6",
|
"version": "2.0.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "serpbear",
|
"name": "serpbear",
|
||||||
"version": "2.0.6",
|
"version": "2.0.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googleapis/searchconsole": "^1.0.5",
|
"@googleapis/searchconsole": "^1.0.5",
|
||||||
"@isaacs/ttlcache": "^1.4.1",
|
"@isaacs/ttlcache": "^1.4.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "serpbear",
|
"name": "serpbear",
|
||||||
"version": "2.0.6",
|
"version": "2.0.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ const proxy:ScraperSettings = {
|
|||||||
|
|
||||||
const $ = cheerio.load(content);
|
const $ = cheerio.load(content);
|
||||||
let lastPosition = 0;
|
let lastPosition = 0;
|
||||||
|
const hasValidContent = $('body').find('#main');
|
||||||
|
if (hasValidContent.length === 0) {
|
||||||
|
const msg = '[ERROR] Scraped search results from proxy do not adhere to expected format. Unable to parse results';
|
||||||
|
console.log(msg);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
const mainContent = $('body').find('#main');
|
const mainContent = $('body').find('#main');
|
||||||
const children = $(mainContent).find('h3');
|
const children = $(mainContent).find('h3');
|
||||||
|
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export const getAdwordsKeywordIdeas = async (credentials:AdwordsCredentials, adw
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// API: https://developers.google.com/google-ads/api/rest/reference/rest/v16/customers/generateKeywordIdeas
|
// API: https://developers.google.com/google-ads/api/rest/reference/rest/v18/customers/generateKeywordIdeas
|
||||||
const customerID = account_id.replaceAll('-', '');
|
const customerID = account_id.replaceAll('-', '');
|
||||||
const geoTargetConstants = countries[country][3]; // '2840';
|
const geoTargetConstants = countries[country][3]; // '2840';
|
||||||
const reqPayload: Record<string, any> = {
|
const reqPayload: Record<string, any> = {
|
||||||
@@ -178,7 +178,7 @@ export const getAdwordsKeywordIdeas = async (credentials:AdwordsCredentials, adw
|
|||||||
reqPayload.siteSeed = { site: domain };
|
reqPayload.siteSeed = { site: domain };
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await fetch(`https://googleads.googleapis.com/v16/customers/${customerID}:generateKeywordIdeas`, {
|
const resp = await fetch(`https://googleads.googleapis.com/v18/customers/${customerID}:generateKeywordIdeas`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -297,7 +297,7 @@ export const getKeywordsVolume = async (keywords: KeywordType[]): Promise<{error
|
|||||||
for (const country in keywordRequests) {
|
for (const country in keywordRequests) {
|
||||||
if (Object.hasOwn(keywordRequests, country) && keywordRequests[country].length > 0) {
|
if (Object.hasOwn(keywordRequests, country) && keywordRequests[country].length > 0) {
|
||||||
try {
|
try {
|
||||||
// API: https://developers.google.com/google-ads/api/rest/reference/rest/v16/customers/generateKeywordHistoricalMetrics
|
// API: https://developers.google.com/google-ads/api/rest/reference/rest/v18/customers/generateKeywordHistoricalMetrics
|
||||||
const customerID = account_id.replaceAll('-', '');
|
const customerID = account_id.replaceAll('-', '');
|
||||||
const geoTargetConstants = countries[country][3]; // '2840';
|
const geoTargetConstants = countries[country][3]; // '2840';
|
||||||
const reqKeywords = keywordRequests[country].map((kw) => kw.keyword);
|
const reqKeywords = keywordRequests[country].map((kw) => kw.keyword);
|
||||||
@@ -306,7 +306,7 @@ export const getKeywordsVolume = async (keywords: KeywordType[]): Promise<{error
|
|||||||
geoTargetConstants: `geoTargetConstants/${geoTargetConstants}`,
|
geoTargetConstants: `geoTargetConstants/${geoTargetConstants}`,
|
||||||
// language: `languageConstants/${language}`,
|
// language: `languageConstants/${language}`,
|
||||||
};
|
};
|
||||||
const resp = await fetch(`https://googleads.googleapis.com/v16/customers/${customerID}:generateKeywordHistoricalMetrics`, {
|
const resp = await fetch(`https://googleads.googleapis.com/v18/customers/${customerID}:generateKeywordHistoricalMetrics`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import path from 'path';
|
|||||||
import { getKeywordsInsight, getPagesInsight } from './insight';
|
import { getKeywordsInsight, getPagesInsight } from './insight';
|
||||||
import { readLocalSCData } from './searchConsole';
|
import { readLocalSCData } from './searchConsole';
|
||||||
|
|
||||||
const serpBearLogo = 'https://erevanto.sirv.com/Images/serpbear/ikAdjQq.png';
|
const serpBearLogo = 'https://serpbear.b-cdn.net/ikAdjQq.png';
|
||||||
const mobileIcon = 'https://erevanto.sirv.com/Images/serpbear/SqXD9rd.png';
|
const mobileIcon = 'https://serpbear.b-cdn.net/SqXD9rd.png';
|
||||||
const desktopIcon = 'https://erevanto.sirv.com/Images/serpbear/Dx3u0XD.png';
|
const desktopIcon = 'https://serpbear.b-cdn.net/Dx3u0XD.png';
|
||||||
const googleIcon = 'https://erevanto.sirv.com/Images/serpbear/Sx3u0X9.png';
|
const googleIcon = 'https://serpbear.b-cdn.net/Sx3u0X9.png';
|
||||||
|
|
||||||
type SCStatsObject = {
|
type SCStatsObject = {
|
||||||
[key:string]: {
|
[key:string]: {
|
||||||
|
|||||||
@@ -127,11 +127,15 @@ export const scrapeKeywordFromGoogle = async (keyword:KeywordType, settings:Sett
|
|||||||
refreshedResults.error = scraperError || 'Unknown Error';
|
refreshedResults.error = scraperError || 'Unknown Error';
|
||||||
if (settings.scraper_type === 'proxy' && error && error.response && error.response.statusText) {
|
if (settings.scraper_type === 'proxy' && error && error.response && error.response.statusText) {
|
||||||
refreshedResults.error = `[${error.response.status}] ${error.response.statusText}`;
|
refreshedResults.error = `[${error.response.status}] ${error.response.statusText}`;
|
||||||
|
} else if (settings.scraper_type === 'proxy' && error) {
|
||||||
|
refreshedResults.error = error;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[ERROR] Scraping Keyword : ', keyword.keyword, '. Error: ', error && error.response && error.response.statusText);
|
console.log('[ERROR] Scraping Keyword : ', keyword.keyword);
|
||||||
if (!(error && error.response && error.response.statusText)) {
|
if (!(error && error.response && error.response.statusText)) {
|
||||||
console.log('[ERROR_MESSAGE]: ', error);
|
console.log('[ERROR_MESSAGE]: ', error);
|
||||||
|
} else {
|
||||||
|
console.log('[ERROR_MESSAGE]: ', error && error.response && error.response.statusText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,9 +152,17 @@ export const extractScrapedResult = (content: string, device: string): SearchRes
|
|||||||
const extractedResult = [];
|
const extractedResult = [];
|
||||||
|
|
||||||
const $ = cheerio.load(content);
|
const $ = cheerio.load(content);
|
||||||
|
const hasValidContent = [...$('body').find('#search'), ...$('body').find('#rso')];
|
||||||
|
if (hasValidContent.length === 0) {
|
||||||
|
const msg = '[ERROR] Scraped search results do not adhere to expected format. Unable to parse results';
|
||||||
|
console.log(msg);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
const hasNumberofResult = $('body').find('#search > div > div');
|
const hasNumberofResult = $('body').find('#search > div > div');
|
||||||
const searchResultItems = hasNumberofResult.find('h3');
|
const searchResultItems = hasNumberofResult.find('h3');
|
||||||
let lastPosition = 0;
|
let lastPosition = 0;
|
||||||
|
console.log('Scraped search results contain ', searchResultItems.length, ' desktop results.');
|
||||||
|
|
||||||
for (let i = 0; i < searchResultItems.length; i += 1) {
|
for (let i = 0; i < searchResultItems.length; i += 1) {
|
||||||
if (searchResultItems[i]) {
|
if (searchResultItems[i]) {
|
||||||
@@ -161,11 +173,12 @@ export const extractScrapedResult = (content: string, device: string): SearchRes
|
|||||||
extractedResult.push({ title, url, position: lastPosition });
|
extractedResult.push({ title, url, position: lastPosition });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mobile Scraper
|
// Mobile Scraper
|
||||||
if (extractedResult.length === 0 && device === 'mobile') {
|
if (extractedResult.length === 0 && device === 'mobile') {
|
||||||
const items = $('body').find('#rso > div');
|
const items = $('body').find('#rso > div');
|
||||||
|
console.log('Scraped search results contain ', items.length, ' mobile results.');
|
||||||
for (let i = 0; i < items.length; i += 1) {
|
for (let i = 0; i < items.length; i += 1) {
|
||||||
const item = $(items[i]);
|
const item = $(items[i]);
|
||||||
const linkDom = item.find('a[role="presentation"]');
|
const linkDom = item.find('a[role="presentation"]');
|
||||||
@@ -181,7 +194,7 @@ export const extractScrapedResult = (content: string, device: string): SearchRes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return extractedResult;
|
return extractedResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user