feat: adiciona templates SEO

This commit is contained in:
Marcio Bevervanso 2026-05-05 11:30:03 -03:00
commit e824be6aba
125 changed files with 28674 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

BIN
Template-01/.DS_Store vendored Normal file

Binary file not shown.

20
Template-01/README.md Normal file
View file

@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/b9e0fe6d-c7c5-42ea-8fbe-61d65dc0914c
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

21
Template-01/index.html Normal file
View file

@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SEO Authority | Insights Técnicos e Estratégicos</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%232563eb'%3E%3Cpath d='M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5'/%3E%3C/svg%3E" />
<meta name="theme-color" content="#ffffff" />
</head>
<body>
<div id="root"></div>
<script type="text/javascript">
function googleTranslateElementInit() {
new google.translate.TranslateElement({pageLanguage: 'pt', includedLanguages: 'en,es,pt', layout: google.translate.TranslateElement.InlineLayout.SIMPLE, autoDisplay: false}, 'google_translate_element');
}
</script>
<script type="text/javascript" src="//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,6 @@
{
"name": "SEOMaster - Blog de Autoridade SEO",
"description": "Um blog completo focado em SEO, com estrutura otimizada, 10 artigos iniciais de alta qualidade e dashboard admin com criação de conteúdo por IA.",
"requestFramePermissions": [],
"majorCapabilities": []
}

5670
Template-01/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

41
Template-01/package.json Normal file
View file

@ -0,0 +1,41 @@
{
"name": "react-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "tsx server.ts",
"build": "vite build",
"preview": "vite preview",
"clean": "rm -rf dist",
"lint": "tsc --noEmit"
},
"dependencies": {
"@google/genai": "^1.51.0",
"@google/generative-ai": "^0.24.1",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"clsx": "^2.1.1",
"cors": "^2.8.6",
"dotenv": "^17.2.3",
"express": "^4.22.1",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"react-helmet-async": "^3.0.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.14.2",
"tailwind-merge": "^3.5.0",
"vite": "^6.2.3"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"autoprefixer": "^10.4.21",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.3"
}
}

View file

@ -0,0 +1,5 @@
User-agent: *
Allow: /
# Sitemaps
Sitemap: https://seu-dominio.com/sitemap.xml

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://seo-authority.digital/</loc>
<lastmod>2026-05-04</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://seo-authority.digital/sobre</loc>
<lastmod>2026-05-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://seo-authority.digital/contato</loc>
<lastmod>2026-05-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://seo-authority.digital/metodologia</loc>
<lastmod>2026-05-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://seo-authority.digital/artigo/futuro-do-seo-2026</loc>
<lastmod>2026-05-04</lastmod>
<priority>0.9</priority>
</url>
<url>
<loc>https://seo-authority.digital/artigo/eeat-guia-completo</loc>
<lastmod>2026-05-04</lastmod>
<priority>0.9</priority>
</url>
<url>
<loc>https://seo-authority.digital/artigo/sinais-de-usuario-ranking</loc>
<lastmod>2026-05-04</lastmod>
<priority>0.9</priority>
</url>
</urlset>

93
Template-01/server.ts Normal file
View file

@ -0,0 +1,93 @@
import express from 'express';
import { createServer as createViteServer } from 'vite';
import { GoogleGenerativeAI } from "@google/generative-ai";
import path from 'path';
import cors from 'cors';
async function startServer() {
const app = express();
const PORT = 3000;
app.use(express.json());
app.use(cors());
// Lazy Gemini Setup
let genAI: GoogleGenerativeAI | null = null;
const getAIModel = () => {
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
throw new Error("GEMINI_API_KEY environment variable is required");
}
if (!genAI) {
genAI = new GoogleGenerativeAI(apiKey);
}
return genAI.getGenerativeModel({ model: "gemini-1.5-flash-8b" });
};
// Translation API
app.post('/api/translate', async (req, res) => {
const { text, targetLang, context, isObject } = req.body;
try {
const model = getAIModel();
let prompt = "";
if (isObject) {
prompt = `Translate the following JSON object values to ${targetLang}.
Keep the keys exactly the same.
Preserve markdown formatting in values.
Return ONLY the translated JSON object.
Object:
${JSON.stringify(text)}`;
} else {
prompt = `Translate the following ${context || 'text'} to ${targetLang}.
Preserve all markdown formatting, technical terms, and HTML structures if present.
Do not add any commentary or prefix. Return only the translated string.
Content:
${text}`;
}
const result = await model.generateContent(prompt);
const response = await result.response;
let resultText = response.text().replace(/```json/g, '').replace(/```/g, '').trim();
if (isObject) {
try {
res.json({ translatedText: JSON.parse(resultText) });
} catch (e) {
console.error("Parse Error:", resultText);
res.json({ translatedText: text }); // Fallback to original if parse fails
}
} else {
res.json({ translatedText: resultText });
}
} catch (error) {
console.error("Translation Error:", error);
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to translate" });
}
});
// Vite Integration
if (process.env.NODE_ENV !== "production") {
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "spa",
});
app.use(vite.middlewares);
} else {
const distPath = path.join(process.cwd(), 'dist');
app.use(express.static(distPath));
app.get('*', (req, res) => {
res.sendFile(path.join(distPath, 'index.html'));
});
}
app.listen(PORT, "0.0.0.0", () => {
console.log(`Server running at http://0.0.0.0:${PORT}`);
});
}
startServer();

48
Template-01/src/App.tsx Normal file
View file

@ -0,0 +1,48 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
import Layout from './components/Layout';
import Home from './pages/Home';
import BlogPost from './pages/BlogPost';
import CategoryPage from './pages/CategoryPage';
import About from './pages/About';
import Methodology from './pages/Methodology';
import Privacy from './pages/Privacy';
import Terms from './pages/Terms';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';
import Bookmarks from './pages/Bookmarks';
import { LanguageProvider } from './contexts/LanguageContext';
import { BookmarksProvider } from './contexts/BookmarksContext';
export default function App() {
return (
<HelmetProvider>
<LanguageProvider>
<BookmarksProvider>
<Router>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/artigo/:slug" element={<BlogPost />} />
<Route path="/categoria/:categorySlug" element={<CategoryPage />} />
<Route path="/sobre" element={<About />} />
<Route path="/metodologia" element={<Methodology />} />
<Route path="/privacidade" element={<Privacy />} />
<Route path="/termos" element={<Terms />} />
<Route path="/contato" element={<Contact />} />
<Route path="/leituras-salvas" element={<Bookmarks />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Layout>
</Router>
</BookmarksProvider>
</LanguageProvider>
</HelmetProvider>
);
}

View file

@ -0,0 +1,113 @@
import { Link } from 'react-router-dom';
import { Article } from '../types';
import { motion } from 'motion/react';
import { Bookmark } from 'lucide-react';
import { useBookmarks } from '../contexts/BookmarksContext';
import { cn } from '../lib/utils';
interface ArticleCardProps {
article: Article;
featured?: boolean;
}
export default function ArticleCard({ article, featured = false }: ArticleCardProps) {
const { toggleBookmark, isBookmarked } = useBookmarks();
const bookmarked = isBookmarked(article.id);
if (featured) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="group relative"
>
<div className="relative overflow-hidden rounded-[32px] bg-slate-100 mb-10">
<Link to={`/artigo/${article.slug}`} className="block">
<img
src={article.image}
alt={article.title}
className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-105 aspect-[16/8]"
referrerPolicy="no-referrer"
/>
</Link>
<button
onClick={() => toggleBookmark(article.id)}
className={cn(
"absolute top-6 right-6 h-12 w-12 rounded-full border-2 border-white/20 backdrop-blur-md flex items-center justify-center transition-all",
bookmarked ? "bg-blue-600 border-blue-600 text-white" : "bg-black/10 text-white hover:bg-white hover:text-blue-600"
)}
>
<Bookmark size={20} fill={bookmarked ? "currentColor" : "none"} />
</button>
</div>
<div className="max-w-3xl">
<div className="flex items-center gap-4 text-[11px] font-bold uppercase tracking-[0.2em] text-blue-600 mb-6">
<span>{article.category}</span>
<span className="h-1 w-1 bg-slate-300 rounded-full" />
<span className="text-slate-400">{article.readTime} reading</span>
</div>
<Link to={`/artigo/${article.slug}`}>
<h2 className="text-4xl sm:text-5xl font-serif font-bold text-slate-950 leading-tight mb-6 hover:text-blue-600 transition-colors">
{article.title}
</h2>
</Link>
<p className="text-xl text-slate-500 leading-relaxed mb-8 line-clamp-3">
{article.excerpt}
</p>
<div className="flex items-center gap-4">
<img src={article.author.avatar} alt={article.author.name} className="h-12 w-12 rounded-full grayscale group-hover:grayscale-0 transition-all" />
<div>
<p className="text-sm font-bold text-slate-900">{article.author.name}</p>
<p className="text-xs text-slate-400 uppercase tracking-widest font-bold mt-1">Lead Strategist</p>
</div>
</div>
</div>
</motion.div>
);
}
return (
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
className="group relative"
>
<div className="relative aspect-[4/3] overflow-hidden rounded-2xl bg-slate-100 mb-6">
<Link to={`/artigo/${article.slug}`} className="block h-full">
<img
src={article.image}
alt={article.title}
className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-110"
referrerPolicy="no-referrer"
/>
</Link>
<button
onClick={() => toggleBookmark(article.id)}
className={cn(
"absolute top-4 right-4 h-10 w-10 rounded-full border border-white/20 backdrop-blur-sm flex items-center justify-center transition-all",
bookmarked ? "bg-blue-600 border-blue-600 text-white" : "bg-black/5 text-white hover:bg-white hover:text-blue-600"
)}
>
<Bookmark size={16} fill={bookmarked ? "currentColor" : "none"} />
</button>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3 text-[10px] font-bold uppercase tracking-widest text-slate-400">
<span>{article.category}</span>
</div>
<Link to={`/artigo/${article.slug}`}>
<h3 className="text-2xl font-serif font-bold text-slate-900 leading-tight group-hover:text-blue-600 transition-colors">
{article.title}
</h3>
</Link>
<p className="text-slate-500 text-md leading-relaxed line-clamp-2">
{article.excerpt}
</p>
</div>
</motion.div>
);
}

View file

@ -0,0 +1,31 @@
import { Link } from 'react-router-dom';
import { Category } from '../types';
import { cn } from '../lib/utils';
import { CATEGORY_MAP } from '../constants';
interface CategoryBadgeProps {
category: Category;
variant?: 'solid' | 'outline' | 'glass';
className?: string;
}
export default function CategoryBadge({ category, variant = 'solid', className }: CategoryBadgeProps) {
const mapping = CATEGORY_MAP[category] || { name: category, slug: category.toLowerCase().replace(/\s+/g, '-') };
const baseStyles = "inline-flex items-center rounded-full px-3 py-1 text-[10px] font-bold uppercase tracking-wider transition-all hover:scale-105 active:scale-95";
const variants = {
solid: "bg-blue-600 text-white hover:bg-blue-700",
outline: "border border-blue-100 text-blue-600 hover:bg-blue-50",
glass: "bg-white/80 backdrop-blur-sm text-blue-900 shadow-sm hover:bg-white"
};
return (
<Link
to={`/categoria/${mapping.slug}`}
className={cn(baseStyles, variants[variant], className)}
onClick={(e) => e.stopPropagation()}
>
{mapping.name}
</Link>
);
}

View file

@ -0,0 +1,83 @@
import { Link } from 'react-router-dom';
import { Mail, Github, Twitter, Linkedin, Instagram, Facebook, Youtube } from 'lucide-react';
export default function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-slate-950 text-white pt-24 pb-12">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-12 gap-16 mb-20">
<div className="md:col-span-12 lg:col-span-12 xl:col-span-5">
<Link to="/" className="flex items-center gap-3 mb-10">
<span className="font-serif text-3xl font-bold italic tracking-tighter">SEO Authority.</span>
</Link>
<p className="text-slate-400 max-w-md mb-12 text-lg leading-relaxed font-medium">
O recurso definitivo para profissionais que buscam excelência técnica e autoridade duradoura no ecossistema de busca.
</p>
<div className="flex flex-wrap gap-4">
{[
{ Icon: Twitter, label: 'Twitter' },
{ Icon: Linkedin, label: 'LinkedIn' },
{ Icon: Instagram, label: 'Instagram' },
{ Icon: Facebook, label: 'Facebook' },
{ Icon: Youtube, label: 'YouTube' },
{ Icon: Github, label: 'GitHub' },
{ Icon: Mail, label: 'Email' }
].map(({ Icon, label }, i) => (
<a
key={i}
href="#"
aria-label={label}
className="h-11 w-11 flex items-center justify-center rounded-full border border-white/10 text-slate-400 hover:text-white hover:border-white hover:bg-white/5 transition-all group"
>
<Icon size={18} className="group-hover:scale-110 transition-transform" />
</a>
))}
</div>
</div>
<div className="md:col-span-4 lg:col-span-3 xl:col-span-2">
<h4 className="font-bold text-white mb-8 uppercase text-[10px] tracking-[0.2em] opacity-40">Seções</h4>
<ul className="space-y-6">
<li><Link to="/categoria/estrategia" className="text-slate-300 hover:text-white text-sm font-bold tracking-tight transition-colors">Estratégia</Link></li>
<li><Link to="/categoria/tecnico" className="text-slate-300 hover:text-white text-sm font-bold tracking-tight transition-colors">Técnico</Link></li>
<li><Link to="/categoria/autoridade" className="text-slate-300 hover:text-white text-sm font-bold tracking-tight transition-colors">Autoridade</Link></li>
<li><Link to="/categoria/negocios" className="text-slate-300 hover:text-white text-sm font-bold tracking-tight transition-colors">Negócios</Link></li>
</ul>
</div>
<div className="md:col-span-4 lg:col-span-3 xl:col-span-2">
<h4 className="font-bold text-white mb-8 uppercase text-[10px] tracking-[0.2em] opacity-40">Publicação</h4>
<ul className="space-y-6">
<li><Link to="/metodologia" className="text-slate-300 hover:text-white text-sm font-bold tracking-tight transition-colors">Metodologia</Link></li>
<li><Link to="/sobre" className="text-slate-300 hover:text-white text-sm font-bold tracking-tight transition-colors">Sobre</Link></li>
<li><Link to="/contato" className="text-slate-300 hover:text-white text-sm font-bold tracking-tight transition-colors">Contato</Link></li>
</ul>
</div>
<div className="md:col-span-4 lg:col-span-6 xl:col-span-3">
<div className="p-8 bg-white/5 rounded-[32px] border border-white/10">
<h5 className="font-serif text-xl font-bold mb-4 italic">Exclusive Insights.</h5>
<p className="text-slate-400 text-xs leading-relaxed mb-6 font-medium">Assine nosso briefing semanal gratuito sobre o futuro do Google.</p>
<a
href="#newsletter"
className="block w-full py-4 bg-white text-slate-950 text-center rounded-xl font-bold text-[10px] uppercase tracking-widest hover:bg-slate-200 transition-colors"
>
Assinar Briefing
</a>
</div>
</div>
</div>
<div className="pt-12 border-t border-white/5 flex flex-col md:flex-row justify-between items-center gap-6 text-[10px] font-bold uppercase tracking-widest text-slate-500">
<p>© {currentYear} SEO AUTHORITY PUBLISHING. ALL RIGHTS RESERVED.</p>
<div className="flex gap-8">
<Link to="/privacidade" className="hover:text-white transition-colors">Privacy Policy</Link>
<Link to="/termos" className="hover:text-white transition-colors">Terms of Service</Link>
</div>
</div>
</div>
</footer>
);
}

View file

@ -0,0 +1,165 @@
import { Link } from 'react-router-dom';
import { Search, Menu, X, TrendingUp, Globe, Twitter, Instagram, Linkedin, Youtube, Bookmark } from 'lucide-react';
import { useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { useLanguage } from '../contexts/LanguageContext';
import { translations } from '../constants';
import SearchOverlay from './SearchOverlay';
import { useBookmarks } from '../contexts/BookmarksContext';
export default function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const { lang, setLang } = useLanguage();
const { bookmarks } = useBookmarks();
const t = translations[lang];
const categories = [
{ name: 'Estratégia', slug: 'estrategia' },
{ name: 'Técnico', slug: 'tecnico' },
{ name: 'Autoridade', slug: 'autoridade' },
{ name: 'Negócios', slug: 'negocios' }
];
return (
<header className="sticky top-0 z-50 w-full bg-white border-b border-slate-100">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-20 items-center justify-between">
<div className="flex items-center gap-10">
<Link to="/" className="flex items-center gap-2 group">
<div className="h-8 w-8 bg-blue-600 rounded-lg rotate-12 group-hover:rotate-0 transition-transform flex items-center justify-center text-white font-serif font-black text-xl">S</div>
<span className="font-serif text-xl font-bold tracking-tight text-slate-900">
SEO<span className="text-blue-600">AUTHORITY.</span>
</span>
</Link>
<nav className="hidden lg:flex items-center gap-6">
{categories.map((cat) => (
<Link
key={cat.slug}
to={`/categoria/${cat.slug}`}
className="text-[11px] font-bold uppercase tracking-[0.15em] text-slate-400 hover:text-slate-900 transition-colors"
>
{cat.name}
</Link>
))}
<Link
to="/contato"
className="text-[11px] font-bold uppercase tracking-[0.15em] text-slate-400 hover:text-slate-900 transition-colors"
>
Contato
</Link>
</nav>
</div>
<div className="flex items-center gap-4">
<div id="google_translate_element" className="hidden"></div>
<div className="hidden lg:flex items-center gap-2 text-[9px] font-black text-blue-600 bg-blue-50/50 border border-blue-100/50 px-3 py-1 rounded-full tracking-[0.1em] uppercase">
<span className="h-1.5 w-1.5 rounded-full bg-blue-600 animate-pulse" />
<span>LIVE CORE 2026</span>
</div>
{/* Language Switcher */}
<div className="hidden sm:flex items-center gap-2 px-3 py-1.5 bg-slate-50 rounded-full border border-slate-100 group">
<Globe size={14} className="text-slate-400 group-hover:text-blue-600 transition-colors" />
<select
value={lang}
onChange={(e) => setLang(e.target.value as any)}
className="bg-transparent text-[10px] font-bold uppercase tracking-widest outline-none cursor-pointer"
>
<option value="pt-br">PT</option>
<option value="en">EN</option>
<option value="es">ES</option>
</select>
</div>
<button
className="text-slate-400 hover:text-slate-900 transition-colors"
onClick={() => setIsSearchOpen(true)}
>
<Search size={20} />
</button>
<Link
to="/leituras-salvas"
className="relative p-2 text-slate-400 hover:text-blue-600 transition-colors"
title="Leituras Salvas"
>
<Bookmark size={20} />
{bookmarks.length > 0 && (
<span className="absolute top-0 right-0 h-4 w-4 bg-blue-600 text-white text-[8px] font-bold rounded-full flex items-center justify-center border-2 border-white">
{bookmarks.length}
</span>
)}
</Link>
<button
className="lg:hidden p-2 text-slate-900"
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
</div>
</div>
<SearchOverlay isOpen={isSearchOpen} onClose={() => setIsSearchOpen(false)} />
<AnimatePresence>
{isMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="lg:hidden border-t border-slate-100 bg-white overflow-hidden"
>
<div className="flex flex-col gap-6 p-8">
<div className="flex flex-col gap-3">
{categories.map((cat) => (
<Link
key={cat.slug}
to={`/categoria/${cat.slug}`}
className="text-xl font-serif font-bold text-slate-900 italic tracking-tight hover:text-blue-600 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
{cat.name}
</Link>
))}
<Link
to="/contato"
className="text-xl font-serif font-bold text-slate-900 italic tracking-tight hover:text-blue-600 transition-colors"
onClick={() => setIsMenuOpen(false)}
>
Contato
</Link>
<Link
to="/leituras-salvas"
className="text-xl font-serif font-bold text-slate-900 italic tracking-tight hover:text-blue-600 transition-colors flex items-center gap-3"
onClick={() => setIsMenuOpen(false)}
>
Leituras Salvas
{bookmarks.length > 0 && (
<span className="h-6 w-6 bg-blue-600 text-white text-[10px] font-bold rounded-full flex items-center justify-center">
{bookmarks.length}
</span>
)}
</Link>
</div>
<div className="pt-8 border-t border-slate-100">
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-300 mb-6">Social</p>
<div className="flex gap-3">
{[Twitter, Linkedin, Instagram, Youtube].map((Icon, i) => (
<a key={i} href="#" className="h-10 w-10 flex items-center justify-center rounded-xl bg-slate-50 text-slate-400 hover:text-blue-600 transition-all">
<Icon size={18} />
</a>
))}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</header>
);
}

View file

@ -0,0 +1,40 @@
import { ReactNode, useEffect } from 'react';
import Header from './Header';
import Footer from './Footer';
import LeadMagnet from './LeadMagnet';
import { useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'motion/react';
interface LayoutProps {
children: ReactNode;
}
export default function Layout({ children }: LayoutProps) {
const { pathname } = useLocation();
// Scroll to top on route change
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return (
<div className="min-h-screen flex flex-col bg-white selection:bg-blue-100 selection:text-blue-900">
<Header />
<main className="flex-grow">
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
{children}
</motion.div>
</AnimatePresence>
</main>
<LeadMagnet />
<Footer />
</div>
);
}

View file

@ -0,0 +1,126 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { X, Download, ShieldCheck, ArrowRight, Mail } from 'lucide-react';
export default function LeadMagnet() {
const [isOpen, setIsOpen] = useState(false);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
// Check if user already dismissed or converted
const hasSeen = localStorage.getItem('seo_lead_magnet_dismissed');
if (hasSeen) return;
const handleMouseLeave = (e: MouseEvent) => {
if (e.clientY <= 0 && !dismissed) {
setIsOpen(true);
}
};
const handleScroll = () => {
const scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
if (scrollPercent > 50 && !dismissed) {
setIsOpen(true);
}
};
document.addEventListener('mouseleave', handleMouseLeave);
window.addEventListener('scroll', handleScroll);
return () => {
document.removeEventListener('mouseleave', handleMouseLeave);
window.removeEventListener('scroll', handleScroll);
};
}, [dismissed]);
const handleClose = () => {
setIsOpen(false);
setDismissed(true);
localStorage.setItem('seo_lead_magnet_dismissed', 'true');
};
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[200] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm">
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="bg-white rounded-[40px] shadow-2xl max-w-4xl w-full overflow-hidden flex flex-col md:flex-row relative"
>
<button
onClick={handleClose}
className="absolute top-6 right-6 h-10 w-10 rounded-full bg-slate-100/80 hover:bg-slate-900 hover:text-white text-slate-400 flex items-center justify-center z-20 transition-all shadow-sm"
aria-label="Fechar"
>
<X size={20} />
</button>
{/* Left side: Visual */}
<div className="md:w-5/12 bg-blue-600 p-12 text-white flex flex-col justify-between relative overflow-hidden">
<div className="relative z-10">
<div className="h-12 w-12 bg-white/20 rounded-2xl flex items-center justify-center mb-8">
<ShieldCheck size={28} />
</div>
<h2 className="text-4xl font-serif font-bold italic tracking-tight leading-tight mb-4">
Domine a Algoritmo 2026.
</h2>
<p className="text-blue-100 font-medium">
Baixe nosso checklist exclusivo de Auditoria Técnica e E-E-A-T para garantir sua autoridade.
</p>
</div>
<div className="relative z-10 mt-12 pt-8 border-t border-white/10">
<div className="flex items-center gap-3">
<div className="flex -space-x-3">
{[1, 2, 3].map(i => (
<img key={i} src={`https://i.pravatar.cc/100?u=${i}`} className="h-8 w-8 rounded-full border-2 border-blue-600" />
))}
</div>
<span className="text-[10px] font-bold uppercase tracking-widest">+12.4k SEOs baixaram</span>
</div>
</div>
{/* Decorative Circle */}
<div className="absolute -bottom-20 -left-20 h-64 w-64 bg-white/10 rounded-full blur-3xl" />
</div>
{/* Right side: Form */}
<div className="md:w-7/12 p-12 md:p-16 flex flex-col justify-center">
<span className="text-[10px] font-black text-blue-600 uppercase tracking-[0.3em] mb-4 block">Recurso Gratuito</span>
<h3 className="text-2xl font-bold text-slate-900 mb-8">Onde enviamos seu <span className="italic">Blueprint de Autoridade?</span></h3>
<form className="space-y-4" onSubmit={(e) => { e.preventDefault(); handleClose(); }}>
<div className="relative">
<Mail className="absolute left-6 top-1/2 -translate-y-1/2 text-slate-300" size={18} />
<input
type="email"
placeholder="Seu melhor e-mail comercial"
required
className="w-full h-16 pl-16 pr-6 bg-slate-50 border border-slate-100 rounded-2xl text-slate-900 placeholder:text-slate-300 focus:outline-none focus:border-blue-600 focus:bg-white transition-all font-medium"
/>
</div>
<button
type="submit"
className="w-full h-16 bg-slate-900 text-white rounded-2xl font-bold uppercase text-xs tracking-[0.2em] flex items-center justify-center gap-3 hover:bg-blue-600 transition-all shadow-xl shadow-slate-100"
>
Receber Agora <Download size={18} />
</button>
</form>
<div className="mt-8 flex items-center justify-center gap-6">
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-300 uppercase tracking-widest">
<ArrowRight size={12} /> PDF interativo
</div>
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-300 uppercase tracking-widest">
<ArrowRight size={12} /> Templates de auditoria
</div>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
}

View file

@ -0,0 +1,153 @@
import { Helmet } from 'react-helmet-async';
interface SEOProps {
title: string;
description: string;
canonical?: string;
ogImage?: string;
ogType?: string;
keywords?: string[];
articleData?: {
publishedTime?: string;
modifiedTime?: string;
author?: string;
authorAvatar?: string;
tags?: string[];
section?: string;
};
}
export default function SEO({
title,
description,
canonical,
ogImage = 'https://images.unsplash.com/photo-1454165833767-027ffea9e778?auto=format&fit=crop&q=80&w=1200',
ogType = 'website',
keywords = [],
articleData
}: SEOProps) {
const siteName = 'SEO Authority Blog';
const siteUrl = window.location.origin;
const fullTitle = `${title} | ${siteName}`;
// Organization Schema
const organizationSchema = {
"@type": "Organization",
"@id": `${siteUrl}/#organization`,
"name": siteName,
"url": siteUrl,
"logo": {
"@type": "ImageObject",
"url": "https://picsum.photos/seed/logo/200/200",
"width": 200,
"height": 200
}
};
// Breadcrumb Schema
const breadcrumbSchema = {
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": siteUrl
}
]
};
if (ogType === 'article') {
breadcrumbSchema.itemListElement.push({
"@type": "ListItem",
"position": 2,
"name": title,
"item": canonical || window.location.href
});
}
// Final Schema
const finalSchema = {
"@context": "https://schema.org",
"@graph": [
organizationSchema,
{
"@type": ogType === 'article' ? "BlogPosting" : "WebSite",
"@id": `${siteUrl}/#website`,
"url": siteUrl,
"name": siteName,
"publisher": { "@id": `${siteUrl}/#organization` },
"inLanguage": "pt-BR"
},
ogType === 'article' ? {
"@type": "BlogPosting",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": canonical || window.location.href
},
"headline": title,
"description": description,
"image": ogImage,
"author": {
"@type": "Person",
"name": articleData?.author || "Equipe SEO",
"image": articleData?.authorAvatar || "https://i.pravatar.cc/150?u=seo"
},
"publisher": { "@id": `${siteUrl}/#organization` },
"datePublished": articleData?.publishedTime,
"dateModified": articleData?.modifiedTime || articleData?.publishedTime,
"keywords": keywords.join(', '),
"articleSection": articleData?.section
} : {
"@type": "WebPage",
"@id": `${siteUrl}/#page`,
"url": canonical || window.location.href,
"name": fullTitle,
"description": description,
"publisher": { "@id": `${siteUrl}/#organization` }
},
breadcrumbSchema
]
};
return (
<Helmet>
<title>{fullTitle}</title>
<meta name="description" content={description} />
{keywords.length > 0 && <meta name="keywords" content={keywords.join(', ')} />}
{/* Canonical URL */}
<link rel="canonical" href={canonical || window.location.href} />
{/* Open Graph / Facebook */}
<meta property="og:type" content={ogType} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImage} />
<meta property="og:site_name" content={siteName} />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImage} />
{/* Article Specifics */}
{ogType === 'article' && articleData && (
<>
{articleData.publishedTime && <meta property="article:published_time" content={articleData.publishedTime} />}
{articleData.modifiedTime && <meta property="article:modified_time" content={articleData.modifiedTime} />}
{articleData.author && <meta property="article:author" content={articleData.author} />}
{articleData.tags?.map(tag => (
<meta key={tag} property="article:tag" content={tag} />
))}
</>
)}
{/* Structured Data */}
<script type="application/ld+json">
{JSON.stringify(finalSchema)}
</script>
</Helmet>
);
}

View file

@ -0,0 +1,157 @@
import { motion, AnimatePresence } from 'motion/react';
import { X, Search as SearchIcon, ArrowRight } from 'lucide-react';
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { articles } from '../data/articles';
import { Article } from '../types';
import { summarizeSearchResults } from '../services/geminiService';
import { Sparkles, Loader2 } from 'lucide-react';
interface SearchOverlayProps {
isOpen: boolean;
onClose: () => void;
}
export default function SearchOverlay({ isOpen, onClose }: SearchOverlayProps) {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Article[]>([]);
const [summary, setSummary] = useState<string | null>(null);
const [loadingSummary, setLoadingSummary] = useState(false);
useEffect(() => {
if (query.trim().length > 2) {
const filtered = articles.filter(a =>
a.title.toLowerCase().includes(query.toLowerCase()) ||
a.category.toLowerCase().includes(query.toLowerCase()) ||
a.tags.some(t => t.toLowerCase().includes(query.toLowerCase()))
).slice(0, 5);
setResults(filtered);
// Trigger AI Summary if results found
if (filtered.length > 0) {
setLoadingSummary(true);
summarizeSearchResults(query, filtered).then(res => {
setSummary(res);
setLoadingSummary(false);
});
}
} else {
setResults([]);
setSummary(null);
}
}, [query]);
// Close on Escape
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, [onClose]);
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] bg-white/95 backdrop-blur-md flex flex-col"
>
<div className="mx-auto max-w-4xl w-full px-6 pt-24">
<div className="flex items-center justify-between mb-12">
<div className="flex items-center gap-3">
<div className="h-10 w-10 bg-blue-600 rounded-xl flex items-center justify-center text-white">
<SearchIcon size={20} />
</div>
<h2 className="text-sm font-bold uppercase tracking-[0.2em] text-slate-400">Pesquisar Insight</h2>
</div>
<button
onClick={onClose}
className="h-12 w-12 rounded-full bg-slate-100 flex items-center justify-center text-slate-500 hover:bg-slate-900 hover:text-white transition-all"
>
<X size={20} />
</button>
</div>
<div className="relative mb-16">
<input
autoFocus
type="text"
placeholder="IA Generativa, SEO Técnico, Link Building..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full bg-transparent border-b-2 border-slate-100 py-6 text-3xl sm:text-5xl font-serif font-bold text-slate-900 placeholder:text-slate-100 focus:outline-none focus:border-blue-600 transition-colors"
/>
</div>
<div className="space-y-4">
{summary && !loadingSummary && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8 p-6 bg-blue-50/50 border border-blue-100 rounded-[2rem] flex gap-4"
>
<div className="h-8 w-8 bg-blue-600 rounded-full flex items-center justify-center text-white shrink-0">
<Sparkles size={16} />
</div>
<div>
<span className="text-[10px] font-black text-blue-600 uppercase tracking-widest block mb-1">Insights da IA</span>
<p className="text-slate-600 text-sm leading-relaxed italic">
"{summary}"
</p>
</div>
</motion.div>
)}
{loadingSummary && (
<div className="flex items-center gap-3 text-slate-300 mb-8 px-6">
<Loader2 size={16} className="animate-spin" />
<span className="text-xs font-bold uppercase tracking-widest">Consultando IA...</span>
</div>
)}
{results.length > 0 ? (
results.map((article) => (
<Link
key={article.id}
to={`/artigo/${article.slug}`}
onClick={onClose}
className="group block p-6 rounded-[2rem] border border-slate-100 hover:bg-white hover:shadow-xl hover:shadow-slate-200 transition-all"
>
<div className="flex items-center justify-between">
<div>
<span className="text-[10px] font-bold text-blue-600 uppercase tracking-widest mb-2 block">{article.category}</span>
<h3 className="text-xl font-bold text-slate-900 group-hover:text-blue-600 transition-colors">{article.title}</h3>
</div>
<div className="h-10 w-10 rounded-full border border-slate-100 flex items-center justify-center text-slate-300 group-hover:bg-blue-600 group-hover:text-white group-hover:border-blue-600 transition-all">
<ArrowRight size={16} />
</div>
</div>
</Link>
))
) : query.trim().length > 2 ? (
<div className="text-center py-20">
<p className="text-slate-400 font-medium italic">Nenhum insight encontrado para "{query}"</p>
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{['Estratégia', 'IA', 'Link Building', 'Técnico'].map((tag) => (
<button
key={tag}
onClick={() => setQuery(tag)}
className="p-4 rounded-2xl bg-slate-50 text-slate-500 text-[10px] font-bold uppercase tracking-widest hover:bg-blue-50 hover:text-blue-600 transition-all"
>
{tag}
</button>
))}
</div>
)}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
);
}

View file

@ -0,0 +1,90 @@
import { useEffect, useState } from 'react';
import { cn } from '../lib/utils';
import { List } from 'lucide-react';
interface TOCItem {
id: string;
text: string;
level: number;
}
export default function TableOfContents({ content }: { content: string }) {
const [activeId, setActiveId] = useState<string>('');
const [headings, setHeadings] = useState<TOCItem[]>([]);
useEffect(() => {
// Extract headings (H2 and H3 only for clarity)
const headingLines = content.split('\n').filter(line =>
line.startsWith('## ') || line.startsWith('### ')
);
const extractedHeadings = headingLines.map(line => {
const level = line.startsWith('###') ? 3 : 2;
const text = line.replace(/^#+\s+/, '').trim();
const id = text.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '');
return { id, text, level };
});
setHeadings(extractedHeadings);
// Initial check (delay slightly for DOM to be ready)
setTimeout(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id);
}
});
},
{ rootMargin: '-10% 0% -80% 0%' }
);
extractedHeadings.forEach((h) => {
const el = document.getElementById(h.id);
if (el) observer.observe(el);
});
return () => observer.disconnect();
}, 500);
}, [content]);
if (headings.length === 0) return null;
return (
<div className="sticky top-28 h-fit hidden xl:block">
<div className="mb-6 flex items-center gap-2 text-[10px] font-black text-slate-300 uppercase tracking-widest">
<List size={14} />
<span>Sumário Estrutural</span>
</div>
<nav className="flex flex-col gap-4 border-l border-slate-100 pl-6">
{headings.map((h) => (
<a
key={h.id}
href={`#${h.id}`}
onClick={(e) => {
e.preventDefault();
document.getElementById(h.id)?.scrollIntoView({ behavior: 'smooth' });
}}
className={cn(
"text-xs font-bold leading-tight transition-all relative block",
activeId === h.id
? "text-blue-600 translate-x-2"
: "text-slate-400 hover:text-slate-900",
h.level === 3 && "pl-4"
)}
>
{activeId === h.id && (
<span className="absolute -left-[25px] top-1/2 -translate-y-1/2 h-1 w-3 bg-blue-600 rounded-full" />
)}
{h.text}
</a>
))}
</nav>
</div>
);
}

View file

