Skip to content

Instantly share code, notes, and snippets.

@quietcactus
Last active January 16, 2026 15:06
Show Gist options
  • Select an option

  • Save quietcactus/559c07eb8683132523b77db415315a29 to your computer and use it in GitHub Desktop.

Select an option

Save quietcactus/559c07eb8683132523b77db415315a29 to your computer and use it in GitHub Desktop.
Enqueues scripts and styles based on if the template part is called on a template
<?php /**
* Discovers and registers template part hooks
* Uses transient caching for performance
*/
add_action('after_setup_theme', function () {
$tp_dir = get_stylesheet_directory() . '/template-parts';
if (! is_dir($tp_dir)) {
return;
}
$cache_key = 'tp_registered_parts_v3';
$cached_parts = get_transient($cache_key);
$is_debug_mode = defined('WP_DEBUG') && WP_DEBUG;
// Use cached data if available and not in debug mode
if ($cached_parts !== false && !$is_debug_mode) {
$template_parts = $cached_parts;
} else {
// Scan directory and build list of template parts
$template_parts = tp_discover_template_parts($tp_dir);
// Cache for 24 hours (DAY_IN_SECONDS = 86400)
if (!$is_debug_mode) {
set_transient($cache_key, $template_parts, DAY_IN_SECONDS);
}
}
// Register hooks for each discovered template part
if (is_array($template_parts)) {
foreach ($template_parts as $slug) {
add_action("get_template_part_{$slug}", 'tp_enqueue_template_part_assets', 10, 2);
}
}
});
/**
* Scans template-parts directory and returns array of slugs to register
*
* @param string $tp_dir Path to template-parts directory
* @return array Array of template part slugs
*/
function tp_discover_template_parts($tp_dir) {
$template_parts = [];
// Walk every file under /template-parts/, skipping "." and ".."
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($tp_dir, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($it as $file_info) {
/** @var SplFileInfo $file_info */
if ($file_info->getExtension() !== 'php') {
continue;
}
$full_path = $file_info->getPathname();
// Normalize path separators for cross-platform compatibility
$full_path = str_replace('\\', '/', $full_path);
$tp_dir_normalized = str_replace('\\', '/', $tp_dir);
// Strip off the ".../template-parts/" prefix:
$relative = str_replace($tp_dir_normalized . '/', '', $full_path);
$path_parts = pathinfo($relative);
$dirname = $path_parts['dirname']; // e.g. "section-intro" or "."
$base = $path_parts['filename']; // e.g. "section-intro"
// Register the full filename path (e.g., template-parts/section-intro/section-intro)
$full_slug = 'template-parts/' . ($dirname !== '.' ? $dirname . '/' : '') . $base;
$template_parts[] = $full_slug;
// Also register the split version for backward compatibility
// Split on first hyphen:
$segments = explode('-', $base, 2);
$slug_base = $segments[0]; // e.g. "section"
// Rebuild the hook's "slug" including sub-folder(s):
$slug_short = ($dirname !== '.' ? $dirname . '/' : '') . $slug_base;
$slug = 'template-parts/' . $slug_short;
$template_parts[] = $slug;
}
return array_unique($template_parts);
}
/**
* Clears template parts cache when theme is switched or updated
*/
function tp_clear_cache() {
delete_transient('tp_registered_parts_v3');
}
add_action('switch_theme', 'tp_clear_cache');
add_action('after_switch_theme', 'tp_clear_cache');
/**
* Enqueues CSS/JS assets for template parts
* Supports modern script attributes and intelligent versioning
*
* Supports both calling syntaxes:
* - get_template_part('template-parts/section-intro/section-intro')
* - get_template_part('template-parts/section-intro/section', 'intro')
*
* Checks for assets in locations (in priority order):
* 1. Co-located: template-parts/section-intro/section-intro.css
* 2. Legacy: css/template-parts/section-intro.css
*
* @param string $slug Template part slug (e.g., 'template-parts/section-intro/section')
* @param string $name Template part name (optional, e.g., 'intro')
*/
function tp_enqueue_template_part_assets($slug, $name) {
// Self-contained versioning - no external dependencies
$cacheVersion = '1.0.0';
$slug_short = preg_replace('#^template-parts/#', '', $slug);
$filename = $slug_short . ($name ? "-{$name}" : '');
$dir = get_stylesheet_directory();
$uri = get_stylesheet_directory_uri();
// Determine versioning strategy
// Use filemtime for development, static version for production
$use_filemtime = defined('WP_DEBUG') && WP_DEBUG;
// Build asset paths to check
// When $name is provided (e.g., 'section', 'intro'), we need to check both:
// 1. The combined filename: template-parts/section-intro/section-intro.css
// 2. The slug as-is: template-parts/section-intro/section.css
$asset_paths = [];
if ($name) {
// Split syntax: get_template_part('template-parts/section-intro/section', 'intro')
// Extract directory and reconstruct full filename
$path_info = pathinfo($slug);
$dir_path = $path_info['dirname'];
$base_name = $path_info['filename'];
// Try the combined version first (e.g., section-intro/section-intro.css)
$combined_filename = $base_name . '-' . $name;
$asset_paths[] = '/' . $dir_path . '/' . $combined_filename;
// Then try the slug as-is (e.g., section-intro/section.css)
$asset_paths[] = '/' . $slug;
} else {
// Full filename syntax: get_template_part('template-parts/section-intro/section-intro')
$asset_paths[] = '/' . $slug;
}
// === CSS LOADING ===
$css_rel = null;
// Check co-located paths
foreach ($asset_paths as $asset_path) {
$css_test = $asset_path . '.css';
if (file_exists($dir . $css_test)) {
$css_rel = $css_test;
break;
}
}
// Fallback to legacy location
if (!$css_rel) {
$css_legacy = "/css/template-parts/{$filename}.css";
if (file_exists($dir . $css_legacy)) {
$css_rel = $css_legacy;
}
}
if ($css_rel) {
$version = $use_filemtime ? filemtime($dir . $css_rel) : $cacheVersion;
wp_enqueue_style(
"tp-{$filename}",
$uri . $css_rel,
[],
$version
);
}
// === JS LOADING ===
$js_rel = null;
// Check co-located paths
foreach ($asset_paths as $asset_path) {
$js_test = $asset_path . '.js';
if (file_exists($dir . $js_test)) {
$js_rel = $js_test;
break;
}
}
// Fallback to legacy location
if (!$js_rel) {
$js_legacy = "/js/template-parts/{$filename}.js";
if (file_exists($dir . $js_legacy)) {
$js_rel = $js_legacy;
}
}
if ($js_rel) {
$version = $use_filemtime ? filemtime($dir . $js_rel) : $cacheVersion;
wp_enqueue_script(
"tp-{$filename}",
$uri . $js_rel,
['jquery'],
$version,
true
);
}
}
/**
* Adds modern script attributes (defer, async, type="module") to template part scripts
*
* @param string $tag Script tag HTML
* @param string $handle Script handle
* @param string $src Script source URL
* @return string Modified script tag
*/
function tp_add_script_attributes($tag, $handle, $src) {
// Only apply to template part scripts
if (strpos($handle, 'tp-') !== 0) {
return $tag;
}
// Default: add defer to all template part scripts for better performance
$attributes = ['defer'];
// Check for .module.js files to add type="module"
if (strpos($src, '.module.js') !== false) {
$attributes[] = 'type="module"';
// Remove defer for modules (not needed, modules are deferred by default)
$attributes = ['type="module"'];
}
// Build new script tag with attributes
$tag = '<script src="' . $src . '"';
foreach ($attributes as $attr) {
if (strpos($attr, '=') !== false) {
// Attribute with value (e.g., type="module")
$tag .= ' ' . $attr;
} else {
// Boolean attribute (e.g., defer, async)
$tag .= ' ' . $attr;
}
}
$tag .= '></script>' . "\n";
return $tag;
}
add_filter('script_loader_tag', 'tp_add_script_attributes', 10, 3);
@quietcactus
Copy link
Author

quietcactus commented Oct 2, 2025

WordPress Template Parts Auto-Loader

A smart WordPress system that automatically discovers and loads CSS/JS assets for template parts. No more manual enqueuing - just drop your assets next to your PHP templates and they load automatically!

Features

  • Auto-discovery: Scans template-parts/ directory and registers hooks automatically
  • Co-located assets: Keep CSS/JS files alongside your PHP templates
  • Performance caching: Directory scan results cached for 24 hours
  • Modern script loading: Automatic defer attributes and ES6 module support
  • Smart versioning: Uses file modification time for development, static version for production
  • Flexible syntax: Supports both full filename and split template part calls
  • Cross-platform: Works on Windows, Mac, and Linux
  • Self-contained: No external dependencies or global variables required

File Organization

Organize your template parts with their assets in the same folder:

template-parts/
├── section-hero/
│   ├── section-hero.php
│   ├── section-hero.css    ← Auto-loaded!
│   └── section-hero.js     ← Auto-loaded!
├── section-intro/
│   ├── section-intro.php
│   ├── section-intro.css
│   └── section-intro.js
└── component-hero-slide/
    ├── component-hero-slide-1.php
    ├── component-hero-slide-1.css
    ├── component-hero-slide-2.php
    └── component-hero-slide-2.css

Usage

Basic Template Part Call

<?php get_template_part('template-parts/section-intro/section-intro'); ?>

Automatically loads: section-intro.css and section-intro.js

Split Syntax (Alternative)

<?php get_template_part('template-parts/section-intro/section', 'intro'); ?>

Also loads: section-intro.css and section-intro.js

Component Variations

<?php get_template_part('template-parts/component-hero-slide/component-hero-slide', '1'); ?>

Loads: component-hero-slide-1.css and component-hero-slide-1.js

Performance Features

Caching

  • Directory scan results cached for 24 hours
  • Cache automatically clears on theme switch
  • Bypasses cache when WP_DEBUG is enabled

Smart Versioning

  • Development: Uses filemtime() for instant cache busting when files change
  • Production: Uses static version '1.0.0' for optimal browser caching

The system automatically chooses the appropriate versioning strategy based on your environment:

// In development (WP_DEBUG = true): Uses file modification time
$version = filemtime($file); // Instant cache busting

// In production (WP_DEBUG = false): Uses static version
$version = '1.0.0'; // Long-term browser caching

Modern Script Loading

  • All template part scripts get defer attribute automatically
  • ES6 modules supported: name files *.module.js for type="module"
  • Non-blocking script loading for better performance

Configuration

Cache Management

// Clear cache manually if needed
delete_transient('tp_registered_parts_v3');

Debug Mode

When WP_DEBUG is enabled:

  • Cache is bypassed (always scans fresh)
  • Uses file modification time for versioning
  • Better for active development

Custom Dependencies

Scripts automatically depend on jQuery. To customize:

// In the asset loading function, modify:
wp_enqueue_script(
    "tp-{$filename}",
    $uri . $js_rel,
    ['jquery'],  // ← Change dependencies here if needed
    $version,
    true
);

Requirements

  • WordPress 5.0+
  • PHP 7.4+
  • Template parts in template-parts/ directory

Asset Naming Conventions

CSS Files

  • section-intro.css → Loads with section-intro template
  • component-hero-slide-1.css → Loads with component-hero-slide-1 template

JavaScript Files

  • section-intro.js → Standard script with defer
  • section-intro.module.js → ES6 module with type="module"

Legacy Support

Assets in legacy locations still work:

  • css/template-parts/section-intro.css
  • js/template-parts/section-intro.js

Benefits

  1. Better Organization: All related files stay together
  2. Automatic Loading: No manual wp_enqueue_style() calls needed
  3. Performance: Cached discovery + smart versioning
  4. Modern Standards: Defer scripts, ES6 module support
  5. Developer Friendly: Works with both template part syntaxes
  6. Zero Configuration: Drop files in place and they work
  7. Self-Contained: No external dependencies or setup required

How It Works

  1. Discovery: Scans template-parts/ directory on theme setup
  2. Registration: Registers get_template_part_{slug} hooks
  3. Loading: When template part is called, assets are automatically enqueued
  4. Caching: Results cached to avoid repeated directory scans

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment