Key takeaways

  • Blade’s {{ $var }} is the direct equivalent of typing <?php echo esc_html($var); ?> by hand, confirmed against Laravel’s own Blade documentation: it runs the value through PHP’s htmlspecialchars automatically. {!! $var !!} is the unescaped version, the Blade equivalent of a bare <?php echo $var; ?>.
  • @if/@endif, @foreach/@endforeach and the rest of Blade’s control-flow directives compile into the identical plain-PHP control structures you already write. They are not a new language layered on top of PHP; they are PHP with the opening and closing tags filed off.
  • @extends('layouts.app') plus @section/@yield replaces the get_header() / get_footer() pattern with real template inheritance: one parent layout file, many child views that each fill in a named section, verified against real resources/views/layouts/app.blade.php and page.blade.php files in Brndle’s public repository.
  • WordPress’s globals, $post, $wp_query, have_posts(), the_content(), work completely unchanged inside a Blade view, because Blade compiles to plain PHP that runs in the exact same request. Acorn’s View Composers and a block’s render_callback exist to hand a view additional, named, structured data, settings, block attributes, not to replace the WordPress Loop.
  • Every syntax claim in this guide is checked against Laravel’s own Blade documentation at laravel.com/docs, and every Brndle-specific example is pulled verbatim from real .blade.php files in the brndle-themes/brndle GitHub repository, not written from memory.

You’ve built dozens of WordPress themes. You know header.php and footer.php by heart, you can write a WP_Query loop half asleep, and the_content() is as automatic to you as a semicolon. Then you open a Brndle project, go looking for single.php, and find resources/views/single.blade.php instead, a file that starts with @extends('layouts.app') and has no opening <?php tag anywhere near the top. Somewhere below that, a variable called $singleLayout gets used without ever being defined in the file you’re looking at, and a few lines later @while(have_posts()) looks almost right but not quite like anything you’ve typed before.

This is the single biggest thing that stalls a WordPress developer’s first week with Blade, and it isn’t Tailwind’s utility classes, it isn’t the Composer/npm build step we covered in setting up Brndle locally, and it isn’t even the view cache we wrote up separately in why your Blade edits aren’t showing up. It’s the silent gap between “this file looks like PHP” and “I actually know what every line in it does and where its data came from.” That gap doesn’t throw an error. It just leaves you staring at a working template unable to explain it, which is a worse position than a stack trace, because nothing is telling you where to look.

This post closes that specific gap, directly, pattern by pattern. For every classic WordPress template habit, outputting a variable, writing a conditional, looping over posts, pulling in a sub-template, wrapping a page in a shared header and footer, we show the exact Blade equivalent, verified against Laravel’s own Blade documentation so the syntax claims are accurate rather than approximate. Then we go one level deeper than syntax: how does WordPress’s data, $post, a WP_Query result, a block’s saved attributes, actually arrive inside a Blade view in the first place, since nothing in the file ever seems to receive it as a function argument? We answer that with real code pulled directly from Brndle’s own public source, not an invented example standing in for one.

In this guide

The translation table: classic PHP pattern to Blade equivalent

Before the detail, the map. Every row below is expanded with real code in its own section further down the page, but if you only needed the lookup table, this is it.

Classic WordPress PHP templateBlade equivalent
<?php echo esc_html($title); ?>{{ $title }}
<?php echo $trusted_html; ?>{!! $trusted_html !!}
<?php if ($x): ?> ... <?php endif; ?>@if ($x) ... @endif
<?php foreach ($items as $item): ?> ... <?php endforeach; ?>@foreach ($items as $item) ... @endforeach
<?php while (have_posts()): the_post(); ?> ... <?php endwhile; ?>@while (have_posts()) @php(the_post()) ... @endwhile
get_template_part('template-parts/content', 'page')@include('partials.content')
get_header(); ... get_footer(); (repeated in every template file)@extends('layouts.app') @section('content') ... @endsection (one shared layout)
A reusable markup chunk with required inputs, usually copy-pasted or built with a function that echos HTMLA Blade component: <x-img :src="$url" :alt="$alt" />
<!-- a developer-only note, stripped by nothing -->{{-- never reaches the rendered HTML at all --}}