@ -0,0 +1,126 @@
import { Category } from './types';
export const CATEGORY_MAP: Record<Category, { name: string; slug: string }> = {
'Estratégia': { name: 'Estratégia', slug: 'estrategia' },
'Técnico': { name: 'Técnico', slug: 'tecnico' },
'Autoridade': { name: 'Autoridade', slug: 'autoridade' },
'Negócios': { name: 'Negócios', slug: 'negocios' },
'SEO para Iniciantes': { name: 'SEO para Iniciantes', slug: 'iniciantes' },
'SEO Técnico': { name: 'SEO Técnico', slug: 'seo-tecnico' },
'SEO On Page': { name: 'SEO On Page', slug: 'on-page' },
'SEO Off Page': { name: 'SEO Off Page', slug: 'off-page' },
'Link Building': { name: 'Link Building', slug: 'link-building' },
'Ferramentas de SEO': { name: 'Ferramentas de SEO', slug: 'ferramentas' },
'SEO para E-commerce': { name: 'SEO para E-commerce', slug: 'e-commerce' },
'SEO para Blogs': { name: 'SEO para Blogs', slug: 'blogs' },
'Atualizações do Google': { name: 'Atualizações do Google', slug: 'google-updates' }
};
export const getCategoryBySlug = (slug: string) => {
return Object.entries(CATEGORY_MAP).find(([_, value]) => value.slug === slug);
};
export const translations: Record<string, any> = {
'pt-br': {
newsletter: 'Newsletter',
subscribe: 'Assinar Agora',
readMore: 'Ler artigo',
latest: 'Últimas',
allArticles: 'Todos os Artigos',
sections: 'Seções',
publishing: 'Publicação',
contact: 'Contato',
about: 'Sobre',
privacy: 'Privacidade',
terms: 'Termos',
featured: 'Destaque',
recent: 'Recentes',
explore: 'Explorar Insights',
search: 'Buscar',
viewAll: 'Ver Todos',
manifesto: 'Manifesto',
methodology: 'Metodologia',
heroTitle: 'A Nova Ordem da Busca Semântica.',
heroSubtitle: 'EDITORIAL DE SEO 2026',
heroDescription: 'O recurso definitivo para profissionais que buscam excelência técnica e autoridade duradoura no ecossistema de busca.',
readManifesto: 'Ler Manifesto',
publishedIn: 'Publicado em',
newsletterTitle: 'Newsletter Semanal',
newsletterDescription: 'Inscreva-se para receber as últimas novidades.',
emailPlaceholder: 'seu@email.com',
subscribeButton: 'Inscrever',
trendsTitle: 'TENDÊNCIAS',
footerNewsletterTitle: 'Entre no Círculo de Autoridade',
footerNewsletterSubtitle: 'Assine para receber análises profundas sobre mudanças algorítmicas, IA e estratégias de busca que o resto do mercado ainda não viu.',
footerNewsletterInput: 'seu@email.com',
footerNewsletterDisclaimer: 'Nós respeitamos sua caixa de entrada. Zero spam, apenas inteligência de busca.'
},
'en': {
newsletter: 'Newsletter',
subscribe: 'Subscribe Now',
readMore: 'Read more',
latest: 'Latest',
allArticles: 'All Articles',
sections: 'Sections',
publishing: 'Publishing',
contact: 'Contact',
about: 'About',
privacy: 'Privacy',
terms: 'Terms',
featured: 'Featured',
recent: 'Recent',
explore: 'Explore Insights',
search: 'Search',
viewAll: 'View All',
manifesto: 'Manifesto',
methodology: 'Methodology',
heroTitle: 'The New Order of Semantic Search.',
heroSubtitle: 'SEO EDITORIAL 2026',
heroDescription: 'The definitive resource for professionals seeking technical excellence and lasting authority in the search ecosystem.',
readManifesto: 'Read Manifesto',
publishedIn: 'Published in',
newsletterTitle: 'Weekly Newsletter',
newsletterDescription: 'Subscribe to get the latest news.',
emailPlaceholder: 'your@email.com',
subscribeButton: 'Subscribe',
trendsTitle: 'TRENDS',
footerNewsletterTitle: 'Join the Circle of Authority',
footerNewsletterSubtitle: 'Subscribe to receive deep insights into algorithmic changes, AI, and search strategies that the rest of the market hasn\'t seen yet.',
footerNewsletterInput: 'your@email.com',
footerNewsletterDisclaimer: 'We respect your inbox. Zero spam, only search intelligence.'
},
'es': {
newsletter: 'Boletín',
subscribe: 'Suscríbete Ahora',
readMore: 'Leer más',
latest: 'Últimos',
allArticles: 'Todos los Artículos',
sections: 'Secciones',
publishing: 'Publicación',
contact: 'Contacto',
about: 'Sobre',
privacy: 'Privacidad',
terms: 'Términos',
featured: 'Destacado',
recent: 'Recientes',
explore: 'Explorar Insights',
search: 'Buscar',
viewAll: 'Ver Todo',
manifesto: 'Manifiesto',
methodology: 'Metodología',
heroTitle: 'El Nuevo Orden de la Búsqueda Semántica.',
heroSubtitle: 'EDITORIAL SEO 2026',
heroDescription: 'El recurso definitivo para profesionales que buscan excelencia técnica y autoridad duradera en el ecosistema de búsqueda.',
readManifesto: 'Leer Manifiesto',
publishedIn: 'Publicado en',
newsletterTitle: 'Boletín Semanal',
newsletterDescription: 'Suscríbete para recibir las últimas noticias.',
emailPlaceholder: 'tu@email.com',
subscribeButton: 'Suscríbete',
trendsTitle: 'TENDENCIAS',
footerNewsletterTitle: 'Únete al Círculo de Autoridad',
footerNewsletterSubtitle: 'Suscríbete para recibir análisis profundos sobre cambios algorítmicos, IA y estrategias de búsqueda que el resto del mercado aún no ha visto.',
footerNewsletterInput: 'tu@email.com',
footerNewsletterDisclaimer: 'Respetamos tu bandeja de entrada. Cero spam, solo inteligencia de búsqueda.'
}
};

View file

@ -0,0 +1,52 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
interface BookmarksContextType {
bookmarks: string[];
toggleBookmark: (id: string) => void;
isBookmarked: (id: string) => boolean;
}
const BookmarksContext = createContext<BookmarksContextType | undefined>(undefined);
export function BookmarksProvider({ children }: { children: React.ReactNode }) {
const [bookmarks, setBookmarks] = useState<string[]>([]);
// Load from localStorage on mount
useEffect(() => {
const saved = localStorage.getItem('seo_bookmarks');
if (saved) {
try {
setBookmarks(JSON.parse(saved));
} catch (e) {
console.error('Failed to parse bookmarks', e);
}
}
}, []);
// Sync with localStorage
const toggleBookmark = (id: string) => {
setBookmarks(prev => {
const next = prev.includes(id)
? prev.filter(item => item !== id)
: [...prev, id];
localStorage.setItem('seo_bookmarks', JSON.stringify(next));
return next;
});
};
const isBookmarked = (id: string) => bookmarks.includes(id);
return (
<BookmarksContext.Provider value={{ bookmarks, toggleBookmark, isBookmarked }}>
{children}
</BookmarksContext.Provider>
);
}
export function useBookmarks() {
const context = useContext(BookmarksContext);
if (context === undefined) {
throw new Error('useBookmarks must be used within a BookmarksProvider');
}
return context;
}

View file

@ -0,0 +1,89 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
type Language = 'pt-br' | 'en' | 'es';
interface LanguageContextType {
lang: Language;
setLang: (lang: Language) => void;
translate: (text: any, context?: string, isObject?: boolean) => Promise<any>;
isTranslating: boolean;
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [lang, setLang] = useState<Language>(() => {
const saved = localStorage.getItem('app-lang');
return (saved as Language) || 'pt-br';
});
const [isTranslating, setIsTranslating] = useState(false);
const [cache, setCache] = useState<Record<string, any>>({});
const translate = async (text: any, context?: string, isObject = false): Promise<any> => {
if (lang === 'pt-br') return text;
// Create unique cache key
const contentKey = typeof text === 'string' ? text : JSON.stringify(text);
const cacheKey = `${lang}-${isObject ? 'obj' : 'txt'}-${contentKey.substring(0, 50)}`;
if (cache[cacheKey]) return cache[cacheKey];
setIsTranslating(true);
try {
const response = await fetch('/api/translate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text,
targetLang: lang === 'en' ? 'English' : 'Spanish',
context,
isObject
}),
});
const data = await response.json();
const result = data.translatedText || text;
setCache(prev => ({ ...prev, [cacheKey]: result }));
return result;
} catch (error) {
console.error("Translation fail:", error);
return text;
} finally {
setIsTranslating(false);
}
};
useEffect(() => {
localStorage.setItem('app-lang', lang);
document.documentElement.lang = lang;
// Trigger Google Translate Widget
const triggerGoogleTranslate = () => {
const select = document.querySelector('.goog-te-combo') as HTMLSelectElement;
if (select) {
const googleLangCode = lang === 'pt-br' ? 'pt' : lang;
select.value = googleLangCode;
select.dispatchEvent(new Event('change'));
}
};
// Delay a bit to ensure the widget is loaded or React finished rendering
const timer = setTimeout(triggerGoogleTranslate, 500);
return () => clearTimeout(timer);
}, [lang]);
return (
<LanguageContext.Provider value={{ lang, setLang, translate, isTranslating }}>
{children}
</LanguageContext.Provider>
);
};
export const useLanguage = () => {
const context = useContext(LanguageContext);
if (context === undefined) {
throw new Error('useLanguage must be used within a LanguageProvider');
}
return context;
};

View file

@ -0,0 +1,654 @@
import { Article } from '../types';
export const articles: Article[] = [
{
id: '1',
slug: 'o-impacto-da-ia-generativa-na-busca-organica-2026',
title: 'O Impacto da IA Generativa na Busca Orgânica em 2026',
excerpt: 'Como as Search Generative Experiences (SGE) estão moldando o comportamento do usuário e o que isso significa para o CTR.',
category: 'Estratégia',
publishedAt: '2026-05-01',
image: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?auto=format&fit=crop&q=80&w=1600',
readTime: '12 min',
author: {
name: 'Dr. Lucas Silva',
role: 'Lead Strategist',
avatar: 'https://i.pravatar.cc/150?u=lucas'
},
metaTitle: 'IA Generativa e SEO em 2026 | Análise Técnica',
metaDescription: "SEO em 2026: Entenda como a IA Generativa e o SGE transformaram o tráfego orgânico. Conheça as táticas de 'Conteúdo de Valor Incrementado' para vencer o Zero-Click.",
tags: ['IA', 'SGE', 'Algoritmo', 'Google'],
lang: 'pt-br',
content: `
# O Impacto da IA Generativa na Busca Orgânica em 2026
A busca não é mais o que costumava ser. Em 2026, a integração total da IA nas páginas de resultados (SERPs) criou um novo paradigma: a busca não apenas por links, mas por respostas diretas e inteligíveis.
## O Desafio do Zero-Click
Com a IA resolvendo dúvidas complexas diretamente no topo da busca, o volume de cliques para sites informativos superficiais despencou. A estratégia vencedora agora foca em **Conteúdo de Valor Incrementado** informações que a IA ainda não consegue sintetizar sem falhas.
### O que são Insights de Propriedade?
Insights de propriedade são dados, opiniões de especialistas e experiências reais que não existem em Large Language Models (LLMs). É a sua "fórmula secreta" que obriga o usuário a clicar para entender a profundidade do assunto.
## Estratégias de Sobrevivência
1. **Foco na Cauda Longa Hiper-Específica:** Consultas onde a opinião humana é necessária.
2. **Autoridade de Tópico (Topic Clusters):** Ser o dono do assunto, não apenas da palavra-chave.
3. **Optimizing for SGE Sources:** Estruturar dados para que seu site seja a fonte citada pela IA.
## Conclusão
A IA não matou o SEO; ela elevou o nível do jogo. Quem produz conteúdo medíocre está fora. Quem produz autoridade técnica está mais forte do que nunca.
`
},
{
id: '2',
slug: 'eeat-a-biblia-da-autoridade-digital',
title: 'E-E-A-T: A Bíblia da Autoridade Digital Moderna',
excerpt: 'Análise profunda sobre Experiência, Especialidade, Autoridade e Confiança no novo algoritmo.',
category: 'Estratégia',
publishedAt: '2026-04-28',
image: 'https://images.unsplash.com/photo-1450101499163-c8848c66ca85?auto=format&fit=crop&q=80&w=1600',
readTime: '15 min',
author: {
name: 'Beatriz Costa',
role: 'Head of Content',
avatar: 'https://i.pravatar.cc/150?u=beatriz'
},
metaTitle: 'Guia Definitivo E-E-A-T para SEO em 2026',
metaDescription: "Domine o E-E-A-T em 2026. Saiba por que a experiência humana é o diferencial contra a IA e como construir autoridade real para dominar os rankings do Google.",
tags: ['EEAT', 'Autoridade', 'Confiança', 'Especialidade'],
lang: 'pt-br',
content: `
# E-E-A-T: A Bíblia da Autoridade Digital Moderna
Se você quer rankear em 2026, você precisa entender o E-E-A-T. Não é um fator de ranking direto, mas uma diretriz que permeia todo o algoritmo do Google.
## O "E" de Experiência
Adicionado alguns anos, o fator experiência tornou-se crucial. O Google quer saber: você realmente usou este produto? Você realmente viveu essa situação?
<blockquote>
"A experiência humana é o único dado que a IA não consegue alucinar com perfeição em 2026."
</blockquote>
## Como demonstrar experiência?
- Use fotos originais, não apenas de bancos de imagem.
- Narre estudos de caso reais.
- Evite generalismos. Use "Eu descobri que..." em vez de "É geralmente aceito que...".
## Especialidade e Autoridade
Estes pilares são construídos através da consistência. Não adianta falar de SEO um dia e de culinária no outro se você quer ser visto como um especialista em tecnologia.
`
},
{
id: '3',
slug: 'auditoria-tecnica-seo-infraestrutura-performance',
title: 'Auditoria Técnica: Garantindo uma Infraestrutura de Elite',
excerpt: 'O guia definitivo para Core Web Vitals, Rendering e Crawl Budget em sites de larga escala.',
category: 'Técnico',
publishedAt: '2026-04-25',
image: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc51?auto=format&fit=crop&q=80&w=1600',
readTime: '18 min',
author: {
name: 'Marcos Tech',
role: 'SEO Engineer',
avatar: 'https://i.pravatar.cc/150?u=marcos'
},
metaTitle: 'Auditoria Técnica de SEO 2026 | Guia para Desenvolvedores',
metaDescription: "Guia definitivo de Auditoria Técnica SEO 2026. Otimize INP, LCP e Rendering para garantir uma infraestrutura de elite e performance máxima em sites de larga escala.",
tags: ['Technical SEO', 'Performance', 'Web Vitals', 'Infrastructure'],
lang: 'pt-br',
content: `
# Auditoria Técnica: Garantindo uma Infraestrutura de Elite
Em 2026, a performance técnica não é um diferencial é o requisito mínimo para entrar na arena.
## O novo Core Web Vital: Interaction to Next Paint (INP)
O INP substituiu oficialmente o FID como a métrica de interatividade. Ele mede quão rápido seu site responde a qualquer interação do usuário.
### Otimizando para INP
- Reduza o tempo de execução do JavaScript principal.
- Use Web Workers para tarefas pesadas.
- Evite layouts complexos que causem reflow excessivo.
## Indexação e Crawl Budget
Para sites com milhares de páginas, cada segundo do bot do Google no seu servidor conta.
- Use Server-Side Rendering (SSR) de forma inteligente.
- Mantenha um Sitemap dinâmico e limpo.
- Monitore erros de 404 e cadeias de redirect.
`
},
{
id: '4',
slug: 'link-building-atraves-de-dados-proprietarios',
title: 'Link Building através de Dados Proprietários',
excerpt: 'Esqueça o outreach em massa. Aprenda a criar pesquisas que atraem links de forma orgânica.',
category: 'Autoridade',
publishedAt: '2026-04-20',
image: 'https://images.unsplash.com/photo-1551288049-bbdac8a28a1e?auto=format&fit=crop&q=80&w=1600',
readTime: '10 min',
author: {
name: 'João Links',
role: 'PR Specialist',
avatar: 'https://i.pravatar.cc/150?u=joao'
},
metaTitle: 'Estratégias de Link Building com Dados | SEO Authority',
metaDescription: "Transforme dados em imãs de links. Aprenda a criar pesquisas proprietárias e reports de mercado que geram backlinks editoriais espontâneos de alta autoridade.",
tags: ['Link Building', 'Backlinks', 'Content Marketing', 'PR'],
lang: 'pt-br',
content: `
# Link Building através de Dados Proprietários
O link building clássico de "troca de favores" está cada vez menos eficiente. O Google valoriza links editoriais espontâneos. A melhor forma de conseguir isso? Ser a fonte da notícia.
## Criando Imãs de Links
Quando você produz um infográfico com dados originais do seu setor, outros blogs e portais de notícias vão citar você como referência.
### Tipos de Conteúdo que Atraem Links:
1. **Pesquisas de Mercado:** Tendências para o próximo ano.
2. **Calculadoras e Ferramentas:** Soluções para problemas práticos.
3. **Análises de Impacto:** Como uma mudança econômica afetou seu nicho.
## A Importância do Texto Âncora
Links com âncoras variadas e naturais para páginas de autoridade (não apenas para a home) são o sinal mais forte de um perfil de links saudável.
`
},
{
id: '5',
slug: 'arquitetura-de-informacao-pillar-pages-e-clusters',
title: 'Arquitetura de Informação: Pillar Pages e Topic Clusters',
excerpt: 'Como estruturar seu blog para que o Google entenda sua autoridade em cada pilar de conteúdo.',
category: 'Estratégia',
publishedAt: '2026-04-15',
image: 'https://images.unsplash.com/photo-1542831371-29b0f74f9713?auto=format&fit=crop&q=80&w=1600',
readTime: '14 min',
author: {
name: 'Beatriz Costa',
role: 'Head of Content',
avatar: 'https://i.pravatar.cc/150?u=beatriz'
},
metaTitle: 'Arquitetura de Conteúdo: Pillar Pages e Clusters em 2026',
metaDescription: "Topic Clusters e Pillar Pages: Descubra como estruturar seu site para dominar a autoridade semântica em 2026. O guia completo para organizar conteúdo estrategicamente.",
tags: ['Architecture', 'Content Strategy', 'Topic Clusters'],
lang: 'pt-br',
content: `
# Arquitetura de Informação: Pillar Pages e Topic Clusters
Organizar o conteúdo do seu blog é tão importante quanto escrevê-lo. Uma estrutura de **Topic Clusters** ajuda o Google a mapear sua expertise.
## O Que é uma Pillar Page?
É um guia abrangente sobre um tema central. Por exemplo, "SEO para Iniciantes".
## O Que são Clusters?
São artigos específicos que aprofundam tópicos mencionados na Pillar Page. Por exemplo, "O que é Robots.txt?".
### O Link Mágico
Todos os artigos satélites (clusters) devem apontar de volta para a Pillar Page. Isso cria uma teia de autoridade.
`
},
{
id: '6',
slug: 'seo-para-e-commerce-otimizando-paginas-de-categoria',
title: 'SEO para E-commerce: O Segredo das Páginas de Categoria',
excerpt: 'Por que as páginas de categoria trazem mais tráfego que as páginas de produto e como otimizá-las.',
category: 'Negócios',
publishedAt: '2026-04-10',
image: 'https://images.unsplash.com/photo-1441986300917-64674bd600d8?auto=format&fit=crop&q=80&w=1600',
readTime: '11 min',
author: {
name: 'Ricardo Vendas',
role: 'E-commerce Expert',
avatar: 'https://i.pravatar.cc/150?u=ricardo'
},
metaTitle: 'SEO para E-commerce: Guias de Categoria em 2026',
metaDescription: "SEO para E-commerce: Por que as categorias são sua maior fonte de tráfego? Aprenda táticas avançadas para otimizar páginas de categoria e converter mais no Google.",
tags: ['E-commerce', 'Category Pages', 'Product SEO'],
lang: 'pt-br',
content: `
# SEO para E-commerce: O Segredo das Páginas de Categoria
Muitos donos de loja focam nas páginas de produto. Mas o volume real de busca está nas **Categorias**.
## Por que focar em Categorias?
Um usuário que busca "tênis de corrida" está em uma fase de consideração muito mais valiosa do que quem busca o modelo específico "Nike Pegasus 41 Azul".
### Otimização On-Page para Categorias:
- **Texto de Apoio:** Não enterre o texto no final da página. Use introduções úteis.
- **Interlinkagem:** Linke para seus produtos mais populares diretamente da categoria.
`
},
{
id: '7',
slug: 'schema-markup-avancado-e-dados-estruturados',
title: 'Schema Markup Avançado: Indo além do básico',
excerpt: 'Utilizando JSON-LD para fornecer contexto profundo aos mecanismos de busca.',
category: 'Técnico',
publishedAt: '2026-04-05',
image: 'https://images.unsplash.com/photo-1516259762381-22954d7d3ad2?auto=format&fit=crop&q=80&w=1600',
readTime: '13 min',
author: {
name: 'Marcos Tech',
role: 'SEO Engineer',
avatar: 'https://i.pravatar.cc/150?u=marcos'
},
metaTitle: 'Schema Markup Avançado JSON-LD | Guia Técnico 2026',
metaDescription: "Schema Markup 2026: Vá além do básico com JSON-LD. Aprenda a fornecer contexto profundo à IA do Google e conquistar Rich Snippets poderosos para seu site.",
tags: ['Schema', 'Structured Data', 'JSON-LD', 'Rich Snippets'],
lang: 'pt-br',
content: `
# Schema Markup Avançado: Indo além do básico
Dados estruturados são o vocabulário que o Google usa para entender o seu conteúdo. Em 2026, ignorar o Schema é como falar uma língua diferente do buscador.
## O Poder do JSON-LD
Prefira sempre a implementação via JSON-LD no cabeçalho ou rodapé do HTML.
### Esquemas Indispensáveis:
- **Article/BlogPosting:** Para conteúdo editorial.
- **FAQPage:** Para ocupar mais espaço na SERP.
- **Person/Organization:** Para reforçar o E-E-A-T do autor.
`
},
{
id: '8',
slug: 'seo-off-page-alem-dos-backlinks',
title: 'SEO Off-Page: A Autoridade além dos Backlinks',
excerpt: 'Menções de marca, sinais sociais e a construção de reputação no ambiente digital.',
category: 'Autoridade',
publishedAt: '2026-04-01',
image: 'https://images.unsplash.com/photo-1557838923-2985c318be48?auto=format&fit=crop&q=80&w=1600',
readTime: '12 min',
author: {
name: 'João Links',
role: 'PR Specialist',
avatar: 'https://i.pravatar.cc/150?u=joao'
},
metaTitle: 'SEO Off-Page e Reputação de Marca em 2026',
metaDescription: "SEO Off-Page em 2026: Autoridade é mais que backlinks. Entenda o peso das menções de marca, sinais sociais e co-ocorrência na reputação digital moderna.",
tags: ['Off Page', 'Reputation', 'Branding', 'Signals'],
lang: 'pt-br',
content: `
# SEO Off-Page: A Autoridade além dos Backlinks
O Google está cada vez melhor em entender menções de marca, mesmo quando elas não vêm acompanhadas de um link (Unlinked Brand Mentions).
## Sinais de Co-ocorrência
Se o nome da sua marca aparece frequentemente perto de palavras-chave como "melhor agência de SEO", o Google começa a associar sua entidade a esse tópico.
`
},
{
id: '9',
slug: 'psicologia-de-busca-entendendo-a-intencao',
title: 'Psicologia de Busca: Entendendo a Intenção Real',
excerpt: 'Por que o volume de busca é uma métrica vaidosa e como focar no que realmente converte.',
category: 'Estratégia',
publishedAt: '2026-03-25',
image: 'https://images.unsplash.com/photo-1518349619113-03114f06ac3a?auto=format&fit=crop&q=80&w=1600',
readTime: '14 min',
author: {
name: 'Dr. Lucas Silva',
role: 'Lead Strategist',
avatar: 'https://i.pravatar.cc/150?u=lucas'
},
metaTitle: 'Psicologia e Intenção de Busca em SEO (2026)',
metaDescription: "Pare de perseguir volume e entenda a Intenção de Busca. Guia de Psicologia de Busca para mapear a jornada do usuário e converter tráfego em resultados reais.",
tags: ['Psychology', 'User Intent', 'Keywords', 'Funnel'],
lang: 'pt-br',
content: `
# Psicologia de Busca: Entendendo a Intenção Real
Nem toda busca é criada igual. O usuário pode estar procurando informação, navegando para um site específico ou pronto para comprar.
## Otimizando para a Intenção
Se o seu artigo é um guia ensinando "como fazer", mas tenta vender um produto agressivamente no primeiro parágrafo, você está quebrando a intenção do usuário.
`
},
{
id: '10',
slug: 'otimizacao-para-busca-por-voz-e-assistentes',
title: 'Otimização para Busca por Voz e Assistentes em 2026',
excerpt: 'Como estruturar seu conteúdo para ser a resposta única da Alexa, Siri e Google Assistant.',
category: 'Técnico',
publishedAt: '2026-03-20',
image: 'https://images.unsplash.com/photo-1589254065878-42c9da997008?auto=format&fit=crop&q=80&w=1600',
readTime: '9 min',
author: {
name: 'Marcos Tech',
role: 'SEO Engineer',
avatar: 'https://i.pravatar.cc/150?u=marcos'
},
metaTitle: 'SEO para Busca por Voz 2026 | Dicas Práticas',
metaDescription: "SEO para Busca por Voz: Como ser a resposta única da Alexa, Siri e Google Assistant em 2026. Otimize seu conteúdo para NLP e linguagem natural hoje.",
tags: ['Voice Search', 'NLP', 'Conversational SEO'],
lang: 'pt-br',
content: `
# Otimização para Busca por Voz e Assistentes em 2026
Com o avanço do Processamento de Linguagem Natural (NLP), a busca por voz tornou-se extremamente precisa.
## Linguagem Natural
As pessoas não falam como escrevem. Em vez de "melhor SEO curso", elas perguntam "Ei Google, qual é o melhor curso de SEO disponível hoje?".
`
},
{
id: '11',
slug: 'seo-local-para-profissionais-liberais',
title: 'SEO Local: Dominando sua Região em 2026',
excerpt: 'Como profissionais liberais podem dominar o Google Maps e a busca local.',
category: 'Negócios',
publishedAt: '2026-03-15',
image: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&q=80&w=1600',
readTime: '8 min',
author: {
name: 'Dr. Lucas Silva',
role: 'Lead Strategist',
avatar: 'https://i.pravatar.cc/150?u=lucas'
},
metaTitle: 'Guia SEO Local 2026 | Google My Business',
metaDescription: "SEO Local 2026: Como dominar o Map Pack e Google My Business. Atraia clientes qualificados em sua região com estratégias de autoridade geográfica e IA local.",
tags: ['Local SEO', 'Google Maps'],
lang: 'pt-br',
content: `
# SEO Local: Dominando sua Região em 2026
O SEO Local tornou-se a espinha dorsal de pequenas e médias empresas. Em 2026, a presença no Google Maps não é apenas uma conveniência, é o destino final da jornada do consumidor local.
## Otimização do Perfil da Empresa
Manter suas informações atualizadas é o passo zero. Mas a autoridade local vai além disso: as avaliações com fotos e palavras-chave específicas do local agora pesam mais do que nunca no "Map Pack".
### O que mudou?
- **Respostas de IA Locais:** O Google Assistant agora usa os dados do seu perfil para responder perguntas complexas sobre disponibilidade e serviços.
- **Links Locais:** Citações em portais de notícias do seu bairro ou cidade transferem uma autoridade geográfica impossível de ignorar.
`
},
{
id: '12',
slug: 'erros-comuns-migracao-de-site',
title: 'Migração de Site: Como não perder tráfego',
excerpt: 'O checklist de sobrevivência para mudar seu domínio ou plataforma sem desastres.',
category: 'Técnico',
publishedAt: '2026-03-10',
image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&q=80&w=1600',
readTime: '20 min',
author: {
name: 'Marcos Tech',
role: 'SEO Engineer',
avatar: 'https://i.pravatar.cc/150?u=marcos_dev'
},
metaTitle: 'Migração de Site 2026 | Checklist de SEO',
metaDescription: "Checklist Definitivo de Migração de Site em 2026. Aprenda a gerenciar redirects 301, monitorar GSC e evitar quedas de tráfego traumáticas durante mudanças.",
tags: ['Migration', 'Redirects'],
lang: 'pt-br',
content: `
# Migração de Site: Como não perder tráfego
Migrar um site é um dos processos mais arriscados em SEO. Um erro no mapeamento de URLs pode destruir anos de autoridade acumulada em poucos dias.
## O Checklist de Sobrevivência
1. **Mapeamento de 1 para 1:** Garanta que cada URL antiga tenha um destino lógico na nova estrutura.
2. **Teste em Staging:** Nunca faça o deploy sem testar os redirects em um ambiente controlado.
3. **Monitoramento Granular:** Após a virada, acompanhe o Google Search Console diariamente para identificar erros 404 inesperados.
`
},
{
id: '13',
slug: 'futuro-da-busca-por-video-youtube-seo',
title: 'O Futuro da Busca por Vídeo e YouTube SEO',
excerpt: 'Como os vídeos estão sendo indexados diretamente nas SERPs principais.',
category: 'Estratégia',
publishedAt: '2026-03-05',
image: 'https://images.unsplash.com/photo-1492724441997-5dc865305da7?auto=format&fit=crop&q=80&w=1600',
readTime: '10 min',
author: {
name: 'Beatriz Costa',
role: 'Head of Content',
avatar: 'https://i.pravatar.cc/150?u=beatriz_c'
},
metaTitle: 'Vertical Search: YouTube e Vídeo SEO em 2026',
metaDescription: "Google Video SEO & YouTube 2026: Como dominar a busca multimodal. Aprenda a otimizar momentos chave, transcrições e capítulos para visibilidade máxima.",
tags: ['Video SEO', 'YouTube'],
lang: 'pt-br',
content: `
# O Futuro da Busca por Vídeo e YouTube SEO
Em 2026, a busca é multimodal. Vídeos curtos e tutoriais aprofundados estão ocupando espaços premium na página principal do Google.
## Indexação de Momentos Chave
O Google não indexa o vídeo, mas também os capítulos e as transcrições, permitindo que o usuário "caia" exatamente no segundo que resolve sua dúvida.
`
},
{
id: '14',
slug: 'ferramentas-gratuitas-seo-indispensaveis',
title: '15 Ferramentas Gratuitas de SEO Indispensáveis',
excerpt: 'O arsenal que você precisa para gerenciar seus projetos sem gastar um centavo.',
category: 'Estratégia',
publishedAt: '2026-03-01',
image: 'https://images.unsplash.com/photo-1551288049-bbdac8a28a1e?auto=format&fit=crop&q=80&w=1600',
readTime: '12 min',
author: {
name: 'Dr. Lucas Silva',
role: 'Lead Strategist',
avatar: 'https://i.pravatar.cc/150?u=lucas_s'
},
metaTitle: 'Ferramentas Gratuitas de SEO 2026',
metaDescription: "As 15 Melhores Ferramentas Gratuitas de SEO para 2026. Tenha o arsenal técnico completo sem gastar um centavo: de Search Console a monitoramento de tendências.",
tags: ['Tools', 'Free'],
lang: 'pt-br',
content: `
# 15 Ferramentas Gratuitas de SEO Indispensáveis
Você não precisa de assinaturas caras de mil dólares por mês para começar a rankear. Aqui está o arsenal básico para 2026:
1. **Google Search Console:** A fonte da verdade direta do buscador.
2. **AnswerThePublic:** Para entender a intenção de busca.
3. **Screaming Frog (Versão Free):** Para auditorias técnicas básicas.
4. **Google Trends:** Para capturar tendências sazonais antes da concorrência.
`
},
{
id: '15',
slug: 'seo-para-saas-estratatégias-de-growth',
title: 'SEO para SaaS: Estratégias de Growth Através do Orgânico',
excerpt: 'Como empresas de software escalam aquisição de usuários usando conteúdo técnico.',
category: 'Negócios',
publishedAt: '2026-02-25',
image: 'https://images.unsplash.com/photo-1526628953301-3e589a6a8b74?auto=format&fit=crop&q=80&w=1600',
readTime: '15 min',
author: {
name: 'Beatriz Costa',
role: 'Head of Content',
avatar: 'https://i.pravatar.cc/150?u=beatriz'
},
metaTitle: 'SaaS SEO Strategy 2026 | Growth Marketing',
metaDescription: "Growth SEO para SaaS: Como escalar a aquisição de usuários no orgânico. Aprenda a estruturar o funil de conteúdo para converter buscas em usuários ativos de software.",
tags: ['SaaS', 'Growth'],
lang: 'pt-br',
content: `
# SEO para SaaS: Estratégias de Growth
O custo de aquisição (CAC) via anúncios subiu 400% nos últimos anos. Para empresas SaaS, o orgânico tornou-se o canal de crescimento mais sustentável.
## O Funil de Conteúdo SaaS
- **TOFU:** Resolvendo problemas genéricos (ex: "Como gerenciar projetos").
- **MOFU:** Comparativos (ex: "Software X vs Software Y").
- **BOFU:** Intenção direta (ex: "Melhor CRM para imobiliárias").
`
},
{
id: '16',
slug: 'analise-de-concorrencia-descobrindo-gaps',
title: 'Análise de Concorrência: Descobrindo Gaps de Conteúdo',
excerpt: 'Como encontrar o que seus concorrentes não estão cobrindo e dominar essas buscas.',
category: 'Estratégia',
publishedAt: '2026-02-20',
image: 'https://images.unsplash.com/photo-1542744173-8e7e53415bb0?auto=format&fit=crop&q=80&w=1600',
readTime: '9 min',
author: {
name: 'Dr. Lucas Silva',
role: 'Lead Strategist',
avatar: 'https://i.pravatar.cc/150?u=lucas'
},
metaTitle: 'Análise de Concorrentes SEO 2026',
metaDescription: "Descubra os pontos fracos dos seus concorrentes. Guia de Análise de Concorrência SEO para identificar gaps de conteúdo e dominar nichos subexplorados.",
tags: ['Competition', 'Strategy'],
lang: 'pt-br',
content: `
# Análise de Concorrência: Gaps de Conteúdo
Não tente ganhar dos gigantes nas palavras-chave mais difíceis. Procure os **Gaps**. Onde a resposta deles é incompleta? Onde eles estão desatualizados?
## Ferramentas de Gap
Use relatórios de "Content Gap" para ver onde seus dois maiores concorrentes rankeiam mas você não. Esse é o seu roteiro de conteúdo para o próximo trimestre.
`
},
{
id: '17',
slug: 'otimizacao-de-imagens-para-google-discover',
title: 'Otimização de Imagens para o Google Discover',
excerpt: 'Como aparecer no feed de notícias do Google com imagens de impacto e tags corretas.',
category: 'Técnico',
publishedAt: '2026-02-15',
image: 'https://images.unsplash.com/photo-1493612276216-ee3925520721?auto=format&fit=crop&q=80&w=1600',
readTime: '7 min',
author: {
name: 'Marcos Tech',
role: 'SEO Engineer',
avatar: 'https://i.pravatar.cc/150?u=marcos'
},
metaTitle: 'Google Discover SEO 2026',
metaDescription: "Google Discover SEO: O segredo para atrair milhões de acessos via imagens de impacto. Requisitos técnicos e táticas visuais para dominar o feed em 2026.",
tags: ['Images', 'Discover'],
lang: 'pt-br',
content: `
# Imagens no Google Discover
O Discover é visual. Se a sua imagem não "parar o polegar" do usuário, seu CTR será nulo.
## Requisitos Técnicos
- **Tamanho:** Mínimo de 1200px de largura.
- **Alt Text:** Descritivo, mas focado no contexto visual.
- **Qualidade:** Evite fotos de estúdio genéricas; fotos reais com impacto emocional performam 3x melhor.
`
},
{
id: '18',
slug: 'seo-para-news-otimizando-artigos-de-noticias',
title: 'SEO para News: Otimizando Artigos em Tempo Real',
excerpt: 'Técnicas para aparecer no Google News e Top Stories de forma instantânea.',
category: 'Estratégia',
publishedAt: '2026-02-10',
image: 'https://images.unsplash.com/photo-1495020689067-958852a7765e?auto=format&fit=crop&q=80&w=1600',
readTime: '11 min',
author: {
name: 'Beatriz Costa',
role: 'Head of Content',
avatar: 'https://i.pravatar.cc/150?u=beatriz'
},
metaTitle: 'Google News SEO 2026',
metaDescription: "Domine o Top Stories e Google News. Aprenda técnicas de SEO em tempo real para portais de notícias, focando em velocidade de indexação e autoridade do autor.",
tags: ['News', 'Real Time'],
lang: 'pt-br',
content: `
# SEO para News
Notícia é o ápice da temporariedade. Rankear no "Top Stories" exige uma combinação de autoridade de marca e velocidade de indexação técnica.
## Entidades e Jornalismo
O Google agora identifica a entidade do jornalista. Quem escreveu? Esse autor tem histórico no assunto? Isso é o E-E-A-T aplicado ao jornalismo digital.
`
},
{
id: '19',
slug: 'impacto-dos-links-nofollow-e-ugc',
title: 'O Impacto Real dos Links Nofollow, Sponsored e UGC',
excerpt: 'Analisando como os hints de atributo de link influenciam o algoritmo moderno.',
category: 'Autoridade',
publishedAt: '2026-02-05',
image: 'https://images.unsplash.com/photo-1544197150-b99a580bb7a8?auto=format&fit=crop&q=80&w=1600',
readTime: '13 min',
author: {
name: 'João Links',
role: 'PR Specialist',
avatar: 'https://i.pravatar.cc/150?u=joao'
},
metaTitle: 'Link Attributes 2026 | Nofollow vs Sponsored',
metaDescription: "Nofollow, Sponsored ou UGC? Entenda o impacto real dos atributos de link no algoritmo do Google em 2026 e quando usar cada um para sua autoridade.",
tags: ['Attributes', 'Links'],
lang: 'pt-br',
content: `
# Links Nofollow e UGC em 2026
Desde que se tornaram "hints", os links nofollow pararam de ser ignorados. O Google agora decide se quer ou não passar autoridade por eles.
## Quando usar cada um?
- **UGC:** Para links em comentários de blog ou fóruns.
- **Sponsored:** Para qualquer tipo de publieditorial or link pago.
- **Nofollow:** Para links que você não quer necessariamente endossar.
`
},
{
id: '20',
slug: 'monetizacao-blog-seo-autoridade',
title: 'Monetização de Blog através de SEO de Autoridade',
excerpt: 'Como transformar seu tráfego orgânico em uma máquina de receita sustentável.',
category: 'Negócios',
publishedAt: '2026-02-01',
image: 'https://images.unsplash.com/photo-1553729459-efe14ef6055d?auto=format&fit=crop&q=80&w=1600',
readTime: '16 min',
author: {
name: 'Ricardo Vendas',
role: 'E-commerce Expert',
avatar: 'https://i.pravatar.cc/150?u=ricardo'
},
metaTitle: 'Monetização SEO 2026 | Guia de Lucratividade',
metaDescription: "Como lucrar com seu blog de autoridade em 2026. Saia das métricas de vaidade e transforme seu tráfego orgânico em uma máquina de receita sustentável.",
tags: ['Monetization', 'Business'],
lang: 'pt-br',
content: `
# Monetização de Blogs de Autoridade
Tráfego é vaidade, lucro é sanidade. Em 2026, os blogs de nicho mais lucrativos saíram do AdSense e foram para modelos de afiliados de alto ticket e produtos digitais próprios.
## Estrutura de Conversão
Cada artigo deve ter um objetivo de conversão claro. Se o usuário veio por uma dúvida técnica, ofereça uma ferramenta ou curso que resolva essa dor.
`
},
{
id: '21',
slug: 'the-future-of-organic-search-2026',
title: 'The Future of Organic Search in 2026',
excerpt: 'How Search Generative Experiences are redefining digital landscapes.',
category: 'Estratégia',
publishedAt: '2026-05-04',
image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=1600',
readTime: '10 min',
author: {
name: 'Dr. Lucas Silva',
role: 'Lead Strategist',
avatar: 'https://i.pravatar.cc/150?u=lucas'
},
metaTitle: 'The Future of Search | English Edition',
metaDescription: "Organic Search 2026: Deep dive into the future of SEO, SGE, and AI-driven user behavior. Learn how to stay ahead in the evolving digital landscape.",
tags: ['AI', 'SEO', 'Future'],
lang: 'en',
content: `
# The Future of Organic Search in 2026
The search landscape is evolving rapidly. Artificial intelligence is no longer just a backend tool; it's the interface itself.
## SGE and User Behavior
Search Generative Experience (SGE) provides direct answers at the top of the SERP, reducing the need for clicks on simple informational queries. Content creators must adapt by offering unique insights that go beyond simple data aggregation.
`
}
];

