Migrating from Laravel Blade to stx

On this page 54

This guide helps Laravel developers transition from Blade templates to stx. stx uses Blade-inspired syntax, making migration straightforward while providing additional features.

Quick Comparison

FeatureLaravel Bladestx
File extension.blade.php.stx
RuntimePHPBun/JavaScript
Expressions{{ $var }}{{ var }}
Raw output{!! $var !!}{!! var !!}
Comments{{-- --}}{{-- --}}
Directives@directive@directive

Variable Syntax

Blade (PHP)

{{ $user->name }}
{{ $items[0] }}
{{ $user['email'] }}

stx (JavaScript)

{{ user.name }}
{{ items[0] }}
{{ user.email }}

Key Differences:

  • No $ prefix for variables
  • Use . notation instead of -> for object properties
  • Both bracket and dot notation work for objects

Expressions & Filters

Blade

{{ strtoupper($name) }}
{{ number_format($price, 2) }}
{{ $date->format('Y-m-d') }}

stx

{{ name | uppercase }}
{{ price | currency }}
{{ date | date('YYYY-MM-DD') }}

stx provides built-in filters. You can also use JavaScript functions:

{{ name.toUpperCase() }}
{{ new Date(timestamp).toLocaleDateString() }}

Conditionals

@if / @elseif / @else

Blade:

@if($user->isAdmin())
    <span>Admin</span>
@elseif($user->isModerator())
    <span>Moderator</span>
@else
    <span>User</span>
@endif

stx:

@if(user.isAdmin())
  <span>Admin</span>
@elseif(user.isModerator())
  <span>Moderator</span>
@else
  <span>User</span>
@endif

@unless

Blade:

@unless($user->isGuest())
    <a href="/dashboard">Dashboard</a>
@endunless

stx:

@unless(user.isGuest())
  <a href="/dashboard">Dashboard</a>
@endunless

@isset / @empty

Blade:

@isset($avatar)
    <img src="{{ $avatar }}">
@endisset

@empty($posts)
    <p>No posts</p>
@endempty

stx:

@isset(avatar)
  <img src="{{ avatar }}">
@endisset

@empty(posts)
  <p>No posts</p>
@endempty

@switch

Blade:

@switch($status)
    @case('pending')
        <span>Pending</span>
        @break
    @case('approved')
        <span>Approved</span>
        @break
    @default
        <span>Unknown</span>
@endswitch

stx:

@switch(status)
  @case('pending')
    <span>Pending</span>
  @case('approved')
    <span>Approved</span>
  @default
    <span>Unknown</span>
@endswitch

Note: stx doesn't require @break after each case - it falls through automatically.

Loops

@foreach

Blade:

@foreach($users as $user)
    <li>{{ $user->name }}</li>
@endforeach

stx:

@foreach(users as user)
  <li>{{ user.name }}</li>
@endforeach

@foreach with $loop

Blade:

@foreach($items as $item)
    @if($loop->first)
        <li class="first">{{ $item }}</li>
    @elseif($loop->last)
        <li class="last">{{ $item }}</li>
    @else
        <li>{{ $item }}</li>
    @endif
@endforeach

stx:

@foreach(items as item)
  @if(loop.first)
    <li class="first">{{ item }}</li>
  @elseif(loop.last)
    <li class="last">{{ item }}</li>
  @else
    <li>{{ item }}</li>
  @endif
@endforeach

Loop Variable Mapping:

Bladestx
$loop->indexloop.index
$loop->iterationloop.iteration
$loop->firstloop.first
$loop->lastloop.last
$loop->countloop.count
$loop->remainingloop.count - loop.iteration
$loop->depthNot available (no nested tracking)
$loop->parentNot available

@forelse

Blade:

@forelse($posts as $post)
    <article>{{ $post->title }}</article>
@empty
    <p>No posts found.</p>
@endforelse

stx:

@forelse(posts as post)
  <article>{{ post.title }}</article>
@empty
  <p>No posts found.</p>
@endforelse

@for / @while

Blade:

@for($i = 0; $i < 10; $i++)
    {{ $i }}
@endfor

@while($condition)
    Processing...
@endwhile

stx:

@for(let i = 0; i < 10; i++)
  {{ i }}
@endfor

@while(condition)
  Processing...
@endwhile

@break / @continue

Works identically in both:

@foreach(users as user)
  @if(user.hidden)
    @continue
  @endif

  @if(loop.index > 10)
    @break
  @endif

  <li>{{ user.name }}</li>
@endforeach

stx also supports conditional versions:

@continue(user.hidden)
@break(loop.index > 10)

Layouts & Inheritance

@extends / @section / @yield

Blade:

{{-- layouts/app.blade.php --}}
<html>
<head>
    @yield('styles')
</head>
<body>
    @yield('content')
    @yield('scripts')
</body>
</html>

