Return

All tests / src/pages/api indexed-pages.ts

100% Statements 32/32
75% Branches 6/8
100% Functions 4/4
100% Lines 30/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79  1x 1x 1x 1x 1x 1x                 1x 4x 4x 2x     4x 2x 2x   2x                         1x                 1x 3x 2x                   4x 4x 4x 3x 1x 1x     1x     3x 3x 3x   1x 1x      
import { NextApiRequest, NextApiResponse } from 'next';
import fs from 'fs';
import path from 'path';
import console from '@/helpers/console';
import { getSearchConsoleClient, SITE_URL } from '@/helpers/searchConsole';
import allowCors from '../../helpers/cors';
import { CACHE } from '@/helpers/http';
 
/**
 * @description Count the pages Google surfaced for the site in the last 28 days,
 * using the Search Console API with the same service account as /api/analytics.
 * Replaces the old SerpAPI/Google-scraping approach that 503'd in production (SDD-001).
 *
 * @returns {Promise<number | null>} - Page count, or null when credentials are missing
 */
const countFromSearchConsole = async (): Promise<number | null> => {
    const webmasters = getSearchConsoleClient();
    if (!webmasters) {
        return null;
    }
 
    const toDay = (date: Date) => date.toISOString().slice(0, 10);
    const end = new Date();
    const start = new Date(end.getTime() - 28 * 24 * 60 * 60 * 1000);
 
    const response = await webmasters.searchanalytics.query({
        siteUrl: SITE_URL,
        requestBody: {
            startDate: toDay(start),
            endDate: toDay(end),
            dimensions: ['page'],
            // Was 25000. The route only needs `rows.length`, and the site publishes ~60 URLs, so the
            // old limit asked Search Console for four hundred times more data than the widget can
            // use — an expensive query repeated per visitor before this route was cached.
            rowLimit: 1000,
        },
    });
 
    return response.data.rows?.length ?? 0;
};
 
/**
 * @description Fallback when Search Console is unavailable: count the URLs we
 * publish in the sitemap instead of fabricating a number.
 *
 * @returns {number} - Number of <loc> entries in public/sitemap.xml
 */
const countFromSitemap = (): number => {
    const sitemap = fs.readFileSync(path.join(process.cwd(), 'public', 'sitemap.xml'), 'utf8');
    return sitemap.match(/<loc>/g)?.length ?? 0;
};
 
/**
 * @description Get the number of pages indexed/surfaced by Google
 * @param _req {NextApiRequest}
 * @param res {NextApiResponse}
 * @returns Promise<void>
 * @example http://localhost:3000/api/indexed-pages
 */
export default allowCors(async function handler(_req: NextApiRequest, res: NextApiResponse) {
    try {
        const num = await countFromSearchConsole();
        if (num !== null && num > 0) {
            res.setHeader('Cache-Control', CACHE.slow);
            return res.status(200).json({ num });
        }
    } catch (err) {
        console.error(`Search Console count failed, falling back to sitemap: ${err}`);
    }
 
    try {
        res.setHeader('Cache-Control', CACHE.slow);
        return res.status(200).json({ num: countFromSitemap() });
    } catch {
        res.setHeader('Cache-Control', CACHE.error);
        return res.status(500).json({ error: 'Unable to count indexed pages' });
    }
});