functions.php, Theme Supports and Assets

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages.

Every classic theme carries a functions.php: the file in which it tells WordPress which features it supports and which CSS and JavaScript files the front end needs. This page is for theme developers. It builds the complete functions.php of the Lumen Press classic theme lumen-classic, explains each theme support it declares, the image sizes and the content width, the editor stylesheet, and the asset API (dependencies, versions, loading strategies and inline styles), and checks the result on a running site: what get_theme_support() returns and what ends up in the rendered <head>.

This page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the version to run in production), PHP 7.4 minimum and 8.3+ recommended, and WP-CLI 2.12.0, and checked on 2026-10-28. WordPress 7.2, in beta and due in December 2026, does not change the APIs described here so far.

The skeleton of lumen-classic (its style.css header and index.php) is introduced in Themes Overview, the template files and The Loop in Classic Theme Anatomy and The Loop, and how a child theme enqueues its parent’s stylesheet in Child Themes.

What functions.php is, and what it is not

functions.php sits in the theme’s root folder and behaves like a plugin that is loaded only while the theme is active. WordPress includes it on every request (front end, admin, REST, WP-CLI, cron) after the active plugins and before the after_setup_theme action, so it can call any WordPress function and register hooks. With a child theme, the child’s functions.php is loaded first and then the parent’s; neither replaces the other.

Because it is tied to the theme, the "plugin territory" rule applies: presentation (theme supports, image sizes, the content width, the theme’s own CSS, fonts and small UI scripts, menu locations, sidebars) belongs in the theme; anything that must survive a theme switch (the lp_book post type and its taxonomies and meta, shortcodes, blocks, REST routes, roles, analytics and SEO tags) belongs in a plugin, lumen-catalog for Lumen Press.

A block theme often has no functions.php at all, because theme.json and core’s defaults cover what classic themes declare by hand; see theme.json Settings. Plugins are introduced in Plugin Fundamentals and hooks in Hooks: Actions and Filters.

The lumen-classic files

The theme gains an assets/ folder for its stylesheets, scripts and fonts, and a parts/ folder for one block-based template part (explained under block-template-parts below).

wp-content/themes/lumen-classic/
├── assets/
│   ├── css/
│   │   ├── editor.css
│   │   └── fonts.css
│   ├── fonts/
│   │   └── literata-variable.woff2
│   └── js/
│       ├── navigation.js
│       └── reading-time.js
├── parts/
│   └── footer.html
├── functions.php
├── index.php
└── style.css

The complete functions.php. Every function, hook name, handle and image size is prefixed with the theme slug, and the file returns early if it is requested directly.

<?php
/**
 * Lumen Classic theme setup and assets.
 *
 * @package Lumen_Classic
 */

defined( 'ABSPATH' ) || exit;

/**
 * Sets the content width in pixels, early so other setup code can read it.
 */
function lumen_classic_content_width() {
	$GLOBALS['content_width'] = apply_filters( 'lumen_classic_content_width', 680 );
}
add_action( 'after_setup_theme', 'lumen_classic_content_width', 0 );

/**
 * Declares theme supports, image sizes and the editor stylesheet.
 */
function lumen_classic_setup() {
	add_theme_support( 'title-tag' );
	add_theme_support( 'automatic-feed-links' );
	add_theme_support( 'post-thumbnails', array( 'post', 'page', 'lp_book' ) );
	set_post_thumbnail_size( 680, 383, true );
	add_image_size( 'lumen-classic-cover', 400, 600, true );
	add_theme_support(
		'html5',
		array( 'search-form', 'comment-form', 'comment-list', 'gallery', 'caption', 'navigation-widgets' )
	);
	add_theme_support(
		'custom-logo',
		array(
			'height'               => 80,
			'width'                => 240,
			'flex-height'          => true,
			'flex-width'           => true,
			'unlink-homepage-logo' => true,
		)
	);
	add_theme_support( 'wp-block-styles' );
	add_theme_support( 'responsive-embeds' );
	add_theme_support( 'align-wide' );
	add_theme_support( 'block-template-parts' );
	add_theme_support( 'editor-styles' );
	add_editor_style( 'assets/css/editor.css' );
}
add_action( 'after_setup_theme', 'lumen_classic_setup' );

