Images and Media API
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages. |
This page is the developer’s view of images in WordPress: how a plugin registers image sizes, prints responsive
<img> tags, creates attachments from uploaded or downloaded files, reads and extends attachment metadata, and edits
images with WP_Image_Editor. The site owner’s view (the Media screens, allowed types, big-image scaling, the 7.1
client-side processing, upload limits and alt text) is in Media Library, and
the loading attributes that core adds by itself are described from the performance angle in
Performance and Caching. Responsive images,
formats and lazy loading outside WordPress are covered in Media Optimization for
the Web.
The page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the
version to run in production), PHP 7.4 minimum and 8.3+ recommended, and WP-CLI 2.12.0, and checked on 2026-10-28.
WordPress 7.2 is in beta and due in December 2026; nothing on this page is announced
to change with it. Every example was run in the official wordpress:7.1.3-php8.4-apache image (PHP 8.4, Imagick
with ImageMagick 7.1, and GD) with MariaDB 11.8 and WP-CLI inside the web container, so it uses the same image
libraries as uploads. The code belongs to the lumen-catalog plugin, whose wp lumen import fetches book covers.
The functions at a glance
An image attachment is a post of type attachment with three meta keys (_wp_attached_file,
_wp_attachment_metadata, _wp_attachment_image_alt), as described in
Media Library. Code reads them through functions:
| Function | Returns |
|---|---|
|
A complete |
|
|
|
URL and path of the "full" file (the |
|
The untouched upload, before scaling or format conversion |
|
Read and write the metadata array (both filtered) |
|
The featured image of a post (its |
The featured image of an lp_book is its cover. An <img> built from wp_get_attachment_url() would lose srcset,
the dimensions that prevent layout shift and every loading attribute.
Registering image sizes
add_image_size( $name, $width, $height, $crop ) (2.9) adds a size that WordPress generates for every image uploaded
afterwards. A width or height of 0 means unconstrained. The crop argument decides how the image fits the box:
$crop |
Result |
|---|---|
|
Soft: scaled to fit inside the box. A 1600 × 2400 cover in a 300 × 300 box becomes 200 × 300. |
|
Hard: scaled to cover the box and cropped to exactly its size, keeping the centre. |
|
Hard, keeping the given position: x is |
WordPress never upscales: a size is made only when the original is larger than the box. set_post_thumbnail_size()
registers post-thumbnail. Sizes live in a global array, so they are registered on every request (after_setup_theme).
Covers are 2:3 portraits shown at 600 px on the book page and 300 px on cards. The plugin registers both, offers the
larger one in the editor’s resolution control, and skips the two very large core sizes for covers:
<?php
defined( 'ABSPATH' ) || exit;
// 600 x 900 repeats the site's must-use plugin so the plugin works without it; re-registering is harmless.
function lumen_catalog_image_sizes() {
add_image_size( 'lp-book-cover', 600, 900, true );
add_image_size( 'lp-book-cover-small', 300, 450, array( 'center', 'top' ) );
}
add_action( 'after_setup_theme', 'lumen_catalog_image_sizes' );
// Offers the large cover size in the editor's "Resolution" control.
function lumen_catalog_size_names( $sizes ) {
$sizes['lp-book-cover'] = __( 'Book cover', 'lumen-catalog' );
return $sizes;
}
add_filter( 'image_size_names_choose', 'lumen_catalog_size_names' );
// A cover is an image attached to a book.
function lumen_catalog_is_cover( $attachment_id ) {
$parent = wp_get_post_parent_id( $attachment_id );
return $parent && 'lp_book' === get_post_type( $parent );
}
// Covers never appear wider than 600 px: skip the two very large core sizes.
function lumen_catalog_skip_cover_sizes( $sizes, $image_meta, $attachment_id ) {
if ( lumen_catalog_is_cover( $attachment_id ) ) {
unset( $sizes['1536x1536'], $sizes['2048x2048'] );
}
return $sizes;
}
add_filter( 'intermediate_image_sizes_advanced', 'lumen_catalog_skip_cover_sizes', 10, 3 );
// The 7.1 client-side path asks for missing sizes through this filter instead.
add_filter( 'wp_get_missing_image_subsizes', 'lumen_catalog_skip_cover_sizes', 10, 3 );
Two filters are needed because the upload paths ask different questions: wp_create_image_subsizes() on the server
filters the list through intermediate_image_sizes_advanced ($attachment_id since 5.3), while the REST API behind
the 7.1 client-side processing asks wp_get_missing_image_subsizes(); with only the first filter, the cover imported
below reports 1536x1536, 2048x2048 as missing. post_parent is set before the sizes are made when a file is uploaded
or sideloaded to a book; an image uploaded in the Library and chosen as featured image later gets every size. WP-CLI
2.12’s wp media image-size prints a crop-position array as soft, although the files are cropped to exactly 300 × 450.
Printing images: srcset, sizes and sizes="auto"
wp_get_attachment_image() adds, in order: src, width, height, class (attachment-<size> size-<size>) and
alt; the loading attributes; srcset and sizes; auto in sizes for lazy images; then it runs the
wp_get_attachment_image_attributes filter. Attributes in $attr win, and false for loading or fetchpriority
removes the attribute.
srcset (4.4) comes from wp_calculate_image_srcset(): every generated file of the same image whose aspect ratio
matches the requested size within one pixel, including "full", but not files wider than max_srcset_image_width (2048
px) unless that file is the src. A cover with no other 2:3 file gets no srcset. sizes comes from
wp_calculate_image_sizes(), whose default (max-width: <width>px) 100vw, <width>px is right only for an image as wide
as its column. A cover in a two-column grid needs its own value (in $attr, or for all images through the
wp_calculate_image_sizes filter), or phones download a file twice as wide as needed.
// Returns the cover of a book as an <img> tag, or ''. $attr can add 'loading', 'fetchpriority', etc.
function lumen_catalog_cover_html( $book_id, $size = 'lp-book-cover-small', array $attr = array() ) {
$attachment_id = get_post_thumbnail_id( $book_id );
if ( ! $attachment_id ) {
return '';
}
// Cards are two per row on phones and 300 px wide from 700 px up.
$defaults = array( 'sizes' => '(max-width: 700px) 45vw, 300px', 'class' => 'lp-book-card__cover' );
if ( '' === get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) {
/* translators: %s: book title. */
$defaults['alt'] = sprintf( __( 'Cover of %s', 'lumen-catalog' ), get_the_title( $book_id ) );
}
return wp_get_attachment_image( $attachment_id, $size, false, array_merge( $defaults, $attr ) );
}
The render.php of lumen/latest-books calls it in its WP_Query loop (see
Static and Dynamic Blocks). For "The Lighthouse Ledger" (book 6,
cover 7) the card call returns (line breaks added, upload URLs shortened to …/):
wp eval 'echo lumen_catalog_cover_html( 6 );' --url=https://lumenpress.test
<img width="300" height="450" src="…/2026/10/978-1-23456-789-7-300x450.jpg" class="lp-book-card__cover"
alt="Cover of The Lighthouse Ledger" sizes="auto, (max-width: 700px) 45vw, 300px" decoding="async" loading="lazy"
srcset="…/978-1-23456-789-7-300x450.jpg 300w, …/978-1-23456-789-7-200x300.jpg 200w,
…/978-1-23456-789-7-683x1024.jpg 683w, …/978-1-23456-789-7-768x1152.jpg 768w,
…/978-1-23456-789-7-600x900.jpg 600w, …/978-1-23456-789-7.jpg 1600w" />
The src file comes first, the 150 × 150 thumbnail is missing (other ratio), and the 1600 px "full" file is in (not
wider than 2048 px). The single-book template passes lp-book-cover, (max-width: 700px) 90vw, 600px,
fetchpriority => 'high' and loading => false, and gets no loading and therefore no auto.
For a lazy image the browser knows the rendered width when the download starts, so sizes="auto" lets it pick the
candidate from the real layout. Since 6.7, wp_get_attachment_image() and the content filters
(wp_img_tag_add_auto_sizes()) prepend auto, when the image has loading="lazy", a width and a sizes, and
auto is not already first. Browsers without support skip it, so the explicit value still
matters. Since 6.7.1 core also prints a contain-intrinsic-size rule for such images (the wp-img-auto-sizes-contain
style since 6.9). Returning false from the wp_img_tag_add_auto_sizes filter (6.7.1) removes both.
Loading attributes and their context
wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) (6.3) decides loading, fetchpriority and
decoding; the rules (threshold of three, 50,000-pixel minimum) are in
Performance and Caching. For code, the context and
the place of the call matter. The same 600 × 900 image, outside the main loop:
wp eval '$a = array( "width" => 600, "height" => 900 );
foreach ( array( "wp_get_attachment_image", "template_part_header", "template" ) as $c ) {
echo str_pad( $c, 24 ), json_encode( wp_get_loading_optimization_attributes( "img", $a, $c ) ), "\n"; }'
wp_get_attachment_image {"decoding":"async","loading":"lazy"}
template_part_header {"decoding":"async","fetchpriority":"high"}
template []
-
Only images in the main query’s loop count against the threshold. An image in a secondary query, such as the one in
lumen/latest-books, is lazy unless it is in the header template part or before the loop of a classic template, so a cover above the fold needsfetchpriority => 'high'orloading => falsefrom the caller. -
Without both
widthandheightonlydecodingis added. -
fetchpriority="low"(7.0) marks initially hidden images (navigation overlay, later carousel slides) and keeps them eager;fetchpriority="auto"(7.0), added by the block visibility support, keeps an image out of the count. -
An image printed while
the_contentis being filtered (a shortcode, a block) gets no attributes in its own call; the content filter adds them later, in document order with the other content images. -
Filters:
wp_get_attachment_image_context,pre_wp_get_loading_optimization_attributes(short-circuit) andwp_get_loading_optimization_attributes.
Creating attachments from code
Three functions in wp-admin/includes/media.php turn a file into an attachment with its sizes and return the ID or a
WP_Error. Outside admin screens, code first requires media.php, file.php and image.php from that folder.
| Function | Use |
|---|---|
|
A file just posted in a form; |
|
A file already on the server, as a |
|
An image at a URL: |
All three run the upload checks of wp_handle_upload() / wp_handle_sideload(), file the upload under the parent
post’s date (media_handle_upload() makes an exception for pages), so a cover attached to a book published in 2024 lands
in uploads/2024/<month>/, and take the title, caption and (since 7.0, through wp_get_image_alttext()) alt text
embedded in the image unless $desc or $post_data overrides them. Covers delivered as files on disk use
media_handle_sideload() with a copy, because it moves tmp_name away:
$tmp = wp_tempnam( $source ); // $source: for example ABSPATH . 'import/978-1-23456-791-0.jpg'.
copy( $source, $tmp );
$attachment_id = media_handle_sideload( array( 'name' => wp_basename( $source ), 'tmp_name' => $tmp ), $book_id, $title );
if ( is_wp_error( $attachment_id ) ) {
@unlink( $tmp ); // Left behind on failure.
}
Importing a cover from a URL in wp lumen import
The import function fires lumen_catalog_book_imported after each new book (see
Hooks: Actions and Filters). The distributor’s rows carry a
cover_url; the plugin downloads it, makes it the featured image, and on a re-run reuses the attachment it already made
from the same URL:
<?php
defined( 'ABSPATH' ) || exit;
// Downloads a cover and makes it the featured image of a book. Returns the attachment ID or a WP_Error.
function lumen_catalog_sideload_cover( $book_id, $url ) {
$url = esc_url_raw( $url, array( 'https', 'http' ) );
if ( '' === $url ) {
return new WP_Error( 'lumen_catalog_cover_url', __( 'Invalid cover URL.', 'lumen-catalog' ) );
}
// No duplicates on a re-run: media_sideload_image() records the URL in _source_url.
$existing = get_posts(
array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'fields' => 'ids', 'numberposts' => 1,
'meta_key' => '_source_url', 'meta_value' => $url )
);
if ( $existing ) {
set_post_thumbnail( $book_id, $existing[0] );
return $existing[0];
}
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$title = get_the_title( $book_id );
$attachment_id = media_sideload_image( $url, $book_id, $title, 'id' );
if ( is_wp_error( $attachment_id ) ) {
return $attachment_id;
}
/* translators: %s: book title. */
update_post_meta( $attachment_id, '_wp_attachment_image_alt', sprintf( __( 'Cover of %s', 'lumen-catalog' ), $title ) );
set_post_thumbnail( $book_id, $attachment_id );
return $attachment_id;
}
function lumen_catalog_import_cover( $book_id, $row ) {
if ( empty( $row['cover_url'] ) ) {
return;
}
$result = lumen_catalog_sideload_cover( $book_id, $row['cover_url'] );
if ( is_wp_error( $result ) && defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::warning( sprintf( 'Cover for book %d: %s', $book_id, $result->get_error_message() ) );
}
}
add_action( 'lumen_catalog_book_imported', 'lumen_catalog_import_cover', 10, 2 );
media_sideload_image() accepts only URL paths ending in .jpg, .jpeg, .jpe, .png, .gif or .webp; an AVIF
or extension-less URL fails with "Invalid image URL." The image_sideload_extensions filter (5.6) adds avif;
extension-less URLs need download_url() plus media_handle_sideload() with a chosen name.
download_url() uses wp_safe_remote_get(), which refuses hosts on private or loopback addresses. In the Docker stack
the distributor mock (covers.distributor.example) is a container on a private address, so the first run printed
Warning: Cover for book 5: A valid URL was not provided. On local and staging sites only, a must-use plugin allows
exactly that host; production talks to the public distributor and does not have it:
<?php
/* Plugin Name: Lumen Local Distributor (local and staging only) */
defined( 'ABSPATH' ) || exit;
// Allow exactly the mock's host, never all private hosts.
function lumen_local_distributor_host( $external, $host ) {
return $external || 'covers.distributor.example' === $host;
}
add_filter( 'http_request_host_is_external', 'lumen_local_distributor_host', 10, 2 );
With it, the import rows for "The Lighthouse Ledger" and "The Glass Orchard" created books 6 and 8 with covers 7 and 9,
each with its alt text, and a second call for book 6 with the same URL returned 7 and created nothing. The command
is in WP-CLI, the HTTP API in
HTTP and Filesystem APIs.
Attachment metadata
wp_generate_attachment_metadata( $id, $file ) (wp-admin/includes/image.php) builds the metadata: for images it
scales big originals, creates the sub-sizes and reads EXIF/IPTC; for PDFs it renders previews; for audio and video it
reads length and codecs. Uploads save it with wp_update_attachment_metadata(), and code that replaces a file in place
calls the same pair. For cover 7 (wp eval over wp_get_attachment_metadata( 7 )):
2026/10/978-1-23456-789-7.jpg 1600x2400 filesize 88037
medium 978-1-23456-789-7-200x300.jpg 200x300 image/jpeg
large 978-1-23456-789-7-683x1024.jpg 683x1024 image/jpeg
thumbnail 978-1-23456-789-7-150x150.jpg 150x150 image/jpeg
medium_large 978-1-23456-789-7-768x1152.jpg 768x1152 image/jpeg
lp-book-cover 978-1-23456-789-7-600x900.jpg 600x900 image/jpeg
lp-book-cover-small 978-1-23456-789-7-300x450.jpg 300x450 image/jpeg
parent: 6, _source_url: http://covers.distributor.example/covers/978-1-23456-789-7.jpg
The array holds width, height, file (relative to the uploads directory), filesize (6.0), sizes (each with
file, width, height, mime-type, filesize), image_meta (camera, caption, copyright, keywords, orientation,
alt) and, after scaling, rotation or conversion, original_image. The REST API exposes it as media_details; see
REST API: Using It.
Extending metadata: wp_generate_attachment_metadata
The wp_generate_attachment_metadata filter receives the metadata, the attachment ID and (5.3) a context: 'create'
for a new file, 'update' when wp_update_image_subsizes() completes the sizes later, after a timed-out upload or in
the finalize step of the 7.1 client-side upload, which fires it a second time for the same file. It also runs on
every wp media regenerate. Callbacks must therefore be idempotent, and must always return the array: returning nothing
erases the metadata, and the image loses its sizes and srcset. Lumen Press flags covers too small for 600 × 900:
<?php
defined( 'ABSPATH' ) || exit;
// Flags covers too small for the 600 x 900 size, so the catalogue team can ask for a better file.
function lumen_catalog_check_cover( $metadata, $attachment_id, $context ) {
if ( ! lumen_catalog_is_cover( $attachment_id ) || empty( $metadata['width'] ) ) {
return $metadata;
}
// Idempotent: the same file always leaves the same meta, whichever pass runs.
if ( $metadata['width'] < 600 || $metadata['height'] < 900 ) {
update_post_meta( $attachment_id, 'lp_cover_too_small', 1 );
} else {
delete_post_meta( $attachment_id, 'lp_cover_too_small' );
}
return $metadata;
}
add_filter( 'wp_generate_attachment_metadata', 'lumen_catalog_check_cover', 10, 3 );
Importing "The Quiet Lighthouse", whose cover is only 400 × 600, with the WebP mapping from
Output formats: image_editor_output_format active, then printing its metadata and flag:
book 12, cover 13: 2026/10/978-1-23456-791-0.webp 400x600, sizes: medium, thumbnail, lp-book-cover-small
lp_cover_too_small: '1'
Keys a plugin adds to the array are lost on the next regeneration; derived values belong in post meta, as here.
Editing images with WP_Image_Editor
WP_Image_Editor (3.5) is the abstract class behind every resize, crop and rotation in core, implemented by
WP_Image_Editor_Imagick and WP_Image_Editor_GD. wp_get_image_editor( $path, $args ) loads the file with the first
class in the wp_image_editors filter (Imagick, then GD) that passes test(), supports the input type and has the
methods in $args['methods'], preferring one that can write the mapped output format; otherwise it returns
image_no_editor. wp_image_editor_supports( $args ) asks without a file.
| Method | Does |
|---|---|
|
Scales into the box; |
|
Several sizes from one load; region and orientation |
|
Compression quality (1–100) and current dimensions |
|
Writes the file; returns |
|
Builds a |
On the official image both libraries handle JPEG, PNG, GIF, WebP and AVIF (GD is the bundled 2.1 with AVIF), and only
Imagick reads HEIC, which is why HEIC uploads need it. wp_image_editor_supports( array( 'mime_type' => 'image/heic' ) )
returned true, and false with wp_image_editors reduced to WP_Image_Editor_GD. Imagick keeps colour profiles
and allocates outside PHP’s memory_limit; GD decodes the bitmap into PHP memory at about 4 bytes per pixel (roughly
100 MB for 6000 × 4000). If ImageMagick lacks a delegate (WebP, say), core falls back to GD for that type. Tools >
Site Health > Info > Media Handling shows what a server has.
Source: Code Reference: wp_image_editors
A share image for each cover
Book pages announce a 1200 × 630 image to social networks (og:image). The plugin derives it from the cover on
demand instead of registering a size, which every image on the site would get:
<?php
defined( 'ABSPATH' ) || exit;
// Creates a 1200 x 630 WebP share image from a cover and lists it as the 'lp-share' size.
function lumen_catalog_make_share_image( $attachment_id ) {
// Start from the untouched upload, not the "-scaled" copy.
$source = wp_get_original_image_path( $attachment_id );
if ( ! $source ) {
return new WP_Error( 'lumen_catalog_not_image', __( 'Not an image.', 'lumen-catalog' ) );
}
$editor = wp_get_image_editor( $source, array( 'methods' => array( 'resize', 'save' ) ) );
if ( is_wp_error( $editor ) ) {
return $editor;
}
if ( ! $editor::supports_mime_type( 'image/webp' ) ) {
return new WP_Error( 'lumen_catalog_no_webp', __( 'The image editor cannot write WebP.', 'lumen-catalog' ) );
}
// Hard crop, keeping the top of the cover (title and author name).
$resized = $editor->resize( 1200, 630, array( 'center', 'top' ) );
if ( is_wp_error( $resized ) ) {
return $resized;
}
// set_quality( 80 ) would be reset when save() converts to WebP, so filter the quality instead.
$quality = static function ( $value, $mime_type ) {
return 'image/webp' === $mime_type ? 80 : $value;
};
add_filter( 'wp_editor_set_quality', $quality, 10, 2 );
$saved = $editor->save( $editor->generate_filename( 'share', null, 'webp' ), 'image/webp' );
remove_filter( 'wp_editor_set_quality', $quality, 10 );
if ( is_wp_error( $saved ) ) {
return $saved;
}
// Listed under 'sizes', the file is deleted with the attachment and found by wp_get_attachment_image_url().
$meta = wp_get_attachment_metadata( $attachment_id );
$meta['sizes']['lp-share'] = array(
'file' => $saved['file'],
'width' => $saved['width'],
'height' => $saved['height'],
'mime-type' => $saved['mime-type'],
);
wp_update_attachment_metadata( $attachment_id, $meta );
return $saved;
}
Source: Code Reference: WP_Image_Editor
For cover 7 it returns 978-1-23456-789-7-share.webp (1200 × 630, image/webp), and
wp_get_attachment_image_url( 7, 'lp-share' ) returns its URL; its ratio keeps it out of srcset. A full
wp media regenerate drops the entry (the file stays), so callers recreate it when the URL comes back empty.
The quality detail is a real trap: when save() converts to another format, the editor resets the quality to that
format’s default (82 for JPEG, 86 for WebP) or the wp_editor_set_quality value, discarding an earlier
set_quality(). Saving the same crop with set_quality( 80 ) wrote quality 86 (3,262 bytes); with the filter, quality
80 (2,734 bytes).
Output formats: image_editor_output_format
The image_editor_output_format filter (5.8) maps an input MIME type to the type the editor writes; its default since
6.7 maps HEIC and HEIF to JPEG. The mapping applies to every save of that input type: every sub-size and a converted
"full" copy, with the uploaded file then named in original_image, while the attachment’s post_mime_type keeps the
uploaded type. The site-owner side, with an AVIF example, is in
Media Library. Lumen Press writes WebP and lowers the
quality of small sizes with the $size argument that wp_editor_set_quality gained in 6.8:
<?php
/* Plugin Name: Lumen Image Formats (WebP sub-sizes; requires WordPress 6.8) */
defined( 'ABSPATH' ) || exit;
// Write JPEG uploads as WebP. Ignored by an editor that cannot write WebP.
function lumen_image_formats_map( $formats ) {
$formats['image/jpeg'] = 'image/webp';
return $formats;
}
add_filter( 'image_editor_output_format', 'lumen_image_formats_map' );
// The WebP default is 86: use 82, and 70 for sizes up to 300 px wide.
function lumen_image_formats_quality( $quality, $mime_type, $size = array() ) {
if ( 'image/webp' !== $mime_type ) {
return $quality;
}
return ( isset( $size['width'] ) && $size['width'] <= 300 ) ? 70 : 82;
}
add_filter( 'wp_editor_set_quality', 'lumen_image_formats_quality', 10, 3 );
wp media regenerate 9 --yes on "The Glass Orchard" cover then produced a metadata file of
2026/10/978-1-23456-790-3.webp, every size as image/webp (862 bytes for 300 × 450, 2,930 for 600 × 900) and
original_image set to the JPEG. An editor that cannot write the mapped type keeps the input format, so the filter
needs no support check. The mapping also applies to plugin code that calls save() with an explicit image/jpeg:
under this filter that file comes out as WebP, with a .webp extension.
Errors and pitfalls
| Symptom | Cause and fix |
|---|---|
|
The |
"A valid URL was not provided." / "Invalid image URL." |
A private or loopback host refused by the safe HTTP functions / a URL path without an allowed image extension. |
A new size is missing on old images |
Sizes are made at upload. Run |
No |
No other file with the same ratio, metadata emptied by a filter, or a |
The hero cover is lazy-loaded |
It is printed outside the main loop. Pass |
|
The save converts formats (use |
Security
-
The upload functions check real content against the extension and the user’s allowed types;
file_put_contents()into uploads bypasses that (see Security for Developers). -
Downloading a URL that a user supplies is a server-side request forgery risk: stay on the safe HTTP functions, allow private hosts one at a time and only outside production, and check
upload_filesand a nonce first. -
wp_get_attachment_image()escapes every attribute; hand-built markup usesesc_url()andesc_attr(). -
Sideloaded originals keep EXIF data, including GPS; generated sizes drop most of it (
image_strip_meta). -
An endpoint that resizes on request (the old "timthumb" pattern) invites denial of service; generate files once.
Where older books and tutorials are outdated
| Older advice | Current practice |
|---|---|
Resize with |
Both deprecated since 3.5; use |
Print |
|
Lazy-load with a plugin, a script or a filter for iframes (Ultimate WordPress Handbook) |
Core lazy-loads images since 5.5 and iframes since 5.7, adds |
|
Pass |