Run through this checklist before zipping and uploading any block. Every item here maps to a validator rule, a production failure, or a common editor crash.
File Structure
- [ ] Block folder is directly inside
custom-blocks/with no subfolders - [ ] Block folder name is lowercase with hyphens only (e.g.
feature-card, notFeatureCard) - [ ] Folder contains exactly:
block.json,index.js,index.asset.php,style.css,render.php - [ ] ZIP file has the block folder at root, not a parent folder containing the block folder
- [ ] All files saved as UTF-8 without BOM
block.json
- [ ]
nameis innamespace/slugformat (e.g.wptruss/feature-card) - [ ]
apiVersionis3 - [ ]
categoryis"wptruss" - [ ]
editorScriptis"file:./index.js" - [ ]
renderis"file:./render.php" - [ ]
wptPanelskey is present with at least"structure": true - [ ]
headingLevelattribute declared with"type": "string"and a valid heading default - [ ]
semanticRoleattribute declared with"type": "string"and a valid tag default - [ ]
themeModeandbackgroundColorattributes declared - [ ]
elementOverridesattribute declared as"type": "object", "default": {} - [ ] All spacing attributes declared:
blockPadding,blockGap,spacingBottom - [ ] All layout attributes declared:
hideOnMobile,hideOnTablet,hideOnDesktop,textAlign
index.js
- [ ] All code inside a single IIFE:
( function () { 'use strict'; ... }() ); - [ ] No
import, noexport, noexport default, no JSX - [ ]
save()returnsnull - [ ] All conditional renders use
condition ? el() : null, nevercondition && el() - [ ] All array attributes guarded with
Array.isArray()before.map() - [ ] No dynamic strings inside
__(), static literals only - [ ]
window.wptElementscalls are null-guarded:window.wptElements ? window.wptElements.buildElementClasses(...) : '' - [ ]
window.wptPanelRegistryfalls back to{}:var REGISTRY = window.wptPanelRegistry || {}; - [ ] Canvas heading uses element classes from
buildElementClasses() - [ ] Canvas heading has placeholder text for fresh block inserts
- [ ]
TextareaControlused for multi-line text, not rawel('textarea', ...) - [ ] Repeater panels extracted to named functions
- [ ] No
.innerHTML,.outerHTML,eval(),fetch(), orXMLHttpRequest
index.asset.php
- [ ] Returns array with
dependenciesandversionkeys - [ ]
wp-blocks,wp-element,wp-block-editor,wp-components,wp-i18nincluded - [ ]
wpt-element-registryincluded if using any element - [ ] One
wpt-element-{name}entry per element the block uses - [ ]
wp-primitivesis NOT in the dependencies array - [ ] No other unregistered or unknown handles
render.php
- [ ]
defined( 'ABSPATH' ) || exit;is the first line - [ ]
getblockwrapper_attributes()is called - [ ] All output escaped:
escattr(),eschtml(),escurl(),wpkses_post() - [ ] Spacing goes via CSS custom properties as inline styles (not modifier classes)
- [ ] Semantic wrapper tag uses
$attributes['semanticRole'] - [ ] Heading tag uses
$attributes['headingLevel']
style.css
- [ ] No hardcoded hex colours, use
var(--wpt-*)tokens - [ ] Hex values only inside
var()as CSS fallbacks - [ ] No
transition:ortransition-*properties - [ ] No
animation:oranimation-*properties - [ ] No
@keyframes - [ ] No
@import - [ ] Theme modifier classes present:
--theme-light,--theme-dark - [ ] Background modifier classes present:
--bg-light,--bg-surface,--bg-primary,--bg-secondary,--bg-dark - [ ] Heading inversion rules for coloured backgrounds
- [ ] Responsive visibility classes:
--hide-mobile,--hide-tablet,--hide-desktop - [ ] Root element consumes block-local CSS custom properties for padding and gap