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.html
template to control how it looks entirely. - Avoid creating a
templates/attachment.html
template 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.php
attachment-media-image.php
attachment-media-video.php
attachment-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$args
parameter 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$html
variable. - 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.php
attachment-media-image.php
attachment-media-video.php
attachment-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