Outputting a variable: echo and esc_html() vs {{ }}

In a classic template, printing a dynamic value correctly means remembering to escape it yourself, and remembering which escaping function fits the context:

<h1><?php echo esc_html( get_the_title() ); ?></h1>
<a href="<?php echo esc_url( $profile_link ); ?>">Profile</a>

Blade’s double-curly-brace echo, {{ }}, does the HTML-context half of that automatically. Per Laravel’s own Blade documentation: “Blade’s {{ }} echo statements are automatically sent through PHP’s htmlspecialchars function to prevent XSS attacks.” That’s the real mechanism, not a paraphrase of it, confirmed directly against laravel.com/docs. The same line becomes:

<h1>{{ get_the_title() }}</h1>

Two honest caveats worth stating plainly rather than glossing over. First, {{ }} wraps the expression in htmlspecialchars, which is the right tool for text rendered inside an HTML body, but it is not a universal replacement for every WordPress escaping function. esc_url() still does URL-specific escaping that htmlspecialchars doesn’t, so an attribute value built from a raw URL is still worth passing through esc_url() before it reaches {{ }}, the same way you would in classic PHP; Blade’s auto-escaping and WordPress’s context-specific escaping functions are complementary, not interchangeable. Second, Blade’s echo syntax works on any PHP expression, not just a bare variable, exactly like a classic echo statement: {{ get_the_title() }}, {{ strtoupper($name) }}, and {{ $count + 1 }} are all valid, because {{ }} compiles to ordinary PHP wrapping ordinary PHP.

Outputting trusted HTML: a bare echo vs {!! !!}

Sometimes the value you’re printing is already HTML you trust, post content that’s already gone through WordPress’s content filters, a WP_Query pagination widget’s generated markup, a shortcode’s rendered output, and escaping it with htmlspecialchars would mangle it into visible entity codes instead of rendering it. In classic PHP that’s just a bare echo, deliberately skipping esc_html():

<?php echo get_search_form( false ); ?>

Blade’s equivalent is the triple-brace-adjacent {!! !!} syntax. Straight from Laravel’s docs: “If you do not want your data to be escaped, you may use the following syntax: {!! $name !!}.” Same example, in Blade:

{!! get_search_form(false) !!}

This is real, working Brndle code, not a constructed example. Here’s the exact pattern from the theme’s own resources/views/index.blade.php, handling the no-results state on the blog archive:

@if(! have_posts())
  <div class="py-12 text-center">
    <p class="text-lg text-text-secondary">{{ __('No posts found.', 'brndle') }}</p>
    {!! get_search_form(false) !!}
  </div>
@else

Notice both syntaxes sitting two lines apart, doing different jobs on purpose: {{ __('No posts found.', 'brndle') }} escapes a translated string because it’s plain text being placed inside a paragraph, while {!! get_search_form(false) !!} stays unescaped because get_search_form() already returns a complete <form> element that needs to render as actual markup. The rule carries over unchanged from classic PHP: use the escaped form by default, and reach for the unescaped form only when the source is a function you trust to have already produced safe HTML, never for raw user input.

Conditionals: if/endif vs @if/@endif

This is the smallest conceptual leap in the whole post, which is exactly why it’s worth stating without hedging: Blade’s conditional directives “function identically to their PHP counterparts,” per Laravel’s documentation, word for word. A classic template mixing HTML and a conditional usually looks like this, using the alternative PHP syntax to avoid a wall of braces:

<?php if ( count( $records ) === 1 ) : ?>
    <p>I have one record!</p>
<?php elseif ( count( $records ) > 1 ) : ?>
    <p>I have multiple records!</p>
<?php else : ?>
    <p>I don't have any records!</p>
<?php endif; ?>

The Blade version, taken directly from Laravel’s own docs for @if/@elseif/@else/@endif, is the same logic with the PHP tags removed and @-prefixed keywords in their place:

@if (count($records) === 1)
    I have one record!
@elseif (count($records) > 1)
    I have multiple records!
@else
    I don't have any records!
@endif

What makes this genuinely easier than the PHP alternative syntax it replaces, not just shorter to type, is that there’s no ?> and <?php pair bracketing every single branch. Here’s real Brndle code showing the same directive doing real work, from resources/views/blocks/team.blade.php, a block view we’ll come back to in the worked example further down:

@if($a['title'])
  <div class="max-w-2xl mx-auto text-center mb-16 reveal">
    @if($a['eyebrow'])
      <p class="text-sm font-semibold text-accent uppercase tracking-[0.15em] mb-3">{{ $a['eyebrow'] }}</p>
    @endif
    <h2 class="text-4xl sm:text-5xl font-bold tracking-tight">{!! wp_kses_post($a['title']) !!}</h2>
  </div>
@endif

Also worth knowing because it has no classic-PHP equivalent people reach for by habit: Blade adds an @unless directive, the inverse of @if, confirmed in the docs as shorthand for @if (! (...)). Brndle’s own layouts/app.blade.php uses it to skip rendering the site header entirely when a setting turns it off: @unless($hideHeader ?? false) @include('sections.header') @endunless. There’s no deep magic there, it’s exactly as readable as it looks, but it’s a directive classic WordPress theming never gave you a dedicated shorthand for.

Looping over an array: foreach vs @foreach

Same story as conditionals: Blade’s loop directives are described in Laravel’s docs as functioning “identically to their PHP counterparts.” A classic foreach over, say, an array of team members pulled from an ACF repeater or a block attribute:

<?php foreach ( $members as $member ) : ?>
    <h3><?php echo esc_html( $member['name'] ); ?></h3>
<?php endforeach; ?>

In Blade, straight from the docs’ own example: @foreach ($users as $user) <p>This is user {{ $user->id }}</p> @endforeach. Here’s the same directive from real Brndle code, iterating a block’s team-member array in blocks/team.blade.php:

@foreach($members as $i => $member)
  <div class="reveal group">
    <h3 class="text-lg font-bold">{{ $member['name'] ?? '' }}</h3>
    @if(!empty($member['role']))
      <p class="text-sm text-accent font-medium mt-0.5">{{ $member['role'] }}</p>
    @endif
  </div>
@endforeach

One genuinely useful feature here with no classic-PHP equivalent most WordPress developers build by hand: Blade automatically exposes a $loop variable inside every @foreach, with properties like $loop->first, $loop->last, $loop->index, and $loop->iteration, confirmed directly in Laravel’s docs. In classic PHP you’d track that yourself with a manual counter ($i === 0, $i === count($array) - 1); in Blade it’s already sitting there as $loop->first and $loop->last inside any @foreach block, no setup required.

The WordPress Loop specifically: have_posts() in Blade

This is the pattern that trips people up hardest, specifically because it looks like it should be a @foreach and isn’t. WordPress’s own Loop isn’t a foreach over an array at all, it’s a while loop driven by two function calls, have_posts() as the condition and the_post() advancing an internal pointer on $wp_query. Classic PHP:

<?php while ( have_posts() ) : the_post(); ?>
    <article>
        <h2><?php the_title(); ?></h2>
        <?php the_content(); ?>
    </article>
<?php endwhile; ?>

Blade has a @while/@endwhile pair for exactly this, functioning identically to PHP’s own while, per the docs. The one wrinkle is the_post(): it’s a function call with no echoed output, a side-effecting statement rather than something you’d ever wrap in {{ }} or {!! !!}. Blade’s @php(...) directive, documented as a one-line form of the full @php ... @endphp block, is built for exactly this: running a single PHP statement for its side effect, not its return value. This is the real code, verbatim, from Brndle’s own resources/views/page.blade.php:

@extends('layouts.app')

