First Commit
This commit is contained in:
Executable
+9
@@ -0,0 +1,9 @@
|
||||
SysCMS Commercial License
|
||||
|
||||
Copyright (c) 2026 Marco Costa. All rights reserved.
|
||||
|
||||
This software is licensed, not sold. Permission is granted to use this software subject to the terms of the SysCMS Commercial License.
|
||||
|
||||
Redistribution, sublicensing, resale, or disclosure of this software, in whole or in part, is strictly prohibited without prior written permission from the author.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
# SEO Module for SysCMS
|
||||
|
||||
Search Engine Optimization module.
|
||||
|
||||
## Features
|
||||
- Meta tags management
|
||||
- Sitemap generation
|
||||
- SEO analysis
|
||||
- Keyword optimization
|
||||
- Social media integration
|
||||
- Robots.txt management
|
||||
|
||||
## Installation
|
||||
1. Upload the `seo.zip` file through the module manager or extract it to the `modules/` directory.
|
||||
2. Run the migrations to register the module.
|
||||
3. Configure SEO settings in the admin panel.
|
||||
|
||||
## Routes
|
||||
- Web routes: `routes/web.php`
|
||||
|
||||
## License
|
||||
Copyright (c) 2026 Marco Costa. All rights reserved.
|
||||
Licensed under the SysCMS Commercial License.
|
||||
+552
@@ -0,0 +1,552 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Seo\Controllers;
|
||||
|
||||
use App\Controllers\BaseAdminController;
|
||||
use Kernel\View;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use App\Models\Module;
|
||||
use Modules\Seo\Models\Redirect;
|
||||
use Illuminate\Database\Capsule\Manager as DB;
|
||||
use Phpml\Tokenization\WhitespaceTokenizer;
|
||||
use Phpml\FeatureExtraction\TfIdfTransformer;
|
||||
use Phpml\FeatureExtraction\TokenCountVectorizer;
|
||||
|
||||
class SeoController extends BaseAdminController
|
||||
{
|
||||
private $moduleName = 'seo';
|
||||
|
||||
/**
|
||||
* Display SEO settings.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$user = $this->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 version="1.0" encoding="UTF-8"?>';
|
||||
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
|
||||
|
||||
// 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 .= '<url>';
|
||||
$xml .= '<loc>' . htmlspecialchars($u) . '</loc>';
|
||||
$xml .= '<changefreq>' . $freq . '</changefreq>';
|
||||
$xml .= '<priority>' . $prio . '</priority>';
|
||||
$xml .= '</url>';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 .= '<url>';
|
||||
$xml .= '<loc>' . htmlspecialchars(url('blog/' . $post->posts_slug)) . '</loc>';
|
||||
$xml .= '<lastmod>' . date('Y-m-d', strtotime($post->posts_published_at ?: ($post->updated_at ?: $post->created_at))) . '</lastmod>';
|
||||
$xml .= '<changefreq>' . $freq . '</changefreq>';
|
||||
$xml .= '<priority>' . $prio . '</priority>';
|
||||
$xml .= '</url>';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 .= '<url>';
|
||||
$xml .= '<loc>' . htmlspecialchars(url('news/show/' . $item->news_slug)) . '</loc>';
|
||||
$xml .= '<lastmod>' . date('Y-m-d', strtotime($item->news_published_at ?: ($item->updated_at ?: $item->created_at))) . '</lastmod>';
|
||||
$xml .= '<changefreq>' . $freq . '</changefreq>';
|
||||
$xml .= '<priority>' . $prio . '</priority>';
|
||||
$xml .= '</url>';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 .= '<url>';
|
||||
$xml .= '<loc>' . htmlspecialchars(url('commercialguide/profile/' . $b->businesses_slug)) . '</loc>';
|
||||
$xml .= '<lastmod>' . date('Y-m-d', strtotime($b->updated_at ?: $b->created_at)) . '</lastmod>';
|
||||
$xml .= '<changefreq>' . $freq . '</changefreq>';
|
||||
$xml .= '<priority>' . $prio . '</priority>';
|
||||
$xml .= '</url>';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 .= '<url>';
|
||||
$xml .= '<loc>' . htmlspecialchars(url('eventsguide/show/' . $e->events_slug)) . '</loc>';
|
||||
$xml .= '<lastmod>' . date('Y-m-d', strtotime($e->updated_at ?: $e->created_at)) . '</lastmod>';
|
||||
$xml .= '<changefreq>' . $freq . '</changefreq>';
|
||||
$xml .= '<priority>' . $prio . '</priority>';
|
||||
$xml .= '</url>';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 .= '<url>';
|
||||
$xml .= '<loc>' . htmlspecialchars(url('faq')) . '</loc>';
|
||||
$xml .= '<changefreq>' . $freq . '</changefreq>';
|
||||
$xml .= '<priority>' . $prio . '</priority>';
|
||||
$xml .= '</url>';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 .= '<url>';
|
||||
$xml .= '<loc>' . htmlspecialchars(url('vitrine/show/' . $item->items_slug)) . '</loc>';
|
||||
$xml .= '<lastmod>' . date('Y-m-d', strtotime($item->updated_at ?: $item->created_at)) . '</lastmod>';
|
||||
$xml .= '<changefreq>' . $freq . '</changefreq>';
|
||||
$xml .= '<priority>' . $prio . '</priority>';
|
||||
$xml .= '</url>';
|
||||
}
|
||||
|
||||
// Expositor Categories
|
||||
if (DB::schema()->hasTable('expositor_categories')) {
|
||||
$categories = DB::table('expositor_categories')->get();
|
||||
foreach ($categories as $cat) {
|
||||
$xml .= '<url>';
|
||||
$xml .= '<loc>' . htmlspecialchars(url('vitrine/category/' . $cat->categories_slug)) . '</loc>';
|
||||
$xml .= '<changefreq>weekly</changefreq>';
|
||||
$xml .= '<priority>0.5</priority>';
|
||||
$xml .= '</url>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 .= '<url>';
|
||||
$xml .= '<loc>' . htmlspecialchars(url('photosalbum/show/' . $album->photos_albums_slug)) . '</loc>';
|
||||
$xml .= '<lastmod>' . date('Y-m-d', strtotime($album->updated_at ?: $album->created_at)) . '</lastmod>';
|
||||
$xml .= '<changefreq>' . $freq . '</changefreq>';
|
||||
$xml .= '<priority>' . $prio . '</priority>';
|
||||
$xml .= '</url>';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 .= '<url>';
|
||||
$xml .= '<loc>' . htmlspecialchars(url($p->pages_slug)) . '</loc>';
|
||||
$xml .= '<lastmod>' . date('Y-m-d', strtotime($p->updated_at ?: $p->created_at)) . '</lastmod>';
|
||||
$xml .= '<changefreq>' . $freq . '</changefreq>';
|
||||
$xml .= '<priority>' . $prio . '</priority>';
|
||||
$xml .= '</url>';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 .= '<url>';
|
||||
$xml .= '<loc>' . htmlspecialchars(url('photoessay/show/' . $e->essay_slug)) . '</loc>';
|
||||
$xml .= '<lastmod>' . date('Y-m-d', strtotime($e->updated_at ?: $e->created_at)) . '</lastmod>';
|
||||
$xml .= '<changefreq>' . $freq . '</changefreq>';
|
||||
$xml .= '<priority>' . $prio . '</priority>';
|
||||
$xml .= '</url>';
|
||||
}
|
||||
}
|
||||
|
||||
$xml .= '</urlset>';
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Seo\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class Redirect
|
||||
* @package Modules\Seo\Models
|
||||
*/
|
||||
class Redirect extends Model
|
||||
{
|
||||
protected $table = 'seo_redirects';
|
||||
protected $primaryKey = 'redirects_id';
|
||||
|
||||
protected $fillable = [
|
||||
'redirects_from',
|
||||
'redirects_to',
|
||||
'redirects_code',
|
||||
'redirects_is_active'
|
||||
];
|
||||
|
||||
/**
|
||||
* Scope for active redirects.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('redirects_is_active', true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
use App\Models\Module;
|
||||
use Modules\Company\Models\Company;
|
||||
|
||||
class InsertSeoModule
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$moduleData = [
|
||||
'modules_name' => 'seo',
|
||||
'modules_display_name' => 'SEO Manager',
|
||||
'modules_description' => 'Gerenciamento de configurações globais de SEO, Sitemap e Robots.txt',
|
||||
'modules_version' => '1.0.0',
|
||||
'modules_author' => 'SysCMS Team',
|
||||
'modules_ative' => true,
|
||||
'modules_public' => true,
|
||||
'modules_blocked' => false,
|
||||
'modules_dependencies' => [],
|
||||
'modules_permissions' => [
|
||||
"seo.view",
|
||||
"seo.manage"
|
||||
],
|
||||
'modules_settings' => [
|
||||
"meta_title" => "SysCMS",
|
||||
"meta_description" => "",
|
||||
"meta_keywords" => "",
|
||||
"og_title" => "",
|
||||
"og_description" => "",
|
||||
"og_image" => "",
|
||||
"twitter_card" => "summary_large_image",
|
||||
"robots_txt" => "User-agent: *\nDisallow: /admin/\nAllow: /"
|
||||
],
|
||||
'modules_path' => 'modules/seo',
|
||||
'modules_namespace' => 'Modules\\Seo',
|
||||
'modules_icon' => 'bi bi-search',
|
||||
'menu_side_bar' => true,
|
||||
'menu_nav_bar' => false,
|
||||
'modules_order' => 90,
|
||||
'modules_install_date' => date('Y-m-d H:i:s'),
|
||||
'modules_update_date' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$existingModule = Module::where('modules_name', 'seo')->first();
|
||||
|
||||
if (!$existingModule) {
|
||||
$module = Module::create($moduleData);
|
||||
|
||||
if (Capsule::schema()->hasTable('bus')) {
|
||||
$companies = Company::all();
|
||||
foreach ($companies as $company) {
|
||||
$exists = Capsule::table('bus_modules')
|
||||
->where('bus_modules_bus_id', $company->bus_id)
|
||||
->where('bus_modules_module_id', $module->modules_id)
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
Capsule::table('bus_modules')->insert([
|
||||
'bus_modules_bus_id' => $company->bus_id,
|
||||
'bus_modules_module_id' => $module->modules_id,
|
||||
'bus_modules_is_enabled' => true,
|
||||
'bus_modules_settings' => json_encode([]),
|
||||
'bus_modules_enabled_by' => 1,
|
||||
'bus_modules_enabled_at' => date('Y-m-d H:i:s'),
|
||||
'bus_modules_show_public_nav' => true,
|
||||
'bus_modules_show_dashboard_nav' => true,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$module = Module::where('modules_name', 'seo')->first();
|
||||
if ($module) {
|
||||
Capsule::table('bus_modules')->where('bus_modules_module_id', $module->modules_id)->delete();
|
||||
$module->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||
|
||||
/**
|
||||
* Migration 002: Create seo_redirects table.
|
||||
*/
|
||||
class CreateSeoRedirectsTable
|
||||
{
|
||||
/**
|
||||
* Run the migration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (!Capsule::schema()->hasTable('seo_redirects')) {
|
||||
Capsule::schema()->create('seo_redirects', function ($table) {
|
||||
$table->increments('redirects_id');
|
||||
$table->string('redirects_from');
|
||||
$table->string('redirects_to');
|
||||
$table->integer('redirects_code')->default(301); // 301 (Permanent) or 302 (Temporary)
|
||||
$table->boolean('redirects_is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Capsule::schema()->dropIfExists('seo_redirects');
|
||||
}
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "SEO"
|
||||
},
|
||||
"menu_seo": "SEO",
|
||||
"page_title": "SEO-Einstellungen",
|
||||
"general": "Allgemein",
|
||||
"social_media": "Soziale Medien",
|
||||
"robots_txt": "Robots.txt",
|
||||
"meta_title": "Standard Meta-Titel",
|
||||
"meta_description": "Standard Meta-Beschreibung",
|
||||
"meta_keywords": "Standard Meta-Schlüsselwörter",
|
||||
"og_title": "OG-Titel (Facebook/LinkedIn)",
|
||||
"og_description": "OG-Beschreibung",
|
||||
"og_image": "Standard OG-Bild-URL",
|
||||
"twitter_card": "Twitter-Karte",
|
||||
"summary": "Zusammenfassung",
|
||||
"summary_large_image": "Zusammenfassung großes Bild",
|
||||
"update_robots_file": "Auch physische Datei public/robots.txt aktualisieren",
|
||||
"save_success": "Einstellungen erfolgreich gespeichert.",
|
||||
"configuration": "SEO-Konfiguration",
|
||||
"meta_title_help": "Wird als Standardtitel verwendet, wenn auf der Seite nicht angegeben.",
|
||||
"meta_keywords_help": "Kommagetrennte Schlüsselwörter.",
|
||||
"og_section": "Open Graph (Facebook/LinkedIn)",
|
||||
"twitter_section": "Twitter-Karte",
|
||||
"twitter_card_type": "Kartentyp",
|
||||
"robots_txt_help": "Der folgende Inhalt wird unter",
|
||||
"robots_txt_content": "robots.txt Inhalt",
|
||||
"save_settings": "Einstellungen speichern",
|
||||
"title": "Titel"
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "SEO"
|
||||
},
|
||||
"menu_seo": "SEO",
|
||||
"page_title": "SEO Settings",
|
||||
"general": "General",
|
||||
"social_media": "Social Media",
|
||||
"robots_txt": "Robots.txt",
|
||||
"meta_title": "Default Meta Title",
|
||||
"meta_description": "Default Meta Description",
|
||||
"meta_keywords": "Default Meta Keywords",
|
||||
"og_title": "OG Title",
|
||||
"og_description": "OG Description",
|
||||
"og_image": "Default OG Image URL",
|
||||
"twitter_card": "Twitter Card",
|
||||
"summary": "Summary",
|
||||
"summary_large_image": "Summary Large Image",
|
||||
"update_robots_file": "Also update physical public/robots.txt file",
|
||||
"save_success": "Settings saved successfully.",
|
||||
"configuration": "SEO Configuration",
|
||||
"meta_title_help": "Used as default title if not specified on page.",
|
||||
"meta_keywords_help": "Comma separated keywords.",
|
||||
"og_section": "Open Graph (Facebook/LinkedIn)",
|
||||
"twitter_section": "Twitter Card",
|
||||
"twitter_card_type": "Card Type",
|
||||
"robots_txt_help": "The content below will be served at",
|
||||
"robots_txt_content": "robots.txt Content",
|
||||
"save_settings": "Save Settings",
|
||||
"title": "Title"
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "SEO"
|
||||
},
|
||||
"menu_seo": "SEO",
|
||||
"page_title": "Configuraciones de SEO",
|
||||
"general": "General",
|
||||
"social_media": "Redes Sociales",
|
||||
"robots_txt": "Robots.txt",
|
||||
"meta_title": "Título Meta Predeterminado",
|
||||
"meta_description": "Descripción Meta Predeterminada",
|
||||
"meta_keywords": "Palabras Clave Meta Predeterminadas",
|
||||
"og_title": "Título OG (Facebook/LinkedIn)",
|
||||
"og_description": "Descripción OG",
|
||||
"og_image": "URL de la Imagen OG Predeterminada",
|
||||
"twitter_card": "Tarjeta de Twitter",
|
||||
"summary": "Resumen",
|
||||
"summary_large_image": "Resumen con Imagen Grande",
|
||||
"update_robots_file": "Actualizar también el archivo físico public/robots.txt",
|
||||
"save_success": "Configuración guardada con éxito.",
|
||||
"configuration": "Configuración SEO",
|
||||
"meta_title_help": "Usado como título predeterminado si no se especifica en la página.",
|
||||
"meta_keywords_help": "Palabras clave separadas por comas.",
|
||||
"og_section": "Open Graph (Facebook/LinkedIn)",
|
||||
"twitter_section": "Tarjeta de Twitter",
|
||||
"twitter_card_type": "Tipo de Tarjeta",
|
||||
"robots_txt_help": "El contenido a continuación se servirá en",
|
||||
"robots_txt_content": "Contenido de robots.txt",
|
||||
"save_settings": "Guardar Configuración",
|
||||
"title": "Título"
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "SEO"
|
||||
},
|
||||
"menu_seo": "SEO",
|
||||
"page_title": "Paramètres SEO",
|
||||
"general": "Général",
|
||||
"social_media": "Réseaux Sociaux",
|
||||
"robots_txt": "Robots.txt",
|
||||
"meta_title": "Titre Meta par Défaut",
|
||||
"meta_description": "Description Meta par Défaut",
|
||||
"meta_keywords": "Mots-clés Meta par Défaut",
|
||||
"og_title": "Titre OG (Facebook/LinkedIn)",
|
||||
"og_description": "Description OG",
|
||||
"og_image": "URL de l'Image OG par Défaut",
|
||||
"twitter_card": "Carte Twitter",
|
||||
"summary": "Résumé",
|
||||
"summary_large_image": "Résumé Grande Image",
|
||||
"update_robots_file": "Mettre à jour également le fichier physique public/robots.txt",
|
||||
"save_success": "Paramètres enregistrés avec succès.",
|
||||
"configuration": "Configuration SEO",
|
||||
"meta_title_help": "Utilisé comme titre par défaut si non spécifié sur la page.",
|
||||
"meta_keywords_help": "Mots-clés séparés par des virgules.",
|
||||
"og_section": "Open Graph (Facebook/LinkedIn)",
|
||||
"twitter_section": "Carte Twitter",
|
||||
"twitter_card_type": "Type de carte",
|
||||
"robots_txt_help": "Le contenu ci-dessous sera servi à",
|
||||
"robots_txt_content": "Contenu robots.txt",
|
||||
"save_settings": "Enregistrer les paramètres",
|
||||
"title": "Titre"
|
||||
}
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "SEO"
|
||||
},
|
||||
"title": "Configurações de SEO",
|
||||
"general": "Geral",
|
||||
"social_media": "Redes Sociais",
|
||||
"robots_txt": "Robots.txt",
|
||||
"meta_title": "Título Meta Padrão",
|
||||
"meta_description": "Descrição Meta Padrão",
|
||||
"meta_keywords": "Palavras-chave Meta Padrão",
|
||||
"og_title": "Título OG (Facebook/LinkedIn)",
|
||||
"og_description": "Descrição OG",
|
||||
"og_image": "URL da Imagem OG Padrão",
|
||||
"twitter_card": "Twitter Card",
|
||||
"summary": "Resumo",
|
||||
"summary_large_image": "Resumo com Imagem Grande",
|
||||
"update_robots_file": "Atualizar também o arquivo físico public/robots.txt",
|
||||
"save_success": "Configurações salvas com sucesso.",
|
||||
"configuration": "Configuração de SEO",
|
||||
"meta_title_help": "Usado como título padrão se não especificado na página.",
|
||||
"meta_keywords_help": "Palavras-chave separadas por vírgula.",
|
||||
"og_section": "Open Graph (Facebook/LinkedIn)",
|
||||
"twitter_section": "Cartão do Twitter",
|
||||
"twitter_card_type": "Tipo de Cartão",
|
||||
"robots_txt_help": "O conteúdo abaixo será servido em",
|
||||
"robots_txt_content": "Conteúdo do robots.txt",
|
||||
"save_settings": "Salvar Configurações",
|
||||
"menu_seo": "SEO",
|
||||
"page_title": "Configurações de SEO",
|
||||
"google_tools": "Ferramentas do Google",
|
||||
"google_analytics_id": "ID do Google Analytics (G-XXXXX)",
|
||||
"google_site_verification": "Código de Verificação do Google Search Console",
|
||||
"serp_preview": "Prévia do Google (SERP)",
|
||||
"og_image_help": "Recomendado: 1200x630px. Se vazio, o sistema tentará usar a imagem do conteúdo.",
|
||||
"og_image_current": "Imagem Atual",
|
||||
"og_image_new": "Alterar Imagem",
|
||||
"select_image": "Selecionar Imagem",
|
||||
"structured_data": "Dados Estruturados (JSON-LD)",
|
||||
"json_ld_type": "Tipo de Esquema",
|
||||
"json_ld_none": "Nenhum",
|
||||
"json_ld_organization": "Organização",
|
||||
"json_ld_local_business": "Negócio Local",
|
||||
"json_ld_name": "Nome da Organização/Negócio",
|
||||
"json_ld_description": "Descrição Curta",
|
||||
"json_ld_url": "URL Oficial",
|
||||
"json_ld_contact": "Informações de Contato",
|
||||
"json_ld_phone": "Telefone",
|
||||
"json_ld_email": "E-mail",
|
||||
"json_ld_address": "Endereço (Rua, Nº)",
|
||||
"json_ld_city": "Cidade",
|
||||
"json_ld_state": "Estado/UF",
|
||||
"json_ld_zip": "CEP",
|
||||
"json_ld_country": "País",
|
||||
"json_ld_social_links": "Links de Redes Sociais (um por linha)",
|
||||
"redirects": "Redirecionamentos",
|
||||
"add_redirect": "Adicionar Redirecionamento",
|
||||
"redirect_from": "De (Caminho relativo, ex: /página-antiga)",
|
||||
"redirect_to": "Para (Caminho ou URL completa)",
|
||||
"redirect_code": "Código HTTP",
|
||||
"redirect_permanent": "301 (Permanente)",
|
||||
"redirect_temporary": "302 (Temporário)",
|
||||
"redirect_added": "Redirecionamento adicionado com sucesso!",
|
||||
"redirect_deleted": "Redirecionamento excluído com sucesso!",
|
||||
"no_redirects": "Nenhum redirecionamento configurado.",
|
||||
"seo_health": "Saúde do SEO",
|
||||
"health_good": "Bom",
|
||||
"health_warning": "Atenção",
|
||||
"health_error": "Crítico",
|
||||
"health_title_length": "Comprimento do Título",
|
||||
"health_desc_length": "Comprimento da Descrição",
|
||||
"health_keywords_presence": "Palavras-chave",
|
||||
"sitemap": "Sitemap XML",
|
||||
"sitemap_configuration": "Configuração do Sitemap",
|
||||
"sitemap_include_static": "Incluir Páginas Estáticas",
|
||||
"sitemap_include_blog": "Incluir Posts do Blog",
|
||||
"sitemap_include_news": "Incluir Notícias",
|
||||
"sitemap_include_guide": "Incluir Guia Comercial",
|
||||
"sitemap_include_events": "Incluir Guia de Eventos",
|
||||
"sitemap_include_faq": "Incluir FAQ",
|
||||
"sitemap_include_expositor": "Incluir Expositor (Vitrine)",
|
||||
"sitemap_include_albums": "Incluir Álbuns de Fotos",
|
||||
"sitemap_include_pages": "Incluir Páginas Personalizadas",
|
||||
"sitemap_include_photoessays": "Incluir Ensaios Fotográficos",
|
||||
"sitemap_frequency": "Frequência de Atualização",
|
||||
"sitemap_priority": "Prioridade Padrão",
|
||||
"daily": "Diário",
|
||||
"weekly": "Semanal",
|
||||
"monthly": "Mensal",
|
||||
"always": "Sempre",
|
||||
"hourly": "De hora em hora",
|
||||
"yearly": "Anual",
|
||||
"never": "Nunca",
|
||||
"ai_generate": "Gerar com IA",
|
||||
"ai_generating": "Gerando...",
|
||||
"ai_success": "Sugestões de IA geradas!",
|
||||
"ai_error": "Erro ao contatar IA.",
|
||||
"ai_prompt": "Analisando site e gerando tags otimizadas..."
|
||||
}
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "seo",
|
||||
"display_name": "SEO Manager",
|
||||
"description": "Gerenciamento de configurações globais de SEO, Sitemap e Robots.txt",
|
||||
"version": "1.0.0",
|
||||
"author": "SysCMS Team",
|
||||
"dependencies": [],
|
||||
"permissions": [
|
||||
"seo.view",
|
||||
"seo.manage"
|
||||
],
|
||||
"settings": {
|
||||
"meta_title": "SysCMS",
|
||||
"meta_description": "",
|
||||
"meta_keywords": "",
|
||||
"og_title": "",
|
||||
"og_description": "",
|
||||
"og_image": "",
|
||||
"twitter_card": "summary_large_image",
|
||||
"robots_txt": "User-agent: *\nDisallow: /admin/\nAllow: /"
|
||||
},
|
||||
"assets": {
|
||||
"css": [],
|
||||
"js": []
|
||||
},
|
||||
"routes": {
|
||||
"web": "routes/web.php",
|
||||
"api": "routes/api.php"
|
||||
},
|
||||
"menu": {
|
||||
"dashboard": {
|
||||
"icon": "bi bi-search",
|
||||
"label": "seo.menu.dashboard",
|
||||
"route": "admin/seo",
|
||||
"order": 90
|
||||
}
|
||||
},
|
||||
"views": "views",
|
||||
"locales": "locales",
|
||||
"module_ative": true,
|
||||
"module_public": true,
|
||||
"module_blocked": false,
|
||||
"ready": true,
|
||||
"tables": []
|
||||
}
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use Kernel\Application;
|
||||
|
||||
return function (Application $app) {
|
||||
// Admin routes (super-admin only or based on permissions)
|
||||
$app->addRoute('GET', '/admin/seo', [\Modules\Seo\Controllers\SeoController::class, 'index']);
|
||||
$app->addRoute('POST', '/admin/seo/save', [\Modules\Seo\Controllers\SeoController::class, 'save']);
|
||||
$app->addRoute('GET', '/admin/seo/ai-generate', [\Modules\Seo\Controllers\SeoController::class, 'generateAi']);
|
||||
$app->addRoute('POST', '/admin/seo/redirect/add', [\Modules\Seo\Controllers\SeoController::class, 'addRedirect']);
|
||||
$app->addRoute('GET', '/admin/seo/redirect/delete/{id}', [\Modules\Seo\Controllers\SeoController::class, 'deleteRedirect']);
|
||||
$app->addRoute('GET', '/admin/seo/manual', [\Modules\Seo\Controllers\SeoController::class, 'manual']);
|
||||
|
||||
// Public routes
|
||||
$app->addRoute('GET', '/sitemap.xml', [\Modules\Seo\Controllers\SeoController::class, 'sitemap']);
|
||||
$app->addRoute('GET', '/robots.txt', [\Modules\Seo\Controllers\SeoController::class, 'robots']);
|
||||
};
|
||||
Executable
+794
@@ -0,0 +1,794 @@
|
||||
{% extends "layouts/dashboard.twig" %}
|
||||
|
||||
{% block title %}{{ page_title }} - {{ platformName }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.serp-preview {
|
||||
max-width: 600px;
|
||||
padding: 15px;
|
||||
border: 1px solid #dfe1e5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
font-family: arial, sans-serif;
|
||||
}
|
||||
.serp-url {
|
||||
color: #202124;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.serp-title {
|
||||
color: #1a0dab;
|
||||
font-size: 20px;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.serp-title:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.serp-description {
|
||||
color: #4d5156;
|
||||
font-size: 14px;
|
||||
line-height: 1.58;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.health-badge {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
}
|
||||
.btn-outline-purple {
|
||||
color: #6f42c1;
|
||||
border-color: #6f42c1;
|
||||
}
|
||||
.btn-outline-purple:hover {
|
||||
background-color: #6f42c1;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">{{ page_title }}</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="{{ url('admin/dashboard') }}">{{ __('layout.dashboard') }}</a></li>
|
||||
<li class="breadcrumb-item active">{{ page_title }}</li>
|
||||
</ol>
|
||||
|
||||
{% include '@seo/admin/nav.twig' ignore missing %}
|
||||
|
||||
{% if flash_messages.success %}
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ flash_messages.success }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="bi bi-search me-1"></i>
|
||||
{{ __('seo.configuration') }}
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" id="btn-ai-generate" class="btn btn-outline-purple btn-sm">
|
||||
<i class="bi bi-cpu me-1"></i> {{ __('seo.ai_generate') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="document.getElementById('seoSettingsForm').submit();">
|
||||
<i class="bi bi-save me-1"></i> {{ __('seo.save_settings') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="seoSettingsForm" action="{{ url('/admin/seo/save') }}" method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ session('csrf_token') }}">
|
||||
<input type="hidden" name="_token" value="{{ session('_token') }}">
|
||||
|
||||
<ul class="nav nav-tabs mb-3" id="seoTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="general-tab" data-bs-toggle="tab" data-bs-target="#general" type="button" role="tab">{{ __('seo.general') }}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="social-tab" data-bs-toggle="tab" data-bs-target="#social" type="button" role="tab">{{ __('seo.social_media') }}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="structured-tab" data-bs-toggle="tab" data-bs-target="#structured" type="button" role="tab">{{ __('seo.structured_data') }}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="google-tab" data-bs-toggle="tab" data-bs-target="#google" type="button" role="tab">{{ __('seo.google_tools') }}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="sitemap-tab" data-bs-toggle="tab" data-bs-target="#sitemap" type="button" role="tab">{{ __('seo.sitemap') }}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="redirects-tab" data-bs-toggle="tab" data-bs-target="#redirects" type="button" role="tab">{{ __('seo.redirects') }}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="robots-tab" data-bs-toggle="tab" data-bs-target="#robots" type="button" role="tab">{{ __('seo.robots_txt') }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="seoTabsContent">
|
||||
<!-- General Tab -->
|
||||
<div class="tab-pane fade show active" id="general" role="tabpanel">
|
||||
<div class="mb-3">
|
||||
<label for="meta_title" class="form-label fw-bold">{{ __('seo.meta_title') }}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="meta_title" name="meta_title" value="{{ settings.meta_title }}" onkeyup="updatePreview()">
|
||||
<span class="input-group-text" id="title-counter">0</span>
|
||||
</div>
|
||||
<div class="form-text">{{ __('seo.meta_title_help') }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="meta_description" class="form-label fw-bold">{{ __('seo.meta_description') }}</label>
|
||||
<div class="input-group">
|
||||
<textarea class="form-control" id="meta_description" name="meta_description" rows="3" onkeyup="updatePreview()">{{ settings.meta_description }}</textarea>
|
||||
<span class="input-group-text" id="desc-counter">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="meta_keywords" class="form-label fw-bold">{{ __('seo.meta_keywords') }}</label>
|
||||
<input type="text" class="form-control" id="meta_keywords" name="meta_keywords" value="{{ settings.meta_keywords }}">
|
||||
<div class="form-text">{{ __('seo.meta_keywords_help') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h6 class="fw-bold mb-3">{{ __('seo.serp_preview') }}</h6>
|
||||
<div class="serp-preview shadow-sm">
|
||||
<div class="serp-url">{{ url('/') }}</div>
|
||||
<div class="serp-title" id="preview-title">Título do Site</div>
|
||||
<div class="serp-description" id="preview-description">Descrição meta aparecerá aqui para visualização nos resultados de busca do Google.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Media Tab -->
|
||||
<div class="tab-pane fade" id="social" role="tabpanel">
|
||||
<h5 class="mt-3">{{ __('seo.og_section') }}</h5>
|
||||
<div class="mb-3">
|
||||
<label for="og_title" class="form-label">{{ __('seo.og_title') }}</label>
|
||||
<input type="text" class="form-control" id="og_title" name="og_title" value="{{ settings.og_title }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="og_description" class="form-label">{{ __('seo.og_description') }}</label>
|
||||
<textarea class="form-control" id="og_description" name="og_description" rows="2">{{ settings.og_description }}</textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-block">{{ __('seo.og_image') }}</label>
|
||||
{% if settings.og_image %}
|
||||
<div class="mb-2">
|
||||
<img src="/storage/uploads/seo/{{ settings.og_image }}" class="img-thumbnail" style="max-height: 150px;">
|
||||
<div class="small text-muted">{{ __('seo.og_image_current') }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<input type="file" class="form-control" name="og_image_file" accept="image/*">
|
||||
<div class="form-text">{{ __('seo.og_image_help') }}</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h5 class="mt-3">{{ __('seo.twitter_section') }}</h5>
|
||||
<div class="mb-3">
|
||||
<label for="twitter_card" class="form-label">{{ __('seo.twitter_card_type') }}</label>
|
||||
<select class="form-select" id="twitter_card" name="twitter_card">
|
||||
<option value="summary" {{ settings.twitter_card == 'summary' ? 'selected' : '' }}>{{ __('seo.summary') }}</option>
|
||||
<option value="summary_large_image" {{ settings.twitter_card == 'summary_large_image' ? 'selected' : '' }}>{{ __('seo.summary_large_image') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Structured Data Tab -->
|
||||
<div class="tab-pane fade" id="structured" role="tabpanel">
|
||||
<h5 class="mt-3">{{ __('seo.structured_data') }} (JSON-LD)</h5>
|
||||
<div class="mb-3">
|
||||
<label for="json_ld_type" class="form-label">{{ __('seo.json_ld_type') }}</label>
|
||||
<select class="form-select" id="json_ld_type" name="json_ld_type" onchange="toggleJsonLdFields()">
|
||||
<option value="none" {{ settings.json_ld_type == 'none' ? 'selected' : '' }}>{{ __('seo.json_ld_none') }}</option>
|
||||
<option value="Organization" {{ settings.json_ld_type == 'Organization' ? 'selected' : '' }}>{{ __('seo.json_ld_organization') }}</option>
|
||||
<option value="LocalBusiness" {{ settings.json_ld_type == 'LocalBusiness' ? 'selected' : '' }}>{{ __('seo.json_ld_local_business') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="json-ld-fields" style="{{ settings.json_ld_type == 'none' or not settings.json_ld_type ? 'display: none;' : '' }}">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="json_ld_name" class="form-label">{{ __('seo.json_ld_name') }}</label>
|
||||
<input type="text" class="form-control" id="json_ld_name" name="json_ld_name" value="{{ settings.json_ld_name }}">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="json_ld_url" class="form-label">{{ __('seo.json_ld_url') }}</label>
|
||||
<input type="url" class="form-control" id="json_ld_url" name="json_ld_url" value="{{ settings.json_ld_url ?: url('/') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="json_ld_description" class="form-label">{{ __('seo.json_ld_description') }}</label>
|
||||
<textarea class="form-control" id="json_ld_description" name="json_ld_description" rows="2">{{ settings.json_ld_description }}</textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="json_ld_phone" class="form-label">{{ __('seo.json_ld_phone') }}</label>
|
||||
<input type="text" class="form-control" id="json_ld_phone" name="json_ld_phone" value="{{ settings.json_ld_phone }}">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="json_ld_email" class="form-label">{{ __('seo.json_ld_email') }}</label>
|
||||
<input type="email" class="form-control" id="json_ld_email" name="json_ld_email" value="{{ settings.json_ld_email }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="json_ld_address" class="form-label">{{ __('seo.json_ld_address') }}</label>
|
||||
<input type="text" class="form-control" id="json_ld_address" name="json_ld_address" value="{{ settings.json_ld_address }}">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="json_ld_city" class="form-label">{{ __('seo.json_ld_city') }}</label>
|
||||
<input type="text" class="form-control" id="json_ld_city" name="json_ld_city" value="{{ settings.json_ld_city }}">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="json_ld_state" class="form-label">{{ __('seo.json_ld_state') }}</label>
|
||||
<input type="text" class="form-control" id="json_ld_state" name="json_ld_state" value="{{ settings.json_ld_state }}">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="json_ld_zip" class="form-label">{{ __('seo.json_ld_zip') }}</label>
|
||||
<input type="text" class="form-control" id="json_ld_zip" name="json_ld_zip" value="{{ settings.json_ld_zip }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="json_ld_social_links" class="form-label">{{ __('seo.json_ld_social_links') }}</label>
|
||||
<textarea class="form-control" id="json_ld_social_links" name="json_ld_social_links" rows="3" placeholder="https://facebook.com/yourpage https://twitter.com/yourprofile">{{ settings.json_ld_social_links }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Google Tools Tab -->
|
||||
<div class="tab-pane fade" id="google" role="tabpanel">
|
||||
<div class="mb-3 mt-3">
|
||||
<label for="google_analytics_id" class="form-label">{{ __('seo.google_analytics_id') }}</label>
|
||||
<input type="text" class="form-control" id="google_analytics_id" name="google_analytics_id" value="{{ settings.google_analytics_id }}" placeholder="G-XXXXXXXXXX">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="google_site_verification" class="form-label">{{ __('seo.google_site_verification') }}</label>
|
||||
<input type="text" class="form-control" id="google_site_verification" name="google_site_verification" value="{{ settings.google_site_verification }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sitemap Tab -->
|
||||
<div class="tab-pane fade" id="sitemap" role="tabpanel">
|
||||
<div class="alert alert-light border border-info mt-3">
|
||||
<i class="bi bi-info-circle text-info me-2"></i>
|
||||
Configure a visibilidade, frequência de atualização e prioridade para cada tipo de conteúdo do seu site.
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 40px;"></th>
|
||||
<th>Tipo de Conteúdo</th>
|
||||
<th style="width: 200px;">Frequência</th>
|
||||
<th style="width: 150px;">Prioridade (0.1 - 1.0)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{# Static Pages #}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="sitemap_include_static" value="1" {{ settings.sitemap_include_static|default('1') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">Páginas Estáticas</div>
|
||||
<div class="small text-muted">Home, Sobre, Contato, etc.</div>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" name="sitemap_freq_static">
|
||||
{% set freqs = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'] %}
|
||||
{% for freq in freqs %}
|
||||
<option value="{{ freq }}" {{ settings.sitemap_freq_static|default('weekly') == freq ? 'selected' : '' }}>{{ __('seo.' ~ freq) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" name="sitemap_prio_static" value="{{ settings.sitemap_prio_static|default('1.0') }}" step="0.1" min="0.1" max="1.0">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{# Blog Posts #}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="sitemap_include_blog" value="1" {{ settings.sitemap_include_blog|default('1') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">Blog</div>
|
||||
<div class="small text-muted">Artigos e postagens individuais</div>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" name="sitemap_freq_blog">
|
||||
{% for freq in freqs %}
|
||||
<option value="{{ freq }}" {{ settings.sitemap_freq_blog|default('daily') == freq ? 'selected' : '' }}>{{ __('seo.' ~ freq) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" name="sitemap_prio_blog" value="{{ settings.sitemap_prio_blog|default('0.8') }}" step="0.1" min="0.1" max="1.0">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{# News #}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="sitemap_include_news" value="1" {{ settings.sitemap_include_news|default('1') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">Notícias</div>
|
||||
<div class="small text-muted">Últimas notícias e comunicados</div>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" name="sitemap_freq_news">
|
||||
{% for freq in freqs %}
|
||||
<option value="{{ freq }}" {{ settings.sitemap_freq_news|default('daily') == freq ? 'selected' : '' }}>{{ __('seo.' ~ freq) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" name="sitemap_prio_news" value="{{ settings.sitemap_prio_news|default('0.7') }}" step="0.1" min="0.1" max="1.0">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{# Commercial Guide #}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="sitemap_include_guide" value="1" {{ settings.sitemap_include_guide|default('1') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">Guia Comercial</div>
|
||||
<div class="small text-muted">Empresas e perfis comerciais</div>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" name="sitemap_freq_guide">
|
||||
{% for freq in freqs %}
|
||||
<option value="{{ freq }}" {{ settings.sitemap_freq_guide|default('monthly') == freq ? 'selected' : '' }}>{{ __('seo.' ~ freq) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" name="sitemap_prio_guide" value="{{ settings.sitemap_prio_guide|default('0.7') }}" step="0.1" min="0.1" max="1.0">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{# Events Guide #}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="sitemap_include_events" value="1" {{ settings.sitemap_include_events|default('1') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">Guia de Eventos</div>
|
||||
<div class="small text-muted">Eventos públicos e datas festivas</div>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" name="sitemap_freq_events">
|
||||
{% for freq in freqs %}
|
||||
<option value="{{ freq }}" {{ settings.sitemap_freq_events|default('daily') == freq ? 'selected' : '' }}>{{ __('seo.' ~ freq) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" name="sitemap_prio_events" value="{{ settings.sitemap_prio_events|default('0.7') }}" step="0.1" min="0.1" max="1.0">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{# FAQ #}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="sitemap_include_faq" value="1" {{ settings.sitemap_include_faq|default('1') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">FAQ</div>
|
||||
<div class="small text-muted">Perguntas frequentes</div>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" name="sitemap_freq_faq">
|
||||
{% for freq in freqs %}
|
||||
<option value="{{ freq }}" {{ settings.sitemap_freq_faq|default('monthly') == freq ? 'selected' : '' }}>{{ __('seo.' ~ freq) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" name="sitemap_prio_faq" value="{{ settings.sitemap_prio_faq|default('0.5') }}" step="0.1" min="0.1" max="1.0">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{# Expositor #}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="sitemap_include_expositor" value="1" {{ settings.sitemap_include_expositor|default('1') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">Expositor (Vitrine)</div>
|
||||
<div class="small text-muted">Produtos, serviços e categorias da vitrine</div>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" name="sitemap_freq_expositor">
|
||||
{% for freq in freqs %}
|
||||
<option value="{{ freq }}" {{ settings.sitemap_freq_expositor|default('daily') == freq ? 'selected' : '' }}>{{ __('seo.' ~ freq) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" name="sitemap_prio_expositor" value="{{ settings.sitemap_prio_expositor|default('0.8') }}" step="0.1" min="0.1" max="1.0">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{# Photo Albums #}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="sitemap_include_albums" value="1" {{ settings.sitemap_include_albums|default('1') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">Álbuns de Fotos</div>
|
||||
<div class="small text-muted">Galerias e álbuns de fotos públicos</div>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" name="sitemap_freq_albums">
|
||||
{% for freq in freqs %}
|
||||
<option value="{{ freq }}" {{ settings.sitemap_freq_albums|default('monthly') == freq ? 'selected' : '' }}>{{ __('seo.' ~ freq) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" name="sitemap_prio_albums" value="{{ settings.sitemap_prio_albums|default('0.6') }}" step="0.1" min="0.1" max="1.0">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{# Custom Pages #}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="sitemap_include_pages" value="1" {{ settings.sitemap_include_pages|default('1') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">Páginas Personalizadas</div>
|
||||
<div class="small text-muted">Páginas criadas via PageBuilder</div>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" name="sitemap_freq_pages">
|
||||
{% for freq in freqs %}
|
||||
<option value="{{ freq }}" {{ settings.sitemap_freq_pages|default('monthly') == freq ? 'selected' : '' }}>{{ __('seo.' ~ freq) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" name="sitemap_prio_pages" value="{{ settings.sitemap_prio_pages|default('0.7') }}" step="0.1" min="0.1" max="1.0">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{# Photo Essays #}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="sitemap_include_photoessays" value="1" {{ settings.sitemap_include_photoessays|default('1') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">Ensaios Fotográficos</div>
|
||||
<div class="small text-muted">Ensaios e galerias do PhotoEssay</div>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" name="sitemap_freq_photoessays">
|
||||
{% for freq in freqs %}
|
||||
<option value="{{ freq }}" {{ settings.sitemap_freq_photoessays|default('monthly') == freq ? 'selected' : '' }}>{{ __('seo.' ~ freq) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" class="form-control form-control-sm" name="sitemap_prio_photoessays" value="{{ settings.sitemap_prio_photoessays|default('0.6') }}" step="0.1" min="0.1" max="1.0">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redirects Tab (Handled separately) -->
|
||||
<div class="tab-pane fade" id="redirects" role="tabpanel">
|
||||
<div class="alert alert-warning mt-3">
|
||||
<i class="bi bi-exclamation-triangle"></i> Redirecionamentos são aplicados globalmente. Tenha cuidado ao configurar caminhos que já existem.
|
||||
</div>
|
||||
<div class="mb-3 d-flex justify-content-end">
|
||||
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#addRedirectModal">
|
||||
<i class="bi bi-plus-lg"></i> {{ __('seo.add_redirect') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __('seo.redirect_from') }}</th>
|
||||
<th>{{ __('seo.redirect_to') }}</th>
|
||||
<th>{{ __('seo.redirect_code') }}</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for redirect in redirects %}
|
||||
<tr>
|
||||
<td><code>{{ redirect.redirects_from }}</code></td>
|
||||
<td><code>{{ redirect.redirects_to }}</code></td>
|
||||
<td><span class="badge bg-secondary">{{ redirect.redirects_code }}</span></td>
|
||||
<td>
|
||||
<a href="{{ url('admin/seo/redirect/delete/' ~ redirect.redirects_id) }}" class="text-danger" onclick="return confirm('Tem certeza?')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">{{ __('seo.no_redirects') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Robots.txt Tab -->
|
||||
<div class="tab-pane fade" id="robots" role="tabpanel">
|
||||
<div class="alert alert-info mt-3">
|
||||
<i class="bi bi-info-circle"></i> {{ __('seo.robots_txt_help') }} <code>/robots.txt</code>.
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="robots_txt" class="form-label">{{ __('seo.robots_txt_content') }}</label>
|
||||
<textarea class="form-control font-monospace" id="robots_txt" name="robots_txt" rows="10">{{ settings.robots_txt }}</textarea>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" value="1" id="update_robots_file" name="update_robots_file">
|
||||
<label class="form-check-label" for="update_robots_file">
|
||||
{{ __('seo.update_robots_file') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Botão removido daqui pois o superior no card-header é o que está funcionando #}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-4">
|
||||
<!-- SEO Health Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<i class="bi bi-heart-pulse me-1"></i>
|
||||
{{ __('seo.seo_health') }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span>{{ __('seo.health_title_length') }}</span>
|
||||
<span id="health-title-badge" class="badge health-badge bg-secondary">...</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span>{{ __('seo.health_desc_length') }}</span>
|
||||
<span id="health-desc-badge" class="badge health-badge bg-secondary">...</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-0">
|
||||
<span>{{ __('seo.health_keywords_presence') }}</span>
|
||||
<span id="health-keywords-badge" class="badge health-badge bg-secondary">...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<i class="bi bi-link-45deg me-1"></i>
|
||||
Links Gerados
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="small fw-bold text-muted d-block mb-1">Sitemap XML</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control form-control-sm" value="{{ url('sitemap.xml') }}" readonly>
|
||||
<a href="{{ url('sitemap.xml') }}" target="_blank" class="btn btn-outline-secondary btn-sm"><i class="bi bi-box-arrow-up-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="small fw-bold text-muted d-block mb-1">Robots.txt</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control form-control-sm" value="{{ url('robots.txt') }}" readonly>
|
||||
<a href="{{ url('robots.txt') }}" target="_blank" class="btn btn-outline-secondary btn-sm"><i class="bi bi-box-arrow-up-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Dicas de SEO
|
||||
</div>
|
||||
<div class="card-body small">
|
||||
<ul class="ps-3 mb-0">
|
||||
<li class="mb-2"><strong>Título:</strong> Mantenha entre 50-60 caracteres.</li>
|
||||
<li class="mb-2"><strong>Descrição:</strong> Idealmente entre 150-160 caracteres.</li>
|
||||
<li class="mb-2"><strong>Schema.org:</strong> Ajuda o Google a entender quem você é.</li>
|
||||
<li><strong>Redirecionamentos:</strong> Evite erros 404 ao mudar URLs.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Redirect Modal -->
|
||||
<div class="modal fade" id="addRedirectModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form action="{{ url('admin/seo/redirect/add') }}" method="POST">
|
||||
{{ csrf_field()|raw }}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ __('seo.add_redirect') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="redirects_from" class="form-label">{{ __('seo.redirect_from') }}</label>
|
||||
<input type="text" class="form-control" id="redirects_from" name="redirects_from" placeholder="/pagina-antiga" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="redirects_to" class="form-label">{{ __('seo.redirect_to') }}</label>
|
||||
<input type="text" class="form-control" id="redirects_to" name="redirects_to" placeholder="/nova-pagina" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="redirects_code" class="form-label">{{ __('seo.redirect_code') }}</label>
|
||||
<select class="form-select" id="redirects_code" name="redirects_code">
|
||||
<option value="301">{{ __('seo.redirect_permanent') }}</option>
|
||||
<option value="302">{{ __('seo.redirect_temporary') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Fechar</button>
|
||||
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function updatePreview() {
|
||||
const titleInput = document.getElementById('meta_title');
|
||||
const descInput = document.getElementById('meta_description');
|
||||
const keywordsInput = document.getElementById('meta_keywords');
|
||||
|
||||
const title = titleInput.value || 'Título do Site';
|
||||
const desc = descInput.value || 'Descrição meta aparecerá aqui para visualização nos resultados de busca do Google.';
|
||||
|
||||
document.getElementById('preview-title').innerText = title;
|
||||
document.getElementById('preview-description').innerText = desc;
|
||||
|
||||
// Counters
|
||||
const titleLen = titleInput.value.length;
|
||||
const descLen = descInput.value.length;
|
||||
|
||||
document.getElementById('title-counter').innerText = titleLen;
|
||||
document.getElementById('desc-counter').innerText = descLen;
|
||||
|
||||
// Health Check
|
||||
const titleBadge = document.getElementById('health-title-badge');
|
||||
if (titleLen === 0) {
|
||||
titleBadge.innerText = '{{ __('seo.health_error') }}';
|
||||
titleBadge.className = 'badge health-badge bg-danger';
|
||||
} else if (titleLen < 30 || titleLen > 65) {
|
||||
titleBadge.innerText = '{{ __('seo.health_warning') }}';
|
||||
titleBadge.className = 'badge health-badge bg-warning text-dark';
|
||||
} else {
|
||||
titleBadge.innerText = '{{ __('seo.health_good') }}';
|
||||
titleBadge.className = 'badge health-badge bg-success';
|
||||
}
|
||||
|
||||
const descBadge = document.getElementById('health-desc-badge');
|
||||
if (descLen === 0) {
|
||||
descBadge.innerText = '{{ __('seo.health_error') }}';
|
||||
descBadge.className = 'badge health-badge bg-danger';
|
||||
} else if (descLen < 120 || descLen > 165) {
|
||||
descBadge.innerText = '{{ __('seo.health_warning') }}';
|
||||
descBadge.className = 'badge health-badge bg-warning text-dark';
|
||||
} else {
|
||||
descBadge.innerText = '{{ __('seo.health_good') }}';
|
||||
descBadge.className = 'badge health-badge bg-success';
|
||||
}
|
||||
|
||||
const kwBadge = document.getElementById('health-keywords-badge');
|
||||
if (keywordsInput.value.trim() === '') {
|
||||
kwBadge.innerText = '{{ __('seo.health_warning') }}';
|
||||
kwBadge.className = 'badge health-badge bg-warning text-dark';
|
||||
} else {
|
||||
kwBadge.innerText = '{{ __('seo.health_good') }}';
|
||||
kwBadge.className = 'badge health-badge bg-success';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleJsonLdFields() {
|
||||
const type = document.getElementById('json_ld_type').value;
|
||||
const fields = document.getElementById('json-ld-fields');
|
||||
fields.style.display = type === 'none' ? 'none' : 'block';
|
||||
}
|
||||
|
||||
// Check hash for active tab
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (window.location.hash) {
|
||||
const tabId = window.location.hash.substring(1) + '-tab';
|
||||
const tab = document.getElementById(tabId);
|
||||
if (tab) {
|
||||
const bsTab = new bootstrap.Tab(tab);
|
||||
bsTab.show();
|
||||
}
|
||||
}
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// AI Generate Logic
|
||||
document.getElementById('btn-ai-generate').addEventListener('click', function() {
|
||||
const btn = this;
|
||||
const originalHtml = btn.innerHTML;
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> {{ __('seo.ai_generating') }}';
|
||||
|
||||
fetch('{{ url('/admin/seo/ai-generate') }}')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('meta_title').value = data.title;
|
||||
document.getElementById('meta_description').value = data.description;
|
||||
document.getElementById('meta_keywords').value = data.keywords;
|
||||
|
||||
// Robots.txt
|
||||
if (data.robots_txt) {
|
||||
document.getElementById('robots_txt').value = data.robots_txt;
|
||||
console.log('Robots.txt preenchido via IA');
|
||||
}
|
||||
|
||||
updatePreview();
|
||||
|
||||
// Alerta informativo
|
||||
const alertMsg = '{{ __('seo.ai_success') }}' + '\n\nAs tags Meta e o arquivo Robots.txt foram preenchidos com sugestões otimizadas.';
|
||||
alert(alertMsg);
|
||||
} else {
|
||||
alert('{{ __('seo.ai_error') }}');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('AI Error:', error);
|
||||
alert('{{ __('seo.ai_error') }}');
|
||||
})
|
||||
.finally(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalHtml;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
{% extends "layouts/dashboard.twig" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">{{ title }}</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="{{ url('admin/dashboard') }}">{{ __('dashboard.title') }}</a></li>
|
||||
<li class="breadcrumb-item active">SEO</li>
|
||||
</ol>
|
||||
|
||||
{% include '@seo/admin/nav.twig' ignore missing %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-3 d-none d-lg-block">
|
||||
<div class="list-group sticky-top" style="top: 100px; z-index: 1;">
|
||||
<a href="#visao-geral" class="list-group-item list-group-item-action">Visão Geral</a>
|
||||
<a href="#meta-tags" class="list-group-item list-group-item-action">Tags Meta</a>
|
||||
<a href="#redes-sociais" class="list-group-item list-group-item-action">Redes Sociais</a>
|
||||
<a href="#sitemap" class="list-group-item list-group-item-action">Sitemap XML</a>
|
||||
<a href="#redirecionamentos" class="list-group-item list-group-item-action">Redirecionamentos</a>
|
||||
<a href="#ia-seo" class="list-group-item list-group-item-action">IA de SEO</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-9">
|
||||
<!-- Visão Geral -->
|
||||
<div class="card shadow mb-4" id="visao-geral">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">1. Visão Geral</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>O módulo de <strong>SEO (Search Engine Optimization)</strong> centraliza as configurações que ajudam o seu site a ser encontrado e bem posicionado no Google e outros buscadores.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Meta Tags -->
|
||||
<div class="card shadow mb-4" id="meta-tags">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">2. Tags Meta (Geral)</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul>
|
||||
<li><strong>Título Meta:</strong> O título que aparece no Google. Ideal entre 50-60 caracteres.</li>
|
||||
<li><strong>Descrição Meta:</strong> O resumo do site nos buscadores. Ideal até 160 caracteres.</li>
|
||||
<li><strong>Palavras-chave:</strong> Termos que descrevem seu negócio.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sitemap -->
|
||||
<div class="card shadow mb-4" id="sitemap">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">3. Sitemap XML</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>O Sitemap é o "mapa" do seu site para os buscadores. Aqui você define quais módulos (Blog, Notícias, Guia) devem ser indexados e qual a frequência de atualização de cada um.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redirecionamentos -->
|
||||
<div class="card shadow mb-4" id="redirecionamentos">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">4. Redirecionamentos (301/302)</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Use esta ferramenta para redirecionar URLs antigas que não existem mais para novas páginas, evitando o erro 404 e mantendo a autoridade do link.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IA de SEO -->
|
||||
<div class="card shadow mb-4" id="ia-seo">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">5. IA de SEO</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>O botão <strong>"Gerar com IA"</strong> analisa todo o conteúdo do seu site localmente e sugere automaticamente as melhores Tags Meta e a estrutura do arquivo Robots.txt.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
<div class="mb-4 d-print-none">
|
||||
<div class="btn-group" role="group" aria-label="Navegação SEO">
|
||||
<a href="/admin/seo" class="btn btn-outline-primary {% if request_path == '/admin/seo' %}active{% endif %}">
|
||||
<i class="bi bi-search"></i> {{ __('seo.menu_seo') }}
|
||||
</a>
|
||||
<a href="/admin/seo/manual" class="btn btn-outline-info {% if request_path == '/admin/seo/manual' %}active{% endif %}">
|
||||
<i class="bi bi-book"></i> Manual
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user