{{-- pages/home.blade.php --}}
@extends('layouts.app')

@section('content')
    <h1>Home Page</h1>
@endsection

stx:

<!-- layouts/app.stx -->
<html>
<head>
  @yield('styles')
</head>
<body>
  @yield('content')
  @yield('scripts')
</body>
</html>

<!-- pages/home.stx -->
@extends('layouts/app')

@section('content')
  <h1>Home Page</h1>
@endsection

Key Differences:

  • Use / instead of . for paths
  • No .blade.php extension needed - just reference the name

@parent

Works identically:

@section('scripts')
  @parent
  <script src="page-specific.js"></script>
@endsection

@push / @stack

Blade:

@push('scripts')
    <script src="app.js"></script>
@endpush

@stack('scripts')

stx:

@push('scripts')
  <script src="app.js"></script>
@endpush

@stack('scripts')

Includes

@include

Blade:

@include('partials.header')
@include('partials.card', ['title' => 'Hello'])

stx:

@include('partials/header')
@include('partials/card', { title: 'Hello' })

Key Differences:

  • Use / for paths
  • Use JavaScript object syntax { key: value } instead of PHP arrays ['key' => 'value']

@includeIf / @includeWhen / @includeUnless

Blade:

@includeIf('partials.optional')
@includeWhen($user->isPremium, 'partials.premium')
@includeUnless($user->isGuest, 'partials.user-menu')

stx:

@includeIf('partials/optional')
@includeWhen(user.isPremium, 'partials/premium')
@includeUnless(user.isGuest, 'partials/user-menu')

@includeFirst

Blade:

@includeFirst(['custom.header', 'default.header'])

stx:

@includeFirst(['custom/header', 'default/header'])

@each

Blade:

@each('partials.item', $items, 'item', 'partials.empty')

stx: Use @foreach with @forelse:

@forelse(items as item)
  @include('partials/item', { item })
@empty
  @include('partials/empty')
@endforelse

Components

Blade Components

Blade:

{{-- components/alert.blade.php --}}
<div class="alert alert-{{ $type }}">
    {{ $slot }}
</div>

{{-- Usage --}}
<x-alert type="warning">
    Be careful!
</x-alert>

stx:

<!-- components/alert.stx -->
<div class="alert alert-{{ type }}">
  {{ slot }}
</div>

<!-- Usage (PascalCase) -->
<Alert type="warning">
  Be careful!
</Alert>

<!-- Or kebab-case -->
<alert type="warning">
  Be careful!
</alert>

<!-- Or @component -->
@component('components/alert', { type: 'warning' })
  Be careful!
@endcomponent

Component Slots

Blade:

<x-card>
    <x-slot:header>
        Card Title
    </x-slot>

    Card content here
</x-card>

stx:

@component('components/card')
  @slot('header')
    Card Title
  @endslot

  Card content here
@endcomponent

Note: stx currently supports default slot. Named slots work via @slot.

Authentication

@auth / @guest

Blade:

@auth
    <a href="/logout">Logout</a>
@endauth

@guest
    <a href="/login">Login</a>
@endguest

stx:

@auth
  <a href="/logout">Logout</a>
@endauth

@guest
  <a href="/login">Login</a>
@endguest

Required Context:

{
  auth: {
    check: true,  // or false for guests
    user: { id: 1, name: 'John' }
  }
}

@can / @cannot

Blade:

@can('edit', $post)
    <button>Edit</button>
@endcan

@cannot('delete', $post)
    <span>Cannot delete</span>
@endcannot

stx:

@can('edit', 'post', post.id)
  <button>Edit</button>
@endcan

@cannot('delete', 'post', post.id)
  <span>Cannot delete</span>
@endcannot

Required Context:

{
  permissions: {
    check: (ability, type, id) => boolean
  }
}

Forms

/ @method

Works identically:

<form method="POST" action="/posts">
  @csrf
  @method('PUT')
  <!-- form fields -->
</form>

@error

Blade:

@error('email')
    <span class="error">{{ $message }}</span>
@enderror

stx:

@error('email')
  <span class="error">{{ message }}</span>
@enderror

Required Context:

{
  errors: {
    email: 'The email field is required.'
  }
}

Environment

@env / @production

Blade:

@env('local')
    <div>Debug panel</div>
@endenv

@production
    <script src="analytics.js"></script>
@endproduction

stx:

@env('local')
  <div>Debug panel</div>
@endenv

@production
  <script src="analytics.js"></script>
@endproduction

Required Context:

{
  env: 'production'  // or 'local', 'staging', 'testing'
}

Raw PHP vs JavaScript

Blade @php

Blade:

@php
    $total = array_sum($prices);
    $formatted = number_format($total, 2);
@endphp

stx @js:

@js
  const total = prices.reduce((sum, p) => sum + p, 0);
  const formatted = total.toFixed(2);
@endjs