@section('content')
  @while(have_posts())
    @php(the_post())
    <article @php(post_class())>
      <div class="max-w-4xl mx-auto px-6 pt-20 pb-24">
        <h1 class="text-4xl sm:text-5xl font-bold tracking-tight text-text-primary [text-wrap:balance]">
          {!! get_the_title() !!}
        </h1>
        <div class="mt-10 prose prose-lg max-w-none">
          @php(the_content())
        </div>
      </div>
    </article>
  @endwhile
@endsection

Read that against your own muscle memory and the mapping is exact: while (have_posts()): the_post(); becomes @while(have_posts()) @php(the_post()), the_content(); becomes @php(the_content()), and post_class(), a function that normally echoes a space-separated class list directly into an attribute, gets wrapped the same way inside the tag: @php(post_class()). Nothing about the WordPress Loop’s actual mechanics changed. The only thing that changed is the punctuation wrapping each statement.

Sub-templates: get_template_part() vs @include

Breaking a template into reusable pieces is standard practice in both worlds; only the lookup syntax changes. Classic WordPress:

<?php get_template_part( 'template-parts/content', 'page' ); ?>

Blade’s @include directive does the equivalent job. Per Laravel’s docs: “Blade’s @include directive allows you to include a Blade view from within another view. All variables that are available to the parent view will be made available to the included view.” That inherited-variables behavior is worth sitting with for a second, because it’s the same mental model as a classic get_template_part() call, which also runs inside the calling scope’s variables rather than an isolated one. Real Brndle code, from resources/views/index.blade.php:

@unless ($useHomepageSections)
  @include('partials.page-header')

  @if($archiveShowCategoryFilter)
    @include('partials.components.category-filter')
  @endif
@endunless

Two syntax details worth flagging explicitly because they’re exactly the kind of thing that produces a silent “view not found” error for a first-timer. First, Blade view names use dot notation for path separators, not slashes: partials.components.category-filter refers to the real file at resources/views/partials/components/category-filter.blade.php, and you never write the .blade.php extension or a leading slash in the directive itself. Second, you can pass extra data into an included view the same way you’d pass an array to get_template_part‘s hypothetical third argument, except @include‘s actually works: @include('view.name', ['status' => 'complete']), confirmed directly in the docs.

The header/footer wrapper: get_header()/get_footer() vs @extends

This is the pattern with the biggest structural payoff, and also the one that looks the least like anything in classic WordPress theming at first glance. Every classic template file repeats the same bookend:

<?php get_header(); ?>

<main>
    <?php /* page-specific content here */ ?>
</main>

<?php get_footer(); ?>

That’s not inheritance, it’s duplication: every one of page.php, single.php, archive.php, and 404.php independently calls get_header() and get_footer(), and if you need to change what sits between the header and the <main> tag site-wide, you’re editing (or hoping a hook catches) every one of those files. Blade’s layout inheritance, built on @extends, @section, and @yield, inverts that relationship: one parent layout owns the full page shell, and every child view just declares what goes into the slots the parent defines. Laravel’s own docs lay out the mechanism with this exact example for the parent layout:

<!-- resources/views/layouts/app.blade.php -->
<html>
    <head>
        <title>App Name - @yield('title')</title>
    </head>
    <body>
        <div class="container">
            @yield('content')
        </div>
    </body>
</html>

And the matching child view, also straight from the docs:

<!-- resources/views/child.blade.php -->
@extends('layouts.app')

@section('title', 'Page Title')

@section('content')
    <p>This is my body content.</p>
@endsection

@extends names which layout this view inherits. @yield('content'), inside the parent, marks where a child’s filled-in section gets dropped in. @section('content') ... @endsection, inside the child, is what actually fills that slot. Brndle’s real theme structure follows this exactly. Every one of page.blade.php, single.blade.php, and index.blade.php opens with the same line, @extends('layouts.app'), and the single shared layout file, resources/views/layouts/app.blade.php, owns the entire page shell: the <html> tag, wp_head(), the skip-link, the conditional header and footer includes, and the single @yield('content') that every child view’s @section('content') block ultimately fills:

