As of WordPress 6.4, front-end attachment (media) page views are disabled for new WordPress installs. This change was a long time comingβattachment pages have been falling out of favor for years. But they can still serve a purpose, especially on photo gallery or photography websites.
Love βem or hate βem, attachments are a reality of theme development, especially if you are publicly distributing your theme. Just because they are disabled for new installs doesnβt mean that there arenβt millions of WordPress sites that still have them turned on or that new users wonβt enable them.
Iβm 100% certain that I am in the minority, but I happen to still like front-end attachment views. Maybe itβs nostalgia for an earlier era of WordPress. Perhaps itβs just me reminiscing about days gone by when I would build full image gallery sites for clients.
Regardless of whether you use them yourself, itβs important to ensure that attachment views work properly and fit in with your theme design.
This is a problem when building block themes, especially if you plan to include a custom attachment template. WordPress doesnβt currently have media blocks that let you dynamically attach data to them (like the current attachmentβs ID).
This issue may be resolved by the Block Bindings/Connections API and the ability to connect custom fields to blocks. Only time will tell, and future features don’t solve our problems today. But it is good to keep them in mind come refactor time.
In this tutorial, you will learn what issues youβll face when designing attachment templates and my recommended methods of solving them. Iβll also present a couple of alternative paths that you can explore.
You can follow along with the code examples in this tutorial via the Dynamic Attachments repository.
Table of Contents
The problem with block-based attachment templates
First, letβs look at what the default attachment page looks like when using the Twenty Twenty-Four theme. In the gallery below, you can see both an image and a video attachment page:


There are immediately some issues that you might want to address as a theme designer:
- The image attachment displays a small thumbnail instead of a larger size that at least fills out the width of the content.
- The video attachment uses the old MediaElement.js player instead of the browser default that the core Video block uses.
- The author and date are shown below the title, which is generally not desirable for attachments. These are also often not the author and date for the media file itself.
- A comment form is not needed for most use cases.
There are a couple of things happening here. Because the Twenty Twenty-Four theme doesnβt include a custom templates/attachment.html file, WordPress automatically falls back to the templates/single.html template, which was specifically designed for blog posts.
For more information on how templates are chosen for front-end views, check out the Template Hierarchy documentation in the Theme Handbook.
When a theme doesnβt include an attachment template, WordPress automatically filters the content, prepending it with the media output. And that doesnβt always look so great with every theme.
In classic themes, this was simple enough to wrangle by writing some custom HTML and PHP in your attachment.php template.
For block themes, the problem is that WordPress provides no block support for attachment templates. Getting that perfect attachment page design is a little trickier with block templates but not insurmountable.
So letβs move forward and solve this in a way that works well for your theme.
Customizing the attachment view on the front end
To create nicer attachment templates, you need a way to use core WordPress blocks with dynamic media data. For example, for image attachments, youβll probably want to grab the image file URL, alt text, and caption and tie them to the core Image block.
The best approach Iβve found is to filter the content as itβs rendered on the front end. That way, you can give the user control over editing the attachment template in the UI. And you donβt have to worry about them breaking your nice media functionality.
The remainder of this tutorial will assume that you are working with Twenty Twenty-Four or a child theme based on it. You will, of course, want to apply these techniques to your own theme, but we need a common foundation to work from for the moment.
There are two roads you can walk down here:
- Create a custom
templates/attachment.htmltemplate to control how it looks entirely. - Avoid creating a
templates/attachment.htmltemplate and only customize the media output.
Either is a valid option, but youβll learn what you need to do for each scenario in the following sections.
Enable attachment pages for testing
Before moving forward, letβs make sure you can actually test attachments in your development environment. Remember that new WordPress installs disable attachment pages, so if youβre spinning up a new site for development, youβll need to turn them on.
Add this code to a custom plugin for your test site:
add_filter( 'pre_option_wp_attachment_pages_enabled', '__return_true' );
In a pinch, you can drop that in your themeβs functions.php file, but donβt forget to remove it before distributing your theme to others. You wouldnβt want to override the userβs preferred setting.
Creating an attachment template
The simplest way of creating a custom attachment template is to copy the content of the Twenty Twenty-Four themeβs templates/single.html file and paste it into a new templates/attachment.html file.
The only requirement is to include the call to the <!-- wp:post-content /--> block in the template. Beyond that, anything you want to do with the design is fair game.
I removed most of the extra block markup in my attachment template, leaving just the Header and Footer template parts, a couple of wrapping Group blocks, and calls to the Post Title and Post Content blocks.
Feel free to borrow my attachment.html file and use it as-is:
<!-- wp:template-part {"slug":"header","area":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","align":"full"} -->
<main class="wp-block-group alignfull">
<!-- wp:group {"style":{"spacing":{"padding":{"top":"var:preset|spacing|50"},"margin":{"bottom":"var:preset|spacing|40"}}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group" style="margin-bottom:var(--wp--preset--spacing--40);padding-top:var(--wp--preset--spacing--50)">
<!-- wp:post-title {"level":1,"fontSize":"x-large"} /-->
</div>
<!-- /wp:group -->
<!-- wp:post-content {"lock":{"move":false,"remove":true},"align":"full","layout":{"type":"constrained"}} /-->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","area":"footer","tagName":"footer"} /-->
Now check to ensure that the template is correctly appearing in the Site Editor. You can do this by visiting Appearance > Editor > Templates > Attachment Pages in your WordPress admin.
It should look like this:

You wonβt see any media output or media blocks at all here. These must be dynamically inserted on the front end.
Disable WordPressβs default filter
If you are not building a custom attachment template, WordPress will automatically insert some default media output. It does this by running the prepend_attachment filter over the content.
Add this code to your functions.php for this scenario:
remove_filter( 'the_content', 'prepend_attachment' );
If you are building a custom attachment template, you donβt need to do this step. But it wonβt negatively affect your template if you remove the filter anyway.
Output dynamic media block markup
Now we need a method of inserting custom media output on the front end. There are several options that I considered here, but I decided that the best course was to filter the output of the render_block_core/post-content hook.
From this point forward, you will be building a custom filter function inside of your themeβs functions.php file. Weβll walk through each piece of the function so that you can learn what it is doingβmaybe itβll even give you a few ideas for other custom filters in the future.
Start by adding the filter call and function in your functions.php file:
add_filter( 'render_block_core/post-content', 'themeslug_render_block', 10, 3 );
function themeslug_render_block( $block_content, $block, $instance ) {
// Custom code will go here.
}
The remaining code snippets in this section should be placed inside the function.
The render_block_core/post-content hook runs whenever the Post Content block is rendered on the front end. You certainly donβt want your filter to run over every instance of this block, so you need to check if the following conditions are met before running your code:
- There must be a post ID passed down via block context.
- We must be viewing a front-end attachment page for the given post ID (checked via the
is_attachment()conditional function).
If any of those conditions are false, you must return the original, unaltered block content back.
Now add this code inside your function:
if (
empty( $instance->context['postId'] )
|| ! is_attachment( $instance->context['postId'] )
) {
return $block_content;
}
Now you need to create a hierarchy of partials (i.e., small PHP template files) for outputting the media part of the attachment page.
For this, youβll need four partial files for handling different types of media. Add these empty files inside a custom /partials folder in your theme:
attachment-media-audio.phpattachment-media-image.phpattachment-media-video.phpattachment-media.php(fallback file)
You donβt have to worry about what goes in those files yet. Weβll get to that step, but it helps to know what files weβre looking for when building our hierarchy.
The most important piece of the next code is the wp_attachment_is() function. You use it to determine what type of media is associated with the attachment page.
To build your partial hierarchy, add this code inside of the themeslug_render_block() function in your functions.php file:
$partials = [];
$html = '';
foreach ( [ 'image', 'video', 'audio' ] as $type ) {
if ( wp_attachment_is( $type, $instance->context['postId'] ) ) {
$partials[] = "partials/attachment-media-{$type}.php";
break;
}
}
$partials[] = 'partials/attachment-media.php';
Weβre starting to get to the point where the real magic happens. But first, there are a couple of important steps. Your function needs to:
- Locate and include the partial file using the
locate_template()function. Youβll also pass the post ID to the$argsparameter so that it is available in the partial template. - Use PHP output buffering to capture the output of the partial.
- Return the block content if nothing was captured.
Add this code inside of your function:
ob_start();
locate_template( $partials, true, false, [
'post_id' => $instance->context['postId']
] );
$block_markup = ob_get_clean();
if ( ! $block_markup ) {
return $block_content;
}
You could include the partial file via a direct file path, but using locate_template() lets child themes overwrite it.
There is a lot that has gone into this function so far, but weβre at the last stage. The final tasks are to:
- Parse the block markup returned from the partial template using the
parse_blocks()function. - Render each block with the
render_block()function and append it to the$htmlvariable. - Return the block content with the media output prepended to it.
Add this final code inside your themeslug_render_block() function:
foreach ( parse_blocks( $block_markup ) as $parsed_block ) {
$html .= render_block( $parsed_block );
}
return $html . $block_content;
Test your code by visiting any attachment page on your site and making sure nothing is broken.
You wonβt see any media output yet because you donβt have any block markup in your partial templates. So letβs do that now.
Building dynamic attachment partials
In the previous section, you added four empty files to your themeβs /partials folder:
attachment-media-audio.phpattachment-media-image.phpattachment-media-video.phpattachment-media.php(fallback file)
You will use these partials to write valid block markup. The function you wrote earlier will then parse that markup and inject it into the Post Content block when a user is visiting an attachment page.
Youβre doing this from custom PHP files instead of HTML-based template parts or patterns because you need the ability to dynamically grab data as needed (like the media URL) via PHP.
Which blocks you use to build your partial templates is entirely up to you, but this table should give you some blocks to work with:
| Files | Blocks |
|---|---|
attachment-media-audio.php | Audio |
attachment-media-image.php | Image, Cover, Media & Text |
attachment-media-video.php | Video |
attachment-media.php | File |
The easiest way to get the block markup you need is to insert an empty block of your choosing in the editor and click the Copy button from the toolbar.
In this screenshot, you can see the process of copying the Image block:

That will give you this code for the Image block:
<!-- wp:image -->
<figure class="wp-block-image"><img alt=""/></figure>
<!-- /wp:image -->
Youβll need to repeat this process for each block that you want to use in your partial templates.
In the next steps, youβll notice that I also used a wrapping Group block. This is for better control over the layout of the page. Feel free to try something different and go your own way. You are building this to fit into your theme, after all.
Building a fallback partial
For my fallback partial, I decided to use coreβs built-in File block. Copy and paste this code into your partials/attachment-media.php file:
<?php
// Get dynamic attachment data.
$url = wp_get_attachment_url( $args['post_id'] );
?>
<!-- wp:group {"align":"full","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull">
<!-- wp:file {"id":<?php echo absint( $args['post_id'] ); ?>,"href":"<?php echo esc_url( $url ); ?>"} -->
<div class="wp-block-file">
<a href="<?php echo esc_url( $url ); ?>"><?php the_title(); ?></a>
<a href="<?php echo esc_url( $url ) ?>" class="wp-block-file__button wp-element-button" download><?php esc_html_e( 'Download', 'x3p0-ideas' ); ?></a>
</div>
<!-- /wp:file -->
</div>
<!-- /wp:group -->
Now view any attachment page on the front end of your site. You should see something like this:

Thatβs not the prettiest design in the world, mostly because the Twenty Twenty-Four theme doesnβt do anything special with the File block. Remember that this is just the fallback. Take some time to give it a nice visual makeover.
Building an image partial
Letβs step it up a notch and make the Image attachment page a little nicer. Add this code to your themeβs partials/attachment-media-image.php file:
<?php
// Get dynamic attachment data.
$caption = wp_get_attachment_caption( $args['post_id'] );
$image = wp_get_attachment_image_src( $args['post_id'], 'large' );
$alt = trim( strip_tags( get_post_meta( $args['post_id'], '_wp_attachment_image_alt', true ) ) );
?>
<!-- wp:group {"align":"full","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull">
<!-- wp:image {"align":"wide","id":<?php echo absint( $args['post_id'] ); ?>,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image alignwide size-large">
<img src="<?php echo esc_url( $image[0] ); ?>" alt="<?php echo esc_attr( $alt ); ?>" />
<?php if ( $caption ) : ?>
<figcaption class="wp-element-caption"><?php echo esc_html( $caption ); ?></figcaption>
<?php endif ?>
</figure>
<!-- /wp:image -->
</div>
<!-- /wp:group -->
Now view any image attachment page on your site. It should look similar to this:

Thatβs much better than the original screenshot shown earlier in this post, right?
Building audio and video partials
Iβve given you the tools that you need to take these steps entirely on your own. Other than selecting an appropriate block for the media type, the process is the same.
If you still need some help or just want some code to copy/paste, check out the /partials folder in the Dynamic Attachments repository.
Taking it to the next level
There is a lot more you can do with this technique, and I could certainly drone on for a few thousand more words walking through other examples. But it is time to let you take over the creative reins.
If you need a little extra inspiration, here is a screenshot from my theme where Iβm adding image metadata:

This is still a bit experimental, but I hope it encourages you to venture out and explore on your own.
Alternative solutions
Before writing this tutorialβand even during the process of writing itβI explored several avenues, hoping to find the best path to solving this issue with block attachment templates. As always, I cannot guarantee that my method is the best, and I also encourage you to try other options.
WordPress has a prepend_attachment filter hook, so you could simply filter the default handling of attachment output. Remember that this hook wonβt fire by default if your theme, a child theme, or a user adds an attachment template.
Of course, you could filter the_content with your own function, outputting the media however youβd like. Just be sure to remove WordPressβ prepend_attachment() filter as shown earlier in this post.
You could add an attachment.php template to the root of your theme folder and handle everything directly in the file (yes, block themes can use classic PHP templates). This would also be overwritten if a child theme or a user adds a block-based attachment template.
Any of those options could work. And you can even reuse bits and pieces of code from this tutorial to make it happen.
The easiest alternative is to not sweat the details and just let WordPress output media that doesnβt match your themeβs design. But youβve already invested too much into this if youβve read this far into the post. You might as well build a cool attachment page now.
Props to @bph and @laurlittle for feedback and review on this post and @saurabhmention for the featured image, taken from the WordPress Photo Directory.
Leave a Reply