render.php runs on the server for every page request that includes this block. WordPress passes the saved attribute values in $attributes. The file outputs the final escaped HTML. It never runs in the editor — that is index.js‘s job.
render.php Template
<?php
defined( 'ABSPATH' ) || exit;
// ── Panel Registry utility classes ─────────────────────────────────────
// wptruss_resolve_classes() reads blockPadding, blockGap, textAlign,
// hideOnMobile, hideOnTablet, hideOnDesktop and returns the corresponding
// utility class string. Always guard with function_exists().
$utility_classes = function_exists( 'wptruss_resolve_classes' )
? wptruss_resolve_classes( $attributes )
: '';
// ── Block modifier classes ──────────────────────────────────────────────
$theme_class = 'wpt-feature-card--theme-' . sanitize_html_class( $attributes['themeMode'] ?? 'light' );
$bg_class = 'wpt-feature-card--bg-' . sanitize_html_class( $attributes['backgroundColor'] ?? 'light' );
$block_classes = implode( ' ', array_filter([
'wpt-feature-card',
$theme_class,
$bg_class,
]));
// ── Spacing via CSS custom properties ───────────────────────────────────
// Spacing values are passed as inline CSS custom properties, not modifier
// classes. style.css reads these on the block root.
// This map must match the map in index.js rootStyle() exactly.
$spacing_map = [
'none' => '0',
'xs' => 'var(--wpt-spacing-xs)',
'sm' => 'var(--wpt-spacing-sm)',
'md' => 'var(--wpt-spacing-md)',
'lg' => 'var(--wpt-spacing-lg)',
'xl' => 'var(--wpt-spacing-xl)',
'2xl' => 'var(--wpt-spacing-2xl)',
'3xl' => 'var(--wpt-spacing-3xl)',
'4xl' => 'var(--wpt-spacing-4xl)',
];
$padding = $attributes['blockPadding'] ?? '3xl';
$gap = $attributes['blockGap'] ?? 'xl';
$inline_style = implode( '; ', array_filter([
isset( $spacing_map[$padding] ) ? '--wpt-feature-card-padding:' . $spacing_map[$padding] : '',
isset( $spacing_map[$gap] ) ? '--wpt-feature-card-gap:' . $spacing_map[$gap] : '',
]));
// ── Tag resolution ──────────────────────────────────────────────────────
$heading_tag = esc_attr( $attributes['headingLevel'] ?? 'h3' );
$semantic_tag = esc_attr( $attributes['semanticRole'] ?? 'article' );
?>
<<?php echo $semantic_tag; ?>
<?php echo get_block_wrapper_attributes([
'class' => $block_classes . ' ' . $utility_classes,
'style' => $inline_style,
]); ?>
>
<<?php echo $heading_tag; ?> class="wpt-feature-card__heading">
<?php echo wp_kses_post( $attributes['heading'] ?? '' ); ?>
</<?php echo $heading_tag; ?>>
<p class="wpt-feature-card__description">
<?php echo wp_kses_post( $attributes['description'] ?? '' ); ?>
</p>
</<?php echo $semantic_tag; ?>>
render.php rules:
defined( 'ABSPATH' ) || exit;must be the first executable line- Always call
getblockwrapper_attributes()— this injects block gap, data attributes, and CSS classes that Gutenberg expects. Never output the wrapper tag without it. - All output is escaped:
escattr(),eschtml(),escurl(),wpkses_post()for rich text fields - All user-supplied input is sanitised before use
- Spacing travels via CSS custom properties set as inline styles, not as modifier classes
- The
$spacing_maparray must exactly match themapobject inindex.jsrootStyle()— keep them in sync if you ever extend the spacing scale