Right, let’s settle this. You’ve probably seen block and define all over the place in your templates and wondered, “Aren’t these the same thing?” I’m here to tell you they are absolutely not, and confusing them is a one-way ticket to a night of head-scratching and template debugging. Think of it this way: define is the architect who draws the blueprints, and block is the contractor who can make on-the-fly modifications when they find a load-bearing wall where the architect promised a skylight.

The define Function: Your Blueprint

define is for declaring a complete, self-contained component. It’s the fundamental unit of reuse. You’re essentially saying, “Here is a named piece of UI, with its own logic and markup, that I can drop anywhere.” It’s your function, your component, your building block.

# Define a reusable "card" component
define "card" {
  param title
  param content

  div class="card" {
    h2 class="card-title" { content = param.title }
    div class="card-content" { content = param.content }
  }
}

# Use it later, as many times as you want
content = define.card(
  title = "My Awesome Post",
  content = "This is the body content of the card."
)

The key here is isolation. What happens inside define "card" stays inside define "card". It doesn’t automatically know about or inherit the context of where you’re calling it from. It’s a clean slate. This is fantastic for predictability but means you can’t just magically reference a variable from the parent scope inside your define block. You have to pass it in explicitly via a param. This is a feature, not a bug—it makes the component’s dependencies clear and prevents spooky action at a distance.

The block Function: The Chameleon

Now, block is a different beast. It doesn’t declare a new component; it references and optionally overrides a part of an existing one. Its entire existence is based on inheritance. You use it inside a define block to create a placeholder, a hook that a parent template can latch onto and replace.

# base.tmpl
define "base_layout" {
  html {
    head {
      title { content = block "title" { content = "Default Title" } }
    }
    body {
      main { content = block "content" }
    }
  }
}

# page.tmpl
template "home" {
  content = define.base_layout(
    # This overrides the "title" block
    title = block "title" { content = "My Homepage - Cool Site" },
    # This overrides the "content" block
    content = block "content" {
      h1 { content = "Welcome Home" }
      p { content = "This is the homepage content." }
    }
  )
}

See what happened? The define "base_layout" sets up the overall structure but uses block to say, “Here are the parts a child template might want to change.” The template "home" then calls that base layout definition and, crucially, provides new implementations for those blocks. The block in the child template is the signal that says, “I’m not just passing in a string here; I’m providing a whole new chunk of template to slot into that placeholder.”

The Critical Difference: Declaration vs. Override

This is the heart of it. You declare a reusable unit with define. You override a placeholder within such a unit using block.

The most common pitfall I see is trying to use block where you should use define. You cannot just slap a block out in the open of your main template and expect it to work. It will be ignored because there’s no parent template for it to override. A block only makes sense inside a define that is intended to be extended.

# WRONG. This block is an orphan and does nothing useful.
block "useless" {
  content = "This is a sad, lonely block."
}

# RIGHT. This block is inside a define, making it a valid override point.
define "my_component" {
  div { content = block "content" }
}

Best Practice: Composition over Inheritance

While the block-based inheritance pattern (like in the base layout example) is classic and works, the modern preference is often for composition. Instead of a deep hierarchy of templates overriding blocks, you compose your UI from smaller, well-defined define components. It’s easier to reason about because data flow is explicit through parameters, not implicit through inherited scope.

Use block for what it’s good for: layout templates and providing sensible defaults that you expect to be overridden. For everything else, lean on define and pass data around explicitly. It feels like more work at first, but your future self, trying to debug why a variable is null at 2 AM, will thank you for the clarity.