The Great PHP Tag Heist — How GrapesJS Ate My Code and How I Fought Back

February 1, 2026 Unknown 6 min read
The Great PHP Tag Heist — How GrapesJS Ate My Code and How I Fought Back

I spent the better part of a weekend chasing a bug that turned out to be something I'd never have guessed. My blog blocks — the nice little draggable components for "Recent Posts", "Categories", and "Sidebar" in my MiniPress page builder — were showing up completely empty. No data. No errors. Just... nothing. This is the story of how I went in circles, almost lost my mind to PHP's `?>` close tag, and eventually found a clean solution by completely abandoning my original approach. --- ## The Setup I'm building MiniPress, a lightweight CMS for my English school website. It has a visual page builder powered by GrapesJS — a drag-and-drop editor where you can build pages by dragging blocks onto a canvas. I'd created blog-specific blocks that were supposed to pull in real data: recent posts, categories, a sidebar with search. All the good stuff. The blocks were built. The database had posts and categories in it. I could verify the data was there with direct SQL queries. But when I loaded the page, the blog sections were completely blank. --- ## Finding the Crime Scene The first clue came when I dumped the raw content stored in the database after GrapesJS had saved a page. What I expected to see: ```html

``` What was actually stored: ```html

<?= htmlspecialchars($post['title']) ?-->

``` GrapesJS had **completely mangled every PHP tag in my blocks**. Opening tags `

` became `?-->`. Inline expressions like `

` got HTML-entity-encoded into `<?= $var ?>`. The editor was treating my server-side code like it was dangerous HTML and neutralising it. And it made sense — GrapesJS is a *client-side* HTML editor. It has no concept of PHP. It saw `

/s', function($m) { return '

'; }, $htmlContent); ob_start(); eval('?>' . $htmlContent); echo ob_get_clean(); ``` The problem showed up almost immediately. The closing tag pattern was wrong — the actual mangled output used `?-->` not `-->`. So I adjusted the regex. That seemed to fix the pattern matching, but then `eval()` started dying. --- ## Round Two: The `?>` Problem Here's where it got maddening. PHP has a rule that is absolute, no exceptions, no workarounds: **`?>` always closes a PHP block**. Always. Even if it's inside a string. Even if it's inside a comment. Even inside a regex pattern. So when I wrote code like this: ```php // Fix closing tags: ?--> becomes ?> $html = preg_replace('/\?-->/', '?>', $html); ``` That `?>` inside my regex string **closed my PHP block right there**. The rest of my code became raw HTML output, and everything broke. I tried splitting it up: ```php $closeTag = '?' . '>'; $html = str_replace('?-->', $closeTag, $html); ``` That worked for *that* line. But then the next problem: my regex patterns contained comments explaining what they did, and those comments had `?>` in them too. Like: ```php // Pattern converts ?--> back to ?> for PHP execution ``` That comment's `?>` closed the block. I removed the comment. New problem — the eval'd content itself contained `?>` in ways that broke the block boundaries mid-execution. I was playing whack-a-mole with a language feature that had zero exceptions. Every fix introduced another `?>` somewhere that killed everything. --- ## Round Three: Going In Circles At this point I'd spent hours on variations of the same approach. Each attempt: 1. Fix the regex pattern → breaks eval 2. Fix eval → regex mismatches something else 3. Fix that regex → introduces a new `?>` somewhere 4. Repeat I was fixing one thing only to have it reappear somewhere else. The circular nature of it was genuinely frustrating. I'd think I had it, test it, and find a new failure mode. The problem was that I was trying to make a fundamentally broken approach work — taking mangled code, un-mangling it, then executing it dynamically. Every step in that chain had failure points, and they kept compounding. --- ## The Breakthrough: Stop Fighting the Editor I took a step back and asked a different question. Instead of *"How do I fix the mangled PHP?"*, I asked: **"What can GrapesJS NOT mangle?"** The answer: plain HTML. Specifically, HTML elements with custom attributes. GrapesJS saves those exactly as-is because they're valid HTML. So instead of putting PHP code in my blocks (which GrapesJS would destroy), I put in **placeholder divs**: ```html

[Recent Blog Posts]

``` GrapesJS sees this as a normal styled div. It saves it perfectly. The editor even shows it as a visible blue dashed box so you know something's there. Then on the server side, *before* the page is sent to the browser, a simple function swaps each placeholder for real rendered content: ```php function mp_render_blog_blocks($html) { return preg_replace_callback( '/]*data-blog-block="([\w-]+)"[^>]*>.*?<\/div>/s', function($matches) { ob_start(); mp_render_blog_block($matches[1]); return ob_get_clean(); }, $html ); } ``` That's it. No eval. No un-mangling. No fighting with `?>`. The regex matches the placeholder div, calls the appropriate render function, and swaps in the real HTML with actual database queries. --- ## The render.php Architecture The solution ended up being beautifully simple once I stopped trying to force the wrong approach: **blocks.php** — Defines what gets dragged into the editor. Each block contains a visible placeholder div with a `data-blog-block` attribute. GrapesJS handles these perfectly. **render.php** — Contains the actual rendering functions. `mp_render_recent_posts()` queries the database and outputs a post grid. `mp_render_sidebar_full()` outputs search, categories, and recent posts. Each function is a clean, standalone piece of PHP that just echoes HTML. **site/index.php** — Three lines added before output: ```php if (function_exists('mp_render_blog_blocks')) { $htmlContent = mp_render_blog_blocks($htmlContent); } ``` The separation is clean: the editor handles *structure* (where blocks go, how the layout looks), and the server handles *data* (what actually gets displayed). They never need to talk to each other except through the simple `data-blog-block` attribute. --- ## What I Learned **Don't fight the tools — work with their constraints.** GrapesJS is a client-side HTML editor. It will always mangle server-side code. Trying to undo that mangling after the fact is a losing battle. The right move was to accept that constraint and design around it. **Eval is a code smell, not a solution.** My first instinct was to dynamically execute PHP, which meant I needed to handle every edge case in how PHP parses code. That's a rabbit hole. The marker-based approach completely avoids it. **When you're going in circles, the problem is usually your approach, not your implementation.** I spent hours perfecting regex patterns and eval workarounds. The moment I questioned the fundamental approach — "should I even be putting PHP in these blocks?" — the solution became obvious. **Visible placeholders beat invisible markers.** I initially tried using HTML comments (`

`) as markers. They worked on the backend but showed as empty invisible space in the editor. Switching to styled placeholder divs meant the editor showed exactly what was there, making it much easier to work with. --- The whole ordeal probably took four or five hours of actual debugging. The final solution — blocks.php, render.php, and three lines in index.php — took about twenty minutes to write once I had the right idea. Sometimes the longest part of solving a problem is realising you're solving the wrong one.