Safe Email Template Refactoring with Before/After Routes

📖 3 minutes read

Refactoring email templates is risky. One typo and your production emails break. Here’s how to iterate safely with side-by-side comparisons.

The Pattern

When you need to redesign an email template:

  1. Duplicate the template with -old suffix
  2. Create test routes for both versions
  3. Modify the new version while keeping old as reference
  4. Compare side-by-side before committing

Step 1: Duplicate Templates

cd resources/views/emails
cp notification.blade.php notification-old.blade.php

Now you have:

  • notification.blade.php — Your working copy (will be modified)
  • notification-old.blade.php — Backup (unchanged)

Step 2: Create Test Routes for Both

// routes/web.php (local/testing only)

Route::get('/test/email/notification-old', function () {
    $request = App\Models\Request::factory()->make();
    return (new App\Mail\RequestNotification($request))
        ->render('emails.notification-old');
});

Route::get('/test/email/notification-new', function () {
    $request = App\Models\Request::factory()->make();
    return new App\Mail\RequestNotification($request);
});

If your mailable uses a hardcoded view name, override it in the route:

Route::get('/test/email/notification-old', function () {
    $mailable = new App\Mail\RequestNotification($request);
    $mailable->view = 'emails.notification-old'; // Override
    return $mailable;
});

Step 3: Iterate on the New Version

Edit notification.blade.php freely. After each change:

  1. Open /test/email/notification-old (baseline)
  2. Open /test/email/notification-new (your changes)
  3. Compare side-by-side

If something breaks, the old version is still rendering correctly for comparison.

Step 4: Add State Parameters

Support different email states in both routes:

Route::get('/test/email/notification-old', function (Request $request) {
    $status = $request->get('status', 'pending');
    $requestModel = App\Models\Request::factory()->make(['status' => $status]);
    
    $mailable = new App\Mail\RequestNotification($requestModel);
    $mailable->view = 'emails.notification-old';
    return $mailable;
});

Route::get('/test/email/notification-new', function (Request $request) {
    $status = $request->get('status', 'pending');
    $requestModel = App\Models\Request::factory()->make(['status' => $status]);
    return new App\Mail\RequestNotification($requestModel);
});

Now compare all states:

  • /test/email/notification-old?status=pending vs /test/email/notification-new?status=pending
  • /test/email/notification-old?status=approved vs /test/email/notification-new?status=approved
  • /test/email/notification-old?status=rejected vs /test/email/notification-new?status=rejected

When to Commit

Once the new version looks good across all states:

  1. Delete notification-old.blade.php
  2. Delete the test routes
  3. Commit notification.blade.php

The -old backup stays in git history if you need it later.

Why This Works

  • Safe iteration — Original always accessible for comparison
  • Visual confirmation — See exactly what changed
  • Catch regressions — Spot broken states before prod
  • Fast rollback — Just restore the -old file if needed

Bonus: Use Browser DevTools to Compare

Open both URLs in separate browser tabs. Use DevTools to inspect HTML structure and spot differences. Chrome’s “Compare” feature in Elements panel is especially useful.

This pattern works for any high-stakes template refactor—emails, PDFs, invoices, reports. Duplicate, compare, commit.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *