From 7101f7878a299ce6b3f774fe4cb0de55387c940e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammet=20G=C3=BCler?= Date: Mon, 16 Feb 2026 14:55:06 +0300 Subject: [PATCH] Fix: AI Blog Assistant retry logic and Subdomain routing for Akademi --- .../Commands/GenerateBlogAssistant.php | 118 ++++++++++++ app/Http/Controllers/PageController.php | 40 ++++ app/Models/BlogTopic.php | 27 +++ app/Services/AiBlogService.php | 180 ++++++++++++++++++ config/services.php | 8 + ..._02_16_004005_create_blog_topics_table.php | 31 +++ database/seeders/BlogTopicSeeder.php | 38 ++++ resources/views/templates/home/hero.blade.php | 3 +- routes/console.php | 6 + routes/web.php | 8 + 10 files changed, 458 insertions(+), 1 deletion(-) create mode 100644 app/Console/Commands/GenerateBlogAssistant.php create mode 100644 app/Models/BlogTopic.php create mode 100644 app/Services/AiBlogService.php create mode 100644 database/migrations/2026_02_16_004005_create_blog_topics_table.php create mode 100644 database/seeders/BlogTopicSeeder.php diff --git a/app/Console/Commands/GenerateBlogAssistant.php b/app/Console/Commands/GenerateBlogAssistant.php new file mode 100644 index 0000000..6252f09 --- /dev/null +++ b/app/Console/Commands/GenerateBlogAssistant.php @@ -0,0 +1,118 @@ +info('Starting AI Blog Assistant...'); + + // 1. Select Topic + $topicName = $this->option('topic'); + $topic = null; + + if ($topicName) { + $topic = BlogTopic::where('name', $topicName)->first(); + if (!$topic) { + $this->error("Topic '{$topicName}' not found."); + return 1; + } + } else { + // Find oldest used active topic + $topic = BlogTopic::active() + ->orderBy('last_used_at', 'asc') // Nulls first usually, or old dates + ->first(); + } + + if (!$topic) { + $this->warn('No active topics found. Please add topics to blog_topics table.'); + return 0; + } + + $this->info("Selected Topic: {$topic->name}"); + + // 2. Generate Content + $this->info('Generating content with Gemini API...'); + try { + $contentData = $aiService->generateContent($topic->name, $topic->context); + } catch (\Exception $e) { + $this->error('Content generation failed: ' . $e->getMessage()); + return 1; + } + + if (empty($contentData) || !isset($contentData['title']) || !isset($contentData['content'])) { + $this->error('Valid content not returned from AI Service.'); + return 1; + } + + $this->info('Content generated successfully.'); + + // 3. Generate/Fetch Image + $this->info('Fetching cover image...'); + $imagePath = null; + if (isset($contentData['image_prompt'])) { + $imagePath = $aiService->fetchAndSaveImage($topic->name, $contentData['image_title'] ?? $contentData['image_prompt']); // Pollinations uses prompt + } + + if (!$imagePath) { + $this->warn('Could not fetch image, proceeding without cover image.'); + } else { + $this->info("Image saved to: {$imagePath}"); + } + + // 4. Create Draft Blog Post + $author = User::first(); // Default to first user (Admin) + + $blog = new Blog(); + $blog->title = $contentData['title']; + $blog->slug = Str::slug($contentData['title']); + $blog->content = $contentData['content']; + $blog->excerpt = $contentData['excerpt'] ?? Str::limit(strip_tags($contentData['content']), 150); + $blog->meta_title = $contentData['meta_title'] ?? $contentData['title']; + $blog->meta_description = $contentData['meta_description'] ?? $blog->excerpt; + $blog->status = 'draft'; // Verification required + $blog->author_id = $author ? $author->id : 1; + $blog->featured_image = $imagePath; + $blog->published_at = null; // Draft + $blog->tags = $contentData['tags'] ?? []; + // category_id? We might need to guess or default. + // For now leave null or set default if exists. + + $blog->save(); + + // 5. Update Topic Usage + $topic->last_used_at = Carbon::now(); + $topic->save(); + + $this->info("Blog post '{$blog->title}' created as DRAFT (ID: {$blog->id})."); + + return 0; + } +} diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 14f16e3..841519b 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -46,8 +46,48 @@ class PageController extends Controller * Display a specific page by slug */ public function show($slug) + { + return $this->handleShow($slug); + } + + /** + * Handle nested page routes (e.g. /kurumsal/hakkimizda) + */ + public function showNested($parentSlug, $slug) + { + return $this->handleShow($slug); + } + + /** + * Handle akademi.truncgil.com.tr subdomain + */ + public function showAkademi() + { + return $this->handleShow('truncgil-akademi'); + } + + /** + * Unified show logic + */ + protected function handleShow($slug) { $settings = $this->getSettings(); + + // 3a. Akademi Sayfası Yönlendirmeleri + if ($slug === 'truncgil-akademi') { + $host = request()->getHost(); + $path = request()->path(); + + // Eğer ana domainden geliyorsa -> Subdomain'e yönlendir + if ($host !== 'akademi.truncgil.com.tr') { + return redirect()->to('https://akademi.truncgil.com.tr/', 301); + } + + // Eğer subdoman'de ama slug ile geliyorsa (/truncgil-akademi) -> Ana dizine yönlendir (/) + if ($host === 'akademi.truncgil.com.tr' && $path !== '/') { + return redirect()->to('https://akademi.truncgil.com.tr/', 301); + } + } // 1. Statik View Kontrolü if (view()->exists("templates.$slug")) { diff --git a/app/Models/BlogTopic.php b/app/Models/BlogTopic.php new file mode 100644 index 0000000..45f03d1 --- /dev/null +++ b/app/Models/BlogTopic.php @@ -0,0 +1,27 @@ + 'datetime', + ]; + + public function scopeActive($query) + { + return $query->where('status', 'active'); + } +} diff --git a/app/Services/AiBlogService.php b/app/Services/AiBlogService.php new file mode 100644 index 0000000..096f39d --- /dev/null +++ b/app/Services/AiBlogService.php @@ -0,0 +1,180 @@ +geminiKey = config('services.gemini.key', env('GEMINI_API_KEY')); + $this->unsplashKey = config('services.unsplash.access_key', env('UNSPLASH_ACCESS_KEY')); + } + + /** + * Generate blog content using Gemini API + */ + public function generateContent(string $topic, ?string $context = null): array + { + $prompt = $this->buildPrompt($topic, $context); + + // Debug logging + $keyStatus = empty($this->geminiKey) ? 'MISSING' : 'PRESENT (' . substr($this->geminiKey, 0, 5) . '...)'; + Log::info("Gemini Service: Key status: $keyStatus"); + + try { + $response = Http::withoutVerifying() + ->retry(3, 10000, function ($exception, $request) { + if ($exception instanceof \Illuminate\Http\Client\RequestException && $exception->response->status() === 429) { + return false; + } + return true; + }) + ->withHeaders(['Content-Type' => 'application/json']) + ->post("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={$this->geminiKey}", [ + 'contents' => [ + [ + 'parts' => [ + ['text' => $prompt] + ] + ] + ], + 'generationConfig' => [ + 'temperature' => 0.7, + 'topK' => 40, + 'topP' => 0.95, + 'maxOutputTokens' => 8192, + 'responseMimeType' => 'application/json', + ] + ]); + + if ($response->failed()) { + Log::error('Gemini API Error Status: ' . $response->status()); + Log::error('Gemini API Error Body: ' . $response->body()); + throw new \Exception('Gemini API request failed: ' . $response->body()); + } + + $data = $response->json(); + + if (!isset($data['candidates'][0]['content']['parts'][0]['text'])) { + Log::error('Gemini API Invalid Response: ' . json_encode($data)); + throw new \Exception('Invalid response from Gemini API.'); + } + + $jsonContent = $data['candidates'][0]['content']['parts'][0]['text']; + + // Clean markdown json blocks if present + $jsonContent = str_replace(['```json', '```'], '', $jsonContent); + + return json_decode($jsonContent, true); + + } catch (\Exception $e) { + Log::error('AI Blog Generation Exception: ' . $e->getMessage()); + throw $e; + } + } + + /** + * Build the prompt for Gemini + */ + private function buildPrompt(string $topic, ?string $context = null): string + { + $contextStr = $context ? "Additional Context: {$context}" : ""; + + return << and

