A blog with 40 posts and 600 uploads has published 640 pages, not 40. Every image, PDF and video in the media library has its own URL, its own title, and on most themes its own comment form.
There is a thirty second tell. Open the media library, click any file, and read the link in the bottom right of the details panel. Core prints “View attachment page” when these pages are live and “View media file” when they are not, and it decides which label to show by reading a single option. That check sits in wp-includes/media-template.php, so it is telling you the truth about your own site rather than a guess.
The mechanism underneath is one sentence long: an attachment is a post, and posts get permalinks. The consequences take longer, and most of the advice written about them is a version or two out of date. WordPress changed the default in 6.4, so what your site does today depends partly on whether it was installed before or after that release.
Assume you have already found the checkbox in your SEO plugin. This is about what sits underneath it, and about the cases where the checkbox is not the thing doing the work.
An attachment is a post
When you upload a file, WordPress writes a row to the posts table with post_type set to attachment. The file goes into the uploads folder, the row goes into the database, and the two are joined by the _wp_attached_file meta value. Core registers that post type in wp-includes/post.php with 'public' => true and 'supports' => array( 'title', 'author', 'comments' ). Public means viewable on the front end. Comments means an attachment can collect them.
Worth noting in the same registration call: 'rewrite' => false and 'query_var' => false. Filtering the post type arguments is not the lever people reach for it to be. The front end URL is assembled elsewhere, by a function of its own.
Two columns on that row decide almost everything that follows. post_parent holds the ID of the post the file was uploaded into, or 0 if you dropped it straight into the media library. post_status is normally inherit, which is core’s way of saying “whatever my parent says”: wp_insert_post() forces any attachment status that is not inherit, private, trash or auto-draft back to inherit. An attachment on a draft post is therefore not public. An attachment with no parent has nothing to inherit from, and it renders anyway, because WP_Query only appends a post_status condition to the query when the request is not singular. A single attachment request is singular, so the status filter never runs.
If that row-plus-file model is new to you, the guide to cleaning up the media library covers it properly, including what an orphaned attachment is and the order in which things must be deleted. The short version for this article: unattached uploads are the ones that behave worst, and most libraries are full of them.
How the URL is built
The permalink comes from get_attachment_link() in wp-includes/link-template.php, and it has more branches than people expect. With pretty permalinks and a valid parent, the attachment slug is appended to the parent’s permalink:
https://example.com/how-to-proof-dough/img-4021/for a file uploaded into that post.https://example.com/how-to-proof-dough/attachment/img-4021/when the slug is numeric or the permalink structure contains%category%. Core inserts an explicitattachmentmarker there, because a parent permalink followed by a number would otherwise be read as pagination.https://example.com/img-4021/when there is no parent at all. Core falls back tohome_url( user_trailingslashit( $post->post_name ) ), which puts the image at the root of the site, in the same namespace as your pages.https://example.com/?attachment_id=123when permalinks are plain, when the attachment is somehow its own parent, when the parent row is missing, or when the parent post type is not viewable.
That third case is the one that surprises people. Files dragged into Media, Add New have post_parent = 0, so they get top level URLs. This is also why attachment slugs are policed so aggressively. In wp_unique_post_slug() the attachment branch carries the comment “Attachment slugs must be unique across all types”, and the query it runs checks post_name against every row in the posts table with no post type constraint at all. That is where logo-2 and logo-3 come from. An upload cannot take a slug that any page, post or product is already using.
The whole result runs through the attachment_link filter before it is returned. Since WordPress 5.6, documented in the filter’s own docblock, returning an empty string from that filter also removes the view attachment page link from the media modal, which is a reasonable thing to do once you have decided these pages should not exist.
What actually renders on one
Template selection goes through get_attachment_template() in wp-includes/template.php, which splits the MIME type on the slash and looks for, in order: image-jpeg.php, jpeg.php, image.php, then attachment.php. Almost no theme ships any of them. When none is found, wp-includes/template-loader.php keeps walking its list, and since is_single is also true for an attachment you end up on single.php or, ultimately, index.php.
The content of the page is generated, not written. Core hooks prepend_attachment() onto the_content in wp-includes/default-filters.php. For an image it emits exactly one thing, a <p class="attachment"> containing the medium size of the file linked to the raw file, built by wp_get_attachment_link( 0, 'medium', false ). For audio and video it emits the matching shortcode player instead. The template loader removes that filter only if a real attachment template was found, on the assumption that a theme with its own template will do the job itself.
So a default attachment page is your site header, a title that is usually the original camera filename, one medium sized image linked to the full file, a comment form, and your footer. There is no body text, no internal linking, no context. The alt attribute on that image comes from the _wp_attachment_image_alt meta value, which is the same field the editor writes, so if you have been filling in alt text properly it will at least be present. That is the only piece of authored content on the page, and it is not visible.
The cost, honestly measured
Attachment pages are not a penalty and they are not going to sink a healthy site. Search engines are good at ignoring near empty URLs, and plenty of large sites have had them switched on for a decade without visible harm. The honest case against them is quieter than the tutorials suggest, and it is made of four smaller things.
The first is proportion. A 40 post blog with 600 uploads has 15 low value URLs for every URL worth ranking. Crawling is a budget, and on a shared host with slow response times that ratio is the difference between a new article being fetched today and being fetched next week. The second is the landing experience. Attachment pages do get indexed, and someone arriving from image search on a bare picture page with no navigation and no explanation leaves immediately. The article that would have answered their question is one click away and invisible.
The third is dilution of intent. If a post is called “How to proof dough” and it contains an image whose title is “proofing dough”, you now have two URLs competing for a similar phrase, and one of them is a picture. Which one gets shown is not always the one you want. The fourth is maintenance noise. Attachment pages support comments, so they collect spam, and every one of them is a URL you have to reason about when you audit the site.
None of that is dramatic. All of it is avoidable in about a minute.
What core does now
WordPress 6.4 added an option called wp_attachment_pages_enabled, and it is the single most useful fact in this article, because almost nothing written before late 2023 mentions it.
New installs get 0. That value is written by populate_options() in wp-admin/includes/schema.php, so a site installed after 6.4 has attachment pages off from the first minute. Existing sites get 1, written by upgrade_640() in wp-admin/includes/upgrade.php, guarded by a check that the site’s database version was below 56657. Core does not silently change the URL behaviour of a site that already has those pages indexed. Two sites on the same WordPress version can therefore behave in opposite ways, which is exactly why generic advice about this topic keeps failing.
The enforcement lives in redirect_canonical(), in wp-includes/canonical.php. The check is blunt: if the request is an attachment and the option is falsy, the redirect target is replaced with wp_get_attachment_url(), and the function sets an internal flag so its trailing slash logic is skipped, since a file URL has no trailing slash. If the attachment has a parent, core also swaps the object it uses for the status check to that parent, because an attachment inherits its parent’s status. The redirect is then issued as a 301 at the end of the same function, behind a guard that re-runs the canonical check on the target first to avoid chained redirects.
Note the destination. Core sends the request to the file, not to the parent post. Ask for /how-to-proof-dough/img-4021/ and you get a 301 to /wp-content/uploads/2026/03/img-4021.jpg. That is correct behaviour for removing an HTML page from the index, and it is not what most people assume the setting does.
There is no admin screen for this. The option does not appear under Settings, Media or anywhere else in wp-admin. You read and write it directly:
# Is this site publishing attachment pages?
wp option get wp_attachment_pages_enabled
# 0 or an empty result: core already 301s attachment URLs to the file.
# 1: the pages render. Switch them off with:
wp option update wp_attachment_pages_enabled 0
# Fully reversible. Set it back to 1 to bring the pages back.
The option is read at request time inside redirect_canonical(), so there are no rewrite rules to flush and nothing to regenerate. The change takes effect on the next uncached request. On multisite the value lives in each site’s own options table, not in the network options.
What the SEO plugins are doing
Both major plugins solved this years before core did, which means most sites now have two mechanisms available and only one of them running.
Rank Math puts it in General Settings, under Links, as Redirect Attachments. The toggle is stored in the rank-math-options-general option under the key attachment_redirect_urls, and a companion key, attachment_redirect_default, holds a URL for attachments that have no parent post to redirect to. The fallback defaults to your home URL. The distinction that matters is that Rank Math’s redirect is aimed at the parent post where one exists, which is the behaviour most people assume they are buying, with the fallback catching the unattached files.
Yoast has carried an equivalent for years as a media pages setting in its advanced settings, and with media pages turned off an attachment URL resolves to the media file, matching what core now does. Yoast is not installed on the machine this article was checked against, so treat the exact menu path as a pointer and read the label in your own version rather than trusting a screenshot from a blog post.
Two honest points about all this. First, neither plugin is adding a capability that core lacks any more. On a site installed after 6.4 the plugin setting may be entirely redundant, and on a site upgraded through 6.4 the plugin may be the only thing doing the work, because core deliberately left the option at 1. Second, both hook template_redirect, which is the same hook core uses. Core registers redirect_canonical in default-filters.php, loaded at line 154 of wp-settings.php, while plugins are not loaded until line 574, so at the default priority of 10 core’s handler always runs first and calls exit. If the option is 0, core’s redirect to the file wins and a plugin hooked at priority 10 never gets a turn.
That interaction is the reason people report that turning a setting on “did nothing”. Both mechanisms were on, and they disagreed about the destination rather than about whether to redirect. If you specifically want visitors sent to the parent article rather than to a .jpg, leave wp_attachment_pages_enabled at 1 and let the plugin do the work.
The code route
If you have no SEO plugin, or you want parent first behaviour without one, this is the whole job. It belongs in a small site specific plugin or a child theme, not in a parent theme that will be updated. The guide to where custom code goes covers the difference.
/**
* Send attachment URLs to the parent post, or to the file if there is no parent.
*/
add_action( 'template_redirect', 'wpie_redirect_attachment_pages' );
function wpie_redirect_attachment_pages() {
if ( ! is_attachment() ) {
return;
}
$attachment = get_queried_object();
if ( ! $attachment instanceof WP_Post ) {
return;
}
$target = '';
if ( $attachment->post_parent ) {
$parent = get_post( $attachment->post_parent );
if ( $parent && 'publish' === $parent->post_status ) {
$target = get_permalink( $parent );
}
}
if ( ! $target ) {
$target = wp_get_attachment_url( $attachment->ID );
}
if ( $target ) {
wp_safe_redirect( $target, 301 );
exit;
}
}
Four things about that snippet. At the default priority it only gets a turn while wp_attachment_pages_enabled is 1, for the ordering reason above, so set the option to 1 if you are using it. If you would rather it win either way, register it ahead of core by passing a priority below 10, for example add_action( 'template_redirect', 'wpie_redirect_attachment_pages', 9 ).
The parent status check matters. Without it, an attachment sitting on a draft or trashed post would be redirected to a URL that returns 404. And wp_safe_redirect() refuses external hosts, so if your uploads are served from a CDN or object storage on a different domain, the file fallback will bounce to your home page instead. Add that host through the allowed_redirect_hosts filter, or drop the fallback and let core handle the parentless files.
Which of those routes is right for you depends on your WordPress version and on what your SEO plugin is already doing, and for a lot of sites the correct answer is now to do nothing at all.
Answer the three questions below and the tool says so plainly when that is the case, rather than handing you code you do not need. When code is the answer it writes the template_redirect version for your chosen target with the right status code, the noindex rule, and a redirect list for the URLs that are already indexed.
Attachment page fixer
Every upload in WordPress gets a page of its own with a heading and almost nothing else. Pick your situation and this works out whether anything needs doing at all, and if it does, prints the code that fits. Everything is worked out in this browser tab, nothing is sent anywhere.
Optional, and worth having. The first filter makes every attachment link point at the file, so the editor stops offering the page. The second keeps an upload from taking the slug a post or page wanted.
Use this instead of the redirect, never alongside it: a page that redirects or answers with an error never gets far enough to print a robots tag.
Built from the URLs you pasted, one permanent redirect to the parent path each. Useful when the attachment posts have been deleted, because then the PHP above has nothing left to look up.
Goes inside the server block, above the location that hands requests to WordPress.
Why 301 and not 404
The instinct to return a 404 is understandable. The page should not exist, so say so. It is the wrong tool here. These URLs were published, some of them were indexed, and a few of them have inbound links from forums, scrapers or social sites. A 301 hands whatever signal those links carry to the parent post and gives the visitor something to read. A 404 discards both, and search engines revisit a 404 for a while before dropping it, so it is also slower.
The other common suggestion, adding a noindex tag to attachment pages and leaving them live, works but is strictly worse. The page still has to be fetched for the tag to be seen, so you keep the crawl cost and the comment spam surface, and you keep the possibility of a visitor landing on a bare image. Redirect them instead. If you later decide you want them back, the option flips in both directions.
Sitemaps and where images belong
Core has never listed attachment pages in its own sitemap. WP_Sitemaps_Posts::get_object_subtypes() collects every public post type and then explicitly unsets attachment before filtering the rest by viewability. If attachment URLs are appearing in your sitemap, an SEO plugin or a dedicated sitemap plugin put them there, and turning that off is usually a single checkbox on the same settings screen as the redirect.
The distinction worth holding on to is between an image URL and an image page. Images belong in a sitemap as image entries nested inside the entry for the page that displays them, which tells a crawler “this article contains these pictures”. They do not belong as standalone page entries, which tells a crawler “here are 600 more documents to fetch”. One helps image search understand context, the other spends your crawl allowance on nothing.
When a plugin does emit image entries, it should reference the full size file rather than one of the generated crops. A single upload produces a whole family of files, and only one of them is the original. The explanation of WordPress image sizes covers which file is which and why -scaled versions exist. Listing a thumbnail in a sitemap is a small waste, but it is the kind of small waste that accumulates across a large library.
When an attachment page earns its place
There is one honest exception, and it is photography. If the photograph is the content rather than an illustration of the content, a page per image is a real content type, and switching the redirect on throws away the thing you are publishing. Portfolio sites, print shops and archives all have a legitimate claim here.
The test is whether the page carries anything a person would read. That means a written title instead of DSC_0481, a caption or description of at least a paragraph, and an attachment.php or image.php template in the theme so the page is more than prepend_attachment() output. Useful extras are the shooting details, which you already have, since cameras write aperture, shutter speed, focal length and often GPS coordinates into the file and WordPress reads a subset of them at upload time. The article on EXIF and GPS data in WordPress uploads covers what survives and what you may not want published. Add internal links back to the gallery, the series or the related work, and the page stops being thin.
If you would not write a paragraph or two about a given photograph, it does not need a page. And if you are building this deliberately, consider a custom post type or ordinary posts with a featured image instead. You get the block editor, taxonomies, excerpts and a normal template, and you avoid fighting machinery that was designed to describe a file rather than to present one.
Symptom and cause
Attachment URLs are still in the index weeks after you turned the redirect on. Redirects do not remove URLs from an index, recrawling does. Fetch one of the URLs and confirm it returns 301. If it does, the work is finished and the index will catch up over the following weeks.
The redirect lands on a bare .jpg instead of the article. That is core’s behaviour with wp_attachment_pages_enabled set to 0, where the target is wp_get_attachment_url(). If you want the parent post, set the option to 1 and use a plugin setting or the snippet above, which only run once core has stood down.
Some attachment URLs redirect and others do not. The ones that misbehave almost always have post_parent = 0. A parent first rule has nothing to aim at, so those requests fall through to whatever the fallback is, or to the page itself if there is no fallback.
Changing the option appeared to do nothing. Check page caching and any CDN first, since a cached HTML response will keep serving the old page. After that, look for a second handler on template_redirect in an SEO plugin or the theme, registered at a priority below 10.
New uploads keep creating root level URLs like /img-4021/. Files added through Media, Add New have no parent, so get_attachment_link() falls back to a site root slug. Uploading from inside the post editor sets post_parent instead, which is one more reason to keep unattached files under control.
Where this leaves you
Attachment pages are not a bug and they were not a bad idea. The attachment_link filter is marked @since 2.0.0, which dates this feature to a time when a WordPress site might genuinely want a page per uploaded file, and core has kept it working ever since rather than break the sites that rely on it. What changed is the ratio. Modern posts carry a dozen images each, libraries run into the thousands, and a feature that once produced a handful of extra URLs now produces most of the URLs on a site.
The fix is genuinely a minute of work, and the useful part is knowing which minute. Read wp_attachment_pages_enabled before you touch anything else, because it tells you whether core is already handling this and whether your plugin setting is doing anything at all. Then decide the destination on purpose: the file if you only care about clearing the index, the parent post if you care about the person who clicked the link. Those are different goals and they need different settings.
After that, treat it as a media library question rather than an SEO one. The sites with the worst attachment page problem are the sites with thousands of unattached uploads, duplicated files and no idea which images are actually used anywhere. Fixing that fixes the URL count as a side effect, and it fixes several other things at the same time.