Guidance for coding agents working in this repository. CLAUDE.md points here.
Elementify is a classic (PHP-template, not block/FSE) WordPress blog theme, originally based on Underscores. Minimum supported versions are PHP 7.4 and WordPress 6.4, declared in style.css and readme.txt. Do not use PHP 8-only syntax in theme code. PHPCompatibility (testVersion 7.4-) and PHPStan (phpVersion.min 70400) enforce this.
Use Node 24 (nvm use reads .nvmrc; engines in package.json requires ^22.22.2 || >=24.15.0) and pnpm only (pinned via packageManager). Build-script approvals and security overrides live in pnpm-workspace.yaml. Do not create package-lock.json or yarn.lock; both are gitignored.
pnpm install && composer install
pnpm start # watch build
pnpm build # production build -> assets/build (committed to git; CI fails if stale)
pnpm lint # lint:js + lint:css + lint:pkg + lint:php in parallel
pnpm lint:js:fix | lint:css:fix | lint:php:fix
pnpm analyze:php # PHPStan level 5
composer run lint # parallel-lint syntax check + phpcs
composer run lint:syntax # syntax only, no style rules
vendor/bin/phpcs inc/classes/class-assets.php # PHPCS on one file
pnpm env:start / env:stop # wp-env (Docker): dev site :8888, tests site :8889, admin/password
pnpm test:e2e # Playwright against the wp-env *tests* site (:8889)
pnpm test:e2e -- tests/e2e/specs/admin.spec.js -g "dashboard" # single spec / single test
pnpm i18n:pot # regenerate languages/elementify.pot (needs WP-CLI)
pnpm release # clean build + make-pot + wp-scripts plugin-zip -> elementify.zipRunning the whole suite will not come back clean. Current baseline on main:
| Command | State |
|---|---|
pnpm run lint:js |
passes |
pnpm run lint:pkg |
passes |
pnpm run analyze:php |
passes (baseline absorbs pre-existing issues) |
composer run lint:syntax |
passes |
pnpm run lint:css |
passes (no-descending-specificity is disabled in .stylelintrc.json; reordering selectors in main.css would change the cascade) |
vendor/bin/phpcs |
fails, 407 errors / 149 warnings across 43 files. Only 38 auto-fixable |
So: lint the files you touched rather than the repo, and do not treat these pre-existing counts as regressions you introduced.
Note that CI runs vendor/bin/phpcs -q --report=checkstyle | cs2pr without pipefail, so the PHP job does not actually fail on PHPCS violations, while the JS job's pnpm run lint:css does fail, so keep CSS lint clean.
- On macOS 13 and older, Playwright ships no Chromium build. Run E2E with
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 PLAYWRIGHT_CHANNEL=chrome pnpm test:e2e.playwright.config.jsforwardsPLAYWRIGHT_CHANNELinto every project. playwright.config.jssetswebServer.commandtopnpm run env:start, so Playwright boots wp-env itself. It needs Docker running.- wp-env's
afterStartactivates the theme on both sites and installs the Theme Check plugin. Tests log in via REST using@wordpress/e2e-test-utils-playwright(admin,requestUtilsfixtures); specs intests/e2e/specs/create their own menus, posts and pages viarequestUtils. - PHPStan uses
phpstan-baseline.neonfor pre-existing issues andtests/phpstan/bootstrap.phpto define theELEMENTIFY_*constants. When you fix a baselined issue, regenerate withvendor/bin/phpstan analyse --memory-limit=2G --generate-baseline phpstan-baseline.neon. phpcs.xmlandphpstan.neonare gitignored local overrides of the.distfiles. Edit the.distfiles.
webpack.config.js extends the @wordpress/scripts default config and overrides entries and output. Entries are js/main, js/customizer, css/main and css/editor; output path is assets/build with clean: true. Each entry gets a sibling *.asset.php manifest (dependencies and content hash), and CSS entries also get a -rtl.css. Images in assets/src/images are copied verbatim. RemoveEmptyScriptsPlugin drops the empty js/main.js webpack emits for the CSS-only entries.
There is no Sass and no Tailwind (Tailwind was deliberately removed). assets/src/css/main.css is about 12k lines of hand-written CSS using --ele-* custom properties and 3,386 ele- prefixed class references. Utility class names in templates only work if main.css defines them.
theme.json (version 2, because the theme supports WordPress 6.4) drives editor settings and global styles. Its palette, layout widths and shadow are var(--ele-…, fallback) references to the :root tokens in main.css, so the light/dark [data-theme] switch also recolours block presets. Core button and link element styles are set to false, as Kadence and Blocksy do, so main.css controls them. Inside .entry-content, main.css resets --wp--style--global--content-size to 100%, because the post column next to the sidebar is already narrower than the 750px contentSize.
The block editor loads assets/build/css/main.css plus assets/build/css/editor.css via add_editor_style(). editor.css mirrors the few main.css rules scoped to front-end-only wrappers (the .ele-button-fill body class, .entry-content). Each mirror block cites the main.css rule it copies; when you change one of those rules, update its mirror.
Dark mode is server-driven: elementify_html_attributes() (inc/helpers/functions.php:652) sets data-theme="dark" when the darkMode cookie exists, else light. main.css keys everything off [data-theme="dark"] / [data-theme="light"].
postcss.config.js and babel.config.js exist at the project root, so wp-scripts uses them instead of its built-in defaults. postcss.config.js therefore has to add cssnano itself for production builds.
- Bootstrap:
functions.phpdefinesELEMENTIFY_*constants and requiresinc/helpers/{autoloader,template-functions,template-tags,functions}.php. It then callselementify_get_theme_instance(), which instantiatesElementify\Inc\Elementify(inc/classes/class-elementify.php). That class registers theme supports and boots the singleton services:Assets,Utils,Customizer,MenusandSidebars. - Autoloading is a custom PSR-like loader (
inc/helpers/autoloader.php), not Composer:Elementify\Inc\Foo_Barmaps toinc/classes/class-foo-bar.php.Elementify\Inc\Traits\Xmaps toinc/traits/trait-x.php.Inc\Widgets\XandInc\Blocks\Xmap toinc/classes/{widgets,blocks}/class-x.php. Neither directory exists yet; the switch case falls through toclasses/when the segment is missing.- Service classes
use Elementify\Inc\Traits\Singletonand are obtained with::get_instance().
- Templates are hook-driven. Root templates (
index.php,archive.php,header.php, etc.) contain little markup. They fire namespaced actions such aselementify_before_content,elementify_content_top,elementify_headerandelementify_content_before_loop. The callbacks that render markup are registered at the bottom ofinc/helpers/template-functions.php(around lines 65-608), often by includingtemplate-parts/.... To change what a page outputs, find the hook callback there; the template file itself usually won't have it. Eachdo_actiondocblock lists its@hookedcallbacks and priorities. Keep those comments in sync. - Template parts:
template-parts/content-*.phpare loop entries.template-parts/components/holds shared pieces (entry header, meta, image, title, read-more, cats, tags, footer);template-parts/components/blog/holds the card variants. Alsotemplate-parts/header/nav.phpandtemplate-parts/footer/copyright.php. - Helper layers:
inc/helpers/functions.php: site identity, nav menus, SVG icon accessors, pagination, breadcrumbs, html attributes.inc/helpers/template-tags.php: thumbnails, posted-on/by, excerpts, entry footer.Utils:clsxclass-name builder and attribute-string helpers.Svg_Icons: an inline SVG icon registry.Breadcrumb_Trail: a vendored breadcrumb library, and the source of most baseline entries.
- Hard-coded settings: components contain "settings" arrays such as
$header_preset = ['desktop' => '1'](template-parts/components/entry-header.php:11). These are placeholders for future Customizer options, which is why the surroundingin_array/array_key_existsguards read as dead code and produce "always true/false" PHPStan findings. - Assets:
Assetsenqueuesassets/build/...using literalELEMENTIFY_DIR_URI . '/assets/build/...'paths, reading version and dependencies throughget_asset_meta()from the*.asset.phpmanifests (it falls back to the theme version if the manifest is missing).main.jsloads deferred and reads translated submenu toggle labels fromwindow.elementifyMenu, injected as an inline script before it. The customizer preview script depends oncustomize-previewandjquery. - Naming: text domain
elementify. Global prefixes areelementify_(functions),ELEMENTIFY_(constants) andElementify(classes), enforced by PHPCSPrefixAllGlobals. Some legacy functions break this and are a large share of the current PHPCS errors:bizness_get_nav_menus(inc/helpers/functions.php:27),twenty_twenty_one_get_social_link_svgandtwenty_twenty_one_nav_menu_social_icons(inc/helpers/functions.php:505,:520), and the misspelledelemetify_pagination(inc/helpers/functions.php:363, called fromtemplate-functions.php:323).wpml-config.xmllists the translatabletheme_mods_elementifykeys; update it when adding text Customizer settings.
- The
ELEMENTIFY_BUILD_*andELEMENTIFY_IMG_URIconstants infunctions.phppoint at/build, but the real output is/assets/build. Nothing in the theme reads them (Assetshard-codes the correct path), so they are dead and misleading. The PHPStan bootstrap mirrors the same wrong values. Renaming them toELEMENTIFY_ASSETS_BUILD_*and pointing them at/assets/buildwould be the consistent fix. readme.txtstill carries Underscores boilerplate: "A starter theme called Elementify.",Contributors: automattic, and a 2015 changelog. Worth cleaning before a WordPress.org submission.style.csshasAuthor: Elementify Themesand ascreenshot.pngof 264 bytes. Both matter for Theme Check.- The theme claims WooCommerce and Jetpack Infinite Scroll support in
readme.txt, but no such support is registered inclass-elementify.php.
- Repo-local skills exist in
.opencode/skills/:commit-msg(conventional commits from the staged diff),changelog(datedCHANGELOG.mdentries; noCHANGELOG.mdexists yet),php-doc-comments,wpcs. Invoke them by name rather than improvising. - Conventional Commit types:
feat,fix,refactor,chore,docs,style,test. Historical subjects are inconsistent (fixes,sytle fixes), so follow the skill's format for new commits. .gitignoreexcludes/assets/build/**/*.map, sopnpm startleaves no untracked files. A productionpnpm builddoes change committed files; CI'sgit diff --exit-code -- assets/buildcatches a stale build, so runpnpm buildbefore pushing CSS or JS changes.- The release zip contents come from the
filesfield inpackage.json(*.php,style.css,theme.json,screenshot.png,readme.txt,wpml-config.xml,assets/build,inc,languages,template-parts), not from an ignore list. Anything new the theme needs at runtime must be added there. - Dependabot runs weekly and groups
@wordpress/*separately from other npm packages, and all Composer dev tools into one group. Expect grouped PRs.