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 (