WordPress Developer Blog

Building dynamic block-based attachment templates in themes

Building dynamic block-based attachment templates in themes

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.

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"} /-->
	<!-- /wp:group -->

	<!-- wp:post-content {"lock":{"move":false,"remove":true},"align":"full","layout":{"type":"constrained"}} /-->

<!-- /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";

$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:


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:

attachment-media-image.phpImage, Cover, Media & Text

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:

// 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>
	<!-- /wp:file -->

<!-- /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:

// 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 ?>
	<!-- /wp:image -->

<!-- /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.

Categories: ,

5 responses to “Building dynamic block-based attachment templates in themes”

  1. Brian Coords Avatar

    Great article! Any thoughts on using the `render_block_core/post-content` filter instead of just `render_block`?


    1. Justin Tadlock Avatar

      Good thing to bring up, Brian. I don’t even know why I didn’t think to use that hook (so many hooks to remember!). It’ll save a check. I’ll update the post to change that.

  2. Mateus Machado Luna Avatar
    Mateus Machado Luna

    Hey Justin, thanks for the great article as always. I think we are all in high hopes for field connections but these tricks can help a lot meanwhile.

    “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”

    This kinda reminded me some confusion I had recently. I had to do something similar, not for attachments but for some CPTs that are dynamically created by my plugin so I couldn’t just put a file there, I had to use `single_template_hierarchy` to include a .php file in the template hierarchy list. This solves well, but my greater issue was that I had to put the wrapper html notation (<html, <head, <body…) just as we use to do in classic themes…. and it just felt wrong!

    Is there something equivalent for block themes? A way to programmatically define which block-based html template should be used for a certain CPT?

    1. Justin Tadlock Avatar

      I’ve actually been working on that same problem for plugins. The best solution I could come up with was to create the wrapping markup myself like you did. I may end up writing a post about it here to share with others. It definitely didn’t feel like the right thing to do because you have to manage that aspect of the templating system (ensuring that you update it if the core markup changes).

      Do you mind opening a feature request for this in the Gutenberg repo? It’d be a good place to at least start moving the discussion forward.

      1. Mateus Machado Luna Avatar

        Sorry for the late answer. I opened the issue: https://github.com/WordPress/gutenberg/issues/58417.

        Not sure if I could explain it well, let’s see if people understand it 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *