Scripts and styles are printed by wp_head itself, at priority 9. A callback hooked to wp_head at the default priority of 10 therefore runs after the queue has been flushed: the handle registers, nothing is printed, and no error appears anywhere to say so.
// registered too late — the queue has already been printed
add_action( 'wp_head', 'td_catalogue_assets' );
// the hook that exists for this, front end only
add_action( 'wp_enqueue_scripts', 'td_catalogue_assets' );
function td_catalogue_assets() {
if ( ! is_post_type_archive( 'product' ) && ! is_tax( 'product_cat' ) ) {
return;
}
$dir = get_stylesheet_directory_uri();
wp_enqueue_style( 'catalogue', $dir . '/css/catalogue.css', array(), '1.2.0' );
wp_enqueue_script( 'catalogue-filters', $dir . '/js/filters.js', array( 'jquery' ), '1.2.0', true );
}
What makes this hard to spot is that it half works. Anything enqueued with the footer flag set is still picked up by the footer pass, so from one callback the script appears and the stylesheet does not — which sends people looking at the CSS. wp_enqueue_scripts is the front-end hook and it handles styles too, despite the name; the admin and the login screen have their own, and reaching for the wrong one is the other common version of this. The property that makes it the right place is that conditional tags are already resolved when it fires, so a template’s assets can be loaded on that template rather than on all of them.