mirror of
https://github.com/towfiqi/serpbear
synced 2025-06-26 18:15:54 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54522b2261 | ||
|
|
495d872bb9 | ||
|
|
07eb4bd94f | ||
|
|
36ed4cf800 | ||
|
|
c8601ebb84 | ||
|
|
56d8b660c5 | ||
|
|
c34c8260c7 | ||
|
|
cab8f518bb | ||
|
|
6e47a6fba7 | ||
|
|
bf911b4e45 | ||
|
|
d7279512cf | ||
|
|
4fef1a9abc | ||
|
|
aeed1f8559 | ||
|
|
12eac2b012 | ||
|
|
649f412303 | ||
|
|
a2edabbdf9 | ||
|
|
3690e97fe7 | ||
|
|
17fb2c40cc | ||
|
|
faa3519a29 | ||
|
|
bc02c929ba | ||
|
|
d9d7c6347e |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -2,6 +2,34 @@
|
|||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Ensures Docker build uses matching npm package versions from package.json ([4fef1a9](https://github.com/towfiqi/serpbear/commit/4fef1a9abc737da67ab1ea0c4efce8194890545e))
|
||||||
|
* Resolves broken Docker build due to croner package version mismatch. ([aeed1f8](https://github.com/towfiqi/serpbear/commit/aeed1f8559e044bf658d930a22fa91f38cfedc6b)), closes [#247](https://github.com/towfiqi/serpbear/issues/247)
|
||||||
|
* Resolves broken Proxy Scraper functionality. ([649f412](https://github.com/towfiqi/serpbear/commit/649f412303dd50127b3736740962863f735f76eb)), closes [#248](https://github.com/towfiqi/serpbear/issues/248)
|
||||||
|
* Resolves Google Ads search volume data loading issue. ([12eac2b](https://github.com/towfiqi/serpbear/commit/12eac2b01235e9eae06882d6a2c50c793b890661))
|
||||||
|
|
||||||
|
### [2.0.5](https://github.com/towfiqi/serpbear/compare/v2.0.4...v2.0.5) (2024-11-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Fixes broken scrape result issue for keywords with special characters. ([d9d7c63](https://github.com/towfiqi/serpbear/commit/d9d7c6347e99e35f1e48f20b1e8f999612d69a72)), closes [#221](https://github.com/towfiqi/serpbear/issues/221)
|
||||||
|
* Fixes misaligned Keywords table UI content. ([faa3519](https://github.com/towfiqi/serpbear/commit/faa3519a29fc61ef8bd2ce9275a6674f1c7946e0))
|
||||||
|
* Resolves "Add Domain" UI confusion. ([17fb2c4](https://github.com/towfiqi/serpbear/commit/17fb2c40cc6b57ab2fe6aeb940dececd1a83411f))
|
||||||
|
* Resolves broken Scrapingrobot scraper on new installs. ([bc02c92](https://github.com/towfiqi/serpbear/commit/bc02c929ba7efd6b4b6a09495af7310c155a26bd)), closes [#243](https://github.com/towfiqi/serpbear/issues/243)
|
||||||
|
|
||||||
### [2.0.4](https://github.com/towfiqi/serpbear/compare/v2.0.3...v2.0.4) (2024-11-10)
|
### [2.0.4](https://github.com/towfiqi/serpbear/compare/v2.0.3...v2.0.4) (2024-11-10)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ COPY --from=builder --chown=nextjs:nodejs /app/.sequelizerc ./.sequelizerc
|
|||||||
COPY --from=builder --chown=nextjs:nodejs /app/entrypoint.sh ./entrypoint.sh
|
COPY --from=builder --chown=nextjs:nodejs /app/entrypoint.sh ./entrypoint.sh
|
||||||
RUN rm package.json
|
RUN rm package.json
|
||||||
RUN npm init -y
|
RUN npm init -y
|
||||||
RUN npm i cryptr dotenv croner @googleapis/searchconsole sequelize-cli @isaacs/ttlcache
|
RUN npm i cryptr@6.0.3 dotenv@16.0.3 croner@9.0.0 @googleapis/searchconsole@1.0.5 sequelize-cli@6.6.2 @isaacs/ttlcache@1.4.1
|
||||||
RUN npm i -g concurrently
|
RUN npm i -g concurrently
|
||||||
|
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { rest } from 'msw';
|
import { http } from 'msw';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
|
|
||||||
export const handlers = [
|
export const handlers = [
|
||||||
rest.get(
|
http.get(
|
||||||
'*/react-query',
|
'*/react-query',
|
||||||
(req, res, ctx) => {
|
({ request, params }) => {
|
||||||
return res(
|
return new Response(
|
||||||
ctx.status(200),
|
JSON.stringify({
|
||||||
ctx.json({
|
|
||||||
name: 'mocked-react-query',
|
name: 'mocked-react-query',
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
const createTestQueryClient = () => new QueryClient({
|
const createTestQueryClient = () => new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const AddDomain = ({ closeModal, domains = [] }: AddDomainProps) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (invalidDomains.length > 0) {
|
if (invalidDomains.length > 0) {
|
||||||
setNewDomainError(`Please Insert Valid Domain URL. Invalid URLs: ${invalidDomains.join(', ')}`);
|
setNewDomainError(`Please Insert Valid Website URL. ${invalidDomains.length > 1 ? `Invalid URLs: ${invalidDomains.join(', ')}` : ''}`);
|
||||||
} else if (domainsTobeAdded.length > 0) {
|
} else if (domainsTobeAdded.length > 0) {
|
||||||
console.log('domainsTobeAdded :', domainsTobeAdded);
|
console.log('domainsTobeAdded :', domainsTobeAdded);
|
||||||
addMutate(domainsTobeAdded);
|
addMutate(domainsTobeAdded);
|
||||||
@@ -51,11 +51,11 @@ const AddDomain = ({ closeModal, domains = [] }: AddDomainProps) => {
|
|||||||
return (
|
return (
|
||||||
<Modal closeModal={() => { closeModal(false); }} title={'Add New Domain'}>
|
<Modal closeModal={() => { closeModal(false); }} title={'Add New Domain'}>
|
||||||
<div data-testid="adddomain_modal">
|
<div data-testid="adddomain_modal">
|
||||||
<h4 className='text-sm mt-4'>Domain URL</h4>
|
<h4 className='text-sm mt-4 pb-2'>Website URL(s)</h4>
|
||||||
<textarea
|
<textarea
|
||||||
className={`w-full h-40 border rounded border-gray-200 p-4 outline-none
|
className={`w-full h-40 border rounded border-gray-200 p-4 outline-none
|
||||||
focus:border-indigo-300 ${newDomainError ? ' border-red-400 focus:border-red-400' : ''}`}
|
focus:border-indigo-300 ${newDomainError ? ' border-red-400 focus:border-red-400' : ''}`}
|
||||||
placeholder="Type or Paste URLs here. Insert Each URL in a New line."
|
placeholder={'Type or Paste URLs here. Insert Each URL in a New line. eg: \nhttps://mysite.com/ \nhttps://anothersite.com/ '}
|
||||||
value={newDomain}
|
value={newDomain}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
onChange={handleDomainInput}>
|
onChange={handleDomainInput}>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ const Keyword = (props: KeywordProps) => {
|
|||||||
className={`keyword relative py-5 px-4 text-gray-600 border-b-[1px] border-gray-200 lg:py-4 lg:px-6 lg:border-0
|
className={`keyword relative py-5 px-4 text-gray-600 border-b-[1px] border-gray-200 lg:py-4 lg:px-6 lg:border-0
|
||||||
lg:flex lg:justify-between lg:items-center ${selected ? ' bg-indigo-50 keyword--selected' : ''} ${lastItem ? 'border-b-0' : ''}`}>
|
lg:flex lg:justify-between lg:items-center ${selected ? ' bg-indigo-50 keyword--selected' : ''} ${lastItem ? 'border-b-0' : ''}`}>
|
||||||
|
|
||||||
<div className=' w-3/4 font-semibold cursor-pointer lg:flex-1 lg:shrink-0 lg:basis-20 lg:w-auto lg:flex lg:items-center'>
|
<div className=' w-3/4 font-semibold cursor-pointer lg:flex-1 lg:shrink-0 lg:basis-28 lg:w-auto lg:flex lg:items-center'>
|
||||||
<button
|
<button
|
||||||
className={`p-0 mr-2 leading-[0px] inline-block rounded-sm pt-0 px-[1px] pb-[3px] border
|
className={`p-0 mr-2 leading-[0px] inline-block rounded-sm pt-0 px-[1px] pb-[3px] border
|
||||||
${selected ? ' bg-blue-700 border-blue-700 text-white' : 'text-transparent'}`}
|
${selected ? ' bg-blue-700 border-blue-700 text-white' : 'text-transparent'}`}
|
||||||
|
|||||||
11
cron.js
11
cron.js
@@ -1,7 +1,8 @@
|
|||||||
|
/* eslint-disable no-new */
|
||||||
const Cryptr = require('cryptr');
|
const Cryptr = require('cryptr');
|
||||||
const { promises } = require('fs');
|
const { promises } = require('fs');
|
||||||
const { readFile } = require('fs');
|
const { readFile } = require('fs');
|
||||||
const Cron = require('croner');
|
const { Cron } = require('croner');
|
||||||
require('dotenv').config({ path: './.env.local' });
|
require('dotenv').config({ path: './.env.local' });
|
||||||
|
|
||||||
const getAppSettings = async () => {
|
const getAppSettings = async () => {
|
||||||
@@ -71,7 +72,7 @@ const runAppCronJobs = () => {
|
|||||||
const scrape_interval = settings.scrape_interval || 'daily';
|
const scrape_interval = settings.scrape_interval || 'daily';
|
||||||
if (scrape_interval !== 'never') {
|
if (scrape_interval !== 'never') {
|
||||||
const scrapeCronTime = generateCronTime(scrape_interval);
|
const scrapeCronTime = generateCronTime(scrape_interval);
|
||||||
Cron(scrapeCronTime, () => {
|
new Cron(scrapeCronTime, () => {
|
||||||
// console.log('### Running Keyword Position Cron Job!');
|
// console.log('### Running Keyword Position Cron Job!');
|
||||||
const fetchOpts = { method: 'POST', headers: { Authorization: `Bearer ${process.env.APIKEY}` } };
|
const fetchOpts = { method: 'POST', headers: { Authorization: `Bearer ${process.env.APIKEY}` } };
|
||||||
fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/cron`, fetchOpts)
|
fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/cron`, fetchOpts)
|
||||||
@@ -89,7 +90,7 @@ const runAppCronJobs = () => {
|
|||||||
if (notif_interval) {
|
if (notif_interval) {
|
||||||
const cronTime = generateCronTime(notif_interval === 'daily' ? 'daily_morning' : notif_interval);
|
const cronTime = generateCronTime(notif_interval === 'daily' ? 'daily_morning' : notif_interval);
|
||||||
if (cronTime) {
|
if (cronTime) {
|
||||||
Cron(cronTime, () => {
|
new Cron(cronTime, () => {
|
||||||
// console.log('### Sending Notification Email...');
|
// console.log('### Sending Notification Email...');
|
||||||
const fetchOpts = { method: 'POST', headers: { Authorization: `Bearer ${process.env.APIKEY}` } };
|
const fetchOpts = { method: 'POST', headers: { Authorization: `Bearer ${process.env.APIKEY}` } };
|
||||||
fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/notify`, fetchOpts)
|
fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/notify`, fetchOpts)
|
||||||
@@ -106,7 +107,7 @@ const runAppCronJobs = () => {
|
|||||||
|
|
||||||
// Run Failed scraping CRON (Every Hour)
|
// Run Failed scraping CRON (Every Hour)
|
||||||
const failedCronTime = generateCronTime('hourly');
|
const failedCronTime = generateCronTime('hourly');
|
||||||
Cron(failedCronTime, () => {
|
new Cron(failedCronTime, () => {
|
||||||
// console.log('### Retrying Failed Scrapes...');
|
// console.log('### Retrying Failed Scrapes...');
|
||||||
|
|
||||||
readFile(`${process.cwd()}/data/failed_queue.json`, { encoding: 'utf-8' }, (err, data) => {
|
readFile(`${process.cwd()}/data/failed_queue.json`, { encoding: 'utf-8' }, (err, data) => {
|
||||||
@@ -135,7 +136,7 @@ const runAppCronJobs = () => {
|
|||||||
// Run Google Search Console Scraper Daily
|
// Run Google Search Console Scraper Daily
|
||||||
if (process.env.SEARCH_CONSOLE_PRIVATE_KEY && process.env.SEARCH_CONSOLE_CLIENT_EMAIL) {
|
if (process.env.SEARCH_CONSOLE_PRIVATE_KEY && process.env.SEARCH_CONSOLE_CLIENT_EMAIL) {
|
||||||
const searchConsoleCRONTime = generateCronTime('daily');
|
const searchConsoleCRONTime = generateCronTime('daily');
|
||||||
Cron(searchConsoleCRONTime, () => {
|
new Cron(searchConsoleCRONTime, () => {
|
||||||
const fetchOpts = { method: 'POST', headers: { Authorization: `Bearer ${process.env.APIKEY}` } };
|
const fetchOpts = { method: 'POST', headers: { Authorization: `Bearer ${process.env.APIKEY}` } };
|
||||||
fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/searchconsole`, fetchOpts)
|
fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/searchconsole`, fetchOpts)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
|
|||||||
2904
package-lock.json
generated
2904
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "serpbear",
|
"name": "serpbear",
|
||||||
"version": "2.0.4",
|
"version": "2.0.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -18,16 +18,16 @@
|
|||||||
"release": "standard-version"
|
"release": "standard-version"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googleapis/searchconsole": "^1.0.0",
|
"@googleapis/searchconsole": "^1.0.5",
|
||||||
"@isaacs/ttlcache": "^1.4.1",
|
"@isaacs/ttlcache": "^1.4.1",
|
||||||
"@types/react-transition-group": "^4.4.5",
|
"@types/react-transition-group": "^4.4.5",
|
||||||
"axios": "^1.1.3",
|
"axios": "^1.7.7",
|
||||||
"axios-retry": "^3.3.1",
|
"axios-retry": "^3.3.1",
|
||||||
"chart.js": "^3.9.1",
|
"chart.js": "^3.9.1",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "^1.0.0",
|
||||||
"concurrently": "^7.6.0",
|
"concurrently": "^7.6.0",
|
||||||
"cookies": "^0.8.0",
|
"cookies": "^0.8.0",
|
||||||
"croner": "^5.3.5",
|
"croner": "^9.0.0",
|
||||||
"cryptr": "^6.0.3",
|
"cryptr": "^6.0.3",
|
||||||
"dayjs": "^1.11.5",
|
"dayjs": "^1.11.5",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
"https-proxy-agent": "^5.0.1",
|
"https-proxy-agent": "^5.0.1",
|
||||||
"isomorphic-fetch": "^3.0.0",
|
"isomorphic-fetch": "^3.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"msw": "^0.49.0",
|
|
||||||
"next": "^12.3.4",
|
"next": "^12.3.4",
|
||||||
"nodemailer": "^6.9.9",
|
"nodemailer": "^6.9.9",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
@@ -50,11 +49,11 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"sequelize": "^6.34.0",
|
"sequelize": "^6.34.0",
|
||||||
"sequelize-typescript": "^2.1.6",
|
"sequelize-typescript": "^2.1.6",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.7",
|
||||||
"umzug": "^3.6.1"
|
"umzug": "^3.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.1.4",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@types/cookies": "^0.7.7",
|
"@types/cookies": "^0.7.7",
|
||||||
"@types/cryptr": "^4.0.1",
|
"@types/cryptr": "^4.0.1",
|
||||||
@@ -74,15 +73,16 @@
|
|||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
|
"msw": "^2.6.4",
|
||||||
"next-router-mock": "^0.9.10",
|
"next-router-mock": "^0.9.10",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"sass": "^1.55.0",
|
"sass": "^1.80.7",
|
||||||
"sequelize-cli": "^6.6.2",
|
"sequelize-cli": "^6.6.2",
|
||||||
"standard-version": "^9.5.0",
|
"standard-version": "^9.5.0",
|
||||||
"stylelint-config-standard": "^29.0.0",
|
"stylelint-config-standard": "^29.0.0",
|
||||||
"tailwindcss": "^3.1.8",
|
"tailwindcss": "^3.4.14",
|
||||||
"typescript": "4.8.4"
|
"typescript": "^4.8.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,14 +36,13 @@ const updatekeywordVolume = async (req: NextApiRequest, res: NextApiResponse<Key
|
|||||||
keywordsToSend = parseKeywords(allKeywords.map((e) => e.get({ plain: true })));
|
keywordsToSend = parseKeywords(allKeywords.map((e) => e.get({ plain: true })));
|
||||||
}
|
}
|
||||||
if (domain) {
|
if (domain) {
|
||||||
// const allDomain = domain === 'all';
|
const allDomain = domain === 'all';
|
||||||
// const allKeywords:Keyword[] = allDomain ? await Keyword.findAll() : await Keyword.findAll(allDomain ? {} : { where: { domain } });
|
const allKeywords:Keyword[] = allDomain ? await Keyword.findAll() : await Keyword.findAll(allDomain ? {} : { where: { domain } });
|
||||||
// keywordsToSend = parseKeywords(allKeywords.map((e) => e.get({ plain: true })));
|
keywordsToSend = parseKeywords(allKeywords.map((e) => e.get({ plain: true })));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keywordsToSend.length > 0) {
|
if (keywordsToSend.length > 0) {
|
||||||
const keywordsVolumeData = await getKeywordsVolume(keywordsToSend);
|
const keywordsVolumeData = await getKeywordsVolume(keywordsToSend);
|
||||||
// console.log('keywordsVolumeData :', keywordsVolumeData);
|
|
||||||
if (keywordsVolumeData.error) {
|
if (keywordsVolumeData.error) {
|
||||||
return res.status(400).json({ keywords: [], error: keywordsVolumeData.error });
|
return res.status(400).json({ keywords: [], error: keywordsVolumeData.error });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,10 @@ const hasdata:ScraperSettings = {
|
|||||||
scrapeURL: (keyword, settings) => {
|
scrapeURL: (keyword, settings) => {
|
||||||
const country = keyword.country || 'US';
|
const country = keyword.country || 'US';
|
||||||
const countryName = countries[country][0];
|
const countryName = countries[country][0];
|
||||||
const location = keyword.city && countryName ? `&location=${encodeURI(`${keyword.city},${countryName}`)}` : '';
|
const location = keyword.city && countryName ? `&location=${encodeURIComponent(`${keyword.city},${countryName}`)}` : '';
|
||||||
return `https://api.scrape-it.cloud/scrape/google/serp?q=${encodeURI(keyword.keyword)}${location}&num=100&gl=${country.toLowerCase()}&deviceType=${keyword.device}`;
|
return `https://api.scrape-it.cloud/scrape/google/serp?q=${encodeURIComponent(keyword.keyword)}${location}&num=100&gl=${country.toLowerCase()}&deviceType=${keyword.device}`;
|
||||||
},
|
},
|
||||||
resultObjectKey: 'organicResults',
|
resultObjectKey: 'organicResults',
|
||||||
|
|
||||||
serpExtractor: (content) => {
|
serpExtractor: (content) => {
|
||||||
const extractedResult = [];
|
const extractedResult = [];
|
||||||
const results: HasDataResult[] = (typeof content === 'string') ? JSON.parse(content) : content as HasDataResult[];
|
const results: HasDataResult[] = (typeof content === 'string') ? JSON.parse(content) : content as HasDataResult[];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
|
|
||||||
const proxy:ScraperSettings = {
|
const proxy:ScraperSettings = {
|
||||||
id: 'proxy',
|
id: 'proxy',
|
||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ const searchapi:ScraperSettings = {
|
|||||||
scrapeURL: (keyword) => {
|
scrapeURL: (keyword) => {
|
||||||
const country = keyword.country || 'US';
|
const country = keyword.country || 'US';
|
||||||
const countryName = countries[country][0];
|
const countryName = countries[country][0];
|
||||||
const location = keyword.city && countryName ? `&location=${encodeURI(`${keyword.city},${countryName}`)}` : '';
|
const location = keyword.city && countryName ? `&location=${encodeURIComponent(`${keyword.city},${countryName}`)}` : '';
|
||||||
return `https://www.searchapi.io/api/v1/search?engine=google&q=${encodeURI(keyword.keyword)}&num=100&gl=${country}&device=${keyword.device}${location}`;
|
return `https://www.searchapi.io/api/v1/search?engine=google&q=${encodeURIComponent(keyword.keyword)}&num=100&gl=${country}&device=${keyword.device}${location}`;
|
||||||
},
|
},
|
||||||
resultObjectKey: 'organic_results',
|
resultObjectKey: 'organic_results',
|
||||||
serpExtractor: (content) => {
|
serpExtractor: (content) => {
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ const serpapi:ScraperSettings = {
|
|||||||
},
|
},
|
||||||
scrapeURL: (keyword, settings) => {
|
scrapeURL: (keyword, settings) => {
|
||||||
const countryName = countries[keyword.country || 'US'][0];
|
const countryName = countries[keyword.country || 'US'][0];
|
||||||
const location = keyword.city && keyword.country ? `&location=${encodeURI(`${keyword.city},${countryName}`)}` : '';
|
const location = keyword.city && keyword.country ? `&location=${encodeURIComponent(`${keyword.city},${countryName}`)}` : '';
|
||||||
return `https://serpapi.com/search?q=${encodeURI(keyword.keyword)}&num=100&gl=${keyword.country}&device=${keyword.device}${location}&api_key=${settings.scaping_api}`;
|
return `https://serpapi.com/search?q=${encodeURIComponent(keyword.keyword)}&num=100&gl=${keyword.country}&device=${keyword.device}${location}&api_key=${settings.scaping_api}`;
|
||||||
},
|
},
|
||||||
resultObjectKey: 'organic_results',
|
resultObjectKey: 'organic_results',
|
||||||
serpExtractor: (content) => {
|
serpExtractor: (content) => {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ const serper:ScraperSettings = {
|
|||||||
scrapeURL: (keyword, settings, countryData) => {
|
scrapeURL: (keyword, settings, countryData) => {
|
||||||
const country = keyword.country || 'US';
|
const country = keyword.country || 'US';
|
||||||
const lang = countryData[country][2];
|
const lang = countryData[country][2];
|
||||||
return `https://google.serper.dev/search?q=${encodeURI(keyword.keyword)}&gl=${country}&hl=${lang}&num=100&apiKey=${settings.scaping_api}`;
|
console.log('Serper URL :', `https://google.serper.dev/search?q=${encodeURIComponent(keyword.keyword)}&gl=${country}&hl=${lang}&num=100&apiKey=${settings.scaping_api}`);
|
||||||
|
return `https://google.serper.dev/search?q=${encodeURIComponent(keyword.keyword)}&gl=${country}&hl=${lang}&num=100&apiKey=${settings.scaping_api}`;
|
||||||
},
|
},
|
||||||
resultObjectKey: 'organic',
|
resultObjectKey: 'organic',
|
||||||
serpExtractor: (content) => {
|
serpExtractor: (content) => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const serply:ScraperSettings = {
|
|||||||
},
|
},
|
||||||
scrapeURL: (keyword) => {
|
scrapeURL: (keyword) => {
|
||||||
const country = scraperCountries.includes(keyword.country.toUpperCase()) ? keyword.country : 'US';
|
const country = scraperCountries.includes(keyword.country.toUpperCase()) ? keyword.country : 'US';
|
||||||
return `https://api.serply.io/v1/search/q=${encodeURI(keyword.keyword)}&num=100&hl=${country}`;
|
return `https://api.serply.io/v1/search/q=${encodeURIComponent(keyword.keyword)}&num=100&hl=${country}`;
|
||||||
},
|
},
|
||||||
resultObjectKey: 'result',
|
resultObjectKey: 'result',
|
||||||
serpExtractor: (content) => {
|
serpExtractor: (content) => {
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ const spaceSerp:ScraperSettings = {
|
|||||||
scrapeURL: (keyword, settings, countryData) => {
|
scrapeURL: (keyword, settings, countryData) => {
|
||||||
const country = keyword.country || 'US';
|
const country = keyword.country || 'US';
|
||||||
const countryName = countries[country][0];
|
const countryName = countries[country][0];
|
||||||
const location = keyword.city ? `&location=${encodeURI(`${keyword.city},${countryName}`)}` : '';
|
const location = keyword.city ? `&location=${encodeURIComponent(`${keyword.city},${countryName}`)}` : '';
|
||||||
const device = keyword.device === 'mobile' ? '&device=mobile' : '';
|
const device = keyword.device === 'mobile' ? '&device=mobile' : '';
|
||||||
const lang = countryData[country][2];
|
const lang = countryData[country][2];
|
||||||
return `https://api.spaceserp.com/google/search?apiKey=${settings.scaping_api}&q=${encodeURI(keyword.keyword)}&pageSize=100&gl=${country}&hl=${lang}${location}${device}&resultBlocks=`;
|
return `https://api.spaceserp.com/google/search?apiKey=${settings.scaping_api}&q=${encodeURIComponent(keyword.keyword)}&pageSize=100&gl=${country}&hl=${lang}${location}${device}&resultBlocks=`;
|
||||||
},
|
},
|
||||||
resultObjectKey: 'organic_results',
|
resultObjectKey: 'organic_results',
|
||||||
serpExtractor: (content) => {
|
serpExtractor: (content) => {
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ const valueSerp:ScraperSettings = {
|
|||||||
scrapeURL: (keyword, settings, countryData) => {
|
scrapeURL: (keyword, settings, countryData) => {
|
||||||
const country = keyword.country || 'US';
|
const country = keyword.country || 'US';
|
||||||
const countryName = countries[country][0];
|
const countryName = countries[country][0];
|
||||||
const location = keyword.city ? `&location=${encodeURI(`${keyword.city},${countryName}`)}` : '';
|
const location = keyword.city ? `&location=${encodeURIComponent(`${keyword.city},${countryName}`)}` : '';
|
||||||
const device = keyword.device === 'mobile' ? '&device=mobile' : '';
|
const device = keyword.device === 'mobile' ? '&device=mobile' : '';
|
||||||
const lang = countryData[country][2];
|
const lang = countryData[country][2];
|
||||||
console.log(`https://api.valueserp.com/search?api_key=${settings.scaping_api}&q=${encodeURI(keyword.keyword)}&gl=${country}&hl=${lang}${device}${location}&num=100&output=json&include_answer_box=false&include_advertiser_info=false`);
|
console.log(`https://api.valueserp.com/search?api_key=${settings.scaping_api}&q=${encodeURIComponent(keyword.keyword)}&gl=${country}&hl=${lang}${device}${location}&num=100&output=json&include_answer_box=false&include_advertiser_info=false`);
|
||||||
return `https://api.valueserp.com/search?api_key=${settings.scaping_api}&q=${encodeURI(keyword.keyword)}&gl=${country}&hl=${lang}${device}${location}&num=100&output=json&include_answer_box=false&include_advertiser_info=false`;
|
return `https://api.valueserp.com/search?api_key=${settings.scaping_api}&q=${encodeURIComponent(keyword.keyword)}&gl=${country}&hl=${lang}${device}${location}&num=100&output=json&include_answer_box=false&include_advertiser_info=false`;
|
||||||
},
|
},
|
||||||
resultObjectKey: 'organic_results',
|
resultObjectKey: 'organic_results',
|
||||||
serpExtractor: (content) => {
|
serpExtractor: (content) => {
|
||||||
|
|||||||
@@ -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]: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios, { AxiosResponse, CreateAxiosDefaults } from 'axios';
|
import axios, { AxiosResponse, CreateAxiosDefaults } from 'axios';
|
||||||
import cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { readFile, writeFile } from 'fs/promises';
|
import { readFile, writeFile } from 'fs/promises';
|
||||||
import HttpsProxyAgent from 'https-proxy-agent';
|
import HttpsProxyAgent from 'https-proxy-agent';
|
||||||
import countries from './countries';
|
import countries from './countries';
|
||||||
@@ -124,14 +124,18 @@ export const scrapeKeywordFromGoogle = async (keyword:KeywordType, settings:Sett
|
|||||||
throw new Error(res);
|
throw new Error(res);
|
||||||
}
|
}
|
||||||
} catch (error:any) {
|
} catch (error:any) {
|
||||||
refreshedResults.error = scraperError;
|
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