92
Template-01/src/index.css Normal file
View file

@ -0,0 +1,92 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Playfair+Display:ital,wght@0,700;0,800;0,900;1,700&display=swap');
@import "tailwindcss";
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-serif: "Playfair Display", ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
}
/* Hide Google Translate Bar */
.goog-te-banner-frame.skiptranslate,
.goog-te-banner-frame,
#goog-gt-tt,
.goog-te-balloon-frame {
display: none !important;
}
body {
top: 0 !important;
}
.goog-tooltip {
display: none !important;
}
.goog-tooltip:hover {
display: none !important;
}
.goog-text-highlight {
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
}
@layer base {
body {
@apply text-slate-900 antialiased bg-white;
}
h1, h2, h3, h4, h5, h6 {
@apply tracking-tight;
}
}
@layer components {
/* High-end Editorial Typography for Markdown Content */
.markdown-body {
@apply mx-auto max-w-3xl text-[19px] text-slate-700 leading-[1.7] font-[Inter];
}
.markdown-body h2 {
@apply text-3xl sm:text-4xl font-serif font-bold text-slate-950 mt-16 mb-8 tracking-tighter;
}
.markdown-body h3 {
@apply text-2xl font-bold text-slate-900 mt-12 mb-6 tracking-tight;
}
.markdown-body p {
@apply mb-8;
}
.markdown-body ul, .markdown-body ol {
@apply mb-10 ml-6 space-y-4;
}
.markdown-body ul {
@apply list-disc;
}
.markdown-body ol {
@apply list-decimal;
}
/* Large Blockquotes for Authority */
.markdown-body blockquote {
@apply pl-10 border-l-4 border-blue-600 italic text-slate-950 font-serif text-3xl my-16 leading-relaxed bg-[#F8FAFC] py-10 pr-10 rounded-r-3xl;
}
.markdown-body strong {
@apply font-bold text-slate-950;
}
.markdown-body a {
@apply text-blue-600 font-bold underline decoration-blue-100 underline-offset-4 decoration-2 hover:decoration-blue-600 transition-all;
}
/* Large Editorial Images */
.markdown-body img {
@apply rounded-[40px] my-16 shadow-xl border border-slate-100;
}
.markdown-body hr {
@apply border-slate-100 my-16;
}
}

View file

@ -0,0 +1,14 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatDate(date: string) {
return new Intl.DateTimeFormat('pt-BR', {
day: 'numeric',
month: 'long',
year: 'numeric',
}).format(new Date(date));
}

10
Template-01/src/main.tsx Normal file
View file

@ -0,0 +1,10 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

View file

@ -0,0 +1,77 @@
import { motion } from 'motion/react';
import SEO from '../components/SEO';
import { Target, Zap, ShieldCheck } from 'lucide-react';
export default function About() {
return (
<>
<SEO
title="Nosso Manifesto | A Ciência da Autoridade Digital"
description="Entenda a filosofia por trás do SEO Authority e por que acreditamos que a busca orgânica é o pilar de qualquer negócio perene."
/>
<section className="pt-24 pb-20 bg-white">
<div className="mx-auto max-w-4xl px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-20"
>
<span className="text-blue-600 font-bold uppercase tracking-[0.3em] text-[10px] mb-6 block">Nosso Manifesto</span>
<h1 className="text-6xl sm:text-7xl font-serif font-bold text-slate-950 mb-8 tracking-tighter">
Verdade sobre <span className="italic font-normal">Algoritmos.</span>
</h1>
<p className="text-xl text-slate-500 leading-relaxed max-w-2xl mx-auto">
No SEO Authority, não acreditamos em "hacks". Acreditamos em autoridade semântica, relevância técnica e o fim do conteúdo genérico.
</p>
</motion.div>
<div className="markdown-body">
<h2>Por que existimos?</h2>
<p>
O mercado de marketing digital foi inundado por conteúdo superficial e táticas de curto prazo que morrem a cada atualização do Google. Nós surgimos para ser o contraponto: uma publicação técnica dedicada à profundidade.
</p>
<div className="my-16 grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="p-8 bg-slate-50 rounded-[32px] border border-slate-100">
<Target className="text-blue-600 mb-6" size={32} />
<h4 className="text-xl font-bold mb-4">Intenção de Busca</h4>
<p className="text-slate-600 text-sm leading-relaxed">
Não focamos em volume de palavras-chave, mas na resolução real do problema do usuário. Se o usuário está satisfeito, o Google também está.
</p>
</div>
<div className="p-8 bg-slate-50 rounded-[32px] border border-slate-100">
<Zap className="text-blue-600 mb-6" size={32} />
<h4 className="text-xl font-bold mb-4">Velocidade Técnica</h4>
<p className="text-slate-600 text-sm leading-relaxed">
A infraestrutura é o alicerce. Sem um site rápido e acessível, a melhor estratégia de conteúdo do mundo é invisível.
</p>
</div>
</div>
<blockquote>
"O SEO moderno não é sobre enganar o robô, é sobre ser a melhor resposta possível em um mar de ruído."
</blockquote>
<h2>Nossa Visão 2026</h2>
<p>
Com a ascensão da IA regenerativa e das buscas conversacionais, a autoridade de marca tornou-se o único escudo contra a desintermediação. Nossa missão é documentar essa transição.
</p>
<div className="mt-12 p-10 bg-slate-950 rounded-[40px] text-white flex flex-col md:flex-row items-center gap-10">
<div className="shrink-0">
<ShieldCheck size={64} className="text-blue-500" />
</div>
<div>
<h3 className="text-white mt-0 mb-4 font-serif">Compromisso com a Precisão</h3>
<p className="text-slate-400 text-sm mb-0">
Cada reportagem ou guia técnico publicado passa por um processo de verificação de dados e testes em propriedades reais antes de chegar até você.
</p>
</div>
</div>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,361 @@
import { useParams, Link, useNavigate } from 'react-router-dom';
import { articles } from '../data/articles';
import SEO from '../components/SEO';
import ReactMarkdown from 'react-markdown';
import { Clock, Calendar, ChevronLeft, Share2, Bookmark, ArrowRight, MessageCircle, Loader2, Sparkles, Twitter, Linkedin, Facebook, Link2, CheckCircle2 } from 'lucide-react';
import { motion, AnimatePresence, useScroll, useSpring } from 'motion/react';
import { formatDate, cn } from '../lib/utils';
import CategoryBadge from '../components/CategoryBadge';
import { useLanguage } from '../contexts/LanguageContext';
import { translations } from '../constants';
import { useState, useEffect } from 'react';
import { Article } from '../types';
import { useBookmarks } from '../contexts/BookmarksContext';
import TableOfContents from '../components/TableOfContents';
export default function BlogPost() {
const { slug } = useParams();
const navigate = useNavigate();
const { lang, translate } = useLanguage();
const { toggleBookmark, isBookmarked } = useBookmarks();
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001
});
const [translatedArticle, setTranslatedArticle] = useState<Article | undefined>();
const [loading, setLoading] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const [showToast, setShowToast] = useState(false);
const originalArticle = articles.find(a => a.slug === slug);
useEffect(() => {
if (!originalArticle) return;
const translateFullArticle = async () => {
// If language is Portuguese, just use the original
if (lang === 'pt-br') {
setTranslatedArticle(originalArticle);
return;
}
setLoading(true);
try {
// Use Gemini to translate all parts in one shot
const translated = await translate({
title: originalArticle.title,
excerpt: originalArticle.excerpt,
content: originalArticle.content,
metaTitle: originalArticle.metaTitle,
metaDescription: originalArticle.metaDescription
}, 'full article content', true);
setTranslatedArticle({
...originalArticle,
...translated
});
} catch (error) {
console.error("Auto-translation error:", error);
setTranslatedArticle(originalArticle);
} finally {
setLoading(false);
}
};
translateFullArticle();
}, [lang, originalArticle, slug]);
const copyToClipboard = () => {
navigator.clipboard.writeText(window.location.href);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
};
const article = translatedArticle || originalArticle;
if (!article) {
return (
<div className="min-h-screen flex items-center justify-center p-4">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Artigo não encontrado</h1>
<button onClick={() => navigate('/')} className="text-blue-600 hover:underline">Voltar para a Home</button>
</div>
</div>
);
}
const relatedArticles = articles.filter(a => a.category === article.category && a.id !== article.id).slice(0, 2);
return (
<>
<SEO
title={article.metaTitle}
description={article.metaDescription}
ogType="article"
ogImage={article.image}
keywords={article.tags}
articleData={{
publishedTime: article.publishedAt,
author: article.author.name,
authorAvatar: article.author.avatar,
tags: article.tags,
section: article.category
}}
/>
<article className="pb-32 bg-white relative">
<motion.div
className="fixed top-20 left-0 right-0 h-1 bg-blue-600 origin-left z-[60]"
style={{ scaleX }}
/>
<AnimatePresence>
{loading && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 bg-white/90 backdrop-blur-md flex items-center justify-center p-6 text-center"
>
<div className="max-w-md w-full p-10 bg-white rounded-[40px] shadow-2xl border border-slate-100">
<div className="relative mb-8 inline-block">
<div className="absolute inset-0 bg-blue-100 rounded-full animate-ping opacity-20" />
<div className="relative bg-blue-50 p-6 rounded-full">
<Loader2 className="w-12 h-12 text-blue-600 animate-spin" />
</div>
<Sparkles className="absolute -top-1 -right-1 text-yellow-400 animate-pulse" />
</div>
<h3 className="text-2xl font-serif font-bold text-slate-900 mb-3 italic">
Afinando a Autoridade...
</h3>
<p className="text-slate-500 font-medium leading-relaxed">
Estamos usando Inteligência Artificial para traduzir esta reportagem mantendo o tom editorial exclusivo.
</p>
<div className="mt-8 flex justify-center gap-1">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
animate={{ scale: [1, 1.5, 1], opacity: [0.3, 1, 0.3] }}
transition={{ duration: 1.5, repeat: Infinity, delay: i * 0.2 }}
className="w-2 h-2 bg-blue-600 rounded-full"
/>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Editorial Header */}
<header className="pt-20 pb-20 border-b border-slate-100">
<div className="mx-auto max-w-4xl px-4">
<div className="flex items-center gap-3 text-blue-600 font-bold uppercase tracking-[0.2em] text-[10px] mb-8">
<div className="h-[1px] w-6 bg-blue-600" />
<span>{article.category}</span>
</div>
<h1 className="text-5xl sm:text-7xl font-serif font-bold text-slate-950 leading-[1.1] mb-12 tracking-tighter">
{article.title}
</h1>
<div className="flex flex-wrap items-center justify-between gap-8 pt-8 border-t border-slate-100">
<div className="flex items-center gap-4">
<img src={article.author.avatar} alt={article.author.name} className="h-12 w-12 rounded-full grayscale" />
<div>
<p className="text-sm font-bold text-slate-900">{article.author.name}</p>
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-400">{article.author.role}</p>
</div>
</div>
<div className="flex items-center gap-8 text-slate-400 font-bold uppercase tracking-widest text-[10px]">
<div className="flex items-center gap-2">
<Calendar size={14} />
<span>{formatDate(article.publishedAt)}</span>
</div>
<div className="flex items-center gap-2">
<Clock size={14} />
<span>{article.readTime} reading</span>
</div>
</div>
</div>
</div>
</header>
{/* Featured Image - Compact editorial style */}
<div className="mx-auto max-w-7xl px-4 lg:px-8 py-16">
<div className="aspect-[21/9] rounded-[40px] overflow-hidden bg-slate-100">
<img
src={article.image}
alt={article.title}
className="h-full w-full object-cover"
referrerPolicy="no-referrer"
/>
</div>
</div>
{/* Post Content */}
<div className="mx-auto max-w-7xl px-4 lg:px-8 flex flex-col lg:flex-row gap-24">
{/* Main Content */}
<div className="lg:w-2/3">
<div className="markdown-body">
<ReactMarkdown
components={{
h2: ({ children }) => {
const id = String(children).toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '');
return <h2 id={id} className="scroll-mt-32 uppercase tracking-tight">{children}</h2>;
},
h3: ({ children }) => {
const id = String(children).toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '');
return <h3 id={id} className="scroll-mt-32">{children}</h3>;
}
}}
>
{article.content}
</ReactMarkdown>
</div>
{/* Tags */}
<div className="mt-16 flex flex-wrap gap-2 pt-8 border-t border-gray-100">
{article.tags.map(tag => (
<span key={tag} className="px-3 py-1 bg-gray-100 text-gray-600 rounded-full text-xs font-bold uppercase tracking-wider">
#{tag}
</span>
))}
</div>
{/* Author Card (Bottom) */}
<div className="mt-16 p-8 bg-gray-50 rounded-3xl flex items-center gap-6 border border-gray-100">
<img src={article.author.avatar} alt={article.author.name} className="h-20 w-20 rounded-full" />
<div>
<h4 className="text-xl font-bold text-gray-900 mb-2">Escrito por {article.author.name}</h4>
<p className="text-gray-600">{article.author.role}. Especialista em estratégias de crescimento orgânico e autoridade digital.</p>
</div>
</div>
</div>
{/* Sidebar */}
<aside className="lg:w-1/3 space-y-12">
<TableOfContents content={article.content} />
{/* Share & Actions */}
<div className="sticky top-24 space-y-12">
<div className="p-8 bg-white border border-gray-100 rounded-3xl shadow-sm">
<h5 className="font-bold text-slate-900 mb-6 uppercase text-[10px] tracking-[0.2em] opacity-50">Compartilhar Artigo</h5>
<div className="flex flex-col gap-3">
<div className="flex gap-2">
<a href="#" className="flex-1 flex items-center justify-center gap-2 py-3 rounded-2xl bg-[#1DA1F2] text-white hover:opacity-90 transition-opacity">
<Twitter size={18} fill="currentColor" />
</a>
<a href="#" className="flex-1 flex items-center justify-center gap-2 py-3 rounded-2xl bg-[#0A66C2] text-white hover:opacity-90 transition-opacity">
<Linkedin size={18} fill="currentColor" />
</a>
<a href="#" className="flex-1 flex items-center justify-center gap-2 py-3 rounded-2xl bg-[#1877F2] text-white hover:opacity-90 transition-opacity">
<Facebook size={18} fill="currentColor" />
</a>
</div>
<button
onClick={copyToClipboard}
className="w-full flex items-center justify-center gap-3 py-4 rounded-2xl bg-slate-50 hover:bg-slate-100 text-slate-900 font-bold text-xs uppercase tracking-widest transition-all"
>
{isCopied ? (
<>Copiado!</>
) : (
<><Link2 size={16} /> Copiar Link</>
)}
</button>
<button
onClick={() => {
toggleBookmark(article.id);
setShowToast(true);
setTimeout(() => setShowToast(false), 3000);
}}
className={cn(
"w-full flex items-center justify-center gap-3 py-4 rounded-2xl border font-bold text-xs uppercase tracking-widest transition-all",
isBookmarked(article.id)
? "bg-blue-600 border-blue-600 text-white shadow-lg shadow-blue-100"
: "border-slate-100 hover:bg-slate-50 text-slate-500"
)}
>
<Bookmark size={16} fill={isBookmarked(article.id) ? "currentColor" : "none"} />
{isBookmarked(article.id) ? "Salvo na Biblioteca" : "Salvar Leitura"}
</button>
</div>
</div>
{/* Related Articles */}
{relatedArticles.length > 0 && (
<div>
<h5 className="font-bold text-gray-900 mb-6 uppercase text-xs tracking-widest">Relacionados</h5>
<div className="space-y-6">
{relatedArticles.map(rel => (
<Link key={rel.id} to={`/artigo/${rel.slug}`} className="group flex flex-col gap-2">
<img src={rel.image} className="aspect-video object-cover rounded-xl" referrerPolicy="no-referrer" />
<h6 className="font-bold text-gray-900 group-hover:text-blue-600 transition-colors leading-snug">
{rel.title}
</h6>
</Link>
))}
</div>
</div>
)}
{/* Lead Magnet */}
<div className="p-8 bg-blue-600 rounded-2xl text-white">
<h5 className="text-2xl font-bold mb-4">Checklist Gratuito</h5>
<p className="text-blue-100 mb-6">Aprenda a otimizar cada página do seu site em menos de 10 minutos.</p>
<button className="w-full p-4 bg-white text-blue-600 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-blue-50 transition-all">
Baixar Checklist <ArrowRight size={18} />
</button>
</div>
</div>
</aside>
</div>
</article>
{/* Page Content */}
<AnimatePresence>
{showToast && (
<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9 }}
className="fixed bottom-8 left-1/2 -translate-x-1/2 z-[100] bg-slate-900 text-white px-8 py-4 rounded-3xl shadow-2xl flex items-center gap-4 min-w-[300px]"
>
<div className="h-10 w-10 bg-emerald-500 rounded-full flex items-center justify-center">
<CheckCircle2 size={20} />
</div>
<div>
<p className="text-sm font-bold tracking-tight">
{isBookmarked(article.id) ? "Artigo Salvo!" : "Removido!"}
</p>
<Link to="/leituras-salvas" className="text-[10px] text-blue-400 font-bold uppercase tracking-widest hover:underline">
Ver minha biblioteca
</Link>
</div>
</motion.div>
)}
</AnimatePresence>
<section className="bg-gray-50 py-16">
<div className="mx-auto max-w-4xl px-4 text-center">
<MessageCircle size={48} className="mx-auto text-blue-600 mb-6" />
<h2 className="text-3xl font-bold mb-4">Gostou deste conteúdo?</h2>
<p className="text-lg text-gray-600 mb-8">Nossa missão é compartilhar conhecimento de SEO de alta performance gratuitamente.</p>
<div className="flex justify-center gap-4">
<Link to="/" className="px-8 py-3 bg-black text-white rounded-full font-bold">Ver mais artigos</Link>
<button className="px-8 py-3 border border-gray-300 rounded-full font-bold">Enviar feedback</button>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,95 @@
import { motion } from 'motion/react';
import { Bookmark as BookmarkIcon, ArrowRight, Trash2 } from 'lucide-react';
import SEO from '../components/SEO';
import { useBookmarks } from '../contexts/BookmarksContext';
import { articles } from '../data/articles';
import { Link } from 'react-router-dom';
import CategoryBadge from '../components/CategoryBadge';
export default function Bookmarks() {
const { bookmarks, toggleBookmark } = useBookmarks();
const savedArticles = articles.filter(a => bookmarks.includes(a.id));
return (
<div className="pt-32 pb-24">
<SEO
title="Leituras Salvas | SEO Authority"
description="Sua lista personalizada de artigos para ler depois."
/>
<div className="max-w-7xl mx-auto px-6">
<header className="mb-16">
<div className="flex items-center gap-3 text-blue-600 font-bold uppercase tracking-[0.2em] text-[10px] mb-6">
<div className="h-[1px] w-6 bg-blue-600" />
<span>Biblioteca Pessoal</span>
</div>
<h1 className="text-6xl font-serif font-bold text-slate-900 italic tracking-tight">
Para ler <span className="text-blue-600 underline underline-offset-8 decoration-blue-200">depois</span>.
</h1>
<p className="mt-6 text-xl text-slate-500 max-w-lg leading-relaxed">
Consulte aqui seus insights salvos para aplicar em sua estratégia quando estiver pronto.
</p>
</header>
{savedArticles.length > 0 ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{savedArticles.map((article, i) => (
<motion.div
key={article.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.1 }}
className="group bg-white rounded-[40px] border border-slate-100 p-8 flex flex-col hover:shadow-2xl hover:shadow-slate-200 transition-all h-full"
>
<div className="flex justify-between items-start mb-8">
<CategoryBadge category={article.category} variant="outline" />
<button
onClick={() => toggleBookmark(article.id)}
className="p-2 text-slate-300 hover:text-red-500 transition-colors"
title="Remover"
>
<Trash2 size={18} />
</button>
</div>
<Link to={`/artigo/${article.slug}`} className="flex-grow">
<h3 className="text-2xl font-serif font-bold text-slate-900 group-hover:text-blue-600 transition-colors mb-4 line-clamp-2 italic">
{article.title}
</h3>
<p className="text-slate-500 text-sm leading-relaxed mb-6 line-clamp-3">
{article.excerpt}
</p>
</Link>
<Link
to={`/artigo/${article.slug}`}
className="inline-flex items-center gap-3 text-slate-900 font-bold uppercase text-[10px] tracking-widest group/link"
>
Continuar Leitura
<ArrowRight size={14} className="group-hover/link:translate-x-1 transition-transform" />
</Link>
</motion.div>
))}
</div>
) : (
<div className="bg-slate-50 rounded-[40px] p-20 text-center border-2 border-dashed border-slate-200">
<div className="h-20 w-20 bg-white shadow-xl rounded-2xl flex items-center justify-center text-slate-200 mx-auto mb-8">
<BookmarkIcon size={32} />
</div>
<h2 className="text-3xl font-serif font-bold text-slate-900 italic mb-4">Sua lista está vazia</h2>
<p className="text-slate-500 mb-10 max-w-md mx-auto">
Navegue pelo nosso editorial e salve os artigos que mais te interessam para consultá-los aqui.
</p>
<Link
to="/"
className="inline-flex h-14 px-8 bg-slate-900 text-white rounded-2xl font-bold uppercase text-[10px] tracking-[0.2em] items-center justify-center gap-3 hover:bg-blue-600 transition-all"
>
Explorar Artigos
</Link>
</div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,81 @@
import { useParams, Link } from 'react-router-dom';
import { articles } from '../data/articles';
import ArticleCard from '../components/ArticleCard';
import SEO from '../components/SEO';
import { ChevronRight } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
import { CATEGORY_MAP } from '../constants';
export default function CategoryPage() {
const { categorySlug } = useParams();
const { lang } = useLanguage();
const getCategoryData = (slug: string) => {
const entry = Object.entries(CATEGORY_MAP).find(([_, value]) => value.slug === slug);
if (entry) {
const [originalName, data] = entry;
return {
name: data.name,
originalName: originalName,
description: `Deep dives, case studies and tactical analysis on ${data.name.toLowerCase()}.`
};
}
return {
name: slug.charAt(0).toUpperCase() + slug.slice(1).replace(/-/g, ' '),
originalName: slug,
description: 'Artigos e análises sobre o ecossistema digital.'
};
};
const { name: categoryName, originalName, description: categoryDescription } = getCategoryData(categorySlug || '');
const filteredArticles = articles.filter(a => {
// If we have a map entry, match by original category name
if (originalName) {
return a.category === originalName;
}
// Fallback strategy
const slugfiedCategory = a.category.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\s+/g, '-');
return slugfiedCategory === categorySlug;
});
return (
<>
<SEO
title={`Artigos sobre ${categoryName}`}
description={`Confira guias e dicas exclusivas sobre ${categoryName} em nosso blog de autoridade SEO.`}
/>
<div className="bg-white py-20 border-b border-slate-100">
<div className="mx-auto max-w-7xl px-4">
<div className="flex items-center gap-3 text-blue-600 font-bold uppercase tracking-[0.2em] text-[10px] mb-6">
<div className="h-[1px] w-6 bg-blue-600" />
<span>Arquivo Editorial</span>
</div>
<h1 className="text-6xl font-serif font-bold text-slate-950 tracking-tighter">{categoryName}</h1>
<p className="text-xl text-slate-500 mt-6 max-w-2xl leading-relaxed">{categoryDescription}</p>
</div>
</div>
<div className="py-24 bg-[#FCFCFC]">
<div className="mx-auto max-w-7xl px-4">
{filteredArticles.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-20">
{filteredArticles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
</div>
) : (
<div className="text-center py-20">
<h2 className="text-2xl font-bold text-gray-400">Nenhum artigo encontrado nesta categoria.</h2>
<Link to="/" className="text-blue-600 font-bold mt-4 inline-block hover:underline">Voltar para a Home</Link>
</div>
)}
</div>
</div>
</>
);
}

View file

@ -0,0 +1,143 @@
import { motion } from 'motion/react';
import { Mail, MessageCircle, Send } from 'lucide-react';
import { useState } from 'react';
import SEO from '../components/SEO';
import Layout from '../components/Layout';
export default function Contact() {
const [submitted, setSubmitted] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
};
return (
<>
<SEO
title="Contato | Blog SEO de Autoridade"
description="Entre em contato conosco para parcerias, consultorias ou dúvidas sobre SEO e marketing digital."
/>
<div className="pt-32 pb-24">
<div className="max-w-7xl mx-auto px-6">
<div className="grid lg:grid-cols-2 gap-20">
{/* Coluna da Esquerda: Info */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="space-y-12"
>
<div>
<h1 className="text-6xl font-serif font-bold text-slate-900 italic mb-6 leading-tight">
Vamos falar <br />de <span className="text-blue-600 underline decoration-blue-200 underline-offset-8">crescimento</span>.
</h1>
<p className="text-xl text-slate-500 max-w-md leading-relaxed">
Tem um projeto em mente ou quer escalar sua autoridade orgânica? Nossa equipe de estrategistas está pronta para ouvir.
</p>
</div>
<div className="space-y-8">
<div className="flex gap-6 group">
<div className="h-14 w-14 rounded-2xl bg-blue-50 flex items-center justify-center text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-all">
<Mail size={24} />
</div>
<div>
<h4 className="font-bold text-slate-900 text-lg">E-mail</h4>
<p className="text-slate-500">contato@seoblog.com</p>
</div>
</div>
<a
href="https://wa.me/5511999999999"
target="_blank"
rel="noopener noreferrer"
className="flex gap-6 group"
>
<div className="h-14 w-14 rounded-2xl bg-emerald-50 flex items-center justify-center text-emerald-600 group-hover:bg-emerald-600 group-hover:text-white transition-all">
<MessageCircle size={24} />
</div>
<div>
<h4 className="font-bold text-slate-900 text-lg">WhatsApp</h4>
<p className="text-slate-500 font-medium group-hover:text-emerald-600 transition-colors">+55 (11) 99999-9999</p>
</div>
</a>
</div>
</motion.div>
{/* Coluna da Direita: Form */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white p-12 rounded-[40px] border border-slate-100 shadow-2xl shadow-slate-200/50"
>
{submitted ? (
<div className="h-full flex flex-col items-center justify-center text-center space-y-6">
<div className="h-20 w-20 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center">
<Send size={32} />
</div>
<h2 className="text-3xl font-bold text-slate-900 italic font-serif">Mensagem enviada!</h2>
<p className="text-slate-500">Obrigado pelo contato. Responderemos em até 24 horas.</p>
<button
onClick={() => setSubmitted(false)}
className="text-blue-600 font-bold uppercase text-xs tracking-widest hover:underline"
>
Enviar outra mensagem
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-slate-400 ml-1">Nome Completo</label>
<input
required
type="text"
placeholder="Ex: João Silva"
className="w-full px-6 py-4 rounded-2xl bg-slate-50 border-none focus:ring-2 focus:ring-blue-600 transition-all font-medium text-slate-900"
/>
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-slate-400 ml-1">E-mail Corporativo</label>
<input
required
type="email"
placeholder="seu@empresa.com"
className="w-full px-6 py-4 rounded-2xl bg-slate-50 border-none focus:ring-2 focus:ring-blue-600 transition-all font-medium text-slate-900"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-slate-400 ml-1">Assunto</label>
<select className="w-full px-6 py-4 rounded-2xl bg-slate-50 border-none focus:ring-2 focus:ring-blue-600 transition-all font-medium text-slate-900 appearance-none">
<option>Consultoria SEO</option>
<option>Parceria de Conteúdo</option>
<option>Reportar Erro Técnico</option>
<option>Outros</option>
</select>
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold uppercase tracking-widest text-slate-400 ml-1">Sua Mensagem</label>
<textarea
required
placeholder="Como podemos ajudar você hoje?"
rows={5}
className="w-full px-6 py-4 rounded-2xl bg-slate-50 border-none focus:ring-2 focus:ring-blue-600 transition-all font-medium text-slate-900 resize-none"
></textarea>
</div>
<button className="w-full bg-slate-900 py-6 rounded-2xl text-white font-bold uppercase text-xs tracking-[.2em] hover:bg-blue-600 transition-all flex items-center justify-center gap-3 group">
Enviar Proposta
<Send size={16} className="group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform" />
</button>
</form>
)}
</motion.div>
</div>
</div>
</div>
</>
);
}

View file

@ -0,0 +1,213 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { articles } from '../data/articles';
import ArticleCard from '../components/ArticleCard';
import SEO from '../components/SEO';
import { ArrowRight, TrendingUp } from 'lucide-react';
import { motion } from 'motion/react';
import { useLanguage } from '../contexts/LanguageContext';
import { translations } from '../constants';
import { Article } from '../types';
export default function Home() {
const { lang, translate } = useLanguage();
const t = translations[lang];
const [displayArticles, setDisplayArticles] = useState<Article[]>(articles);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (lang === 'pt-br') {
setDisplayArticles(articles);
return;
}
const translateHeaders = async () => {
setLoading(true);
try {
// Translate only titles and categories for Home page items
const itemsToTranslate = articles.map(a => ({
title: a.title,
category: a.category
}));
const translatedItems = await translate(itemsToTranslate, 'article titles and categories', true);
const newArticles = articles.map((a, i) => ({
...a,
title: translatedItems[i]?.title || a.title,
category: translatedItems[i]?.category || a.category
}));
setDisplayArticles(newArticles);
} catch (error) {
console.error("Home translation error:", error);
} finally {
setLoading(false);
}
};
translateHeaders();
}, [lang, translate]);
const featuredArticle = displayArticles[0];
const recentArticles = displayArticles.slice(1, 4);
const secondaryArticles = displayArticles.slice(4, 10);
return (
<>
<SEO
title={t.heroTitle}
description={t.heroDescription}
/>
{/* Editorial Hero */}
<section className="relative pt-24 pb-20 border-b border-slate-100 bg-white overflow-hidden">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex items-center gap-3 text-blue-600 font-bold uppercase tracking-[0.3em] text-[9px] mb-8"
>
<span>{t.heroSubtitle}</span>
<div className="h-[1px] w-12 bg-blue-100" />
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
className="text-6xl sm:text-8xl font-serif font-bold text-slate-950 mb-10 leading-[0.95] tracking-tight"
>
{lang === 'pt-br' ? (
<>A Nova Ordem da <br /><span className="italic font-normal text-blue-600">Busca Semântica.</span></>
) : t.heroTitle}
</motion.h1>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="flex flex-col sm:flex-row items-start sm:items-center gap-12"
>
<p className="text-xl text-slate-400 max-w-md leading-relaxed font-medium">
{t.heroDescription}
</p>
<div className="flex flex-col gap-4">
<Link
to="/sobre"
className="px-8 py-4 bg-slate-950 text-white rounded-2xl font-bold text-[10px] uppercase tracking-[0.2em] hover:bg-blue-600 transition-all shadow-xl shadow-slate-200 flex items-center justify-center"
>
{t.readManifesto}
</Link>
<div className="flex items-center gap-3">
<div className="h-1.5 w-1.5 bg-blue-600 rounded-full animate-ping" />
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest italic">{t.publishedIn} Maio 2026</span>
</div>
</div>
</motion.div>
</div>
</div>
</section>
{/* Featured Insight */}
<section className="py-24 bg-[#FCFCFC]">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-start">
<div className="lg:col-span-8">
<div className="flex items-center justify-between mb-8 pb-4 border-b border-slate-200">
<h2 className="font-serif text-3xl font-bold italic">{t.recent}</h2>
<Link to="/blog" className="text-xs font-bold uppercase tracking-widest text-slate-400 hover:text-slate-950">{t.viewAll}</Link>
</div>
<ArticleCard article={featuredArticle} featured />
</div>
<div className="lg:col-span-4 space-y-12">
<div className="bg-slate-950 p-10 rounded-[40px] text-white">
<h3 className="text-3xl font-serif font-bold mb-6">{t.newsletterTitle}</h3>
<p className="text-slate-400 mb-8 leading-relaxed">
{t.newsletterDescription}
</p>
<form className="space-y-4" onSubmit={(e) => e.preventDefault()}>
<input
type="email"
placeholder={t.emailPlaceholder}
className="w-full bg-white/10 border border-white/20 rounded-2xl px-6 py-4 text-white placeholder:text-white/30 focus:outline-none focus:border-white/50"
/>
<button className="w-full bg-blue-600 hover:bg-blue-500 py-4 rounded-2xl font-bold transition-all">
{t.subscribeButton}
</button>
</form>
</div>
<div>
<h4 className="font-bold uppercase tracking-widest text-xs text-slate-400 mb-8 pb-4 border-b border-slate-100">{t.trendsTitle}</h4>
<div className="space-y-8">
{recentArticles.map((article, i) => (
<Link key={article.id} to={`/artigo/${article.slug}`} className="group flex gap-4">
<span className="font-serif text-3xl italic text-slate-200 font-bold">0{i+1}</span>
<div>
<h5 className="font-bold text-slate-900 group-hover:text-blue-600 transition-colors leading-snug">
{article.title}
</h5>
<p className="text-xs text-slate-500 mt-2 uppercase font-bold tracking-wider">{article.category}</p>
</div>
</Link>
))}
</div>
</div>
</div>
</div>
</div>
</section>
{/* More Insights */}
<section className="py-24 bg-white">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between mb-16 px-4">
<h2 className="font-serif text-4xl font-bold">{t.explore}</h2>
<Link to="/arquivo" className="h-12 w-12 rounded-full border border-slate-200 flex items-center justify-center hover:border-slate-950 transition-colors">
<ArrowRight size={18} />
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-16">
{secondaryArticles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
</div>
</div>
</section>
{/* Newsletter Section */}
<section id="newsletter" className="py-24 bg-white border-t border-slate-100">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="bg-slate-950 rounded-[48px] p-10 sm:p-20 text-center text-white relative overflow-hidden">
<div className="relative z-10 max-w-2xl mx-auto">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
className="inline-flex items-center gap-2 px-3 py-1 bg-white/10 text-white rounded-full text-[10px] font-bold uppercase tracking-widest mb-8"
>
<span>Editorial VIP</span>
</motion.div>
<h2 className="text-4xl sm:text-6xl font-serif font-bold mb-8 italic">{t.footerNewsletterTitle}</h2>
<p className="text-slate-400 text-lg mb-12">
{t.footerNewsletterSubtitle}
</p>
<form className="flex flex-col sm:flex-row gap-4 max-w-md mx-auto" onSubmit={(e) => e.preventDefault()}>
<input
type="email"
placeholder={t.footerNewsletterInput}
className="flex-grow px-8 py-5 rounded-2xl bg-white/5 border border-white/10 text-white focus:outline-none focus:border-blue-600 transition-colors"
/>
<button className="px-10 py-5 bg-blue-600 text-white rounded-2xl font-bold hover:bg-blue-500 transition-all active:scale-95">
{t.subscribe}
</button>
</form>
<p className="mt-8 text-xs text-slate-500 uppercase tracking-widest font-bold">{t.footerNewsletterDisclaimer}</p>
</div>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,80 @@
import { motion } from 'motion/react';
import { Link } from 'react-router-dom';
import SEO from '../components/SEO';
import { BarChart3, Database, Search, FileCheck } from 'lucide-react';
export default function Methodology() {
const steps = [
{
icon: Database,
title: "Coleta de Dados",
desc: "Analisamos mais de 1 milhão de SERPs mensais para identificar padrões de mudança nos algoritmos do Google Anthropic e OpenAI."
},
{
icon: Search,
title: "Análise Semântica",
desc: "Nossos modelos identificam quais entidades e contextos estão gerando os maiores ganhos de visibilidade orgânica."
},
{
icon: BarChart3,
title: "Teste de Campo",
desc: "Implementamos teorias em sites controlados antes de documentá-las como estratégias recomendadas."
},
{
icon: FileCheck,
title: "Validação Editorial",
desc: "Todo insight é revisado por estrategistas com mais de 10 anos de experiência no mercado de busca."
}
];
return (
<>
<SEO
title="Metodologia de Análise | Como avaliamos o Google"
description="Saiba como o SEO Authority produz seus reports e análises técnicas. Dados reais, testes de campo e transparência."
/>
<section className="pt-24 pb-32 bg-[#FCFCFC]">
<div className="mx-auto max-w-7xl px-4 lg:px-8">
<div className="max-w-4xl mb-24">
<span className="text-blue-600 font-bold uppercase tracking-[0.3em] text-[10px] mb-6 block">Padrão de Qualidade</span>
<h1 className="text-5xl sm:text-7xl font-serif font-bold text-slate-950 mb-8 tracking-tighter">
Nossa <span className="italic font-normal">Metodologia.</span>
</h1>
<p className="text-xl text-slate-500 leading-relaxed">
O SEO deixou de ser uma ciência exata para se tornar uma ciência de dados e comportamento humano. Veja como deciframos o código.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-px bg-slate-200 border border-slate-200 rounded-[40px] overflow-hidden">
{steps.map((step, i) => (
<div key={i} className="bg-white p-12 sm:p-16 flex flex-col gap-8 hover:bg-slate-50 transition-colors">
<div className="h-14 w-14 rounded-2xl bg-slate-950 text-white flex items-center justify-center">
<step.icon size={28} />
</div>
<div>
<span className="text-slate-300 font-serif text-4xl block mb-4 italic">0{i+1}.</span>
<h3 className="text-2xl font-bold text-slate-950 mb-4 tracking-tight">{step.title}</h3>
<p className="text-slate-500 leading-relaxed">{step.desc}</p>
</div>
</div>
))}
</div>
<div className="mt-24 p-12 bg-white rounded-[40px] border border-slate-100 text-center max-w-3xl mx-auto shadow-sm">
<h4 className="text-2xl font-serif font-bold mb-6">Acesso aos Dados Brutos</h4>
<p className="text-slate-500 mb-8">
Parceiros e assinantes da modalidade <span className="text-slate-950 font-bold">Authority+</span> têm acesso aos nossos datasets proprietários de flutuação de SERP.
</p>
<Link
to="/contato"
className="px-8 py-4 bg-slate-950 text-white rounded-xl font-bold text-sm uppercase tracking-widest hover:bg-slate-800 transition-all inline-block"
>
Conhecer Assinatura Premium
</Link>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,59 @@
import { motion } from 'motion/react';
import { Link } from 'react-router-dom';
import { Home, ArrowLeft, Search } from 'lucide-react';
import SEO from '../components/SEO';
import Layout from '../components/Layout';
export default function NotFound() {
return (
<div className="min-h-[80vh] flex items-center justify-center pt-20">
<SEO
title="404 - Página Não Encontrada"
description="A página que você está procurando não existe ou foi movida."
/>
<div className="max-w-xl w-full px-6 text-center">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="mb-8"
>
<div className="inline-flex h-24 w-24 items-center justify-center rounded-3xl bg-blue-50 text-blue-600 mb-8 rotate-12">
<Search size={48} />
</div>
<h1 className="text-8xl font-serif font-black text-slate-900 mb-4 italic tracking-tighter">404</h1>
<h2 className="text-3xl font-serif font-bold text-slate-900 mb-6 italic">Página Perdida no Espaço...</h2>
<p className="text-slate-500 text-lg leading-relaxed mb-10">
O conteúdo que você busca parece ter sido reindexado ou movido para uma nova dimensão. Vamos te levar de volta para um lugar seguro.
</p>
</motion.div>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
to="/"
className="w-full sm:w-auto h-14 px-8 bg-slate-900 text-white rounded-2xl font-bold uppercase text-[10px] tracking-[0.2em] flex items-center justify-center gap-3 hover:bg-blue-600 transition-all group"
>
<Home size={16} />
Voltar para Home
</Link>
<button
onClick={() => window.history.back()}
className="w-full sm:w-auto h-14 px-8 border border-slate-200 text-slate-500 rounded-2xl font-bold uppercase text-[10px] tracking-[0.2em] flex items-center justify-center gap-3 hover:bg-slate-50 transition-all"
>
<ArrowLeft size={16} />
Página Anterior
</button>
</div>
<div className="mt-20 pt-10 border-t border-slate-100">
<p className="text-[10px] font-bold text-slate-300 uppercase tracking-[0.3em] mb-8">Navegue pelas categorias</p>
<div className="flex flex-wrap justify-center gap-2">
{['Estratégia', 'IA', 'Link Building', 'Técnico'].map((cat) => (
<span key={cat} className="px-4 py-2 bg-slate-50 text-slate-500 rounded-lg text-xs font-bold uppercase tracking-widest">{cat}</span>
))}
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,39 @@
import { motion } from 'motion/react';
import SEO from '../components/SEO';
export default function Privacy() {
return (
<>
<SEO
title="Política de Privacidade | SEO Authority"
description="Saiba como tratamos seus dados e nosso compromisso com a transparência editorial."
/>
<section className="pt-24 pb-32 bg-white">
<div className="mx-auto max-w-4xl px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-16"
>
<h1 className="text-5xl font-serif font-bold text-slate-950 mb-6 tracking-tighter">Política de Privacidade</h1>
<p className="text-slate-500 uppercase tracking-widest text-[10px] font-bold">Última atualização: Maio de 2026</p>
</motion.div>
<div className="markdown-body">
<p>Sua privacidade é importante para nós. É política do SEO Authority respeitar a sua privacidade em relação a qualquer informação sua que possamos coletar no site.</p>
<h2>1. Coleta de Informações</h2>
<p>Solicitamos informações pessoais apenas quando realmente precisamos delas para lhe fornecer um serviço, como a nossa newsletter. Fazemo-lo por meios justos e legais, com o seu conhecimento e consentimento.</p>
<h2>2. Uso de Dados</h2>
<p>Não compartilhamos informações de identificação pessoal publicamente ou com terceiros, exceto quando exigido por lei. O uso de cookies é limitado à análise de tráfego (Google Analytics) para melhorar sua experiência editorial.</p>
<h2>3. Retenção de Dados</h2>
<p>Apenas retemos as informações coletadas pelo tempo necessário para fornecer o serviço solicitado. Quando armazenamos dados, protegemos dentro de meios comercialmente aceitáveis para evitar perdas e roubos.</p>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,37 @@
import { motion } from 'motion/react';
import SEO from '../components/SEO';
export default function Terms() {
return (
<>
<SEO
title="Termos de Serviço | SEO Authority"
description="Leia os termos de uso da nossa plataforma e publicações digitais."
/>
<section className="pt-24 pb-32 bg-white">
<div className="mx-auto max-w-4xl px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-16"
>
<h1 className="text-5xl font-serif font-bold text-slate-950 mb-6 tracking-tighter">Termos de Serviço</h1>
<p className="text-slate-500 uppercase tracking-widest text-[10px] font-bold">Última atualização: Maio de 2026</p>
</motion.div>
<div className="markdown-body">
<h2>1. Termos</h2>
<p>Ao acessar o site SEO Authority, você concorda em cumprir estes termos de serviço, todas as leis e regulamentos aplicáveis e concorda que é responsável pelo cumprimento de todas as leis locais aplicáveis.</p>
<h2>2. Licença de Uso</h2>
<p>O conteúdo publicado é protegido por direitos autorais. É permitida a citação de trechos mediante link de atribuição para a fonte original. O uso comercial dos dados sem autorização prévia é proibido.</p>
<h2>3. Isenção de Responsabilidade</h2>
<p>Os materiais no site são fornecidos 'como estão'. O SEO Authority não oferece garantias, expressas ou implícitas, e por este meio isenta e nega todas as outras garantias, incluindo, sem limitação, garantias implícitas ou condições de comercialização.</p>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,57 @@
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: (process.env.GEMINI_API_KEY as string) });
export async function translateText(text: string, targetLang: string) {
if (!text) return text;
try {
const response = await ai.models.generateContent({
model: "gemini-3-flash-preview",
contents: `Translate the following text to ${targetLang}. Keep the markdown formatting and don't add any extra commentary. Just the translation: \n\n${text}`,
});
return response.text || text;
} catch (error) {
console.error("Translation error:", error);
return text;
}
}
export async function translateArticle(article: any, targetLang: string) {
try {
const prompt = `Translate the following article object to ${targetLang}.
Return a JSON object with the same structure.
Only translate the 'title', 'excerpt', 'content', 'metaTitle', and 'metaDescription' fields.
Keep the 'id', 'slug', 'category', 'publishedAt', 'image', 'readTime', 'author', 'tags', and 'lang' (set lang to ${targetLang}) fields exactly as they are.
Article to translate:
${JSON.stringify({
title: article.title,
excerpt: article.excerpt,
content: article.content,
metaTitle: article.metaTitle,
metaDescription: article.metaDescription
})}
`;
const response = await ai.models.generateContent({
model: "gemini-3-flash-preview",
contents: prompt,
config: {
responseMimeType: "application/json",
}
});
const translatedParts = JSON.parse(response.text || "{}");
return {
...article,
...translatedParts,
lang: targetLang
};
} catch (error) {
console.error("Article translation error:", error);
return article;
}
}

View file

@ -0,0 +1,54 @@
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY || '' });
export async function generateSEOArticle(topic: string) {
try {
const response = await ai.models.generateContent({
model: "gemini-3-flash-preview",
contents: `Crie um artigo de blog profissional e altamente otimizado para SEO sobre o seguinte tópico: ${topic}.
O artigo deve estar em Português do Brasil.
Retorne APENAS um JSON no seguinte formato:
{
"title": "Título otimizado com palavra-chave",
"excerpt": "Um resumo atraente do artigo",
"content": "Conteúdo completo em Markdown, com H2, H3, bullets e parágrafos curtos",
"category": "SEO para Iniciantes",
"metaTitle": "Meta title SEO",
"metaDescription": "Meta description persuasiva",
"tags": ["tag1", "tag2"],
"readTime": "X min"
}
Certifique-se de que o conteúdo seja profundo, profissional e focado em autoridade.`,
config: {
responseMimeType: "application/json"
}
});
return JSON.parse(response.text || '{}');
} catch (error) {
console.error("Erro ao gerar artigo:", error);
throw error;
}
}
export async function summarizeSearchResults(query: string, results: any[]) {
try {
const titles = results.map(r => r.title).join(', ');
const response = await ai.models.generateContent({
model: "gemini-1.5-flash",
contents: `O usuário pesquisou por "${query}" em um blog de autoridade em SEO.
Encontramos os seguintes artigos relacionados: ${titles}.
Crie um resumo muito curto (máximo 200 caracteres) explicando por que esses tópicos são importantes para a estratégia de SEO do usuário.
Seja direto, profissional e encorajador. Use Português do Brasil.`,
});
return response.text;
} catch (error) {
console.error("Erro ao resumir busca:", error);
return null;
}
}

77
Template-01/src/types.ts Normal file
View file

@ -0,0 +1,77 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
export type Category =
| 'Estratégia'
| 'Técnico'
| 'Autoridade'
| 'Negócios'
| 'SEO para Iniciantes'
| 'SEO Técnico'
| 'SEO On Page'
| 'SEO Off Page'
| 'Link Building'
| 'Ferramentas de SEO'
| 'SEO para E-commerce'
| 'SEO para Blogs'
| 'Atualizações do Google';
export interface Author {
name: string;
role: string;
avatar: string;
}
export interface Article {
id: string;
slug: string;
title: string;
excerpt: string;
content: string;
category: Category;
author: Author;
publishedAt: string;
image: string;
readTime: string;
metaTitle: string;
metaDescription: string;
tags: string[];
lang: 'pt-br' | 'en' | 'es';
}
export interface Translations {
newsletter: string;
subscribe: string;
readMore: string;
latest: string;
allArticles: string;
sections: string;
publishing: string;
contact: string;
about: string;
privacy: string;
terms: string;
featured: string;
recent: string;
explore: string;
search: string;
viewAll: string;
manifesto: string;
methodology: string;
heroTitle: string;
heroSubtitle: string;
heroDescription: string;
readManifesto: string;
publishedIn: string;
newsletterTitle: string;
newsletterDescription: string;
emailPlaceholder: string;
subscribeButton: string;
trendsTitle: string;
footerNewsletterTitle: string;
footerNewsletterSubtitle: string;
footerNewsletterInput: string;
footerNewsletterDisclaimer: string;
}

26
Template-01/tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

View file

@ -0,0 +1,24 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import {defineConfig, loadEnv} from 'vite';
export default defineConfig(({mode}) => {
const env = loadEnv(mode, '.', '');
return {
plugins: [react(), tailwindcss()],
define: {
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
server: {
// HMR is disabled in AI Studio via DISABLE_HMR env var.
// Do not modify—file watching is disabled to prevent flickering during agent edits.
hmr: process.env.DISABLE_HMR !== 'true',
},
};
});

20
Template-02/README.md Normal file
View file

@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/b9e0fe6d-c7c5-42ea-8fbe-61d65dc0914c
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

32
Template-02/index.html Normal file
View file

@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SEO Authority | Insights Técnicos e Estratégicos</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%232563eb'%3E%3Cpath d='M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5'/%3E%3C/svg%3E" />
<meta name="theme-color" content="#ffffff" />
<style>
/* Hide Google Translate Bar */
.goog-te-banner-frame.skiptranslate, .goog-te-gadget-icon { display: none !important; }
body { top: 0px !important; }
.goog-te-menu-value { display: none !important; }
#google_translate_element { display: none; }
.goog-tooltip { display: none !important; }
.goog-tooltip:hover { display: none !important; }
.goog-text-highlight { background-color: transparent !important; border: none !important; box-shadow: none !important; }
</style>
</head>
<body>
<div id="google_translate_element"></div>
<div id="root"></div>
<script type="text/javascript">
function googleTranslateElementInit() {
new google.translate.TranslateElement({pageLanguage: 'pt', includedLanguages: 'en,es,pt', layout: google.translate.TranslateElement.InlineLayout.SIMPLE, autoDisplay: false}, 'google_translate_element');
}
</script>
<script type="text/javascript" src="//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,6 @@
{
"name": "SEOHUB - Intelligence & Authority",
"description": "Plataforma de autoridade em SEO com foco em insights técnicos, semânticos e estratégicos para a nova economia digital.",
"requestFramePermissions": [],
"majorCapabilities": []
}

5670
Template-02/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

41
Template-02/package.json Normal file
View file

@ -0,0 +1,41 @@
{
"name": "react-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "tsx server.ts",
"build": "vite build",
"preview": "vite preview",
"clean": "rm -rf dist",
"lint": "tsc --noEmit"
},
"dependencies": {
"@google/genai": "^1.51.0",
"@google/generative-ai": "^0.24.1",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"clsx": "^2.1.1",
"cors": "^2.8.6",
"dotenv": "^17.2.3",
"express": "^4.22.1",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"react-helmet-async": "^3.0.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.14.2",
"tailwind-merge": "^3.5.0",
"vite": "^6.2.3"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"autoprefixer": "^10.4.21",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.3"
}
}

View file

@ -0,0 +1,5 @@
User-agent: *
Allow: /
# Sitemaps
Sitemap: https://seu-dominio.com/sitemap.xml

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://seo-authority.digital/</loc>
<lastmod>2026-05-04</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://seo-authority.digital/sobre</loc>
<lastmod>2026-05-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://seo-authority.digital/contato</loc>
<lastmod>2026-05-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://seo-authority.digital/metodologia</loc>
<lastmod>2026-05-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://seo-authority.digital/artigo/futuro-do-seo-2026</loc>
<lastmod>2026-05-04</lastmod>
<priority>0.9</priority>
</url>
<url>
<loc>https://seo-authority.digital/artigo/eeat-guia-completo</loc>
<lastmod>2026-05-04</lastmod>
<priority>0.9</priority>
</url>
<url>
<loc>https://seo-authority.digital/artigo/sinais-de-usuario-ranking</loc>
<lastmod>2026-05-04</lastmod>
<priority>0.9</priority>
</url>
</urlset>

93
Template-02/server.ts Normal file
View file

@ -0,0 +1,93 @@
import express from 'express';
import { createServer as createViteServer } from 'vite';
import { GoogleGenerativeAI } from "@google/generative-ai";
import path from 'path';
import cors from 'cors';
async function startServer() {
const app = express();
const PORT = 3000;
app.use(express.json());
app.use(cors());
// Lazy Gemini Setup
let genAI: GoogleGenerativeAI | null = null;
const getAIModel = () => {
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
throw new Error("GEMINI_API_KEY environment variable is required");
}
if (!genAI) {
genAI = new GoogleGenerativeAI(apiKey);
}
return genAI.getGenerativeModel({ model: "gemini-1.5-flash-8b" });
};
// Translation API
app.post('/api/translate', async (req, res) => {
const { text, targetLang, context, isObject } = req.body;
try {
const model = getAIModel();
let prompt = "";
if (isObject) {
prompt = `Translate the following JSON object values to ${targetLang}.
Keep the keys exactly the same.
Preserve markdown formatting in values.
Return ONLY the translated JSON object.
Object:
${JSON.stringify(text)}`;
} else {
prompt = `Translate the following ${context || 'text'} to ${targetLang}.
Preserve all markdown formatting, technical terms, and HTML structures if present.
Do not add any commentary or prefix. Return only the translated string.
Content:
${text}`;
}
const result = await model.generateContent(prompt);
const response = await result.response;
let resultText = response.text().replace(/```json/g, '').replace(/```/g, '').trim();
if (isObject) {
try {
res.json({ translatedText: JSON.parse(resultText) });
} catch (e) {
console.error("Parse Error:", resultText);
res.json({ translatedText: text }); // Fallback to original if parse fails
}
} else {
res.json({ translatedText: resultText });
}
} catch (error) {
console.error("Translation Error:", error);
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to translate" });
}
});
// Vite Integration
if (process.env.NODE_ENV !== "production") {
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "spa",
});
app.use(vite.middlewares);
} else {
const distPath = path.join(process.cwd(), 'dist');
app.use(express.static(distPath));
app.get('*', (req, res) => {
res.sendFile(path.join(distPath, 'index.html'));
});
}
app.listen(PORT, "0.0.0.0", () => {
console.log(`Server running at http://0.0.0.0:${PORT}`);
});
}
startServer();

50
Template-02/src/App.tsx Normal file
View file

@ -0,0 +1,50 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
import Layout from './components/Layout';
import Home from './pages/Home';
import BlogPost from './pages/BlogPost';
import CategoryPage from './pages/CategoryPage';
import About from './pages/About';
import Methodology from './pages/Methodology';
import Privacy from './pages/Privacy';
import Terms from './pages/Terms';
import Contact from './pages/Contact';
import Archive from './pages/Archive';
import NotFound from './pages/NotFound';
import Bookmarks from './pages/Bookmarks';
import { LanguageProvider } from './contexts/LanguageContext';
import { BookmarksProvider } from './contexts/BookmarksContext';
export default function App() {
return (
<HelmetProvider>
<LanguageProvider>
<BookmarksProvider>
<Router>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/artigo/:slug" element={<BlogPost />} />
<Route path="/categoria/:categorySlug" element={<CategoryPage />} />
<Route path="/sobre" element={<About />} />
<Route path="/metodologia" element={<Methodology />} />
<Route path="/privacidade" element={<Privacy />} />
<Route path="/termos" element={<Terms />} />
<Route path="/contato" element={<Contact />} />
<Route path="/arquivo" element={<Archive />} />
<Route path="/leituras-salvas" element={<Bookmarks />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Layout>
</Router>
</BookmarksProvider>
</LanguageProvider>
</HelmetProvider>
);
}

View file

@ -0,0 +1,133 @@
import { Link } from 'react-router-dom';
import { Article } from '../types';
import { motion } from 'motion/react';
import { Bookmark, Clock, User, ArrowRight } from 'lucide-react';
import { useBookmarks } from '../contexts/BookmarksContext';
import { cn } from '../lib/utils';
interface ArticleCardProps {
article: Article;
featured?: boolean;
}
export default function ArticleCard({ article, featured = false }: ArticleCardProps) {
const { toggleBookmark, isBookmarked } = useBookmarks();
const bookmarked = isBookmarked(article.id);
if (featured) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="group relative flex flex-col md:flex-row gap-8 lg:gap-12 bg-white p-6 rounded-[2.5rem] border border-slate-100 shadow-sm hover:shadow-xl hover:shadow-slate-200/50 transition-all duration-500"
>
<div className="md:w-1/2 relative overflow-hidden rounded-[2rem] bg-slate-100">
<Link to={`/artigo/${article.slug}`} className="block h-full">
<img
src={article.image}
alt={article.title}
className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-105 aspect-square md:aspect-video lg:aspect-[4/3]"
referrerPolicy="no-referrer"
/>
</Link>
<button
onClick={() => toggleBookmark(article.id)}
className={cn(
"absolute top-6 right-6 h-12 w-12 rounded-full border border-white/20 backdrop-blur-md flex items-center justify-center transition-all shadow-lg",
bookmarked ? "bg-brand border-brand text-white" : "bg-black/10 text-white hover:bg-white hover:text-brand"
)}
>
<Bookmark size={20} fill={bookmarked ? "currentColor" : "none"} />
</button>
</div>
<div className="md:w-1/2 flex flex-col justify-center">
<div className="flex items-center gap-4 text-[10px] font-bold uppercase tracking-widest text-brand mb-6 bg-brand/5 w-fit px-3 py-1 rounded-full border border-brand/10">
<span>{article.category}</span>
</div>
<Link to={`/artigo/${article.slug}`}>
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-display font-extrabold text-slate-950 leading-[1.1] mb-6 hover:text-brand transition-colors">
{article.title}
</h2>
</Link>
<p className="text-lg text-slate-500 leading-relaxed mb-8 line-clamp-3 font-medium">
{article.excerpt}
</p>
<div className="mt-auto flex items-center justify-between pt-8 border-t border-slate-50">
<div className="flex items-center gap-3">
<img src={article.author.avatar} alt={article.author.name} className="h-10 w-10 rounded-full border border-slate-100" />
<div>
<p className="text-sm font-bold text-slate-900 leading-none">{article.author.name}</p>
<p className="text-[10px] text-slate-400 uppercase tracking-widest font-bold mt-1.5">Autor</p>
</div>
</div>
<div className="flex items-center gap-1.5 text-slate-400 font-bold text-[10px] uppercase tracking-widest bg-slate-50 px-3 py-1.5 rounded-lg border border-slate-100">
<Clock size={12} className="text-brand" />
<span>{article.readTime}</span>
</div>
</div>
</div>
</motion.div>
);
}
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="group flex flex-col bg-white rounded-3xl border border-slate-100 overflow-hidden hover:shadow-xl hover:shadow-slate-200/50 transition-all duration-500"
>
<div className="relative aspect-[16/10] overflow-hidden bg-slate-100 rounded-t-[2.5rem]">
<Link to={`/artigo/${article.slug}`} className="block h-full">
<img
src={article.image}
alt={article.title}
className="h-full w-full object-cover grayscale-[0.5] group-hover:grayscale-0 transition-all duration-700 group-hover:scale-105"
referrerPolicy="no-referrer"
/>
</Link>
<button
onClick={() => toggleBookmark(article.id)}
className={cn(
"absolute top-6 right-6 h-11 w-11 rounded-full border border-white/20 backdrop-blur-md flex items-center justify-center transition-all opacity-0 group-hover:opacity-100 translate-y-2 group-hover:translate-y-0",
bookmarked ? "bg-brand border-brand text-white opacity-100 translate-y-0" : "bg-black/20 text-white hover:bg-white hover:text-brand shadow-xl"
)}
>
<Bookmark size={18} fill={bookmarked ? "currentColor" : "none"} />
</button>
</div>
<div className="p-10 flex flex-col flex-grow">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3">
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-brand">{article.category}</span>
</div>
<span className="text-[10px] font-bold text-slate-300 uppercase tracking-widest">{article.readTime}</span>
</div>
<Link to={`/artigo/${article.slug}`}>
<h3 className="text-2xl lg:text-3xl font-display font-black text-slate-950 leading-[1.15] mb-6 group-hover:text-brand transition-colors">
{article.title}
</h3>
</Link>
<p className="text-slate-500 text-sm leading-relaxed line-clamp-3 font-medium mb-10">
{article.excerpt}
</p>
<div className="mt-auto pt-8 border-t border-slate-50 flex items-center justify-between">
<div className="flex items-center gap-3">
<img src={article.author.avatar} alt={article.author.name} className="h-8 w-8 rounded-full grayscale hover:grayscale-0 transition-all border border-slate-100" />
<span className="text-[10px] font-black text-slate-950 uppercase tracking-widest">{article.author.name}</span>
</div>
<div className="h-8 w-8 rounded-full bg-slate-50 flex items-center justify-center text-slate-400 group-hover:bg-brand group-hover:text-white transition-all">
<ArrowRight size={14} />
</div>
</div>
</div>
</motion.div>
);
}

View file

@ -0,0 +1,31 @@
import { Link } from 'react-router-dom';
import { Category } from '../types';
import { cn } from '../lib/utils';
import { CATEGORY_MAP } from '../constants';
interface CategoryBadgeProps {
category: Category;
variant?: 'solid' | 'outline' | 'glass';
className?: string;
}
export default function CategoryBadge({ category, variant = 'solid', className }: CategoryBadgeProps) {
const mapping = CATEGORY_MAP[category] || { name: category, slug: category.toLowerCase().replace(/\s+/g, '-') };
const baseStyles = "inline-flex items-center rounded-full px-3 py-1 text-[10px] font-bold uppercase tracking-wider transition-all hover:scale-105 active:scale-95";
const variants = {
solid: "bg-brand text-white hover:bg-brand-dark",
outline: "border border-brand/10 text-brand hover:bg-brand/5",
glass: "bg-white/80 backdrop-blur-sm text-slate-900 shadow-sm hover:bg-white"
};
return (
<Link
to={`/categoria/${mapping.slug}`}
className={cn(baseStyles, variants[variant], className)}
onClick={(e) => e.stopPropagation()}
>
{mapping.name}
</Link>
);
}

View file

@ -0,0 +1,91 @@
import { Link } from 'react-router-dom';
import { Mail, Github, Twitter, Linkedin, Instagram, Facebook, Youtube, Zap, ArrowUpRight } from 'lucide-react';
export default function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-white border-t border-slate-100 pt-24 pb-12">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-12 gap-16 mb-20">
<div className="md:col-span-12 lg:col-span-5">
<Link to="/" className="flex items-center gap-3 mb-8 group">
<div className="h-9 w-9 bg-brand rounded-xl flex items-center justify-center text-white shadow-lg shadow-teal-500/20">
<Zap size={20} fill="white" />
</div>
<span className="font-display text-2xl font-black tracking-tight text-slate-950">
SEO<span className="text-brand">HUB</span>
</span>
</Link>
<p className="text-slate-500 max-w-md mb-8 text-base leading-relaxed font-medium">
Democratizando a inteligência de busca com insights técnicos e estratégicos para a nova economia digital.
</p>
<div className="flex flex-wrap gap-3">
{[
{ Icon: Twitter, label: 'Twitter' },
{ Icon: Linkedin, label: 'LinkedIn' },
{ Icon: Instagram, label: 'Instagram' },
{ Icon: Facebook, label: 'Facebook' },
{ Icon: Github, label: 'GitHub' }
].map(({ Icon, label }, i) => (
<a
key={i}
href="#"
aria-label={label}
className="h-10 w-10 flex items-center justify-center rounded-xl border border-slate-100 text-slate-400 hover:text-brand hover:border-brand/20 hover:bg-brand/5 transition-all group"
>
<Icon size={18} className="group-hover:scale-110 transition-transform" />
</a>
))}
</div>
</div>
<div className="md:col-span-4 lg:col-span-2">
<h4 className="font-bold text-slate-950 mb-6 uppercase text-[11px] tracking-widest">Navegação</h4>
<ul className="space-y-4">
<li><Link to="/categoria/estrategia" className="text-slate-500 hover:text-brand text-sm font-semibold transition-colors flex items-center gap-1 group">Estratégia <ArrowUpRight size={12} className="opacity-0 group-hover:opacity-100 transition-opacity" /></Link></li>
<li><Link to="/categoria/tecnico" className="text-slate-500 hover:text-brand text-sm font-semibold transition-colors flex items-center gap-1 group">Técnico <ArrowUpRight size={12} className="opacity-0 group-hover:opacity-100 transition-opacity" /></Link></li>
<li><Link to="/categoria/autoridade" className="text-slate-500 hover:text-brand text-sm font-semibold transition-colors flex items-center gap-1 group">Autoridade <ArrowUpRight size={12} className="opacity-0 group-hover:opacity-100 transition-opacity" /></Link></li>
<li><Link to="/categoria/negocios" className="text-slate-500 hover:text-brand text-sm font-semibold transition-colors flex items-center gap-1 group">Negócios <ArrowUpRight size={12} className="opacity-0 group-hover:opacity-100 transition-opacity" /></Link></li>
</ul>
</div>
<div className="md:col-span-4 lg:col-span-2">
<h4 className="font-bold text-slate-950 mb-6 uppercase text-[11px] tracking-widest">Recursos</h4>
<ul className="space-y-4">
<li><Link to="/metodologia" className="text-slate-500 hover:text-brand text-sm font-semibold transition-colors">Metodologia</Link></li>
<li><Link to="/sobre" className="text-slate-500 hover:text-brand text-sm font-semibold transition-colors">Sobre Nós</Link></li>
<li><Link to="/contato" className="text-slate-500 hover:text-brand text-sm font-semibold transition-colors">Contato</Link></li>
<li><Link to="/arquivo" className="text-slate-500 hover:text-brand text-sm font-semibold transition-colors">Arquivo</Link></li>
</ul>
</div>
<div className="md:col-span-4 lg:col-span-3">
<div className="p-8 bg-slate-50 rounded-3xl border border-slate-100">
<h5 className="font-display text-lg font-bold text-slate-950 mb-2">Newsletter Pro</h5>
<p className="text-slate-500 text-xs leading-relaxed mb-6 font-medium">Assine nosso briefing semanal gratuito sobre o futuro do Google.</p>
<form className="space-y-3" onSubmit={(e) => e.preventDefault()}>
<input
type="email"
placeholder="Seu melhor email..."
className="w-full px-4 py-2.5 rounded-xl border border-slate-200 bg-white text-sm focus:outline-none focus:ring-4 focus:ring-teal-100 transition-all"
/>
<button className="w-full py-2.5 bg-brand text-white rounded-xl font-bold text-xs uppercase tracking-widest hover:bg-brand-dark transition-all shadow-md shadow-brand/10">
Inscrever-se
</button>
</form>
</div>
</div>
</div>
<div className="pt-12 border-t border-slate-100 flex flex-col md:flex-row justify-between items-center gap-6 text-[11px] font-bold uppercase tracking-widest text-slate-400">
<p>© {currentYear} SEO HUB DIGITAL. TODOS OS DIREITOS RESERVADOS.</p>
<div className="flex gap-8">
<Link to="/privacidade" className="hover:text-brand transition-colors">Privacidade</Link>
<Link to="/termos" className="hover:text-brand transition-colors">Termos</Link>
</div>
</div>
</div>
</footer>
);
}

