Skip to content

Documentation

Cloudflare page: check the returned content (JavaScript)

Check access to the expected content on the ScrapingCourse demonstration page. The result is one verification record rather than a product catalog.

Choose JavaScript, paste the complete source from this article and set the run parameters below. The source for this collection is ScrapingCourse. This walkthrough applies to the specified demonstration page.

Example settings

{
  "path": "/cloudflare-challenge",
  "mode": "challenge",
  "expected": 1
}

path selects the page, mode selects an algorithm branch, and expected sets the control count for this example. It is not a plan limit. When the source dataset changes, check the expected count against it.

How the algorithm works

  1. Select /cloudflare-challenge and the challenge mode, which is separate from card collection.
  2. Open the page with the browser SDK and the current auto_captcha value. The scenario checks the content returned by the page.
  3. Read the body text with DOM tools and normalize repeated whitespace and line breaks to one space.
  4. Match the demonstration page success text using a case-insensitive expression with bypassed variants or Congratulations.
  5. Only after a match, emit name containing the page path and passed: true. Use that path as the record key.
  6. Close the browser in finally. Product-card extraction and product-count comparison do not run in challenge mode.

Details that matter

Success depends on the control text of this particular page. expected: 1 describes one output record; this branch does not count products.

JavaScript details

JavaScript uses an asynchronous run function. Await network and browser actions; parsing already received HTML is synchronous. collect closes over the shared product-key Set. Its return value is the card count in the current HTML, not the number of new records.

Expected output

This branch emits one record with name and passed. name is /cloudflare-challenge; passed is true only after matching the control text. The final seen count check is skipped for challenge mode.

Complete scenario source

The source is shared by this collection. This example runs its challenge branch using the parameters above. Copy the complete block into the JavaScript editor; no fixed sign-in credentials or service keys need to be added.

async function run(ctx) {
  const origin = 'https://www.scrapingcourse.com';
  const path = String(ctx.param('path', '/pagination'));
  const mode = String(ctx.param('mode', 'http'));
  const auto = ctx.param('auto_captcha', true) !== false;
  const seen = new Set();
  const collect = (html) => {
    const dom = ctx.dom(html);
    for (const card of dom.qa('.product-item, li.product')) {
      const name = card.q('.product-name, .woocommerce-loop-product__title')?.text().trim() ?? '';
      const key = card.q('a')?.attr('href') ?? name;
      if (!name || seen.has(key)) continue;
      const price = ctx.text().money(card.q('.product-price, .price')?.text() ?? '');
      const image = card.q('img')?.attr('src') ?? '';
      seen.add(key);
      ctx.emit({name, price, image}, key);
    }
    return dom.qa('.product-item, li.product').length;
  };
  if (mode === 'http') {
    let url = origin + path;
    const visited = new Set();
    while (url && visited.size < 50) {
      if (visited.has(url)) throw new Error('Pagination returned an already processed page');
      visited.add(url);
      const response = await ctx.http().withoutCache().get(url, auto);
      if (!response.ok()) throw new Error('The source could not be opened: ' + response.status);
      const html = response.body;
      collect(html);
      const next = ctx.dom(html).attr('a[rel="next"], a.next.page-numbers', 'href');
      url = next ? ctx.url().absolutize(url, next) : null;
    }
    if (url) throw new Error('The pagination safety limit was reached');
  } else {
    const page = await ctx.browser().open(origin + path, ['*/ajax/products*'], {}, auto);
    try {
      if (mode === 'login') {
        const dom = ctx.dom(await page.content());
        const hint = dom.qa('p').map(node => node.text()).find(text => text.includes('Demo Credentials:')) ?? '';
        const password = hint.match(/Password:\s*([^\s|]+)/i)?.[1];
        let email = hint.match(/Email:\s*([^\s|]+@[^\s|]+)/i)?.[1];
        const encoded = dom.attr('[data-cfemail]', 'data-cfemail');
        if (!email && encoded && encoded.length > 2) {
          const key = parseInt(encoded.slice(0, 2), 16);
          email = '';
          for (let i = 2; i < encoded.length; i += 2) email += String.fromCharCode(parseInt(encoded.slice(i, i + 2), 16) ^ key);
        }
        if (!email || !password) throw new Error('The page does not contain demo credentials');
        await page.type('#email', email);
        await page.type('#password', password);
        await page.click('button[type="submit"]');
      }
      if (mode === 'challenge') {
        const text = (ctx.dom(await page.content()).text('body') ?? '').replace(/\s+/g, ' ');
        if (!/You (?:successfully )?bypassed|successfully bypassed|Congratulations/i.test(text)) throw new Error('The expected page content was not received');
        ctx.emit({name: path, passed: true}, path);
      } else {
        await page.waitFor('.product-name:not(:empty)', 15000);
        if (mode === 'button') {
          let complete = false;
          for (let i = 0; i < 30; i++) {
            const before = ctx.dom(await page.content());
            collect(await page.content());
            const button = before.q('#load-more-btn');
            if (!button || /display:\s*none/.test(button.attr('style') ?? '')) { complete = true; break; }
            const offset = button.attr('data-offset');
            const count = before.qa('.product-item').length;
            await page.click('#load-more-btn');
            const response = await page.waitForResponse('*offset=' + offset, 15000);
            if (!response || (Number(response.status) < 200 || Number(response.status) >= 300) || response.truncated || response.error) throw new Error('The next batch could not be loaded');
            if (String(response.body ?? '').trim() === '') { complete = true; break; }
            await page.waitFor('.product-item:nth-child(' + (count + 1) + ')');
          }
          if (!complete) throw new Error('The batch loading safety limit was reached');
        }
        if (mode === 'scroll') {
          let stable = 0;
          let previous = 0;
          for (let i = 0; i < 40 && stable < 3; i++) {
            await page.scroll(3, 400);
            const count = collect(await page.content());
            stable = count === previous ? stable + 1 : 0;
            previous = count;
          }
          if (stable < 3) throw new Error('The feed is still loading: increase the scroll limit');
        }
        collect(await page.content());
      }
    } finally { await page.close(); }
  }
  const expected = Number(ctx.param('expected', 1));
  if (mode !== 'challenge' && seen.size !== expected) throw new Error('Incomplete result: ' + seen.size + ' instead of ' + expected);
}

Related reading

Shared algorithm and parameters · HTTP requests · Browser actions · Find HTML elements