ScreenshotNeo

BlogHow-to

How to Automatically Add Website Thumbnails to a Directory in WordPress

Generate screenshots for directory listings, save them as WordPress thumbnails, and display them in cards—with a working integration pattern and failure handling.

By the ScreenshotNeo team4 October 202610 min read

To automatically add website thumbnails to a WordPress directory, connect four pieces: each listing needs an external website URL, a screenshot capture step, a place to store and associate the resulting image, and a directory card template that displays it. For a directory built from posts or a custom post type, WordPress featured images are a natural association. If your directory plugin stores images in its own fields, save the screenshot there and update that plugin’s card template instead.

The details depend on your directory plugin: first identify its listing post type, URL field, image field, and save/update hooks. The example below is a starting integration for a custom post type named site_listing with URL metadata named _listing_url. Adapt those names to your site before enabling it.

1. Check how your directory stores listings

Before writing code, find these details in the directory plugin’s documentation or source:

  • Listing type: Is each directory entry a post or custom post type?
  • Website URL: Which field contains the external site URL?
  • Thumbnail field: Does the listing use the WordPress featured image, an attachment ID in custom metadata, or a URL?
  • Save hook: Can your integration run when a listing is first saved or when its URL changes?
  • Card template: Where is the public archive or card markup rendered?

WordPress’s featured image documentation describes featured images as representative images for posts, pages, and custom post types. The function the_post_thumbnail() displays the current post’s thumbnail. A screenshot shortcode inserted into post content does not automatically associate an image with every directory record or make a custom card template display it.

2. Choose where generated screenshots will live

For most WordPress directories, download the rendered image and add it to the Media Library. That gives the listing an attachment ID and lets the theme use the normal featured image functions and responsive image markup. A durable object store can also work if your directory and theme support that arrangement.

Do not assume a screenshot provider’s render URL is permanent. For example, Urlbox’s quickstart documentation says a returned render URL expires after 30 days unless the image is downloaded or saved to a cloud bucket. Plan a refresh or durable-storage strategy for whichever service you use.

3. Generate and attach a thumbnail when a listing is saved

This illustrative plugin uses a post-save hook, queues work for a later request, captures the listing URL through ScreenshotNeo, saves the returned WebP file into the Media Library, and assigns it as the featured image. It intentionally uses a sample post type and URL field; change both to match your directory. Install the code as a small site-specific plugin rather than editing a third-party plugin file.

<?php
/**
 * Plugin Name: Directory Website Thumbnails
 * Description: Creates a website screenshot for site_listing posts.
 */

add_action( 'save_post_site_listing', 'sneo_queue_listing_thumbnail', 20, 3 );
function sneo_queue_listing_thumbnail( $post_id, $post, $update ) {
    if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
        return;
    }
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }
    if ( 'publish' !== $post->post_status ) {
        return;
    }

    $url = get_post_meta( $post_id, '_listing_url', true );
    if ( ! is_string( $url ) || ! wp_http_validate_url( $url ) ) {
        return;
    }

    // Avoid scheduling duplicate jobs for repeated saves.
    if ( ! wp_next_scheduled( 'sneo_make_listing_thumbnail', array( $post_id ) ) ) {
        wp_schedule_single_event( time() + 5, 'sneo_make_listing_thumbnail', array( $post_id ) );
    }
}