<!-- resources/views/layouts/app.blade.php, abridged -->
<body @php(body_class('font-sans antialiased bg-surface-primary text-text-primary'))>
  @php(wp_body_open())
  <div id="app">
    @unless($hideHeader ?? false)
      @include('sections.header')
    @endunless

    <main id="main" class="main">
      @yield('content')
    </main>

    @unless($hideFooter ?? false)
      @include('sections.footer')
    @endunless
  </div>
  @php(wp_footer())
</body>

And here is a real child view filling that slot, the whole of Brndle’s single.blade.php:

@extends('layouts.app')

@section('content')
  @while(have_posts())
    @php
      the_post();
      $allowedSingle = ['standard', 'hero-immersive', 'sidebar', 'editorial', 'cinematic', 'presentation', 'split', 'minimal-dark'];
      $layout = in_array($singleLayout, $allowedSingle) ? $singleLayout : 'standard';
    @endphp
    @include('partials.single.' . $layout)
  @endwhile
@endsection

Change the header markup once, in sections.header, included from the one shared layout, and every page type on the site picks it up, with no hook, no filter, and no editing eight separate template files. That’s the actual payoff classic WordPress theming has no direct equivalent for: not shorter syntax, structurally centralized layout.

How WordPress data actually reaches a Blade view

Syntax is only half the gap. The other half is the question that doesn’t even have an obvious place to ask it: if single.blade.php never calls view('single', ['post' => $post]), where does have_posts() actually get its data from inside that file? And a layer up, where does a variable like $hideTitle or $singleLayout, used in these files with no definition anywhere nearby, actually come from? There are two completely different answers here, and conflating them is exactly how this gets confusing.

First answer: nothing changed for the WordPress Loop and its globals. Blade is not a sandbox. Every .blade.php file compiles down to plain PHP, and that compiled PHP executes in the exact same request, with the exact same global state, $post, $wp_query, the whole template-tag API, that a classic single.php would have had. have_posts() works inside a Blade view for the identical reason it works inside a classic template: it’s reading WordPress’s global query object, which was already populated before any template, Blade or otherwise, ever started rendering. Nobody “passes” $post into a Blade view as an argument, because it never needed to be passed in the first place, the same as it never did in classic PHP theming. This is the detail that resolves most of the “but where does the data come from” confusion on its own: the WordPress Loop is orthogonal to Blade, not replaced by it.

Second answer: Acorn’s View Composers are the mechanism for everything else, theme settings, layout choices, computed strings, data that genuinely isn’t sitting in a WordPress global waiting to be read. A View Composer is a PHP class, extending Roots\Acorn\View\Composer, that declares which view names it applies to and returns an array of named values that get bound into those views automatically, every time they render, without the view file ever calling anything to request them. This is real, unedited code from Brndle’s own app/View/Composers/Post.php:

namespace Brndle\View\Composers;

use Roots\Acorn\View\Composer;

class Post extends Composer
{
    protected static $views = [
        'partials.page-header',
        'partials.content',
        'partials.content-*',
        'partials.single.*',
        'partials.archive.*',
    ];

    public function override(): array
    {
        return [
            'title' => $this->resolveTitle(),
            'readingTime' => $this->resolveReadingTime(),
            'pagination' => wp_link_pages([
                'echo' => 0,
                'before' => '<p>' . __('Pages:', 'brndle'),
                'after' => '</p>',
            ]),
        ];
    }
    // ...
}

Any view matching those patterns, partials.single.standard, partials.archive.grid, and so on, automatically receives $title, $readingTime, and $pagination as ready-to-use variables, computed once by this one class, with zero code inside the view itself requesting them. Brndle also ships an App.php composer bound to '*', every single view in the theme, which is how $siteName, $siteLogo, and similar globally-needed values end up available everywhere without a single get_bloginfo() call scattered across dozens of template files. Think of a View Composer as the Acorn-native, pattern-matched version of the global variables a classic functions.php might set via a template_redirect hook, except declared once, scoped explicitly to the views that need it, and testable as its own class.

Block attributes follow a third path worth naming separately, because it looks similar to a View Composer but is a different mechanism entirely: a plain view() call with an explicit data array, the same function signature Laravel itself uses. Here’s the real render callback from Brndle’s app/Providers/BlockServiceProvider.php, registering every custom Gutenberg block:

'render_callback' => function (array $attributes, string $content) use ($viewName, $blockSlug, $blockName) {
    $attributes = \Brndle\Blocks\AttributeMigrations::apply($blockName, $attributes);

    // Render through Acorn's Blade engine
    if (function_exists('Roots\\view')) {
        return view($viewName, [
            'attributes' => $attributes,
            'content' => $content,
        ])->render();
    }

    return $content;
}

That’s the full loop closed: WordPress calls this render callback with whatever attributes the block editor saved, the callback hands them to view() as a plain array, and that array lands inside the matching Blade file, resources/views/blocks/team.blade.php for the team block, as a variable literally named $attributes. Open that real file and the very first lines alias it for readability, nothing more:

@php
  $a = $attributes;
  $members = $a['members'] ?? [];
  $isDark = ($a['variant'] ?? 'light') === 'dark';
@endphp

So, three mechanisms, three different jobs: WordPress’s own globals and template tags, unchanged, for anything the Loop already gives you; Acorn View Composers for theme-level data bound automatically to a pattern of view names; and an explicit view($name, [...]) call, most visibly in a block’s render callback, for data that’s scoped to one specific render. None of the three require you to understand Laravel’s routing, its controllers, or anything else outside this narrow slice of how a view gets its variables.

A complete small example, built both ways

Theory lands better next to a full, working comparison, so here’s one small, self-contained feature, a grid of team member cards, built the classic WordPress way and then the Blade way, with the Blade version adapted directly from Brndle’s real blocks/team.blade.php rather than invented fresh for this post.

Classic PHP, assuming $members is an array of ['name' => ..., 'role' => ..., 'photo' => ..., 'bio' => ...] pulled from ACF or a block’s saved attributes:

<section class="team-section">
    <div class="team-grid">
        <?php foreach ( $members as $member ) : ?>
            <div class="team-card">
                <?php if ( ! empty( $member['photo'] ) ) : ?>
                    <img src="<?php echo esc_url( $member['photo'] ); ?>"
                         alt="<?php echo esc_attr( $member['name'] ?? '' ); ?>" />
                <?php else : ?>
                    <div class="team-card__placeholder">
                        <?php echo esc_html( strtoupper( substr( $member['name'] ?? 'T', 0, 1 ) ) ); ?>
                    </div>
                <?php endif; ?>

                <h3><?php echo esc_html( $member['name'] ?? '' ); ?></h3>

                <?php if ( ! empty( $member['role'] ) ) : ?>
                    <p class="team-card__role"><?php echo esc_html( $member['role'] ); ?></p>
                <?php endif; ?>

                <?php if ( ! empty( $member['bio'] ) ) : ?>
                    <p class="team-card__bio"><?php echo esc_html( $member['bio'] ); ?></p>
                <?php endif; ?>
            </div>
        <?php endforeach; ?>
    </div>
</section>

The Blade version, adapted from the real, shipped resources/views/blocks/team.blade.php:

@php
  $members = $attributes['members'] ?? [];
@endphp

<section class="team-section">
  <div class="team-grid">
    @foreach($members as $member)
      <div class="team-card">
        @if(!empty($member['photo']))
          <x-img :src="$member['photo']" :alt="$member['name'] ?? ''" />
        @else
          <div class="team-card__placeholder">
            {{ strtoupper(substr($member['name'] ?? 'T', 0, 1)) }}
          </div>
        @endif

        <h3>{{ $member['name'] ?? '' }}</h3>

        @if(!empty($member['role']))
          <p class="team-card__role">{{ $member['role'] }}</p>
        @endif

        @if(!empty($member['bio']))
          <p class="team-card__bio">{{ $member['bio'] }}</p>
        @endif
      </div>
    @endforeach
  </div>
</section>

