getMemes($request->input('search')); } public function search(string $search): Response { // Convert + back to spaces for search $searchTerm = str_replace('+', ' ', $search); return $this->getMemes($searchTerm); } private function getMemes(?string $search = null): Response { $memes = $this->memeMediaService->searchMemes($search, 24); // Set up SEO meta tags $title = $search ? ucfirst($search).' Memes in MEMEFA.ST' : 'Meme Library - Thousands of Video Meme Templates'; if ($search) { // Get SEO descriptions from config $descriptions = config('platform.seo_descriptions.search_descriptions', []); // Use deterministic selection based on search term hash $searchHash = crc32($search); $descriptionIndex = abs($searchHash) % count($descriptions); $descriptionTemplate = $descriptions[$descriptionIndex]; // Replace keyword placeholder $description = str_replace('__KEYWORD__', $search, $descriptionTemplate); } else { $description = 'Browse thousands of video meme templates ready for TikTok, Instagram Reels, Threads and YouTube Shorts. Create viral content in minutes with our meme editor.'; } SEOMeta::setTitle($title, false); SEOMeta::setDescription($description); SEOMeta::setCanonical(request()->url()); // Add pagination rel links if ($memes->previousPageUrl()) { SEOMeta::addMeta('link:prev', $memes->previousPageUrl(), 'rel'); } if ($memes->nextPageUrl()) { SEOMeta::addMeta('link:next', $memes->nextPageUrl(), 'rel'); } // OpenGraph tags OpenGraph::setTitle($title); OpenGraph::setDescription($description); OpenGraph::setUrl(request()->url()); OpenGraph::addProperty('type', 'website'); // Twitter Card TwitterCard::setTitle($title); TwitterCard::setDescription($description); TwitterCard::setType('summary_large_image'); // Get available types for filter $types = $this->memeMediaService->getAvailableTypes(); // Get popular keywords for filter $popularKeywords = $this->memeMediaService->getPopularKeywords(20); return Inertia::render('memes/index', [ 'memes' => $memes, 'types' => $types, 'popularKeywords' => $popularKeywords, 'filters' => [ 'search' => $search, ], 'dynamicDescription' => $search ? $description : null, ]); } public function show(string $slug): Response { $meme = $this->memeMediaService->findBySlug($slug); // Get related memes based on similar keywords $relatedMemes = $this->memeMediaService->getRelatedMemes($meme, 6); // Set up SEO meta tags for individual meme page $title = "{$meme->name} - Make Video Memes with MEMEFA.ST"; $description = $meme->description ? "This meme is about: {$meme->description}." : "Create {$meme->name} video memes with our online editor. Perfect for TikTok, Instagram Reels, and YouTube Shorts."; SEOMeta::setTitle($title, false); SEOMeta::setDescription($description); SEOMeta::setCanonical(request()->url()); SEOMeta::addKeyword(collect([$meme->keywords, $meme->action_keywords, $meme->emotion_keywords, $meme->misc_keywords])->flatten()->filter()->implode(', ')); // OpenGraph tags OpenGraph::setTitle($title); OpenGraph::setDescription($description); OpenGraph::setUrl(request()->url()); OpenGraph::addProperty('type', 'video.other'); OpenGraph::addImage(route('memes.og', $meme->ids)); // Twitter Card TwitterCard::setTitle($title); TwitterCard::setDescription($description); TwitterCard::setType('summary_large_image'); TwitterCard::setImage(route('memes.og', $meme->ids)); return Inertia::render('memes/show', [ 'meme' => $meme, 'relatedMemes' => $relatedMemes, ]); } public function generateOG(string $ids): HttpResponse { // Get the meme media using the service $meme = $this->memeMediaService->findByHashIds($ids); // Load the template image $templatePath = public_path('memefast-og-template.jpg'); if (! file_exists($templatePath)) { abort(404, 'Template image not found'); } // Create ImageManager with Imagick driver $manager = new ImageManager(new Driver); // Load the template image $image = $manager->read($templatePath); // Ensure RGB colorspace for proper color rendering $imagick = $image->core()->native(); $imagick->setColorspace(\Imagick::COLORSPACE_SRGB); // Load the meme image from URL if ($meme->webp_url) { try { // Use cURL to get the image content with proper headers $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $meme->webp_url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (compatible; MemeFast OG Generator)'); $imageContent = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($imageContent !== false && $httpCode === 200) { $memeImage = $manager->read($imageContent); // Image positioning variables $imageX = 0; // Horizontal offset $imageY = 100; // Vertical offset // Resize meme image to 1.5x the template height while maintaining aspect ratio $memeImage = $memeImage->scale(height: 1350); // Place the meme image vertically centered, horizontally right-justified $image->place($memeImage, 'center-right', $imageX, $imageY); } else { $image->text('HTTP Error: '.$httpCode, 200, 200, function ($font) { $font->size(20); $font->color('#ff0000'); $font->align('center'); }); } } catch (\Exception $e) { // If meme image fails to load, add debug text $image->text('Image failed to load: '.$e->getMessage(), 200, 200, function ($font) { $font->size(20); $font->color('#ff0000'); $font->align('center'); }); } } // Add the meme name as text with proper kerning and line wrapping if ($meme->name) { $fontPath = resource_path('fonts/Geist/Geist-Bold.ttf'); // Fallback to built-in font if TTF not found if (file_exists($fontPath)) { // Use native Imagick for text kerning with line wrapping $imagick = $image->core()->native(); // Get the native Imagick object // First draw the green outline $drawOutline = new \ImagickDraw; $drawOutline->setFillColor('#00FF00'); $drawOutline->setFont($fontPath); $drawOutline->setFontSize(70); $drawOutline->setTextAlignment(\Imagick::ALIGN_LEFT); $drawOutline->setStrokeColor('#00FF00'); $drawOutline->setStrokeWidth(20); // Thicker stroke for outline effect // Then draw the black text on top $draw = new \ImagickDraw; $draw->setFillColor('#000000'); $draw->setFont($fontPath); $draw->setFontSize(70); // $draw->setTextKerning(-4); // Negative for tighter kerning $draw->setTextAlignment(\Imagick::ALIGN_LEFT); // Text wrapping - 70% of canvas width (1600 * 0.7 = 1120px) $maxWidth = 1120; $text = 'Make meme videos instantly with '.$meme->name.' meme with'; // Split text into words $words = explode(' ', $text); $lines = []; $currentLine = ''; foreach ($words as $word) { $testLine = $currentLine.($currentLine ? ' ' : '').$word; // Get text metrics for the test line $metrics = $imagick->queryFontMetrics($draw, $testLine); if ($metrics['textWidth'] > $maxWidth && $currentLine !== '') { // Line is too long, save current line and start new one $lines[] = $currentLine; $currentLine = $word; } else { $currentLine = $testLine; } } // Add the last line if ($currentLine) { $lines[] = $currentLine; } $lines[] .= 'MEMEFAST'; // Text positioning variables $textX = 100; // Horizontal position $textY = 320; // Base vertical position // Calculate line height and starting Y position $lineHeight = 75; // Tighter line spacing $totalHeight = count($lines) * $lineHeight; $startY = $textY - ($totalHeight / 2); // Center vertically // Draw each line (left-aligned) - first the outline, then the text foreach ($lines as $index => $line) { $y = $startY + ($index * $lineHeight); // Check if this is the MEMEFA.ST line if ($line === 'MEMEFAST') { $extraSizing = 40; // Use Bungee font for MEMEFA.ST $bungeeFontPath = resource_path('fonts/Bungee/Bungee-Regular.ttf'); if (file_exists($bungeeFontPath)) { // Create separate draw objects for Bungee font $bungeeOutline = new \ImagickDraw; $bungeeOutline->setFillColor('#00FF00'); $bungeeOutline->setFont($bungeeFontPath); $bungeeOutline->setFontSize(70); $bungeeOutline->setTextAlignment(\Imagick::ALIGN_LEFT); $bungeeOutline->setStrokeColor('#00FF00'); $bungeeOutline->setStrokeWidth(20); $bungeeText = new \ImagickDraw; $bungeeText->setFillColor('#000000'); $bungeeText->setFont($bungeeFontPath); $bungeeText->setFontSize(70 + $extraSizing); $bungeeText->setTextAlignment(\Imagick::ALIGN_LEFT); // Draw with Bungee font $bungeeOutline->annotation($textX, $y + $extraSizing, $line); $bungeeText->annotation($textX, $y + $extraSizing, $line); $imagick->drawImage($bungeeOutline); $imagick->drawImage($bungeeText); } else { // Fallback to regular font $drawOutline->annotation($textX, $y + $extraSizing, $line); $draw->annotation($textX, $y + $extraSizing, $line); } } else { // Use regular font for other lines $drawOutline->annotation($textX, $y, $line); $draw->annotation($textX, $y, $line); } } // Apply the drawing to the image (only for non-Bungee lines) $imagick->drawImage($drawOutline); // Draw outline first $imagick->drawImage($draw); // Draw text on top } else { $image->text($meme->name, 400, 450, function ($font) { $font->size(80); $font->color('#000000'); $font->align('center'); $font->valign('middle'); }); } } // Generate the image and return as response $encodedImage = $image->toJpeg(100); return response($encodedImage, 200, [ 'Content-Type' => 'image/jpeg', 'Cache-Control' => 'public, max-age=3600', ]); } }