Sites 13 / 13
Syntax Reference
Every Blade construct DevDojo Sites supports on top of plain HTML, with exact behavior — the one page to bookmark.
Syntax Reference
Everything DevDojo Sites adds on top of plain HTML, on one page — bookmark it, ⌘F it, ship it. Sites speaks a focused subset of Blade, Laravel's templating language: echoes put values on the page, directives shape structure, and <x-…> tags pull in components. Two ground rules before the details:
{{ }}escapes what it prints. Text and attribute values are HTML-escaped, so a stray<or&in your data can't break the page. Reach for{!! !!}only when you want raw HTML.- Undefined variables are build errors. If you echo
{{ $subtitle }}and nothing named$subtitleis in scope, the build stops and tells you where. The fix is either to declare it (in@props, or as a collection) or to default it with??. This strictness is a feature — a typo surfaces immediately instead of rendering blank.
Folder conventions
Your site mirrors a Laravel site:
| Folder | Purpose |
|---|---|
resources/views/pages/ |
Your pages. pages/about.blade.php → /about; index.blade.php → /; folders nest (pages/blog/hi.blade.php → /blog/hi). Dynamic: pages/post/[post.slug].blade.php serves /post/{slug} per post collection entry, $post bound to the match (details). |
resources/views/components/layouts/ |
Page shells, used with <x-layouts.name>, holding {{ $slot }}. |
resources/views/components/ |
Reusable components, used with <x-name> (folders become dots). |
resources/data/site.json |
Global data, available everywhere as $site. |
resources/data/collections/ |
JSON arrays — each name.json is bound as $name everywhere: pages, layouts, and components, exactly like $site. |
resources/data/content/post/ |
Markdown items, bound as the $post collection. |
resources/css/site.css |
Your Tailwind entry + custom CSS. @vite inlines it. |
public/ |
Files served verbatim from your site root (public/robots.txt → /robots.txt, public/images/logo.svg → /images/logo.svg). |
Every template file ends in .blade.php.
Echoing values
| Syntax | Behavior |
|---|---|
{{ $expr }} |
Escaped output. HTML-special characters (& < > " ') become entities. Use this for all text and attribute values. |
{!! $expr !!} |
Raw, unescaped output. Only for HTML you trust — like a Markdown item's rendered content. |
{{-- comment --}} |
A Blade comment, removed entirely from the output. |
<h1>{{ $site->name }}</h1>
<p>{{ $plan->name }} — priced at {{ $plan->price }}</p>
<article>{!! $item->content !!}</article>
{{-- This note never reaches the browser. --}}
HTML comments still run their tags. A Blade tag inside an ordinary <!-- … --> comment is still compiled — <!-- {{ $secret }} --> prints the value. To actually hide a block (tags and all), wrap it in a Blade comment {{-- … --}}, which is removed before anything runs.
Control flow
The whole set — nothing else is available.
@if / @elseif / @else
@if ($plan->featured)
<span class="badge">Popular</span>
@elseif ($plan->legacy)
<span class="badge">Legacy</span>
@else
<span class="badge">Standard</span>
@endif
Works at the top level of a page or inside a loop. Unlike simpler template tools, Blade gives you real @elseif and @else — no need to write a second inverted condition.
@foreach
@foreach ($features as $feature)
<li>{{ $feature->title }}</li>
@endforeach
@foreach ($settings as $key => $value)
<tr><td>{{ $key }}</td><td>{{ $value }}</td></tr>
@endforeach
Loops can be nested. Items render in the order they appear in the JSON file.
@break and @continue
Both take an optional condition. @break stops the loop; @continue skips to the next item. @break with a condition is the idiom for "show the first N," since the dialect has no method calls:
@foreach ($sites as $site)
<article>{{ $site->title }}</article>
@break($loop->iteration == 3) {{-- render 3, then stop --}}
@endforeach
$loop inside a loop
| Field | What it is |
|---|---|
$loop->index |
Position, starting at 0 |
$loop->iteration |
Position, starting at 1 |
$loop->first |
true on the first item |
$loop->last |
true on the last item |
$loop->count |
Total number of items |
$loop->even / $loop->odd |
true on even / odd iterations |
(That's the full set. remaining, depth, and parent — the field for reaching an outer loop from a nested one — need your site running as a full app; see the boundary below.)
Components
Components live under resources/views/components/ and are used with an <x-…> tag. Dots are folders: components/sections/hero.blade.php → <x-sections.hero/>.
<x-nav/> {{-- self-closing --}}
<x-sections.hero heading="Welcome"/> {{-- with a prop --}}
<x-card>Body goes in the slot</x-card> {{-- paired, with slot content --}}
@props — declaring inputs
@props(['heading' => 'Features', 'items'])
- A
'key' => 'default'entry declares a prop with a fallback used when the tag omits it. Defaults are scalars (strings, numbers,true/false/null). - A bare
'items'entry declares a prop with no default — the form for a prop you always intend to pass in explicitly, such as a bound attribute carrying a different slice of data per instance (a collection read directly needs no@propsentry named after it at all). You can't give an array a default here; usenulland guard with@if. - Declared props become
$variablesinside the component. A component also sees the global$siteand every collection underresources/data/collections/by name, automatically — no prop, no bound attribute needed. A@propsdefault can never override an already-present collection of that name; only a value the tag actually passes does.
String vs. bound attributes
| On the tag | Passes |
|---|---|
heading="Our work" |
The literal string Our work. |
:items="$sites" |
The evaluated expression — here, the whole $sites collection. |
The : prefix means "this value is an expression, not a string." Use it to hand a component per-instance data — a different slice than the collection it could already read directly:
<x-sections.feature-grid heading="Why us" :items="$features"/>
Slots
- Default slot — the tag's body, read as
{{ $slot }}in the component. - Named slots — declare the name in
@props, fill it with<x-slot:name>…</x-slot>:
<x-layouts.marketing title="Launch">
<x-slot:hero><h1>We're live!</h1></x-slot>
<p>Everything you need, today.</p>
</x-layouts.marketing>
The expression grammar
Everything inside {{ }}, an @if, a :bound attribute, or a @break condition is an expression. The supported pieces:
| Piece | Examples |
|---|---|
| Variables | $site, $plan, $loop |
| Property access | $plan->price, $site->name |
| Array access | $items[0], $row['key'] |
| Null-coalescing | $site->tagline ?? 'Welcome' |
| Comparisons | == != > < >= <= |
| Logic | && (and) ` |
| Literals | 'text', "text", 42, 2.5, true, false, null |
Comparisons follow PHP's rules: '1' == 1 is true, [] and '' and 0 are falsy. When a value might be missing, default it with ?? — this is also the sanctioned way to reference something optional, since ?? suppresses the undefined-variable error for its left side: {{ $site->phone ?? '' }} is always safe.
Data bindings
| Source | Available as | Notes |
|---|---|---|
resources/data/site.json |
$site |
A flat object, available everywhere — pages, layouts, and components. |
resources/data/collections/plans.json |
$plans |
A JSON array, available everywhere — pages, layouts, and components — exactly like $site. A component's own @props default can't override it; only a passed attribute can. Order = the order in the file. |
resources/data/content/post/*.md |
$post |
Markdown items, ordered by filename. Each carries its frontmatter plus content (rendered HTML) and link (its route). |
Read a field with the arrow: {{ $plan->name }}, {{ $site->tagline }}. Loop a collection with @foreach. Render a Markdown item's body with {!! $item->content !!}.
@vite — Tailwind and your CSS
@vite('resources/css/site.css')
Put this in your layout's <head>. It renders the Tailwind CSS engine plus your resources/css/site.css inlined — so Tailwind utilities work everywhere and your custom styles ship. Start your CSS with @import "tailwindcss";. Behavior is identical in the preview, on your published site, and in ZIP exports. (Bundling JavaScript through @vite is an app-tier feature; for a static site, use plain <script> tags or a CDN.)
Escaping a literal @
Blade treats @word as a directive. To print a literal one — common in CDN URLs and email-in-code — double it:
<script src="https://unpkg.com/@@tailwindcss/browser"></script>
{{-- renders: https://unpkg.com/@tailwindcss/browser --}}
(An @ mid-word, like in a mailto: address, doesn't need escaping — only an @ at a word boundary.)
When something's off
Sites tells you exactly what's wrong, in one of two ways:
- Build errors stop the page and point at the line: an undefined variable, a malformed expression, echoing a whole array, or a missing directive. The preview shows the message so you can fix it fast.
- Missing components render an HTML comment in place —
<!-- pocketknife:missing component "sections.hero" -->— rather than breaking the page. If a section seems to have silently vanished, check the preview's page source for one.
The app-tier boundary — your upgrade story
DevDojo Sites are static: they compile to plain HTML that serves from anywhere, with no server running your code. That's what makes them fast, cheap, and portable. The trade-off is a deliberate line: some perfectly valid Blade needs a running application behind it, so it's not part of the static subset.
These constructs are the boundary — writing one signals "this needs your site running as a full app" instead of producing output:
- Function and method calls of any kind —
count($items),route('home'),asset(...),$post->format(). No()calls at all. - Directives beyond the ones above —
@php,@auth,@guest,@include,@can,@error,@forelse, and the rest. $attributes, the?->operator, string concatenation with., arithmetic (+ - * /), ternaries (? :— use??instead), assignments, and closures.
None of this is a dead end — it's the dial from a static site to a full Laravel app. When your site genuinely needs authentication, form submissions, a database, or dynamic logic, that's the moment to upgrade it to an app, and this exact syntax starts working. Until then, build the static shell — precompute values into your data files, link with plain <a href>, and load assets with <link> / <script> / <img> tags.
A page that uses (almost) everything
<x-layouts.main title="Home" description="Everything, on one page.">
<x-sections.hero heading="Hi there" :items="$features"/>
<section class="mx-auto max-w-5xl py-20">
@foreach ($features as $feature)
<div>
<span>#{{ $loop->iteration }}</span>
<h3>{{ $feature->title }}</h3>
@if ($feature->highlight)
<span>★ Featured</span>
@endif
</div>
@break($loop->iteration == 3)
@endforeach
<a href="/about">About us</a>
</section>
</x-layouts.main>