@@ -43,6 +43,12 @@ interface InterpreterOptions {
4343 binaryCallback : ( output : any , mimeType : string ) => ( void | Promise < void > ) ;
4444 debug : boolean ;
4545 type ?: 'extract' | 'scrape' | 'crawl' | 'search' | 'doc-extract' | 'doc-parse' ;
46+ /**
47+ * This outputs the user's pereference for screenshooting a website
48+ * This let's the crawl/search capture the screenshot during the first pass instead
49+ * of having to go and revisit every page afterwards (#1105). This reduces our 2N scan time.
50+ */
51+ formats ?: string [ ] ;
4652 debugChannel : Partial < {
4753 activeId : ( id : number ) => void ,
4854 debugMessage : ( msg : string ) => void ,
@@ -385,14 +391,16 @@ export default class Interpreter extends EventEmitter {
385391 * Returns the optimal Playwright `waitUntil` navigation strategy based on
386392 * whether the current operation requires visual rendering.
387393 *
388- * - `'networkidle'` — used when screenshots are requested; waits for all
389- * sub-resources so the page renders correctly.
394+ * - `'networkidle'` — used when `visualRenderRequired` (extract mode) is set;
395+ * waits for all sub-resources so the page renders correctly.
396+ * Screenshot capture during crawl/search handles its own
397+ * networkidle wait separately, in captureScreenshotsForCurrentPage.
390398 * - `'domcontentloaded'` — used for all DOM-only operations (scraping, crawling,
391399 * extraction, search); skips stylesheet/image loading for
392400 * maximum speed.
393401 *
394- * @param blockOverride Pass `true` when the caller will take a screenshot
395- * or requires styled layout . Defaults to `false`.
402+ * @param blockOverride Pass `true` to force a fully-rendered wait regardless of
403+ * `visualRenderRequired` . Defaults to `false`.
396404 */
397405 private getNavigationWaitStrategy (
398406 blockOverride ?: boolean
@@ -426,6 +434,64 @@ export default class Interpreter extends EventEmitter {
426434 } ) ;
427435 }
428436
437+ /**
438+ * If user selected either screenshot-visible or screenshot-fullpage output format,
439+ * this returns true.
440+ */
441+ private screenshotFormatsRequested ( ) : boolean {
442+ const formats = this . options . formats ?? [ ] ;
443+ return formats . includes ( 'screenshot-visible' ) || formats . includes ( 'screenshot-fullpage' ) ;
444+ }
445+
446+ private async captureScreenshotsForCurrentPage (
447+ page : Page ,
448+ keyPrefix : 'crawl' | 'search' ,
449+ pageNumber : number ,
450+ ) : Promise < { screenshotVisible ?: string ; screenshotFullpage ?: string } > {
451+ const keys : { screenshotVisible ?: string ; screenshotFullpage ?: string } = { } ;
452+ if ( ! this . screenshotFormatsRequested ( ) ) return keys ;
453+
454+ const formats = this . options . formats ?? [ ] ;
455+
456+ // Bounded, non-fatal render-readiness before screenshotting.
457+ // A capped networkidle race lets never-idle pages (chat/ads/websockets) settle
458+ // without hanging navigation; timeouts here are swallowed, never fatal. Navigation
459+ // stays on the fast domcontentloaded path (see the crawl/search goto calls).
460+ await page . waitForLoadState ( 'networkidle' , { timeout : 10000 } ) . catch ( ( ) => { } ) ;
461+ await this . waitForImagesLoaded ( page ) ;
462+ await new Promise ( resolve => setTimeout ( resolve , 1000 ) ) ;
463+
464+ if ( formats . includes ( 'screenshot-visible' ) ) {
465+ try {
466+ const buffer = await page . screenshot ( { type : 'png' } ) ;
467+ const key = `${ keyPrefix } -${ pageNumber } -screenshot-visible` ;
468+ await this . options . binaryCallback (
469+ { name : key , data : buffer , mimeType : 'image/png' } ,
470+ 'image/png' ,
471+ ) ;
472+ keys . screenshotVisible = key ;
473+ } catch ( error : any ) {
474+ this . log ( `Failed to capture visible screenshot of ${ page . url ( ) } : ${ error . message } ` , Level . WARN ) ;
475+ }
476+ }
477+
478+ if ( formats . includes ( 'screenshot-fullpage' ) ) {
479+ try {
480+ const buffer = await page . screenshot ( { type : 'png' , fullPage : true } ) ;
481+ const key = `${ keyPrefix } -${ pageNumber } -screenshot-fullpage` ;
482+ await this . options . binaryCallback (
483+ { name : key , data : buffer , mimeType : 'image/png' } ,
484+ 'image/png' ,
485+ ) ;
486+ keys . screenshotFullpage = key ;
487+ } catch ( error : any ) {
488+ this . log ( `Failed to capture full-page screenshot of ${ page . url ( ) } : ${ error . message } ` , Level . WARN ) ;
489+ }
490+ }
491+
492+ return keys ;
493+ }
494+
429495 /**
430496 * Returns true if any of the remaining blocks in the workflow require a visual render
431497 * before the next page navigation.
@@ -1484,6 +1550,12 @@ export default class Interpreter extends EventEmitter {
14841550 pageResult . metadata . depth = depth ;
14851551 crawlResults . push ( pageResult ) ;
14861552
1553+ // Screenshots are taken here instead of 2N after every crawl (#1105).
1554+ Object . assign (
1555+ pageResult ,
1556+ await this . captureScreenshotsForCurrentPage ( page , 'crawl' , crawlResults . length ) ,
1557+ ) ;
1558+
14871559 this . log ( `✓ Scraped ${ url } (${ pageResult . wordCount } words, depth ${ depth } )` , Level . LOG ) ;
14881560
14891561 if ( crawlConfig . followLinks && depth < crawlConfig . maxDepth ) {
@@ -1830,8 +1902,8 @@ export default class Interpreter extends EventEmitter {
18301902 await page . goto ( result . url , {
18311903 waitUntil : this . getNavigationWaitStrategy ( ) ,
18321904 timeout : 30000
1833- } ) . catch ( ( ) => {
1834- this . log ( `Failed to navigate to ${ result . url } , skipping...` , Level . WARN ) ;
1905+ } ) . catch ( ( err ) => {
1906+ throw new Error ( `Navigation failed: ${ err . message } ` ) ;
18351907 } ) ;
18361908
18371909 await this . waitForPageReady ( page , currentWorkflow || [ ] ) ;
@@ -1883,7 +1955,7 @@ export default class Interpreter extends EventEmitter {
18831955 } ;
18841956 } ) ;
18851957
1886- scrapedResults . push ( {
1958+ const scrapedEntry = {
18871959 searchResult : {
18881960 query : searchConfig . query ,
18891961 position : result . position ,
@@ -1900,7 +1972,14 @@ export default class Interpreter extends EventEmitter {
19001972 links : pageData . links ,
19011973 wordCount : pageData . wordCount ,
19021974 scrapedAt : new Date ( ) . toISOString ( )
1903- } ) ;
1975+ } ;
1976+ scrapedResults . push ( scrapedEntry ) ;
1977+
1978+ // Screenshots are taken here instead of 2N after every crawl (#1105).
1979+ Object . assign (
1980+ scrapedEntry ,
1981+ await this . captureScreenshotsForCurrentPage ( page , 'search' , i + 1 ) ,
1982+ ) ;
19041983
19051984 this . log ( `✓ Scraped ${ result . url } (${ pageData . wordCount } words)` , Level . LOG ) ;
19061985
0 commit comments