View file

@ -0,0 +1,192 @@
import { Link } from 'react-router-dom';
import { Search, Menu, X, Globe, Twitter, Instagram, Linkedin, Youtube, Bookmark, Zap } from 'lucide-react';
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { useLanguage } from '../contexts/LanguageContext';
import { translations } from '../constants';
import SearchOverlay from './SearchOverlay';
import { useBookmarks } from '../contexts/BookmarksContext';
import { cn } from '../lib/utils';
export default function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const { lang, setLang } = useLanguage();
const { bookmarks } = useBookmarks();
const t = translations[lang];
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 20);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const categories = [
{ name: 'Estratégia', slug: 'estrategia' },
{ name: 'Técnico', slug: 'tecnico' },
{ name: 'Autoridade', slug: 'autoridade' },
{ name: 'Negócios', slug: 'negocios' }
];
return (
<header className={cn(
"sticky top-0 z-50 w-full transition-all duration-300",
scrolled ? "bg-white/80 backdrop-blur-xl border-b border-slate-100 shadow-sm py-2" : "bg-white border-b border-transparent py-4"
)}>
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
<div className="flex items-center gap-12">
<Link to="/" className="flex items-center gap-2 group">
<div className="h-9 w-9 bg-brand rounded-xl flex items-center justify-center text-white shadow-lg shadow-teal-500/20">
<Zap size={20} fill="white" />
</div>
<span className="font-display text-xl font-black tracking-tight text-slate-950">
SEO<span className="text-brand">HUB</span>
</span>
</Link>
<nav className="hidden lg:flex items-center gap-8">
{categories.map((cat) => (
<Link
key={cat.slug}
to={`/categoria/${cat.slug}`}
className="text-xs font-bold uppercase tracking-widest text-slate-500 hover:text-brand transition-colors"
>
{cat.name}
</Link>
))}
<Link
to="/contato"
className="text-xs font-bold uppercase tracking-widest text-slate-500 hover:text-brand transition-colors"
>
Contato
</Link>
<Link
to="/arquivo"
className="text-xs font-bold uppercase tracking-widest text-slate-500 hover:text-brand transition-colors"
>
Arquivo
</Link>
</nav>
</div>
<div className="flex items-center gap-3">
<div className="hidden md:flex items-center gap-2 pr-4 border-r border-slate-100 italic font-bold text-[10px] text-slate-400">
Insights Semânticos v2.0
</div>
{/* Language Switcher */}
<button
onClick={() => setLang(lang === 'pt-br' ? 'en' : 'pt-br')}
className="hidden sm:flex items-center gap-3 px-4 py-2 hover:bg-slate-50 rounded-xl transition-all group"
>
<Globe size={16} className="text-slate-400 group-hover:text-brand transition-colors" />
<span className="text-[10px] font-black uppercase tracking-widest text-slate-500 group-hover:text-slate-950">
{lang === 'pt-br' ? 'PT-BR' : 'EN-US'}
</span>
</button>
<button
className="p-2.5 text-slate-500 hover:text-brand hover:bg-brand/5 rounded-xl transition-all"
onClick={() => setIsSearchOpen(true)}
>
<Search size={20} />
</button>
<Link
to="/leituras-salvas"
className="relative p-2.5 text-slate-500 hover:text-brand hover:bg-brand/5 rounded-xl transition-all"
title="Leituras Salvas"
>
<Bookmark size={20} />
<AnimatePresence>
{bookmarks.length > 0 && (
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
className="absolute top-1.5 right-1.5 h-4 w-4 bg-brand text-white text-[8px] font-black rounded-full flex items-center justify-center border-2 border-white"
>
{bookmarks.length}
</motion.span>
)}
</AnimatePresence>
</Link>
<button
className="lg:hidden p-2.5 text-slate-950 hover:bg-slate-50 rounded-xl transition-all"
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
</div>
</div>
<SearchOverlay isOpen={isSearchOpen} onClose={() => setIsSearchOpen(false)} />
<AnimatePresence>
{isMenuOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="lg:hidden border-t border-slate-100 bg-white shadow-xl overflow-hidden"
>
<div className="flex flex-col gap-8 p-8">
<div className="flex flex-col gap-4">
{categories.map((cat) => (
<Link
key={cat.slug}
to={`/categoria/${cat.slug}`}
className="text-2xl font-display font-bold text-slate-950 hover:text-brand transition-colors"
onClick={() => setIsMenuOpen(false)}
>
{cat.name}
</Link>
))}
<Link
to="/contato"
className="text-2xl font-display font-bold text-slate-950 hover:text-brand transition-colors"
onClick={() => setIsMenuOpen(false)}
>
Contato
</Link>
<Link
to="/arquivo"
className="text-2xl font-display font-bold text-slate-950 hover:text-brand transition-colors"
onClick={() => setIsMenuOpen(false)}
>
Arquivo
</Link>
<Link
to="/leituras-salvas"
className="text-2xl font-display font-bold text-slate-950 hover:text-brand transition-colors flex items-center gap-3"
onClick={() => setIsMenuOpen(false)}
>
Leituras Salvas
{bookmarks.length > 0 && (
<span className="h-6 w-6 bg-brand text-white text-xs font-black rounded-full flex items-center justify-center">
{bookmarks.length}
</span>
)}
</Link>
</div>
<div className="pt-8 border-t border-slate-100 flex items-center justify-between">
<div className="flex gap-4">
{[Twitter, Linkedin, Instagram, Youtube].map((Icon, i) => (
<a key={i} href="#" className="h-11 w-11 flex items-center justify-center rounded-xl bg-slate-50 text-slate-500 hover:text-brand hover:bg-brand/5 transition-all border border-slate-100">
<Icon size={20} />
</a>
))}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</header>
);
}