/**
 * Offers the cover size in the editor's image size menu.
 *
 * @param array $sizes Size names keyed by slug.
 * @return array
 */
function lumen_classic_image_size_names( $sizes ) {
	$sizes['lumen-classic-cover'] = __( 'Book cover', 'lumen-classic' );
	return $sizes;
}
add_filter( 'image_size_names_choose', 'lumen_classic_image_size_names' );

/**
 * Returns a cache-busting version for a theme file: its modification time, or the theme version.
 *
 * @param string $file Path relative to the theme folder.
 * @return string
 */
function lumen_classic_asset_version( $file ) {
	$path = get_theme_file_path( $file );
	return file_exists( $path ) ? (string) filemtime( $path ) : wp_get_theme()->get( 'Version' );
}

/**
 * Enqueues the front-end styles and scripts.
 */
function lumen_classic_enqueue_assets() {
	wp_enqueue_style(
		'lumen-classic-fonts',
		get_theme_file_uri( 'assets/css/fonts.css' ),
		array(),
		lumen_classic_asset_version( 'assets/css/fonts.css' )
	);

	wp_enqueue_style(
		'lumen-classic-style',
		get_stylesheet_uri(),
		array( 'lumen-classic-fonts' ),
		wp_get_theme()->get( 'Version' )
	);

	$accent = sanitize_hex_color( get_theme_mod( 'lumen_classic_accent_color', '#a3271f' ) );
	if ( $accent ) {
		wp_add_inline_style( 'lumen-classic-style', ':root { --lumen-classic-accent: ' . $accent . '; }' );
	}

	wp_enqueue_script(
		'lumen-classic-navigation',
		get_theme_file_uri( 'assets/js/navigation.js' ),
		array(),
		lumen_classic_asset_version( 'assets/js/navigation.js' ),
		array(
			'in_footer' => false,
			'strategy'  => 'defer',
		)
	);

	if ( is_singular( 'lp_book' ) ) {
		wp_enqueue_script(
			'lumen-classic-reading-time',
			get_theme_file_uri( 'assets/js/reading-time.js' ),
			array( 'lumen-classic-navigation', 'wp-i18n' ),
			lumen_classic_asset_version( 'assets/js/reading-time.js' ),
			array( 'strategy' => 'defer' )
		);
		wp_set_script_translations( 'lumen-classic-reading-time', 'lumen-classic' );
	}

	if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
		wp_enqueue_script( 'comment-reply' );
	}
}
add_action( 'wp_enqueue_scripts', 'lumen_classic_enqueue_assets' );

after_setup_theme

after_setup_theme fires once per request, right after the active theme’s functions.php files are loaded and before init. Theme supports must be declared here rather than at the top level of the file (where a child theme cannot remove them, because its file runs first) or on init, which the Theme Handbook notes is too late for some features, such as post thumbnail support. A child theme that wants to change a parent’s support hooks its own function to after_setup_theme with a later priority (11 or more) and calls remove_theme_support() or add_theme_support() again.

lumen-classic uses two callbacks: the content width at priority 0, so any later setup code can read it, and the rest at the default priority 10.

Content width

$content_width is a global, in pixels, that tells WordPress the maximum width of the content column. Core reads it in two places: oEmbed (wp_embed_defaults() sizes YouTube and other embeds to it) and the editor, where large images inserted into the content are constrained to it (image_constrain_size_for_editor()). Setting it through a filterable function rather than a bare $content_width = 680; at file level lets a child theme or plugin change it.

A theme with a theme.json does not need the global: settings.layout.contentSize plays the same role and also generates the layout CSS. The block theme lumen uses 680px there for the same column.

Image sizes

