Ensuring Avatar Consistency: Syncing Local Assets in Laravel User Profiles
Have you ever meticulously migrated a crucial data point, only to find parts of your application stubbornly clinging to outdated references? This was precisely the challenge we faced with user avatars in the devlog-ist/landing project, which manages user profiles for job and mentorship boards.
The Unseen 404: A Tale of Two URLs
Our landing project recently upgraded its avatar storage, moving from external CDN links (like those from LinkedIn) to locally stored WebP files. The profile.photo_path attribute was successfully updated to point to these new local assets. However, a subtle but significant issue emerged: other parts of the application, particularly the job and mentorship boards, rendered avatars directly using users.avatar_url. This field, unfortunately, was still pointing to the old, expired LinkedIn CDN URLs, resulting in frustrating 404 broken image icons across critical user-facing views.
The Solution: Synchronized Avatar References
The fix involved ensuring that users.avatar_url always reflects the true source of the avatar. After a successful download and storage of a local avatar file, we now explicitly update users.avatar_url to point to the new asset('storage/...') path. This change was implemented in two key areas:
1. Real-time Synchronization on OAuth Login
For new users, or existing users re-authenticating via OAuth (e.g., Socialite), the avatar_url is updated immediately after the avatar file is downloaded and saved locally. This ensures that a user's avatar_url is always fresh and points to our internal storage.
Consider a Socialite callback where a user's profile is synchronized:
use App\Models\User;
use Illuminate\Support\Facades\Storage;
class SocialiteController extends Controller
{
public function handleProviderCallback(string $provider)
{
$socialiteUser = Socialite::driver($provider)->user();
$user = User::firstOrCreate(
['email' => $socialiteUser->getEmail()],
['name' => $socialiteUser->getName()]
);
if ($socialiteUser->getAvatar()) {
// Download and store the avatar locally
$contents = file_get_contents($socialiteUser->getAvatar());
$filename = 'avatars/' . Str::uuid() . '.webp';
Storage::disk('public')->put($filename, $contents);
// Update the profile's photo path
$user->profile->photo_path = $filename;
$user->profile->save();
// CRITICAL: Also update user.avatar_url to the local asset URL
$user->avatar_url = asset('storage/' . $filename);
$user->save();
}
Auth::login($user, true);
return redirect('/dashboard');
}
}
This snippet illustrates how users.avatar_url is explicitly set to the asset() helper function's output, pointing to the newly stored local file.
2. Batch Backfill for Existing Profiles
To address existing users whose profile.photo_path was already local but users.avatar_url was still an old CDN link, we implemented a batch Artisan command. This command scans profiles with local photo_path values and updates their corresponding users.avatar_url if it's found to be inconsistent.
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\Storage;
class DownloadExternalAvatarsCommand extends Command
{
protected $signature = 'app:download-external-avatars';
protected $description = 'Backfills user.avatar_url for profiles with local photo_path.';
public function handle()
{
$this->info('Starting avatar URL backfill...');
User::whereHas('profile', function ($query) {
$query->whereNotNull('photo_path'); // Has a local photo_path
})
->where(function ($query) {
$query->where('avatar_url', 'LIKE', '%linkedin.com%') // Still has old external URL
->orWhereNull('avatar_url'); // Or is null
})
->chunkById(100, function ($users) {
foreach ($users as $user) {
if ($user->profile->photo_path && Storage::disk('public')->exists($user->profile->photo_path)) {
$localUrl = asset('storage/' . $user->profile->photo_path);
if ($user->avatar_url !== $localUrl) {
$user->avatar_url = $localUrl;
$user->save();
$this->line("Updated avatar_url for user #{$user->id}");
}
}
}
});
$this->info('Avatar URL backfill complete.');
}
}
This command serves as a critical one-time pass to ensure data consistency across all user profiles.
Results and Next Steps
By implementing both real-time synchronization and a robust backfill mechanism, we've eliminated the pervasive 404 errors, ensuring that all user avatars now display correctly across the landing application. The user experience is significantly improved, with consistent and reliable image display.
After deploying such a fix, remember to clear any relevant application caches (e.g., php artisan profiles:cache-recruiter-catalog) to ensure that stale data isn't served. Regularly audit your data synchronization processes to catch inconsistencies early. Consider implementing automated tests that verify avatar_url against photo_path to prevent similar regressions in the future.