View file

@ -0,0 +1,40 @@
import { ReactNode, useEffect } from 'react';
import Header from './Header';
import Footer from './Footer';
import LeadMagnet from './LeadMagnet';
import { useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'motion/react';
interface LayoutProps {
children: ReactNode;
}
export default function Layout({ children }: LayoutProps) {
const { pathname } = useLocation();
// Scroll to top on route change
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return (
<div className="min-h-screen flex flex-col bg-white selection:bg-brand/10 selection:text-brand">
<Header />
<main className="flex-grow">
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
{children}
</motion.div>
</AnimatePresence>
</main>
<LeadMagnet />
<Footer />
</div>
);
}

View file

@ -0,0 +1,126 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { X, Download, ShieldCheck, ArrowRight, Mail } from 'lucide-react';
export default function LeadMagnet() {
const [isOpen, setIsOpen] = useState(false);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
// Check if user already dismissed or converted
const hasSeen = localStorage.getItem('seo_lead_magnet_dismissed');
if (hasSeen) return;
const handleMouseLeave = (e: MouseEvent) => {
if (e.clientY <= 0 && !dismissed) {
setIsOpen(true);
}
};
const handleScroll = () => {
const scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
if (scrollPercent > 50 && !dismissed) {
setIsOpen(true);
}
};
document.addEventListener('mouseleave', handleMouseLeave);
window.addEventListener('scroll', handleScroll);
return () => {
document.removeEventListener('mouseleave', handleMouseLeave);
window.removeEventListener('scroll', handleScroll);
};
}, [dismissed]);
const handleClose = () => {
setIsOpen(false);
setDismissed(true);
localStorage.setItem('seo_lead_magnet_dismissed', 'true');
};
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[200] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm">
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="bg-white rounded-[40px] shadow-2xl max-w-4xl w-full overflow-hidden flex flex-col md:flex-row relative"
>
<button
onClick={handleClose}
className="absolute top-6 right-6 h-10 w-10 rounded-full bg-slate-100/80 hover:bg-slate-900 hover:text-white text-slate-400 flex items-center justify-center z-20 transition-all shadow-sm"
aria-label="Fechar"
>
<X size={20} />
</button>
{/* Left side: Visual */}
<div className="md:w-5/12 bg-brand p-12 text-white flex flex-col justify-between relative overflow-hidden">
<div className="relative z-10">
<div className="h-12 w-12 bg-white/20 rounded-2xl flex items-center justify-center mb-8">
<ShieldCheck size={28} />
</div>
<h2 className="text-4xl font-display font-black tracking-tight leading-[1.05] mb-6">
Domine o<br />Algoritmo 2026.
</h2>
<p className="text-brand-light font-medium leading-relaxed">
Baixe nosso checklist exclusivo de Auditoria Técnica e E-E-A-T para garantir sua autoridade.
</p>
</div>
<div className="relative z-10 mt-12 pt-8 border-t border-white/10">
<div className="flex items-center gap-3">
<div className="flex -space-x-3">
{[1, 2, 3].map(i => (
<img key={i} src={`https://i.pravatar.cc/100?u=${i}`} className="h-8 w-8 rounded-full border-2 border-brand" />
))}
</div>
<span className="text-[10px] font-black uppercase tracking-widest">+12.4k SEOs baixaram</span>
</div>
</div>
{/* Decorative Circle */}
<div className="absolute -bottom-20 -left-20 h-64 w-64 bg-white/10 rounded-full blur-3xl" />
</div>
{/* Right side: Form */}
<div className="md:w-7/12 p-12 md:p-16 flex flex-col justify-center">
<span className="text-[10px] font-black text-brand uppercase tracking-[0.3em] mb-4 block">Recurso Gratuito</span>
<h3 className="text-2xl font-display font-black text-slate-950 mb-8 leading-tight">Onde enviamos seu <span className="text-brand">Blueprint de Autoridade?</span></h3>
<form className="space-y-4" onSubmit={(e) => { e.preventDefault(); handleClose(); }}>
<div className="relative">
<Mail className="absolute left-6 top-1/2 -translate-y-1/2 text-slate-300" size={18} />
<input
type="email"
placeholder="Seu melhor e-mail comercial"
required
className="w-full h-16 pl-16 pr-6 bg-slate-50 border border-slate-100 rounded-2xl text-slate-950 placeholder:text-slate-300 focus:outline-none focus:border-brand focus:bg-white focus:ring-4 focus:ring-brand/5 transition-all font-medium"
/>
</div>
<button
type="submit"
className="w-full h-16 bg-slate-950 text-white rounded-2xl font-black uppercase text-[10px] tracking-[0.3em] flex items-center justify-center gap-3 hover:bg-brand transition-all shadow-xl shadow-slate-900/10"
>
Receber Agora <Download size={18} />
</button>
</form>
<div className="mt-8 flex items-center justify-center gap-6">
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-300 uppercase tracking-widest">
<ArrowRight size={12} /> PDF interativo
</div>
<div className="flex items-center gap-2 text-[10px] font-bold text-slate-300 uppercase tracking-widest">
<ArrowRight size={12} /> Templates de auditoria
</div>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
}

View file

@ -0,0 +1,158 @@
import { Helmet } from 'react-helmet-async';
interface SEOProps {
title: string;
description: string;
canonical?: string;
ogImage?: string;
ogType?: string;
keywords?: string[];
articleData?: {
publishedTime?: string;
modifiedTime?: string;
author?: string;
authorAvatar?: string;
tags?: string[];
section?: string;
};
}
export default function SEO({
title,
description,
canonical,
ogImage = 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&q=80&w=1200',
ogType = 'website',
keywords = [],
articleData
}: SEOProps) {
const siteName = 'SEOHUB';
const siteTitle = 'SEOHUB | Inteligência & Autoridade Semântica';
const siteUrl = window.location.origin;
const fullTitle = `${title} | ${siteName}`;
// Organization Schema
const organizationSchema = {
"@type": "Organization",
"@id": `${siteUrl}/#organization`,
"name": "SEOHUB Digital",
"url": siteUrl,
"logo": {
"@type": "ImageObject",
"url": `${siteUrl}/logo.png`, // Placeholder, organization usually has logo
"width": 600,
"height": 600
},
"sameAs": [
"https://twitter.com/seohub",
"https://linkedin.com/company/seohub"
]
};
// Breadcrumb Schema
const breadcrumbSchema = {
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": siteUrl
}
]
};
if (ogType === 'article') {
breadcrumbSchema.itemListElement.push({
"@type": "ListItem",
"position": 2,
"name": title,
"item": canonical || window.location.href
});
}
// Final Schema
const finalSchema = {
"@context": "https://schema.org",
"@graph": [
organizationSchema,
{
"@type": ogType === 'article' ? "BlogPosting" : "WebSite",
"@id": `${siteUrl}/#website`,
"url": siteUrl,
"name": siteName,
"publisher": { "@id": `${siteUrl}/#organization` },
"inLanguage": "pt-BR"
},
ogType === 'article' ? {
"@type": "BlogPosting",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": canonical || window.location.href
},
"headline": title,
"description": description,
"image": ogImage,
"author": {
"@type": "Person",
"name": articleData?.author || "Equipe SEO",
"image": articleData?.authorAvatar || "https://i.pravatar.cc/150?u=seo"
},
"publisher": { "@id": `${siteUrl}/#organization` },
"datePublished": articleData?.publishedTime,
"dateModified": articleData?.modifiedTime || articleData?.publishedTime,
"keywords": keywords.join(', '),
"articleSection": articleData?.section
} : {
"@type": "WebPage",
"@id": `${siteUrl}/#page`,
"url": canonical || window.location.href,
"name": fullTitle,
"description": description,
"publisher": { "@id": `${siteUrl}/#organization` }
},
breadcrumbSchema
]
};
return (
<Helmet>
<title>{fullTitle}</title>
<meta name="description" content={description} />
{keywords.length > 0 && <meta name="keywords" content={keywords.join(', ')} />}
{/* Canonical URL */}
<link rel="canonical" href={canonical || window.location.href} />
{/* Open Graph / Facebook */}
<meta property="og:type" content={ogType} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImage} />
<meta property="og:site_name" content={siteName} />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImage} />
{/* Article Specifics */}
{ogType === 'article' && articleData && (
<>
{articleData.publishedTime && <meta property="article:published_time" content={articleData.publishedTime} />}
{articleData.modifiedTime && <meta property="article:modified_time" content={articleData.modifiedTime} />}
{articleData.author && <meta property="article:author" content={articleData.author} />}
{articleData.tags?.map(tag => (
<meta key={tag} property="article:tag" content={tag} />
))}
</>
)}
{/* Structured Data */}
<script type="application/ld+json">
{JSON.stringify(finalSchema)}
</script>
</Helmet>
);
}

View file

@ -0,0 +1,157 @@
import { motion, AnimatePresence } from 'motion/react';
import { X, Search as SearchIcon, ArrowRight } from 'lucide-react';
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { articles } from '../data/articles';
import { Article } from '../types';
import { summarizeSearchResults } from '../services/geminiService';
import { Sparkles, Loader2 } from 'lucide-react';
interface SearchOverlayProps {
isOpen: boolean;
onClose: () => void;
}
export default function SearchOverlay({ isOpen, onClose }: SearchOverlayProps) {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Article[]>([]);
const [summary, setSummary] = useState<string | null>(null);
const [loadingSummary, setLoadingSummary] = useState(false);
useEffect(() => {
if (query.trim().length > 2) {
const filtered = articles.filter(a =>
a.title.toLowerCase().includes(query.toLowerCase()) ||
a.category.toLowerCase().includes(query.toLowerCase()) ||
a.tags.some(t => t.toLowerCase().includes(query.toLowerCase()))
).slice(0, 5);
setResults(filtered);
// Trigger AI Summary if results found
if (filtered.length > 0) {
setLoadingSummary(true);
summarizeSearchResults(query, filtered).then(res => {
setSummary(res);
setLoadingSummary(false);
});
}
} else {
setResults([]);
setSummary(null);
}
}, [query]);
// Close on Escape
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, [onClose]);
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] bg-white/95 backdrop-blur-md flex flex-col"
>
<div className="mx-auto max-w-4xl w-full px-6 pt-24">
<div className="flex items-center justify-between mb-12">
<div className="flex items-center gap-3">
<div className="h-10 w-10 bg-brand rounded-xl flex items-center justify-center text-white">
<SearchIcon size={20} />
</div>
<h2 className="text-sm font-bold uppercase tracking-[0.2em] text-slate-400">Pesquisar Insight</h2>
</div>
<button
onClick={onClose}
className="h-12 w-12 rounded-full bg-slate-100 flex items-center justify-center text-slate-500 hover:bg-slate-900 hover:text-white transition-all"
>
<X size={20} />
</button>
</div>
<div className="relative mb-16">
<input
autoFocus
type="text"
placeholder="IA Generativa, SEO Técnico, Link Building..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full bg-transparent border-b-2 border-slate-100 py-8 text-4xl sm:text-6xl font-display font-black text-slate-950 placeholder:text-slate-100 focus:outline-none focus:border-brand transition-all"
/>
</div>
<div className="space-y-4">
{summary && !loadingSummary && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8 p-6 bg-brand/5 border border-brand/10 rounded-[2rem] flex gap-4"
>
<div className="h-8 w-8 bg-brand rounded-full flex items-center justify-center text-white shrink-0">
<Sparkles size={16} />
</div>
<div>
<span className="text-[10px] font-black text-brand uppercase tracking-widest block mb-1">Insights da IA</span>
<p className="text-slate-600 text-sm leading-relaxed italic">
"{summary}"
</p>
</div>
</motion.div>
)}
{loadingSummary && (
<div className="flex items-center gap-3 text-slate-300 mb-8 px-6">
<Loader2 size={16} className="animate-spin" />
<span className="text-xs font-bold uppercase tracking-widest">Consultando IA...</span>
</div>
)}
{results.length > 0 ? (
results.map((article) => (
<Link
key={article.id}
to={`/artigo/${article.slug}`}
onClick={onClose}
className="group block p-6 rounded-[2rem] border border-slate-100 hover:bg-white hover:shadow-xl hover:shadow-slate-200 transition-all"
>
<div className="flex items-center justify-between">
<div>
<span className="text-[10px] font-bold text-brand uppercase tracking-widest mb-2 block">{article.category}</span>
<h3 className="text-xl font-bold text-slate-900 group-hover:text-brand transition-colors">{article.title}</h3>
</div>
<div className="h-10 w-10 rounded-full border border-slate-100 flex items-center justify-center text-slate-300 group-hover:bg-brand group-hover:text-white group-hover:border-brand transition-all">
<ArrowRight size={16} />
</div>
</div>
</Link>
))
) : query.trim().length > 2 ? (
<div className="text-center py-20">
<p className="text-slate-400 font-medium italic">Nenhum insight encontrado para "{query}"</p>
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{['Estratégia', 'IA', 'Link Building', 'Técnico'].map((tag) => (
<button
key={tag}
onClick={() => setQuery(tag)}
className="p-4 rounded-2xl bg-slate-50 text-slate-500 text-[10px] font-bold uppercase tracking-widest hover:bg-brand/5 hover:text-brand transition-all"
>
{tag}
</button>
))}
</div>
)}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
);
}

View file

@ -0,0 +1,107 @@
import { useEffect, useState } from 'react';
import { cn } from '../lib/utils';
import { List } from 'lucide-react';
import { motion } from 'motion/react';
interface TOCItem {
id: string;
text: string;
level: number;
}
export default function TableOfContents({ content }: { content: string }) {
const [activeId, setActiveId] = useState<string>('');
const [headings, setHeadings] = useState<TOCItem[]>([]);
useEffect(() => {
// Extract headings (H2 and H3 only for clarity)
const headingLines = content.split('\n').filter(line =>
line.startsWith('## ') || line.startsWith('### ')
);
const extractedHeadings = headingLines.map(line => {
const level = line.startsWith('###') ? 3 : 2;
const text = line.replace(/^#+\s+/, '').trim();
const id = text.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '');
return { id, text, level };
});
setHeadings(extractedHeadings);
// Initial check (delay slightly for DOM to be ready)
setTimeout(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id);
}
});
},
{ rootMargin: '-10% 0% -80% 0%' }
);
extractedHeadings.forEach((h) => {
const el = document.getElementById(h.id);
if (el) observer.observe(el);
});
return () => observer.disconnect();
}, 500);
}, [content]);
if (headings.length === 0) return null;
return (
<div className="h-fit">
<div className="mb-10 flex items-center justify-between">
<h5 className="text-[10px] font-black text-slate-300 uppercase tracking-[0.2em] flex items-center gap-2">
<div className="h-1 w-4 bg-brand" />
Roteiro
</h5>
<span className="text-[9px] font-bold text-slate-200">
{headings.length} SEÇÕES
</span>
</div>
<nav className="flex flex-col">
{headings.map((h, i) => (
<a
key={h.id}
href={`#${h.id}`}
onClick={(e) => {
e.preventDefault();
document.getElementById(h.id)?.scrollIntoView({ behavior: 'smooth' });
}}
className={cn(
"group py-3 border-b border-slate-50 flex items-baseline gap-4 transition-all relative",
activeId === h.id
? "text-brand"
: "text-slate-400 hover:text-slate-900",
h.level === 3 && "pl-8"
)}
>
<span className={cn(
"text-[9px] font-mono transition-colors",
activeId === h.id ? "text-brand" : "text-slate-200 group-hover:text-slate-400"
)}>
{i + 1 < 10 ? `0${i + 1}` : i + 1}
</span>
<span className="text-[11px] font-bold leading-tight">
{h.text}
</span>
{activeId === h.id && (
<motion.div
layoutId="active-toc"
className="absolute left-0 top-0 bottom-0 w-[2px] bg-brand"
/>
)}
</a>
))}
</nav>
</div>
);
}

View file

@ -0,0 +1,126 @@
import { Category } from './types';
export const CATEGORY_MAP: Record<Category, { name: string; slug: string }> = {
'Estratégia': { name: 'Estratégia', slug: 'estrategia' },
'Técnico': { name: 'Técnico', slug: 'tecnico' },
'Autoridade': { name: 'Autoridade', slug: 'autoridade' },
'Negócios': { name: 'Negócios', slug: 'negocios' },
'SEO para Iniciantes': { name: 'SEO para Iniciantes', slug: 'iniciantes' },
'SEO Técnico': { name: 'SEO Técnico', slug: 'seo-tecnico' },
'SEO On Page': { name: 'SEO On Page', slug: 'on-page' },
'SEO Off Page': { name: 'SEO Off Page', slug: 'off-page' },
'Link Building': { name: 'Link Building', slug: 'link-building' },
'Ferramentas de SEO': { name: 'Ferramentas de SEO', slug: 'ferramentas' },
'SEO para E-commerce': { name: 'SEO para E-commerce', slug: 'e-commerce' },
'SEO para Blogs': { name: 'SEO para Blogs', slug: 'blogs' },
'Atualizações do Google': { name: 'Atualizações do Google', slug: 'google-updates' }
};
export const getCategoryBySlug = (slug: string) => {
return Object.entries(CATEGORY_MAP).find(([_, value]) => value.slug === slug);
};
export const translations: Record<string, any> = {
'pt-br': {
newsletter: 'Newsletter',
subscribe: 'Assinar Agora',
readMore: 'Ler artigo',
latest: 'Últimas',
allArticles: 'Todos os Artigos',
sections: 'Seções',
publishing: 'Publicação',
contact: 'Contato',
about: 'Sobre',
privacy: 'Privacidade',
terms: 'Termos',
featured: 'Destaque',
recent: 'Recentes',
explore: 'Explorar Insights',
search: 'Buscar',
viewAll: 'Ver Todos',
manifesto: 'Manifesto',
methodology: 'Metodologia',
heroTitle: 'A Nova Ordem da Busca Semântica.',
heroSubtitle: 'EDITORIAL DE SEO 2026',
heroDescription: 'O recurso definitivo para profissionais que buscam excelência técnica e autoridade duradoura no ecossistema de busca.',
readManifesto: 'Ler Manifesto',
publishedIn: 'Publicado em',
newsletterTitle: 'Newsletter Semanal',
newsletterDescription: 'Inscreva-se para receber as últimas novidades.',
emailPlaceholder: 'seu@email.com',
subscribeButton: 'Inscrever',
trendsTitle: 'TENDÊNCIAS',
footerNewsletterTitle: 'Entre no Círculo de Autoridade',
footerNewsletterSubtitle: 'Assine para receber análises profundas sobre mudanças algorítmicas, IA e estratégias de busca que o resto do mercado ainda não viu.',
footerNewsletterInput: 'seu@email.com',
footerNewsletterDisclaimer: 'Nós respeitamos sua caixa de entrada. Zero spam, apenas inteligência de busca.'
},
'en': {
newsletter: 'Newsletter',
subscribe: 'Subscribe Now',
readMore: 'Read more',
latest: 'Latest',
allArticles: 'All Articles',
sections: 'Sections',
publishing: 'Publishing',
contact: 'Contact',
about: 'About',
privacy: 'Privacy',
terms: 'Terms',
featured: 'Featured',
recent: 'Recent',
explore: 'Explore Insights',
search: 'Search',
viewAll: 'View All',
manifesto: 'Manifesto',
methodology: 'Methodology',
heroTitle: 'The New Order of Semantic Search.',
heroSubtitle: 'SEO EDITORIAL 2026',
heroDescription: 'The definitive resource for professionals seeking technical excellence and lasting authority in the search ecosystem.',
readManifesto: 'Read Manifesto',
publishedIn: 'Published in',
newsletterTitle: 'Weekly Newsletter',
newsletterDescription: 'Subscribe to get the latest news.',
emailPlaceholder: 'your@email.com',
subscribeButton: 'Subscribe',
trendsTitle: 'TRENDS',
footerNewsletterTitle: 'Join the Circle of Authority',
footerNewsletterSubtitle: 'Subscribe to receive deep insights into algorithmic changes, AI, and search strategies that the rest of the market hasn\'t seen yet.',
footerNewsletterInput: 'your@email.com',
footerNewsletterDisclaimer: 'We respect your inbox. Zero spam, only search intelligence.'
},
'es': {
newsletter: 'Boletín',
subscribe: 'Suscríbete Ahora',
readMore: 'Leer más',
latest: 'Últimos',
allArticles: 'Todos los Artículos',
sections: 'Secciones',
publishing: 'Publicación',
contact: 'Contacto',
about: 'Sobre',
privacy: 'Privacidad',
terms: 'Términos',
featured: 'Destacado',
recent: 'Recientes',
explore: 'Explorar Insights',
search: 'Buscar',
viewAll: 'Ver Todo',
manifesto: 'Manifiesto',
methodology: 'Metodología',
heroTitle: 'El Nuevo Orden de la Búsqueda Semántica.',
heroSubtitle: 'EDITORIAL SEO 2026',
heroDescription: 'El recurso definitivo para profesionales que buscan excelencia técnica y autoridad duradera en el ecosistema de búsqueda.',
readManifesto: 'Leer Manifiesto',
publishedIn: 'Publicado en',
newsletterTitle: 'Boletín Semanal',
newsletterDescription: 'Suscríbete para recibir las últimas noticias.',
emailPlaceholder: 'tu@email.com',
subscribeButton: 'Suscríbete',
trendsTitle: 'TENDENCIAS',
footerNewsletterTitle: 'Únete al Círculo de Autoridad',
footerNewsletterSubtitle: 'Suscríbete para recibir análisis profundos sobre cambios algorítmicos, IA y estrategias de búsqueda que el resto del mercado aún no ha visto.',
footerNewsletterInput: 'tu@email.com',
footerNewsletterDisclaimer: 'Respetamos tu bandeja de entrada. Cero spam, solo inteligencia de búsqueda.'
}
};

View file

@ -0,0 +1,52 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
interface BookmarksContextType {
bookmarks: string[];
toggleBookmark: (id: string) => void;
isBookmarked: (id: string) => boolean;
}
const BookmarksContext = createContext<BookmarksContextType | undefined>(undefined);
export function BookmarksProvider({ children }: { children: React.ReactNode }) {
const [bookmarks, setBookmarks] = useState<string[]>([]);
// Load from localStorage on mount
useEffect(() => {
const saved = localStorage.getItem('seo_bookmarks');
if (saved) {
try {
setBookmarks(JSON.parse(saved));
} catch (e) {
console.error('Failed to parse bookmarks', e);
}
}
}, []);
// Sync with localStorage
const toggleBookmark = (id: string) => {
setBookmarks(prev => {
const next = prev.includes(id)
? prev.filter(item => item !== id)
: [...prev, id];
localStorage.setItem('seo_bookmarks', JSON.stringify(next));
return next;
});
};
const isBookmarked = (id: string) => bookmarks.includes(id);
return (
<BookmarksContext.Provider value={{ bookmarks, toggleBookmark, isBookmarked }}>
{children}
</BookmarksContext.Provider>
);
}
export function useBookmarks() {
const context = useContext(BookmarksContext);
if (context === undefined) {
throw new Error('useBookmarks must be used within a BookmarksProvider');
}
return context;
}

View file

@ -0,0 +1,52 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
type Language = 'pt-br' | 'en' | 'es';
interface LanguageContextType {
lang: Language;
setLang: (lang: Language) => void;
translate: (text: any, context?: string, isObject?: boolean) => Promise<any>;
isTranslating: boolean;
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [lang, setLang] = useState<Language>(() => {
const saved = localStorage.getItem('app-lang');
return (saved as Language) || 'pt-br';
});
useEffect(() => {
localStorage.setItem('app-lang', lang);
document.documentElement.lang = lang;
// Trigger Google Translate Widget
const triggerGoogleTranslate = () => {
const select = document.querySelector('.goog-te-combo') as HTMLSelectElement;
if (select) {
const googleLangCode = lang === 'pt-br' ? 'pt' : lang;
select.value = googleLangCode;
select.dispatchEvent(new Event('change'));
}
};
// Delay a bit to ensure the widget is loaded
const timer = setTimeout(triggerGoogleTranslate, 1000);
return () => clearTimeout(timer);
}, [lang]);
return (
<LanguageContext.Provider value={{ lang, setLang, translate: async (t) => t, isTranslating: false }}>
{children}
</LanguageContext.Provider>
);
};
export const useLanguage = () => {
const context = useContext(LanguageContext);
if (context === undefined) {
throw new Error('useLanguage must be used within a LanguageProvider');
}
return context;
};

View file

