Skip to main content

Why nested columns render through fw_inner_row / fw_inner_column alias shortcodes

· 3 min read
Jon-Michael Lastimosa
Senior Web Developer

The question: The first time I put a column inside a column, the frontend came out broken — the content truncated and a literal [/column][/row] showed up as text on the page. Why does nesting the same element blow up, and how do we render a column-inside-a-column without WordPress choking?

Context

WordPress' do_shortcode() uses a non-recursive regex: it cannot pair the same shortcode tag nested inside itself. A nested builder tree renders as [column] … [row] … [column] … [/column] … [/row] … [/column] — the tag column (and row) repeats in one branch. The parser binds the outer [column] to the first [/column] it finds (the inner one), truncating the outer column's content and dumping the trailing [/column][/row] onto the page as literal text. That was the exact broken output.

This never bit the builder before because in a normal section → row → column → leaf tree each tag appears once per branch. Nested columns are the first time a tag repeats along a single path.

Options considered

ApproachTrade-off
Distinct alias tags for the nested level (fw_inner_row / fw_inner_column) rendered by the same row/column instancesTiny, surgical; works with the existing shortcode pipeline. The rendered path becomes column → fw_inner_row → fw_inner_column → leaf with no repeated tag, so the parser is happy. Needs a one-line tag swap in the notation generator + a couple of add_shortcode aliases.
Render the whole builder tree to HTML server-side, bypassing shortcodes for nested contentSidesteps the parser entirely, but means re-implementing how builder content renders — a second, parallel render path next to do_shortcode, and every shortcode's view/enqueue/atts decode would need to be reachable from it. Large surface, high drift risk.
Depth-numbered tags (fw_inner_row_1, fw_inner_column_1, _2, …) for arbitrary depthFully general, but registers an unbounded set of tags for a case the editor caps at one level. Complexity with no user-facing payoff.

Decision

Alias tags. fw_inner_row and fw_inner_column are registered with add_shortcode() pointing at the existing row and column instances — FW_Shortcode::render() keys off $this (the instance's own tag, view, options), not the tag WordPress passes in, so an aliased tag renders byte-for-byte like the real one. The page-builder notation generator emits the alias tags for a row synthesized inside a column (and the columns within it); a small normalize_nested_columns() pass covers the correction-disabled single-item render path (the live editor re-rendering one column) so that route can't re-break. We register one alias level, matching the editor's one-level cap.

Why

The alias approach is the smallest change that works with the machinery we already have. The shortcode render pipeline — view resolution, options decode, static enqueue, and the recursive do_shortcode($content) inside each view — all keeps working untouched; we only rename the tag on the way out so WordPress' parser sees a unique tag per nesting path. Rendering the tree to HTML directly would have meant building and maintaining a whole second render path, and depth-numbered tags would add unbounded registration for depth the editor never produces.

Status

Accepted, with a known one-level bound. Because we register a single alias level, a hand-built or imported tree nested two-plus levels deep would re-collide (fw_inner_row repeating along the path). That's out of scope: the editor enforces one nested level, so the case can't arise through normal authoring. If deeper nesting is ever needed, this becomes depth-numbered aliases — a bounded extension of the same idea, not a redesign.