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
| Feature | Laravel Blade | stx |
|---|---|---|
| File extension | .blade.php | .stx |
| Runtime | PHP | Bun/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())
<ahref="">Dashboard</a>
@endunless
@isset / @empty
Blade:
@isset($avatar)
<img src="{{ $avatar }}">
@endisset
@empty($posts)
<p>No posts</p>
@endempty
stx:
@isset(avatar)
<imgsrc="">
@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)
<liclass="">{{ item }}</li>
@elseif(loop.last)
<liclass="">{{ item }}</li>
@else
<li>{{ item }}</li>
@endif
@endforeach
Loop Variable Mapping:
| Blade | stx |
|---|---|
$loop->index | loop.index |
$loop->iteration | loop.iteration |
$loop->first | loop.first |
$loop->last | loop.last |
$loop->count | loop.count |
$loop->remaining | loop.count - loop.iteration |
$loop->depth | Not available (no nested tracking) |
$loop->parent | Not 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.phpextension needed - just reference the name
@parent
Works identically:
@section('scripts')
@parent
<scriptsrc=""script
@endsection
@push / @stack
Blade:
@push('scripts')
<script src="app.js"></script>
@endpush
@stack('scripts')
stx:
@push('scripts')
<scriptsrc=""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 -->
<divclass="">
{{ slot }}
</div>
<!-- Usage (PascalCase) -->
<Alerttype="">
Be careful!
</Alert>
<!-- Or kebab-case -->
<alerttype="">
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
<ahref="">Logout</a>
@endauth
@guest
<ahref="">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:
<formmethod=""action="">
@csrf
@method('PUT')
<!-- form fields -->
</form>
@error
Blade:
@error('email')
<span class="error">{{ $message }}</span>
@enderror
stx:
@error('email')
<spanclass="">{{ 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
<scriptsrc=""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>
<scriptnonce=""
// 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:
-
File Extension: Rename
.blade.phpto.stx -
Variables: Remove
$prefix{{ $user }}→{{ user }}
-
Object Access: Change
->to.{{ $user->name }}→{{ user.name }}
-
Arrays/Objects: Use JS syntax
['key' => 'value']→{ key: 'value' }
-
Paths: Use
/instead of.@include('partials.header')→@include('partials/header')
-
PHP Functions: Replace with JS equivalents or filters
strtoupper($x)→x | uppercaseorx.toUpperCase()
-
Date Formatting: Use JS Date or filters
$date->format('Y-m-d')→date | date('YYYY-MM-DD')
-
Number Formatting: Use filters
number_format($x, 2)→x | number(2)
-
Raw PHP: Replace
@phpwith@jsor@ts -
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)
<imgsrc=""alt="">
@endif
<divclass="">
By {{ post.author.name }} on {{ post.createdAt | date('MMMM D, YYYY') }}
</div>
<divclass="">
{!! post.content !!}
</div>
@auth
@can('edit', 'post', post.id)
<ahref="">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
<formmethod=""action="">
@csrf
<inputtype=""name=""value="">
<textareaname=""placeholder=""></textarea>
@error('body')
<spanclass="">{{ message }}</span>
@enderror
<buttontype="">Submit</button>
</form>
@endauth
</article>
@endsection
@push('scripts')
<scriptsrc=""script
@endpush
Need Help?
- Directive Reference - Complete list of all directives
- Configuration - Configuration options
- GitHub Issues - Report problems or ask questions