@ -0,0 +1,654 @@
import { Article } from '../types';
export const articles: Article[] = [
{
id: '1',
slug: 'o-impacto-da-ia-generativa-na-busca-organica-2026',
title: 'O Impacto da IA Generativa na Busca Orgânica em 2026',
excerpt: 'Como as Search Generative Experiences (SGE) estão moldando o comportamento do usuário e o que isso significa para o CTR.',
category: 'Estratégia',
publishedAt: '2026-05-01',
image: 'https://images.unsplash.com/photo-1673191898498-9bac443b2407?auto=format&fit=crop&q=80&w=1600',
readTime: '12 min',
author: {
name: 'Dr. Lucas Silva',
role: 'Lead Strategist',
avatar: 'https://i.pravatar.cc/150?u=lucas'
},
metaTitle: 'IA Generativa e SEO em 2026 | Análise Técnica',
metaDescription: "SEO em 2026: Entenda como a IA Generativa e o SGE transformaram o tráfego orgânico. Conheça as táticas de 'Conteúdo de Valor Incrementado' para vencer o Zero-Click.",
tags: ['IA', 'SGE', 'Algoritmo', 'Google'],
lang: 'pt-br',
content: `
# O Impacto da IA Generativa na Busca Orgânica em 2026
A busca não é mais o que costumava ser. Em 2026, a integração total da IA nas páginas de resultados (SERPs) criou um novo paradigma: a busca não apenas por links, mas por respostas diretas e inteligíveis.
## O Desafio do Zero-Click
Com a IA resolvendo dúvidas complexas diretamente no topo da busca, o volume de cliques para sites informativos superficiais despencou. A estratégia vencedora agora foca em **Conteúdo de Valor Incrementado** informações que a IA ainda não consegue sintetizar sem falhas.
### O que são Insights de Propriedade?
Insights de propriedade são dados, opiniões de especialistas e experiências reais que não existem em Large Language Models (LLMs). É a sua "fórmula secreta" que obriga o usuário a clicar para entender a profundidade do assunto.
## Estratégias de Sobrevivência
1. **Foco na Cauda Longa Hiper-Específica:** Consultas onde a opinião humana é necessária.
2. **Autoridade de Tópico (Topic Clusters):** Ser o dono do assunto, não apenas da palavra-chave.
3. **Optimizing for SGE Sources:** Estruturar dados para que seu site seja a fonte citada pela IA.
## Conclusão
A IA não matou o SEO; ela elevou o nível do jogo. Quem produz conteúdo medíocre está fora. Quem produz autoridade técnica está mais forte do que nunca.
`
},
{
id: '2',
slug: 'eeat-a-biblia-da-autoridade-digital',
title: 'E-E-A-T: A Bíblia da Autoridade Digital Moderna',
excerpt: 'Análise profunda sobre Experiência, Especialidade, Autoridade e Confiança no novo algoritmo.',
category: 'Estratégia',
publishedAt: '2026-04-28',
image: 'https://images.unsplash.com/photo-1552664730-d307ca884978?auto=format&fit=crop&q=80&w=1600',
readTime: '15 min',
author: {
name: 'Beatriz Costa',
role: 'Head of Content',
avatar: 'https://i.pravatar.cc/150?u=beatriz'
},
metaTitle: 'Guia Definitivo E-E-A-T para SEO em 2026',
metaDescription: "Domine o E-E-A-T em 2026. Saiba por que a experiência humana é o diferencial contra a IA e como construir autoridade real para dominar os rankings do Google.",
tags: ['EEAT', 'Autoridade', 'Confiança', 'Especialidade'],
lang: 'pt-br',
content: `
# E-E-A-T: A Bíblia da Autoridade Digital Moderna
Se você quer rankear em 2026, você precisa entender o E-E-A-T. Não é um fator de ranking direto, mas uma diretriz que permeia todo o algoritmo do Google.
## O "E" de Experiência
Adicionado alguns anos, o fator experiência tornou-se crucial. O Google quer saber: você realmente usou este produto? Você realmente viveu essa situação?
<blockquote>
"A experiência humana é o único dado que a IA não consegue alucinar com perfeição em 2026."
</blockquote>
## Como demonstrar experiência?
- Use fotos originais, não apenas de bancos de imagem.
- Narre estudos de caso reais.
- Evite generalismos. Use "Eu descobri que..." em vez de "É geralmente aceito que...".
## Especialidade e Autoridade
Estes pilares são construídos através da consistência. Não adianta falar de SEO um dia e de culinária no outro se você quer ser visto como um especialista em tecnologia.
`
},
{
id: '3',
slug: 'auditoria-tecnica-seo-infraestrutura-performance',
title: 'Auditoria Técnica: Garantindo uma Infraestrutura de Elite',
excerpt: 'O guia definitivo para Core Web Vitals, Rendering e Crawl Budget em sites de larga escala.',
category: 'Técnico',
publishedAt: '2026-04-25',
image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&q=80&w=1600',
readTime: '18 min',
author: {
name: 'Marcos Tech',
role: 'SEO Engineer',
avatar: 'https://i.pravatar.cc/150?u=marcos'
},
metaTitle: 'Auditoria Técnica de SEO 2026 | Guia para Desenvolvedores',
metaDescription: "Guia definitivo de Auditoria Técnica SEO 2026. Otimize INP, LCP e Rendering para garantir uma infraestrutura de elite e performance máxima em sites de larga escala.",
tags: ['Technical SEO', 'Performance', 'Web Vitals', 'Infrastructure'],
lang: 'pt-br',
content: `
# Auditoria Técnica: Garantindo uma Infraestrutura de Elite
Em 2026, a performance técnica não é um diferencial é o requisito mínimo para entrar na arena.
## O novo Core Web Vital: Interaction to Next Paint (INP)
O INP substituiu oficialmente o FID como a métrica de interatividade. Ele mede quão rápido seu site responde a qualquer interação do usuário.
### Otimizando para INP
- Reduza o tempo de execução do JavaScript principal.
- Use Web Workers para tarefas pesadas.
- Evite layouts complexos que causem reflow excessivo.
## Indexação e Crawl Budget
Para sites com milhares de páginas, cada segundo do bot do Google no seu servidor conta.
- Use Server-Side Rendering (SSR) de forma inteligente.
- Mantenha um Sitemap dinâmico e limpo.
- Monitore erros de 404 e cadeias de redirect.
`
},
{
id: '4',
slug: 'link-building-atraves-de-dados-proprietarios',
title: 'Link Building através de Dados Proprietários',
excerpt: 'Esqueça o outreach em massa. Aprenda a criar pesquisas que atraem links de forma orgânica.',
category: 'Autoridade',
publishedAt: '2026-04-20',
image: 'https://images.unsplash.com/photo-1557838923-2985c318be48?auto=format&fit=crop&q=80&w=1600',
readTime: '10 min',
author: {
name: 'João Links',
role: 'PR Specialist',
avatar: 'https://i.pravatar.cc/150?u=joao'
},
metaTitle: 'Estratégias de Link Building com Dados | SEO Authority',
metaDescription: "Transforme dados em imãs de links. Aprenda a criar pesquisas proprietárias e reports de mercado que geram backlinks editoriais espontâneos de alta autoridade.",
tags: ['Link Building', 'Backlinks', 'Content Marketing', 'PR'],
lang: 'pt-br',
content: `
# Link Building através de Dados Proprietários
O link building clássico de "troca de favores" está cada vez menos eficiente. O Google valoriza links editoriais espontâneos. A melhor forma de conseguir isso? Ser a fonte da notícia.
## Criando Imãs de Links
Quando você produz um infográfico com dados originais do seu setor, outros blogs e portais de notícias vão citar você como referência.
### Tipos de Conteúdo que Atraem Links:
1. **Pesquisas de Mercado:** Tendências para o próximo ano.
2. **Calculadoras e Ferramentas:** Soluções para problemas práticos.
3. **Análises de Impacto:** Como uma mudança econômica afetou seu nicho.
## A Importância do Texto Âncora
Links com âncoras variadas e naturais para páginas de autoridade (não apenas para a home) são o sinal mais forte de um perfil de links saudável.
`
},
{
id: '5',
slug: 'arquitetura-de-informacao-pillar-pages-e-clusters',
title: 'Arquitetura de Informação: Pillar Pages e Topic Clusters',
excerpt: 'Como estruturar seu blog para que o Google entenda sua autoridade em cada pilar de conteúdo.',
category: 'Estratégia',
publishedAt: '2026-04-15',
image: 'https://images.unsplash.com/photo-1542744094-3a31f272c490?auto=format&fit=crop&q=80&w=1600',
readTime: '14 min',
author: {
name: 'Beatriz Costa',
role: 'Head of Content',
avatar: 'https://i.pravatar.cc/150?u=beatriz'
},
metaTitle: 'Arquitetura de Conteúdo: Pillar Pages e Clusters em 2026',
metaDescription: "Topic Clusters e Pillar Pages: Descubra como estruturar seu site para dominar a autoridade semântica em 2026. O guia completo para organizar conteúdo estrategicamente.",
tags: ['Architecture', 'Content Strategy', 'Topic Clusters'],
lang: 'pt-br',
content: `
# Arquitetura de Informação: Pillar Pages e Topic Clusters
Organizar o conteúdo do seu blog é tão importante quanto escrevê-lo. Uma estrutura de **Topic Clusters** ajuda o Google a mapear sua expertise.
## O Que é uma Pillar Page?
É um guia abrangente sobre um tema central. Por exemplo, "SEO para Iniciantes".
## O Que são Clusters?
São artigos específicos que aprofundam tópicos mencionados na Pillar Page. Por exemplo, "O que é Robots.txt?".
### O Link Mágico
Todos os artigos satélites (clusters) devem apontar de volta para a Pillar Page. Isso cria uma teia de autoridade.
`
},
{
id: '6',
slug: 'seo-para-e-commerce-otimizando-paginas-de-categoria',
title: 'SEO para E-commerce: O Segredo das Páginas de Categoria',
excerpt: 'Por que as páginas de categoria trazem mais tráfego que as páginas de produto e como otimizá-las.',
category: 'Negócios',
publishedAt: '2026-04-10',
image: 'https://images.unsplash.com/photo-1556742049-045a728bbd66?auto=format&fit=crop&q=80&w=1600',
readTime: '11 min',
author: {
name: 'Ricardo Vendas',
role: 'E-commerce Expert',
avatar: 'https://i.pravatar.cc/150?u=ricardo'
},
metaTitle: 'SEO para E-commerce: Guias de Categoria em 2026',
metaDescription: "SEO para E-commerce: Por que as categorias são sua maior fonte de tráfego? Aprenda táticas avançadas para otimizar páginas de categoria e converter mais no Google.",
tags: ['E-commerce', 'Category Pages', 'Product SEO'],
lang: 'pt-br',
content: `
# SEO para E-commerce: O Segredo das Páginas de Categoria
Muitos donos de loja focam nas páginas de produto. Mas o volume real de busca está nas **Categorias**.
## Por que focar em Categorias?
Um usuário que busca "tênis de corrida" está em uma fase de consideração muito mais valiosa do que quem busca o modelo específico "Nike Pegasus 41 Azul".
### Otimização On-Page para Categorias:
- **Texto de Apoio:** Não enterre o texto no final da página. Use introduções úteis.
- **Interlinkagem:** Linke para seus produtos mais populares diretamente da categoria.
`
},
{
id: '7',
slug: 'schema-markup-avancado-e-dados-estruturados',
title: 'Schema Markup Avançado: Indo além do básico',
excerpt: 'Utilizando JSON-LD para fornecer contexto profundo aos mecanismos de busca.',
category: 'Técnico',
publishedAt: '2026-04-05',
image: 'https://images.unsplash.com/photo-1555066931-4365d14bab8c?auto=format&fit=crop&q=80&w=1600',
readTime: '13 min',
author: {
name: 'Marcos Tech',
role: 'SEO Engineer',
avatar: 'https://i.pravatar.cc/150?u=marcos'
},
metaTitle: 'Schema Markup Avançado JSON-LD | Guia Técnico 2026',
metaDescription: "Schema Markup 2026: Vá além do básico com JSON-LD. Aprenda a fornecer contexto profundo à IA do Google e conquistar Rich Snippets poderosos para seu site.",
tags: ['Schema', 'Structured Data', 'JSON-LD', 'Rich Snippets'],
lang: 'pt-br',
content: `
# Schema Markup Avançado: Indo além do básico
Dados estruturados são o vocabulário que o Google usa para entender o seu conteúdo. Em 2026, ignorar o Schema é como falar uma língua diferente do buscador.
## O Poder do JSON-LD
Prefira sempre a implementação via JSON-LD no cabeçalho ou rodapé do HTML.
### Esquemas Indispensáveis:
- **Article/BlogPosting:** Para conteúdo editorial.
- **FAQPage:** Para ocupar mais espaço na SERP.
- **Person/Organization:** Para reforçar o E-E-A-T do autor.
`
},
{
id: '8',
slug: 'seo-off-page-alem-dos-backlinks',
title: 'SEO Off-Page: A Autoridade além dos Backlinks',
excerpt: 'Menções de marca, sinais sociais e a construção de reputação no ambiente digital.',
category: 'Autoridade',
publishedAt: '2026-04-01',
image: 'https://images.unsplash.com/photo-1557838923-2985c318be48?auto=format&fit=crop&q=80&w=1600',
readTime: '12 min',
author: {
name: 'João Links',
role: 'PR Specialist',
avatar: 'https://i.pravatar.cc/150?u=joao'
},
metaTitle: 'SEO Off-Page e Reputação de Marca em 2026',
metaDescription: "SEO Off-Page em 2026: Autoridade é mais que backlinks. Entenda o peso das menções de marca, sinais sociais e co-ocorrência na reputação digital moderna.",
tags: ['Off Page', 'Reputation', 'Branding', 'Signals'],
lang: 'pt-br',
content: `
# SEO Off-Page: A Autoridade além dos Backlinks
O Google está cada vez melhor em entender menções de marca, mesmo quando elas não vêm acompanhadas de um link (Unlinked Brand Mentions).
## Sinais de Co-ocorrência
Se o nome da sua marca aparece frequentemente perto de palavras-chave como "melhor agência de SEO", o Google começa a associar sua entidade a esse tópico.
`
},
{
id: '9',
slug: 'psicologia-de-busca-entendendo-a-intencao',
title: 'Psicologia de Busca: Entendendo a Intenção Real',
excerpt: 'Por que o volume de busca é uma métrica vaidosa e como focar no que realmente converte.',
category: 'Estratégia',
publishedAt: '2026-03-25',
image: 'https://images.unsplash.com/photo-1518349619113-03114f06ac3a?auto=format&fit=crop&q=80&w=1600',
readTime: '14 min',
author: {
name: 'Dr. Lucas Silva',
role: 'Lead Strategist',
avatar: 'https://i.pravatar.cc/150?u=lucas'
},
metaTitle: 'Psicologia e Intenção de Busca em SEO (2026)',
metaDescription: "Pare de perseguir volume e entenda a Intenção de Busca. Guia de Psicologia de Busca para mapear a jornada do usuário e converter tráfego em resultados reais.",
tags: ['Psychology', 'User Intent', 'Keywords', 'Funnel'],
lang: 'pt-br',
content: `
# Psicologia de Busca: Entendendo a Intenção Real
Nem toda busca é criada igual. O usuário pode estar procurando informação, navegando para um site específico ou pronto para comprar.
## Otimizando para a Intenção
Se o seu artigo é um guia ensinando "como fazer", mas tenta vender um produto agressivamente no primeiro parágrafo, você está quebrando a intenção do usuário.
`
},
{
id: '10',
slug: 'otimizacao-para-busca-por-voz-e-assistentes',
title: 'Otimização para Busca por Voz e Assistentes em 2026',
excerpt: 'Como estruturar seu conteúdo para ser a resposta única da Alexa, Siri e Google Assistant.',
category: 'Técnico',
publishedAt: '2026-03-20',
image: 'https://images.unsplash.com/photo-1589254065878-42c9da997008?auto=format&fit=crop&q=80&w=1600',
readTime: '9 min',
author: {
name: 'Marcos Tech',
role: 'SEO Engineer',
avatar: 'https://i.pravatar.cc/150?u=marcos'
},
metaTitle: 'SEO para Busca por Voz 2026 | Dicas Práticas',
metaDescription: "SEO para Busca por Voz: Como ser a resposta única da Alexa, Siri e Google Assistant em 2026. Otimize seu conteúdo para NLP e linguagem natural hoje.",
tags: ['Voice Search', 'NLP', 'Conversational SEO'],
lang: 'pt-br',
content: `
# Otimização para Busca por Voz e Assistentes em 2026
Com o avanço do Processamento de Linguagem Natural (NLP), a busca por voz tornou-se extremamente precisa.
## Linguagem Natural
As pessoas não falam como escrevem. Em vez de "melhor SEO curso", elas perguntam "Ei Google, qual é o melhor curso de SEO disponível hoje?".
`
},
{
id: '11',
slug: 'seo-local-para-profissionais-liberais',
title: 'SEO Local: Dominando sua Região em 2026',
excerpt: 'Como profissionais liberais podem dominar o Google Maps e a busca local.',
category: 'Negócios',
publishedAt: '2026-03-15',
image: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&q=80&w=1600',
readTime: '8 min',
author: {
name: 'Dr. Lucas Silva',
role: 'Lead Strategist',
avatar: 'https://i.pravatar.cc/150?u=lucas'
},
metaTitle: 'Guia SEO Local 2026 | Google My Business',
metaDescription: "SEO Local 2026: Como dominar o Map Pack e Google My Business. Atraia clientes qualificados em sua região com estratégias de autoridade geográfica e IA local.",
tags: ['Local SEO', 'Google Maps'],
lang: 'pt-br',
content: `
# SEO Local: Dominando sua Região em 2026
O SEO Local tornou-se a espinha dorsal de pequenas e médias empresas. Em 2026, a presença no Google Maps não é apenas uma conveniência, é o destino final da jornada do consumidor local.
## Otimização do Perfil da Empresa
Manter suas informações atualizadas é o passo zero. Mas a autoridade local vai além disso: as avaliações com fotos e palavras-chave específicas do local agora pesam mais do que nunca no "Map Pack".
### O que mudou?
- **Respostas de IA Locais:** O Google Assistant agora usa os dados do seu perfil para responder perguntas complexas sobre disponibilidade e serviços.
- **Links Locais:** Citações em portais de notícias do seu bairro ou cidade transferem uma autoridade geográfica impossível de ignorar.
`
},
{
id: '12',
slug: 'erros-comuns-migracao-de-site',
title: 'Migração de Site: Como não perder tráfego',
excerpt: 'O checklist de sobrevivência para mudar seu domínio ou plataforma sem desastres.',
category: 'Técnico',
publishedAt: '2026-03-10',
image: 'https://images.unsplash.com/photo-1504384308090-c894fdcc538d?auto=format&fit=crop&q=80&w=1600',
readTime: '20 min',
author: {
name: 'Marcos Tech',
role: 'SEO Engineer',
avatar: 'https://i.pravatar.cc/150?u=marcos_dev'
},
metaTitle: 'Migração de Site 2026 | Checklist de SEO',
metaDescription: "Checklist Definitivo de Migração de Site em 2026. Aprenda a gerenciar redirects 301, monitorar GSC e evitar quedas de tráfego traumáticas durante mudanças.",
tags: ['Migration', 'Redirects'],
lang: 'pt-br',
content: `
# Migração de Site: Como não perder tráfego
Migrar um site é um dos processos mais arriscados em SEO. Um erro no mapeamento de URLs pode destruir anos de autoridade acumulada em poucos dias.
## O Checklist de Sobrevivência
1. **Mapeamento de 1 para 1:** Garanta que cada URL antiga tenha um destino lógico na nova estrutura.
2. **Teste em Staging:** Nunca faça o deploy sem testar os redirects em um ambiente controlado.
3. **Monitoramento Granular:** Após a virada, acompanhe o Google Search Console diariamente para identificar erros 404 inesperados.
`
},
{
id: '13',
slug: 'futuro-da-busca-por-video-youtube-seo',
title: 'O Futuro da Busca por Vídeo e YouTube SEO',
excerpt: 'Como os vídeos estão sendo indexados diretamente nas SERPs principais.',
category: 'Estratégia',
publishedAt: '2026-03-05',
image: 'https://images.unsplash.com/photo-1492724441997-5dc865305da7?auto=format&fit=crop&q=80&w=1600',
readTime: '10 min',
author: {
name: 'Beatriz Costa',
role: 'Head of Content',
avatar: 'https://i.pravatar.cc/150?u=beatriz_c'
},
metaTitle: 'Vertical Search: YouTube e Vídeo SEO em 2026',
metaDescription: "Google Video SEO & YouTube 2026: Como dominar a busca multimodal. Aprenda a otimizar momentos chave, transcrições e capítulos para visibilidade máxima.",
tags: ['Video SEO', 'YouTube'],
lang: 'pt-br',
content: `
# O Futuro da Busca por Vídeo e YouTube SEO
Em 2026, a busca é multimodal. Vídeos curtos e tutoriais aprofundados estão ocupando espaços premium na página principal do Google.
## Indexação de Momentos Chave
O Google não indexa o vídeo, mas também os capítulos e as transcrições, permitindo que o usuário "caia" exatamente no segundo que resolve sua dúvida.
`
},
{
id: '14',
slug: 'ferramentas-gratuitas-seo-indispensaveis',
title: '15 Ferramentas Gratuitas de SEO Indispensáveis',
excerpt: 'O arsenal que você precisa para gerenciar seus projetos sem gastar um centavo.',
category: 'Estratégia',
publishedAt: '2026-03-01',
image: 'https://images.unsplash.com/photo-1551288049-bbdac8a28a1e?auto=format&fit=crop&q=80&w=1600',
readTime: '12 min',
author: {
name: 'Dr. Lucas Silva',
role: 'Lead Strategist',
avatar: 'https://i.pravatar.cc/150?u=lucas_s'
},
metaTitle: 'Ferramentas Gratuitas de SEO 2026',
metaDescription: "As 15 Melhores Ferramentas Gratuitas de SEO para 2026. Tenha o arsenal técnico completo sem gastar um centavo: de Search Console a monitoramento de tendências.",
tags: ['Tools', 'Free'],
lang: 'pt-br',
content: `
# 15 Ferramentas Gratuitas de SEO Indispensáveis
Você não precisa de assinaturas caras de mil dólares por mês para começar a rankear. Aqui está o arsenal básico para 2026:
1. **Google Search Console:** A fonte da verdade direta do buscador.
2. **AnswerThePublic:** Para entender a intenção de busca.
3. **Screaming Frog (Versão Free):** Para auditorias técnicas básicas.
4. **Google Trends:** Para capturar tendências sazonais antes da concorrência.
`
},
{
id: '15',
slug: 'seo-para-saas-estratatégias-de-growth',
title: 'SEO para SaaS: Estratégias de Growth Através do Orgânico',
excerpt: 'Como empresas de software escalam aquisição de usuários usando conteúdo técnico.',
category: 'Negócios',
publishedAt: '2026-02-25',
image: 'https://images.unsplash.com/photo-1526628953301-3e589a6a8b74?auto=format&fit=crop&q=80&w=1600',
readTime: '15 min',
author: {
name: 'Beatriz Costa',
role: 'Head of Content',
avatar: 'https://i.pravatar.cc/150?u=beatriz'
},
metaTitle: 'SaaS SEO Strategy 2026 | Growth Marketing',
metaDescription: "Growth SEO para SaaS: Como escalar a aquisição de usuários no orgânico. Aprenda a estruturar o funil de conteúdo para converter buscas em usuários ativos de software.",
tags: ['SaaS', 'Growth'],
lang: 'pt-br',
content: `
# SEO para SaaS: Estratégias de Growth
O custo de aquisição (CAC) via anúncios subiu 400% nos últimos anos. Para empresas SaaS, o orgânico tornou-se o canal de crescimento mais sustentável.
## O Funil de Conteúdo SaaS
- **TOFU:** Resolvendo problemas genéricos (ex: "Como gerenciar projetos").
- **MOFU:** Comparativos (ex: "Software X vs Software Y").
- **BOFU:** Intenção direta (ex: "Melhor CRM para imobiliárias").
`
},
{
id: '16',
slug: 'analise-de-concorrencia-descobrindo-gaps',
title: 'Análise de Concorrência: Descobrindo Gaps de Conteúdo',
excerpt: 'Como encontrar o que seus concorrentes não estão cobrindo e dominar essas buscas.',
category: 'Estratégia',
publishedAt: '2026-02-20',
image: 'https://images.unsplash.com/photo-1542744173-8e7e53415bb0?auto=format&fit=crop&q=80&w=1600',
readTime: '9 min',
author: {
name: 'Dr. Lucas Silva',
role: 'Lead Strategist',
avatar: 'https://i.pravatar.cc/150?u=lucas'
},
metaTitle: 'Análise de Concorrentes SEO 2026',
metaDescription: "Descubra os pontos fracos dos seus concorrentes. Guia de Análise de Concorrência SEO para identificar gaps de conteúdo e dominar nichos subexplorados.",
tags: ['Competition', 'Strategy'],
lang: 'pt-br',
content: `
# Análise de Concorrência: Gaps de Conteúdo
Não tente ganhar dos gigantes nas palavras-chave mais difíceis. Procure os **Gaps**. Onde a resposta deles é incompleta? Onde eles estão desatualizados?
## Ferramentas de Gap
Use relatórios de "Content Gap" para ver onde seus dois maiores concorrentes rankeiam mas você não. Esse é o seu roteiro de conteúdo para o próximo trimestre.
`
},
{
id: '17',
slug: 'otimizacao-de-imagens-para-google-discover',
title: 'Otimização de Imagens para o Google Discover',
excerpt: 'Como aparecer no feed de notícias do Google com imagens de impacto e tags corretas.',
category: 'Técnico',
publishedAt: '2026-02-15',
image: 'https://images.unsplash.com/photo-1493612276216-ee3925520721?auto=format&fit=crop&q=80&w=1600',
readTime: '7 min',
author: {
name: 'Marcos Tech',
role: 'SEO Engineer',
avatar: 'https://i.pravatar.cc/150?u=marcos'
},
metaTitle: 'Google Discover SEO 2026',
metaDescription: "Google Discover SEO: O segredo para atrair milhões de acessos via imagens de impacto. Requisitos técnicos e táticas visuais para dominar o feed em 2026.",
tags: ['Images', 'Discover'],
lang: 'pt-br',
content: `
# Imagens no Google Discover
O Discover é visual. Se a sua imagem não "parar o polegar" do usuário, seu CTR será nulo.
## Requisitos Técnicos
- **Tamanho:** Mínimo de 1200px de largura.
- **Alt Text:** Descritivo, mas focado no contexto visual.
- **Qualidade:** Evite fotos de estúdio genéricas; fotos reais com impacto emocional performam 3x melhor.
`
},
{
id: '18',
slug: 'seo-para-news-otimizando-artigos-de-noticias',
title: 'SEO para News: Otimizando Artigos em Tempo Real',
excerpt: 'Técnicas para aparecer no Google News e Top Stories de forma instantânea.',
category: 'Estratégia',
publishedAt: '2026-02-10',
image: 'https://images.unsplash.com/photo-1495020689067-958852a7765e?auto=format&fit=crop&q=80&w=1600',
readTime: '11 min',
author: {
name: 'Beatriz Costa',
role: 'Head of Content',
avatar: 'https://i.pravatar.cc/150?u=beatriz'
},
metaTitle: 'Google News SEO 2026',
metaDescription: "Domine o Top Stories e Google News. Aprenda técnicas de SEO em tempo real para portais de notícias, focando em velocidade de indexação e autoridade do autor.",
tags: ['News', 'Real Time'],
lang: 'pt-br',
content: `
# SEO para News
Notícia é o ápice da temporariedade. Rankear no "Top Stories" exige uma combinação de autoridade de marca e velocidade de indexação técnica.
## Entidades e Jornalismo
O Google agora identifica a entidade do jornalista. Quem escreveu? Esse autor tem histórico no assunto? Isso é o E-E-A-T aplicado ao jornalismo digital.
`
},
{
id: '19',
slug: 'impacto-dos-links-nofollow-e-ugc',
title: 'O Impacto Real dos Links Nofollow, Sponsored e UGC',
excerpt: 'Analisando como os hints de atributo de link influenciam o algoritmo moderno.',
category: 'Autoridade',
publishedAt: '2026-02-05',
image: 'https://images.unsplash.com/photo-1544197150-b99a580bb7a8?auto=format&fit=crop&q=80&w=1600',
readTime: '13 min',
author: {
name: 'João Links',
role: 'PR Specialist',
avatar: 'https://i.pravatar.cc/150?u=joao'
},
metaTitle: 'Link Attributes 2026 | Nofollow vs Sponsored',
metaDescription: "Nofollow, Sponsored ou UGC? Entenda o impacto real dos atributos de link no algoritmo do Google em 2026 e quando usar cada um para sua autoridade.",
tags: ['Attributes', 'Links'],
lang: 'pt-br',
content: `
# Links Nofollow e UGC em 2026
Desde que se tornaram "hints", os links nofollow pararam de ser ignorados. O Google agora decide se quer ou não passar autoridade por eles.
## Quando usar cada um?
- **UGC:** Para links em comentários de blog ou fóruns.
- **Sponsored:** Para qualquer tipo de publieditorial or link pago.
- **Nofollow:** Para links que você não quer necessariamente endossar.
`
},
{
id: '20',
slug: 'monetizacao-blog-seo-autoridade',
title: 'Monetização de Blog através de SEO de Autoridade',
excerpt: 'Como transformar seu tráfego orgânico em uma máquina de receita sustentável.',
category: 'Negócios',
publishedAt: '2026-02-01',
image: 'https://images.unsplash.com/photo-1553729459-efe14ef6055d?auto=format&fit=crop&q=80&w=1600',
readTime: '16 min',
author: {
name: 'Ricardo Vendas',
role: 'E-commerce Expert',
avatar: 'https://i.pravatar.cc/150?u=ricardo'
},
metaTitle: 'Monetização SEO 2026 | Guia de Lucratividade',
metaDescription: "Como lucrar com seu blog de autoridade em 2026. Saia das métricas de vaidade e transforme seu tráfego orgânico em uma máquina de receita sustentável.",
tags: ['Monetization', 'Business'],
lang: 'pt-br',
content: `
# Monetização de Blogs de Autoridade
Tráfego é vaidade, lucro é sanidade. Em 2026, os blogs de nicho mais lucrativos saíram do AdSense e foram para modelos de afiliados de alto ticket e produtos digitais próprios.
## Estrutura de Conversão
Cada artigo deve ter um objetivo de conversão claro. Se o usuário veio por uma dúvida técnica, ofereça uma ferramenta ou curso que resolva essa dor.
`
},
{
id: '21',
slug: 'the-future-of-organic-search-2026',
title: 'The Future of Organic Search in 2026',
excerpt: 'How Search Generative Experiences are redefining digital landscapes.',
category: 'Estratégia',
publishedAt: '2026-05-04',
image: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=1600',
readTime: '10 min',
author: {
name: 'Dr. Lucas Silva',
role: 'Lead Strategist',
avatar: 'https://i.pravatar.cc/150?u=lucas'
},
metaTitle: 'The Future of Search | English Edition',
metaDescription: "Organic Search 2026: Deep dive into the future of SEO, SGE, and AI-driven user behavior. Learn how to stay ahead in the evolving digital landscape.",
tags: ['AI', 'SEO', 'Future'],
lang: 'en',
content: `
# The Future of Organic Search in 2026
The search landscape is evolving rapidly. Artificial intelligence is no longer just a backend tool; it's the interface itself.
## SGE and User Behavior
Search Generative Experience (SGE) provides direct answers at the top of the SERP, reducing the need for clicks on simple informational queries. Content creators must adapt by offering unique insights that go beyond simple data aggregation.
`
}
];

126
Template-02/src/index.css Normal file
View file

@ -0,0 +1,126 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Outfit:wght@300;400;500;600;700;800&display=swap');
@import "tailwindcss";
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-display: "Outfit", sans-serif;
--color-brand: #0d9488; /* Teal 600 */
--color-brand-light: #2dd4bf; /* Teal 400 */
--color-brand-dark: #0f766e; /* Teal 700 */
--radius-xl: 1rem;
--radius-2xl: 1.5rem;
--radius-3xl: 2.5rem;
--spacing-section: 12rem;
}
/* Hide Google Translate Bar */
.goog-te-banner-frame.skiptranslate,
.goog-te-banner-frame,
#goog-gt-tt,
.goog-te-balloon-frame {
display: none !important;
}
body {
top: 0 !important;
}
.goog-tooltip {
display: none !important;
}
.goog-tooltip:hover {
display: none !important;
}
.goog-text-highlight {
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
}
@layer base {
body {
@apply text-slate-900 antialiased bg-white font-sans selection:bg-brand/10 selection:text-brand;
}
h1, h2, h3, h4, h5, h6 {
@apply font-display tracking-tight text-slate-950;
}
}
@layer components {
.section-padding {
@apply py-32 sm:py-48;
}
.markdown-body {
@apply mx-auto max-w-3xl text-[18px] text-slate-600 leading-[1.8] font-sans;
}
.markdown-body h2 {
@apply text-3xl sm:text-4xl font-display font-bold mt-16 mb-8 tracking-tight text-slate-900 border-b border-slate-100 pb-4;
}
.markdown-body h3 {
@apply text-2xl font-bold mt-12 mb-6 tracking-tight text-slate-800;
}
.markdown-body p {
@apply mb-6;
}
.markdown-body ul, .markdown-body ol {
@apply mb-8 ml-6 space-y-3;
}
.markdown-body blockquote {
@apply pl-8 border-l-4 border-brand-light italic text-slate-800 text-xl my-12 leading-relaxed bg-slate-50 py-8 pr-8 rounded-r-2xl;
}
.markdown-body strong {
@apply font-semibold text-slate-900;
}
.markdown-body a {
@apply text-brand font-medium underline decoration-brand/30 underline-offset-4 hover:decoration-brand transition-all;
}
.markdown-body img {
@apply rounded-3xl my-12 shadow-md border border-slate-100;
}
/* Glass Effects */
.glass {
@apply bg-white/70 backdrop-blur-xl border border-white/20 shadow-sm;
}
.glass-dark {
@apply bg-slate-950/80 backdrop-blur-xl border border-white/10 shadow-2xl;
}
/* Buttons */
.btn-primary {
@apply px-6 py-3 bg-brand text-white rounded-xl font-semibold hover:bg-brand-dark transition-all active:scale-95 shadow-sm hover:shadow-teal-200/50;
}
.btn-outline {
@apply px-6 py-3 border border-slate-200 rounded-xl font-semibold hover:bg-slate-50 hover:border-slate-300 transition-all active:scale-95 text-slate-700;
}
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #e2e8f0;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #cbd5e1;
}

View file

@ -0,0 +1,14 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatDate(date: string) {
return new Intl.DateTimeFormat('pt-BR', {
day: 'numeric',
month: 'long',
year: 'numeric',
}).format(new Date(date));
}

10
Template-02/src/main.tsx Normal file
View file

@ -0,0 +1,10 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

View file