post-thumbnails support enables the Featured Image panel. Passing an array of post types restricts it, which is why lumen-classic lists lp_book explicitly: a theme that supports thumbnails only for post hides the panel on books, however the plugin registered the post type (the post type also needs thumbnail in its supports).

  • set_post_thumbnail_size() defines the post-thumbnail size used by the_post_thumbnail() without arguments (680 × 383, hard-cropped to 16:9).

  • add_image_size() adds a named size. lumen-classic-cover is 400 × 600 with crop set to true, so covers of any ratio are cut to 2:3 from the centre. With false, the image is scaled to fit inside the box and keeps its ratio.

  • The image_size_names_choose filter puts the new size in the editor’s resolution menu; without it the size exists but authors cannot pick it.

Sizes are generated when an image is uploaded. Images uploaded before the theme was activated do not have the new sizes until they are regenerated with wp media regenerate (WP-CLI) or a plugin. Every registered size costs disk space and upload time for every image, so a theme registers only the sizes its templates print. Responsive images (srcset), the big-image threshold and the core sizes are covered in Media Library and Images and Media API.

Theme supports

add_theme_support( $feature, ...$args ) records a feature in the global $_wp_theme_features; current_theme_supports( $feature ) and get_theme_support( $feature ) read it. Most features are a simple flag, some take an array of options. The features declared by lumen-classic, and the related ones a classic theme meets:

Feature Since Effect

title-tag

4.1

WordPress prints the <title> element from wp_head(); the theme must not hard-code one in header.php. Change the text with the document_title_parts filter.

automatic-feed-links

3.0

Adds the RSS <link rel="alternate"> tags for the posts and comments feeds to <head>.

post-thumbnails

2.9

Enables featured images, for all post types or the ones listed.

html5

3.6

Uses HTML5 markup for the listed core outputs: search-form, comment-form, comment-list, gallery, caption, and navigation-widgets (5.5, the <nav> wrapper of menu, archive and category widgets). An array is required. script and style (5.3) removed the type attributes; since 7.0 they are deprecated and ignored, because core no longer prints type="text/javascript" or type="text/css" for any theme.

custom-logo

4.5

Adds the Site Logo control (Customizer, and the Site Logo block). The options give the suggested crop size; unlink-homepage-logo (5.5) stops linking the logo on the front page. Print it with the_custom_logo().

wp-block-styles

5.0

Loads core’s opinionated block styles (the theme.css of each block, such as the quote’s left border) on the front end. Not recommended for themes with a theme.json, which style blocks there instead.

responsive-embeds

5.0

Adds the wp-embed-responsive body class, so embedded videos keep their aspect ratio as the column shrinks.

align-wide

5.0

Offers the Wide and Full alignments in the block toolbar. The theme’s CSS must style .alignwide and .alignfull.

editor-styles

5.0

Makes the block editor load the stylesheets registered with add_editor_style(). Since 6.8 it also gives a classic theme the Appearance > Design screen (Style Book and Patterns).

block-template-parts

6.1

Lets a classic theme ship block template parts in parts/ and edit them in the Site Editor; templates still come from PHP files.

appearance-tools

6.5

Opts in to border, link colour, spacing, dimension and similar design tools for blocks in themes without theme.json.

menus, widgets

n/a

Not declared directly; register_nav_menus() and register_sidebar() add them. See Navigation and Menus and Widgets and Sidebars.

custom-background, custom-header

3.4

Customizer controls for a background and a header image; see Customizer API.

editor-color-palette, editor-font-sizes, editor-gradient-presets, disable-custom-colors, disable-custom-font-sizes

5.0, 5.4

Classic-theme versions of colour, font-size and gradient presets. A theme that ships theme.json sets these under settings.color and settings.typography instead, and theme.json wins.

The editor stylesheet

add_editor_style() registers a stylesheet, relative to the theme folder, for the editor; editor-styles support is what makes the block editor actually load it. The block editor rewrites the selectors so they apply only to the editing canvas (body becomes .editor-styles-wrapper), so the file targets plain elements and the theme’s own classes, never editor class names:

/* assets/css/editor.css: the editing canvas uses the front-end typography and column. */
body {
	font-family: "Literata", Georgia, serif;
	max-width: 680px;
}

Since 7.1 the post editor is always iframed: add_editor_style() files and enqueue_block_assets styles load inside the iframe, while admin_enqueue_scripts CSS stays outside it. wp_enqueue_block_style() attaches per-block CSS that loads only where the block appears.

block-template-parts

With block-template-parts, lumen-classic keeps its footer as block markup in parts/footer.html and prints it from PHP, so an editor can change the footer in the Site Editor without touching the theme:

<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">Lumen Press, independent publishers since 2009.</p>
<!-- /wp:paragraph -->
<footer><?php block_template_part( 'footer' ); ?></footer>
<?php wp_footer(); ?>

The Site Editor opens in a template-parts-only mode for such themes: parts can be edited, but not created or deleted. Going further (block templates, theme.json) is the hybrid path described in Hybrid Themes and Migration to Block Themes.

Checking the supports with WP-CLI

The site used below is the standard local stack with lumen-classic active and a stand-in registration of lp_book (the real one lives in the lumen-catalog plugin; see Custom Post Types and Taxonomies). The Compose file is the one from Themes Overview; other setups are in Local Development Environments.

WP-CLI loads the active theme, so after_setup_theme has run when wp eval executes:

docker compose exec cli wp eval '
foreach ( array( "title-tag", "post-thumbnails", "html5", "custom-logo", "editor-styles", "wp-block-styles",
	"responsive-embeds", "align-wide", "block-template-parts", "menus", "widgets", "custom-background" ) as $f ) {
	echo $f, ": ", wp_json_encode( get_theme_support( $f ) ), "\n";
}
echo "content_width: ", $GLOBALS["content_width"], "\n";
echo "embed defaults: ", wp_json_encode( wp_embed_defaults() ), "\n";
echo "editor stylesheets: ", wp_json_encode( get_editor_stylesheets() ), "\n";
var_dump( current_theme_supports( "post-thumbnails", "lp_book" ), current_theme_supports( "post-thumbnails", "attachment" ) );
'
title-tag: true
post-thumbnails: [["post","page","lp_book"]]
html5: [["search-form","comment-form","comment-list","gallery","caption","navigation-widgets"]]
custom-logo: [{"width":240,"height":80,"flex-width":true,"flex-height":true,"header-text":"","unlink-homepage-logo":true}]
editor-styles: true
wp-block-styles: true
responsive-embeds: true
align-wide: true
block-template-parts: true
menus: false
widgets: false
custom-background: false
content_width: 680
embed defaults: {"width":680,"height":1000}
editor stylesheets: ["http:\/\/lumenpress.test\/wp-content\/themes\/lumen-classic\/assets\/css\/editor.css"]
bool(true)
bool(false)

What the output shows:

  • Flag features return true. Features with arguments return an array of the arguments, which is why post-thumbnails and html5 are wrapped in an extra array; the first element is the list that was passed.

  • custom-logo is merged with core’s defaults (header-text was not passed).

  • Features never declared return false; menus and widgets stay false because the theme registers no menu location or sidebar yet.

  • oEmbed uses the content width (680) and a height of 1.5 times the width capped at 1000; current_theme_supports( 'post-thumbnails', 'lp_book' ) checks the post type list.

  • The editor stylesheet URL is http only because a CLI request is not HTTPS; in the browser it follows the site URL.

The registered sizes, including the theme’s two:

docker compose exec cli wp eval 'foreach ( wp_get_registered_image_subsizes() as $n => $s ) { printf( "%-20s %4d x %4d %s\n", $n, $s["width"], $s["height"], $s["crop"] ? "crop" : "fit" ); }'
thumbnail             150 x  150 crop
medium                300 x  300 fit
medium_large          768 x    0 fit
large                1024 x 1024 fit
1536x1536            1536 x 1536 fit
2048x2048            2048 x 2048 fit
post-thumbnail        680 x  383 crop
lumen-classic-cover   400 x  600 crop

