P
Recherches récentes

The Punky page builder, under the hood

18 vues

Building your own page builder is madness. It's also the best decision we ever made. There are dozens of them already, in every language — so why reinvent the wheel? Because the wheel we wanted didn't exist. Here's how ours works, and what that choice really brings us.

The concept: a page is data

It all starts with a simple idea: a page isn't a file you hand-code, it's data you manipulate. We draw a hard line between the content (what to show, in what order) and the rendering (how to turn it into HTML).

This data is JSON: a tree of blocks. The visual editor only edits that tree — drag a block, change a property, reorder. Nothing more. And because it's data, it's portable, versionable (it lives in Git like everything else) and importable from one site to another. A section we like, we copy-paste it.

The tree: sections → columns → blocks

The tree has three levels, always the same. A section (the full-width band, with its background, padding and grid) contains columns (the 12-column layout), which contain blocks (the content: heading, text, image, slider, accordion, posts…).

page.json
[
  {
    "type": "section",
    "container": "max-w-6xl",
    "columns": [
      {
        "gridColumn": "col-span-6",
        "blocks": [
          { "type": "heading", "level": 2, "content": "Hello" },
          { "type": "text", "content": "<p>A paragraph.</p>" }
        ]
      }
    ]
  }
]

That's it. This regularity is deliberate: three levels, never more. You can nest a group block for advanced cases, but the mental model stays constant — section → column → block. A developer who opens the JSON understands the page structure in ten seconds, no documentation needed.

From JSON to HTML: generating Blade

On save, the builder doesn't just store the JSON: it generates a Blade file on disk. The page served to the visitor is near-static HTML, with no cost of interpreting the JSON on the fly. We pay the transformation cost once, at save time — not on every visit. Fast by construction.

Each block type has its generator — HeadingGenerator, ImageGenerator, SliderGenerator… — whose only job is to turn a JSON node into clean markup. Adding a block type means adding a generator; the rest of the chain doesn't move. The JSON above produces, for example:

generated page (excerpt)
<section id="section-a1b2" class="py-12">
  <div class="container max-w-6xl">
    <div class="col-span-6">
      <h2 class="font-display text-3xl">Hello</h2>
      <p>A paragraph.</p>
    </div>
  </div>
</section>

One practical consequence to know: if you modify a generator, you have to redeploy AND re-save the page in the builder to regenerate the Blade. The HTML on disk doesn't update on its own. It's the price of speed, and we own it.

The tricky part: CSS scoping

The subtlest piece is the per-section custom CSS. If we left the rules as-is, a .ma-classe written in one section would leak onto all the others. So we automatically prefix every selector with the section's id.

scoping
/* What you write in the section panel */
.ma-classe { color: #FF5310; }

/* What gets generated, prefixed by the section id */
#section-xyz .ma-classe { color: #FF5310; }

/* @keyframes stay global; the & is replaced by the block id */
@keyframes pulse { from { opacity: .5 } to { opacity: 1 } }

A few subtleties: the @keyframes are preserved (they must stay global), and the SCSS-style & is handled — replaced by the block id, to write self-referencing rules. These are invisible details, but they're what keep a stylesheet from ever stepping on itself.

Why build it yourself?

That leaves the real question: why all this, when Gutenberg, Elementor, Webflow, Builder.io, GrapesJS, Puck… exist in every language? These tools are excellent, and for most projects, they are the ones to choose. But they all impose the same deal: you adopt their model, their markup, their ecosystem — and you bend to it.

But for us, the builder isn't an add-on: it's the core of the product. It powers every client site and the agency itself. At that scale, three things matter, and no third-party tool gave them to us together: total control of the generated markup (hence performance and accessibility), native integration with the rest of the stack (CodeIgniter, the media library, authentication, multilingual, the sitemap, the Redis cache), and the freedom to evolve — a client need becomes a new block, not a workaround.

CriterionThird-party builderIn-house builder
Generated markupimposed, often verbosecontrolled line by line
Performancevariableoptimized by design
Stack integrationvia plugins / APInative
Product evolutiontheir roadmapours
Upfront cost & maintenancelowhigh
Choose it if…single site, time-to-marketthe builder = core product

And there's the lock-in: a third-party builder is a subscription, a dependency, a roadmap you don't control. The day it raises its prices, changes its licensing model (the recent license U-turns all over the place aren't reassuring) or shuts down, you suffer it. Ours, we answer for.

When NOT to do it

Let's be honest: this isn't universal advice. Writing your own builder is expensive — upfront, and forever, because you have to maintain it for life. For a single site, a blog, a one-shot project, it's absurd: take Gutenberg, Webflow or an open-source GrapesJS / Puck, and move on.

The math only tips toward "in-house" when the builder becomes a strategic asset: you reuse it across dozens of projects, you turn it into a product, you need control nothing else offers. That's our case. For most people, it isn't theirs — and that's perfectly fine.

The lesson: being able to say yes

An in-house builder gives us what no closed solution ever will: control of the markup, of performance, and of the product's evolution. When a client has a specific need, we add a block. We don't work around a limit imposed by a third-party vendor.

That's what technological independence is: being able to say yes.

Partager