Fix: AI Blog Assistant retry logic and Subdomain routing for Akademi

This commit is contained in:
Muhammet Güler
2026-02-16 14:55:06 +03:00
parent 0fa8e4e435
commit 7101f7878a
10 changed files with 458 additions and 1 deletions
@@ -0,0 +1,118 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\BlogTopic;
use App\Models\Blog; // Ensure this model exists and has 'status' column
use App\Models\User;
use App\Services\AiBlogService;
use Illuminate\Support\Str;
use Carbon\Carbon;
class GenerateBlogAssistant extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'blog:generate-assistant {--topic= : Specific topic to generate}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate a new AI blog post draft based on active topics.';
/**
* Execute the console command.
*/
public function handle(AiBlogService $aiService)
{
\Illuminate\Support\Facades\Log::info('GenerateBlogAssistant triggered. Args: ' . json_encode($_SERVER['argv'] ?? []));
$this->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;
}
}
+40
View File
@@ -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")) {
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class BlogTopic extends Model
{
use HasFactory;
protected $fillable = [
'name',
'context',
'status',
'last_used_at',
];
protected $casts = [
'last_used_at' => 'datetime',
];
public function scopeActive($query)
{
return $query->where('status', 'active');
}
}
+180
View File
@@ -0,0 +1,180 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
class AiBlogService
{
protected $geminiKey;
protected $unsplashKey;
public function __construct()
{
$this->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 <<<EOT
You are an expert content writer for "Trunçgil Teknoloji", a leading technology company specializing in web software, mobile apps, IoT, and digital transformation.
Write a comprehensive, SEO-friendly blog post about the topic: "{$topic}".
{$contextStr}
**IMPORTANT**: All content in the JSON response (title, content, excerpt, meta_data) MUST BE IN TURKISH language. Only the `image_prompt` should be in English.
Requirements:
1. **Format**: Return ONLY a valid JSON object. No markdown formatting outside the JSON structure.
2. **Language**: Turkish (Türkçe).
3. **Tone**: Professional, authoritative, yet accessible. Enthusiastic about technology.
4. **Brand Integration**: Naturally mention "Trunçgil Teknoloji" at least 2-3 times. Position Trunçgil Teknoloji as a solution provider or expert in this field. Use phrases like "Trunçgil Teknoloji olarak biz...", "Deneyimli ekibimizle...", etc.
5. **Structure**:
* `title`: Catchy, SEO-optimized title (Max 60 chars) - IN TURKISH.
* `meta_title`: SEO Meta Title - IN TURKISH.
* `meta_description`: SEO Meta Description (Max 160 chars) - IN TURKISH.
* `content`: Full HTML blog post content.
* Use <h2> and <h3> tags for headings.
* Use <p> for paragraphs.
* Include a bulleted list (<ul>/<li>) if appropriate.
* Do NOT use <h1> or <html>/<body> 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;
}
}
}