Enqueueing styles and scripts

Assets reach the page through the dependency-aware queues of WP_Styles and WP_Scripts, never through <link> or <script> tags typed into header.php. Enqueueing lets WordPress print each file once, in dependency order, lets child themes and plugins dequeue or replace a file by its handle, and lets caching and optimisation plugins work on the list. Front-end assets are enqueued on the wp_enqueue_scripts action (the name covers styles too); the admin uses admin_enqueue_scripts, the login screen login_enqueue_scripts, and the editor enqueue_block_editor_assets (editor UI) or enqueue_block_assets (editor content and front end).

The parameters

wp_enqueue_style( $handle, $src, $deps, $ver, $media ) and wp_enqueue_script( $handle, $src, $deps, $ver, $args ) share their first four parameters. wp_register_style() and wp_register_script() take the same ones and only make the handle known; a later wp_enqueue_*( $handle ) or a dependency on it prints the file.

Parameter Meaning

$handle

Unique name, prefixed with the theme slug. Styles print with the id <handle>-css, scripts with <handle>-js. Enqueueing a handle already registered reuses the registration and ignores the other arguments, which is how a child theme replaces a parent’s file (see Child Themes).

$src

Full URL. get_theme_file_uri( 'assets/js/navigation.js' ) (4.7) returns the child theme’s copy when one exists and the parent’s otherwise; get_stylesheet_uri() returns the active theme’s style.css; get_template_directory_uri() always points at the parent.

$deps

Handles that must load first. lumen-classic-style depends on lumen-classic-fonts; lumen-classic-reading-time depends on the theme’s navigation script and on core’s wp-i18n. If a dependency is not registered, the dependent file is dropped from the page; since 6.9.1 a "called incorrectly" notice names the missing handle (visible with WP_DEBUG).

$ver

Appended as ?ver=. The default false appends the WordPress version, which changes only on core updates (stale caches after a theme deploy, and the core version disclosed); null appends nothing. Use the theme version, or the file’s modification time for files that change between releases.

$media (styles)

The media attribute, all by default.

$args (scripts)

Since 6.3 an array with in_footer (bool) and strategy (defer or async). Before 6.3 the fifth parameter was the boolean $in_footer, which is still accepted.

lumen_classic_asset_version() uses filemtime() on the file that get_theme_file_path() resolves, so the URL changes whenever the file changes, without bumping the theme version on every edit. It falls back to the theme version if the file is missing. On servers that deploy every file with the same timestamp (some container images and release tools do), the modification time stops changing and the theme version is the safer choice; build tools that write a content hash (such as the index.asset.php files generated by @wordpress/scripts for blocks) are better still. The main style.css keeps the theme version from its header, so its handle matches what Child Themes expects.

Loading strategies: defer and async

A plain <script src> in the <head> blocks HTML parsing until it has downloaded and run, which is why older themes moved every script to the footer. Since 6.3 a script can ask for a loading strategy instead:

  • defer: downloads in parallel, runs after the document is parsed, in document order. Suitable for almost all theme scripts, and it can stay in the <head> (in_footer false) so the download starts early.

  • async: downloads in parallel and runs as soon as it arrives, in no particular order. Only for independent scripts with no dependents, such as a self-contained statistics snippet.

The strategy is a request. WordPress computes an eligible strategy from the dependency tree so that execution order is never broken: a delayed script must have only delayed dependents, and a script is never upgraded from defer to async. Four variations on lumen-classic-navigation were enqueued from a must-use test plugin to see what core prints:

<?php
// wp-content/mu-plugins/lumen-strategy-probe.php: local test only.
add_action(
	'wp_enqueue_scripts',
	function () {
		$probe = isset( $_GET['probe'] ) ? sanitize_key( wp_unslash( $_GET['probe'] ) ) : '';
		if ( 'blocking' === $probe ) {
			wp_enqueue_script( 'lumen-probe-blocking', get_theme_file_uri( 'assets/js/probe.js' ), array( 'lumen-classic-navigation' ), '1' );
		}
		if ( 'after' === $probe ) {
			wp_add_inline_script( 'lumen-classic-navigation', 'window.lumenProbe = true;' );
		}
		if ( 'before' === $probe ) {
			wp_add_inline_script( 'lumen-classic-navigation', 'window.lumenProbe = true;', 'before' );
		}
		if ( 'async' === $probe ) {
			wp_enqueue_script( 'lumen-probe-async', get_theme_file_uri( 'assets/js/probe.js' ), array(), '1', array( 'strategy' => 'async', 'in_footer' => true ) );
			wp_enqueue_script( 'lumen-probe-dep', get_theme_file_uri( 'assets/js/probe2.js' ), array( 'lumen-probe-async' ), '1', array( 'strategy' => 'defer', 'in_footer' => true ) );
		}
	},
	20
);

The resulting tags (URLs shortened to the file name):

Probe Printed tags Why

blocking

<script data-wp-strategy="defer" id="lumen-classic-navigation-js" src="…navigation.js…"> then <script id="lumen-probe-blocking-js" src="…probe.js…">

A blocking dependent forces the dependency to load blocking too; data-wp-strategy records what was asked for, the missing defer attribute shows what was granted.

after

the navigation tag without defer, then <script id="lumen-classic-navigation-js-after">

An inline script added after a handle (the default position of wp_add_inline_script()) turns the handle blocking.

before

<script id="lumen-classic-navigation-js-before"> then the navigation tag with defer

Inline code placed before the handle does not change its strategy.

async

<script data-wp-strategy="async" defer id="lumen-probe-async-js" …> then <script data-wp-strategy="defer" defer id="lumen-probe-dep-js" …>

An async script with a deferred dependent is downgraded to defer.

Data a script needs before it runs (configuration, a REST nonce for a script that calls the API) is therefore passed with wp_add_inline_script( $handle, $code, 'before' ) or wp_localize_script(), both of which print before the file and keep it deferred. Since 6.5, JavaScript modules have their own API (wp_register_script_module(), wp_enqueue_script_module()), which prints type="module" scripts, deferred by nature; the Interactivity API uses it.

Inline CSS

wp_add_inline_style( $handle, $css ) prints CSS in a <style id="<handle>-inline-css"> element right after the handle’s stylesheet. lumen-classic uses it to turn an accent colour stored as a theme modification into a CSS custom property. The value comes from the database, so it is validated with sanitize_hex_color(), which returns an empty value for anything that is not a #rgb or #rrggbb colour, before it is concatenated into CSS. The function returns false and prints nothing if the handle is not registered, and passing a string that contains </style> triggers a "called incorrectly" notice: the $css argument is the CSS only, without tags. How the accent setting is registered and sanitised in the Customizer is shown in Customizer API.

Conditional loading and core scripts

lumen-classic-reading-time is enqueued only on single book pages (is_singular( 'lp_book' )): conditional tags work inside wp_enqueue_scripts because the main query has run by then. Its JavaScript uses wp.i18n from the wp-i18n dependency, and wp_set_script_translations() loads the theme’s JSON translations for it (see Internationalization):

( () => {
	const { sprintf, _n } = wp.i18n;
	const content = document.querySelector( '.entry-content' );
	const target = document.querySelector( '.lumen-classic-reading-time' );
	if ( ! content || ! target ) {
		return;
	}
	const minutes = Math.max( 1, Math.round( content.textContent.trim().split( /\s+/ ).length / 230 ) );
	target.textContent = sprintf( _n( '%d minute read', '%d minutes read', minutes, 'lumen-classic' ), minutes );
} )();