tags for headings. + * Use

for paragraphs. + * Include a bulleted list (

    /
  • ) if appropriate. + * Do NOT use

    or / tags. + * Ensure the content is informative and valuable (min 600 words). + * ALL CONTENT MUST BE TURKISH. + * `excerpt`: A short summary (1-2 sentences) - IN TURKISH. + * `tags`: Array of 5-8 relevant tags (Turkish). + * `image_prompt`: A creative English prompt to generate a cover image for this article using an AI image generator. + +JSON Schema: +{ + "title": "string", + "meta_title": "string", + "meta_description": "string", + "content": "html_string", + "excerpt": "string", + "tags": ["string"], + "image_prompt": "string" +} +EOT; + } + + /** + * Fetch and save a cover image + */ + public function fetchAndSaveImage(string $keyword, string $imagePrompt): ?string + { + $imageUrl = null; + + // 1. Try Unsplash if key exists + if ($this->unsplashKey) { + try { + $response = Http::withoutVerifying()->get('https://api.unsplash.com/search/photos', [ + 'query' => $keyword, + 'orientation' => 'landscape', + 'per_page' => 1, + 'client_id' => $this->unsplashKey + ]); + + if ($response->successful() && !empty($response['results'])) { + $imageUrl = $response['results'][0]['urls']['regular']; + } + } catch (\Exception $e) { + Log::warning('Unsplash API failed: ' . $e->getMessage()); + } + } + + // 2. Fallback to Pollinations AI (using the generated prompt) + if (!$imageUrl) { + // Encode the prompt + $encodedPrompt = urlencode($imagePrompt . " high quality, technological, photorealistic, 4k"); + $imageUrl = "https://image.pollinations.ai/prompt/{$encodedPrompt}"; + } + + if (!$imageUrl) { + return null; + } + + // Download and Save + try { + $imageContent = Http::withoutVerifying()->get($imageUrl)->body(); + $filename = 'blog/ai-generated-' . Str::slug($keyword) . '-' . time() . '.jpg'; + + Storage::disk('public')->put($filename, $imageContent); + + return $filename; + } catch (\Exception $e) { + Log::error('Image download failed: ' . $e->getMessage()); + return null; + } + } +} diff --git a/config/services.php b/config/services.php index 6182e4b..992cfd8 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,12 @@ return [ ], ], + 'gemini' => [ + 'key' => env('GEMINI_API_KEY'), + ], + + 'unsplash' => [ + 'access_key' => env('UNSPLASH_ACCESS_KEY'), + ], + ]; diff --git a/database/migrations/2026_02_16_004005_create_blog_topics_table.php b/database/migrations/2026_02_16_004005_create_blog_topics_table.php new file mode 100644 index 0000000..962cc2c --- /dev/null +++ b/database/migrations/2026_02_16_004005_create_blog_topics_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->text('context')->nullable(); // Additional context/prompt for AI + $table->string('status')->default('active'); // active, passive + $table->timestamp('last_used_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('blog_topics'); + } +}; diff --git a/database/seeders/BlogTopicSeeder.php b/database/seeders/BlogTopicSeeder.php new file mode 100644 index 0000000..e25effb --- /dev/null +++ b/database/seeders/BlogTopicSeeder.php @@ -0,0 +1,38 @@ +insert([ + 'name' => $topic, + 'context' => 'Trunçgil Teknoloji çözümlerine ve uzmanlığına vurgu yapın.', + 'status' => 'active', + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } +} diff --git a/resources/views/templates/home/hero.blade.php b/resources/views/templates/home/hero.blade.php index a5711ba..5d206fb 100644 --- a/resources/views/templates/home/hero.blade.php +++ b/resources/views/templates/home/hero.blade.php @@ -5,7 +5,8 @@

    Hayatı kolaylaştıran çözümler sunuyoruz

    {{ t('Hayatı kolaylaştıran uygulamaları insan odaklı, akılcı ve sade bir biçimde gerçekleştirmek için var gücümüzle çalışıyoruz.') }}

    diff --git a/routes/console.php b/routes/console.php index 3c9adf1..1552d35 100644 --- a/routes/console.php +++ b/routes/console.php @@ -2,7 +2,13 @@ use Illuminate\Foundation\Inspiring; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Schedule; +use App\Console\Commands\GenerateBlogAssistant; Artisan::command('inspire', function () { $this->comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); + +// Schedule the AI Blog Assistant +// Run daily at 09:00 AM +Schedule::command(GenerateBlogAssistant::class)->dailyAt('09:00'); diff --git a/routes/web.php b/routes/web.php index 3efa821..3e10c36 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,11 @@ use App\Http\Controllers\TemplatePreviewController; use App\Http\Controllers\Api\SiteTranslationController; use App\Models\Page; +// Subdomain Routes +Route::domain('akademi.truncgil.com.tr')->group(function () { + Route::get('/', [PageController::class, 'showAkademi']); +}); + // Debug Route - Veritabanı kontrolü Route::redirect('/index', '/', 301); Route::redirect('/index.php', '/', 301); @@ -126,5 +131,8 @@ Route::get('/logo-preview', function (Illuminate\Http\Request $request) { "; })->name('logo.preview'); +// Nested Pages (Parent/Child) +Route::get('/{parentSlug}/{slug}', [PageController::class, 'showNested'])->name('page.show_nested'); + // Pages (en sonda olmalı - catch-all) Route::get('/{slug}', [PageController::class, 'show'])->name('page.show');