add_action( 'sneo_make_listing_thumbnail', 'sneo_create_listing_thumbnail' );
function sneo_create_listing_thumbnail( $post_id ) {
    $post = get_post( $post_id );
    if ( ! $post || 'publish' !== $post->post_status ) {
        return;
    }

    $url = get_post_meta( $post_id, '_listing_url', true );
    if ( ! is_string( $url ) || ! wp_http_validate_url( $url ) ) {
        update_post_meta( $post_id, '_thumbnail_capture_status', 'invalid_url' );
        return;
    }

    // Store the key in wp-config.php or a secrets manager, not post metadata.
    if ( ! defined( 'SCREENSHOTNEO_API_KEY' ) || ! SCREENSHOTNEO_API_KEY ) {
        update_post_meta( $post_id, '_thumbnail_capture_status', 'missing_api_key' );
        return;
    }

    $endpoint = add_query_arg(
        array(
            'access_key' => SCREENSHOTNEO_API_KEY,
            'url'        => $url,
            'format'     => 'webp',
            'image_width' => 640,
        ),
        'https://api.screenshotneo.com/v1/shot'
    );

    $response = wp_remote_get( $endpoint, array( 'timeout' => 90 ) );
    if ( is_wp_error( $response ) ) {
        update_post_meta( $post_id, '_thumbnail_capture_status', 'request_error' );
        return;
    }

    $status = wp_remote_retrieve_response_code( $response );
    $body   = wp_remote_retrieve_body( $response );
    if ( 200 !== $status || '' === $body ) {
        update_post_meta( $post_id, '_thumbnail_capture_status', 'capture_failed' );
        return;
    }

    // Save a temporary file, then let WordPress copy it into uploads.
    $tmp = wp_tempnam( 'listing-shot.webp' );
    if ( ! $tmp || false === file_put_contents( $tmp, $body ) ) {
        update_post_meta( $post_id, '_thumbnail_capture_status', 'file_error' );
        return;
    }

    require_once ABSPATH . 'wp-admin/includes/file.php';
    require_once ABSPATH . 'wp-admin/includes/media.php';
    require_once ABSPATH . 'wp-admin/includes/image.php';

    $file = array(
        'name'     => 'listing-' . absint( $post_id ) . '.webp',
        'type'     => 'image/webp',
        'tmp_name' => $tmp,
        'error'    => 0,
        'size'     => filesize( $tmp ),
    );
    $attachment_id = media_handle_sideload( $file, $post_id, 'Website thumbnail' );
    if ( is_wp_error( $attachment_id ) ) {
        @unlink( $tmp );
        update_post_meta( $post_id, '_thumbnail_capture_status', 'media_error' );
        return;
    }

    set_post_thumbnail( $post_id, $attachment_id );
    update_post_meta( $post_id, '_thumbnail_capture_status', 'complete' );
    update_post_meta( $post_id, '_thumbnail_source_url', esc_url_raw( $url ) );
}

Set the key in wp-config.php above the “stop editing” line:

define( 'SCREENSHOTNEO_API_KEY', 'YOUR_API_KEY' );

The save hook only handles newly saved or updated published records. For a CSV import or hundreds of existing listings, use a background queue or a batch process rather than making one slow screenshot request during each import row. Add retry scheduling for temporary failures, rate-limit according to your account and workflow, and avoid replacing a working thumbnail until a new capture succeeds. The WordPress scheduled-event mechanism depends on site activity; sites requiring predictable job execution should configure a system scheduler to trigger WordPress cron.

4. Display the thumbnail in the directory card

If the directory’s post type supports featured images, declare that support in the site theme during setup. WordPress documents that thumbnail support must be declared before the init hook, commonly through after_setup_theme; it can be scoped to the relevant post type using add_theme_support().

add_action( 'after_setup_theme', function () {
    add_theme_support( 'post-thumbnails', array( 'site_listing' ) );
} );

In the directory card template, output the thumbnail with a fallback:

<?php if ( has_post_thumbnail() ) : ?>
    <?php the_post_thumbnail( 'medium', array( 'loading' => 'lazy' ) ); ?>
<?php else : ?>
    <div class="listing-thumbnail listing-thumbnail--empty" aria-hidden="true"></div>
<?php endif; ?>

Use the image size appropriate for the card layout. WordPress accepts a registered size name or width and height dimensions; see the function reference. For custom image fields, read the attachment ID or image URL saved by the directory plugin and render it in the plugin’s documented card template override. Do not modify vendor plugin files, since updates can overwrite those changes.

5. Use a screenshot API or a WordPress plugin

There are three common ways to implement the capture step:

  • Screenshot API integration: Best suited when listings are imported, need automatic refresh, or require custom retry and storage behavior. You connect a listing save/update event to capture and persist the result.
  • Screenshot shortcode plugin: Urlbox’s WordPress plugin documents embedding a screenshot of a URL using a shortcode and requires an Urlbox account and API credentials. It is useful for placing a known URL screenshot in content; confirm that it fits your directory’s data model before relying on it for automatic listing cards.
  • Dashboard screenshot plugin: Page Preview describes screenshots of your own published pages in dashboard post listings. That is an admin workflow; it does not establish automatic screenshots of external sites for public directory cards.