@ -0,0 +1,92 @@
import { motion } from 'motion/react';
import SEO from '../components/SEO';
import { Target, Zap, ShieldCheck, ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
export default function About() {
return (
<>
<SEO
title="Nosso Manifesto | Inteligência & Autoridade Digital"
description="Entenda a filosofia por trás do SEOHUB e por que acreditamos que a busca orgânica é o pilar de qualquer negócio perene."
/>
<section className="relative pt-32 pb-24 overflow-hidden">
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full max-w-7xl h-full -z-10 bg-[radial-gradient(circle_at_top_right,rgba(20,184,166,0.05),transparent_50%)]" />
<div className="mx-auto max-w-4xl px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center mb-24"
>
<span className="inline-flex items-center gap-2 px-3 py-1 bg-brand/10 text-brand rounded-full text-[10px] font-black uppercase tracking-widest mb-8 border border-brand/20">Nosso Manifesto</span>
<h1 className="text-5xl sm:text-7xl font-display font-black text-slate-950 mb-8 tracking-tight">
A Nova Inteligência <span className="text-brand">Orgânica.</span>
</h1>
<p className="text-xl text-slate-600 leading-relaxed max-w-2xl mx-auto font-medium">
No SEOHUB, não acreditamos em "hacks". Acreditamos em autoridade semântica, relevância técnica e o fim do conteúdo genérico.
</p>
</motion.div>
<div className="markdown-body">
<h2>Por que existimos?</h2>
<p>
O mercado de marketing digital foi inundado por conteúdo superficial e táticas de curto prazo que morrem a cada atualização do Google. Nós surgimos para ser o contraponto: uma publicação estratégica dedicada à profundidade e ao futuro da busca.
</p>
<div className="my-20 grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="p-10 bg-white rounded-3xl border border-slate-100 shadow-sm hover:shadow-xl transition-all">
<div className="h-14 w-14 bg-brand/10 rounded-2xl flex items-center justify-center text-brand mb-8">
<Target size={28} />
</div>
<h4 className="text-2xl font-display font-bold mb-4 text-slate-950">Intenção & Contexto</h4>
<p className="text-slate-500 text-sm leading-relaxed font-medium">
Não focamos em volume de palavras-chave, mas na resolução real do problema do usuário. Na era semântica, o contexto é o novo rei.
</p>
</div>
<div className="p-10 bg-white rounded-3xl border border-slate-100 shadow-sm hover:shadow-xl transition-all">
<div className="h-14 w-14 bg-brand/10 rounded-2xl flex items-center justify-center text-brand mb-8">
<Zap size={28} />
</div>
<h4 className="text-2xl font-display font-bold mb-4 text-slate-950">Infraestrutura Ágil</h4>
<p className="text-slate-500 text-sm leading-relaxed font-medium">
A performance é o alicerce. Sem um ecossistema técnico rápido e acessível, a melhor estratégia de conteúdo é irrelevante.
</p>
</div>
</div>
<blockquote className="my-24">
"O SEO moderno não é sobre enganar o robô, é sobre ser a melhor resposta possível em um mar de ruído infinito."
</blockquote>
<h2>Nossa Visão 2026</h2>
<p>
Com a ascensão da IA regenerativa e das buscas conversacionais, a autoridade de marca tornou-se o único escudo contra a desintermediação. Nossa missão é documentar essa transição.
</p>
<div className="mt-20 p-12 bg-slate-950 rounded-[3rem] text-white flex flex-col md:flex-row items-center gap-12 relative overflow-hidden shadow-2xl">
<div className="absolute top-0 right-0 w-64 h-64 bg-brand/10 blur-3xl rounded-full" />
<div className="shrink-0 relative z-10">
<ShieldCheck size={72} className="text-brand" />
</div>
<div className="relative z-10">
<h3 className="text-white mt-0 mb-4 font-display text-3xl">Compromisso com a Precisão</h3>
<p className="text-slate-400 text-base mb-0 font-medium">
Cada framework ou guia técnico passa por um processo rigoroso de verificação e testes em propriedades reais antes de ser compartilhado com nossa comunidade.
</p>
</div>
</div>
</div>
<div className="mt-24 text-center">
<Link to="/contato" className="btn-primary inline-flex items-center gap-2 group">
Trabalhe Conosco
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</Link>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,56 @@
import { motion } from 'motion/react';
import { articles } from '../data/articles';
import ArticleCard from '../components/ArticleCard';
import SEO from '../components/SEO';
import { useLanguage } from '../contexts/LanguageContext';
import { translations } from '../constants';
export default function Archive() {
const { lang } = useLanguage();
const t = translations[lang];
return (
<>
<SEO
title="Arquivo Editorial | SEOHUB"
description="Explore nossa coleção completa de artigos, guias e estudos de caso sobre SEO e IA."
/>
<div className="pt-32 pb-24 min-h-screen bg-white">
<div className="max-w-7xl mx-auto px-6">
<header className="mb-20">
<div className="flex items-center gap-4 mb-8">
<span className="w-12 h-[1px] bg-brand" />
<span className="text-[10px] font-black uppercase tracking-[0.3em] text-brand">Memória Editorial</span>
</div>
<h1 className="text-6xl sm:text-8xl font-display font-black text-slate-950 tracking-[-0.04em] leading-[0.9] mb-8">
Arquivo<br />Completo.
</h1>
<p className="text-xl text-slate-500 max-w-2xl font-medium leading-relaxed">
Uma linha do tempo técnica sobre a evolução da busca orgânica e inteligência semântica.
</p>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-12">
{articles.map((article, index) => (
<motion.div
key={article.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<ArticleCard article={article} />
</motion.div>
))}
</div>
{articles.length === 0 && (
<div className="text-center py-32">
<p className="text-slate-400 font-bold uppercase tracking-widest text-sm">Nenhum registro encontrado.</p>
</div>
)}
</div>
</div>
</>
);
}

View file

@ -0,0 +1,406 @@
import { useParams, Link, useNavigate } from 'react-router-dom';
import { articles } from '../data/articles';
import SEO from '../components/SEO';
import ReactMarkdown from 'react-markdown';
import { Clock, Calendar, Share2, Bookmark, ArrowRight, MessageCircle, Loader2, Sparkles, Twitter, Linkedin, Facebook, Link2, CheckCircle2 } from 'lucide-react';
import { motion, AnimatePresence, useScroll, useSpring } from 'motion/react';
import { formatDate, cn } from '../lib/utils';
import { useLanguage } from '../contexts/LanguageContext';
import { translations } from '../constants';
import { useState, useEffect } from 'react';
import { Article } from '../types';
import { useBookmarks } from '../contexts/BookmarksContext';
import TableOfContents from '../components/TableOfContents';
export default function BlogPost() {
const { slug } = useParams();
const navigate = useNavigate();
const { lang, translate } = useLanguage();
const { toggleBookmark, isBookmarked } = useBookmarks();
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001
});
const [translatedArticle, setTranslatedArticle] = useState<Article | undefined>();
const [loading, setLoading] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const [showToast, setShowToast] = useState(false);
const originalArticle = articles.find(a => a.slug === slug);
useEffect(() => {
if (!originalArticle) return;
const translateFullArticle = async () => {
// If language is Portuguese, just use the original
if (lang === 'pt-br') {
setTranslatedArticle(originalArticle);
return;
}
setLoading(true);
try {
// Use Gemini to translate all parts in one shot
const translated = await translate({
title: originalArticle.title,
excerpt: originalArticle.excerpt,
content: originalArticle.content,
metaTitle: originalArticle.metaTitle,
metaDescription: originalArticle.metaDescription
}, 'full article content', true);
setTranslatedArticle({
...originalArticle,
...translated
});
} catch (error) {
console.error("Auto-translation error:", error);
setTranslatedArticle(originalArticle);
} finally {
setLoading(false);
}
};
translateFullArticle();
}, [lang, originalArticle, slug, translate]);
const copyToClipboard = () => {
navigator.clipboard.writeText(window.location.href);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
};
const article = translatedArticle || originalArticle;
if (!article) {
return (
<div className="min-h-screen flex items-center justify-center p-4">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Artigo não encontrado</h1>
<button onClick={() => navigate('/')} className="btn-primary">Voltar para a Home</button>
</div>
</div>
);
}
const relatedArticles = articles.filter(a => a.category === article.category && a.id !== article.id).slice(0, 2);
return (
<>
<SEO
title={article.metaTitle}
description={article.metaDescription}
ogType="article"
ogImage={article.image}
keywords={article.tags}
articleData={{
publishedTime: article.publishedAt,
author: article.author.name,
authorAvatar: article.author.avatar,
tags: article.tags,
section: article.category
}}
/>
<article className="pb-32 bg-white relative">
<motion.div
className="fixed top-20 left-0 right-0 h-1.5 bg-brand origin-left z-[60]"
style={{ scaleX }}
/>
<AnimatePresence>
{loading && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 bg-white/90 backdrop-blur-md flex items-center justify-center p-6 text-center"
>
<div className="max-w-md w-full p-12 bg-white rounded-3xl shadow-2xl border border-slate-100 italic">
<div className="relative mb-8 inline-block">
<div className="absolute inset-0 bg-brand/10 rounded-full animate-ping opacity-20" />
<div className="relative bg-brand/5 p-6 rounded-full">
<Loader2 className="w-12 h-12 text-brand animate-spin" />
</div>
<Sparkles className="absolute -top-1 -right-1 text-brand-light animate-pulse" />
</div>
<h3 className="text-2xl font-display font-bold text-slate-900 mb-3">
Interpretando Contexto...
</h3>
<p className="text-slate-500 font-medium leading-relaxed">
Utilizando IA para manter a precisão técnica da reportagem original.
</p>
<div className="mt-8 flex justify-center gap-1">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
animate={{ scale: [1, 1.5, 1], opacity: [0.3, 1, 0.3] }}
transition={{ duration: 1.5, repeat: Infinity, delay: i * 0.2 }}
className="w-2.5 h-2.5 bg-brand rounded-full"
/>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Hero Header */}
<header className="pt-24 pb-16 bg-slate-50 border-b border-slate-100">
<div className="mx-auto max-w-4xl px-4 text-center">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="inline-flex items-center gap-2 px-3 py-1 bg-brand/10 text-brand-dark rounded-full text-[10px] font-black uppercase tracking-widest mb-8 border border-brand/20"
>
<span>{article.category}</span>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="text-4xl sm:text-6xl lg:text-7xl font-display font-black text-slate-950 leading-[1.05] mb-12 tracking-tight"
>
{article.title}
</motion.h1>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="flex flex-wrap items-center justify-center gap-8 pt-10 border-t border-slate-200"
>
<div className="flex items-center gap-4">
<img src={article.author.avatar} alt={article.author.name} className="h-12 w-12 rounded-full border-2 border-white shadow-sm" />
<div className="text-left">
<p className="text-sm font-black text-slate-950">{article.author.name}</p>
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-400">{article.author.role}</p>
</div>
</div>
<div className="flex items-center gap-6 text-slate-400 font-bold uppercase tracking-widest text-[10px]">
<div className="flex items-center gap-2">
<Calendar size={14} className="text-brand" />
<span>{formatDate(article.publishedAt)}</span>
</div>
<div className="flex items-center gap-2">
<Clock size={14} className="text-brand" />
<span>{article.readTime} reading</span>
</div>
</div>
</motion.div>
</div>
</header>
{/* Featured Image */}
<div className="mx-auto max-w-7xl px-4 lg:px-8 -mt-12 mb-20">
<motion.div
initial={{ opacity: 0, scale: 1.05 }}
animate={{ opacity: 1, scale: 1 }}
className="aspect-[21/9] rounded-[3rem] overflow-hidden bg-slate-100 shadow-2xl shadow-slate-200 border border-slate-100"
>
<img
src={article.image}
alt={article.title}
className="h-full w-full object-cover"
referrerPolicy="no-referrer"
/>
</motion.div>
</div>
{/* Content Layout - Enhanced Editorial Spacing */}
<div className="mx-auto max-w-7xl px-4 lg:px-12 flex flex-col lg:flex-row gap-24 lg:gap-32">
{/* Main Content */}
<div className="lg:w-[65%]">
<div className="markdown-body">
<ReactMarkdown
components={{
h2: ({ children }) => {
const id = String(children).toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '');
return (
<h2 id={id} className="group relative scroll-mt-32 uppercase tracking-tight">
<span className="absolute -left-8 top-1 opacity-0 group-hover:opacity-100 text-brand transition-opacity">#</span>
{children}
</h2>
);
},
h3: ({ children }) => {
const id = String(children).toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '');
return <h3 id={id} className="scroll-mt-32">{children}</h3>;
}
}}
>
{article.content}
</ReactMarkdown>
</div>
{/* Tags */}
<div className="mt-16 flex flex-wrap gap-2 pt-8 border-t border-slate-100">
{article.tags.map(tag => (
<span key={tag} className="px-3 py-1 bg-slate-50 text-slate-500 rounded-lg text-[10px] font-black uppercase tracking-widest border border-slate-100 hover:border-brand/30 transition-colors">
#{tag}
</span>
))}
</div>
{/* Author Card */}
<div className="mt-16 p-10 bg-slate-50 rounded-[2.5rem] flex flex-col sm:flex-row items-center gap-8 border border-slate-100 shadow-sm">
<img src={article.author.avatar} alt={article.author.name} className="h-24 w-24 rounded-3xl object-cover shadow-lg" />
<div className="text-center sm:text-left">
<h4 className="text-2xl font-display font-bold text-slate-900 mb-2">Escrito por {article.author.name}</h4>
<p className="text-slate-500 font-medium leading-relaxed">{article.author.role}. Especialista em inteligência de busca e autor de múltiplos estudos sobre o algoritmo do Google.</p>
</div>
</div>
</div>
{/* Sidebar - Refined Editorial Aside */}
<aside className="lg:w-[35%] relative">
<div className="sticky top-32 space-y-20">
<TableOfContents content={article.content} />
{/* Engagement & Distribution */}
<div className="pt-12 border-t border-slate-100">
<div className="flex flex-col gap-8">
<div className="flex items-center justify-between">
<h5 className="font-black text-slate-400 uppercase text-[10px] tracking-[0.2em]">Interagir</h5>
<div className="flex gap-2">
<button className="h-10 w-10 rounded-full border border-slate-100 flex items-center justify-center text-slate-400 hover:text-brand hover:border-brand/20 transition-all">
<MessageCircle size={16} />
</button>
</div>
</div>
<div className="grid grid-cols-4 gap-3">
{[
{ Icon: Twitter, color: 'bg-slate-900', hover: 'hover:bg-slate-800' },
{ Icon: Linkedin, color: 'bg-[#0A66C2]', hover: 'hover:opacity-90' },
{ Icon: Facebook, color: 'bg-teal-600', hover: 'hover:opacity-90' },
{ Icon: Link2, color: 'bg-slate-100 text-slate-600', hover: 'hover:bg-brand hover:text-white', action: copyToClipboard }
].map((item, i) => (
<button
key={i}
onClick={item.action}
className={cn(
"h-12 flex items-center justify-center rounded-2xl transition-all shadow-sm",
item.color,
item.hover,
!item.action && "text-white"
)}
>
<item.Icon size={18} fill={!item.action ? "currentColor" : "none"} />
</button>
))}
</div>
<button
onClick={() => {
toggleBookmark(article.id);
setShowToast(true);
setTimeout(() => setShowToast(false), 3000);
}}
className={cn(
"w-full flex items-center justify-center gap-3 py-4 rounded-2xl border font-black text-[10px] uppercase tracking-[0.2em] transition-all",
isBookmarked(article.id)
? "bg-brand border-brand text-white shadow-xl shadow-brand/20"
: "border-slate-100 bg-white hover:bg-slate-50 text-slate-500 shadow-sm"
)}
>
<Bookmark size={14} fill={isBookmarked(article.id) ? "currentColor" : "none"} />
{isBookmarked(article.id) ? "Guardado" : "Salvar Insight"}
</button>
</div>
</div>
{/* Minimalist Related Feed */}
{relatedArticles.length > 0 && (
<div className="pt-12 border-t border-slate-100">
<h5 className="font-black text-slate-400 mb-10 uppercase text-[10px] tracking-[0.2em]">Contexto Adicional</h5>
<div className="space-y-12">
{relatedArticles.map(rel => (
<Link key={rel.id} to={`/artigo/${rel.slug}`} className="group flex flex-col gap-4">
<div className="overflow-hidden rounded-[2rem] aspect-[16/10] bg-slate-50 border border-slate-100 relative shadow-sm">
<img src={rel.image} className="w-full h-full object-cover grayscale group-hover:grayscale-0 group-hover:scale-105 transition-all duration-700" referrerPolicy="no-referrer" />
<div className="absolute top-4 left-4">
<span className="px-2 py-0.5 bg-white/90 backdrop-blur-sm rounded-full text-[8px] font-black uppercase tracking-widest text-slate-900 border border-slate-100">
{rel.category}
</span>
</div>
</div>
<h6 className="font-display font-bold text-slate-950 group-hover:text-brand transition-colors leading-tight text-xl">
{rel.title}
</h6>
</Link>
))}
</div>
</div>
)}
{/* Promo Card - High Contrast */}
<div className="p-12 bg-slate-950 rounded-[3rem] text-white relative overflow-hidden group shadow-2xl">
<div className="absolute top-0 right-0 w-48 h-48 bg-brand/10 blur-[100px] rounded-full" />
<div className="relative z-10">
<Sparkles className="text-brand mb-6" size={24} />
<h5 className="text-2xl font-display font-black mb-4 leading-tight">Masterclass SEO 2026</h5>
<p className="text-slate-400 mb-8 font-medium leading-relaxed text-sm">Domine os novos sinais de autoridade do algoritmo.</p>
<button className="w-full py-4 bg-brand text-white rounded-2xl font-black text-[10px] uppercase tracking-[0.2em] shadow-lg shadow-brand/20 hover:bg-brand-dark transition-all">
Acessar Agora
</button>
</div>
</div>
</div>
</aside>
</div>
</article>
{/* Toast Notification */}
<AnimatePresence>
{showToast && (
<motion.div
initial={{ opacity: 0, y: 50, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="fixed bottom-8 left-1/2 -translate-x-1/2 z-[100] bg-slate-950 text-white px-8 py-5 rounded-[2rem] shadow-2xl flex items-center gap-4 min-w-[320px] border border-white/10"
>
<div className="h-11 w-11 bg-brand rounded-full flex items-center justify-center shadow-lg shadow-brand/20">
<CheckCircle2 size={22} />
</div>
<div>
<p className="text-sm font-black tracking-tight">
{isBookmarked(article.id) ? "Adicionado à Biblioteca!" : "Removido!"}
</p>
<Link to="/leituras-salvas" className="text-[10px] text-brand-light font-bold uppercase tracking-widest hover:text-white transition-colors">
Ver Minha Coleção
</Link>
</div>
</motion.div>
)}
</AnimatePresence>
<section className="bg-slate-50 py-32 border-t border-slate-100">
<div className="mx-auto max-w-4xl px-4 text-center">
<div className="w-20 h-20 bg-white rounded-3xl shadow-xl shadow-slate-200 flex items-center justify-center mx-auto mb-10 border border-slate-100">
<MessageCircle size={32} className="text-brand" />
</div>
<h2 className="text-4xl sm:text-5xl font-display font-black text-slate-950 mb-6">Continue evoluindo.</h2>
<p className="text-lg text-slate-500 mb-12 font-medium max-w-2xl mx-auto">Nossas publicações são desenhadas para transformar iniciantes em autoridades no ecossistema de busca.</p>
<div className="flex flex-wrap justify-center gap-4">
<Link to="/" className="btn-primary">Explorar mais artigos</Link>
<button className="btn-outline">Compartilhar Feedback</button>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,97 @@
import { motion } from 'motion/react';
import { Bookmark as BookmarkIcon, ArrowRight, Trash2 } from 'lucide-react';
import SEO from '../components/SEO';
import { useBookmarks } from '../contexts/BookmarksContext';
import { articles } from '../data/articles';
import { Link } from 'react-router-dom';
import CategoryBadge from '../components/CategoryBadge';
export default function Bookmarks() {
const { bookmarks, toggleBookmark } = useBookmarks();
const savedArticles = articles.filter(a => bookmarks.includes(a.id));
return (
<div className="pt-32 pb-24">
<SEO
title="Leituras Salvas | SEO Authority"
description="Sua lista personalizada de artigos para ler depois."
/>
<div className="max-w-7xl mx-auto px-6">
<header className="mb-20">
<div className="flex items-center gap-4 mb-8">
<span className="w-12 h-[1px] bg-brand" />
<span className="text-[10px] font-black uppercase tracking-[0.3em] text-brand">Biblioteca Pessoal</span>
</div>
<h1 className="text-6xl sm:text-8xl font-display font-black text-slate-950 tracking-[-0.04em] leading-[0.9] mb-8">
Para ler<br /><span className="text-brand">depois.</span>
</h1>
<p className="mt-8 text-xl text-slate-500 max-w-2xl font-medium leading-relaxed border-l-2 border-slate-100 pl-6">
Consulte aqui seus insights salvos para aplicar em sua estratégia quando estiver pronto.
</p>
</header>
{savedArticles.length > 0 ? (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-12">
{savedArticles.map((article, i) => (
<motion.div
key={article.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.1 }}
className="group bg-white rounded-[3rem] border border-slate-100 p-10 flex flex-col hover:shadow-2xl transition-all h-full"
>
<div className="flex justify-between items-start mb-10">
<CategoryBadge category={article.category} variant="outline" />
<button
onClick={() => toggleBookmark(article.id)}
className="p-2 text-slate-300 hover:text-red-500 transition-colors"
title="Remover"
>
<Trash2 size={18} />
</button>
</div>
<Link to={`/artigo/${article.slug}`} className="flex-grow">
<h3 className="text-2xl font-display font-black text-slate-950 group-hover:text-brand transition-colors mb-6 leading-tight">
{article.title}
</h3>
<p className="text-slate-500 text-sm leading-relaxed mb-10 line-clamp-3 font-medium">
{article.excerpt}
</p>
</Link>
<Link
to={`/artigo/${article.slug}`}
className="inline-flex items-center gap-3 text-slate-950 font-black uppercase text-[10px] tracking-widest group/link"
>
Continuar Leitura
<div className="h-8 w-8 rounded-full bg-slate-50 flex items-center justify-center text-slate-400 group-hover/link:bg-brand group-hover/link:text-white transition-all">
<ArrowRight size={14} />
</div>
</Link>
</motion.div>
))}
</div>
) : (
<div className="bg-slate-50 rounded-[3rem] p-20 text-center border border-slate-100">
<div className="h-20 w-20 bg-white shadow-xl rounded-2xl flex items-center justify-center text-slate-200 mx-auto mb-8">
<BookmarkIcon size={32} />
</div>
<h2 className="text-3xl font-display font-black text-slate-950 mb-4">Sua lista está vazia</h2>
<p className="text-slate-500 mb-10 max-w-md mx-auto font-medium">
Navegue pelo nosso editorial e salve os artigos que mais te interessam para consultá-los aqui.
</p>
<Link
to="/"
className="btn-primary inline-flex items-center gap-3"
>
Explorar Artigos
</Link>
</div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,83 @@
import { useParams, Link } from 'react-router-dom';
import { articles } from '../data/articles';
import ArticleCard from '../components/ArticleCard';
import SEO from '../components/SEO';
import { ChevronRight } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
import { CATEGORY_MAP } from '../constants';
export default function CategoryPage() {
const { categorySlug } = useParams();
const { lang } = useLanguage();
const getCategoryData = (slug: string) => {
const entry = Object.entries(CATEGORY_MAP).find(([_, value]) => value.slug === slug);
if (entry) {
const [originalName, data] = entry;
return {
name: data.name,
originalName: originalName,
description: `Deep dives, case studies and tactical analysis on ${data.name.toLowerCase()}.`
};
}
return {
name: slug.charAt(0).toUpperCase() + slug.slice(1).replace(/-/g, ' '),
originalName: slug,
description: 'Artigos e análises sobre o ecossistema digital.'
};
};
const { name: categoryName, originalName, description: categoryDescription } = getCategoryData(categorySlug || '');
const filteredArticles = articles.filter(a => {
// If we have a map entry, match by original category name
if (originalName) {
return a.category === originalName;
}
// Fallback strategy
const slugfiedCategory = a.category.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\s+/g, '-');
return slugfiedCategory === categorySlug;
});
return (
<>
<SEO
title={`Artigos sobre ${categoryName}`}
description={`Confira guias e dicas exclusivas sobre ${categoryName} em nosso blog de autoridade SEO.`}
/>
<div className="bg-white py-24 border-b border-slate-100">
<div className="mx-auto max-w-7xl px-6">
<div className="flex items-center gap-4 text-brand font-black uppercase tracking-[0.3em] text-[10px] mb-8">
<div className="h-[1px] w-12 bg-brand" />
<span>Arquivo Editorial</span>
</div>
<h1 className="text-6xl sm:text-8xl font-display font-black text-slate-950 tracking-[-0.04em] leading-[0.9]">{categoryName}</h1>
<p className="text-xl text-slate-500 mt-8 max-w-2xl font-medium leading-relaxed border-l-2 border-slate-100 pl-6">{categoryDescription}</p>
</div>
</div>
<div className="py-24 bg-white">
<div className="mx-auto max-w-7xl px-6">
{filteredArticles.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-12 gap-y-20">
{filteredArticles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
</div>
) : (
<div className="text-center py-32 bg-slate-50 rounded-[3rem] border border-slate-100">
<h2 className="text-xl font-display font-black text-slate-300 uppercase tracking-widest mb-6">Nenhum insight encontrado</h2>
<Link to="/" className="btn-primary inline-flex items-center gap-3">
Voltar para a Home
</Link>
</div>
)}
</div>
</div>
</>
);
}

View file

@ -0,0 +1,143 @@
import { motion } from 'motion/react';
import { Mail, MessageCircle, Send } from 'lucide-react';
import { useState } from 'react';
import SEO from '../components/SEO';
import Layout from '../components/Layout';
export default function Contact() {
const [submitted, setSubmitted] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
};
return (
<>
<SEO
title="Contato | Blog SEO de Autoridade"
description="Entre em contato conosco para parcerias, consultorias ou dúvidas sobre SEO e marketing digital."
/>
<div className="pt-32 pb-24">
<div className="max-w-7xl mx-auto px-6">
<div className="grid lg:grid-cols-2 gap-20">
{/* Coluna da Esquerda: Info */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="space-y-12"
>
<div>
<h1 className="text-6xl font-display font-black text-slate-950 mb-8 leading-[0.9] tracking-tight">
Vamos falar <br />de <span className="text-brand">crescimento.</span>
</h1>
<p className="text-xl text-slate-500 max-w-md font-medium leading-relaxed">
Tem um projeto em mente ou quer escalar sua autoridade orgânica? Nossa equipe de estrategistas está pronta para ouvir.
</p>
</div>
<div className="space-y-8">
<div className="flex gap-6 group">
<div className="h-14 w-14 rounded-2xl bg-brand/10 flex items-center justify-center text-brand group-hover:bg-brand group-hover:text-white transition-all">
<Mail size={24} />
</div>
<div>
<h4 className="font-black text-slate-950 uppercase tracking-widest text-xs mb-1">E-mail</h4>
<p className="text-slate-500 font-medium">contato@seohub.ai</p>
</div>
</div>
<a
href="https://wa.me/5511999999999"
target="_blank"
rel="noopener noreferrer"
className="flex gap-6 group"
>
<div className="h-14 w-14 rounded-2xl bg-brand/10 flex items-center justify-center text-brand group-hover:bg-brand group-hover:text-white transition-all">
<MessageCircle size={24} />
</div>
<div>
<h4 className="font-black text-slate-950 uppercase tracking-widest text-xs mb-1">WhatsApp</h4>
<p className="text-slate-500 font-medium group-hover:text-brand transition-colors">+55 (11) 99999-9999</p>
</div>
</a>
</div>
</motion.div>
{/* Coluna da Direita: Form */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white p-12 rounded-[3rem] border border-slate-100 shadow-2xl transition-all"
>
{submitted ? (
<div className="h-full flex flex-col items-center justify-center text-center space-y-6 py-12">
<div className="h-20 w-20 bg-brand/10 text-brand rounded-full flex items-center justify-center">
<Send size={32} />
</div>
<h2 className="text-3xl font-display font-black text-slate-950">Mensagem enviada!</h2>
<p className="text-slate-500 font-medium">Obrigado pelo contato. Responderemos em até 24 horas.</p>
<button
onClick={() => setSubmitted(false)}
className="text-brand font-black uppercase text-[10px] tracking-widest hover:underline"
>
Enviar outra mensagem
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-8">
<div className="grid md:grid-cols-2 gap-8">
<div className="space-y-3">
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 ml-1">Nome Completo</label>
<input
required
type="text"
placeholder="Ex: João Silva"
className="w-full px-6 py-4 rounded-2xl bg-slate-50 border border-transparent focus:bg-white focus:border-brand focus:ring-4 focus:ring-brand/10 transition-all font-medium text-slate-950"
/>
</div>
<div className="space-y-3">
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 ml-1">E-mail Corporativo</label>
<input
required
type="email"
placeholder="seu@empresa.com"
className="w-full px-6 py-4 rounded-2xl bg-slate-50 border border-transparent focus:bg-white focus:border-brand focus:ring-4 focus:ring-brand/10 transition-all font-medium text-slate-950"
/>
</div>
</div>
<div className="space-y-3">
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 ml-1">Assunto</label>
<select className="w-full px-6 py-4 rounded-2xl bg-slate-50 border border-transparent focus:bg-white focus:border-brand focus:ring-4 focus:ring-brand/10 transition-all font-medium text-slate-950 appearance-none">
<option>Consultoria SEO</option>
<option>Parceria de Conteúdo</option>
<option>Reportar Erro Técnico</option>
<option>Outros</option>
</select>
</div>
<div className="space-y-3">
<label className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 ml-1">Sua Mensagem</label>
<textarea
required
placeholder="Como podemos ajudar você hoje?"
rows={5}
className="w-full px-6 py-4 rounded-2xl bg-slate-50 border border-transparent focus:bg-white focus:border-brand focus:ring-4 focus:ring-brand/10 transition-all font-medium text-slate-950 resize-none"
></textarea>
</div>
<button className="w-full bg-slate-950 py-6 rounded-2xl text-white font-black uppercase text-[10px] tracking-[0.3em] hover:bg-brand transition-all flex items-center justify-center gap-3 group shadow-xl shadow-slate-900/10">
Enviar Proposta
<Send size={16} className="group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform" />
</button>
</form>
)}
</motion.div>
</div>
</div>
</div>
</>
);
}

View file

@ -0,0 +1,291 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { articles } from '../data/articles';
import ArticleCard from '../components/ArticleCard';
import SEO from '../components/SEO';
import { ArrowRight, TrendingUp, Zap } from 'lucide-react';
import { motion } from 'motion/react';
import { useLanguage } from '../contexts/LanguageContext';
import { translations } from '../constants';
import { Article } from '../types';
export default function Home() {
const { lang, translate } = useLanguage();
const t = translations[lang];
const [displayArticles, setDisplayArticles] = useState<Article[]>(articles);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (lang === 'pt-br') {
setDisplayArticles(articles);
return;
}
const translateHeaders = async () => {
setLoading(true);
try {
// Translate only titles and categories for Home page items
const itemsToTranslate = articles.map(a => ({
title: a.title,
category: a.category
}));
const translatedItems = await translate(itemsToTranslate, 'article titles and categories', true);
const newArticles = articles.map((a, i) => ({
...a,
title: translatedItems[i]?.title || a.title,
category: translatedItems[i]?.category || a.category
}));
setDisplayArticles(newArticles);
} catch (error) {
console.error("Home translation error:", error);
} finally {
setLoading(false);
}
};
translateHeaders();
}, [lang, translate]);
const featuredArticle = displayArticles[0];
const recentArticles = displayArticles.slice(1, 4);
const secondaryArticles = displayArticles.slice(4, 10);
return (
<>
<SEO
title={t.heroTitle}
description={t.heroDescription}
/>
{/* Modern Hero Section */}
<section className="relative pt-32 pb-24 overflow-hidden">
{/* Background elements */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full max-w-7xl h-full -z-10 bg-[radial-gradient(circle_at_top_right,rgba(20,184,166,0.08),transparent_50%),radial-gradient(circle_at_bottom_left,rgba(13,148,136,0.05),transparent_50%)]" />
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex flex-col items-center text-center max-w-4xl mx-auto">
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="inline-flex items-center gap-2 px-3 py-1 bg-brand/10 text-brand rounded-full text-xs font-bold uppercase tracking-wider mb-8 border border-brand/20"
>
<TrendingUp size={14} />
<span>{t.heroSubtitle}</span>
</motion.div>
<motion.h1
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ type: "spring", damping: 20 }}
className="text-6xl sm:text-8xl lg:text-[10rem] font-display font-black text-slate-950 mb-10 leading-[0.9] tracking-[-0.04em]"
>
{lang === 'pt-br' ? (
<>Poder<br /><span className="text-brand">Semântico.</span></>
) : <>Semantic<br /><span className="text-brand">Authority.</span></>}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="text-lg sm:text-xl text-slate-600 max-w-2xl leading-relaxed mb-12 font-medium"
>
{t.heroDescription}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="flex flex-col sm:flex-row items-center gap-6"
>
<Link
to="/sobre"
className="btn-primary flex items-center gap-2 group"
>
{t.readManifesto}
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</Link>
<div className="flex items-center gap-2 text-slate-400 font-medium">
<span className="flex h-2 w-2 rounded-full bg-brand animate-pulse" />
<span className="text-sm uppercase tracking-widest">{t.publishedIn} Maio 2026</span>
</div>
</motion.div>
</div>
</div>
</section>
{/* Editorial Spotlight Section */}
<section className="py-24 bg-white">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex flex-col gap-32">
{/* Spotlight Feature - Refined Magazine Layout */}
<div className="relative">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-center">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
className="lg:col-span-7"
>
<div className="relative aspect-[4/3] sm:aspect-[16/10] overflow-hidden rounded-[2rem] shadow-2xl">
<img
src={featuredArticle.image}
alt={featuredArticle.title}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/20 to-transparent" />
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="lg:col-span-5"
>
<div className="flex items-center gap-4 mb-8">
<span className="w-12 h-[1px] bg-brand" />
<span className="text-[10px] font-black uppercase tracking-[0.3em] text-brand">{featuredArticle.category}</span>
</div>
<Link to={`/artigo/${featuredArticle.slug}`}>
<h2 className="text-4xl sm:text-5xl lg:text-6xl font-display font-black text-slate-950 mb-8 leading-[1.05] tracking-tight hover:text-brand transition-colors">
{featuredArticle.title}
</h2>
</Link>
<p className="text-lg text-slate-500 font-medium leading-relaxed mb-10 border-l-2 border-slate-100 pl-6">
{featuredArticle.excerpt}
</p>
<div className="flex items-center gap-6 mb-12">
<div className="flex items-center gap-3">
<img src={featuredArticle.author.avatar} alt={featuredArticle.author.name} className="h-10 w-10 rounded-full border border-slate-100" />
<div className="text-left">
<p className="text-xs font-black text-slate-950">{featuredArticle.author.name}</p>
<p className="text-[9px] font-bold uppercase tracking-widest text-slate-400">{featuredArticle.author.role}</p>
</div>
</div>
</div>
<Link to={`/artigo/${featuredArticle.slug}`} className="btn-primary inline-flex items-center gap-3 group">
Iniciar Leitura
<ArrowRight size={18} className="group-hover:translate-x-1 transition-transform" />
</Link>
</motion.div>
</div>
</div>
{/* Newsletter Mid-Bar - Minimalist & Integrated */}
<div className="bg-slate-50 rounded-[3rem] p-4 sm:p-6 border border-slate-100">
<div className="flex flex-col lg:flex-row items-center gap-8">
<div className="flex-grow flex items-center gap-6 pl-6">
<div className="h-12 w-12 bg-white rounded-2xl flex items-center justify-center text-brand shadow-sm">
<Zap size={22} fill="currentColor" />
</div>
<div>
<h3 className="text-sm font-black text-slate-950 uppercase tracking-widest">Briefing Semanal</h3>
<p className="text-xs text-slate-500 font-medium">Insights técnicos direto na sua inbox.</p>
</div>
</div>
<form className="flex flex-col sm:flex-row gap-2 w-full lg:w-auto lg:pr-2" onSubmit={(e) => e.preventDefault()}>
<input
type="email"
placeholder="E-mail profissional..."
className="bg-white border border-slate-200 rounded-2xl px-6 py-3.5 text-sm focus:outline-none focus:ring-2 focus:ring-brand/20 w-full lg:w-72"
/>
<button className="bg-slate-950 text-white font-black text-[10px] uppercase tracking-widest px-8 py-3.5 rounded-2xl hover:bg-slate-800 transition-all">
Inscrever
</button>
</form>
</div>
</div>
{/* Content Explorer Section */}
<div>
<div className="flex items-center justify-between mb-16 px-4">
<h3 className="text-[10px] font-black text-slate-300 uppercase tracking-[0.3em] flex items-center gap-3">
<div className="h-1 w-8 bg-slate-100" />
Explorar Insights
</h3>
<Link to="/blog" className="text-[10px] font-black text-brand uppercase tracking-widest hover:underline">Ver Todos</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-12">
{recentArticles.map((article) => (
<ArticleCard key={article.id} article={article} />
))}
</div>
</div>
</div>
</div>
</section>
{/* Grid Explorer */}
<section className="py-24 bg-slate-50 border-t border-slate-100">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mb-16 text-center max-w-2xl mx-auto">
<h2 className="text-4xl font-display font-black text-slate-950 mb-4">{t.explore}</h2>
<p className="text-slate-500 font-medium leading-relaxed">Mergulhe em clusters de conhecimento organizados por intenção de busca.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{secondaryArticles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
</div>
<div className="mt-20 text-center">
<Link to="/arquivo" className="btn-outline inline-flex items-center gap-3">
Explore o Arquivo Semântico
<ArrowRight size={18} />
</Link>
</div>
</div>
</section>
{/* Modern Newsletter Banner */}
<section id="newsletter" className="py-24 bg-slate-50">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="bg-brand rounded-[3rem] p-12 sm:p-20 text-center text-white relative overflow-hidden shadow-2xl shadow-brand/20">
{/* Abstract Background Shapes */}
<div className="absolute top-0 left-0 w-64 h-64 bg-white/10 blur-3xl -translate-x-1/2 -translate-y-1/2 rounded-full" />
<div className="absolute bottom-0 right-0 w-96 h-96 bg-brand-light/20 blur-3xl translate-x-1/4 translate-y-1/4 rounded-full" />
<div className="relative z-10 max-w-2xl mx-auto">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
className="inline-flex items-center gap-2 px-3 py-1 bg-white/20 text-white rounded-full text-[10px] font-bold uppercase tracking-widest mb-8"
>
<span>Exclusivo para Membros</span>
</motion.div>
<h2 className="text-4xl sm:text-5xl lg:text-6xl font-display font-extrabold mb-8 leading-tight">{t.footerNewsletterTitle}</h2>
<p className="text-teal-50 mb-12 font-medium">
{t.footerNewsletterSubtitle}
</p>
<form className="flex flex-col sm:flex-row gap-4 max-w-lg mx-auto" onSubmit={(e) => e.preventDefault()}>
<input
type="email"
placeholder={t.footerNewsletterInput}
className="flex-grow px-6 py-4 rounded-2xl bg-white text-slate-900 focus:outline-none focus:ring-4 focus:ring-white/20 transition-all font-medium"
/>
<button className="px-10 py-4 bg-slate-950 text-white rounded-2xl font-bold hover:bg-slate-900 transition-all active:scale-95 shadow-xl">
{t.subscribe}
</button>
</form>
<p className="mt-8 text-[10px] text-teal-100 uppercase tracking-widest font-bold opacity-80">{t.footerNewsletterDisclaimer}</p>
</div>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,80 @@
import { motion } from 'motion/react';
import { Link } from 'react-router-dom';
import SEO from '../components/SEO';
import { BarChart3, Database, Search, FileCheck } from 'lucide-react';
export default function Methodology() {
const steps = [
{
icon: Database,
title: "Coleta de Dados",
desc: "Analisamos mais de 1 milhão de SERPs mensais para identificar padrões de mudança nos algoritmos do Google Anthropic e OpenAI."
},
{
icon: Search,
title: "Análise Semântica",
desc: "Nossos modelos identificam quais entidades e contextos estão gerando os maiores ganhos de visibilidade orgânica."
},
{
icon: BarChart3,
title: "Teste de Campo",
desc: "Implementamos teorias em sites controlados antes de documentá-las como estratégias recomendadas."
},
{
icon: FileCheck,
title: "Validação Editorial",
desc: "Todo insight é revisado por estrategistas com mais de 10 anos de experiência no mercado de busca."
}
];
return (
<>
<SEO
title="Metodologia de Análise | Como avaliamos o Google"
description="Saiba como o SEO Authority produz seus reports e análises técnicas. Dados reais, testes de campo e transparência."
/>
<section className="pt-24 pb-32 bg-[#FCFCFC]">
<div className="mx-auto max-w-7xl px-4 lg:px-8">
<div className="max-w-4xl mb-24">
<span className="text-brand font-black uppercase tracking-[0.3em] text-[10px] mb-6 block">Padrão de Qualidade</span>
<h1 className="text-6xl sm:text-8xl font-display font-black text-slate-950 mb-8 leading-[0.9] tracking-[-0.04em]">
Nossa<br />Metodologia.
</h1>
<p className="text-xl text-slate-500 leading-relaxed font-medium border-l-2 border-slate-100 pl-6">
O SEO deixou de ser uma ciência exata para se tornar uma ciência de dados e comportamento humano. Veja como deciframos o código.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-px bg-slate-100 border border-slate-100 rounded-[3rem] overflow-hidden">
{steps.map((step, i) => (
<div key={i} className="bg-white p-12 sm:p-16 flex flex-col gap-8 hover:bg-slate-50/50 transition-colors">
<div className="h-14 w-14 rounded-2xl bg-slate-950 text-white flex items-center justify-center shadow-lg shadow-slate-950/10">
<step.icon size={28} />
</div>
<div>
<span className="text-slate-100 font-display font-black text-6xl block mb-6 tracking-tighter">0{i+1}</span>
<h3 className="text-2xl font-display font-black text-slate-950 mb-4 tracking-tight uppercase tracking-widest text-sm">{step.title}</h3>
<p className="text-slate-500 font-medium leading-relaxed">{step.desc}</p>
</div>
</div>
))}
</div>
<div className="mt-24 p-12 bg-slate-50 rounded-[3rem] border border-slate-100 text-center max-w-3xl mx-auto">
<h4 className="text-2xl font-display font-black text-slate-950 mb-6">Acesso aos Dados Brutos</h4>
<p className="text-slate-500 font-medium mb-10">
Parceiros e assinantes da modalidade <span className="text-slate-950 font-black uppercase tracking-widest text-[10px]">Authority+</span> têm acesso aos nossos datasets proprietários de flutuação de SERP.
</p>
<Link
to="/contato"
className="btn-primary"
>
Conhecer Assinatura Premium
</Link>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,59 @@
import { motion } from 'motion/react';
import { Link } from 'react-router-dom';
import { Home, ArrowLeft, Search } from 'lucide-react';
import SEO from '../components/SEO';
import Layout from '../components/Layout';
export default function NotFound() {
return (
<div className="min-h-[80vh] flex items-center justify-center pt-20">
<SEO
title="404 - Página Não Encontrada"
description="A página que você está procurando não existe ou foi movida."
/>
<div className="max-w-xl w-full px-6 text-center">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="mb-8"
>
<div className="inline-flex h-24 w-24 items-center justify-center rounded-3xl bg-brand/10 text-brand mb-8 rotate-12">
<Search size={48} />
</div>
<h1 className="text-8xl sm:text-9xl font-display font-black text-slate-950 mb-8 tracking-[-0.04em] leading-[0.9]">404</h1>
<h2 className="text-2xl font-display font-black text-slate-950 uppercase tracking-widest mb-6">Página Não Encontrada</h2>
<p className="text-slate-500 text-lg leading-relaxed mb-10 font-medium max-w-md mx-auto border-l-2 border-slate-100 pl-6 text-left">
O conteúdo que você busca parece ter sido reindexado ou movido para uma nova dimensão semântica. Vamos te levar de volta para um lugar seguro.
</p>
</motion.div>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
to="/"
className="btn-primary w-full sm:w-auto inline-flex items-center gap-3 group"
>
<Home size={18} />
Voltar para Home
</Link>
<button
onClick={() => window.history.back()}
className="btn-outline w-full sm:w-auto inline-flex items-center gap-3 group"
>
<ArrowLeft size={18} />
Página Anterior
</button>
</div>
<div className="mt-20 pt-10 border-t border-slate-100">
<p className="text-[10px] font-bold text-slate-300 uppercase tracking-[0.3em] mb-8">Navegue pelas categorias</p>
<div className="flex flex-wrap justify-center gap-2">
{['Estratégia', 'IA', 'Link Building', 'Técnico'].map((cat) => (
<span key={cat} className="px-4 py-2 bg-slate-50 text-slate-500 rounded-lg text-xs font-bold uppercase tracking-widest">{cat}</span>
))}
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,39 @@
import { motion } from 'motion/react';
import SEO from '../components/SEO';
export default function Privacy() {
return (
<>
<SEO
title="Política de Privacidade | SEO Authority"
description="Saiba como tratamos seus dados e nosso compromisso com a transparência editorial."
/>
<section className="pt-24 pb-32 bg-white">
<div className="mx-auto max-w-4xl px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-16"
>
<h1 className="text-5xl font-serif font-bold text-slate-950 mb-6 tracking-tighter">Política de Privacidade</h1>
<p className="text-slate-500 uppercase tracking-widest text-[10px] font-bold">Última atualização: Maio de 2026</p>
</motion.div>
<div className="markdown-body">
<p>Sua privacidade é importante para nós. É política do SEO Authority respeitar a sua privacidade em relação a qualquer informação sua que possamos coletar no site.</p>
<h2>1. Coleta de Informações</h2>
<p>Solicitamos informações pessoais apenas quando realmente precisamos delas para lhe fornecer um serviço, como a nossa newsletter. Fazemo-lo por meios justos e legais, com o seu conhecimento e consentimento.</p>
<h2>2. Uso de Dados</h2>
<p>Não compartilhamos informações de identificação pessoal publicamente ou com terceiros, exceto quando exigido por lei. O uso de cookies é limitado à análise de tráfego (Google Analytics) para melhorar sua experiência editorial.</p>
<h2>3. Retenção de Dados</h2>
<p>Apenas retemos as informações coletadas pelo tempo necessário para fornecer o serviço solicitado. Quando armazenamos dados, protegemos dentro de meios comercialmente aceitáveis para evitar perdas e roubos.</p>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,37 @@
import { motion } from 'motion/react';
import SEO from '../components/SEO';
export default function Terms() {
return (
<>
<SEO
title="Termos de Serviço | SEO Authority"
description="Leia os termos de uso da nossa plataforma e publicações digitais."
/>
<section className="pt-24 pb-32 bg-white">
<div className="mx-auto max-w-4xl px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-16"
>
<h1 className="text-5xl font-serif font-bold text-slate-950 mb-6 tracking-tighter">Termos de Serviço</h1>
<p className="text-slate-500 uppercase tracking-widest text-[10px] font-bold">Última atualização: Maio de 2026</p>
</motion.div>
<div className="markdown-body">
<h2>1. Termos</h2>
<p>Ao acessar o site SEO Authority, você concorda em cumprir estes termos de serviço, todas as leis e regulamentos aplicáveis e concorda que é responsável pelo cumprimento de todas as leis locais aplicáveis.</p>
<h2>2. Licença de Uso</h2>
<p>O conteúdo publicado é protegido por direitos autorais. É permitida a citação de trechos mediante link de atribuição para a fonte original. O uso comercial dos dados sem autorização prévia é proibido.</p>
<h2>3. Isenção de Responsabilidade</h2>
<p>Os materiais no site são fornecidos 'como estão'. O SEO Authority não oferece garantias, expressas ou implícitas, e por este meio isenta e nega todas as outras garantias, incluindo, sem limitação, garantias implícitas ou condições de comercialização.</p>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,57 @@
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: (process.env.GEMINI_API_KEY as string) });
export async function translateText(text: string, targetLang: string) {
if (!text) return text;
try {
const response = await ai.models.generateContent({
model: "gemini-3-flash-preview",
contents: `Translate the following text to ${targetLang}. Keep the markdown formatting and don't add any extra commentary. Just the translation: \n\n${text}`,
});
return response.text || text;
} catch (error) {
console.error("Translation error:", error);
return text;
}
}
export async function translateArticle(article: any, targetLang: string) {
try {
const prompt = `Translate the following article object to ${targetLang}.
Return a JSON object with the same structure.
Only translate the 'title', 'excerpt', 'content', 'metaTitle', and 'metaDescription' fields.
Keep the 'id', 'slug', 'category', 'publishedAt', 'image', 'readTime', 'author', 'tags', and 'lang' (set lang to ${targetLang}) fields exactly as they are.
Article to translate:
${JSON.stringify({
title: article.title,
excerpt: article.excerpt,
content: article.content,
metaTitle: article.metaTitle,
metaDescription: article.metaDescription
})}
`;
const response = await ai.models.generateContent({
model: "gemini-3-flash-preview",
contents: prompt,
config: {
responseMimeType: "application/json",
}
});
const translatedParts = JSON.parse(response.text || "{}");
return {
...article,
...translatedParts,
lang: targetLang
};
} catch (error) {
console.error("Article translation error:", error);
return article;
}
}

View file

@ -0,0 +1,54 @@
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY || '' });
export async function generateSEOArticle(topic: string) {
try {
const response = await ai.models.generateContent({
model: "gemini-3-flash-preview",
contents: `Crie um artigo de blog profissional e altamente otimizado para SEO sobre o seguinte tópico: ${topic}.
O artigo deve estar em Português do Brasil.
Retorne APENAS um JSON no seguinte formato:
{
"title": "Título otimizado com palavra-chave",
"excerpt": "Um resumo atraente do artigo",
"content": "Conteúdo completo em Markdown, com H2, H3, bullets e parágrafos curtos",
"category": "SEO para Iniciantes",
"metaTitle": "Meta title SEO",
"metaDescription": "Meta description persuasiva",
"tags": ["tag1", "tag2"],
"readTime": "X min"
}
Certifique-se de que o conteúdo seja profundo, profissional e focado em autoridade.`,
config: {
responseMimeType: "application/json"
}
});
return JSON.parse(response.text || '{}');
} catch (error) {
console.error("Erro ao gerar artigo:", error);
throw error;
}
}
export async function summarizeSearchResults(query: string, results: any[]) {
try {
const titles = results.map(r => r.title).join(', ');
const response = await ai.models.generateContent({
model: "gemini-1.5-flash",
contents: `O usuário pesquisou por "${query}" em um blog de autoridade em SEO.
Encontramos os seguintes artigos relacionados: ${titles}.
Crie um resumo muito curto (máximo 200 caracteres) explicando por que esses tópicos são importantes para a estratégia de SEO do usuário.
Seja direto, profissional e encorajador. Use Português do Brasil.`,
});
return response.text;
} catch (error) {
console.error("Erro ao resumir busca:", error);
return null;
}
}

77
Template-02/src/types.ts Normal file
View file

@ -0,0 +1,77 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
export type Category =
| 'Estratégia'
| 'Técnico'
| 'Autoridade'
| 'Negócios'
| 'SEO para Iniciantes'
| 'SEO Técnico'
| 'SEO On Page'
| 'SEO Off Page'
| 'Link Building'
| 'Ferramentas de SEO'
| 'SEO para E-commerce'
| 'SEO para Blogs'
| 'Atualizações do Google';
export interface Author {
name: string;
role: string;
avatar: string;
}
export interface Article {
id: string;
slug: string;
title: string;
excerpt: string;
content: string;
category: Category;
author: Author;
publishedAt: string;
image: string;
readTime: string;
metaTitle: string;
metaDescription: string;
tags: string[];
lang: 'pt-br' | 'en' | 'es';
}
export interface Translations {
newsletter: string;
subscribe: string;
readMore: string;
latest: string;
allArticles: string;
sections: string;
publishing: string;
contact: string;
about: string;
privacy: string;
terms: string;
featured: string;
recent: string;
explore: string;
search: string;
viewAll: string;
manifesto: string;
methodology: string;
heroTitle: string;
heroSubtitle: string;
heroDescription: string;
readManifesto: string;
publishedIn: string;
newsletterTitle: string;
newsletterDescription: string;
emailPlaceholder: string;
subscribeButton: string;
trendsTitle: string;
footerNewsletterTitle: string;
footerNewsletterSubtitle: string;
footerNewsletterInput: string;
footerNewsletterDisclaimer: string;
}

26
Template-02/tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

View file

@ -0,0 +1,24 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import {defineConfig, loadEnv} from 'vite';
export default defineConfig(({mode}) => {
const env = loadEnv(mode, '.', '');
return {
plugins: [react(), tailwindcss()],
define: {
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
server: {
// HMR is disabled in AI Studio via DISABLE_HMR env var.
// Do not modify—file watching is disabled to prevent flickering during agent edits.
hmr: process.env.DISABLE_HMR !== 'true',
},
};
});

BIN
Template-03/.DS_Store vendored Normal file

Binary file not shown.

20
Template-03/README.md Normal file
View file

@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/b9e0fe6d-c7c5-42ea-8fbe-61d65dc0914c
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

32
Template-03/index.html Normal file
View file

@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SEO Authority | Insights Técnicos e Estratégicos</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%232563eb'%3E%3Cpath d='M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5'/%3E%3C/svg%3E" />
<meta name="theme-color" content="#ffffff" />
<style>
/* Hide Google Translate Bar */
.goog-te-banner-frame.skiptranslate, .goog-te-gadget-icon { display: none !important; }
body { top: 0px !important; }
.goog-te-menu-value { display: none !important; }
#google_translate_element { display: none; }
.goog-tooltip { display: none !important; }
.goog-tooltip:hover { display: none !important; }
.goog-text-highlight { background-color: transparent !important; border: none !important; box-shadow: none !important; }
</style>
</head>
<body>
<div id="google_translate_element"></div>
<div id="root"></div>
<script type="text/javascript">
function googleTranslateElementInit() {
new google.translate.TranslateElement({pageLanguage: 'pt', includedLanguages: 'en,es,pt', layout: google.translate.TranslateElement.InlineLayout.SIMPLE, autoDisplay: false}, 'google_translate_element');
}
</script>
<script type="text/javascript" src="//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,6 @@
{
"name": "NODE_SEARCH - Neural Intelligence",
"description": "Plataforma de inteligência neural em busca orgânica com foco em autoridade técnica e semântica.",
"requestFramePermissions": [],
"majorCapabilities": []
}

5670
Template-03/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

41
Template-03/package.json Normal file
View file

@ -0,0 +1,41 @@
{
"name": "react-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "tsx server.ts",
"build": "vite build",
"preview": "vite preview",
"clean": "rm -rf dist",
"lint": "tsc --noEmit"
},
"dependencies": {
"@google/genai": "^1.51.0",
"@google/generative-ai": "^0.24.1",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"clsx": "^2.1.1",
"cors": "^2.8.6",
"dotenv": "^17.2.3",
"express": "^4.22.1",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"react-helmet-async": "^3.0.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.14.2",
"tailwind-merge": "^3.5.0",
"vite": "^6.2.3"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"autoprefixer": "^10.4.21",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.3"
}
}

View file

@ -0,0 +1,5 @@
User-agent: *
Allow: /
# Sitemaps
Sitemap: https://seu-dominio.com/sitemap.xml

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://seo-authority.digital/</loc>
<lastmod>2026-05-04</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://seo-authority.digital/sobre</loc>
<lastmod>2026-05-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://seo-authority.digital/contato</loc>
<lastmod>2026-05-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://seo-authority.digital/metodologia</loc>
<lastmod>2026-05-04</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://seo-authority.digital/artigo/futuro-do-seo-2026</loc>
<lastmod>2026-05-04</lastmod>
<priority>0.9</priority>
</url>
<url>
<loc>https://seo-authority.digital/artigo/eeat-guia-completo</loc>
<lastmod>2026-05-04</lastmod>
<priority>0.9</priority>
</url>
<url>
<loc>https://seo-authority.digital/artigo/sinais-de-usuario-ranking</loc>
<lastmod>2026-05-04</lastmod>
<priority>0.9</priority>
</url>
</urlset>

97
Template-03/server.ts Normal file
View file

@ -0,0 +1,97 @@
import express from 'express';
import { createServer as createViteServer } from 'vite';
import { GoogleGenAI } from "@google/genai";
import path from 'path';
import cors from 'cors';
async function startServer() {
const app = express();
const PORT = 3000;
app.use(express.json());
app.use(cors());
// Lazy Gemini Setup
let ai: GoogleGenAI | null = null;
const getAI = () => {
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
throw new Error("GEMINI_API_KEY environment variable is required");
}
if (!ai) {
ai = new GoogleGenAI({ apiKey });
}
return ai;
};
// Translation API
app.post('/api/translate', async (req, res) => {
const { text, targetLang, context, isObject } = req.body;
try {
const client = getAI();
let prompt = "";
if (isObject) {
prompt = `Translate the following JSON object values to ${targetLang}.
Keep the keys exactly the same.
Preserve markdown formatting in values.
Return ONLY the translated JSON object.
Object:
${JSON.stringify(text)}`;
} else {
prompt = `Translate the following ${context || 'text'} to ${targetLang}.
Preserve all markdown formatting, technical terms, and HTML structures if present.
Do not add any commentary or prefix. Return only the translated string.
Content:
${text}`;
}
const response = await client.models.generateContent({
model: "gemini-3-flash-preview",
contents: prompt,
config: isObject ? { responseMimeType: "application/json" } : undefined
});
let resultText = response.text || '';
if (isObject) {
try {
res.json({ translatedText: JSON.parse(resultText) });
} catch (e) {
console.error("Parse Error:", resultText);
res.json({ translatedText: text }); // Fallback to original if parse fails
}
} else {
res.json({ translatedText: resultText });
}
} catch (error) {
console.error("Translation Error:", error);
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to translate" });
}
});
// Vite Integration
if (process.env.NODE_ENV !== "production") {
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "spa",
});
app.use(vite.middlewares);
} else {
const distPath = path.join(process.cwd(), 'dist');
app.use(express.static(distPath));
app.get('*', (req, res) => {
res.sendFile(path.join(distPath, 'index.html'));
});
}
app.listen(PORT, "0.0.0.0", () => {
console.log(`Server running at http://0.0.0.0:${PORT}`);
});
}
startServer();

50
Template-03/src/App.tsx Normal file
View file

@ -0,0 +1,50 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
import Layout from './components/Layout';
import Home from './pages/Home';
import BlogPost from './pages/BlogPost';
import CategoryPage from './pages/CategoryPage';
import About from './pages/About';
import Methodology from './pages/Methodology';
import Privacy from './pages/Privacy';
import Terms from './pages/Terms';
import Contact from './pages/Contact';
import Archive from './pages/Archive';
import NotFound from './pages/NotFound';
import Bookmarks from './pages/Bookmarks';
import { LanguageProvider } from './contexts/LanguageContext';
import { BookmarksProvider } from './contexts/BookmarksContext';
export default function App() {
return (
<HelmetProvider>
<LanguageProvider>
<BookmarksProvider>
<Router>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/artigo/:slug" element={<BlogPost />} />
<Route path="/categoria/:categorySlug" element={<CategoryPage />} />
<Route path="/sobre" element={<About />} />
<Route path="/metodologia" element={<Methodology />} />
<Route path="/privacidade" element={<Privacy />} />
<Route path="/termos" element={<Terms />} />
<Route path="/contato" element={<Contact />} />
<Route path="/arquivo" element={<Archive />} />
<Route path="/leituras-salvas" element={<Bookmarks />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Layout>
</Router>
</BookmarksProvider>
</LanguageProvider>
</HelmetProvider>
);
}

View file

@ -0,0 +1,142 @@
import { Link } from 'react-router-dom';
import { Article } from '../types';
import { motion } from 'motion/react';
import { Bookmark, Clock, User, ArrowRight } from 'lucide-react';
import { useBookmarks } from '../contexts/BookmarksContext';
import { cn } from '../lib/utils';
interface ArticleCardProps {
article: Article;
featured?: boolean;
}
export default function ArticleCard({ article, featured = false }: ArticleCardProps) {
const { toggleBookmark, isBookmarked } = useBookmarks();
const bookmarked = isBookmarked(article.id);
const sysId = `NODE_IDX_0${article.id.slice(0, 1)}`;
if (featured) {
return (
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
className="group relative flex flex-col md:flex-row gap-0 bg-transparent border border-white/5 hover:bg-brand transition-all duration-500 overflow-hidden"
>
<div className="md:w-5/12 relative overflow-hidden bg-black grayscale group-hover:grayscale-0 transition-all duration-700 border-r border-white/5">
<Link to={`/artigo/${article.slug}`} className="block h-full">
<img
src={article.image}
alt={article.title}
className="h-full w-full object-cover transition-transform duration-1000 group-hover:scale-110 aspect-square md:aspect-auto"
referrerPolicy="no-referrer"
/>
</Link>
<div className="absolute inset-0 bg-brand/10 group-hover:bg-transparent transition-colors" />
<div className="absolute top-4 left-4 h-6 w-6 border-l border-t border-brand/50" />
</div>
<div className="md:w-7/12 flex flex-col p-6 lg:p-10 relative">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3 text-[9px] font-mono font-black uppercase tracking-[0.2em] text-brand px-2 py-0.5 bg-brand/5 border-l border-brand w-fit group-hover:bg-black group-hover:text-white transition-colors">
<span>{article.category}</span>
</div>
<span className="text-[8px] font-mono font-black text-slate-800 group-hover:text-black/40 transition-colors uppercase tracking-[0.3em]">{sysId}</span>
</div>
<Link to={`/artigo/${article.slug}`}>
<h2 className="text-2xl sm:text-3xl lg:text-4xl font-display font-black text-white leading-[0.95] mb-6 group-hover:text-black transition-colors italic uppercase tracking-tighter">
{article.title}
</h2>
</Link>
<p className="text-xs font-mono text-slate-500 leading-relaxed mb-10 line-clamp-2 uppercase tracking-tight group-hover:text-black/60 transition-colors">
{" >> "} {article.excerpt}
</p>
<div className="mt-auto flex items-center justify-between pt-8 border-t border-white/10 group-hover:border-black/10 transition-colors font-mono">
<div className="flex items-center gap-4">
<div className="h-12 w-12 border border-white/10 p-0.5 group-hover:border-black/20 transition-colors bg-white/5">
<img src={article.author.avatar} alt={article.author.name} className="h-full w-full object-cover grayscale opacity-50 group-hover:opacity-100 group-hover:grayscale-0 transition-all" />
</div>
<div>
<p className="text-[10px] font-black text-white leading-none uppercase tracking-wider group-hover:text-black transition-colors">{article.author.name}</p>
<p className="text-[8px] text-slate-700 uppercase tracking-widest font-black mt-1.5 group-hover:text-black/40 transition-colors italic">ARCHITECT_INFRA_v4</p>
</div>
</div>
<div className="flex items-center gap-2 text-brand font-black text-[9px] uppercase tracking-widest bg-brand/5 px-3 py-1 border border-brand/20 group-hover:bg-black group-hover:text-white group-hover:border-black transition-all">
<Clock size={11} />
<span>{article.readTime}</span>
</div>
</div>
</div>
</motion.div>
);
}
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="group flex flex-col bg-transparent border border-white/5 hover:border-brand/40 transition-all duration-500 overflow-hidden h-full relative"
>
<div className="absolute top-0 right-0 w-2 h-2 border-r border-t border-white/10 group-hover:border-brand/50 transition-colors m-4" />
<div className="relative aspect-[16/10] overflow-hidden bg-black grayscale group-hover:grayscale-0 transition-all duration-700">
<Link to={`/artigo/${article.slug}`} className="block h-full">
<img
src={article.image}
alt={article.title}
className="h-full w-full object-cover group-hover:scale-105 transition-all duration-1000"
referrerPolicy="no-referrer"
/>
</Link>
<div className="absolute inset-0 bg-brand/5 group-hover:bg-transparent transition-colors" />
<button
onClick={() => toggleBookmark(article.id)}
className={cn(
"absolute bottom-4 right-4 h-10 w-10 border border-white/10 backdrop-blur-md flex items-center justify-center transition-all opacity-0 group-hover:opacity-100",
bookmarked ? "bg-brand border-brand text-black opacity-100" : "bg-black/60 text-brand hover:bg-brand hover:text-black"
)}
>
<Bookmark size={14} fill={bookmarked ? "currentColor" : "none"} />
</button>
</div>
<div className="p-6 lg:p-8 flex flex-col flex-grow relative">
<div className="absolute bottom-0 right-0 w-16 h-16 bg-brand/5 blur-3xl -z-10" />
<div className="flex items-center justify-between mb-5 font-mono">
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-brand/60 border-l-2 border-brand pl-3 group-hover:text-brand transition-colors">{article.category}</span>
<span className="text-[8px] font-mono font-black text-slate-800 uppercase tracking-widest">{sysId}</span>
</div>
<Link to={`/artigo/${article.slug}`} className="flex-grow">
<h3 className="text-xl font-display font-black text-white leading-[0.95] mb-4 group-hover:text-brand transition-colors italic uppercase tracking-tight">
{article.title}
</h3>
<p className="text-slate-600 text-xs leading-relaxed line-clamp-2 font-mono uppercase tracking-tight mb-10">
{article.excerpt}
</p>
</Link>
<div className="mt-auto pt-8 border-t border-white/5 flex items-center justify-between font-mono">
<div className="flex items-center gap-3">
<div className="h-9 w-9 border border-white/10 p-0.5 group-hover:border-brand/40 transition-colors bg-white/5">
<img src={article.author.avatar} alt={article.author.name} className="h-full w-full object-cover grayscale opacity-40 group-hover:opacity-100 group-hover:grayscale-0 transition-all" />
</div>
<div className="flex flex-col">
<span className="text-[9px] font-black text-white uppercase tracking-wider">{article.author.name}</span>
<span className="text-[7px] text-slate-700 uppercase tracking-widest italic">{article.readTime}</span>
</div>
</div>
<div className="h-10 w-10 border border-white/5 flex items-center justify-center text-slate-700 group-hover:bg-brand group-hover:text-black group-hover:border-brand transition-all">
<ArrowRight size={16} />
</div>
</div>
</div>
</motion.div>
);
}

View file

@ -0,0 +1,31 @@
import { Link } from 'react-router-dom';
import { Category } from '../types';
import { cn } from '../lib/utils';
import { CATEGORY_MAP } from '../constants';
interface CategoryBadgeProps {
category: Category;
variant?: 'solid' | 'outline' | 'glass';
className?: string;
}
export default function CategoryBadge({ category, variant = 'solid', className }: CategoryBadgeProps) {
const mapping = CATEGORY_MAP[category] || { name: category, slug: category.toLowerCase().replace(/\s+/g, '-') };
const baseStyles = "inline-flex items-center rounded-full px-3 py-1 text-[10px] font-bold uppercase tracking-wider transition-all hover:scale-105 active:scale-95";
const variants = {
solid: "bg-brand text-white hover:bg-brand-dark",
outline: "border border-brand/10 text-brand hover:bg-brand/5",
glass: "bg-white/80 backdrop-blur-sm text-slate-900 shadow-sm hover:bg-white"
};
return (
<Link
to={`/categoria/${mapping.slug}`}
className={cn(baseStyles, variants[variant], className)}
onClick={(e) => e.stopPropagation()}
>
{mapping.name}
</Link>
);
}

View file

@ -0,0 +1,74 @@
import { Link } from 'react-router-dom';
import { Twitter, Linkedin, Instagram, Facebook, Youtube, Zap, ArrowUpRight, Github } from 'lucide-react';
export default function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-black border-t border-white/5 pt-24 pb-12 relative overflow-hidden font-mono">
<div className="mx-auto max-w-[1400px] px-6 lg:px-12 relative z-10">
<div className="grid grid-cols-1 md:grid-cols-12 gap-16 mb-20">
<div className="md:col-span-12 lg:col-span-4">
<Link to="/" className="flex items-center gap-3 mb-8 group">
<div className="h-8 w-8 bg-brand flex items-center justify-center text-black skew-x-[-6deg]">
<Zap size={16} fill="currentColor" className="skew-x-[6deg]" />
</div>
<span className="font-display text-xl font-black tracking-tight text-white uppercase italic">
NODE<span className="text-brand">_IDX</span>
</span>
</Link>
<p className="text-slate-700 max-w-sm mb-8 text-[9px] uppercase tracking-[0.3em] leading-relaxed font-black">
Deciphering the neural future of organic search across the strategic spectrum. [SIGNAL_V3.0]
</p>
<div className="flex gap-1.5">
{[Twitter, Linkedin, Github].map((Icon, i) => (
<a
key={i}
href="#"
className="h-10 w-10 flex items-center justify-center border border-white/5 text-slate-700 hover:text-brand hover:border-brand/20 transition-all font-mono"
>
<Icon size={14} />
</a>
))}
</div>
</div>
<div className="md:col-span-6 lg:col-span-2 invisible lg:visible">
<h4 className="text-white text-[9px] font-black uppercase tracking-[0.4em] mb-8 italic">NAV_CORE</h4>
<ul className="space-y-3 font-mono text-[9px] text-slate-700 font-black uppercase tracking-widest leading-loose">
<li><Link to="/categoria/estrategia" className="hover:text-brand transition-colors">STRATEGY_FEED</Link></li>
<li><Link to="/categoria/tecnico" className="hover:text-brand transition-colors">TECH_OPS</Link></li>
<li><Link to="/categoria/autoridade" className="hover:text-brand transition-colors">AUTHORITY_LOGS</Link></li>
</ul>
</div>
<div className="md:col-span-6 lg:col-span-4">
<div className="p-8 border border-white/5 bg-white/[0.01]">
<h5 className="text-white font-black uppercase text-[9px] tracking-[0.4em] mb-4 italic">BRIEFING_SYNC</h5>
<form className="flex gap-2" onSubmit={(e) => e.preventDefault()}>
<input
type="email"
placeholder="ENDPOINT_ID..."
className="flex-grow h-10 bg-black border border-white/5 px-4 text-[9px] font-black uppercase tracking-widest focus:border-brand outline-none"
/>
<button className="h-10 px-6 bg-brand text-black font-black text-[9px] uppercase tracking-widest hover:bg-white transition-all">
SYNC
</button>
</form>
</div>
</div>
</div>
<div className="pt-10 border-t border-white/5 flex flex-col md:flex-row justify-between items-center gap-6 text-[8px] font-black uppercase tracking-[0.3em] text-slate-800">
<p>
[ © NODE_IDX_{currentYear} ] DATASET_ENCRYPTED
</p>
<div className="flex gap-10">
<Link to="/privacidade" className="hover:text-brand transition-colors">PRIVACY_PROTOCOLS</Link>
<Link to="/termos" className="hover:text-brand transition-colors">STRUCTURE_INFO</Link>
</div>
</div>
</div>
</footer>
);
}

View file

@ -0,0 +1,172 @@
import { Link } from 'react-router-dom';
import { Search, Menu, X, Globe, Twitter, Instagram, Linkedin, Youtube, Bookmark, Zap } from 'lucide-react';
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { useLanguage } from '../contexts/LanguageContext';
import { translations } from '../constants';
import SearchOverlay from './SearchOverlay';
import { useBookmarks } from '../contexts/BookmarksContext';
import { cn } from '../lib/utils';
interface HeaderProps {
onSearchOpen: () => void;
}
export default function Header({ onSearchOpen }: HeaderProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const { lang, setLang } = useLanguage();
const { bookmarks } = useBookmarks();
const t = translations[lang];
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 20);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const categories = [
{ name: 'Estratégia', slug: 'estrategia' },
{ name: 'Técnico', slug: 'tecnico' },
{ name: 'Autoridade', slug: 'autoridade' },
{ name: 'Negócios', slug: 'negocios' }
];
return (
<header className={cn(
"fixed top-0 z-50 w-full transition-all duration-300",
scrolled ? "bg-black/95 backdrop-blur-3xl border-b border-brand/10 py-2 shadow-xl" : "bg-transparent py-4"
)}>
<div className="mx-auto max-w-[1400px] px-6 lg:px-12">
<div className="flex h-14 items-center justify-between">
<div className="flex items-center gap-12">
<Link to="/" className="flex items-center gap-3 group">
<div className="h-8 w-8 bg-brand flex items-center justify-center text-black group-hover:bg-brand-light transition-all skew-x-[-6deg]">
<Zap size={16} fill="currentColor" className="skew-x-[6deg]" />
</div>
<span className="font-display text-xl font-black tracking-tight text-white uppercase italic">
NODE<span className="text-brand">_IDX</span>
</span>
</Link>
<nav className="hidden lg:flex items-center gap-8">
{categories.map((cat) => (
<Link
key={cat.slug}
to={`/categoria/${cat.slug}`}
className="text-[10px] font-mono font-black uppercase tracking-[0.2em] text-slate-500 hover:text-brand transition-colors relative group"
>
{cat.name}
<span className="absolute -bottom-1 left-0 w-0 h-px bg-brand transition-all group-hover:w-full" />
</Link>
))}
</nav>
</div>
<div className="flex items-center gap-3">
<div className="hidden xl:flex items-center gap-2 pr-6 border-r border-white/10 text-[8px] font-mono font-black text-slate-700 uppercase tracking-[0.3em]">
<span className="h-1 w-1 bg-brand animate-pulse" />
Operational
</div>
{/* Language Switcher */}
<button
onClick={() => setLang(lang === 'pt-br' ? 'en' : 'pt-br')}
className="hidden sm:flex items-center gap-3 px-5 py-2 hover:bg-brand/5 border border-white/5 hover:border-brand/30 transition-all group skew-x-[-12deg]"
>
<Globe size={14} className="text-slate-400 group-hover:text-brand transition-colors skew-x-[12deg]" />
<span className="text-[10px] font-mono font-black uppercase tracking-widest text-slate-400 group-hover:text-white skew-x-[12deg]">
{lang === 'pt-br' ? 'PT' : 'EN'}
</span>
</button>
<button
className="p-3 text-slate-400 hover:text-brand hover:bg-brand/5 border border-white/5 hover:border-brand/20 transition-all skew-x-[-12deg]"
onClick={onSearchOpen}
>
<Search size={18} className="skew-x-[12deg]" />
</button>
<Link
to="/leituras-salvas"
className="relative p-3 text-slate-400 hover:text-brand hover:bg-brand/5 border border-white/5 hover:border-brand/20 transition-all skew-x-[-12deg]"
title="Leituras Salvas"
>
<Bookmark size={18} className="skew-x-[12deg]" />
<AnimatePresence>
{bookmarks.length > 0 && (
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
className="absolute -top-1 -right-1 h-5 w-5 bg-brand text-black text-[9px] font-black flex items-center justify-center border border-black skew-x-[12deg]"
>
{bookmarks.length}
</motion.span>
)}
</AnimatePresence>
</Link>
<button
className="lg:hidden p-3 text-white hover:bg-brand/10 border border-white/10 transition-all skew-x-[-12deg]"
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
<div className="skew-x-[12deg]">
{isMenuOpen ? <X size={20} /> : <Menu size={20} />}
</div>
</button>
</div>
</div>
</div>
<AnimatePresence>
{isMenuOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="lg:hidden bg-black border-t border-brand/20 overflow-hidden"
>
<div className="flex flex-col gap-10 p-10 font-mono">
<div className="flex flex-col gap-6">
{categories.map((cat) => (
<Link
key={cat.slug}
to={`/categoria/${cat.slug}`}
className="text-4xl font-display font-black text-white hover:text-brand transition-colors uppercase italic"
onClick={() => setIsMenuOpen(false)}
>
{" >> "} {cat.name}
</Link>
))}
<div className="h-px w-full bg-white/5 my-4" />
<Link
to="/contato"
className="text-xl font-black uppercase tracking-[0.4em] text-slate-600 hover:text-brand transition-colors"
onClick={() => setIsMenuOpen(false)}
>
Contact_Protocol
</Link>
<Link
to="/arquivo"
className="text-xl font-black uppercase tracking-[0.4em] text-slate-600 hover:text-brand transition-colors"
onClick={() => setIsMenuOpen(false)}
>
Archive_Files
</Link>
</div>
<div className="pt-10 border-t border-white/5 flex items-center justify-start gap-4">
{[Twitter, Linkedin, Instagram].map((Icon, i) => (
<a key={i} href="#" className="h-14 w-14 flex items-center justify-center bg-white/5 text-slate-400 hover:text-brand hover:border-brand/40 transition-all border border-white/5 skew-x-[-12deg]">
<Icon size={20} className="skew-x-[12deg]" />
</a>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</header>
);
}

View file

@ -0,0 +1,43 @@
import { ReactNode, useEffect, useState } from 'react';
import Header from './Header';
import Footer from './Footer';
import LeadMagnet from './LeadMagnet';
import SearchOverlay from './SearchOverlay';
import { useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'motion/react';
interface LayoutProps {
children: ReactNode;
}
export default function Layout({ children }: LayoutProps) {
const { pathname } = useLocation();
const [isSearchOpen, setIsSearchOpen] = useState(false);
// Scroll to top on route change
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return (
<div className="min-h-screen flex flex-col bg-[#020203] selection:bg-brand/10 selection:text-brand text-slate-300">
<Header onSearchOpen={() => setIsSearchOpen(true)} />
<SearchOverlay isOpen={isSearchOpen} onClose={() => setIsSearchOpen(false)} />
<main className="flex-grow">
<AnimatePresence mode="wait">
<motion.div
key={pathname}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
{children}
</motion.div>
</AnimatePresence>
</main>
<LeadMagnet />
<Footer />
</div>
);
}

View file

@ -0,0 +1,108 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { X, Download, ShieldCheck, ArrowRight, Mail } from 'lucide-react';
export default function LeadMagnet() {
const [isOpen, setIsOpen] = useState(false);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
// Check if user already dismissed or converted
const hasSeen = localStorage.getItem('seo_lead_magnet_dismissed');
if (hasSeen) return;
const handleMouseLeave = (e: MouseEvent) => {
if (e.clientY <= 0 && !dismissed) {
setIsOpen(true);
}
};
const handleScroll = () => {
const scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
if (scrollPercent > 50 && !dismissed) {
setIsOpen(true);
}
};
document.addEventListener('mouseleave', handleMouseLeave);
window.addEventListener('scroll', handleScroll);
return () => {
document.removeEventListener('mouseleave', handleMouseLeave);
window.removeEventListener('scroll', handleScroll);
};
}, [dismissed]);
const handleClose = () => {
setIsOpen(false);
setDismissed(true);
localStorage.setItem('seo_lead_magnet_dismissed', 'true');
};
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[200] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm">
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="bg-[#08080a] border border-white/10 shadow-2xl max-w-2xl w-full overflow-hidden flex flex-col relative"
>
<button
onClick={handleClose}
className="absolute top-4 right-4 h-8 w-8 bg-white/5 hover:bg-brand hover:text-black text-slate-500 flex items-center justify-center z-20 transition-all"
aria-label="Fechar"
>
<X size={16} />
</button>
<div className="p-8 md:p-12">
<div className="flex items-center gap-3 mb-6">
<div className="h-10 w-10 bg-brand/10 flex items-center justify-center text-brand skew-x-[-6deg]">
<ShieldCheck size={20} className="skew-x-[6deg]" />
</div>
<span className="text-[9px] font-mono font-black text-brand uppercase tracking-[0.4em]">Resource_Found</span>
</div>
<h2 className="text-3xl sm:text-4xl font-display font-black text-white italic tracking-tight leading-none mb-6">
Dominate the<br /><span className="text-brand">Neural_Index 2026.</span>
</h2>
<p className="text-slate-500 font-mono text-[11px] leading-relaxed uppercase tracking-widest mb-10 max-w-lg">
Download our exclusive blueprint for technical systems and semantic authority protocols.
</p>
<form className="space-y-4" onSubmit={(e) => { e.preventDefault(); handleClose(); }}>
<div className="relative">
<Mail className="absolute left-0 top-1/2 -translate-y-1/2 text-brand" size={16} />
<input
type="email"
placeholder="ENTER_ENDPOINT_ID..."
required
className="w-full h-12 pl-8 border-b border-white/10 bg-transparent text-white placeholder:text-slate-800 focus:outline-none focus:border-brand transition-all font-mono text-xs uppercase tracking-widest"
/>
</div>
<button
type="submit"
className="btn-primary w-full"
>
Sync_Now <Download size={16} />
</button>
</form>
<div className="mt-8 pt-8 border-t border-white/5 flex flex-wrap gap-x-8 gap-y-4">
<div className="flex items-center gap-2 text-[8px] font-black text-slate-700 uppercase tracking-widest">
<ArrowRight size={10} className="text-brand" /> Interactive Protocol
</div>
<div className="flex items-center gap-2 text-[8px] font-black text-slate-700 uppercase tracking-widest">
<ArrowRight size={10} className="text-brand" /> Audit Framework
</div>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
}

View file

@ -0,0 +1,158 @@
import { Helmet } from 'react-helmet-async';
interface SEOProps {
title: string;
description: string;
canonical?: string;
ogImage?: string;
ogType?: string;
keywords?: string[];
articleData?: {
publishedTime?: string;
modifiedTime?: string;
author?: string;
authorAvatar?: string;
tags?: string[];
section?: string;
};
}
export default function SEO({
title,
description,
canonical,
ogImage = 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&q=80&w=1200',
ogType = 'website',
keywords = [],
articleData
}: SEOProps) {
const siteName = 'NODE_SEARCH';
const siteTitle = 'NODE_SEARCH | Inteligência Neural & Autoridade de Busca';
const siteUrl = window.location.origin;
const fullTitle = `${title} | ${siteName}`;
// Organization Schema
const organizationSchema = {
"@type": "Organization",
"@id": `${siteUrl}/#organization`,
"name": "NODE_SEARCH Digital",
"url": siteUrl,
"logo": {
"@type": "ImageObject",
"url": `${siteUrl}/logo.png`, // Placeholder, organization usually has logo
"width": 600,
"height": 600
},
"sameAs": [
"https://twitter.com/seohub",
"https://linkedin.com/company/seohub"
]
};
// Breadcrumb Schema
const breadcrumbSchema = {
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": siteUrl
}
]
};
if (ogType === 'article') {
breadcrumbSchema.itemListElement.push({
"@type": "ListItem",
"position": 2,
"name": title,
"item": canonical || window.location.href
});
}
// Final Schema
const finalSchema = {
"@context": "https://schema.org",
"@graph": [
organizationSchema,
{
"@type": ogType === 'article' ? "BlogPosting" : "WebSite",
"@id": `${siteUrl}/#website`,
"url": siteUrl,
"name": siteName,
"publisher": { "@id": `${siteUrl}/#organization` },
"inLanguage": "pt-BR"
},
ogType === 'article' ? {
"@type": "BlogPosting",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": canonical || window.location.href
},
"headline": title,
"description": description,
"image": ogImage,
"author": {
"@type": "Person",
"name": articleData?.author || "Equipe SEO",
"image": articleData?.authorAvatar || "https://i.pravatar.cc/150?u=seo"
},
"publisher": { "@id": `${siteUrl}/#organization` },
"datePublished": articleData?.publishedTime,
"dateModified": articleData?.modifiedTime || articleData?.publishedTime,
"keywords": keywords.join(', '),
"articleSection": articleData?.section
} : {
"@type": "WebPage",
"@id": `${siteUrl}/#page`,
"url": canonical || window.location.href,
"name": fullTitle,
"description": description,
"publisher": { "@id": `${siteUrl}/#organization` }
},
breadcrumbSchema
]
};
return (
<Helmet>
<title>{fullTitle}</title>
<meta name="description" content={description} />
{keywords.length > 0 && <meta name="keywords" content={keywords.join(', ')} />}
{/* Canonical URL */}
<link rel="canonical" href={canonical || window.location.href} />
{/* Open Graph / Facebook */}
<meta property="og:type" content={ogType} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImage} />
<meta property="og:site_name" content={siteName} />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImage} />
{/* Article Specifics */}
{ogType === 'article' && articleData && (
<>
{articleData.publishedTime && <meta property="article:published_time" content={articleData.publishedTime} />}
{articleData.modifiedTime && <meta property="article:modified_time" content={articleData.modifiedTime} />}
{articleData.author && <meta property="article:author" content={articleData.author} />}
{articleData.tags?.map(tag => (
<meta key={tag} property="article:tag" content={tag} />
))}
</>
)}
{/* Structured Data */}
<script type="application/ld+json">
{JSON.stringify(finalSchema)}
</script>
</Helmet>
);
}

Some files were not shown because too many files have changed in this diff Show more