Adding JavaScript and CSS files to a WordPress site through direct <script> and <link> tags hard-coded into a theme template is one of the most common mistakes made by developers coming to WordPress from static HTML or non-CMS backgrounds. It works on the surface, but it bypasses the entire WordPress asset management system and creates a cascade of problems. Scripts added directly to header.php do not participate in dependency resolution: if your custom script depends on jQuery, hardcoding both in the wrong order causes a silent JavaScript error in the browser with no warning in the template itself. Duplicate script loading is another frequent consequence — a theme loads jQuery, a plugin loads jQuery again because it cannot detect the theme’s hard-coded version, and the browser executes the library twice. Direct tags also break the WordPress script debug mode (SCRIPT_DEBUG), prevent minification and concatenation plugins from processing your assets, and make it impossible for child themes to override parent theme scripts cleanly. The correct approach is to use wp_enqueue_scripts action hook together with wp_enqueue_script() and wp_enqueue_style(). The enqueueing functions accept a handle, a file URL, an array of dependency handles, a version string, and for scripts, a flag for footer placement. WordPress uses these parameters to build a dependency graph, deduplicate scripts that multiple plugins or themes request, and output everything in the correct order. The $deps array is the key to dependency resolution: listing 'jquery' as a dependency guarantees jQuery loads before your script regardless of load order in the source. Setting $in_footer to true moves your script to just before </body>, improving perceived page load time without manual template editing. The version string, typically your theme version constant or a file modification timestamp, controls cache busting: incrementing it forces browsers and CDNs to fetch the updated file immediately without requiring users to manually clear their cache. The snippet below shows the complete recommended pattern for a theme that adds one custom stylesheet and one custom script with a jQuery dependency.
Problem: Scripts and styles added directly to templates bypass WordPress dependency resolution and cause duplicate loading issues.
Solution: Add the following code to your functions.php file:
<?php
add_action( 'wp_enqueue_scripts', 'ha_enqueue_theme_assets' );
function ha_enqueue_theme_assets() {
$theme_version = wp_get_theme()->get( 'Version' );
// Enqueue main stylesheet
wp_enqueue_style(
'ha-main-style', // unique handle
get_stylesheet_uri(), // URL
array(), // no CSS dependencies
$theme_version // version for cache busting
);
// Enqueue custom JavaScript with jQuery as dependency
wp_enqueue_script(
'ha-main-script', // unique handle
get_template_directory_uri() . '/js/main.js', // URL
array( 'jquery' ), // depends on jQuery
$theme_version, // version
true // load in footer
);
}
NOTE: To enqueue assets only on specific pages, wrap the enqueue call in a conditional tag such as is_singular(), is_front_page(), or is_page( 'contact' ). Loading scripts globally on every page when they are only needed on one template wastes bandwidth and slows down unrelated pages. For admin-only assets use the admin_enqueue_scripts hook instead of wp_enqueue_scripts — the latter does not fire on admin pages at all.