Skip to main content

Building with Blade

The source tree behind every website — how pages become URLs, how layouts and components fit together, and how the templates compile.

Every Max AI website is written in Blade, the template language that ships with the Laravel framework. If you have never met it, the short version is: Blade is HTML with a small set of extra instructions for printing values, repeating markup, and pulling reusable pieces into a page.

The official Blade documentation is a good companion to this page. Read it knowing one thing up front: your website uses the static part of Blade, described below and listed exhaustively in the syntax reference.

Templates in, static website out

Your website has no server running behind it. It is a folder of finished files sitting on a fast global network, which is why it loads quickly, never goes down under traffic, and cannot be hacked through a login form it does not have.

So Blade runs at build time, not when a visitor arrives:

  1. You (or the assistant) save a template.
  2. The platform compiles every page in your website.
  3. The result — plain HTML, CSS, images — replaces the previous build.
  4. Your preview and your free dev address show it immediately. Your custom domain keeps serving the last version you published until you press Publish.

The compile is fast enough to be invisible; saving a file and looking at the preview is the whole loop.

The source tree

There are exactly two top-level folders, and everything you write lives in one of them.

resources/
  views/
    pages/                 ← one file per URL
      index.blade.php
      about.blade.php
      contact.blade.php
      404.blade.php
    components/
      layouts/
        main.blade.php     ← the document every page uses
      sections/
        nav.blade.php      ← reusable page sections
        hero.blade.php
        footer.blade.php
  data/
    site.json              ← read as $site
    collections/
      services.json        ← read as $services
      posts/*.json         ← a large collection, one file per entry, read as $posts
  css/
    site.css               ← the stylesheet

public/
  images/                  ← Media panel uploads
  robots.txt
  favicon.svg
  • resources/ holds sources. Nothing in it is served directly; it is compiled.
  • public/ is served verbatim from the root of your website. public/robots.txt is /robots.txt; public/images/hero.webp is /images/hero.webp.

Every template file ends in .blade.php — pages, layouts, and sections alike. Do not create index.html or any other file at the root of the tree: the build generates those from your sources and overwrites anything you put there by hand.

Pages and URLs

A file under resources/views/pages/ becomes a URL. The mapping is entirely mechanical:

Page file URL
pages/index.blade.php /
pages/about.blade.php /about
pages/services/plumbing.blade.php /services/plumbing
pages/blog/index.blade.php /blog
pages/404.blade.php the "page not found" page

Addresses are clean — /about, not /about.html. There is one more kind of page, named [collection.field].blade.php, which generates one URL per row of your data; that is covered in Site data and collections.

The quickest way to add a page is still New page… in the builder menu: it asks for a name and a URL, writes a correctly wrapped Blade page for you, and opens it. You can then edit it, or describe what it should contain to the assistant.

Printing values

Three pieces of syntax cover everything you output:

{{ $site->name }}           {{-- escaped: safe for any text --}}
{!! $post->content !!}      {{-- raw: only for HTML you trust --}}
{{-- a comment; never reaches the browser --}}

Use {{ }} for all text and attribute values — it escapes special characters, so a business name containing & or < renders correctly instead of breaking the markup. Reach for {!! !!} only when the value is deliberately HTML, such as an inline SVG icon stored in your data.

Layouts

A layout is the full HTML document your pages are poured into: the <head>, the navigation, the footer, the closing tags. Writing it once means a change to the navigation is a change to every page.

resources/views/components/layouts/main.blade.php:

@props(['title' => 'Home', 'description' => ''])
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $title }}</title>
<meta name="description" content="{{ $description }}">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
@vite(['resources/css/site.css'])
</head>
<body class="bg-canvas text-ink antialiased">
    <a href="#main-content" class="sr-only focus:not-sr-only">Skip to content</a>
    <x-sections.nav/>
    <main id="main-content">{{ $slot }}</main>
    <x-sections.footer/>
</body>
</html>

Two things are doing the work:

  • @props([...]) declares what the layout accepts and what each value defaults to. Here, a page can pass a title and a description; if it passes neither, the defaults apply.
  • {{ $slot }} is where the page's own content is dropped in.

A page then wraps itself in that layout:

<x-layouts.main title="About us" description="Licensed local pros since 2009.">
    <section class="mx-auto max-w-5xl px-6 py-20">
        <h1 class="text-4xl font-bold">About {{ $site->name }}</h1>
        <p class="mt-4 text-lg text-muted">{{ $site->tagline }}</p>
    </section>
</x-layouts.main>

Everything between the opening and closing tag becomes $slot. Attributes on the tag — title, description — arrive as the props the layout declared.

Blade in a full Laravel application also offers @extends, @section, and @yield. Those are not part of the static subset. Layouts here are always components.

Section components

Anything you would otherwise copy between pages belongs in a component under resources/views/components/. The convention is components/sections/ for the visible building blocks of a page — a hero, a services grid, a testimonial band, a call to action.

A component is addressed by its path with dots instead of slashes:

File Tag
components/layouts/main.blade.php <x-layouts.main>
components/sections/hero.blade.php <x-sections.hero/>
components/sections/feature-grid.blade.php <x-sections.feature-grid/>

A page becomes a short, readable list of the sections it is made of:

<x-layouts.main title="Keystone Home Services" description="Booked in minutes.">
    <x-sections.hero/>
    <x-sections.services/>
    <x-sections.reviews/>
    <x-sections.cta/>
</x-layouts.main>

Passing values in

Components take props exactly as layouts do:

{{-- components/sections/cta.blade.php --}}
@props(['heading' => 'Ready when you are', 'label' => 'Get a free estimate'])

<section class="bg-primary py-20 text-primary-foreground">
    <div class="mx-auto max-w-3xl px-6 text-center">
        <h2 class="text-3xl font-semibold">{{ $heading }}</h2>
        <a href="/contact" class="mt-8 inline-block rounded-full px-6 py-3">
            {{ $label }}
        </a>
    </div>
</section>
<x-sections.cta heading="Book your visit today"/>

A prop default may be a plain value — a string, a number, true, false — or an empty array [] or a flat list. Nested or keyed array defaults are not supported; put structured data in a collection instead.

What a component can see

A component sees three things and nothing else:

  1. The props passed to it.
  2. $site, your global site data.
  3. Every data collection, by name — whether it is one file or a folder of entry files.

It does not inherit the variables of the page that used it. That is deliberate: a section behaves the same wherever you drop it.

Control flow

Blade's conditionals and loops work as you would expect:

@if ($site->phone)
    <a href="{{ $site->phone_href }}">{{ $site->phone }}</a>
@elseif ($site->email)
    <a href="mailto:{{ $site->email }}">{{ $site->email }}</a>
@else
    <a href="/contact">Contact us</a>
@endif
@foreach ($services as $service)
    <article class="border-t border-line py-6">
        <h3 class="font-semibold">{{ $service->name }}</h3>
        <p class="mt-2 text-muted">{{ $service->blurb }}</p>
    </article>
@endforeach

A property your data does not contain is a compile error rather than an empty value, so use ?? when something is genuinely optional: {{ $site->tagline ?? '' }}.

Inside a loop, $loop describes where you are — $loop->first, $loop->last, $loop->index, $loop->iteration, $loop->count, $loop->even, $loop->odd:

@foreach ($reviews as $review)
    @if ($loop->first)
        <blockquote class="text-2xl font-medium">{{ $review->quote }}</blockquote>
    @else
        <blockquote class="text-base text-muted">{{ $review->quote }}</blockquote>
    @endif
@endforeach

@break and @continue accept a condition, which is how you show "the first three of something":

@foreach ($posts as $post)
    @break($loop->iteration == 4)
    <a href="/blog/{{ $post->slug }}">{{ $post->title }}</a>
@endforeach

Expressions are deliberately simple: variables, property access ($post->title), comparisons, &&, ||, !, parentheses, literals, and ?? for a fallback. There are no function calls. The full list is in the syntax reference.

Styling

Your website is styled with Tailwind CSS utility classes written directly in the markup, plus one stylesheet at resources/css/site.css:

@import 'tailwindcss';

@theme {
    --color-canvas: #ffffff;
    --color-ink: oklch(23% 0.012 277);
    --color-muted: oklch(46% 0.012 277);
    --color-line: oklch(91.5% 0.005 277);
    --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
}

Every name you declare in @theme becomes a real utility: --color-canvas gives you bg-canvas, text-canvas, border-canvas, and the rest. Retuning a handful of tokens recolors the whole website without touching a single page.

The layout links the stylesheet with @vite(['resources/css/site.css']). You do not have to configure anything: the build compiles your stylesheet down to exactly the utilities your pages actually use and links the result.

Your brand colors

Four tokens are guaranteed to exist on every website, whether or not the stylesheet mentions them:

  • --color-primary and --color-primary-foreground
  • --color-secondary and --color-secondary-foreground

They carry the colors chosen on the website's Branding page, which means bg-primary text-primary-foreground on a button keeps following the brand when someone changes it there — while a hard-coded bg-indigo-600 does not. Prefer the tokens for buttons, links, active states, and accents.

Write class names out in full

The build scans your markup for class names, so only classes that appear literally in a template exist in the finished stylesheet. class="bg-primary" works. Assembling a class name from pieces does not.

Images and other files

Anything you upload through the builder's Media panel lands in public/images/, and you reference it by its served address:

<img src="/images/storefront.webp" alt="Our shop on Alder Street" class="rounded-xl">

Uploads are already resized and compressed for the web. Give every meaningful image a purposeful alt; use alt="" aria-hidden="true" for images that are pure decoration.

Other public/ files — robots.txt, a favicon, a PDF menu — work exactly the same way: put them in public/, link them from the root.

JavaScript

Client-side JavaScript is fine. Put a single <script> at the end of the layout's <body> — one copy for the whole website, not one inside each section — and keep it optional: pages must render completely with JavaScript disabled. Gate any reveal animation behind a class the script itself adds, so a script that fails to run costs you the animation and never the content.

Link between pages with plain <a href="/about">. Never navigate with JavaScript.

What happens when you save

Every save recompiles the website, and the build does more than render templates:

  • Every page under pages/ is compiled, and public/ is copied over the top.
  • sitemap.xml and llms.txt are generated from the pages that exist, unless you ship your own.
  • Your stylesheet is compiled with only the utilities your pages use.
  • Internal links that point nowhere, images whose files are missing, and fonts used but never loaded are all reported.
  • The platform's health-check file is written, so publishing always works.

Problems are reported, not fatal: a template with a mistake in it still produces a page, which is why a broken template usually shows up as a section that renders wrong or goes missing rather than as an error screen. See what to do when a template will not compile.

Learn more

The Laravel documentation covers Blade in depth. These sections are the ones that map onto what you can use here:

Anything in those pages that is not in the syntax reference needs a running application, and will not compile here.