DEV Community

Cover image for 🚀 How to Add Dynamic Blog Content to Your AI Email Builder (Laravel + React)
Gurinder Chauhan
Gurinder Chauhan

Posted on

🚀 How to Add Dynamic Blog Content to Your AI Email Builder (Laravel + React)

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
Enter fullscreen mode Exit fullscreen mode

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__',
];
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Build the Blog Schema

The build method defines the structure, styling, and behavior of your blog element. It contains:

  1. Custom element info (ID, name, icon)
  2. Structural schema (how emails render selected posts)
  3. Source information (API instructions and pagination)
  4. Result map (maps API fields to placeholders)
  5. 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;
}
Enter fullscreen mode Exit fullscreen mode

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',
                    ]
                ),
            ],
        ),
    ];
}
Enter fullscreen mode Exit fullscreen mode

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__"],
                        ]],
                    ],
                ],
            ],
        ],
    ];
}
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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,
    ]);
}
Enter fullscreen mode Exit fullscreen mode

Step 9: Drag and Drop Blog Content in Email Builder

Once your Blog custom element is ready, go to your Email Builder dashboard:

  1. Add your saved header and footer blocks to the canvas.
  2. Open the elements panel and locate the Blog element.
  3. Drag it between the header and footer.
  4. 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)