Line for line, the logic is identical, the same null-coalescing fallbacks, the same conditional photo-or-placeholder branch, the same four fields rendered in the same order. What’s different is density and where escaping happens: the classic version spends roughly a third of its characters on <?php, ?>, and esc_html() call wrappers, while the Blade version gets the same escaping for free from {{ }} and drops the PHP tag pairs entirely, leaving the actual markup structure easier to scan. Neither version is “more correct”; they produce the same escaped, safe HTML. The real Brndle source additionally swaps the hand-written <img> tag for <x-img :src="..." :alt="..." />, Brndle’s own component that automatically emits AVIF/WebP <picture> sources when they exist on disk, a capability a hand-rolled <img> tag in a classic template would need its own separate PHP function to replicate.

Common beginner mistakes

  • Reaching for {!! !!} out of habit whenever something “looks like it isn’t escaping.” The most common real mistake isn’t forgetting to escape, it’s over-correcting the other way: wrapping something in {!! !!} because {{ }} “didn’t seem to work,” when the actual problem was unrelated. Default to {{ }} for every plain string and reach for {!! !!} only for values you know are already-rendered HTML from a trusted WordPress function, never for anything that touched user input.
  • Assuming $post or the query needs to be explicitly passed into a Blade view. It doesn’t, and trying to “fix” this by manually re-fetching get_post() or re-running a WP_Query inside a Blade file that’s already inside the standard WordPress template hierarchy is redundant work solving a problem that was never there. The data-flow section above explains exactly why.
  • Using slashes instead of dots in a view name. @include('partials/header') will not resolve; Blade view names are always dot-separated, @include('partials.header'), regardless of how the file path looks on disk with its forward slashes.
  • Forgetting that an edited Blade file might be served from Acorn’s compiled view cache. If you change a .blade.php file and the page genuinely does not reflect it after a normal refresh, that’s very likely a separate, already-documented issue, not a syntax mistake, covered start to finish in why your Blade edits aren’t showing up.
  • Dropping raw <?php ?> tags into a .blade.php file out of old habit. Blade does let plain PHP tags pass through untouched, since Blade files are PHP underneath, but doing this defeats the entire point of adopting Blade’s directives and reintroduces the exact bracket-heavy visual noise the @if/@foreach syntax exists to remove. If you’re reaching for <?php, check whether an existing directive already covers it first.
  • Writing heavy business logic inline in a @php block instead of a View Composer. A short @php block for a couple of local variables, as in the real team.blade.php examples above, is normal and idiomatic. A sprawling @php block computing several unrelated values that several different views all need is usually a sign that logic belongs in a View Composer instead, where it’s written once, tested once, and automatically available everywhere it’s needed.

Questions developers ask

Is Blade a completely different language from PHP?

No. Per Laravel’s own documentation, “all Blade templates are compiled into plain PHP code and cached until they are modified.” A .blade.php file is PHP with a thin templating syntax layered on top, and every directive, @if, @foreach, {{ }}, compiles down to ordinary PHP control structures and echo statements before it ever executes.

Do I lose access to $post, WP_Query, or the usual template tags inside a Blade view?

No. Because Blade compiles to plain PHP running in the same WordPress request, every global WordPress sets up before rendering any template, $post, $wp_query, and every template tag like the_title() or get_the_content(), is available inside a Blade view exactly as it would be in a classic PHP template file.

What’s the real difference between @include and a component like <x-img />, and when should I use each?

@include pulls in another view and, per the docs, automatically shares “all variables that are available to the parent view” with it, the same inherited-scope behavior as a classic get_template_part() call. A component is a more self-contained unit: it declares its own explicit inputs as attributes (<x-img :src="$url" :alt="$alt" />) rather than implicitly inheriting whatever happens to be in scope, which makes it easier to reuse somewhere its surrounding variables don’t match. Reach for @include for page-structure chunks that naturally belong to the current context (a header, a page title block); reach for a component when you want something genuinely portable and self-contained, like Brndle’s own <x-img>, which needs nothing from its surroundings beyond the attributes you hand it directly.

Does {{ }}’s escaping break post content from the_content(), which already contains real HTML?