Or use @ts for TypeScript:

@ts
  const total: number = prices.reduce((sum: number, p: number) => sum + p, 0);
@endts

JSON Output

@json

Blade:

<script>
    const config = @json($config);
</script>

stx:

<script>
  const config = @json(config);
</script>

Features Not in Blade

stx adds several features not found in Laravel Blade:

Filters

{{ text | uppercase | truncate(100) }}
{{ price | currency }}
{{ date | date('MMMM D, YYYY') }}

Content Security Policy

<head>
  @csp
</head>
<script nonce="{{ cspNonce }}">
  // Safe inline script
</script>

Animations

@transition('fade', 300)
  <div>Animated content</div>
@endtransition

SEO Helpers

@seo({
  title: 'Page Title',
  description: 'Description',
  openGraph: { image: '/og.jpg' }
})

Accessibility

@screenReader
  <span>Opens in new window</span>
@endscreenReader

Script Variables

Variables in <script> tags are automatically available:

<script>
const pageTitle = 'Welcome'
const items = ['a', 'b', 'c']
</script>

<h1>{{ pageTitle }}</h1>
@foreach(items as item)
  <span>{{ item }}</span>
@endforeach

Migration Checklist

When migrating a Blade template:

  1. File Extension: Rename .blade.php to .stx

  2. Variables: Remove $ prefix

    • {{ $user }} → {{ user }}
  3. Object Access: Change -> to .

    • {{ $user->name }} → {{ user.name }}
  4. Arrays/Objects: Use JS syntax

    • ['key' => 'value'] → { key: 'value' }
  5. Paths: Use / instead of .

    • @include('partials.header') → @include('partials/header')
  6. PHP Functions: Replace with JS equivalents or filters

    • strtoupper($x) → x | uppercase or x.toUpperCase()
  7. Date Formatting: Use JS Date or filters

    • $date->format('Y-m-d') → date | date('YYYY-MM-DD')
  8. Number Formatting: Use filters

    • number_format($x, 2) → x | number(2)
  9. Raw PHP: Replace @php with @js or @ts

  10. Components: Update syntax

    • <x-component> → <Component> or @component()

Example: Full Template Migration

Original Blade Template

{{-- resources/views/posts/show.blade.php --}}
@extends('layouts.app')

@section('title', $post->title)

@section('content')
<article>
    <h1>{{ $post->title }}</h1>

    @if($post->featured_image)
        <img src="{{ $post->featured_image }}" alt="{{ $post->title }}">
    @endif

    <div class="meta">
        By {{ $post->author->name }} on {{ $post->created_at->format('F j, Y') }}
    </div>

    <div class="content">
        {!! $post->content !!}
    </div>

    @auth
        @can('edit', $post)
            <a href="{{ route('posts.edit', $post) }}">Edit Post</a>
        @endcan
    @endauth

    <h2>Comments ({{ $post->comments->count() }})</h2>

    @forelse($post->comments as $comment)
        @include('partials.comment', ['comment' => $comment])
    @empty
        <p>No comments yet.</p>
    @endforelse

    @auth
        <form method="POST" action="{{ route('comments.store') }}">
            @csrf
            <input type="hidden" name="post_id" value="{{ $post->id }}">

            <textarea name="body" placeholder="Add a comment..."></textarea>
            @error('body')
                <span class="error">{{ $message }}</span>
            @enderror

            <button type="submit">Submit</button>
        </form>
    @endauth
</article>
@endsection

@push('scripts')
<script src="/js/comments.js"></script>
@endpush

Migrated stx Template

<!-- pages/posts/show.stx -->
@extends('layouts/app')

@section('title', post.title)

@section('content')
<article>
  <h1>{{ post.title }}</h1>

  @if(post.featuredImage)
    <img src="{{ post.featuredImage }}" alt="{{ post.title }}">
  @endif

  <div class="meta">
    By {{ post.author.name }} on {{ post.createdAt | date('MMMM D, YYYY') }}
  </div>

  <div class="content">
    {!! post.content !!}
  </div>

  @auth
    @can('edit', 'post', post.id)
      <a href="@route('posts.edit', { id: post.id })">Edit Post</a>
    @endcan
  @endauth

  <h2>Comments ({{ post.comments.length }})</h2>

  @forelse(post.comments as comment)
    @include('partials/comment', { comment })
  @empty
    <p>No comments yet.</p>
  @endforelse

  @auth
    <form method="POST" action="@route('comments.store')">
      @csrf
      <input type="hidden" name="post_id" value="{{ post.id }}">

      <textarea name="body" placeholder="Add a comment..."></textarea>
      @error('body')
        <span class="error">{{ message }}</span>
      @enderror

      <button type="submit">Submit</button>
    </form>
  @endauth
</article>
@endsection

@push('scripts')
<script src="/js/comments.js"></script>
@endpush

Need Help?