Troubleshooting

WordPress Attachment Pages: The Thin Pages You Did Not Know You Published

Every file you upload to WordPress gets a permalink, a template and a comment form. Here is the mechanism underneath attachment pages, what core changed in 6.4, and the correct way to redirect them.

WordPress Attachment Pages: The Thin Pages You Did Not Know You Published

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 explicit attachment marker 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 to home_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=123 when 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.

Table of the four URL shapes get_attachment_link produces: a slug under the parent post, an attachment marker for numeric slugs or category structures, a site root URL for files with no parent, and a query string URL when permalinks are plain.

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.

Table showing that a WordPress site installed on 6.4 or later has wp_attachment_pages_enabled set to 0 and redirects attachment URLs to the file, while a site upgraded through 6.4 has the option set to 1 and still renders the page, with the canonical.php condition below it.

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.

Working it out

Working it out
Search engine side

    WunderPaint
    The Dynamic Design and Automation Studio
    WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

    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.

    Table comparing what a default attachment page carries against what a photography page needs: core supplies only a medium image linked to the file and a comment form, while a written title, caption, custom template, EXIF details and internal links all have to be added.

    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.

    WordPress Attachment Pages: The Thin Pages You Did Not Know You Published

    Table of Contents

    Learn it by building something

    Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

    One mail a week, and then it ends.
    Unsubscribe in one click.

    Troubleshooting

    Gutenberg Block Invalid Content: Validate and Repair Block Markup

    The editor says a block contains unexpected or invalid content and stops there: no line, no attribute, no cause. Here is what it compared, why block markup drifts after a migration, and how to find the exact character that broke it.

    Photo Editing

    Get a depth map from any photo and use it for real lens blur

    A cutout blur draws a line and blurs one side of it, which is a thing no lens has ever done. A depth model gives every pixel a distance instead, so sharpness can fall away gradually the way it does optically. The map is a downloadable asset in its own right, and none of it leaves your browser.

    WordPress Development

    Dynamic Templates: Build the Design Once, Let Your Posts Fill It In

    You build the layout once, tell a few layers where to get their content, and from then on every post, product or page renders its own version.

    Photo Editing

    Dithering Side by Side: Why Atkinson Looks Like a Mac and Bayer Like a Game

    Eight dithering methods on one picture, with the same palette and the same options. Every kernel and divisor written out, the Bayer matrix built by recursion, and the linear light step that almost every quick implementation skips.

    Design Fundamentals

    Ten Typography Mistakes That Give an Amateur Design Away

    Bad type is rarely the wrong font. It is almost always spacing, size and a handful of habits nobody ever told you to drop.

    CSS & Front-End

    CSS Gradient Generator: Gradients That Do Not Band

    A gradient that looked smooth in the design tool grows stripes on the page. The cause is arithmetic: 8 bit colour, two similar colours and 1600 pixels to cover. How to count the steps before you ship, the three real fixes, and a builder that measures the banding for you.

    Download the free WunderPaint Plugin for WordPress

    The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

    The Image Editor & Design Studio

    Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

    Free

    Chaos Art

    Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

    Pro

    Particle Strokes

    Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

    Pro

    City Diorama

    Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

    Free

    Papercut Art

    Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

    Pro

    3D Earth Studio

    A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

    Free

    Mystic Studio

    Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

    Free

    Marble Bath

    Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.

    Free

    Day Ring

    Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.

    Free

    Code Shot

    Turn code into a gorgeous, share-ready image - syntax highlighting, editor themes, window frames and diff highlighting - then drop it into your design as a re-editable layer.

    Pro

    3D Solar System Studio

    Build a date-accurate 3D solar system - real planet positions for any date, photoreal textures and one slider from artistic to true scale - then drop it into your design as an editable layer.

    Pro

    3D Molecule Studio

    Build a real 3D molecule - from a curated library, the periodic table or a SMILES string - then style it, measure it and drop it into your design as an editable layer.

    Pro

    3D Textile Studio

    Drop your design onto cloth that behaves like the material you pick: silk falls soft, felt holds its shape, flag fabric snaps in the wind. Hang it, blow it and drape it, then lay the finished drape back into your document as a picture.