This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Sparkling — a free classic (non-block) WordPress theme by Colorlib, v2.6.0, derived from Underscores (_s) and built on Bootstrap 3.4.1. Text domain: sparkling. Verified against WordPress 7.0.2 on PHP 8.5.
This directory is a WordPress theme folder, not an application project: there is no package.json, composer.json, linter config, or test suite committed. Files ship exactly as they sit on disk.
- No build step.
style.cssandassets/css/*.cssare edited directly — there is no SCSS source. The.min.css/.min.jsfiles are committed alongside their unminified twins and must be regenerated by hand (npx terser in.js -c -m -o out.min.js) when the source changes — nothing does it automatically. - Which file is actually loaded matters.
sparkling_scripts()(functions.php:311) enqueues the minifiedbootstrap.min.jsandskip-link-focus-fix.min.jsbut the unminifiedfunctions.js; the Customizer enqueues unminifiedcustomizer.js. Check the enqueue before editing a.minfile. - Line endings are CRLF throughout. Keep them — mixing produces noisy diffs and a PHPCS warning.
- To run it, copy or symlink this folder into a WordPress install's
wp-content/themes/. - Version lives in the
style.csstheme header and is read once into theSPARKLING_VERSIONconstant (functions.php:8), which cache-busts every theme asset. Bump the header and every asset URL follows — never hardcode a version in an enqueue. The About screen's Changelog tab renderschangelog.txtfrom the theme root.
The theme was hardened in 2.5.0; keep these invariants when touching it.
- Every AJAX handler needs a capability check and
check_ajax_referer(). Do not hook privileged work toadmin_init—admin-ajax.phpfiresadmin_initbefore its authentication branch, so anadmin_initcallback that acts on$_GETis reachable by anonymous requests. That was a real unauthenticated front-page takeover in 2.4.11. - Colours printed into the inline
<style>block must go throughsparkling_css_color()(inc/extras.php:299), which re-validates at output time rather than trusting stored options. Customizer sanitizers reject invalid input, but options saved by older versions may still hold arbitrary text. - Anything rendering user-supplied data (tag names, display names, author bios, titles) must be escaped — contributors can otherwise store markup that runs for every visitor.
- Widgets sanitize on save in
update(); the layout metabox validates against$site_layoutbefore writing post meta.
$site_layout and $options_categories are declared at the top of functions.php but filled by sparkling_init_globals() on init (functions.php:434). Two reasons, both of which bit the theme before: calling esc_html__() while functions.php is parsed triggers WordPress 6.7+'s _load_textdomain_just_in_time notice on every request, and get_categories() at parse time added a term query to every request including REST, cron and AJAX. Anything reading those globals must run on init or later.
Settings live in one of two places, and reading from the wrong one silently returns the default:
- Serialized
sparklingoption array (legacy Options Framework style) — the majority. Customizer settings are declared as'sparkling[key]'with'type' => 'option', and read with the theme's own helperof_get_option( 'key', $default )(functions.php:472), which unpacksget_option( 'sparkling' ). - Plain theme mods — only
sparkling_excerptsandsparkling_page_comments. Declared without thesparkling[...]wrapper and read withget_theme_mod().
When adding an option, match the declaration style in inc/customizer.php to the read function in the template.
Colors, typography, and other option-driven styles are not in style.css. get_sparkling_theme_options() (inc/extras.php:256) echoes a <style> block into wp_head built by concatenating option values into selectors. Styling that must respond to a Customizer setting goes there; static styling goes in style.css.
get_layout_class() (functions.php:565) resolves layout by precedence: per-post site_layout meta (set by the metabox in inc/metaboxes.php) → WooCommerce page's woo_site_layout option → global site_layout option. It returns a CSS class (side-pull-left, side-pull-right, no-sidebar, full-width) applied to the .row in header.php:96.
The sidebar #secondary is always rendered; no-sidebar and full-width hide it purely via CSS (style.css:223). Column widths come from sparkling_main_content_bootstrap_classes() (functions.php:31).
Wrapper divs are split across three files, so page templates must close what they open:
header.phpopens#page→#content→.container.main-content-area→.row→.main-content-innersidebar.phpcloses.main-content-inner, then emits#secondaryfooter.phpcloses.row,.container,.site-content,#page
A template that calls get_sidebar() is balanced. A template that skips it must close .main-content-inner itself — see page-fullwidth.php:39. Getting this wrong breaks the page markup rather than failing loudly.
The theme had no third-party framework as of 2.6.0. The bundled Epsilon framework (MachoThemes, abandoned upstream) was deleted: the theme used exactly one thing from it — a checkbox control, seven times — while its AJAX layer registered handlers with no nonce or capability check, including a dispatcher that called static methods named in $_POST.
Sparkling_Customize_Toggle_Control (inc/class-sparkling-customize-toggle-control.php) replaces it. It is presentation only — it renders a checkbox bound with $this->link(), and the value is stored and sanitised entirely by the WP_Customize_Setting it attaches to. That is why the swap changed no saved data, and it's the property to preserve if you add controls: put validation in the setting's sanitize_callback, never in the control.
Epsilon_Control_Toggle survives as a deprecated subclass alias so child themes that registered their own controls with it keep working. Don't use it in new code, and don't remove it without a major version.
Switch styling lives in assets/css/customizer.css, loaded only on customize_controls_enqueue_scripts.
- Templates:
index.php/archive.php/search.phpdispatch totemplate-parts/content-{post-format}.phpviaget_template_part();content.phpis the fallback, withcontent-single.php,content-page.php,content-video.php,content-none.phpalongside. - Navigation:
WP_Bootstrap_Navwalker(inc/class-wp-bootstrap-navwalker.php) emits BS3 markup.sparkling_make_top_level_menu_clickable()(inc/extras.php:537) injects inline JS onwp_footer. Three menu locations:primary,footer-links,social-menu. - Customizer sections (all under the
sparkling_main_optionspanel): content, slider, layout, call-to-action, typography, header, footer, social, archive. - Widgets:
Sparkling_Social_Widget,Sparkling_Popular_Posts,Sparkling_Categoriesin inc/widgets/, registered insparkling_widgets_init(). Seven widget areas: sidebar, 3 homepage, 3 footer. - Admin welcome screen: inc/welcome-screen/ adds an "About Sparkling" theme page; the recommended-plugins list is data in inc/welcome-screen/welcome-page-setup.php.
- WooCommerce: support declared in
sparkling_woo_setup();is_it_woocommerce_page()andget_woocommerce_page_id()(functions.php:505) exist because layout resolution needs to identify WC pages including the shortcode-based cart/checkout.
- Every theme function is prefixed
sparkling_, and template-level ones are wrapped inif ( ! function_exists( ... ) ) : ... endif;so child themes can override. Preserve this when adding functions. - WordPress coding standards formatting: tabs for indent, spaces inside parentheses,
esc_html__()/esc_html_e()for output. - Reuse the existing sanitize callbacks (
sparkling_sanitize_checkbox,_hexcolor,_nohtml,_number,_textarea,_slidecat,_layout,_typo_*) at inc/customizer.php:936 rather than writing new ones. - Assets are conditionally enqueued: flexslider CSS/JS only when
is_home()/is_front_page()and the slider option is on; academicons only when its option is on. Keep new assets conditional the same way. - i18n:
.po/.mopairs plussparkling.potin languages/.wpml-config.xmlwhitelists exactly four option keys (w2f_cfa_text,w2f_cfa_button,w2f_cfa_link,custom_footer_text) as WPML admin texts — a new user-entered option that needs translating must be added there too.
assets/js/functions.js is plain DOM APIs, no jQuery, and is enqueued with no jQuery dependency, in the footer. It keeps window.SparklingIsMobile and window.generateMobileMenu as globals because child themes may call them, reproduces jQuery's swing easing so the scroll-to-top motion is unchanged, and honours prefers-reduced-motion.
Bootstrap's own JS still requires jQuery and still loads it — only theme-owned code dropped the dependency.
Do not mix Bootstrap majors. Until 2.5.0 the theme shipped Bootstrap 3.3.7 CSS with Bootstrap 4.0.0 JS against Bootstrap 3 markup. The mobile menu only opened because BS4's JS adds .show and BS3 has an unrelated .show utility (display:block !important) — so the collapse animation never ran, and .open state was hand-patched in functions.js. Both files are now stock 3.4.1 (which is also the CVE-2019-8331 fix).
There is no committed test suite, but this theme is straightforward to verify and regressions here are visual and silent:
- Run it on WordPress 7.0.2 / PHP 8.5 with
WP_DEBUGandWP_DEBUG_LOGon and sweep every template (home, single, each post format, page, full-width, category, tag, author, date, search, empty search, attachment, 404, feeds). The target is zero notices — 2.5.0 achieves that. - Exercise the option-driven paths too; they are dormant by default and hid several bugs: slider on/off and slide-links on/off, call-to-action, all colour pickers, sticky header, academicons.
PHPCompatibilityWPattestVersion 8.5reports 0 errors/0 warnings; keep it there.- The
WordPress.Security.*PHPCS sniffs report ~56 remaining hits that are reviewed false positives:sparkling_css_color()-validated colours, core-supplied$args['before_widget'],wp_list_categories()output, and pre-escaped$time_string. Check new hits against that list before assuming they are noise. - A pixel diff across templates at desktop and mobile widths is the fastest way to prove a refactor changed nothing — every 2.5.0 change was verified at 0 changed pixels.