It would, which is exactly why you don’t use {{ }} for it. the_content() is a void function that echoes directly rather than returning a string, so in Blade it’s wrapped with @php(the_content()), as shown in the real page.blade.php example above, not placed inside {{ }} at all. If you’re instead working with a function that returns already-rendered HTML as a string, like get_the_content() variants or get_search_form(false), that’s the {!! !!} case covered earlier.

Can I still use get_template_part() inside a Blade file instead of @include?

You technically can, since it’s still a valid WordPress function call and Blade files are still PHP, but it mixes two different view-resolution systems in one place for no benefit: get_template_part() looks for classic .php files in the theme’s template-part locations, which isn’t where your .blade.php partials live. Use @include for anything that’s actually a Blade view.

Why does @extends(‘layouts.app’) use dots when the file is at resources/views/layouts/app.blade.php?

Blade’s view-naming convention always uses dot notation to represent the path from resources/views/, with the .blade.php extension always omitted from the directive. layouts.app maps to resources/views/layouts/app.blade.php, partials.components.category-filter maps to resources/views/partials/components/category-filter.blade.php, and so on, consistently, everywhere a view name is referenced, in @extends, @include, or a view() call.

What happens if I put plain <?php ?> tags inside a .blade.php file? Does it break?

It doesn’t break. Blade’s compiler passes raw PHP tags through untouched, since the whole file is eventually plain PHP anyway. The practical downside isn’t a technical failure, it’s readability: mixing raw <?php ?> blocks with Blade directives in the same file reintroduces the visual clutter Blade’s directive syntax exists specifically to remove, and makes a file harder for the next developer to scan consistently.

Do I need to learn Laravel as a framework to use Blade in a theme like Brndle?

No. Blade is the one piece of Laravel that Acorn brings into WordPress theming most directly, and everything covered in this guide, echoing, conditionals, loops, layout inheritance, components, is usable on its own without routing, Eloquent models, migrations, or any of Laravel’s other subsystems. Acorn’s View Composers are the one additional concept worth understanding, covered in full above, and even that’s a small, self-contained pattern rather than a doorway into the rest of the framework.

Is Blade’s $loop variable the same thing as WordPress’s $wp_query->current_post?

They’re conceptually similar, position-tracking inside an iteration, but they’re unrelated mechanisms serving different loops. $loop is Blade’s own variable, automatically available inside any @foreach, tracking that specific foreach’s position ($loop->first, $loop->last, $loop->index). $wp_query->current_post is WordPress’s own counter for the Loop specifically, advanced by the_post() inside a @while (have_posts()) block, not a @foreach. Don’t expect $loop to exist inside a @while (have_posts()) block; it’s a @foreach-only feature.

If I edit a Blade file and nothing visibly changes, is that a syntax mistake I made?

Possibly, but check the more common cause first. If the page shows your exact previous markup, byte for byte, rather than a broken or half-rendered version of your new markup, that’s the signature of Acorn serving a stale compiled view from its cache, not a syntax error in what you wrote. The full symptom checklist and fix are in why your Blade edits aren’t showing up.

Where this leaves you

Nothing in this translation is exotic once it’s named. {{ }} is esc_html() you don’t have to call yourself. @if and @foreach are the control structures you already know, with the PHP tags filed off. @extends/@section/@yield is the header-and-footer pattern turned inside out, into one owned layout instead of a dozen duplicated calls. And the data that fills these views arrives through exactly three channels: WordPress’s own globals and template tags, completely unchanged; Acorn’s View Composers, for theme-level data bound by view-name pattern; and an explicit view($name, [...]) call, most visibly inside a block’s render callback, for data scoped to one render. None of it replaces what you already know about WordPress. All of it sits directly on top of it, verified here against Laravel’s own Blade documentation and against real, shipped .blade.php files from Brndle’s own repository, not approximated from memory. The syntax is genuinely the easy part. Once the mapping is explicit instead of implicit, there’s nothing left in a Brndle Blade view that should read as unfamiliar.