Tired of static email templates? In this tutorial, we’ll show you how to integrate dynamic content — like blog posts, podcasts, or live updates — into your AI Email Builder. By the end, you’ll have a fully dynamic email template that automatically populates content without writing any frontend logic.
We’ll use Laravel and React in this example, but the same principles apply to Inertia Vue.js, Livewire, and FilamentPHP setups. You can explore the official builder at https://emailbuilder.dev.
Prerequisites
Before you start, make sure you have:
- A Laravel project with AI Email Builder installed.
- Any supported frontend: Inertia React, Inertia Vue.js, Livewire, or FilamentPHP.
- Some blog posts already stored in your database.
If you haven’t installed the Email Builder yet, follow the guides at https://emailbuilder.dev.
Step 1: Scaffold a Custom Blog Element
Open your terminal and run the command to scaffold a custom element:
php artisan email-builder:custom-element-scaffold
The command will prompt you for a Custom Element ID. Enter: blog
Next, it will ask if the element is loopable. Choose yes since newsletters often contain multiple blog posts.
This will create a Blog.php file inside App\EmailBuilder\CustomElements.
Step 2: Define Blog Placeholders in Blog.php
Open Blog.php. The first step is to define placeholders that map to the blog post fields returned by the API. These placeholders will automatically populate the email content.
protected array $placeholders = [
'loopId' => '__LOOP_ITEM_ID__',
'loopTitle' => '__LOOP_ITEM_TITLE__',
'loopDescription' => '__LOOP_ITEM_DESCRIPTION__',
'loopPublishAt' => '__LOOP_ITEM_PUBLISH_AT__',
'loopAuthor' => '__LOOP_ITEM_AUTHOR__',
'loopCategory' => '__LOOP_ITEM_CATEGORY__',
'loopImage' => '__LOOP_ITEM_IMAGE__',
'loopLink' => '__LOOP_ITEM_LINK__',
];
Step 3: Add the API Source URL
Next, point the custom element to the API route where blog posts will be fetched.
public function sourceUrl(): string
{
return route('api.email-builder.dynamic-data.blog-posts.index');
}
Step 4: Build the Blog Schema
The build method defines the structure, styling, and behavior of your blog element. It contains:
- Custom element info (ID, name, icon)
- Structural schema (how emails render selected posts)
- Source information (API instructions and pagination)
- Result map (maps API fields to placeholders)
- Display schema (how posts appear in the selection window)
Here’s the main structure:
public function build(): static
{
$this->schema = [
'id' => 'blog',
'name' => 'Blog',
'title' => 'Display Blog items',
'description' => 'Add Blog items in the email.',
'loopable' => true,
'dynamic' => true,
'icon' => 'svg:<svg ...></svg>',
'schema' => [
'featured-content' => [
$this->makeSection(stack: false, config: ['padding' => '10px', 'background-color' => '#ffffff'], content: $this->itemContent(featured: true)),
],
'content' => [
$this->makeSection(stack: true, config: ['padding' => '10px', 'background-color' => '#ffffff'], content: $this->itemContent()),
],
],
'source' => PaginationSource::make()
->sourceUrl($this->options['sourceUrl'])
->paginated()
->sorts([
['label' => 'Latest', 'value' => 'orderBy=latest'],
['label' => 'Oldest', 'value' => 'orderBy=oldest'],
])
->toArray(),
'resultMap' => [
'fields' => [
'LOOP_ITEM_ID' => 'id',
'LOOP_ITEM_TITLE' => 'title',
'LOOP_ITEM_DESCRIPTION' => 'description',
'LOOP_ITEM_PUBLISH_AT' => 'published_at',
'LOOP_ITEM_AUTHOR' => 'author',
'LOOP_ITEM_CATEGORY' => 'category',
'LOOP_ITEM_IMAGE' => 'image',
'LOOP_ITEM_LINK' => 'link',
],
],
'display' => [
'type' => 'card',
'template' => $this->itemDisplaySchema(),
],
];
return $this;
}
Step 5: Understand the itemContent Method
The itemContent method defines how each blog item is rendered inside the email body.
- Featured items occupy 100% width
- Regular items occupy 50% width for two-column layout
- Includes image, title, description, category, and a Read More button
protected function itemContent(bool $featured = false): array
{
return [
$this->makeColumn(
width: $featured ? '100%' : '50%',
config: ['padding' => '0px'],
content: [
$this->makeImage(
src: $this->placeholderLoopImage(),
href: $this->placeholderLoopLink(),
config: ['padding' => '0'],
),
],
),
$this->makeColumn(
width: $featured ? '100%' : '50%',
config: ['padding' => '10px', 'vertical-align' => 'top'],
content: [
$this->makeText(
text: $this->placeholderLoopTitle(),
config: ['font-size' => $featured ? '16px' : '14px', 'font-weight' => '700', 'color' => '#ef4444', 'padding' => '0 0 10px 0']
),
$this->makeText(
text: $this->placeholderLoopDescription(),
config: ['font-size' => '12px', 'font-weight' => '500', 'color' => '#475569', 'padding' => '0 0 10px 0']
),
$this->makeText(
text: "Category: {$this->placeholderLoopCategory()}",
config: ['font-size' => '12px', 'font-weight' => '500', 'color' => '#475569', 'padding' => '0 0 10px 0']
),
$this->makeButton(
text: "Read More",
config: [
'href' => $this->placeholderLoopLink(),
'padding' => '0 0 10px 0',
'align' => 'left',
]
),
],
),
];
}
Step 6: Understand the itemDisplaySchema Method
The itemDisplaySchema method defines how items appear in the Email Builder selection window:
- Each blog item is a card with image, title, description, and metadata
- Supports a loopable content layout for multiple items
- Inline CSS ensures styling is consistent
protected function itemDisplaySchema(): array
{
return [
'config' => [
'style' => $this->displayStyleTagContent(),
],
'wrap' => [
'className' => 'blog-items-wrap',
],
'loopableContent' => [
"type" => "div",
"className" => "blog-item",
"loopable" => true,
"content" => [
[
"type" => "div",
"className" => "blog-featured-img-wrap",
"style" => "display: flex;",
"content" => [
["type" => "img", "className" => "blog-featured-img", "src" => "__LOOP_ITEM_IMAGE__", "alt" => "Placeholder"],
],
],
[
"type" => "div",
"className" => "blog-detail",
"content" => [
["type" => "h3", "className" => "blog-title", "text" => "__LOOP_ITEM_TITLE__"],
["type" => "p", "className" => "blog-description", "text" => "__LOOP_ITEM_DESCRIPTION__"],
["type" => "div", "className" => "blog-meta", "style" => "display: flex; gap: 16px;", "content" => [
["type" => "p", "className" => "blog-meta-date", "text" => "__LOOP_ITEM_PUBLISH_AT__"],
["type" => "p", "className" => "blog-meta-author", "text" => "__LOOP_ITEM_AUTHOR__"],
["type" => "p", "className" => "blog-meta-category", "text" => "__LOOP_ITEM_CATEGORY__"],
["type" => "a", "className" => "blog-meta-link", "text" => "Link", "href" => "__LOOP_ITEM_LINK__"],
]],
],
],
],
],
];
}
Step 7: Create the Blog API Endpoint
In your api.php file, add a route to serve blog posts:
Route::middleware(['auth', 'auth:sanctum'])->group(function () {
Route::get(
'/email-builder/dynamic-data/blog-posts',
[PostsController::class, 'emailBuilderList']
)->name('api.email-builder.dynamic-data.blog-posts.index');
});
Step 8: Implement emailBuilderList in PostsController
This method returns blog posts in a JSON format for the Email Builder. It supports pagination, search, and ordering.
public function emailBuilderList(Request $request)
{
$perPage = $request->get('perPage') ?: 20;
$page = $request->get('page') ?: 1;
$result = Post::query()
->when(filled($request->get('query')), fn($q) => $q->where('title', 'like', '%' . $request->get('query') . '%'))
->when($request->get('orderBy') === 'latest', fn($q) => $q->orderByDesc('published_at'))
->when($request->get('orderBy') === 'oldest', fn($q) => $q->orderBy('published_at', 'asc'))
->paginate(perPage: $perPage > 100 ? 20 : $perPage, page: $page)
->through(fn($model) => [
'id' => $model->id,
'title' => $model->title,
'description' => $model->description,
'image' => $model->image,
'author' => $model->author,
'published_at' => Carbon::parse($model->published_at)->format('M d, Y'),
'category' => $model->category,
'link' => $model->link,
]);
return response()->json([
'status' => 'success',
'result' => $result,
]);
}
Step 9: Drag and Drop Blog Content in Email Builder
Once your Blog custom element is ready, go to your Email Builder dashboard:
- Add your saved header and footer blocks to the canvas.
- Open the elements panel and locate the Blog element.
- Drag it between the header and footer.
- Select the blog posts to display — they’ll auto-populate in the email in the order you select.
Step 10: Preview and Send
The Email Builder will render your blog content dynamically. Featured items occupy full width, while regular items display in two columns. All styling is handled inline, and your selection window shows a neat card layout for each post.
Visit https://emailbuilder.dev to learn more and explore additional features.
Conclusion
With just a few steps, you can now:
- Scaffold a custom blog element
- Connect it to a secure API endpoint
- Map placeholders for dynamic content
- Build a flexible schema with featured and regular items
- Control the selection window using
itemDisplaySchema - Render dynamic email content with
itemContent - Drag and drop blog posts into emails for instant dynamic rendering
This approach works for any dynamic content, whether it’s blog posts, podcasts, or live updates — all without touching frontend code.
Explore more about AI Email Builder: https://emailbuilder.dev
References & Docs
Top comments (0)