comment-reply is a core script, registered but not enqueued; themes enqueue it only on singular views with threaded comments open. Other bundled libraries (jquery, wp-api-fetch, wp-dom-ready) are used through their handles too, never through a copy shipped in the theme or loaded from a CDN. jQuery runs in noConflict mode, so code that uses it wraps itself: jQuery( function ( $ ) { … } );.

The rendered head

The site is served with wp server inside the WP-CLI container, and two pages are requested with the site’s host name: the home page and the single book /books/the-glass-orchard/. The script prints only the tags that come from the theme, the feeds and the title:

docker compose exec -d cli wp server --host=127.0.0.1 --port=8080
docker compose exec cli php -r '
  $c = stream_context_create( array( "http" => array( "header" => "Host: lumenpress.test\r\nX-Forwarded-Proto: https\r\n" ) ) );
  $h = file_get_contents( "http://127.0.0.1:8080/books/the-glass-orchard/", false, $c );
  preg_match( "#<head>(.*)</head>#s", $h, $m );
  foreach ( explode( "\n", $m[1] ) as $l ) {
    if ( preg_match( "/lumen-classic|<title|wp-i18n|wp-hooks|feed/", $l ) ) { echo trim( $l ), "\n"; }
  }'

The local wp-config.php has one extra line above "That’s all, stop editing", the one a site behind a TLS-terminating proxy uses, so the request header makes WordPress build https URLs (see wp-config.php Reference):

if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) { $_SERVER['HTTPS'] = 'on'; }

The output:

<title>The Glass Orchard &#8211; Lumen Press</title>
<link rel="alternate" type="application/rss+xml" title="Lumen Press &raquo; Feed" href="https://lumenpress.test/feed/" />
<link rel="alternate" type="application/rss+xml" title="Lumen Press &raquo; Comments Feed" href="https://lumenpress.test/comments/feed/" />
<link rel='stylesheet' id='lumen-classic-fonts-css' href='https://lumenpress.test/wp-content/themes/lumen-classic/assets/css/fonts.css?ver=1793197100' media='all' />
<link rel='stylesheet' id='lumen-classic-style-css' href='https://lumenpress.test/wp-content/themes/lumen-classic/style.css?ver=0.1.0' media='all' />
<style id="lumen-classic-style-inline-css">
:root { --lumen-classic-accent: #a3271f; }
/*# sourceURL=lumen-classic-style-inline-css */
<script data-wp-strategy="defer" defer id="lumen-classic-navigation-js" src="https://lumenpress.test/wp-content/themes/lumen-classic/assets/js/navigation.js?ver=1793197100"></script>
<script id="wp-hooks-js" src="https://lumenpress.test/wp-includes/js/dist/hooks.min.js?ver=f0f188028580e8dc1255"></script>
<script id="wp-i18n-js" src="https://lumenpress.test/wp-includes/js/dist/i18n.min.js?ver=1dfe7db3940c23ea9216"></script>
<script id="wp-i18n-js-after">
//# sourceURL=wp-i18n-js-after
<script data-wp-strategy="defer" defer id="lumen-classic-reading-time-js" src="https://lumenpress.test/wp-content/themes/lumen-classic/assets/js/reading-time.js?ver=1793197100"></script>

What to read in it:

  • The <title> comes from title-tag and the feed links from automatic-feed-links.

  • The fonts stylesheet prints before style.css because of the dependency, and the inline custom property follows the stylesheet it was attached to.

  • The ver values are the files' modification times, except style.css, which carries the theme version; core files carry content hashes.

  • wp-i18n and its own dependency wp-hooks are printed as normal blocking scripts before the deferred reading-time.js. A delayed script may depend on blocking ones; only the reverse forces a fallback.

  • reading-time.js has no in_footer, so it prints in the head with defer. On the home page it and the two core scripts are absent; the rest is identical.

  • comment-reply is absent: the stand-in lp_book has no comments support, so comments_open() is false.

Further down, core adds its own inline styles (wp-block-library-inline-css, per-block styles such as wp-block-paragraph-inline-css for the footer part, classic-theme-styles-inline-css and global-styles-inline-css): for classic themes 7.1 loads block CSS only for the blocks present on the page, which is also how wp-block-styles arrives. Front-end assets are never concatenated by core; CONCATENATE_SCRIPTS applies to the admin only.

Older books and tutorials

Older advice Current state (7.1)

Hard-code <link rel="stylesheet"> and <script> tags, and the <title> element, in header.php

Enqueue on wp_enqueue_scripts and declare title-tag; hard-coded tags cannot be dequeued, are printed twice when a plugin enqueues the same library, and bypass the version and strategy handling.

Move every script to the footer to avoid blocking (the boolean $in_footer)

Since 6.3 use 'strategy' => 'defer'; the script can stay in the head and still not block. The boolean still works.

Concatenate CSS and JavaScript into one file and use image sprites for speed, and rely on WordPress’s built-in minification (Pro WordPress)

Over HTTP/2 and HTTP/3 several small cached files are not slower, and core neither minifies nor concatenates front-end assets. Ship minified files from a build step, enqueue only what a page uses, and see Performance and Caching.

jQuery code that calls $ directly (Pro WordPress)

WordPress loads jQuery in noConflict mode; wrap the code or use jQuery. New theme scripts rarely need jQuery at all.

Load web fonts from Google’s servers with a <link> in the header

Bundle the font files and enqueue a local @font-face stylesheet (as lumen-classic-fonts does), or use the Font Library; see Typography and Fonts.

add_theme_support( 'html5', array( …, 'script', 'style' ) ) to drop the type attributes

Since 7.0 the two values are deprecated and ignored; core prints no type on scripts and styles anyway.

Declaring colour palettes and font sizes with editor-color-palette and editor-font-sizes

Still supported for classic themes, but a theme.json is the current way and also feeds the Style Book; see Hybrid Themes and Migration to Block Themes.

Registering custom post types and shortcodes in functions.php

Plugin territory: they belong in lumen-catalog, so the content survives a theme switch.

The full list of corrections is collected in Where Older Books and Tutorials Are Outdated.

Common errors and pitfalls

Symptom Cause and fix

The Featured Image panel is missing on books

post-thumbnails lists post types without lp_book, or the post type has no thumbnail support. Check with wp eval 'var_dump( current_theme_supports( "post-thumbnails", "lp_book" ) );'.

An enqueued script never appears

One of its dependencies is not registered (a typo in the handle, or a plugin handle that is not active; WP_DEBUG shows a notice since 6.9.1), or it was enqueued on init or in a template after wp_head() ran. Enqueue on wp_enqueue_scripts and check the handles.

A script asked for defer prints without it

A dependent is blocking, or an inline script was added after it. Compare the data-wp-strategy attribute with the attributes actually printed, and move inline code to before.

Visitors keep an old CSS file after a deploy

$ver was omitted (WordPress version) or the theme version was not bumped. Use the file’s modification time or a content hash.

Every asset URL changes on every deploy

The deploy rewrites modification times. Use the theme version or a build hash instead of filemtime().

The editor stylesheet has no effect in the block editor

editor-styles is not declared, or the path passed to add_editor_style() is not relative to the theme folder. get_editor_stylesheets() lists what was found.

Editor CSS targets .editor-styles-wrapper or admin classes and breaks in 7.1

The editor is iframed; write editor CSS for plain elements and enqueue content CSS on enqueue_block_assets.

New image sizes are missing for old images

Sizes are generated at upload. Run wp media regenerate after adding a size.

A child theme’s stylesheet replaces the parent’s instead of adding to it

get_stylesheet_uri() points at the child’s style.css when a child is active; the child registers the parent’s handle first, as described in Child Themes.

Security notes: functions.php runs on every request with full privileges and must not echo anything (output before headers breaks redirects and REST responses). Values that go into inline CSS or JavaScript are validated or escaped for that context first (sanitize_hex_color() above, wp_json_encode() for script data). Bundling assets instead of loading them from third-party CDNs protects visitors' privacy and removes a supply-chain risk.