getCurrentUser(); if (!$user || !$user->hasPermission('seo.view')) { return new Response($this->view->render('components/alerts.twig', [ 'type' => 'danger', 'message' => 'Access denied.' ]), 403); } $module = Module::where('modules_name', $this->moduleName)->first(); if (!$module) { return new Response('Module not found', 404); } $settings = $module->modules_settings ?: []; $redirects = Redirect::orderBy('created_at', 'desc')->get(); return $this->renderAdminView('@seo/admin/index.twig', [ 'page_title' => 'SEO Settings', 'settings' => $settings, 'redirects' => $redirects ]); } /** * Save SEO settings. * * @param Request $request * @return Response */ public function save(Request $request): Response { try { $user = $this->getCurrentUser(); if (!$user || !$user->hasPermission('seo.manage')) { return new Response($this->view->render('components/alerts.twig', [ 'type' => 'danger', 'message' => 'Access denied.' ]), 403); } $module = Module::where('modules_name', $this->moduleName)->first(); if (!$module) { $this->addFlash('error', 'Módulo SEO não encontrado no banco de dados.'); return $this->redirect('/admin/seo'); } $data = $request->request->all(); $settings = $module->modules_settings ?: []; // Handle image upload if ($request->files->has('og_image_file')) { $file = $request->files->get('og_image_file'); if ($file && $file->isValid()) { $uploadDir = dirname(__DIR__, 4) . '/storage/uploads/seo'; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0755, true); } // Delete old image if (!empty($settings['og_image'])) { $oldPath = $uploadDir . '/' . $settings['og_image']; if (file_exists($oldPath) && is_file($oldPath)) { @unlink($oldPath); } } $filename = 'og_default_' . uniqid() . '.' . $file->guessExtension(); $file->move($uploadDir, $filename); $settings['og_image'] = $filename; } } $fields = [ 'meta_title', 'meta_description', 'meta_keywords', 'og_title', 'og_description', 'twitter_card', 'robots_txt', 'google_analytics_id', 'google_site_verification', 'json_ld_type', 'json_ld_name', 'json_ld_description', 'json_ld_url', 'json_ld_contact_phone', 'json_ld_contact_email', 'json_ld_address', 'json_ld_city', 'json_ld_state', 'json_ld_zip', 'json_ld_country', 'json_ld_social_links', 'sitemap_freq_static', 'sitemap_prio_static', 'sitemap_freq_blog', 'sitemap_prio_blog', 'sitemap_freq_news', 'sitemap_prio_news', 'sitemap_freq_guide', 'sitemap_prio_guide', 'sitemap_freq_events', 'sitemap_prio_events', 'sitemap_freq_faq', 'sitemap_prio_faq', 'sitemap_freq_expositor', 'sitemap_prio_expositor', 'sitemap_freq_albums', 'sitemap_prio_albums', 'sitemap_freq_pages', 'sitemap_prio_pages', 'sitemap_freq_photoessays', 'sitemap_prio_photoessays' ]; foreach ($fields as $field) { $settings[$field] = $data[$field] ?? null; } // Handle checkboxes explicitly $checkboxes = [ 'sitemap_include_static', 'sitemap_include_blog', 'sitemap_include_news', 'sitemap_include_guide', 'sitemap_include_events', 'sitemap_include_faq', 'sitemap_include_expositor', 'sitemap_include_albums', 'sitemap_include_pages', 'sitemap_include_photoessays' ]; foreach ($checkboxes as $cb) { $settings[$cb] = isset($data[$cb]) ? '1' : '0'; } $module->modules_settings = $settings; $module->save(); // Also update physical robots.txt if requested if (isset($data['update_robots_file']) && $data['update_robots_file'] == '1') { $this->updateRobotsFile($settings['robots_txt'] ?? ''); } $this->addFlash('success', __('seo.save_success')); } catch (\Exception $e) { \App\Helpers\Log::error('Error saving SEO settings: ' . $e->getMessage()); $this->addFlash('error', 'Erro ao salvar: ' . $e->getMessage()); } return $this->redirect('/admin/seo'); } /** * Generate SEO suggestions using local PHP-ML. */ public function generateAi(Request $request): Response { try { $user = $this->getCurrentUser(); if (!$user || !$user->hasPermission('seo.manage')) { return new Response(json_encode(['success' => false, 'error' => 'Access denied']), 403, ['Content-Type' => 'application/json']); } $corpus = []; // 1. Gather text from various modules to analyze if (DB::schema()->hasTable('blog_posts')) { $corpus[] = DB::table('blog_posts')->where('posts_status', 'published')->pluck('posts_title')->implode(' '); $corpus[] = DB::table('blog_posts')->where('posts_status', 'published')->pluck('posts_content')->implode(' '); } if (DB::schema()->hasTable('news')) { $corpus[] = DB::table('news')->where('news_status', 'published')->pluck('news_title')->implode(' '); } if (DB::schema()->hasTable('commercialguide_businesses')) { $corpus[] = DB::table('commercialguide_businesses')->where('businesses_status', 'approved')->pluck('businesses_name')->implode(' '); } if (DB::schema()->hasTable('pages')) { $corpus[] = DB::table('pages')->where('pages_is_published', 1)->pluck('pages_title')->implode(' '); $corpus[] = DB::table('pages')->where('pages_is_published', 1)->pluck('pages_content')->implode(' '); } $allText = strtolower(strip_tags(implode(' ', $corpus))); $allText = preg_replace('/[[:punct:]]/', '', $allText); // Remove punctuation // Stop words in PT-BR $stopWords = ['de', 'a', 'o', 'que', 'e', 'do', 'da', 'em', 'um', 'para', 'com', 'no', 'na', 'uma', 'os', 'as', 'dos', 'das', 'seu', 'sua', 'ou', 'quando', 'muito', 'nos', 'já', 'eu', 'também', 'só', 'pelo', 'pela', 'até', 'isso', 'ela', 'entre', 'depois', 'sem', 'mesmo', 'aos', 'seus', 'quem', 'nas', 'me', 'esse', 'eles', 'você', 'essa', 'num', 'nem', 'suas', 'meu', 'às', 'minha', 'numa', 'pelos', 'elas', 'qual', 'nós', 'lhe', 'deles', 'essas', 'esses', 'pelas', 'este', 'dele', 'tu', 'te', 'vocês', 'vos', 'lhes', 'meus', 'minhas', 'teu', 'tua', 'teus', 'tuas', 'nosso', 'nossa', 'nossos', 'nossas', 'está', 'estão', 'estivemos', 'estiveram', 'estava', 'estávamos', 'estavam', 'estivera', 'estivéramos', 'esteja', 'estejamos', 'estejam', 'estivesse', 'estivéssemos', 'estivessem', 'estiver', 'estivermos', 'estiverem', 'tenho', 'tem', 'temos', 'têm', 'tinha', 'tínhamos', 'tinham', 'tive', 'teve', 'tivemos', 'tiveram', 'tivera', 'tivéramos', 'tenha', 'tenhamos', 'tenham', 'tivesse', 'tivéssemos', 'tivessem', 'tiver', 'tivermos', 'tiverem', 'houve', 'houvemos', 'houveram', 'houvera', 'houvéramos', 'haja', 'hajamos', 'hajam', 'houvesse', 'houvéssemos', 'houvessem', 'houver', 'houvermos', 'houverem', 'como', 'mais', 'foi', 'foram', 'sou', 'somos', 'são', 'era', 'éramos', 'eram', 'seja', 'sejamos', 'sejam', 'fosse', 'fôssemos', 'fossem', 'for', 'formos', 'forem', 'seria', 'seríamos', 'seriam', 'será', 'serão', 'tenho', 'tem', 'temos', 'têm', 'tinha', 'tínhamos', 'tinham', 'tive', 'teve', 'tivemos', 'tiveram', 'tivera', 'tivéramos', 'tenha', 'tenhamos', 'tenham', 'tivesse', 'tivéssemos', 'tivessem', 'tiver', 'tivermos', 'tiverem', 'houve', 'houvemos', 'houveram', 'houvera', 'houvéramos', 'haja', 'hajamos', 'hajam', 'houvesse', 'houvéssemos', 'houvessem', 'houver', 'houvermos', 'houverem', 'mais', 'menos', 'muito', 'tudo', 'nada', 'sempre', 'nunca', 'talvez', 'ainda', 'assim', 'apenas', 'quase', 'então', 'entanto', 'pode', 'podem', 'poderia', 'deveria', 'querer', 'fazer', 'ir', 'ver', 'dar', 'vir', 'saber', 'parecer', 'falar', 'dizer']; // Tokenize and count $tokenizer = new WhitespaceTokenizer(); $tokens = $tokenizer->tokenize($allText); $tokens = array_filter($tokens, function($t) use ($stopWords) { return strlen($t) > 3 && !in_array($t, $stopWords); }); $counts = array_count_values($tokens); arsort($counts); $topKeywords = array_slice(array_keys($counts), 0, 10); $topThree = array_slice($topKeywords, 0, 3); $siteName = $_SESSION['bus_name'] ?? 'SysCMS'; if (empty($topThree)) { $suggestedTitle = $siteName . " - Portal de Conteúdo"; $suggestedDesc = "Bem-vindo ao portal " . $siteName . ". Aqui você encontra as melhores informações, notícias e serviços da nossa região."; $topKeywords = ['portal', 'noticias', 'serviços', 'empresas', 'eventos']; } else { $suggestedTitle = ucwords(implode(' | ', $topThree)) . ' - ' . $siteName; $suggestedDesc = "Confira o melhor em " . implode(', ', array_slice($topKeywords, 0, 5)) . " no portal " . $siteName . ". Conteúdo atualizado, guia de empresas, eventos e muito mais!"; } $sitemapUrl = url('/sitemap.xml'); $suggestedRobots = "# Gerado automaticamente via IA do SysCMS\n"; $suggestedRobots .= "User-agent: *\n"; $suggestedRobots .= "Allow: /\n"; $suggestedRobots .= "Disallow: /admin/\n"; $suggestedRobots .= "Disallow: /login\n"; $suggestedRobots .= "Disallow: /register\n"; $suggestedRobots .= "Disallow: /api/\n"; $suggestedRobots .= "Disallow: /vendor/\n"; $suggestedRobots .= "Disallow: /storage/logs/\n"; $suggestedRobots .= "\nSitemap: {$sitemapUrl}"; \App\Helpers\Log::info('SEO AI suggestions generated successfully'); return new Response(json_encode([ 'success' => true, 'title' => $suggestedTitle, 'description' => $suggestedDesc, 'keywords' => implode(', ', $topKeywords), 'robots_txt' => $suggestedRobots ]), 200, ['Content-Type' => 'application/json']); } catch (\Exception $e) { \App\Helpers\Log::error('AI Generation error: ' . $e->getMessage()); return new Response(json_encode(['success' => false, 'error' => $e->getMessage()]), 500, ['Content-Type' => 'application/json']); } } /** * Add a redirect. */ public function addRedirect(Request $request): Response { $user = $this->getCurrentUser(); if (!$user || !$user->hasPermission('seo.manage')) { return new Response('Access denied', 403); } $from = $request->request->get('redirects_from'); $to = $request->request->get('redirects_to'); $code = (int) $request->request->get('redirects_code', 301); if ($from && $to) { Redirect::create([ 'redirects_from' => $from, 'redirects_to' => $to, 'redirects_code' => $code, 'redirects_is_active' => true ]); $this->addFlash('success', __('seo.redirect_added')); } return $this->redirect('admin/seo#redirects'); } /** * Delete a redirect. */ public function deleteRedirect(Request $request, $id): Response { $user = $this->getCurrentUser(); if (!$user || !$user->hasPermission('seo.manage')) { return new Response('Access denied', 403); } $redirect = Redirect::find($id); if ($redirect) { $redirect->delete(); $this->addFlash('success', __('seo.redirect_deleted')); } return $this->redirect('admin/seo#redirects'); } /** * Update physical robots.txt file in public directory. */ private function updateRobotsFile($content) { $path = dirname(__DIR__, 4) . '/public/robots.txt'; @file_put_contents($path, $content); } /** * User Manual for SEO. */ public function manual(Request $request): Response { return $this->renderAdminView('seo/views/admin/manual.twig', [ 'title' => 'Manual do Usuário - SEO' ]); } /** * Generate Sitemap XML. * This is a public route. */ public function sitemap(Request $request): Response { $module = Module::where('modules_name', $this->moduleName)->first(); $settings = $module->modules_settings ?: []; $xml = ''; $xml .= ''; // Static Pages if (($settings['sitemap_include_static'] ?? '1') == '1') { $freq = $settings['sitemap_freq_static'] ?? 'weekly'; $prio = $settings['sitemap_prio_static'] ?? '1.0'; $urls = [ url('/'), url('/login'), url('/register'), url('/about'), url('/contact'), url('/services') ]; foreach ($urls as $u) { $xml .= ''; $xml .= '' . htmlspecialchars($u) . ''; $xml .= '' . $freq . ''; $xml .= '' . $prio . ''; $xml .= ''; } } // Blog Posts (if module exists and enabled) if (($settings['sitemap_include_blog'] ?? '1') == '1' && DB::schema()->hasTable('blog_posts')) { $freq = $settings['sitemap_freq_blog'] ?? 'daily'; $prio = $settings['sitemap_prio_blog'] ?? '0.8'; $posts = DB::table('blog_posts') ->where('posts_status', 'published') ->orderBy('created_at', 'desc') ->get(); foreach ($posts as $post) { $xml .= ''; $xml .= '' . htmlspecialchars(url('blog/' . $post->posts_slug)) . ''; $xml .= '' . date('Y-m-d', strtotime($post->posts_published_at ?: ($post->updated_at ?: $post->created_at))) . ''; $xml .= '' . $freq . ''; $xml .= '' . $prio . ''; $xml .= ''; } } // News (if module exists and enabled) if (($settings['sitemap_include_news'] ?? '1') == '1' && DB::schema()->hasTable('news')) { $freq = $settings['sitemap_freq_news'] ?? 'daily'; $prio = $settings['sitemap_prio_news'] ?? '0.7'; $news = DB::table('news') ->where('news_status', 'published') ->orderBy('created_at', 'desc') ->get(); foreach ($news as $item) { $xml .= ''; $xml .= '' . htmlspecialchars(url('news/show/' . $item->news_slug)) . ''; $xml .= '' . date('Y-m-d', strtotime($item->news_published_at ?: ($item->updated_at ?: $item->created_at))) . ''; $xml .= '' . $freq . ''; $xml .= '' . $prio . ''; $xml .= ''; } } // Commercial Guide (if enabled) if (($settings['sitemap_include_guide'] ?? '1') == '1' && DB::schema()->hasTable('commercialguide_businesses')) { $freq = $settings['sitemap_freq_guide'] ?? 'monthly'; $prio = $settings['sitemap_prio_guide'] ?? '0.7'; $businesses = DB::table('commercialguide_businesses') ->where('businesses_status', 'approved') ->get(); foreach ($businesses as $b) { $xml .= ''; $xml .= '' . htmlspecialchars(url('commercialguide/profile/' . $b->businesses_slug)) . ''; $xml .= '' . date('Y-m-d', strtotime($b->updated_at ?: $b->created_at)) . ''; $xml .= '' . $freq . ''; $xml .= '' . $prio . ''; $xml .= ''; } } // Events Guide (if enabled) if (($settings['sitemap_include_events'] ?? '1') == '1' && DB::schema()->hasTable('eventsguide_events')) { $freq = $settings['sitemap_freq_events'] ?? 'daily'; $prio = $settings['sitemap_prio_events'] ?? '0.7'; $events = DB::table('eventsguide_events') ->where('events_status', 'approved') ->get(); foreach ($events as $e) { $xml .= ''; $xml .= '' . htmlspecialchars(url('eventsguide/show/' . $e->events_slug)) . ''; $xml .= '' . date('Y-m-d', strtotime($e->updated_at ?: $e->created_at)) . ''; $xml .= '' . $freq . ''; $xml .= '' . $prio . ''; $xml .= ''; } } // FAQ (if enabled) if (($settings['sitemap_include_faq'] ?? '1') == '1' && DB::schema()->hasTable('faqs')) { $freq = $settings['sitemap_freq_faq'] ?? 'monthly'; $prio = $settings['sitemap_prio_faq'] ?? '0.5'; $faqs = DB::table('faqs') ->where('faqs_active', 1) ->where('faqs_visibility', 'public') ->get(); if (count($faqs) > 0) { $xml .= ''; $xml .= '' . htmlspecialchars(url('faq')) . ''; $xml .= '' . $freq . ''; $xml .= '' . $prio . ''; $xml .= ''; } } // Expositor / Vitrine (if enabled) if (($settings['sitemap_include_expositor'] ?? '1') == '1' && DB::schema()->hasTable('expositor_items')) { $freq = $settings['sitemap_freq_expositor'] ?? 'daily'; $prio = $settings['sitemap_prio_expositor'] ?? '0.8'; $items = DB::table('expositor_items') ->where('items_status', 'published') ->where('items_visibility', 'public') ->get(); foreach ($items as $item) { $xml .= ''; $xml .= '' . htmlspecialchars(url('vitrine/show/' . $item->items_slug)) . ''; $xml .= '' . date('Y-m-d', strtotime($item->updated_at ?: $item->created_at)) . ''; $xml .= '' . $freq . ''; $xml .= '' . $prio . ''; $xml .= ''; } // Expositor Categories if (DB::schema()->hasTable('expositor_categories')) { $categories = DB::table('expositor_categories')->get(); foreach ($categories as $cat) { $xml .= ''; $xml .= '' . htmlspecialchars(url('vitrine/category/' . $cat->categories_slug)) . ''; $xml .= 'weekly'; $xml .= '0.5'; $xml .= ''; } } } // Photo Albums (if enabled) if (($settings['sitemap_include_albums'] ?? '1') == '1' && DB::schema()->hasTable('photos_albums')) { $freq = $settings['sitemap_freq_albums'] ?? 'monthly'; $prio = $settings['sitemap_prio_albums'] ?? '0.6'; $albums = DB::table('photos_albums') ->where('photos_albums_status', 'published') ->where('photos_albums_visibility', 'public') ->get(); foreach ($albums as $album) { $xml .= ''; $xml .= '' . htmlspecialchars(url('photosalbum/show/' . $album->photos_albums_slug)) . ''; $xml .= '' . date('Y-m-d', strtotime($album->updated_at ?: $album->created_at)) . ''; $xml .= '' . $freq . ''; $xml .= '' . $prio . ''; $xml .= ''; } } // Custom Pages (PageBuilder) if (($settings['sitemap_include_pages'] ?? '1') == '1' && DB::schema()->hasTable('pages')) { $freq = $settings['sitemap_freq_pages'] ?? 'monthly'; $prio = $settings['sitemap_prio_pages'] ?? '0.7'; $pages = DB::table('pages') ->where('pages_is_published', 1) ->where('pages_public', 1) ->get(); foreach ($pages as $p) { $xml .= ''; $xml .= '' . htmlspecialchars(url($p->pages_slug)) . ''; $xml .= '' . date('Y-m-d', strtotime($p->updated_at ?: $p->created_at)) . ''; $xml .= '' . $freq . ''; $xml .= '' . $prio . ''; $xml .= ''; } } // Photo Essays if (($settings['sitemap_include_photoessays'] ?? '1') == '1' && DB::schema()->hasTable('photoessays')) { $freq = $settings['sitemap_freq_photoessays'] ?? 'monthly'; $prio = $settings['sitemap_prio_photoessays'] ?? '0.6'; $essays = DB::table('photoessays') ->where('essay_status', 'approved') ->get(); foreach ($essays as $e) { $xml .= ''; $xml .= '' . htmlspecialchars(url('photoessay/show/' . $e->essay_slug)) . ''; $xml .= '' . date('Y-m-d', strtotime($e->updated_at ?: $e->created_at)) . ''; $xml .= '' . $freq . ''; $xml .= '' . $prio . ''; $xml .= ''; } } $xml .= ''; $response = new Response($xml); $response->headers->set('Content-Type', 'application/xml'); return $response; } /** * Serve virtual robots.txt if file doesn't exist or we want to override. */ public function robots(Request $request): Response { $module = Module::where('modules_name', $this->moduleName)->first(); $content = "User-agent: *\nDisallow: /admin/\nAllow: /"; if ($module && isset($module->modules_settings['robots_txt'])) { $content = $module->modules_settings['robots_txt']; } $response = new Response($content); $response->headers->set('Content-Type', 'text/plain'); return $response; } }