Docs Block Developer Guide

Building a New Block — render.php

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_map array must exactly match the map object in index.js rootStyle() — keep them in sync if you ever extend the spacing scale