Compare solutions by checking URL and image-field support, save hooks, synchronous versus asynchronous capture, where output persists, refresh behavior, request quotas and cost, and whether target sites block capture or show region-specific content. Do not assume a plugin supports an arbitrary directory plugin until its documentation confirms the integration.

6. Or skip the browser setup

ScreenshotNeo can capture a website URL with one API request and return an image. The code below follows the ScreenshotNeo API documentation; this cURL example writes a WebP response to a file:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await (await import('node:fs/promises')).writeFile('shot.webp', bytes);

In the WordPress integration above, use the same endpoint when the listing is saved, then persist the image in the Media Library. ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Sign up for free and get 1,000 screenshots a month with no card.

7. Handle updates, failures, and bulk imports

  • URL changed: Compare the new URL to the saved source URL and enqueue a fresh capture. Keep the old image until the replacement has been captured and stored.
  • Site is unavailable or blocks rendering: Record the failure state, retain a fallback, and retry later with a limit rather than retrying every page view.
  • Large import: Queue jobs, track pending/success/failure states, and process in batches. Do not hold an importer or visitor request open while a browser renders every URL.
  • Image refresh: Choose a refresh policy, such as recapturing only when the URL changes or refreshing on a schedule. The right interval depends on how often directory sites change.
  • Privacy and credentials: Keep API keys on the server, not in browser JavaScript or public image URLs. Validate listing URLs and avoid accepting arbitrary internal addresses from untrusted users.
  • Image layout: Use a consistent crop and aspect ratio in CSS, reserve card space to reduce layout shifts, and include a meaningful alternative text strategy if the image conveys information.

Troubleshooting

Symptom Likely cause What to do
No thumbnail appears The post type lacks thumbnail support, the save hook did not run, or the card template reads a different field. Check the listing post type, enable thumbnail support before init, inspect capture status metadata, and confirm the card template uses the stored attachment or custom field.
Capture job never runs WordPress scheduled events have not been triggered, or the post is not published. Confirm the save conditions and scheduled event. For reliable processing on a low-traffic site, configure a system scheduler to invoke WordPress cron.
Invalid URL or request rejected The listing field is empty, malformed, or not a public URL accepted by the capture service. Normalize and validate the URL before enqueueing. Check redirects and the service’s restrictions on private/internal addresses.
Image file is empty or corrupt The response may contain an error instead of image bytes, or the request timed out. Check HTTP status and response headers before sideloading; record the provider’s error details securely and retry temporary failures.
Media Library upload fails Temporary file permissions, unsupported file handling, or insufficient disk space. Check PHP and WordPress upload limits, writable temporary/uploads directories, and disk space; retain the previous thumbnail.
Old screenshot remains after URL change The integration did not detect the URL change or no refresh task was queued. Store the source URL alongside the attachment and compare it on save; enqueue a replacement only when needed.
Cards have inconsistent crop or size Different source aspect ratios or image sizes are being rendered. Use a consistent registered image size or fixed aspect-ratio container with object-fit: cover.

Performance, reliability, and cost

Screenshot capture is a network operation and can take much longer than a normal WordPress database update. Running it in a background task keeps listing submission responsive. For large directories, store a job state and retry only transient failures with a maximum attempt count. Cache each successful capture and regenerate only when the URL or chosen capture settings change.

Estimate capture volume before selecting a provider: initial directory population plus URL-change recaptures and scheduled refreshes. ScreenshotNeo’s stated plans are Free: 1,000 shots per month with no card; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; Business: $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Only clean shots are billed; bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, with response headers identifying the page verdict and billing state. Check current terms before purchase.

FAQ

Yes. Save the generated file as a Media Library attachment, then assign its ID as the listing’s featured image if the post type and theme support thumbnails.

Will a screenshot shortcode fill every directory card automatically?

Not by itself. A shortcode embeds content where it is rendered; automatic directory thumbnails also require listing-level URL/image association and card-template output.

Should screenshots be regenerated on every page load?

No. Generate on listing creation, URL changes, or an intentional refresh schedule, then reuse the stored image.

Can I use this exact code with any directory plugin?

No. The example assumes the post type and metadata names shown. Adapt the hook, URL field, and image storage to the plugin’s documented integration points.