Compare commits

...

49 Commits

Author SHA1 Message Date
avanzato dc697c8962 Release PROD: Aplicativos de Atrair Leads
Deploy PROD Avanzato / deploy-prod (push) Successful in 45s
2026-06-03 22:19:56 -03:00
avanzato 667447c389 Merge remote-tracking branch 'origin/develop' 2026-06-03 22:19:52 -03:00
avanzato e3f35d808d Log release HML: Aplicativos de Atrair Leads
Deploy HML Avanzato / deploy-hml (push) Successful in 37s
2026-06-03 22:07:59 -03:00
avanzato fdfb4606f0 Aplicativos de Atrair Leads 2026-06-03 22:07:59 -03:00
avanzato e5c18bb6fc Log release HML: Aplicativos de Atrair Leads
Deploy HML Avanzato / deploy-hml (push) Successful in 2m21s
2026-06-02 22:34:40 -03:00
avanzato 6dfdf01830 Aplicativos de Atrair Leads 2026-06-02 22:34:39 -03:00
avanzato cd6bcf5b3d Release PROD: Paginas e SEO master, correção javascript google
Deploy PROD Avanzato / deploy-prod (push) Successful in 35s
2026-06-02 14:21:08 -03:00
avanzato 35e5727e60 Merge remote-tracking branch 'origin/develop' 2026-06-02 12:50:56 -03:00
avanzato 96563b30e9 Log release HML: Paginas e SEO master
Deploy HML Avanzato / deploy-hml (push) Successful in 42s
2026-06-02 12:43:58 -03:00
avanzato 80dbb986f4 Paginas e SEO master 2026-06-02 12:43:57 -03:00
avanzato 6db7b5200b Log release HML: Paginas e SEO master
Deploy HML Avanzato / deploy-hml (push) Successful in 1m17s
2026-06-02 12:07:35 -03:00
avanzato 52cda4304b Paginas e SEO master 2026-06-02 12:07:34 -03:00
avanzato a85a6c9b3f Log release HML: Paginas e SEO master
Deploy HML Avanzato / deploy-hml (push) Successful in 46s
2026-06-02 11:45:37 -03:00
avanzato 2900b9afd9 Paginas e SEO master 2026-06-02 11:45:36 -03:00
avanzato 433773c4b8 Log release HML: Paginas e SEO master
Deploy HML Avanzato / deploy-hml (push) Successful in 47s
2026-06-02 10:42:23 -03:00
avanzato 6140142bab Paginas e SEO master 2026-06-02 10:42:23 -03:00
avanzato 529036bfe9 Release PROD: Paginas e SEO master
Deploy PROD Avanzato / deploy-prod (push) Successful in 34s
2026-06-01 20:28:34 -03:00
avanzato 52f86cbba5 Release PROD: Paginas e SEO master
Deploy PROD Avanzato / deploy-prod (push) Successful in 43s
2026-06-01 19:56:51 -03:00
avanzato 3eb41133f4 Merge remote-tracking branch 'origin/develop' 2026-06-01 19:56:36 -03:00
avanzato e8f15d63ed Log release HML: Paginas e SEO master
Deploy HML Avanzato / deploy-hml (push) Successful in 42s
2026-06-01 19:55:54 -03:00
avanzato 24786e3512 Paginas e SEO master 2026-06-01 19:55:53 -03:00
avanzato 7bcc582664 Release PROD: Paginas e SEO master
Deploy PROD Avanzato / deploy-prod (push) Successful in 42s
2026-06-01 19:44:45 -03:00
avanzato 4f15851f09 Merge remote-tracking branch 'origin/develop' 2026-06-01 19:43:55 -03:00
avanzato 15ce64e786 Log release HML: Paginas e SEO master
Deploy HML Avanzato / deploy-hml (push) Successful in 35s
2026-06-01 19:39:58 -03:00
avanzato 913e717a21 Paginas e SEO master 2026-06-01 19:39:58 -03:00
avanzato aa275ef538 Log release HML: Revisão Logo, Favicon, Robots
Deploy HML Avanzato / deploy-hml (push) Successful in 39s
2026-06-01 19:30:24 -03:00
avanzato 19f2848fec Revisão Logo, Favicon, Robots 2026-06-01 19:30:24 -03:00
avanzato 10e910eb6b Release PROD: Revisão Logo, Favicon, Robots
Deploy PROD Avanzato / deploy-prod (push) Successful in 34s
2026-05-31 00:58:08 -03:00
avanzato 9885f59b06 Merge remote-tracking branch 'origin/develop' 2026-05-31 00:58:01 -03:00
avanzato d79c9c48a9 Log release HML: Revisão Logo, Favicon, Robots
Deploy HML Avanzato / deploy-hml (push) Successful in 33s
2026-05-31 00:57:46 -03:00
avanzato 7f6c800ab0 Revisão Logo, Favicon, Robots 2026-05-31 00:57:45 -03:00
avanzato ec3e439973 Release PROD: Revisão Logo, Favicon, Robots
Deploy PROD Avanzato / deploy-prod (push) Successful in 33s
2026-05-31 00:29:03 -03:00
avanzato bfb022866b Merge remote-tracking branch 'origin/develop' 2026-05-31 00:29:00 -03:00
avanzato be22792b6d Log release HML: Revisão Logo, Favicon, Robots
Deploy HML Avanzato / deploy-hml (push) Successful in 37s
2026-05-31 00:28:42 -03:00
avanzato cb08c53b40 Revisão Logo, Favicon, Robots 2026-05-31 00:28:42 -03:00
avanzato 8c056b0c50 Release PROD: Revisão Logo, Favicon, Robots
Deploy PROD Avanzato / deploy-prod (push) Successful in 33s
2026-05-31 00:03:02 -03:00
avanzato b284c4d258 Merge remote-tracking branch 'origin/develop' 2026-05-31 00:02:57 -03:00
avanzato 3d36e98185 Log release HML: Revisão Logo, Favicon, Robots
Deploy HML Avanzato / deploy-hml (push) Successful in 59s
2026-05-30 23:54:49 -03:00
avanzato 4e26c13240 Revisão Logo, Favicon, Robots 2026-05-30 23:54:48 -03:00
avanzato d15474365c Release PROD: Favivon - Correção
Deploy PROD Avanzato / deploy-prod (push) Successful in 33s
2026-05-30 23:15:12 -03:00
avanzato e9f47c5b1e Merge remote-tracking branch 'origin/develop' 2026-05-30 23:15:09 -03:00
avanzato af8ef2693d Release PROD: Favivon - Correção
Deploy PROD Avanzato / deploy-prod (push) Successful in 34s
2026-05-30 20:36:09 -03:00
avanzato 5b729b23be Merge remote-tracking branch 'origin/develop' 2026-05-30 20:35:32 -03:00
avanzato 41312945d9 Release PROD: Atualização e correção de diretorios
Deploy PROD Avanzato / deploy-prod (push) Successful in 30s
2026-05-30 19:30:29 -03:00
avanzato bb73783e9f Merge remote-tracking branch 'origin/develop' 2026-05-30 19:30:26 -03:00
avanzato 57c4abf055 Merge branch 'develop'
Deploy PROD Avanzato / deploy-prod (push) Successful in 50s
2026-05-29 01:15:14 -03:00
avanzato f493c55040 Merge branch 'develop'
Deploy PROD Avanzato / deploy-prod (push) Successful in 31s
2026-05-27 22:45:17 -03:00
avanzato 0a0d2382d1 Merge branch 'develop'
Deploy PROD Avanzato / deploy-prod (push) Successful in 54s
2026-05-27 16:17:46 -03:00
avanzato 47fdcab3b4 Adiciona deploy automático produção
Deploy PROD Avanzato / deploy-prod (push) Successful in 58s
2026-05-26 22:07:42 -03:00
162 changed files with 13053 additions and 1071 deletions
Vendored
BIN
View File
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
name: Deploy PROD Avanzato
on:
push:
branches:
- main
jobs:
deploy-prod:
runs-on: linux_amd64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Instalar dependências
run: npm install
- name: Build projeto
run: npm run build
- name: Compactar dist
run: tar -czf dist.tar.gz dist
- name: Enviar para servidor PROD
run: scp -o StrictHostKeyChecking=no -i /data/avz_hml_deploy dist.tar.gz root@191.252.100.174:/tmp/avz-site-prod-dist.tar.gz
- name: Publicar PROD
run: |
ssh -o StrictHostKeyChecking=no -i /data/avz_hml_deploy root@191.252.100.174 '
rm -rf /opt/avanzato/site/app/dist_old
mv /opt/avanzato/site/app/dist /opt/avanzato/site/app/dist_old
mkdir -p /opt/avanzato/site/app/dist
tar -xzf /tmp/avz-site-prod-dist.tar.gz -C /opt/avanzato/site/app
rm -f /tmp/avz-site-prod-dist.tar.gz
docker restart avanzato-site
'
+4
View File
@@ -40,3 +40,7 @@ Thumbs.db
# Scripts de deploy local
scripts/.env.local
# ZIPs internos (nunca commite!)
*.zip
avanzato-*.zip
+96
View File
@@ -0,0 +1,96 @@
# Deploy com Docker - Avanzato
## O Problema
O Nginx no Docker nao vem configurado para SPA React. Ao acessar `/avaliacao` diretamente, da 404.
## A Solucao
### Opcao 1: Configurar Nginx dentro do container (RECOMENDADO)
**1. Criar um novo Dockerfile:**
```dockerfile
FROM nginx:alpine
# Copiar o site
COPY dist/ /usr/share/nginx/html/
# Copiar a configuracao customizada
COPY nginx-docker.conf /etc/nginx/conf.d/default.conf
# Expor porta 80
EXPOSE 80
# Iniciar Nginx
CMD ["nginx", "-g", "daemon off;"]
```
**2. Build e run:**
```bash
# Build
npm run build
bash scripts/build-static-routes.sh dist
docker build -t avanzato-site .
# Run
docker run -d -p 80:80 --name avanzato avanzato-site
```
### Opcao 2: Usar docker-compose
**docker-compose.yml:**
```yaml
version: '3.8'
services:
avanzato:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./dist:/usr/share/nginx/html:ro
- ./nginx-docker.conf:/etc/nginx/conf.d/default.conf:ro
container_name: avanzato-site
```
**Comandos:**
```bash
npm run build
bash scripts/build-static-routes.sh dist
docker-compose up -d
```
### Opcao 3: Container ja rodando (hotfix)
Se o container ja esta rodando:
```bash
# Copiar config para dentro do container
docker cp nginx-docker.conf avanzato-site:/etc/nginx/conf.d/default.conf
# Recarregar Nginx
docker exec avanzato-site nginx -s reload
```
## Verificar se funcionou
```bash
# Testar
curl -I http://localhost/avaliacao
# Deve retornar HTTP 200, nao 404
```
## A regra magica
A unica coisa que importa e esta linha no Nginx:
```nginx
location / {
try_files $uri $uri/ /index.html;
}
```
Isso diz ao Nginx: "se nao encontrar o arquivo, manda para index.html" — onde o React pega a rota.
+116
View File
@@ -0,0 +1,116 @@
# Guia Rapido - Configurar Nginx (SPA React)
## O Problema
URLs como `avanzato.com.br/avaliacao` dao 404 quando acessadas diretamente.
Funcionam apenas quando clica no menu (navegacao interna do React).
## A Solucao
O Nginx precisa redirecionar todas as rotas para `index.html`.
## Passo a Passo
### 1. Copiar a config do projeto para o servidor
No seu Mac, envie o arquivo `nginx.conf` do projeto para o servidor:
```bash
# Enviar o arquivo nginx.conf do projeto para o servidor
scp /Users/avanzato/Projetos/Avanzato/avanzato-site/app/nginx.conf root@SEU_SERVIDOR:/etc/nginx/sites-available/avanzato
```
Ou, se ja estiver logado no servidor:
```bash
# Logar no servidor
ssh root@SEU_SERVIDOR
# Criar o arquivo de config
cat > /etc/nginx/sites-available/avanzato << 'EOF'
server {
listen 80;
server_name avanzato.com.br;
root /var/www/avanzato;
index index.html;
# ESSA LINHA E A CHAVE - redireciona tudo para index.html
location / {
try_files $uri $uri/ /index.html;
}
# Cache para assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 6M;
add_header Cache-Control "public";
}
# Seguranca
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
# Bloquear arquivos ocultos
location ~ /\. {
deny all;
}
}
EOF
```
### 2. Ativar o site
```bash
# Criar link simbolico (ativa o site)
ln -s /etc/nginx/sites-available/avanzato /etc/nginx/sites-enabled/avanzato
# Remover o default (se existir)
rm -f /etc/nginx/sites-enabled/default
```
### 3. Testar e aplicar
```bash
# Testar a configuracao (mostra erros se houver)
nginx -t
# Se der OK, recarregar o Nginx
systemctl reload nginx
```
### 4. Verificar se funcionou
```bash
# Testar uma rota direta
curl -I https://avanzato.com.br/avaliacao
# Deve retornar HTTP 200, nao 404
```
---
## Comandos Uteis
| Comando | O que faz |
|---------|-----------|
| `nginx -t` | Testa a configuracao (sem aplicar) |
| `systemctl reload nginx` | Recarrega config sem derrubar o site |
| `systemctl restart nginx` | Reinicia o Nginx |
| `systemctl status nginx` | Verifica se esta rodando |
| `tail -f /var/log/nginx/error.log` | Ver erros em tempo real |
---
## Importante
Se voce usa **Apache** em vez de Nginx, a config esta no arquivo `.htaccess` que ja vai no `dist/`:
```apache
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^ index.html [L]
```
Basta garantir que o `.htaccess` esta na raiz do site e o `mod_rewrite` esta ativado.
+95
View File
@@ -0,0 +1,95 @@
# MAPA DE ATUACAO AVANZATO - Dados Reais
## Cidades Atendidas (com clientes)
### 1. Guarulhos (Matriz) - 11 clientes
| Cliente | Segmento | Servicos |
|---------|----------|----------|
| Dellacont | Contabilidade | Suporte + Servidor + Backup |
| Madri | Contabilidade | Suporte + Servidor Nuvem + Backup |
| Mouradi Naddi | Advocacia | Suporte + Servidor + Backup |
| EFLAW | Advocacia | Suporte + Servidor + Backup |
| Moraes Advogados | Advocacia | Suporte + Servidor + Backup |
| Thalls | Industria | Suporte + Servidor + Backup + VoIP |
| Ganiko | BPO | Suporte + Servidor Nuvem + Backup |
| DCarvalho | Embalagens | Suporte + Servidor Nuvem + Backup |
| Garuppy | Logistica | Suporte + Servidor Nuvem + Backup |
| Premier | Monitoramento | Suporte + Servidor + Backup |
| Condominio | VoIP | Interligacao aptos a portaria |
### 2. Mogi das Cruzes - 1 cliente
| Cliente | Segmento | Servicos |
|---------|----------|----------|
| Alamo | Monitoramento | Suporte + Servidor + Backup |
### 3. Ipiranga (Sao Paulo) - 1 cliente
| Cliente | Segmento | Servicos |
|---------|----------|----------|
| ONG/Terceiro Setor | Institucional | Suporte + Servidor + Backup + Cloud + Seguranca |
### 4. Cerqueira Cesar (Sao Paulo) - 1 cliente
| Cliente | Segmento | Servicos |
|---------|----------|----------|
| Museu de Faculdade Publica | Acervo Digital | Implantacao Tainacan + ICA-AtoM |
### 5. Vila Formosa (Sao Paulo) - 1 cliente
| Cliente | Segmento | Servicos |
|---------|----------|----------|
| Apoio 24 Horas | Monitoramento | Servidores + pfSense + VPN + Suporte |
### 6. Ribeirao Preto - 1 cliente
| Cliente | Segmento | Servicos |
|---------|----------|----------|
| RMA | Portaria Remota | VoIP em Condominios |
### 7. Bras (Sao Paulo) - 1 cliente
| Cliente | Segmento | Servicos |
|---------|----------|----------|
| Lucena Textil | Textil | Colocation + Rede + VPN + Backup |
### 8. Santana (Sao Paulo) - 1 cliente
| Cliente | Segmento | Servicos |
|---------|----------|----------|
| Quantica Eletrica | Eventos | Suporte + Servidor Nuvem + Backup |
### 9. Mongagua - 1 cliente
| Cliente | Segmento | Servicos |
|---------|----------|----------|
| GateOne e Fox | Monitoramento | Servidores + pfSense + VPN + Suporte |
### 10. Sao Paulo / Cangaiba - 1 cliente
| Cliente | Segmento | Servicos |
|---------|----------|----------|
| RollPrime | VoIP | 2 Ramais VoIP |
### 11. Sao Paulo / Jurubatuba - 1 cliente
| Cliente | Segmento | Servicos |
|---------|----------|----------|
| Piso.com | Piso de Borracha | Suporte + Servidor Nuvem + Backup (em negociacao) |
## Total: 20 clientes ativos em 11 localidades
## Segmentos Atendidos
- Contabilidade (2)
- Advocacia (3)
- Industria (1)
- BPO (1)
- Embalagens (1)
- Logistica (1)
- Monitoramento (4)
- Institucional (1)
- Acervo Digital (1)
- Portaria Remota (1)
- Textil (1)
- Eventos (1)
- VoIP (2)
- Piso de Borracha (1)
## Servicos Mais Vendidos (ranking)
1. Suporte a Computadores (100% dos clientes)
2. Administracao de Servidor (~80%)
3. Backup (~80%)
4. VPN (~40%)
5. VoIP (~25%)
6. Cloud (~15%)
7. Seguranca/Colocation (~10%)
BIN
View File
Binary file not shown.
-246
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+42 -11
View File
@@ -5,6 +5,36 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- DETECAO DE AMBIENTE - Inline (executa antes de tudo) -->
<script>
(function() {
var h = location.hostname;
var IS_PROD = h === 'avanzato.com.br' || h === 'www.avanzato.com.br';
// Torna disponivel para outros scripts
window.__IS_PROD__ = IS_PROD;
// Se NAO for producao, desativa tracking globalmente
if (!IS_PROD) {
window.__AVANZATO_ENV__ = h === 'hml.avanzato.com.br' ? 'homologation' : 'development';
// Bloqueia gtag
window.dataLayer = { push: function(){} };
window.gtag = function(){};
// Bloqueia fbq (Facebook Pixel)
window.fbq = function(){};
// Bloqueia Mautic
window.MauticSDKLoaded = true;
console.log('[Avanzato] Ambiente: ' + window.__AVANZATO_ENV__ + ' | Tracking bloqueado');
} else {
window.__AVANZATO_ENV__ = 'production';
}
})();
</script>
<!-- Google tag (gtag.js) - Ads + Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=AW-16714813566"></script>
<script>
@@ -70,6 +100,7 @@
<title>Avanzato Tecnologia | TI Gerenciada, Cloud e Seguranca em Guarulhos/SP</title>
<meta name="title" content="Avanzato Tecnologia | TI Gerenciada, Cloud e Seguranca em Guarulhos/SP" />
<meta name="description" content="Solucoes em TI Gerenciada, Cloud Computing e Seguranca da Informacao. +23 anos de experiencia, +500 clientes atendidos. Suporte 24/7 em Guarulhos e regiao." />
<meta name="app-version" content="1.0.0" />
<meta name="keywords" content="TI Gerenciada, Cloud Computing, Seguranca da Informacao, Suporte Tecnico, Tecnologia da Informacao, Guarulhos, Sao Paulo, Infraestrutura de TI, Backup, Firewall, Consultoria TI" />
<meta name="author" content="Avanzato Tecnologia" />
<meta name="robots" content="index, follow" id="meta-robots" />
@@ -78,9 +109,9 @@
<!-- BLOQUEAR INDEXACAO EM HOMOLOGACAO -->
<script>
(function() {
var hostname = window.location.hostname;
if (hostname !== 'avanzato.com.br') {
// Homologacao: bloquear indexacao
// Usa IS_PROD ja definido no inicio do documento
if (!window.__IS_PROD__) {
// Homologacao/Desenvolvimento: bloquear indexacao
document.getElementById('meta-robots').setAttribute('content', 'noindex, nofollow');
document.getElementById('meta-googlebot').setAttribute('content', 'noindex, nofollow');
// Adicionar tambem X-Robots-Tag via meta http-equiv
@@ -88,7 +119,9 @@
meta.httpEquiv = 'X-Robots-Tag';
meta.content = 'noindex, nofollow';
document.head.appendChild(meta);
console.log('%c[SEO] Homologacao detectada - indexacao BLOQUEADA (noindex, nofollow)', 'background:#dc2626;color:#fff;font-weight:bold;padding:2px 6px;border-radius:3px');
console.log('%c[SEO] ' + (window.__AVANZATO_ENV__ || 'nao-producao') + ' - indexacao BLOQUEADA', 'background:#dc2626;color:#fff;font-weight:bold;padding:2px 6px;border-radius:3px');
} else {
console.log('%c[SEO] Producao - indexacao PERMITIDA (index, follow)', 'background:#16a34a;color:#fff;font-weight:bold;padding:2px 6px;border-radius:3px');
}
})();
</script>
@@ -113,11 +146,9 @@
<meta property="twitter:image" content="https://avanzato.com.br/og-image.jpg" />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="./site.webmanifest" />
<link rel="icon" type="image/png" href="https://avanzato.com.br/favicon.png" />
<link rel="shortcut icon" type="image/png" href="https://avanzato.com.br/favicon.png" />
<link rel="manifest" href="/site.webmanifest" />
<!-- Theme Color -->
<meta name="theme-color" content="#cbf400" />
@@ -208,8 +239,8 @@
}
}
</script>
<script type="module" crossorigin src="./assets/index-C1GgsDvQ.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Cg4oF5jQ.css">
<script type="module" crossorigin src="/assets/index-DLMsdH5y.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-8ILJAHKZ.css">
</head>
<body>
<div id="root"></div>
+10 -23
View File
@@ -1,26 +1,13 @@
# =============================================================================
# ROBOTS.TXT - VERSAO HOMOLOGACAO/DESENVOLVIMENTO
# =============================================================================
# Este arquivo BLOQUEIA TODOS os crawlers.
#
# Para PRODUCAO (avanzato.com.br), substitua por:
# User-agent: *
# Allow: /
# Sitemap: https://avanzato.com.br/sitemap.xml
# =============================================================================
# ============================================================================
# AVANZATO TECNOLOGIA - ROBOTS.TXT (GENERICO)
# ============================================================================
# O controle real de indexacao e feito pelo <meta name="robots"> no HTML:
# - Producao (avanzato.com.br): index, follow
# - Homologacao (hml.avanzato.com.br): noindex, nofollow
# - Desenvolvimento (localhost): noindex, nofollow
# ============================================================================
User-agent: *
Disallow: /
Allow: /
# Bloqueio explicito de todos os bots
User-agent: Googlebot
Disallow: /
User-agent: Bingbot
Disallow: /
User-agent: Slurp
Disallow: /
# Sitemap comentado (descomente em producao)
# Sitemap: https://avanzato.com.br/sitemap.xml
Sitemap: https://avanzato.com.br/sitemap.xml
+116
View File
@@ -165,4 +165,120 @@
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<!-- SEO Local - Onde Atuamos -->
<url>
<loc>https://avanzato.com.br/onde-atuamos</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-guarulhos</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-mogi-das-cruzes</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-ipiranga</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-vila-formosa</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-cerqueira-cesar</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-santana</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-bras</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-ribeirao-preto</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<!-- Nichos adicionais -->
<url>
<loc>https://avanzato.com.br/suporte-ti-industria</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-portaria-remota</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-clinicas</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-imobiliarias</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-recrutamento-selecao</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<!-- Avanzato Tools -->
<url>
<loc>https://avanzato.com.br/ferramentas</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/spf-checker</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/dkim-checker</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/dmarc-checker</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/mx-lookup</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/ssl-checker</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/email-security-score</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/microsoft-365-analyzer</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
</urlset>
+45 -12
View File
@@ -5,6 +5,36 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- DETECAO DE AMBIENTE - Inline (executa antes de tudo) -->
<script>
(function() {
var h = location.hostname;
var IS_PROD = h === 'avanzato.com.br' || h === 'www.avanzato.com.br';
// Torna disponivel para outros scripts
window.__IS_PROD__ = IS_PROD;
// Se NAO for producao, desativa tracking globalmente
if (!IS_PROD) {
window.__AVANZATO_ENV__ = h === 'hml.avanzato.com.br' ? 'homologation' : 'development';
// Bloqueia gtag
window.dataLayer = { push: function(){} };
window.gtag = function(){};
// Bloqueia fbq (Facebook Pixel)
window.fbq = function(){};
// Bloqueia Mautic
window.MauticSDKLoaded = true;
console.log('[Avanzato] Ambiente: ' + window.__AVANZATO_ENV__ + ' | Tracking bloqueado');
} else {
window.__AVANZATO_ENV__ = 'production';
}
})();
</script>
<!-- Google tag (gtag.js) - Ads + Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=AW-16714813566"></script>
<script>
@@ -49,10 +79,6 @@
fbq('init', '1223271852059785');
fbq('track', 'PageView');
</script>
<noscript>
<img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id=1223271852059785&ev=PageView&noscript=1"/>
</noscript>
<!-- Mautic Tracking -->
<script>
@@ -74,6 +100,7 @@
<title>Avanzato Tecnologia | TI Gerenciada, Cloud e Seguranca em Guarulhos/SP</title>
<meta name="title" content="Avanzato Tecnologia | TI Gerenciada, Cloud e Seguranca em Guarulhos/SP" />
<meta name="description" content="Solucoes em TI Gerenciada, Cloud Computing e Seguranca da Informacao. +23 anos de experiencia, +500 clientes atendidos. Suporte 24/7 em Guarulhos e regiao." />
<meta name="app-version" content="1.0.0" />
<meta name="keywords" content="TI Gerenciada, Cloud Computing, Seguranca da Informacao, Suporte Tecnico, Tecnologia da Informacao, Guarulhos, Sao Paulo, Infraestrutura de TI, Backup, Firewall, Consultoria TI" />
<meta name="author" content="Avanzato Tecnologia" />
<meta name="robots" content="index, follow" id="meta-robots" />
@@ -82,9 +109,9 @@
<!-- BLOQUEAR INDEXACAO EM HOMOLOGACAO -->
<script>
(function() {
var hostname = window.location.hostname;
if (hostname !== 'avanzato.com.br') {
// Homologacao: bloquear indexacao
// Usa IS_PROD ja definido no inicio do documento
if (!window.__IS_PROD__) {
// Homologacao/Desenvolvimento: bloquear indexacao
document.getElementById('meta-robots').setAttribute('content', 'noindex, nofollow');
document.getElementById('meta-googlebot').setAttribute('content', 'noindex, nofollow');
// Adicionar tambem X-Robots-Tag via meta http-equiv
@@ -92,7 +119,9 @@
meta.httpEquiv = 'X-Robots-Tag';
meta.content = 'noindex, nofollow';
document.head.appendChild(meta);
console.log('%c[SEO] Homologacao detectada - indexacao BLOQUEADA (noindex, nofollow)', 'background:#dc2626;color:#fff;font-weight:bold;padding:2px 6px;border-radius:3px');
console.log('%c[SEO] ' + (window.__AVANZATO_ENV__ || 'nao-producao') + ' - indexacao BLOQUEADA', 'background:#dc2626;color:#fff;font-weight:bold;padding:2px 6px;border-radius:3px');
} else {
console.log('%c[SEO] Producao - indexacao PERMITIDA (index, follow)', 'background:#16a34a;color:#fff;font-weight:bold;padding:2px 6px;border-radius:3px');
}
})();
</script>
@@ -117,10 +146,8 @@
<meta property="twitter:image" content="https://avanzato.com.br/og-image.jpg" />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="icon" type="image/png" href="https://avanzato.com.br/favicon.png" />
<link rel="shortcut icon" type="image/png" href="https://avanzato.com.br/favicon.png" />
<link rel="manifest" href="/site.webmanifest" />
<!-- Theme Color -->
@@ -217,6 +244,12 @@
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<!-- Facebook Pixel noscript (moved from head to body to fix parse5 warning) -->
<noscript>
<img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id=1223271852059785&ev=PageView&noscript=1"/>
</noscript>
<!-- Noscript content for SEO -->
<noscript>
<div style="padding: 20px; text-align: center; font-family: Urbanist, Arial, sans-serif; background: #0e0e0e; color: white; min-height: 100vh;">
@@ -0,0 +1,24 @@
# Release PROD
Data: sáb 30 mai 2026 19:30:29 -03
Ambiente:
PROD
Descrição:
Atualização e correção de diretorios
Branch:
main
Commit:
bb73783
Último commit develop:
146652a
Arquivos alterados no último commit:
Status:
Preparado para publicação em produção
@@ -0,0 +1,24 @@
# Release PROD
Data: sáb 30 mai 2026 20:36:09 -03
Ambiente:
PROD
Descrição:
Favivon - Correção
Branch:
main
Commit:
5b729b23
Último commit develop:
643f628a
Arquivos alterados no último commit:
Status:
Preparado para publicação em produção
@@ -0,0 +1,24 @@
# Release PROD
Data: sáb 30 mai 2026 23:15:11 -03
Ambiente:
PROD
Descrição:
Favivon - Correção
Branch:
main
Commit:
e9f47c5b
Último commit develop:
2dc62486
Arquivos alterados no último commit:
Status:
Preparado para publicação em produção
-230
View File
@@ -1,230 +0,0 @@
# Release HML
Data: sáb 30 mai 2026 23:52:07 -03
Ambiente:
HML
Descrição:
Revisão logo
Branch:
develop
Arquivos alterados antes do commit:
D DEPLOY-DOCKER.md
D GUIA-NGINX.md
M index.html
D infra/releases/2026-05-30_19-57-59_HML.md
D infra/releases/2026-05-30_23-14-47_HML.md
D nginx-docker.conf
M node_modules/.package-lock.json
M node_modules/.vite/deps/@radix-ui_react-dialog.js
M node_modules/.vite/deps/@radix-ui_react-label.js
M node_modules/.vite/deps/@radix-ui_react-radio-group.js
M node_modules/.vite/deps/@radix-ui_react-select.js
M node_modules/.vite/deps/_metadata.json
D node_modules/.vite/deps/chunk-D4MYACAJ.js
D node_modules/.vite/deps/chunk-D4MYACAJ.js.map
D node_modules/.vite/deps/chunk-KKEH7IGY.js
D node_modules/.vite/deps/chunk-KKEH7IGY.js.map
D node_modules/.vite/deps/chunk-QF76HVLY.js
D node_modules/.vite/deps/chunk-QF76HVLY.js.map
D node_modules/.vite/deps/chunk-X34PLQQA.js
D node_modules/.vite/deps/chunk-X34PLQQA.js.map
D node_modules/.vite/deps/react-helmet-async.js
D node_modules/.vite/deps/react-helmet-async.js.map
D node_modules/invariant/CHANGELOG.md
D node_modules/invariant/LICENSE
D node_modules/invariant/README.md
D node_modules/invariant/browser.js
D node_modules/invariant/invariant.js
D node_modules/invariant/invariant.js.flow
D node_modules/invariant/package.json
D node_modules/react-fast-compare/LICENSE
D node_modules/react-fast-compare/README.md
D node_modules/react-fast-compare/index.d.ts
D node_modules/react-fast-compare/index.js
D node_modules/react-fast-compare/package.json
D node_modules/react-helmet-async/LICENSE
D node_modules/react-helmet-async/README.md
D node_modules/react-helmet-async/lib/Dispatcher.d.ts
D node_modules/react-helmet-async/lib/HelmetData.d.ts
D node_modules/react-helmet-async/lib/Provider.d.ts
D node_modules/react-helmet-async/lib/React19Dispatcher.d.ts
D node_modules/react-helmet-async/lib/client.d.ts
D node_modules/react-helmet-async/lib/constants.d.ts
D node_modules/react-helmet-async/lib/index.d.ts
D node_modules/react-helmet-async/lib/index.esm.js
D node_modules/react-helmet-async/lib/index.js
D node_modules/react-helmet-async/lib/reactVersion.d.ts
D node_modules/react-helmet-async/lib/server.d.ts
D node_modules/react-helmet-async/lib/types.d.ts
D node_modules/react-helmet-async/lib/utils.d.ts
D node_modules/react-helmet-async/package.json
D node_modules/shallowequal/LICENSE
D node_modules/shallowequal/README.md
D node_modules/shallowequal/index.js
D node_modules/shallowequal/index.js.flow
D node_modules/shallowequal/index.original.js
D node_modules/shallowequal/package.json
M package-lock.json
M package.json
M public/sitemap.xml
D public/version.json
D scripts/build-static-routes.sh
M scripts/dev.sh
M src/App.tsx
D src/components/SEO.tsx
M src/config/site.ts
M src/main.tsx
M src/pages/Blog.tsx
M src/pages/Cases.tsx
M src/pages/Condominios.tsx
M src/pages/Privacidade.tsx
M src/pages/Sobre.tsx
M src/pages/Tecnologias.tsx
M src/pages/Termos.tsx
M src/pages/nichos/Advocacia.tsx
M src/pages/nichos/BackupCorporativo.tsx
M src/pages/nichos/Contabilidade.tsx
M src/pages/nichos/FirewallPfSense.tsx
M src/pages/nichos/Monitoramento.tsx
M src/pages/nichos/VoipEmpresarial.tsx
M src/pages/services/Backup.tsx
M src/pages/services/CloudComputing.tsx
M src/pages/services/Consultoria.tsx
M src/pages/services/Noc.tsx
M src/pages/services/Pabx.tsx
M src/pages/services/Redes.tsx
M src/pages/services/Seguranca.tsx
M src/pages/services/Servidores.tsx
M src/pages/services/Suporte.tsx
M src/pages/services/TiGerenciada.tsx
M src/pages/tech/Cisco.tsx
M src/pages/tech/Linux.tsx
M src/pages/tech/PfSense.tsx
M src/pages/tech/Proxmox.tsx
M src/pages/tech/Sharepoint.tsx
M src/pages/tech/Vmware.tsx
M src/pages/tech/WindowsServer.tsx
M src/sections/Footer.tsx
M src/sections/Navigation.tsx
M vite.config.ts
?? .github/
?? .gitignore
?? infra/releases/2026-05-30_23-52-07_HML.md
Status:
Preparado para publicação
Commit:
b3b8917a
Arquivos no commit:
.github/workflows/README-CI-CD.md
.github/workflows/deploy.yml
.gitignore
DEPLOY-DOCKER.md
GUIA-NGINX.md
index.html
infra/releases/2026-05-30_19-57-59_HML.md
infra/releases/2026-05-30_23-14-47_HML.md
infra/releases/2026-05-30_23-52-07_HML.md
nginx-docker.conf
node_modules/.package-lock.json
node_modules/.vite/deps/@radix-ui_react-dialog.js
node_modules/.vite/deps/@radix-ui_react-label.js
node_modules/.vite/deps/@radix-ui_react-radio-group.js
node_modules/.vite/deps/@radix-ui_react-select.js
node_modules/.vite/deps/_metadata.json
node_modules/.vite/deps/chunk-D4MYACAJ.js
node_modules/.vite/deps/chunk-D4MYACAJ.js.map
node_modules/.vite/deps/chunk-KKEH7IGY.js
node_modules/.vite/deps/chunk-KKEH7IGY.js.map
node_modules/.vite/deps/chunk-QF76HVLY.js
node_modules/.vite/deps/chunk-QF76HVLY.js.map
node_modules/.vite/deps/chunk-X34PLQQA.js
node_modules/.vite/deps/chunk-X34PLQQA.js.map
node_modules/.vite/deps/react-helmet-async.js
node_modules/.vite/deps/react-helmet-async.js.map
node_modules/invariant/CHANGELOG.md
node_modules/invariant/LICENSE
node_modules/invariant/README.md
node_modules/invariant/browser.js
node_modules/invariant/invariant.js
node_modules/invariant/invariant.js.flow
node_modules/invariant/package.json
node_modules/react-fast-compare/LICENSE
node_modules/react-fast-compare/README.md
node_modules/react-fast-compare/index.d.ts
node_modules/react-fast-compare/index.js
node_modules/react-fast-compare/package.json
node_modules/react-helmet-async/LICENSE
node_modules/react-helmet-async/README.md
node_modules/react-helmet-async/lib/Dispatcher.d.ts
node_modules/react-helmet-async/lib/HelmetData.d.ts
node_modules/react-helmet-async/lib/Provider.d.ts
node_modules/react-helmet-async/lib/React19Dispatcher.d.ts
node_modules/react-helmet-async/lib/client.d.ts
node_modules/react-helmet-async/lib/constants.d.ts
node_modules/react-helmet-async/lib/index.d.ts
node_modules/react-helmet-async/lib/index.esm.js
node_modules/react-helmet-async/lib/index.js
node_modules/react-helmet-async/lib/reactVersion.d.ts
node_modules/react-helmet-async/lib/server.d.ts
node_modules/react-helmet-async/lib/types.d.ts
node_modules/react-helmet-async/lib/utils.d.ts
node_modules/react-helmet-async/package.json
node_modules/shallowequal/LICENSE
node_modules/shallowequal/README.md
node_modules/shallowequal/index.js
node_modules/shallowequal/index.js.flow
node_modules/shallowequal/index.original.js
node_modules/shallowequal/package.json
package-lock.json
package.json
public/sitemap.xml
public/version.json
scripts/build-static-routes.sh
scripts/dev.sh
src/App.tsx
src/components/SEO.tsx
src/config/site.ts
src/main.tsx
src/pages/Blog.tsx
src/pages/Cases.tsx
src/pages/Condominios.tsx
src/pages/Privacidade.tsx
src/pages/Sobre.tsx
src/pages/Tecnologias.tsx
src/pages/Termos.tsx
src/pages/nichos/Advocacia.tsx
src/pages/nichos/BackupCorporativo.tsx
src/pages/nichos/Contabilidade.tsx
src/pages/nichos/FirewallPfSense.tsx
src/pages/nichos/Monitoramento.tsx
src/pages/nichos/VoipEmpresarial.tsx
src/pages/services/Backup.tsx
src/pages/services/CloudComputing.tsx
src/pages/services/Consultoria.tsx
src/pages/services/Noc.tsx
src/pages/services/Pabx.tsx
src/pages/services/Redes.tsx
src/pages/services/Seguranca.tsx
src/pages/services/Servidores.tsx
src/pages/services/Suporte.tsx
src/pages/services/TiGerenciada.tsx
src/pages/tech/Cisco.tsx
src/pages/tech/Linux.tsx
src/pages/tech/PfSense.tsx
src/pages/tech/Proxmox.tsx
src/pages/tech/Sharepoint.tsx
src/pages/tech/Vmware.tsx
src/pages/tech/WindowsServer.tsx
src/sections/Footer.tsx
src/sections/Navigation.tsx
vite.config.ts
Status final:
Publicado no Gitea / aguardando pipeline HML
@@ -0,0 +1,24 @@
# Release PROD
Data: dom 31 mai 2026 00:03:02 -03
Ambiente:
PROD
Descrição:
Revisão Logo, Favicon, Robots
Branch:
main
Commit:
b284c4d2
Último commit develop:
3d36e981
Arquivos alterados no último commit:
Status:
Preparado para publicação em produção
@@ -0,0 +1,24 @@
# Release PROD
Data: dom 31 mai 2026 00:29:03 -03
Ambiente:
PROD
Descrição:
Revisão Logo, Favicon, Robots
Branch:
main
Commit:
bfb02286
Último commit develop:
be22792b
Arquivos alterados no último commit:
Status:
Preparado para publicação em produção
@@ -0,0 +1,24 @@
# Release PROD
Data: dom 31 mai 2026 00:58:08 -03
Ambiente:
PROD
Descrição:
Revisão Logo, Favicon, Robots
Branch:
main
Commit:
9885f59b
Último commit develop:
d79c9c48
Arquivos alterados no último commit:
Status:
Preparado para publicação em produção
@@ -0,0 +1,24 @@
# Release PROD
Data: seg 1 jun 2026 19:44:45 -03
Ambiente:
PROD
Descrição:
Paginas e SEO master
Branch:
main
Commit:
4f15851f
Último commit develop:
15ce64e7
Arquivos alterados no último commit:
Status:
Preparado para publicação em produção
@@ -0,0 +1,24 @@
# Release PROD
Data: seg 1 jun 2026 19:56:50 -03
Ambiente:
PROD
Descrição:
Paginas e SEO master
Branch:
main
Commit:
3eb41133
Último commit develop:
e8f15d63
Arquivos alterados no último commit:
Status:
Preparado para publicação em produção
@@ -0,0 +1,24 @@
# Release PROD
Data: seg 1 jun 2026 20:28:34 -03
Ambiente:
PROD
Descrição:
Paginas e SEO master
Branch:
main
Commit:
52f86cbb
Último commit develop:
e8f15d63
Arquivos alterados no último commit:
infra/releases/2026-06-01_19-56-50_PROD.md
Status:
Preparado para publicação em produção
@@ -0,0 +1,24 @@
# Release PROD
Data: ter 2 jun 2026 14:21:07 -03
Ambiente:
PROD
Descrição:
Paginas e SEO master, correção javascript google
Branch:
main
Commit:
35e5727e
Último commit develop:
96563b30
Arquivos alterados no último commit:
Status:
Preparado para publicação em produção
+57
View File
@@ -0,0 +1,57 @@
# Release HML
Data: qua 3 jun 2026 22:07:59 -03
Ambiente:
HML
Descrição:
Aplicativos de Atrair Leads
Branch:
develop
Arquivos alterados antes do commit:
M dist/index.html
D infra/releases/2026-06-02_22-34-39_HML.md
M src/App.tsx
M src/sections/About.tsx
M src/sections/CondominiosCTA.tsx
M src/sections/Hero.tsx
M src/sections/Navigation.tsx
M src/sections/Services.tsx
?? infra/releases/2026-06-03_22-07-58_HML.md
?? nginx/
?? public/about-image.webp
?? public/hero-dashboard.webp
?? public/service-cloud.webp
?? public/service-security.webp
?? public/service-support.webp
?? public/service-ti.webp
Status:
Preparado para publicação
Commit:
fdfb4606
Arquivos no commit:
dist/index.html
infra/releases/2026-06-02_22-34-39_HML.md
infra/releases/2026-06-03_22-07-58_HML.md
nginx/avanzato.conf
public/about-image.webp
public/hero-dashboard.webp
public/service-cloud.webp
public/service-security.webp
public/service-support.webp
public/service-ti.webp
src/App.tsx
src/sections/About.tsx
src/sections/CondominiosCTA.tsx
src/sections/Hero.tsx
src/sections/Navigation.tsx
src/sections/Services.tsx
Status final:
Publicado no Gitea / aguardando pipeline HML
@@ -0,0 +1,24 @@
# Release PROD
Data: qua 3 jun 2026 22:19:56 -03
Ambiente:
PROD
Descrição:
Aplicativos de Atrair Leads
Branch:
main
Commit:
667447c3
Último commit develop:
e3f35d80
Arquivos alterados no último commit:
Status:
Preparado para publicação em produção
+30
View File
@@ -0,0 +1,30 @@
# ============================================================================
# AVANZATO - Configuracao Nginx para Docker
# ============================================================================
# Coloque este arquivo em: /etc/nginx/conf.d/default.conf
# dentro do container Docker
# ============================================================================
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# === CORRECAO CRITICA: SPA React ===
# Redireciona TODAS as rotas para index.html
location / {
try_files $uri $uri/ /index.html;
}
# Cache para assets estaticos
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 6M;
add_header Cache-Control "public, immutable";
}
# Seguranca
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
}
+132
View File
@@ -0,0 +1,132 @@
# ============================================================================
# Configuracao Nginx - Avanzato Tecnologia
# ============================================================================
# Inclui: CSP, HSTS, X-Frame-Options, cache headers, gzip, brotli
# Copiar para: /etc/nginx/sites-available/avanzato.com.br
# Ativar: ln -s /etc/nginx/sites-available/avanzato.com.br /etc/nginx/sites-enabled/
# ============================================================================
server {
listen 80;
listen [::]:80;
server_name avanzato.com.br www.avanzato.com.br;
# Redirecionar HTTP para HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name avanzato.com.br www.avanzato.com.br;
# SSL (certbot gerencia automaticamente)
ssl_certificate /etc/letsencrypt/live/avanzato.com.br/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/avanzato.com.br/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/avanzato.com.br/chain.pem;
# SSL hardening
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTS - HTTP Strict Transport Security
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Root do projeto
root /var/www/avanzato/dist;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml
font/woff2;
# ============================================================================
# CACHE HEADERS - Assets estaticos
# ============================================================================
# Imagens, fontes, videos - 1 mes
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot|mp4|webm)$ {
expires 1M;
add_header Cache-Control "public, immutable" always;
add_header Vary "Accept-Encoding" always;
access_log off;
}
# CSS e JS - 1 ano (tem hash no nome)
location ~* \.(css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable" always;
add_header Vary "Accept-Encoding" always;
access_log off;
}
# HTML - nao cachear
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header Pragma "no-cache" always;
}
# ============================================================================
# SECURITY HEADERS
# ============================================================================
# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://connect.facebook.net https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://dns.google https://crt.sh https://mkt.avanzato.com.br https://www.google-analytics.com https://www.facebook.com; frame-src 'self' https://mkt.avanzato.com.br; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests;" always;
# X-Frame-Options - protecao contra clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;
# X-Content-Type-Options
add_header X-Content-Type-Options "nosniff" always;
# Referrer Policy
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Permissions Policy
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=()" always;
# Cross-Origin Opener Policy
add_header Cross-Origin-Opener-Policy "same-origin" always;
# Cross-Origin Resource Policy
add_header Cross-Origin-Resource-Policy "cross-origin" always;
# ============================================================================
# REACT ROUTER - SPA fallback
# ============================================================================
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
}
# Sitemap e robots - acessiveis
location = /sitemap.xml {
try_files $uri =404;
add_header Content-Type "application/xml" always;
}
location = /robots.txt {
try_files $uri =404;
add_header Content-Type "text/plain" always;
}
}
+35
View File
@@ -5904,6 +5904,15 @@
"node": ">=12"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmmirror.com/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -6828,6 +6837,26 @@
"react": "^19.2.3"
}
},
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
"license": "MIT"
},
"node_modules/react-helmet-async": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/react-helmet-async/-/react-helmet-async-3.0.0.tgz",
"integrity": "sha512-nA3IEZfXiclgrz4KLxAhqJqIfFDuvzQwlKwpdmzZIuC1KNSghDEIXmyU0TKtbM+NafnkICcwx8CECFrZ/sL/1w==",
"license": "Apache-2.0",
"dependencies": {
"invariant": "^2.2.4",
"react-fast-compare": "^3.2.2",
"shallowequal": "^1.1.0"
},
"peerDependencies": {
"react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-hook-form": {
"version": "7.70.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.70.0.tgz",
@@ -7291,6 +7320,12 @@
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -1,7 +1,7 @@
"use client";
import {
Presence
} from "./chunk-3Q7Z2X7T.js";
} from "./chunk-INCCO7S4.js";
import {
Combination_default,
DismissableLayer,
@@ -9,7 +9,7 @@ import {
Portal,
hideOthers,
useFocusGuards
} from "./chunk-26QBR6Z3.js";
} from "./chunk-LIWCVBQK.js";
import {
Primitive,
composeEventHandlers,
@@ -17,7 +17,7 @@ import {
createContextScope,
useControllableState,
useId
} from "./chunk-2ZZW3GAI.js";
} from "./chunk-MOFDO34C.js";
import {
composeRefs,
useComposedRefs
+3 -3
View File
@@ -1,13 +1,13 @@
"use client";
import {
Presence
} from "./chunk-3Q7Z2X7T.js";
} from "./chunk-INCCO7S4.js";
import {
createCollection,
useDirection,
usePrevious,
useSize
} from "./chunk-5BI4FO63.js";
} from "./chunk-L2OGOWTU.js";
import {
Primitive,
composeEventHandlers,
@@ -15,7 +15,7 @@ import {
useCallbackRef,
useControllableState,
useId
} from "./chunk-2ZZW3GAI.js";
} from "./chunk-MOFDO34C.js";
import {
useComposedRefs
} from "./chunk-2VUH7NEY.js";
+8 -8
View File
@@ -1,10 +1,4 @@
"use client";
import {
createCollection,
useDirection,
usePrevious,
useSize
} from "./chunk-5BI4FO63.js";
import {
Combination_default,
DismissableLayer,
@@ -12,7 +6,13 @@ import {
Portal,
hideOthers,
useFocusGuards
} from "./chunk-26QBR6Z3.js";
} from "./chunk-LIWCVBQK.js";
import {
createCollection,
useDirection,
usePrevious,
useSize
} from "./chunk-L2OGOWTU.js";
import {
Primitive,
composeEventHandlers,
@@ -21,7 +21,7 @@ import {
useControllableState,
useId,
useLayoutEffect2
} from "./chunk-2ZZW3GAI.js";
} from "./chunk-MOFDO34C.js";
import {
composeRefs,
useComposedRefs
+34 -28
View File
@@ -1,109 +1,115 @@
{
"hash": "baa9bd3c",
"hash": "bef2a67d",
"configHash": "eccf13b4",
"lockfileHash": "f25e87f1",
"browserHash": "4d672fc5",
"lockfileHash": "c4c8a51c",
"browserHash": "e7109be1",
"optimized": {
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "2adf36c9",
"fileHash": "dd97ceba",
"needsInterop": true
},
"react-dom": {
"src": "../../react-dom/index.js",
"file": "react-dom.js",
"fileHash": "91ce8077",
"fileHash": "8becf687",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "051319e1",
"fileHash": "2c7217d2",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "1945fc54",
"fileHash": "e28ec614",
"needsInterop": true
},
"@radix-ui/react-dialog": {
"src": "../../@radix-ui/react-dialog/dist/index.mjs",
"file": "@radix-ui_react-dialog.js",
"fileHash": "8be1d102",
"fileHash": "22041c72",
"needsInterop": false
},
"@radix-ui/react-label": {
"src": "../../@radix-ui/react-label/dist/index.mjs",
"file": "@radix-ui_react-label.js",
"fileHash": "7c8329ad",
"fileHash": "d33ac0ed",
"needsInterop": false
},
"@radix-ui/react-radio-group": {
"src": "../../@radix-ui/react-radio-group/dist/index.mjs",
"file": "@radix-ui_react-radio-group.js",
"fileHash": "c1cf0673",
"fileHash": "2be9d4b7",
"needsInterop": false
},
"@radix-ui/react-select": {
"src": "../../@radix-ui/react-select/dist/index.mjs",
"file": "@radix-ui_react-select.js",
"fileHash": "e8a77682",
"fileHash": "51dd47da",
"needsInterop": false
},
"@radix-ui/react-slot": {
"src": "../../@radix-ui/react-slot/dist/index.mjs",
"file": "@radix-ui_react-slot.js",
"fileHash": "4581ea7a",
"fileHash": "32dfa860",
"needsInterop": false
},
"class-variance-authority": {
"src": "../../class-variance-authority/dist/index.mjs",
"file": "class-variance-authority.js",
"fileHash": "e840c683",
"fileHash": "f4a8c6f4",
"needsInterop": false
},
"clsx": {
"src": "../../clsx/dist/clsx.mjs",
"file": "clsx.js",
"fileHash": "79cbbdef",
"fileHash": "e5a299ae",
"needsInterop": false
},
"gsap": {
"src": "../../gsap/index.js",
"file": "gsap.js",
"fileHash": "096a1c72",
"fileHash": "76791308",
"needsInterop": false
},
"gsap/ScrollTrigger": {
"src": "../../gsap/ScrollTrigger.js",
"file": "gsap_ScrollTrigger.js",
"fileHash": "05527b31",
"fileHash": "187ec0aa",
"needsInterop": false
},
"lucide-react": {
"src": "../../lucide-react/dist/esm/lucide-react.js",
"file": "lucide-react.js",
"fileHash": "4c9ad860",
"fileHash": "58304198",
"needsInterop": false
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "93025179",
"fileHash": "b30625c8",
"needsInterop": true
},
"react-helmet-async": {
"src": "../../react-helmet-async/lib/index.esm.js",
"file": "react-helmet-async.js",
"fileHash": "79a82144",
"needsInterop": false
},
"react-router-dom": {
"src": "../../react-router-dom/dist/index.mjs",
"file": "react-router-dom.js",
"fileHash": "a7f80be8",
"fileHash": "4427109e",
"needsInterop": false
},
"tailwind-merge": {
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
"file": "tailwind-merge.js",
"fileHash": "e87c9277",
"fileHash": "9511be49",
"needsInterop": false
}
},
@@ -114,17 +120,17 @@
"chunk-YWBEB5PG": {
"file": "chunk-YWBEB5PG.js"
},
"chunk-3Q7Z2X7T": {
"file": "chunk-3Q7Z2X7T.js"
"chunk-INCCO7S4": {
"file": "chunk-INCCO7S4.js"
},
"chunk-5BI4FO63": {
"file": "chunk-5BI4FO63.js"
"chunk-LIWCVBQK": {
"file": "chunk-LIWCVBQK.js"
},
"chunk-26QBR6Z3": {
"file": "chunk-26QBR6Z3.js"
"chunk-L2OGOWTU": {
"file": "chunk-L2OGOWTU.js"
},
"chunk-2ZZW3GAI": {
"file": "chunk-2ZZW3GAI.js"
"chunk-MOFDO34C": {
"file": "chunk-MOFDO34C.js"
},
"chunk-2VUH7NEY": {
"file": "chunk-2VUH7NEY.js"
+143
View File
@@ -0,0 +1,143 @@
import {
useLayoutEffect2
} from "./chunk-MOFDO34C.js";
import {
useComposedRefs
} from "./chunk-2VUH7NEY.js";
import {
require_react
} from "./chunk-WUR7D6NS.js";
import {
__toESM
} from "./chunk-G3PMV62Z.js";
// node_modules/@radix-ui/react-presence/dist/index.mjs
var React2 = __toESM(require_react(), 1);
var React = __toESM(require_react(), 1);
function useStateMachine(initialState, machine) {
return React.useReducer((state, event) => {
const nextState = machine[state][event];
return nextState ?? state;
}, initialState);
}
var Presence = (props) => {
const { present, children } = props;
const presence = usePresence(present);
const child = typeof children === "function" ? children({ present: presence.isPresent }) : React2.Children.only(children);
const ref = useComposedRefs(presence.ref, getElementRef(child));
const forceMount = typeof children === "function";
return forceMount || presence.isPresent ? React2.cloneElement(child, { ref }) : null;
};
Presence.displayName = "Presence";
function usePresence(present) {
const [node, setNode] = React2.useState();
const stylesRef = React2.useRef(null);
const prevPresentRef = React2.useRef(present);
const prevAnimationNameRef = React2.useRef("none");
const initialState = present ? "mounted" : "unmounted";
const [state, send] = useStateMachine(initialState, {
mounted: {
UNMOUNT: "unmounted",
ANIMATION_OUT: "unmountSuspended"
},
unmountSuspended: {
MOUNT: "mounted",
ANIMATION_END: "unmounted"
},
unmounted: {
MOUNT: "mounted"
}
});
React2.useEffect(() => {
const currentAnimationName = getAnimationName(stylesRef.current);
prevAnimationNameRef.current = state === "mounted" ? currentAnimationName : "none";
}, [state]);
useLayoutEffect2(() => {
const styles = stylesRef.current;
const wasPresent = prevPresentRef.current;
const hasPresentChanged = wasPresent !== present;
if (hasPresentChanged) {
const prevAnimationName = prevAnimationNameRef.current;
const currentAnimationName = getAnimationName(styles);
if (present) {
send("MOUNT");
} else if (currentAnimationName === "none" || styles?.display === "none") {
send("UNMOUNT");
} else {
const isAnimating = prevAnimationName !== currentAnimationName;
if (wasPresent && isAnimating) {
send("ANIMATION_OUT");
} else {
send("UNMOUNT");
}
}
prevPresentRef.current = present;
}
}, [present, send]);
useLayoutEffect2(() => {
if (node) {
let timeoutId;
const ownerWindow = node.ownerDocument.defaultView ?? window;
const handleAnimationEnd = (event) => {
const currentAnimationName = getAnimationName(stylesRef.current);
const isCurrentAnimation = currentAnimationName.includes(CSS.escape(event.animationName));
if (event.target === node && isCurrentAnimation) {
send("ANIMATION_END");
if (!prevPresentRef.current) {
const currentFillMode = node.style.animationFillMode;
node.style.animationFillMode = "forwards";
timeoutId = ownerWindow.setTimeout(() => {
if (node.style.animationFillMode === "forwards") {
node.style.animationFillMode = currentFillMode;
}
});
}
}
};
const handleAnimationStart = (event) => {
if (event.target === node) {
prevAnimationNameRef.current = getAnimationName(stylesRef.current);
}
};
node.addEventListener("animationstart", handleAnimationStart);
node.addEventListener("animationcancel", handleAnimationEnd);
node.addEventListener("animationend", handleAnimationEnd);
return () => {
ownerWindow.clearTimeout(timeoutId);
node.removeEventListener("animationstart", handleAnimationStart);
node.removeEventListener("animationcancel", handleAnimationEnd);
node.removeEventListener("animationend", handleAnimationEnd);
};
} else {
send("ANIMATION_END");
}
}, [node, send]);
return {
isPresent: ["mounted", "unmountSuspended"].includes(state),
ref: React2.useCallback((node2) => {
stylesRef.current = node2 ? getComputedStyle(node2) : null;
setNode(node2);
}, [])
};
}
function getAnimationName(styles) {
return styles?.animationName || "none";
}
function getElementRef(element) {
let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get;
let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
if (mayWarn) {
return element.ref;
}
getter = Object.getOwnPropertyDescriptor(element, "ref")?.get;
mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
if (mayWarn) {
return element.props.ref;
}
return element.props.ref || element.ref;
}
export {
Presence
};
//# sourceMappingURL=chunk-INCCO7S4.js.map
File diff suppressed because one or more lines are too long
+248
View File
@@ -0,0 +1,248 @@
import {
createContextScope,
useLayoutEffect2
} from "./chunk-MOFDO34C.js";
import {
composeRefs,
useComposedRefs
} from "./chunk-2VUH7NEY.js";
import {
require_jsx_runtime
} from "./chunk-2YVA4HRZ.js";
import {
require_react
} from "./chunk-WUR7D6NS.js";
import {
__toESM
} from "./chunk-G3PMV62Z.js";
// node_modules/@radix-ui/react-collection/dist/index.mjs
var import_react = __toESM(require_react(), 1);
// node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot/dist/index.mjs
var React = __toESM(require_react(), 1);
var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
function createSlot(ownerName) {
const SlotClone = createSlotClone(ownerName);
const Slot2 = React.forwardRef((props, forwardedRef) => {
const { children, ...slotProps } = props;
const childrenArray = React.Children.toArray(children);
const slottable = childrenArray.find(isSlottable);
if (slottable) {
const newElement = slottable.props.children;
const newChildren = childrenArray.map((child) => {
if (child === slottable) {
if (React.Children.count(newElement) > 1) return React.Children.only(null);
return React.isValidElement(newElement) ? newElement.props.children : null;
} else {
return child;
}
});
return (0, import_jsx_runtime.jsx)(SlotClone, { ...slotProps, ref: forwardedRef, children: React.isValidElement(newElement) ? React.cloneElement(newElement, void 0, newChildren) : null });
}
return (0, import_jsx_runtime.jsx)(SlotClone, { ...slotProps, ref: forwardedRef, children });
});
Slot2.displayName = `${ownerName}.Slot`;
return Slot2;
}
var Slot = createSlot("Slot");
function createSlotClone(ownerName) {
const SlotClone = React.forwardRef((props, forwardedRef) => {
const { children, ...slotProps } = props;
if (React.isValidElement(children)) {
const childrenRef = getElementRef(children);
const props2 = mergeProps(slotProps, children.props);
if (children.type !== React.Fragment) {
props2.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef;
}
return React.cloneElement(children, props2);
}
return React.Children.count(children) > 1 ? React.Children.only(null) : null;
});
SlotClone.displayName = `${ownerName}.SlotClone`;
return SlotClone;
}
var SLOTTABLE_IDENTIFIER = /* @__PURE__ */ Symbol("radix.slottable");
function createSlottable(ownerName) {
const Slottable2 = ({ children }) => {
return (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
};
Slottable2.displayName = `${ownerName}.Slottable`;
Slottable2.__radixId = SLOTTABLE_IDENTIFIER;
return Slottable2;
}
var Slottable = createSlottable("Slottable");
function isSlottable(child) {
return React.isValidElement(child) && typeof child.type === "function" && "__radixId" in child.type && child.type.__radixId === SLOTTABLE_IDENTIFIER;
}
function mergeProps(slotProps, childProps) {
const overrideProps = { ...childProps };
for (const propName in childProps) {
const slotPropValue = slotProps[propName];
const childPropValue = childProps[propName];
const isHandler = /^on[A-Z]/.test(propName);
if (isHandler) {
if (slotPropValue && childPropValue) {
overrideProps[propName] = (...args) => {
const result = childPropValue(...args);
slotPropValue(...args);
return result;
};
} else if (slotPropValue) {
overrideProps[propName] = slotPropValue;
}
} else if (propName === "style") {
overrideProps[propName] = { ...slotPropValue, ...childPropValue };
} else if (propName === "className") {
overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(" ");
}
}
return { ...slotProps, ...overrideProps };
}
function getElementRef(element) {
let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get;
let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
if (mayWarn) {
return element.ref;
}
getter = Object.getOwnPropertyDescriptor(element, "ref")?.get;
mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
if (mayWarn) {
return element.props.ref;
}
return element.props.ref || element.ref;
}
// node_modules/@radix-ui/react-collection/dist/index.mjs
var import_jsx_runtime2 = __toESM(require_jsx_runtime(), 1);
var import_react2 = __toESM(require_react(), 1);
var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
function createCollection(name) {
const PROVIDER_NAME = name + "CollectionProvider";
const [createCollectionContext, createCollectionScope] = createContextScope(PROVIDER_NAME);
const [CollectionProviderImpl, useCollectionContext] = createCollectionContext(
PROVIDER_NAME,
{ collectionRef: { current: null }, itemMap: /* @__PURE__ */ new Map() }
);
const CollectionProvider = (props) => {
const { scope, children } = props;
const ref = import_react.default.useRef(null);
const itemMap = import_react.default.useRef(/* @__PURE__ */ new Map()).current;
return (0, import_jsx_runtime2.jsx)(CollectionProviderImpl, { scope, itemMap, collectionRef: ref, children });
};
CollectionProvider.displayName = PROVIDER_NAME;
const COLLECTION_SLOT_NAME = name + "CollectionSlot";
const CollectionSlotImpl = createSlot(COLLECTION_SLOT_NAME);
const CollectionSlot = import_react.default.forwardRef(
(props, forwardedRef) => {
const { scope, children } = props;
const context = useCollectionContext(COLLECTION_SLOT_NAME, scope);
const composedRefs = useComposedRefs(forwardedRef, context.collectionRef);
return (0, import_jsx_runtime2.jsx)(CollectionSlotImpl, { ref: composedRefs, children });
}
);
CollectionSlot.displayName = COLLECTION_SLOT_NAME;
const ITEM_SLOT_NAME = name + "CollectionItemSlot";
const ITEM_DATA_ATTR = "data-radix-collection-item";
const CollectionItemSlotImpl = createSlot(ITEM_SLOT_NAME);
const CollectionItemSlot = import_react.default.forwardRef(
(props, forwardedRef) => {
const { scope, children, ...itemData } = props;
const ref = import_react.default.useRef(null);
const composedRefs = useComposedRefs(forwardedRef, ref);
const context = useCollectionContext(ITEM_SLOT_NAME, scope);
import_react.default.useEffect(() => {
context.itemMap.set(ref, { ref, ...itemData });
return () => void context.itemMap.delete(ref);
});
return (0, import_jsx_runtime2.jsx)(CollectionItemSlotImpl, { ...{ [ITEM_DATA_ATTR]: "" }, ref: composedRefs, children });
}
);
CollectionItemSlot.displayName = ITEM_SLOT_NAME;
function useCollection(scope) {
const context = useCollectionContext(name + "CollectionConsumer", scope);
const getItems = import_react.default.useCallback(() => {
const collectionNode = context.collectionRef.current;
if (!collectionNode) return [];
const orderedNodes = Array.from(collectionNode.querySelectorAll(`[${ITEM_DATA_ATTR}]`));
const items = Array.from(context.itemMap.values());
const orderedItems = items.sort(
(a, b) => orderedNodes.indexOf(a.ref.current) - orderedNodes.indexOf(b.ref.current)
);
return orderedItems;
}, [context.collectionRef, context.itemMap]);
return getItems;
}
return [
{ Provider: CollectionProvider, Slot: CollectionSlot, ItemSlot: CollectionItemSlot },
useCollection,
createCollectionScope
];
}
// node_modules/@radix-ui/react-direction/dist/index.mjs
var React3 = __toESM(require_react(), 1);
var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1);
var DirectionContext = React3.createContext(void 0);
function useDirection(localDir) {
const globalDir = React3.useContext(DirectionContext);
return localDir || globalDir || "ltr";
}
// node_modules/@radix-ui/react-use-size/dist/index.mjs
var React4 = __toESM(require_react(), 1);
function useSize(element) {
const [size, setSize] = React4.useState(void 0);
useLayoutEffect2(() => {
if (element) {
setSize({ width: element.offsetWidth, height: element.offsetHeight });
const resizeObserver = new ResizeObserver((entries) => {
if (!Array.isArray(entries)) {
return;
}
if (!entries.length) {
return;
}
const entry = entries[0];
let width;
let height;
if ("borderBoxSize" in entry) {
const borderSizeEntry = entry["borderBoxSize"];
const borderSize = Array.isArray(borderSizeEntry) ? borderSizeEntry[0] : borderSizeEntry;
width = borderSize["inlineSize"];
height = borderSize["blockSize"];
} else {
width = element.offsetWidth;
height = element.offsetHeight;
}
setSize({ width, height });
});
resizeObserver.observe(element, { box: "border-box" });
return () => resizeObserver.unobserve(element);
} else {
setSize(void 0);
}
}, [element]);
return size;
}
// node_modules/@radix-ui/react-use-previous/dist/index.mjs
var React5 = __toESM(require_react(), 1);
function usePrevious(value) {
const ref = React5.useRef({ value, previous: value });
return React5.useMemo(() => {
if (ref.current.value !== value) {
ref.current.previous = ref.current.value;
ref.current.value = value;
}
return ref.current.previous;
}, [value]);
}
export {
createCollection,
useDirection,
useSize,
usePrevious
};
//# sourceMappingURL=chunk-L2OGOWTU.js.map
File diff suppressed because one or more lines are too long
+1352
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+354
View File
@@ -0,0 +1,354 @@
import {
composeRefs
} from "./chunk-2VUH7NEY.js";
import {
require_react_dom
} from "./chunk-YF4B4G2L.js";
import {
require_jsx_runtime
} from "./chunk-2YVA4HRZ.js";
import {
require_react
} from "./chunk-WUR7D6NS.js";
import {
__toESM
} from "./chunk-G3PMV62Z.js";
// node_modules/@radix-ui/primitive/dist/index.mjs
var canUseDOM = !!(typeof window !== "undefined" && window.document && window.document.createElement);
function composeEventHandlers(originalEventHandler, ourEventHandler, { checkForDefaultPrevented = true } = {}) {
return function handleEvent(event) {
originalEventHandler?.(event);
if (checkForDefaultPrevented === false || !event.defaultPrevented) {
return ourEventHandler?.(event);
}
};
}
// node_modules/@radix-ui/react-context/dist/index.mjs
var React = __toESM(require_react(), 1);
var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
function createContext2(rootComponentName, defaultContext) {
const Context = React.createContext(defaultContext);
const Provider = (props) => {
const { children, ...context } = props;
const value = React.useMemo(() => context, Object.values(context));
return (0, import_jsx_runtime.jsx)(Context.Provider, { value, children });
};
Provider.displayName = rootComponentName + "Provider";
function useContext2(consumerName) {
const context = React.useContext(Context);
if (context) return context;
if (defaultContext !== void 0) return defaultContext;
throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``);
}
return [Provider, useContext2];
}
function createContextScope(scopeName, createContextScopeDeps = []) {
let defaultContexts = [];
function createContext3(rootComponentName, defaultContext) {
const BaseContext = React.createContext(defaultContext);
const index = defaultContexts.length;
defaultContexts = [...defaultContexts, defaultContext];
const Provider = (props) => {
const { scope, children, ...context } = props;
const Context = scope?.[scopeName]?.[index] || BaseContext;
const value = React.useMemo(() => context, Object.values(context));
return (0, import_jsx_runtime.jsx)(Context.Provider, { value, children });
};
Provider.displayName = rootComponentName + "Provider";
function useContext2(consumerName, scope) {
const Context = scope?.[scopeName]?.[index] || BaseContext;
const context = React.useContext(Context);
if (context) return context;
if (defaultContext !== void 0) return defaultContext;
throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``);
}
return [Provider, useContext2];
}
const createScope = () => {
const scopeContexts = defaultContexts.map((defaultContext) => {
return React.createContext(defaultContext);
});
return function useScope(scope) {
const contexts = scope?.[scopeName] || scopeContexts;
return React.useMemo(
() => ({ [`__scope${scopeName}`]: { ...scope, [scopeName]: contexts } }),
[scope, contexts]
);
};
};
createScope.scopeName = scopeName;
return [createContext3, composeContextScopes(createScope, ...createContextScopeDeps)];
}
function composeContextScopes(...scopes) {
const baseScope = scopes[0];
if (scopes.length === 1) return baseScope;
const createScope = () => {
const scopeHooks = scopes.map((createScope2) => ({
useScope: createScope2(),
scopeName: createScope2.scopeName
}));
return function useComposedScopes(overrideScopes) {
const nextScopes = scopeHooks.reduce((nextScopes2, { useScope, scopeName }) => {
const scopeProps = useScope(overrideScopes);
const currentScope = scopeProps[`__scope${scopeName}`];
return { ...nextScopes2, ...currentScope };
}, {});
return React.useMemo(() => ({ [`__scope${baseScope.scopeName}`]: nextScopes }), [nextScopes]);
};
};
createScope.scopeName = baseScope.scopeName;
return createScope;
}
// node_modules/@radix-ui/react-use-layout-effect/dist/index.mjs
var React2 = __toESM(require_react(), 1);
var useLayoutEffect2 = globalThis?.document ? React2.useLayoutEffect : () => {
};
// node_modules/@radix-ui/react-id/dist/index.mjs
var React3 = __toESM(require_react(), 1);
var useReactId = React3[" useId ".trim().toString()] || (() => void 0);
var count = 0;
function useId(deterministicId) {
const [id, setId] = React3.useState(useReactId());
useLayoutEffect2(() => {
if (!deterministicId) setId((reactId) => reactId ?? String(count++));
}, [deterministicId]);
return deterministicId || (id ? `radix-${id}` : "");
}
// node_modules/@radix-ui/react-use-controllable-state/dist/index.mjs
var React5 = __toESM(require_react(), 1);
var React22 = __toESM(require_react(), 1);
// node_modules/@radix-ui/react-use-effect-event/dist/index.mjs
var React4 = __toESM(require_react(), 1);
var useReactEffectEvent = React4[" useEffectEvent ".trim().toString()];
var useReactInsertionEffect = React4[" useInsertionEffect ".trim().toString()];
// node_modules/@radix-ui/react-use-controllable-state/dist/index.mjs
var useInsertionEffect = React5[" useInsertionEffect ".trim().toString()] || useLayoutEffect2;
function useControllableState({
prop,
defaultProp,
onChange = () => {
},
caller
}) {
const [uncontrolledProp, setUncontrolledProp, onChangeRef] = useUncontrolledState({
defaultProp,
onChange
});
const isControlled = prop !== void 0;
const value = isControlled ? prop : uncontrolledProp;
if (true) {
const isControlledRef = React5.useRef(prop !== void 0);
React5.useEffect(() => {
const wasControlled = isControlledRef.current;
if (wasControlled !== isControlled) {
const from = wasControlled ? "controlled" : "uncontrolled";
const to = isControlled ? "controlled" : "uncontrolled";
console.warn(
`${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`
);
}
isControlledRef.current = isControlled;
}, [isControlled, caller]);
}
const setValue = React5.useCallback(
(nextValue) => {
if (isControlled) {
const value2 = isFunction(nextValue) ? nextValue(prop) : nextValue;
if (value2 !== prop) {
onChangeRef.current?.(value2);
}
} else {
setUncontrolledProp(nextValue);
}
},
[isControlled, prop, setUncontrolledProp, onChangeRef]
);
return [value, setValue];
}
function useUncontrolledState({
defaultProp,
onChange
}) {
const [value, setValue] = React5.useState(defaultProp);
const prevValueRef = React5.useRef(value);
const onChangeRef = React5.useRef(onChange);
useInsertionEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
React5.useEffect(() => {
if (prevValueRef.current !== value) {
onChangeRef.current?.(value);
prevValueRef.current = value;
}
}, [value, prevValueRef]);
return [value, setValue, onChangeRef];
}
function isFunction(value) {
return typeof value === "function";
}
// node_modules/@radix-ui/react-primitive/dist/index.mjs
var React7 = __toESM(require_react(), 1);
var ReactDOM = __toESM(require_react_dom(), 1);
// node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot/dist/index.mjs
var React6 = __toESM(require_react(), 1);
var import_jsx_runtime2 = __toESM(require_jsx_runtime(), 1);
function createSlot(ownerName) {
const SlotClone = createSlotClone(ownerName);
const Slot2 = React6.forwardRef((props, forwardedRef) => {
const { children, ...slotProps } = props;
const childrenArray = React6.Children.toArray(children);
const slottable = childrenArray.find(isSlottable);
if (slottable) {
const newElement = slottable.props.children;
const newChildren = childrenArray.map((child) => {
if (child === slottable) {
if (React6.Children.count(newElement) > 1) return React6.Children.only(null);
return React6.isValidElement(newElement) ? newElement.props.children : null;
} else {
return child;
}
});
return (0, import_jsx_runtime2.jsx)(SlotClone, { ...slotProps, ref: forwardedRef, children: React6.isValidElement(newElement) ? React6.cloneElement(newElement, void 0, newChildren) : null });
}
return (0, import_jsx_runtime2.jsx)(SlotClone, { ...slotProps, ref: forwardedRef, children });
});
Slot2.displayName = `${ownerName}.Slot`;
return Slot2;
}
var Slot = createSlot("Slot");
function createSlotClone(ownerName) {
const SlotClone = React6.forwardRef((props, forwardedRef) => {
const { children, ...slotProps } = props;
if (React6.isValidElement(children)) {
const childrenRef = getElementRef(children);
const props2 = mergeProps(slotProps, children.props);
if (children.type !== React6.Fragment) {
props2.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef;
}
return React6.cloneElement(children, props2);
}
return React6.Children.count(children) > 1 ? React6.Children.only(null) : null;
});
SlotClone.displayName = `${ownerName}.SlotClone`;
return SlotClone;
}
var SLOTTABLE_IDENTIFIER = /* @__PURE__ */ Symbol("radix.slottable");
function createSlottable(ownerName) {
const Slottable2 = ({ children }) => {
return (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
};
Slottable2.displayName = `${ownerName}.Slottable`;
Slottable2.__radixId = SLOTTABLE_IDENTIFIER;
return Slottable2;
}
var Slottable = createSlottable("Slottable");
function isSlottable(child) {
return React6.isValidElement(child) && typeof child.type === "function" && "__radixId" in child.type && child.type.__radixId === SLOTTABLE_IDENTIFIER;
}
function mergeProps(slotProps, childProps) {
const overrideProps = { ...childProps };
for (const propName in childProps) {
const slotPropValue = slotProps[propName];
const childPropValue = childProps[propName];
const isHandler = /^on[A-Z]/.test(propName);
if (isHandler) {
if (slotPropValue && childPropValue) {
overrideProps[propName] = (...args) => {
const result = childPropValue(...args);
slotPropValue(...args);
return result;
};
} else if (slotPropValue) {
overrideProps[propName] = slotPropValue;
}
} else if (propName === "style") {
overrideProps[propName] = { ...slotPropValue, ...childPropValue };
} else if (propName === "className") {
overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(" ");
}
}
return { ...slotProps, ...overrideProps };
}
function getElementRef(element) {
let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get;
let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
if (mayWarn) {
return element.ref;
}
getter = Object.getOwnPropertyDescriptor(element, "ref")?.get;
mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning;
if (mayWarn) {
return element.props.ref;
}
return element.props.ref || element.ref;
}
// node_modules/@radix-ui/react-primitive/dist/index.mjs
var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
var NODES = [
"a",
"button",
"div",
"form",
"h2",
"h3",
"img",
"input",
"label",
"li",
"nav",
"ol",
"p",
"select",
"span",
"svg",
"ul"
];
var Primitive = NODES.reduce((primitive, node) => {
const Slot2 = createSlot(`Primitive.${node}`);
const Node = React7.forwardRef((props, forwardedRef) => {
const { asChild, ...primitiveProps } = props;
const Comp = asChild ? Slot2 : node;
if (typeof window !== "undefined") {
window[/* @__PURE__ */ Symbol.for("radix-ui")] = true;
}
return (0, import_jsx_runtime3.jsx)(Comp, { ...primitiveProps, ref: forwardedRef });
});
Node.displayName = `Primitive.${node}`;
return { ...primitive, [node]: Node };
}, {});
function dispatchDiscreteCustomEvent(target, event) {
if (target) ReactDOM.flushSync(() => target.dispatchEvent(event));
}
// node_modules/@radix-ui/react-use-callback-ref/dist/index.mjs
var React8 = __toESM(require_react(), 1);
function useCallbackRef(callback) {
const callbackRef = React8.useRef(callback);
React8.useEffect(() => {
callbackRef.current = callback;
});
return React8.useMemo(() => (...args) => callbackRef.current?.(...args), []);
}
export {
composeEventHandlers,
createContext2,
createContextScope,
useLayoutEffect2,
useId,
useControllableState,
Primitive,
dispatchDiscreteCustomEvent,
useCallbackRef
};
//# sourceMappingURL=chunk-MOFDO34C.js.map
File diff suppressed because one or more lines are too long
+1150
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+69
View File
@@ -0,0 +1,69 @@
2.2.4 / 2018-03-13
==================
* Use flow strict mode (i.e. `@flow strict`).
2.2.3 / 2018-02-19
==================
* Change license from BSD+Patents to MIT.
2.2.2 / 2016-11-15
==================
* Add LICENSE file.
* Misc housekeeping.
2.2.1 / 2016-03-09
==================
* Use `NODE_ENV` variable instead of `__DEV__` to cache `process.env.NODE_ENV`.
2.2.0 / 2015-11-17
==================
* Use `error.name` instead of `Invariant Violation`.
2.1.3 / 2015-11-17
==================
* Remove `@provideModule` pragma.
2.1.2 / 2015-10-27
==================
* Fix license.
2.1.1 / 2015-09-20
==================
* Use correct SPDX license.
* Test "browser.js" using browserify.
* Switch from "envify" to "loose-envify".
2.1.0 / 2015-06-03
==================
* Add "envify" as a dependency.
* Fixed license field in "package.json".
2.0.0 / 2015-02-21
==================
* Switch to using the "browser" field. There are now browser and server versions that respect the "format" in production.
1.0.2 / 2014-09-24
==================
* Added tests, npmignore and gitignore.
* Clarifications in README.
1.0.1 / 2014-09-24
==================
* Actually include 'invariant.js'.
1.0.0 / 2014-09-24
==================
* Initial release.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013-present, Facebook, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+37
View File
@@ -0,0 +1,37 @@
# invariant
[![Build Status](https://travis-ci.org/zertosh/invariant.svg?branch=master)](https://travis-ci.org/zertosh/invariant)
A mirror of Facebook's `invariant` (e.g. [React](https://github.com/facebook/react/blob/v0.13.3/src/vendor/core/invariant.js), [flux](https://github.com/facebook/flux/blob/2.0.2/src/invariant.js)).
A way to provide descriptive errors in development but generic errors in production.
## Install
With [npm](http://npmjs.org) do:
```sh
npm install invariant
```
## `invariant(condition, message)`
```js
var invariant = require('invariant');
invariant(someTruthyVal, 'This will not throw');
// No errors
invariant(someFalseyVal, 'This will throw an error with this message');
// Error: Invariant Violation: This will throw an error with this message
```
**Note:** When `process.env.NODE_ENV` is not `production`, the message is required. If omitted, `invariant` will throw regardless of the truthiness of the condition. When `process.env.NODE_ENV` is `production`, the message is optional so they can be minified away.
### Browser
When used with [browserify](https://github.com/substack/node-browserify), it'll use `browser.js` (instead of `invariant.js`) and the [envify](https://github.com/hughsk/envify) transform will inline the value of `process.env.NODE_ENV`.
### Node
The node version is optimized around the performance implications of accessing `process.env`. The value of `process.env.NODE_ENV` is cached, and repeatedly used instead of reading `process.env`. See [Server rendering is slower with npm react #812](https://github.com/facebook/react/issues/812)
+49
View File
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
/**
* Use invariant() to assert state which your program assumes to be true.
*
* Provide sprintf-style format (only %s is supported) and arguments
* to provide information about what broke and what you were
* expecting.
*
* The invariant message will be stripped in production, but the invariant
* will remain to ensure logic does not differ in production.
*/
var invariant = function(condition, format, a, b, c, d, e, f) {
if (process.env.NODE_ENV !== 'production') {
if (format === undefined) {
throw new Error('invariant requires an error message argument');
}
}
if (!condition) {
var error;
if (format === undefined) {
error = new Error(
'Minified exception occurred; use the non-minified dev environment ' +
'for the full error message and additional helpful warnings.'
);
} else {
var args = [a, b, c, d, e, f];
var argIndex = 0;
error = new Error(
format.replace(/%s/g, function() { return args[argIndex++]; })
);
error.name = 'Invariant Violation';
}
error.framesToPop = 1; // we don't care about invariant's own frame
throw error;
}
};
module.exports = invariant;
+51
View File
@@ -0,0 +1,51 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
/**
* Use invariant() to assert state which your program assumes to be true.
*
* Provide sprintf-style format (only %s is supported) and arguments
* to provide information about what broke and what you were
* expecting.
*
* The invariant message will be stripped in production, but the invariant
* will remain to ensure logic does not differ in production.
*/
var NODE_ENV = process.env.NODE_ENV;
var invariant = function(condition, format, a, b, c, d, e, f) {
if (NODE_ENV !== 'production') {
if (format === undefined) {
throw new Error('invariant requires an error message argument');
}
}
if (!condition) {
var error;
if (format === undefined) {
error = new Error(
'Minified exception occurred; use the non-minified dev environment ' +
'for the full error message and additional helpful warnings.'
);
} else {
var args = [a, b, c, d, e, f];
var argIndex = 0;
error = new Error(
format.replace(/%s/g, function() { return args[argIndex++]; })
);
error.name = 'Invariant Violation';
}
error.framesToPop = 1; // we don't care about invariant's own frame
throw error;
}
};
module.exports = invariant;
+7
View File
@@ -0,0 +1,7 @@
/* @flow strict */
declare module.exports: (
condition: any,
format?: string,
...args: Array<any>
) => void;
+35
View File
@@ -0,0 +1,35 @@
{
"name": "invariant",
"version": "2.2.4",
"description": "invariant",
"keywords": [
"test",
"invariant"
],
"license": "MIT",
"author": "Andres Suarez <zertosh@gmail.com>",
"files": [
"browser.js",
"invariant.js",
"invariant.js.flow"
],
"repository": "https://github.com/zertosh/invariant",
"scripts": {
"test": "NODE_ENV=production tap test/*.js && NODE_ENV=development tap test/*.js"
},
"dependencies": {
"loose-envify": "^1.0.0"
},
"devDependencies": {
"browserify": "^11.0.1",
"flow-bin": "^0.67.1",
"tap": "^1.4.0"
},
"main": "invariant.js",
"browser": "browser.js",
"browserify": {
"transform": [
"loose-envify"
]
}
}
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2018 Formidable Labs
Copyright (c) 2017 Evgeny Poberezkin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+165
View File
@@ -0,0 +1,165 @@
[![React Fast Compare — Formidable, We build the modern web](https://raw.githubusercontent.com/FormidableLabs/react-fast-compare/master/react-fast-compare-Hero.png)](https://formidable.com/open-source/)
[![Downloads][downloads_img]][npm_site]
[![Bundle Size][bundle_img]](#bundle-size)
[![GH Actions Status][actions_img]][actions_site]
[![Coverage Status][cov_img]][cov_site]
[![npm version][npm_img]][npm_site]
[![Maintenance Status][maintenance_img]](#maintenance-status)
The fastest deep equal comparison for React. Very quick general-purpose deep
comparison, too. Great for `React.memo` and `shouldComponentUpdate`.
This is a fork of the brilliant
[fast-deep-equal](https://github.com/epoberezkin/fast-deep-equal) with some
extra handling for React.
![benchmark chart](https://raw.githubusercontent.com/FormidableLabs/react-fast-compare/master/assets/benchmarking.png "benchmarking chart")
(Check out the [benchmarking details](#benchmarking-this-library).)
## Install
```sh
$ yarn add react-fast-compare
# or
$ npm install react-fast-compare
```
## Highlights
- ES5 compatible; works in node.js (0.10+) and browsers (IE9+)
- deeply compares any value (besides objects with circular references)
- handles React-specific circular references, like elements
- checks equality Date and RegExp objects
- should as fast as [fast-deep-equal](https://github.com/epoberezkin/fast-deep-equal) via a single unified library, and with added guardrails for circular references.
- small: under 660 bytes minified+gzipped
## Usage
```jsx
const isEqual = require("react-fast-compare");
// general usage
console.log(isEqual({ foo: "bar" }, { foo: "bar" })); // true
// React.memo
// only re-render ExpensiveComponent when the props have deeply changed
const DeepMemoComponent = React.memo(ExpensiveComponent, isEqual);
// React.Component shouldComponentUpdate
// only re-render AnotherExpensiveComponent when the props have deeply changed
class AnotherExpensiveComponent extends React.Component {
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
render() {
// ...
}
}
```
## Do I Need `React.memo` (or `shouldComponentUpdate`)?
> What's faster than a really fast deep comparison? No deep comparison at all.
—This Readme
Deep checks in `React.memo` or a `shouldComponentUpdate` should not be used blindly.
First, see if the default
[React.memo](https://reactjs.org/docs/react-api.html#reactmemo) or
[PureComponent](https://reactjs.org/docs/react-api.html#reactpurecomponent)
will work for you. If it won't (if you need deep checks), it's wise to make
sure you've correctly indentified the bottleneck in your application by
[profiling the performance](https://reactjs.org/docs/optimizing-performance.html#profiling-components-with-the-chrome-performance-tab).
After you've determined that you _do_ need deep equality checks and you've
identified the minimum number of places to apply them, then this library may
be for you!
## Benchmarking this Library
The absolute values are much less important than the relative differences
between packages.
Benchmarking source can be found
[here](https://github.com/FormidableLabs/react-fast-compare/blob/master/benchmark/index.js).
Each "operation" consists of running all relevant tests. The React benchmark
uses both the generic tests and the react tests; these runs will be slower
simply because there are more tests in each operation.
The results below are from a local test on a laptop _(stats last updated 6/2/2020)_:
### Generic Data
```
react-fast-compare x 177,600 ops/sec ±1.73% (92 runs sampled)
fast-deep-equal x 184,211 ops/sec ±0.65% (87 runs sampled)
lodash.isEqual x 39,826 ops/sec ±1.32% (86 runs sampled)
nano-equal x 176,023 ops/sec ±0.89% (92 runs sampled)
shallow-equal-fuzzy x 146,355 ops/sec ±0.64% (89 runs sampled)
fastest: fast-deep-equal
```
`react-fast-compare` and `fast-deep-equal` should be the same speed for these
tests; any difference is just noise. `react-fast-compare` won't be faster than
`fast-deep-equal`, because it's based on it.
### React and Generic Data
```
react-fast-compare x 86,392 ops/sec ±0.70% (93 runs sampled)
fast-deep-equal x 85,567 ops/sec ±0.95% (92 runs sampled)
lodash.isEqual x 7,369 ops/sec ±1.78% (84 runs sampled)
fastest: react-fast-compare,fast-deep-equal
```
Two of these packages cannot handle comparing React elements, because they
contain circular reference: `nano-equal` and `shallow-equal-fuzzy`.
### Running Benchmarks
```sh
$ yarn install
$ yarn run benchmark
```
## Differences between this library and `fast-deep-equal`
`react-fast-compare` is based on `fast-deep-equal`, with some additions:
- `react-fast-compare` has `try`/`catch` guardrails for stack overflows from undetected (non-React) circular references.
- `react-fast-compare` has a _single_ unified entry point for all uses. No matter what your target application is, `import equal from 'react-fast-compare'` just works. `fast-deep-equal` has multiple entry points for different use cases.
This version of `react-fast-compare` tracks `fast-deep-equal@3.1.1`.
## Bundle Size
There are a variety of ways to calculate bundle size for JavaScript code.
You can see our size test code in the `compress` script in
[`package.json`](https://github.com/FormidableLabs/react-fast-compare/blob/master/package.json).
[Bundlephobia's calculation](https://bundlephobia.com/result?p=react-fast-compare) is slightly higher,
as they [do not mangle during minification](https://github.com/pastelsky/package-build-stats/blob/v6.1.1/src/getDependencySizeTree.js#L139).
## License
[MIT](https://github.com/FormidableLabs/react-fast-compare/blob/readme/LICENSE)
## Contributing
Please see our [contributions guide](./CONTRIBUTING.md).
## Maintenance Status
**Active:** Formidable is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome.
[actions_img]: https://github.com/FormidableLabs/react-fast-compare/actions/workflows/ci.yml/badge.svg
[actions_site]: https://github.com/formidablelabs/react-fast-compare/actions/workflows/ci.yml
[cov_img]: https://codecov.io/gh/FormidableLabs/react-fast-compare/branch/master/graph/badge.svg
[cov_site]: https://codecov.io/gh/FormidableLabs/react-fast-compare
[npm_img]: https://badge.fury.io/js/react-fast-compare.svg
[npm_site]: http://badge.fury.io/js/react-fast-compare
[appveyor_img]: https://ci.appveyor.com/api/projects/status/github/formidablelabs/react-fast-compare?branch=master&svg=true
[appveyor_site]: https://ci.appveyor.com/project/FormidableLabs/react-fast-compare
[bundle_img]: https://img.shields.io/badge/minzipped%20size-656%20B-flatgreen.svg
[downloads_img]: https://img.shields.io/npm/dm/react-fast-compare.svg
[maintenance_img]: https://img.shields.io/badge/maintenance-active-flatgreen.svg
+2
View File
@@ -0,0 +1,2 @@
declare function isEqual<A = any, B = any>(a: A, b: B): boolean;
export = isEqual;
+139
View File
@@ -0,0 +1,139 @@
/* global Map:readonly, Set:readonly, ArrayBuffer:readonly */
var hasElementType = typeof Element !== 'undefined';
var hasMap = typeof Map === 'function';
var hasSet = typeof Set === 'function';
var hasArrayBuffer = typeof ArrayBuffer === 'function' && !!ArrayBuffer.isView;
// Note: We **don't** need `envHasBigInt64Array` in fde es6/index.js
function equal(a, b) {
// START: fast-deep-equal es6/index.js 3.1.3
if (a === b) return true;
if (a && b && typeof a == 'object' && typeof b == 'object') {
if (a.constructor !== b.constructor) return false;
var length, i, keys;
if (Array.isArray(a)) {
length = a.length;
if (length != b.length) return false;
for (i = length; i-- !== 0;)
if (!equal(a[i], b[i])) return false;
return true;
}
// START: Modifications:
// 1. Extra `has<Type> &&` helpers in initial condition allow es6 code
// to co-exist with es5.
// 2. Replace `for of` with es5 compliant iteration using `for`.
// Basically, take:
//
// ```js
// for (i of a.entries())
// if (!b.has(i[0])) return false;
// ```
//
// ... and convert to:
//
// ```js
// it = a.entries();
// while (!(i = it.next()).done)
// if (!b.has(i.value[0])) return false;
// ```
//
// **Note**: `i` access switches to `i.value`.
var it;
if (hasMap && (a instanceof Map) && (b instanceof Map)) {
if (a.size !== b.size) return false;
it = a.entries();
while (!(i = it.next()).done)
if (!b.has(i.value[0])) return false;
it = a.entries();
while (!(i = it.next()).done)
if (!equal(i.value[1], b.get(i.value[0]))) return false;
return true;
}
if (hasSet && (a instanceof Set) && (b instanceof Set)) {
if (a.size !== b.size) return false;
it = a.entries();
while (!(i = it.next()).done)
if (!b.has(i.value[0])) return false;
return true;
}
// END: Modifications
if (hasArrayBuffer && ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
length = a.length;
if (length != b.length) return false;
for (i = length; i-- !== 0;)
if (a[i] !== b[i]) return false;
return true;
}
if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags;
// START: Modifications:
// Apply guards for `Object.create(null)` handling. See:
// - https://github.com/FormidableLabs/react-fast-compare/issues/64
// - https://github.com/epoberezkin/fast-deep-equal/issues/49
if (a.valueOf !== Object.prototype.valueOf && typeof a.valueOf === 'function' && typeof b.valueOf === 'function') return a.valueOf() === b.valueOf();
if (a.toString !== Object.prototype.toString && typeof a.toString === 'function' && typeof b.toString === 'function') return a.toString() === b.toString();
// END: Modifications
keys = Object.keys(a);
length = keys.length;
if (length !== Object.keys(b).length) return false;
for (i = length; i-- !== 0;)
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
// END: fast-deep-equal
// START: react-fast-compare
// custom handling for DOM elements
if (hasElementType && a instanceof Element) return false;
// custom handling for React/Preact
for (i = length; i-- !== 0;) {
if ((keys[i] === '_owner' || keys[i] === '__v' || keys[i] === '__o') && a.$$typeof) {
// React-specific: avoid traversing React elements' _owner
// Preact-specific: avoid traversing Preact elements' __v and __o
// __v = $_original / $_vnode
// __o = $_owner
// These properties contain circular references and are not needed when
// comparing the actual elements (and not their owners)
// .$$typeof and ._store on just reasonable markers of elements
continue;
}
// all other properties should be traversed as usual
if (!equal(a[keys[i]], b[keys[i]])) return false;
}
// END: react-fast-compare
// START: fast-deep-equal
return true;
}
return a !== a && b !== b;
}
// end fast-deep-equal
module.exports = function isEqual(a, b) {
try {
return equal(a, b);
} catch (error) {
if (((error.message || '').match(/stack|recursion/i))) {
// warn on circular references, don't crash
// browsers give this different errors name and messages:
// chrome/safari: "RangeError", "Maximum call stack size exceeded"
// firefox: "InternalError", too much recursion"
// edge: "Error", "Out of stack space"
console.warn('react-fast-compare cannot handle circular refs');
return false;
}
// some other error. we should definitely know about these
throw error;
}
};
+105
View File
@@ -0,0 +1,105 @@
{
"name": "react-fast-compare",
"version": "3.2.2",
"description": "Fastest deep equal comparison for React. Great for React.memo & shouldComponentUpdate. Also really fast general-purpose deep comparison.",
"main": "index.js",
"typings": "index.d.ts",
"scripts": {
"preversion": "yarn test",
"benchmark": "node benchmark",
"eslint": "eslint \"*.js\" benchmark test",
"tslint": "eslint test/typescript/*.tsx",
"test-browser": "karma start test/browser/karma.conf.js",
"test-node": "mocha \"test/node/*.spec.js\"",
"test-node-cov": "nyc mocha \"test/node/*.spec.js\"",
"test-ts-usage": "tsc --esModuleInterop --jsx react --noEmit test/typescript/sample-react-redux-usage.tsx test/typescript/sample-usage.tsx",
"test-ts-defs": "tsc --target ES5 index.d.ts",
"test": "builder concurrent --buffer eslint tslint test-ts-usage test-ts-defs test-node-cov test-browser",
"compress": "terser --compress --mangle=\"toplevel:true\" -- index.js",
"size-min-gz": "yarn -s compress | gzip -9 | wc -c"
},
"repository": {
"type": "git",
"url": "https://github.com/FormidableLabs/react-fast-compare"
},
"keywords": [
"fast",
"equal",
"react",
"compare",
"shouldComponentUpdate",
"deep-equal"
],
"author": "Chris Bolin",
"license": "MIT",
"bugs": {
"url": "https://github.com/FormidableLabs/react-fast-compare/issues"
},
"homepage": "https://github.com/FormidableLabs/react-fast-compare",
"devDependencies": {
"@babel/core": "^7.21.0",
"@babel/preset-env": "^7.20.2",
"@changesets/cli": "^2.26.1",
"@svitejs/changesets-changelog-github-compact": "^0.1.1",
"@testing-library/dom": "^9.0.1",
"@testing-library/preact": "^3.2.3",
"@types/node": "^18.15.0",
"@types/react": "^16.9.35",
"@types/react-dom": "^16.9.8",
"@types/react-redux": "^7.1.25",
"@typescript-eslint/parser": "^5.54.1",
"assert": "^2.0.0",
"babel-loader": "^9.1.2",
"benchmark": "^2.1.4",
"builder": "^5.0.0",
"codecov": "^3.8.3",
"core-js": "^3.29.0",
"eslint": "^8.35.0",
"eslint-plugin-react": "^7.32.2",
"fast-deep-equal": "3.1.3",
"fast-deep-equal-git": "epoberezkin/fast-deep-equal#v3.1.3",
"jsdom": "^21.1.0",
"jsdom-global": "^3.0.2",
"karma": "^6.4.1",
"karma-chrome-launcher": "^3.1.1",
"karma-firefox-launcher": "^2.1.2",
"karma-mocha": "^2.0.1",
"karma-mocha-reporter": "^2.2.5",
"karma-safari-launcher": "^1.0.0",
"karma-webpack": "^5.0.0",
"lodash": "^4.17.10",
"mocha": "^10.2.0",
"nano-equal": "^2.0.2",
"nyc": "^15.1.0",
"preact": "^10.13.1",
"process": "^0.11.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"react-test-renderer": "^18.2.0",
"redux": "^4.2.1",
"shallow-equal-fuzzy": "0.0.2",
"sinon": "^15.0.1",
"terser": "^5.16.6",
"typescript": "^4.9.5",
"webpack": "^5.76.0"
},
"publishConfig": {
"provenance": true
},
"nyc": {
"exclude": [
"**/test/**",
"node_modules"
],
"reporter": [
"lcov",
"text-summary"
]
},
"files": [
"index.js",
"index.d.ts"
],
"types": "index.d.ts"
}
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2018 The New York Times Company
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+240
View File
@@ -0,0 +1,240 @@
# react-helmet-async
[![CI](https://github.com/staylor/react-helmet-async/actions/workflows/ci.yml/badge.svg)](https://github.com/staylor/react-helmet-async/actions/workflows/ci.yml)
[Announcement post on Times Open blog](https://open.nytimes.com/the-future-of-meta-tag-management-for-modern-react-development-ec26a7dc9183)
This package is a fork of [React Helmet](https://github.com/nfl/react-helmet).
`<Helmet>` usage is synonymous, but server and client now requires `<HelmetProvider>` to encapsulate state per request.
`react-helmet` relies on `react-side-effect`, which is not thread-safe. If you are doing anything asynchronous on the server, you need Helmet to encapsulate data on a per-request basis, this package does just that.
## React 19
React 19 has built-in support for hoisting `<title>`, `<meta>`, `<link>`, `<style>`, and `<script>` elements to `<head>`. Starting with version 3.0.0, this package detects the React version at runtime:
- **React 19+**: `<Helmet>` renders actual DOM elements and lets React handle hoisting them to `<head>`. `<HelmetProvider>` becomes a transparent passthrough. The existing API is fully compatible — you do not need to change any code.
- **React 1618**: The existing behavior is preserved. `<Helmet>` collects all instances, deduplicates tags, and applies changes to the DOM via manual manipulation (client) or serializes them for the response (server).
> **Note:** `htmlAttributes` and `bodyAttributes` do not have a React 19 equivalent, so they are still applied via direct DOM manipulation on both code paths.
If you are starting a new React 19 project and do not need `htmlAttributes`/`bodyAttributes`, SSR `context` serialization, `onChangeClientState`, `prioritizeSeoTags`, or `titleTemplate` support, you may not need this package at all — React 19's built-in metadata handling may be sufficient.
## Usage
**New in 1.0.0:** No more default export! `import { Helmet } from 'react-helmet-async'`
The main way that this package differs from `react-helmet` is that it requires using a Provider to encapsulate Helmet state for your React tree. If you use libraries like Redux or Apollo, you are already familiar with this paradigm:
```javascript
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Helmet, HelmetProvider } from 'react-helmet-async';
const app = (
<HelmetProvider>
<App>
<Helmet>
<title>Hello World</title>
<link rel="canonical" href="https://www.tacobell.com/" />
</Helmet>
<h1>Hello World</h1>
</App>
</HelmetProvider>
);
createRoot(document.getElementById('app')).render(app);
```
On the server, we will no longer use static methods to extract state. `react-side-effect`
exposed a `.rewind()` method, which Helmet used when calling `Helmet.renderStatic()`. Instead, we are going
to pass a `context` prop to `HelmetProvider`, which will hold our state specific to each request.
```javascript
import React from 'react';
import { renderToString } from 'react-dom/server';
import { Helmet, HelmetProvider } from 'react-helmet-async';
const helmetContext = {};
const app = (
<HelmetProvider context={helmetContext}>
<App>
<Helmet>
<title>Hello World</title>
<link rel="canonical" href="https://www.tacobell.com/" />
</Helmet>
<h1>Hello World</h1>
</App>
</HelmetProvider>
);
const html = renderToString(app);
const { helmet } = helmetContext;
// helmet.title.toString() etc…
```
> **React 19 SSR note:** When using React 19, `<title>`, `<meta>`, and `<link>` tags rendered inside `<Helmet>` are included directly in the React render output and hoisted to `<head>` by React itself. The `context` object will not be populated with helmet state on React 19. If you rely on the `context` for server rendering, you can render these tags directly in your component tree instead and let React 19 handle them natively.
## Streams
This package only works with streaming if your `<head>` data is output outside of `renderToNodeStream()`.
This is possible if your data hydration method already parses your React tree. Example:
```javascript
import through from 'through';
import { renderToNodeStream } from 'react-dom/server';
import { getDataFromTree } from 'react-apollo';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import template from 'server/template';
const helmetContext = {};
const app = (
<HelmetProvider context={helmetContext}>
<App>
<Helmet>
<title>Hello World</title>
<link rel="canonical" href="https://www.tacobell.com/" />
</Helmet>
<h1>Hello World</h1>
</App>
</HelmetProvider>
);
await getDataFromTree(app);
const [header, footer] = template({
helmet: helmetContext.helmet,
});
res.status(200);
res.write(header);
renderToNodeStream(app)
.pipe(
through(
function write(data) {
this.queue(data);
},
function end() {
this.queue(footer);
this.queue(null);
}
)
)
.pipe(res);
```
> **React 19:** React 19's `renderToReadableStream` natively handles `<title>`, `<meta>`, and `<link>` hoisting during streaming, so the manual context extraction shown above is not necessary.
## Usage in Jest
While testing in using jest, if there is a need to emulate SSR, the following string is required to have the test behave the way they are expected to.
```javascript
import { HelmetProvider } from 'react-helmet-async';
HelmetProvider.canUseDOM = false;
```
> This is only relevant for React 1618. On React 19, `HelmetProvider` is a passthrough and `canUseDOM` has no effect.
## Prioritizing tags for SEO
It is understood that in some cases for SEO, certain tags should appear earlier in the HEAD. Using the `prioritizeSeoTags` flag on any `<Helmet>` component allows the server render of react-helmet-async to expose a method for prioritizing relevant SEO tags.
In the component:
```javascript
<Helmet prioritizeSeoTags>
<title>A fancy webpage</title>
<link rel="notImportant" href="https://www.chipotle.com" />
<meta name="whatever" value="notImportant" />
<link rel="canonical" href="https://www.tacobell.com" />
<meta property="og:title" content="A very important title"/>
</Helmet>
```
In your server template:
```javascript
<html>
<head>
${helmet.title.toString()}
${helmet.priority.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
${helmet.script.toString()}
</head>
...
</html>
```
Will result in:
```html
<html>
<head>
<title>A fancy webpage</title>
<meta property="og:title" content="A very important title"/>
<link rel="canonical" href="https://www.tacobell.com" />
<meta name="whatever" value="notImportant" />
<link rel="notImportant" href="https://www.chipotle.com" />
</head>
...
</html>
```
A list of prioritized tags and attributes can be found in [constants.ts](./src/constants.ts).
> **React 19:** The `prioritizeSeoTags` flag has no effect on React 19, since tags are rendered as regular JSX elements and their order in `<head>` is determined by React's rendering order.
## Usage without Context
You can optionally use `<Helmet>` outside a context by manually creating a stateful `HelmetData` instance, and passing that stateful object to each `<Helmet>` instance:
```js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { Helmet, HelmetData } from 'react-helmet-async';
const helmetData = new HelmetData({});
const app = (
<App>
<Helmet helmetData={helmetData}>
<title>Hello World</title>
<link rel="canonical" href="https://www.tacobell.com/" />
</Helmet>
<h1>Hello World</h1>
</App>
);
const html = renderToString(app);
const { helmet } = helmetData.context;
```
> **React 19:** The `helmetData` prop is ignored on React 19, since `<Helmet>` renders elements directly without the need for external state management.
## Compatibility
| React Version | Behavior |
|---|---|
| 16.6+ | Full support via `HelmetProvider` context and manual DOM updates |
| 17.x | Full support via `HelmetProvider` context and manual DOM updates |
| 18.x | Full support via `HelmetProvider` context and manual DOM updates |
| 19.x+ | Renders native JSX elements; React handles `<head>` hoisting |
## Development
```bash
pnpm install
pnpm test # unit tests
pnpm run test:e2e # server + browser e2e tests
pnpm run test:all # everything
```
## License
Licensed under the Apache 2.0 License, Copyright © 2018 Scott Taylor
+23
View File
@@ -0,0 +1,23 @@
import { Component } from 'react';
import type { HelmetServerState } from './types';
export interface DispatcherContextProp {
setHelmet: (newState: HelmetServerState | null) => void;
helmetInstances: {
get: () => HelmetDispatcher[];
add: (helmet: HelmetDispatcher) => void;
remove: (helmet: HelmetDispatcher) => void;
};
}
interface DispatcherProps {
context: DispatcherContextProp;
}
export default class HelmetDispatcher extends Component<DispatcherProps> {
rendered: boolean;
shouldComponentUpdate(nextProps: DispatcherProps): boolean;
componentDidUpdate(): void;
componentWillUnmount(): void;
emitChange(): void;
init(): void;
render(): null;
}
export {};
+26
View File
@@ -0,0 +1,26 @@
import type HelmetDispatcher from './Dispatcher';
import type { HelmetServerState } from './types';
export declare function clearInstances(): void;
export interface HelmetDataType {
instances: HelmetDispatcher[];
context: HelmetDataContext;
}
interface HelmetDataContext {
helmet: HelmetServerState | null;
}
export declare const isDocument: boolean;
export default class HelmetData implements HelmetDataType {
instances: never[];
canUseDOM: boolean;
context: HelmetDataContext;
value: {
setHelmet: (serverState: HelmetServerState | null) => void;
helmetInstances: {
get: () => HelmetDispatcher[];
add: (instance: HelmetDispatcher) => void;
remove: (instance: HelmetDispatcher) => void;
};
};
constructor(context: any, canUseDOM?: boolean);
}
export {};
+17
View File
@@ -0,0 +1,17 @@
import type { PropsWithChildren } from 'react';
import React, { Component } from 'react';
import HelmetData from './HelmetData';
import type { HelmetServerState } from './types';
export declare const Context: React.Context<{}>;
interface ProviderProps {
context?: {
helmet?: HelmetServerState | null;
};
}
export default class HelmetProvider extends Component<PropsWithChildren<ProviderProps>> {
static canUseDOM: boolean;
helmetData: HelmetData | null;
constructor(props: PropsWithChildren<ProviderProps>);
render(): React.JSX.Element;
}
export {};
+38
View File
@@ -0,0 +1,38 @@
import React, { Component } from 'react';
import type { HelmetProps } from './types';
interface React19DispatcherProps extends HelmetProps {
/**
* The processed props including mapped children. These come from Helmet's
* mapChildrenToProps or the raw API props.
*/
[key: string]: any;
}
/**
* React 19+ Dispatcher: Instead of manual DOM manipulation, this component
* renders actual JSX elements. React 19 automatically hoists <title>, <meta>,
* <link>, <style>, and <script async> to <head>.
*
* For htmlAttributes and bodyAttributes, we still apply via direct DOM
* manipulation since React 19 doesn't handle those.
*/
export default class React19Dispatcher extends Component<React19DispatcherProps> {
componentDidMount(): void;
componentDidUpdate(): void;
componentWillUnmount(): void;
resolveTitle(): string | undefined;
renderTitle(): React.DetailedReactHTMLElement<{
[key: string]: any;
}, HTMLElement> | null;
renderBase(): React.DetailedReactHTMLElement<{
[key: string]: any;
}, HTMLElement> | null;
renderMeta(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null;
renderLink(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null;
renderScript(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null;
renderStyle(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null;
renderNoscript(): React.DetailedReactHTMLElement<React.HTMLAttributes<HTMLElement>, HTMLElement>[] | null;
render(): React.FunctionComponentElement<{
children?: React.ReactNode;
}>;
}
export {};
+3
View File
@@ -0,0 +1,3 @@
import type { StateUpdate } from './types';
declare const handleStateChangeOnClient: (newState: StateUpdate) => void;
export default handleStateChangeOnClient;
+58
View File
@@ -0,0 +1,58 @@
export declare enum TAG_PROPERTIES {
CHARSET = "charset",
CSS_TEXT = "cssText",
HREF = "href",
HTTPEQUIV = "http-equiv",
INNER_HTML = "innerHTML",
ITEM_PROP = "itemprop",
NAME = "name",
PROPERTY = "property",
REL = "rel",
SRC = "src"
}
export declare enum ATTRIBUTE_NAMES {
BODY = "bodyAttributes",
HTML = "htmlAttributes",
TITLE = "titleAttributes"
}
export declare enum TAG_NAMES {
BASE = "base",
BODY = "body",
HEAD = "head",
HTML = "html",
LINK = "link",
META = "meta",
NOSCRIPT = "noscript",
SCRIPT = "script",
STYLE = "style",
TITLE = "title",
FRAGMENT = "Symbol(react.fragment)"
}
export declare const SEO_PRIORITY_TAGS: {
link: {
rel: string[];
};
script: {
type: string[];
};
meta: {
charset: string;
name: string[];
property: string[];
};
};
export declare const VALID_TAG_NAMES: TAG_NAMES[];
export declare const REACT_TAG_MAP: {
accesskey: string;
charset: string;
class: string;
contenteditable: string;
contextmenu: string;
'http-equiv': string;
itemprop: string;
tabindex: string;
};
export declare const HTML_TAG_MAP: {
[key: string]: string;
};
export declare const HELMET_ATTRIBUTE = "data-rh";
+21
View File
@@ -0,0 +1,21 @@
import type { PropsWithChildren } from 'react';
import React, { Component } from 'react';
import type { HelmetProps } from './types';
export type { Attributes, BodyProps, HelmetDatum, HelmetHTMLBodyDatum, HelmetHTMLElementDatum, HelmetProps, HelmetServerState, HelmetTags, HtmlProps, LinkProps, MetaProps, StateUpdate, TagList, TitleProps, } from './types';
export { default as HelmetData } from './HelmetData';
export { default as HelmetProvider } from './Provider';
export declare class Helmet extends Component<PropsWithChildren<HelmetProps>> {
static defaultProps: {
defer: boolean;
encodeSpecialCharacters: boolean;
prioritizeSeoTags: boolean;
};
shouldComponentUpdate(nextProps: HelmetProps): boolean;
private mapNestedChildrenToProps;
private flattenArrayTypeChildren;
private mapObjectTypeChildren;
private mapArrayTypeChildrenToProps;
private warnOnInvalidChildren;
private mapChildrenToProps;
render(): React.JSX.Element;
}
+983
View File
@@ -0,0 +1,983 @@
// src/index.tsx
import React5, { Component as Component4 } from "react";
import fastCompare from "react-fast-compare";
import invariant from "invariant";
// src/Provider.tsx
import React3, { Component } from "react";
// src/server.ts
import React from "react";
// src/constants.ts
var TAG_NAMES = /* @__PURE__ */ ((TAG_NAMES2) => {
TAG_NAMES2["BASE"] = "base";
TAG_NAMES2["BODY"] = "body";
TAG_NAMES2["HEAD"] = "head";
TAG_NAMES2["HTML"] = "html";
TAG_NAMES2["LINK"] = "link";
TAG_NAMES2["META"] = "meta";
TAG_NAMES2["NOSCRIPT"] = "noscript";
TAG_NAMES2["SCRIPT"] = "script";
TAG_NAMES2["STYLE"] = "style";
TAG_NAMES2["TITLE"] = "title";
TAG_NAMES2["FRAGMENT"] = "Symbol(react.fragment)";
return TAG_NAMES2;
})(TAG_NAMES || {});
var SEO_PRIORITY_TAGS = {
link: { rel: ["amphtml", "canonical", "alternate"] },
script: { type: ["application/ld+json"] },
meta: {
charset: "",
name: ["generator", "robots", "description"],
property: [
"og:type",
"og:title",
"og:url",
"og:image",
"og:image:alt",
"og:description",
"twitter:url",
"twitter:title",
"twitter:description",
"twitter:image",
"twitter:image:alt",
"twitter:card",
"twitter:site"
]
}
};
var VALID_TAG_NAMES = Object.values(TAG_NAMES);
var REACT_TAG_MAP = {
accesskey: "accessKey",
charset: "charSet",
class: "className",
contenteditable: "contentEditable",
contextmenu: "contextMenu",
"http-equiv": "httpEquiv",
itemprop: "itemProp",
tabindex: "tabIndex"
};
var HTML_TAG_MAP = Object.entries(REACT_TAG_MAP).reduce(
(carry, [key, value]) => {
carry[value] = key;
return carry;
},
{}
);
var HELMET_ATTRIBUTE = "data-rh";
// src/utils.ts
var HELMET_PROPS = {
DEFAULT_TITLE: "defaultTitle",
DEFER: "defer",
ENCODE_SPECIAL_CHARACTERS: "encodeSpecialCharacters",
ON_CHANGE_CLIENT_STATE: "onChangeClientState",
TITLE_TEMPLATE: "titleTemplate",
PRIORITIZE_SEO_TAGS: "prioritizeSeoTags"
};
var getInnermostProperty = (propsList, property) => {
for (let i = propsList.length - 1; i >= 0; i -= 1) {
const props = propsList[i];
if (Object.prototype.hasOwnProperty.call(props, property)) {
return props[property];
}
}
return null;
};
var getTitleFromPropsList = (propsList) => {
let innermostTitle = getInnermostProperty(propsList, "title" /* TITLE */);
const innermostTemplate = getInnermostProperty(propsList, HELMET_PROPS.TITLE_TEMPLATE);
if (Array.isArray(innermostTitle)) {
innermostTitle = innermostTitle.join("");
}
if (innermostTemplate && innermostTitle) {
return innermostTemplate.replace(/%s/g, () => innermostTitle);
}
const innermostDefaultTitle = getInnermostProperty(propsList, HELMET_PROPS.DEFAULT_TITLE);
return innermostTitle || innermostDefaultTitle || void 0;
};
var getOnChangeClientState = (propsList) => getInnermostProperty(propsList, HELMET_PROPS.ON_CHANGE_CLIENT_STATE) || (() => {
});
var getAttributesFromPropsList = (tagType, propsList) => propsList.filter((props) => typeof props[tagType] !== "undefined").map((props) => props[tagType]).reduce((tagAttrs, current) => ({ ...tagAttrs, ...current }), {});
var getBaseTagFromPropsList = (primaryAttributes, propsList) => propsList.filter((props) => typeof props["base" /* BASE */] !== "undefined").map((props) => props["base" /* BASE */]).reverse().reduce((innermostBaseTag, tag) => {
if (!innermostBaseTag.length) {
const keys = Object.keys(tag);
for (let i = 0; i < keys.length; i += 1) {
const attributeKey = keys[i];
const lowerCaseAttributeKey = attributeKey.toLowerCase();
if (primaryAttributes.indexOf(lowerCaseAttributeKey) !== -1 && tag[lowerCaseAttributeKey]) {
return innermostBaseTag.concat(tag);
}
}
}
return innermostBaseTag;
}, []);
var warn = (msg) => console && typeof console.warn === "function" && console.warn(msg);
var getTagsFromPropsList = (tagName, primaryAttributes, propsList) => {
const approvedSeenTags = {};
return propsList.filter((props) => {
if (Array.isArray(props[tagName])) {
return true;
}
if (typeof props[tagName] !== "undefined") {
warn(
`Helmet: ${tagName} should be of type "Array". Instead found type "${typeof props[tagName]}"`
);
}
return false;
}).map((props) => props[tagName]).reverse().reduce((approvedTags, instanceTags) => {
const instanceSeenTags = {};
instanceTags.filter((tag) => {
let primaryAttributeKey;
const keys2 = Object.keys(tag);
for (let i = 0; i < keys2.length; i += 1) {
const attributeKey = keys2[i];
const lowerCaseAttributeKey = attributeKey.toLowerCase();
if (primaryAttributes.indexOf(lowerCaseAttributeKey) !== -1 && !(primaryAttributeKey === "rel" /* REL */ && tag[primaryAttributeKey].toLowerCase() === "canonical") && !(lowerCaseAttributeKey === "rel" /* REL */ && tag[lowerCaseAttributeKey].toLowerCase() === "stylesheet")) {
primaryAttributeKey = lowerCaseAttributeKey;
}
if (primaryAttributes.indexOf(attributeKey) !== -1 && (attributeKey === "innerHTML" /* INNER_HTML */ || attributeKey === "cssText" /* CSS_TEXT */ || attributeKey === "itemprop" /* ITEM_PROP */)) {
primaryAttributeKey = attributeKey;
}
}
if (!primaryAttributeKey || !tag[primaryAttributeKey]) {
return false;
}
const value = tag[primaryAttributeKey].toLowerCase();
if (!approvedSeenTags[primaryAttributeKey]) {
approvedSeenTags[primaryAttributeKey] = {};
}
if (!instanceSeenTags[primaryAttributeKey]) {
instanceSeenTags[primaryAttributeKey] = {};
}
if (!approvedSeenTags[primaryAttributeKey][value]) {
instanceSeenTags[primaryAttributeKey][value] = true;
return true;
}
return false;
}).reverse().forEach((tag) => approvedTags.push(tag));
const keys = Object.keys(instanceSeenTags);
for (let i = 0; i < keys.length; i += 1) {
const attributeKey = keys[i];
const tagUnion = {
...approvedSeenTags[attributeKey],
...instanceSeenTags[attributeKey]
};
approvedSeenTags[attributeKey] = tagUnion;
}
return approvedTags;
}, []).reverse();
};
var getAnyTrueFromPropsList = (propsList, checkedTag) => {
if (Array.isArray(propsList) && propsList.length) {
for (let index = 0; index < propsList.length; index += 1) {
const prop = propsList[index];
if (prop[checkedTag]) {
return true;
}
}
}
return false;
};
var reducePropsToState = (propsList) => ({
baseTag: getBaseTagFromPropsList(["href" /* HREF */], propsList),
bodyAttributes: getAttributesFromPropsList("bodyAttributes" /* BODY */, propsList),
defer: getInnermostProperty(propsList, HELMET_PROPS.DEFER),
encode: getInnermostProperty(propsList, HELMET_PROPS.ENCODE_SPECIAL_CHARACTERS),
htmlAttributes: getAttributesFromPropsList("htmlAttributes" /* HTML */, propsList),
linkTags: getTagsFromPropsList(
"link" /* LINK */,
["rel" /* REL */, "href" /* HREF */],
propsList
),
metaTags: getTagsFromPropsList(
"meta" /* META */,
[
"name" /* NAME */,
"charset" /* CHARSET */,
"http-equiv" /* HTTPEQUIV */,
"property" /* PROPERTY */,
"itemprop" /* ITEM_PROP */
],
propsList
),
noscriptTags: getTagsFromPropsList("noscript" /* NOSCRIPT */, ["innerHTML" /* INNER_HTML */], propsList),
onChangeClientState: getOnChangeClientState(propsList),
scriptTags: getTagsFromPropsList(
"script" /* SCRIPT */,
["src" /* SRC */, "innerHTML" /* INNER_HTML */],
propsList
),
styleTags: getTagsFromPropsList("style" /* STYLE */, ["cssText" /* CSS_TEXT */], propsList),
title: getTitleFromPropsList(propsList),
titleAttributes: getAttributesFromPropsList("titleAttributes" /* TITLE */, propsList),
prioritizeSeoTags: getAnyTrueFromPropsList(propsList, HELMET_PROPS.PRIORITIZE_SEO_TAGS)
});
var flattenArray = (possibleArray) => Array.isArray(possibleArray) ? possibleArray.join("") : possibleArray;
var checkIfPropsMatch = (props, toMatch) => {
const keys = Object.keys(props);
for (let i = 0; i < keys.length; i += 1) {
if (toMatch[keys[i]] && toMatch[keys[i]].includes(props[keys[i]])) {
return true;
}
}
return false;
};
var prioritizer = (elementsList, propsToMatch) => {
if (Array.isArray(elementsList)) {
return elementsList.reduce(
(acc, elementAttrs) => {
if (checkIfPropsMatch(elementAttrs, propsToMatch)) {
acc.priority.push(elementAttrs);
} else {
acc.default.push(elementAttrs);
}
return acc;
},
{ priority: [], default: [] }
);
}
return { default: elementsList, priority: [] };
};
var without = (obj, key) => {
return {
...obj,
[key]: void 0
};
};
// src/server.ts
var SELF_CLOSING_TAGS = ["noscript" /* NOSCRIPT */, "script" /* SCRIPT */, "style" /* STYLE */];
var encodeSpecialCharacters = (str, encode = true) => {
if (encode === false) {
return String(str);
}
return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
};
var generateElementAttributesAsString = (attributes) => Object.keys(attributes).reduce((str, key) => {
const attr = typeof attributes[key] !== "undefined" ? `${key}="${attributes[key]}"` : `${key}`;
return str ? `${str} ${attr}` : attr;
}, "");
var generateTitleAsString = (type, title, attributes, encode) => {
const attributeString = generateElementAttributesAsString(attributes);
const flattenedTitle = flattenArray(title);
return attributeString ? `<${type} ${HELMET_ATTRIBUTE}="true" ${attributeString}>${encodeSpecialCharacters(
flattenedTitle,
encode
)}</${type}>` : `<${type} ${HELMET_ATTRIBUTE}="true">${encodeSpecialCharacters(
flattenedTitle,
encode
)}</${type}>`;
};
var generateTagsAsString = (type, tags, encode = true) => tags.reduce((str, t) => {
const tag = t;
const attributeHtml = Object.keys(tag).filter(
(attribute) => !(attribute === "innerHTML" /* INNER_HTML */ || attribute === "cssText" /* CSS_TEXT */)
).reduce((string, attribute) => {
const attr = typeof tag[attribute] === "undefined" ? attribute : `${attribute}="${encodeSpecialCharacters(tag[attribute], encode)}"`;
return string ? `${string} ${attr}` : attr;
}, "");
const tagContent = tag.innerHTML || tag.cssText || "";
const isSelfClosing = SELF_CLOSING_TAGS.indexOf(type) === -1;
return `${str}<${type} ${HELMET_ATTRIBUTE}="true" ${attributeHtml}${isSelfClosing ? `/>` : `>${tagContent}</${type}>`}`;
}, "");
var convertElementAttributesToReactProps = (attributes, initProps = {}) => Object.keys(attributes).reduce((obj, key) => {
const mapped = REACT_TAG_MAP[key];
obj[mapped || key] = attributes[key];
return obj;
}, initProps);
var generateTitleAsReactComponent = (_type, title, attributes) => {
const initProps = {
key: title,
[HELMET_ATTRIBUTE]: true
};
const props = convertElementAttributesToReactProps(attributes, initProps);
return [React.createElement("title" /* TITLE */, props, title)];
};
var generateTagsAsReactComponent = (type, tags) => tags.map((tag, i) => {
const mappedTag = {
key: i,
[HELMET_ATTRIBUTE]: true
};
Object.keys(tag).forEach((attribute) => {
const mapped = REACT_TAG_MAP[attribute];
const mappedAttribute = mapped || attribute;
if (mappedAttribute === "innerHTML" /* INNER_HTML */ || mappedAttribute === "cssText" /* CSS_TEXT */) {
const content = tag.innerHTML || tag.cssText;
mappedTag.dangerouslySetInnerHTML = { __html: content };
} else {
mappedTag[mappedAttribute] = tag[attribute];
}
});
return React.createElement(type, mappedTag);
});
var getMethodsForTag = (type, tags, encode = true) => {
switch (type) {
case "title" /* TITLE */:
return {
toComponent: () => generateTitleAsReactComponent(type, tags.title, tags.titleAttributes),
toString: () => generateTitleAsString(type, tags.title, tags.titleAttributes, encode)
};
case "bodyAttributes" /* BODY */:
case "htmlAttributes" /* HTML */:
return {
toComponent: () => convertElementAttributesToReactProps(tags),
toString: () => generateElementAttributesAsString(tags)
};
default:
return {
toComponent: () => generateTagsAsReactComponent(type, tags),
toString: () => generateTagsAsString(type, tags, encode)
};
}
};
var getPriorityMethods = ({ metaTags, linkTags, scriptTags, encode }) => {
const meta = prioritizer(metaTags, SEO_PRIORITY_TAGS.meta);
const link = prioritizer(linkTags, SEO_PRIORITY_TAGS.link);
const script = prioritizer(scriptTags, SEO_PRIORITY_TAGS.script);
const priorityMethods = {
toComponent: () => [
...generateTagsAsReactComponent("meta" /* META */, meta.priority),
...generateTagsAsReactComponent("link" /* LINK */, link.priority),
...generateTagsAsReactComponent("script" /* SCRIPT */, script.priority)
],
toString: () => (
// generate all the tags as strings and concatenate them
`${getMethodsForTag("meta" /* META */, meta.priority, encode)} ${getMethodsForTag(
"link" /* LINK */,
link.priority,
encode
)} ${getMethodsForTag("script" /* SCRIPT */, script.priority, encode)}`
)
};
return {
priorityMethods,
metaTags: meta.default,
linkTags: link.default,
scriptTags: script.default
};
};
var mapStateOnServer = (props) => {
const {
baseTag,
bodyAttributes,
encode = true,
htmlAttributes,
noscriptTags,
styleTags,
title = "",
titleAttributes,
prioritizeSeoTags
} = props;
let { linkTags, metaTags, scriptTags } = props;
let priorityMethods = {
toComponent: () => [],
toString: () => ""
};
if (prioritizeSeoTags) {
({ priorityMethods, linkTags, metaTags, scriptTags } = getPriorityMethods(props));
}
return {
priority: priorityMethods,
base: getMethodsForTag("base" /* BASE */, baseTag, encode),
bodyAttributes: getMethodsForTag("bodyAttributes" /* BODY */, bodyAttributes, encode),
htmlAttributes: getMethodsForTag("htmlAttributes" /* HTML */, htmlAttributes, encode),
link: getMethodsForTag("link" /* LINK */, linkTags, encode),
meta: getMethodsForTag("meta" /* META */, metaTags, encode),
noscript: getMethodsForTag("noscript" /* NOSCRIPT */, noscriptTags, encode),
script: getMethodsForTag("script" /* SCRIPT */, scriptTags, encode),
style: getMethodsForTag("style" /* STYLE */, styleTags, encode),
title: getMethodsForTag("title" /* TITLE */, { title, titleAttributes }, encode)
};
};
var server_default = mapStateOnServer;
// src/HelmetData.ts
var instances = [];
var isDocument = !!(typeof window !== "undefined" && window.document && window.document.createElement);
var HelmetData = class {
instances = [];
canUseDOM = isDocument;
context;
value = {
setHelmet: (serverState) => {
this.context.helmet = serverState;
},
helmetInstances: {
get: () => this.canUseDOM ? instances : this.instances,
add: (instance) => {
(this.canUseDOM ? instances : this.instances).push(instance);
},
remove: (instance) => {
const index = (this.canUseDOM ? instances : this.instances).indexOf(instance);
(this.canUseDOM ? instances : this.instances).splice(index, 1);
}
}
};
constructor(context, canUseDOM) {
this.context = context;
this.canUseDOM = canUseDOM || false;
if (!canUseDOM) {
context.helmet = server_default({
baseTag: [],
bodyAttributes: {},
encodeSpecialCharacters: true,
htmlAttributes: {},
linkTags: [],
metaTags: [],
noscriptTags: [],
scriptTags: [],
styleTags: [],
title: "",
titleAttributes: {}
});
}
}
};
// src/reactVersion.ts
import React2 from "react";
var major = parseInt(React2.version.split(".")[0], 10);
var isReact19 = major >= 19;
// src/Provider.tsx
var defaultValue = {};
var Context = React3.createContext(defaultValue);
var HelmetProvider = class _HelmetProvider extends Component {
static canUseDOM = isDocument;
helmetData;
constructor(props) {
super(props);
if (isReact19) {
this.helmetData = null;
} else {
this.helmetData = new HelmetData(this.props.context || {}, _HelmetProvider.canUseDOM);
}
}
render() {
if (isReact19) {
return /* @__PURE__ */ React3.createElement(React3.Fragment, null, this.props.children);
}
return /* @__PURE__ */ React3.createElement(Context.Provider, { value: this.helmetData.value }, this.props.children);
}
};
// src/Dispatcher.tsx
import { Component as Component2 } from "react";
import shallowEqual from "shallowequal";
// src/client.ts
var updateTags = (type, tags) => {
const headElement = document.head || document.querySelector("head" /* HEAD */);
const tagNodes = headElement.querySelectorAll(`${type}[${HELMET_ATTRIBUTE}]`);
const oldTags = [].slice.call(tagNodes);
const newTags = [];
let indexToDelete;
if (tags && tags.length) {
tags.forEach((tag) => {
const newElement = document.createElement(type);
for (const attribute in tag) {
if (Object.prototype.hasOwnProperty.call(tag, attribute)) {
if (attribute === "innerHTML" /* INNER_HTML */) {
newElement.innerHTML = tag.innerHTML;
} else if (attribute === "cssText" /* CSS_TEXT */) {
const cssText = tag.cssText;
newElement.appendChild(document.createTextNode(cssText));
} else {
const attr = attribute;
const value = typeof tag[attr] === "undefined" ? "" : tag[attr];
newElement.setAttribute(attribute, value);
}
}
}
newElement.setAttribute(HELMET_ATTRIBUTE, "true");
if (oldTags.some((existingTag, index) => {
indexToDelete = index;
return newElement.isEqualNode(existingTag);
})) {
oldTags.splice(indexToDelete, 1);
} else {
newTags.push(newElement);
}
});
}
oldTags.forEach((tag) => tag.parentNode?.removeChild(tag));
newTags.forEach((tag) => headElement.appendChild(tag));
return {
oldTags,
newTags
};
};
var updateAttributes = (tagName, attributes) => {
const elementTag = document.getElementsByTagName(tagName)[0];
if (!elementTag) {
return;
}
const helmetAttributeString = elementTag.getAttribute(HELMET_ATTRIBUTE);
const helmetAttributes = helmetAttributeString ? helmetAttributeString.split(",") : [];
const attributesToRemove = [...helmetAttributes];
const attributeKeys = Object.keys(attributes);
for (const attribute of attributeKeys) {
const value = attributes[attribute] || "";
if (elementTag.getAttribute(attribute) !== value) {
elementTag.setAttribute(attribute, value);
}
if (helmetAttributes.indexOf(attribute) === -1) {
helmetAttributes.push(attribute);
}
const indexToSave = attributesToRemove.indexOf(attribute);
if (indexToSave !== -1) {
attributesToRemove.splice(indexToSave, 1);
}
}
for (let i = attributesToRemove.length - 1; i >= 0; i -= 1) {
elementTag.removeAttribute(attributesToRemove[i]);
}
if (helmetAttributes.length === attributesToRemove.length) {
elementTag.removeAttribute(HELMET_ATTRIBUTE);
} else if (elementTag.getAttribute(HELMET_ATTRIBUTE) !== attributeKeys.join(",")) {
elementTag.setAttribute(HELMET_ATTRIBUTE, attributeKeys.join(","));
}
};
var updateTitle = (title, attributes) => {
if (typeof title !== "undefined" && document.title !== title) {
document.title = flattenArray(title);
}
updateAttributes("title" /* TITLE */, attributes);
};
var commitTagChanges = (newState, cb) => {
const {
baseTag,
bodyAttributes,
htmlAttributes,
linkTags,
metaTags,
noscriptTags,
onChangeClientState,
scriptTags,
styleTags,
title,
titleAttributes
} = newState;
updateAttributes("body" /* BODY */, bodyAttributes);
updateAttributes("html" /* HTML */, htmlAttributes);
updateTitle(title, titleAttributes);
const tagUpdates = {
baseTag: updateTags("base" /* BASE */, baseTag),
linkTags: updateTags("link" /* LINK */, linkTags),
metaTags: updateTags("meta" /* META */, metaTags),
noscriptTags: updateTags("noscript" /* NOSCRIPT */, noscriptTags),
scriptTags: updateTags("script" /* SCRIPT */, scriptTags),
styleTags: updateTags("style" /* STYLE */, styleTags)
};
const addedTags = {};
const removedTags = {};
Object.keys(tagUpdates).forEach((tagType) => {
const { newTags, oldTags } = tagUpdates[tagType];
if (newTags.length) {
addedTags[tagType] = newTags;
}
if (oldTags.length) {
removedTags[tagType] = tagUpdates[tagType].oldTags;
}
});
if (cb) {
cb();
}
onChangeClientState(newState, addedTags, removedTags);
};
var _helmetCallback = null;
var handleStateChangeOnClient = (newState) => {
if (_helmetCallback) {
cancelAnimationFrame(_helmetCallback);
}
if (newState.defer) {
_helmetCallback = requestAnimationFrame(() => {
commitTagChanges(newState, () => {
_helmetCallback = null;
});
});
} else {
commitTagChanges(newState);
_helmetCallback = null;
}
};
var client_default = handleStateChangeOnClient;
// src/Dispatcher.tsx
var HelmetDispatcher = class extends Component2 {
rendered = false;
shouldComponentUpdate(nextProps) {
return !shallowEqual(nextProps, this.props);
}
componentDidUpdate() {
this.emitChange();
}
componentWillUnmount() {
const { helmetInstances } = this.props.context;
helmetInstances.remove(this);
this.emitChange();
}
emitChange() {
const { helmetInstances, setHelmet } = this.props.context;
let serverState = null;
const state = reducePropsToState(
helmetInstances.get().map((instance) => {
const { context: _context, ...props } = instance.props;
return props;
})
);
if (HelmetProvider.canUseDOM) {
client_default(state);
} else if (server_default) {
serverState = server_default(state);
}
setHelmet(serverState);
}
// componentWillMount will be deprecated
// for SSR, initialize on first render
// constructor is also unsafe in StrictMode
init() {
if (this.rendered) {
return;
}
this.rendered = true;
const { helmetInstances } = this.props.context;
helmetInstances.add(this);
this.emitChange();
}
render() {
this.init();
return null;
}
};
// src/React19Dispatcher.tsx
import React4, { Component as Component3 } from "react";
var react19Instances = [];
var toHtmlAttributes = (props) => {
const result = {};
for (const key of Object.keys(props)) {
result[HTML_TAG_MAP[key] || key] = props[key];
}
return result;
};
var toReactProps = (attrs) => {
const result = {};
for (const key of Object.keys(attrs)) {
const mapped = REACT_TAG_MAP[key];
result[mapped || key] = attrs[key];
}
return result;
};
var applyAttributes = (tagName, attributes) => {
if (!isDocument)
return;
const el = document.getElementsByTagName(tagName)[0];
if (!el)
return;
const managedAttr = "data-rh-managed";
const prev = el.getAttribute(managedAttr);
const prevKeys = prev ? prev.split(",") : [];
const nextKeys = Object.keys(attributes);
for (const key of prevKeys) {
if (!nextKeys.includes(key)) {
el.removeAttribute(key);
}
}
for (const key of nextKeys) {
const value = attributes[key];
if (value === void 0 || value === null || value === false) {
el.removeAttribute(key);
} else if (value === true) {
el.setAttribute(key, "");
} else {
el.setAttribute(key, String(value));
}
}
if (nextKeys.length > 0) {
el.setAttribute(managedAttr, nextKeys.join(","));
} else {
el.removeAttribute(managedAttr);
}
};
var syncAllAttributes = () => {
const htmlAttrs = {};
const bodyAttrs = {};
for (const instance of react19Instances) {
const { htmlAttributes, bodyAttributes } = instance.props;
if (htmlAttributes) {
Object.assign(htmlAttrs, toHtmlAttributes(htmlAttributes));
}
if (bodyAttributes) {
Object.assign(bodyAttrs, toHtmlAttributes(bodyAttributes));
}
}
applyAttributes("html" /* HTML */, htmlAttrs);
applyAttributes("body" /* BODY */, bodyAttrs);
};
var React19Dispatcher = class extends Component3 {
componentDidMount() {
react19Instances.push(this);
syncAllAttributes();
}
componentDidUpdate() {
syncAllAttributes();
}
componentWillUnmount() {
const index = react19Instances.indexOf(this);
if (index !== -1) {
react19Instances.splice(index, 1);
}
syncAllAttributes();
}
resolveTitle() {
const { title, titleTemplate, defaultTitle } = this.props;
if (title && titleTemplate) {
return titleTemplate.replace(/%s/g, () => Array.isArray(title) ? title.join("") : title);
}
return title || defaultTitle || void 0;
}
renderTitle() {
const title = this.resolveTitle();
if (title === void 0)
return null;
const titleAttributes = this.props.titleAttributes || {};
return React4.createElement("title" /* TITLE */, toReactProps(titleAttributes), title);
}
renderBase() {
const { base } = this.props;
if (!base)
return null;
return React4.createElement("base" /* BASE */, toReactProps(base));
}
renderMeta() {
const { meta } = this.props;
if (!meta || !Array.isArray(meta))
return null;
return meta.map(
(attrs, i) => React4.createElement("meta" /* META */, {
key: i,
...toReactProps(attrs)
})
);
}
renderLink() {
const { link } = this.props;
if (!link || !Array.isArray(link))
return null;
return link.map(
(attrs, i) => React4.createElement("link" /* LINK */, {
key: i,
...toReactProps(attrs)
})
);
}
renderScript() {
const { script } = this.props;
if (!script || !Array.isArray(script))
return null;
return script.map((attrs, i) => {
const { innerHTML, ...rest } = attrs;
const props = toReactProps(rest);
if (innerHTML) {
props.dangerouslySetInnerHTML = { __html: innerHTML };
}
return React4.createElement("script" /* SCRIPT */, { key: i, ...props });
});
}
renderStyle() {
const { style } = this.props;
if (!style || !Array.isArray(style))
return null;
return style.map((attrs, i) => {
const { cssText, ...rest } = attrs;
const props = toReactProps(rest);
if (cssText) {
props.dangerouslySetInnerHTML = { __html: cssText };
}
return React4.createElement("style" /* STYLE */, { key: i, ...props });
});
}
renderNoscript() {
const { noscript } = this.props;
if (!noscript || !Array.isArray(noscript))
return null;
return noscript.map((attrs, i) => {
const { innerHTML, ...rest } = attrs;
const props = toReactProps(rest);
if (innerHTML) {
props.dangerouslySetInnerHTML = { __html: innerHTML };
}
return React4.createElement("noscript" /* NOSCRIPT */, { key: i, ...props });
});
}
render() {
return React4.createElement(
React4.Fragment,
null,
this.renderTitle(),
this.renderBase(),
this.renderMeta(),
this.renderLink(),
this.renderScript(),
this.renderStyle(),
this.renderNoscript()
);
}
};
// src/index.tsx
var Helmet = class extends Component4 {
static defaultProps = {
defer: true,
encodeSpecialCharacters: true,
prioritizeSeoTags: false
};
shouldComponentUpdate(nextProps) {
return !fastCompare(without(this.props, "helmetData"), without(nextProps, "helmetData"));
}
mapNestedChildrenToProps(child, nestedChildren) {
if (!nestedChildren) {
return null;
}
switch (child.type) {
case "script" /* SCRIPT */:
case "noscript" /* NOSCRIPT */:
return {
innerHTML: nestedChildren
};
case "style" /* STYLE */:
return {
cssText: nestedChildren
};
default:
throw new Error(
`<${child.type} /> elements are self-closing and can not contain children. Refer to our API for more information.`
);
}
}
flattenArrayTypeChildren(child, arrayTypeChildren, newChildProps, nestedChildren) {
return {
...arrayTypeChildren,
[child.type]: [
...arrayTypeChildren[child.type] || [],
{
...newChildProps,
...this.mapNestedChildrenToProps(child, nestedChildren)
}
]
};
}
mapObjectTypeChildren(child, newProps, newChildProps, nestedChildren) {
switch (child.type) {
case "title" /* TITLE */:
return {
...newProps,
[child.type]: nestedChildren,
titleAttributes: { ...newChildProps }
};
case "body" /* BODY */:
return {
...newProps,
bodyAttributes: { ...newChildProps }
};
case "html" /* HTML */:
return {
...newProps,
htmlAttributes: { ...newChildProps }
};
default:
return {
...newProps,
[child.type]: { ...newChildProps }
};
}
}
mapArrayTypeChildrenToProps(arrayTypeChildren, newProps) {
let newFlattenedProps = { ...newProps };
Object.keys(arrayTypeChildren).forEach((arrayChildName) => {
newFlattenedProps = {
...newFlattenedProps,
[arrayChildName]: arrayTypeChildren[arrayChildName]
};
});
return newFlattenedProps;
}
warnOnInvalidChildren(child, nestedChildren) {
invariant(
VALID_TAG_NAMES.some((name) => child.type === name),
typeof child.type === "function" ? `You may be attempting to nest <Helmet> components within each other, which is not allowed. Refer to our API for more information.` : `Only elements types ${VALID_TAG_NAMES.join(
", "
)} are allowed. Helmet does not support rendering <${child.type}> elements. Refer to our API for more information.`
);
invariant(
!nestedChildren || typeof nestedChildren === "string" || Array.isArray(nestedChildren) && !nestedChildren.some((nestedChild) => typeof nestedChild !== "string"),
`Helmet expects a string as a child of <${child.type}>. Did you forget to wrap your children in braces? ( <${child.type}>{\`\`}</${child.type}> ) Refer to our API for more information.`
);
return true;
}
mapChildrenToProps(children, newProps) {
let arrayTypeChildren = {};
React5.Children.forEach(children, (child) => {
if (!child || !child.props) {
return;
}
const { children: nestedChildren, ...childProps } = child.props;
const newChildProps = Object.keys(childProps).reduce((obj, key) => {
obj[HTML_TAG_MAP[key] || key] = childProps[key];
return obj;
}, {});
let { type } = child;
if (typeof type === "symbol") {
type = type.toString();
} else {
this.warnOnInvalidChildren(child, nestedChildren);
}
switch (type) {
case "Symbol(react.fragment)" /* FRAGMENT */:
newProps = this.mapChildrenToProps(nestedChildren, newProps);
break;
case "link" /* LINK */:
case "meta" /* META */:
case "noscript" /* NOSCRIPT */:
case "script" /* SCRIPT */:
case "style" /* STYLE */:
arrayTypeChildren = this.flattenArrayTypeChildren(
child,
arrayTypeChildren,
newChildProps,
nestedChildren
);
break;
default:
newProps = this.mapObjectTypeChildren(child, newProps, newChildProps, nestedChildren);
break;
}
});
return this.mapArrayTypeChildrenToProps(arrayTypeChildren, newProps);
}
render() {
const { children, ...props } = this.props;
let newProps = { ...props };
let { helmetData } = props;
if (children) {
newProps = this.mapChildrenToProps(children, newProps);
}
if (helmetData && !(helmetData instanceof HelmetData)) {
const data = helmetData;
helmetData = new HelmetData(data.context, true);
delete newProps.helmetData;
}
if (isReact19) {
return /* @__PURE__ */ React5.createElement(React19Dispatcher, { ...newProps });
}
return helmetData ? /* @__PURE__ */ React5.createElement(HelmetDispatcher, { ...newProps, context: helmetData.value }) : /* @__PURE__ */ React5.createElement(Context.Consumer, null, (context) => /* @__PURE__ */ React5.createElement(HelmetDispatcher, { ...newProps, context }));
}
};
export {
Helmet,
HelmetData,
HelmetProvider
};
+1014
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
export declare const isReact19: boolean;
+45
View File
@@ -0,0 +1,45 @@
import React from 'react';
import type { MappedServerState } from './types';
declare const mapStateOnServer: (props: MappedServerState) => {
priority: {
toComponent: () => React.ReactElement[];
toString: () => string;
};
base: {
toComponent: () => {};
toString: () => string;
};
bodyAttributes: {
toComponent: () => {};
toString: () => string;
};
htmlAttributes: {
toComponent: () => {};
toString: () => string;
};
link: {
toComponent: () => {};
toString: () => string;
};
meta: {
toComponent: () => {};
toString: () => string;
};
noscript: {
toComponent: () => {};
toString: () => string;
};
script: {
toComponent: () => {};
toString: () => string;
};
style: {
toComponent: () => {};
toString: () => string;
};
title: {
toComponent: () => {};
toString: () => string;
};
};
export default mapStateOnServer;
+84
View File
@@ -0,0 +1,84 @@
import type { HTMLAttributes, JSX } from 'react';
import type HelmetData from './HelmetData';
export type Attributes = {
[key: string]: string;
};
interface OtherElementAttributes {
[key: string]: string | number | boolean | null | undefined;
}
export type HtmlProps = JSX.IntrinsicElements['html'] & OtherElementAttributes;
export type BodyProps = JSX.IntrinsicElements['body'] & OtherElementAttributes;
export type LinkProps = JSX.IntrinsicElements['link'];
export type MetaProps = JSX.IntrinsicElements['meta'] & {
charset?: string | undefined;
'http-equiv'?: string | undefined;
itemprop?: string | undefined;
};
export type TitleProps = HTMLAttributes<HTMLTitleElement>;
export interface HelmetTags {
baseTag: HTMLBaseElement[];
linkTags: HTMLLinkElement[];
metaTags: HTMLMetaElement[];
noscriptTags: HTMLElement[];
scriptTags: HTMLScriptElement[];
styleTags: HTMLStyleElement[];
}
export interface HelmetDatum {
toString(): string;
toComponent(): React.ReactElement[];
}
export interface HelmetHTMLBodyDatum {
toString(): string;
toComponent(): React.HTMLAttributes<HTMLBodyElement>;
}
export interface HelmetHTMLElementDatum {
toString(): string;
toComponent(): React.HTMLAttributes<HTMLHtmlElement>;
}
export interface HelmetServerState {
base: HelmetDatum;
bodyAttributes: HelmetHTMLBodyDatum;
htmlAttributes: HelmetHTMLElementDatum;
link: HelmetDatum;
meta: HelmetDatum;
noscript: HelmetDatum;
script: HelmetDatum;
style: HelmetDatum;
title: HelmetDatum;
priority: HelmetDatum;
}
export type MappedServerState = HelmetProps & HelmetTags & {
encode?: boolean;
};
export interface TagList {
[key: string]: HTMLElement[];
}
export interface StateUpdate extends HelmetTags {
bodyAttributes: BodyProps;
defer: boolean;
htmlAttributes: HtmlProps;
onChangeClientState: (newState: StateUpdate, addedTags: TagList, removedTags: TagList) => void;
title: string;
titleAttributes: TitleProps;
}
export interface HelmetProps {
async?: boolean;
base?: Attributes;
bodyAttributes?: BodyProps;
defaultTitle?: string;
defer?: boolean;
encodeSpecialCharacters?: boolean;
helmetData?: HelmetData;
htmlAttributes?: HtmlProps;
onChangeClientState?: (newState: StateUpdate, addedTags: HelmetTags, removedTags: HelmetTags) => void;
link?: LinkProps[];
meta?: MetaProps[];
noscript?: Attributes[];
script?: Attributes[];
style?: Attributes[];
title?: string;
titleAttributes?: Attributes;
titleTemplate?: string;
prioritizeSeoTags?: boolean;
}
export {};
+33
View File
@@ -0,0 +1,33 @@
interface PropList {
[key: string]: any;
}
type PropsList = PropList[];
type AttributeList = string[];
interface MatchProps {
[key: string]: string | AttributeList;
}
declare const reducePropsToState: (propsList: PropsList) => {
baseTag: any;
bodyAttributes: any;
defer: any;
encode: any;
htmlAttributes: any;
linkTags: any;
metaTags: any;
noscriptTags: any;
onChangeClientState: any;
scriptTags: any;
styleTags: any;
title: any;
titleAttributes: any;
prioritizeSeoTags: boolean;
};
export declare const flattenArray: (possibleArray: string[] | string) => string;
export { reducePropsToState };
export declare const prioritizer: (elementsList: HTMLElement[], propsToMatch: MatchProps) => {
priority: HTMLElement[];
default: HTMLElement[];
};
export declare const without: (obj: PropList, key: string) => {
[x: string]: any;
};
+81
View File
@@ -0,0 +1,81 @@
{
"name": "react-helmet-async",
"version": "3.0.0",
"description": "Thread-safe Helmet for React 1618, with native support for React 19+",
"sideEffects": false,
"main": "./lib/index.js",
"module": "./lib/index.esm.js",
"types": "./lib/index.d.ts",
"exports": {
".": {
"import": {
"types": "./lib/index.d.ts",
"default": "./lib/index.esm.js"
},
"require": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
}
}
},
"repository": "http://github.com/staylor/react-helmet-async",
"author": "Scott Taylor <scott.c.taylor@mac.com>",
"license": "Apache-2.0",
"files": [
"lib/"
],
"dependencies": {
"invariant": "^2.2.4",
"react-fast-compare": "^3.2.2",
"shallowequal": "^1.1.0"
},
"devDependencies": {
"@commitlint/cli": "18.4.3",
"@commitlint/config-conventional": "18.4.3",
"@playwright/test": "^1.58.2",
"@remix-run/eslint-config": "2.3.1",
"@testing-library/jest-dom": "6.1.5",
"@testing-library/react": "14.1.2",
"@types/eslint": "8.44.8",
"@types/invariant": "2.2.37",
"@types/jsdom": "21.1.6",
"@types/react": "18.2.39",
"@types/react-dom": "^18.3.7",
"@types/shallowequal": "1.1.5",
"@vitejs/plugin-react": "4.2.0",
"esbuild": "0.19.8",
"eslint": "8.54.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-prettier": "5.0.1",
"husky": "8.0.3",
"jsdom": "22.1.0",
"playwright": "^1.58.2",
"prettier": "3.1.0",
"raf": "3.4.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"rimraf": "5.0.5",
"tsx": "4.6.1",
"typescript": "5.2.2",
"vite": "4.5.0",
"vitest": "0.34.6"
},
"peerDependencies": {
"react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"scripts": {
"clean": "rimraf lib",
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint --report-unused-disable-directives .",
"lint-fix": "pnpm lint --fix",
"test": "vitest run",
"test:e2e:server": "vitest run --config e2e/vitest.config.ts",
"test:e2e:browser": "playwright test --config e2e/playwright.config.ts",
"test:e2e": "pnpm run test:e2e:server && pnpm run test:e2e:browser",
"test:all": "pnpm test && pnpm run test:e2e",
"test-watch": "pnpm test -- --watch",
"test-update": "pnpm test -- -u",
"compile": "pnpm run clean && NODE_ENV=production tsx build.ts && pnpm run types",
"prepare": "pnpm run compile && husky install",
"types": "tsc --project tsconfig.build.json"
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Alberto Leal <mailforalberto@gmail.com> (github.com/dashed)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+63
View File
@@ -0,0 +1,63 @@
# shallowequal [![Build Status](https://travis-ci.org/dashed/shallowequal.svg)](https://travis-ci.org/dashed/shallowequal) [![Downloads](https://img.shields.io/npm/dm/shallowequal.svg)](https://npmjs.com/shallowequal) [![npm version](https://img.shields.io/npm/v/shallowequal.svg?style=flat)](https://www.npmjs.com/package/shallowequal)
[![Greenkeeper badge](https://badges.greenkeeper.io/dashed/shallowequal.svg)](https://greenkeeper.io/)
> `shallowequal` is like lodash's [`isEqualWith`](https://lodash.com/docs/4.17.4#isEqualWith) but for shallow (strict) equal.
`shallowequal(value, other, [customizer], [thisArg])`
Performs a ***shallow equality*** comparison between two values (i.e. `value` and `other`) to determine if they are equivalent.
The equality is performed by iterating through keys on the given `value`, and returning `false` whenever any key has values which are not **strictly equal** between `value` and `other`. Otherwise, return `true` whenever the values of all keys are strictly equal.
If `customizer` (expected to be a function) is provided it is invoked to compare values. If `customizer` returns `undefined` (i.e. `void 0`), then comparisons are handled by the `shallowequal` function instead.
The `customizer` is bound to `thisArg` and invoked with three arguments: `(value, other, key)`.
**NOTE:** Docs are (shamelessly) adapted from [lodash's v3.x docs](https://lodash.com/docs/3.10.1#isEqualWith)
## Install
```sh
$ yarn add shallowequal
# npm v5+
$ npm install shallowequal
# before npm v5
$ npm install --save shallowequal
```
## Usage
```js
const shallowequal = require('shallowequal');
const object = { 'user': 'fred' };
const other = { 'user': 'fred' };
object == other;
// → false
shallowequal(object, other);
// → true
```
## Credit
Code for `shallowEqual` originated from https://github.com/gaearon/react-pure-render/ and has since been refactored to have the exact same API as `lodash.isEqualWith` (as of `v4.17.4`).
## Development
- `node.js` and `npm`. See: https://github.com/creationix/nvm#installation
- `yarn`. See: https://yarnpkg.com/en/docs/install
- `npm` dependencies. Run: `yarn install`
### Chores
- Lint: `yarn lint`
- Test: `yarn test`
- Pretty: `yarn pretty`
- Pre-publish: `yarn prepublish`
## License
MIT.
+46
View File
@@ -0,0 +1,46 @@
//
module.exports = function shallowEqual(objA, objB, compare, compareContext) {
var ret = compare ? compare.call(compareContext, objA, objB) : void 0;
if (ret !== void 0) {
return !!ret;
}
if (objA === objB) {
return true;
}
if (typeof objA !== "object" || !objA || typeof objB !== "object" || !objB) {
return false;
}
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
var bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB);
// Test for A's keys different from B.
for (var idx = 0; idx < keysA.length; idx++) {
var key = keysA[idx];
if (!bHasOwnProperty(key)) {
return false;
}
var valueA = objA[key];
var valueB = objB[key];
ret = compare ? compare.call(compareContext, valueA, valueB, key) : void 0;
if (ret === false || (ret === void 0 && valueA !== valueB)) {
return false;
}
}
return true;
};
+8
View File
@@ -0,0 +1,8 @@
// @flow
declare module.exports: <T, U>(
objA?: ?T,
objB?: ?U,
compare?: ?(objValue: any, otherValue: any, key?: string) => boolean | void,
compareContext?: ?any
) => boolean;
+51
View File
@@ -0,0 +1,51 @@
// @flow
module.exports = function shallowEqual<T, U>(
objA?: ?T,
objB?: ?U,
compare?: ?(objValue: any, otherValue: any, key?: string) => boolean | void,
compareContext?: ?any
): boolean {
var ret = compare ? compare.call(compareContext, objA, objB) : void 0;
if (ret !== void 0) {
return !!ret;
}
if (objA === objB) {
return true;
}
if (typeof objA !== "object" || !objA || typeof objB !== "object" || !objB) {
return false;
}
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
var bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB);
// Test for A's keys different from B.
for (var idx = 0; idx < keysA.length; idx++) {
var key = keysA[idx];
if (!bHasOwnProperty(key)) {
return false;
}
var valueA = objA[key];
var valueB = objB[key];
ret = compare ? compare.call(compareContext, valueA, valueB, key) : void 0;
if (ret === false || (ret === void 0 && valueA !== valueB)) {
return false;
}
}
return true;
};
+60
View File
@@ -0,0 +1,60 @@
{
"name": "shallowequal",
"version": "1.1.0",
"description": "Like lodash isEqualWith but for shallow equal.",
"main": "index.js",
"scripts": {
"lint": "eslint index.js test",
"test": "mocha --require babel-register",
"build:strip-flow":
"flow-remove-types --pretty index.original.js > index.js",
"build:gen-flow": "flow gen-flow-files index.original.js > index.js.flow",
"build": "npm run build:strip-flow && npm run build:gen-flow",
"prepublish":
"npm run build && npm run pretty && npm run lint && npm run test",
"travis": "npm run lint && npm run test",
"pretty": "prettier --write --tab-width 2 'test/**/*.js' '*.{js,js.flow}'",
"precommit": "lint-staged"
},
"lint-staged": {
"*.{js,json,css,js.flow}": ["prettier --write", "git add"]
},
"author": {
"name": "Alberto Leal",
"email": "mailforalberto@gmail.com",
"url": "github.com/dashed"
},
"repository": "dashed/shallowequal",
"license": "MIT",
"files": ["index.js", "index.js.flow", "index.original.js"],
"keywords": [
"shallowequal",
"shallow",
"equal",
"isequal",
"compare",
"isequalwith"
],
"eslintConfig": {
"parser": "babel-eslint",
"env": {
"browser": true,
"node": true,
"mocha": true
},
"extends": ["eslint:recommended"]
},
"devDependencies": {
"babel-eslint": "^8.0.0",
"babel-preset-env": "^1.6.1",
"babel-register": "^6.24.1",
"chai": "^4.0.0",
"eslint": "^4.7.1",
"flow-bin": "^0.75.0",
"flow-remove-types": "^1.2.3",
"husky": "^0.14.3",
"lint-staged": "^6.0.0",
"mocha": "^5.0.0",
"prettier": "^1.9.2"
}
}
+36
View File
@@ -51,6 +51,7 @@
"react": "^19.2.0",
"react-day-picker": "^9.13.0",
"react-dom": "^19.2.0",
"react-helmet-async": "^3.0.0",
"react-hook-form": "^7.70.0",
"react-resizable-panels": "^4.2.2",
"react-router-dom": "^7.14.0",
@@ -6744,6 +6745,15 @@
"node": ">=12"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmmirror.com/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -7668,6 +7678,26 @@
"react": "^19.2.3"
}
},
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
"license": "MIT"
},
"node_modules/react-helmet-async": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/react-helmet-async/-/react-helmet-async-3.0.0.tgz",
"integrity": "sha512-nA3IEZfXiclgrz4KLxAhqJqIfFDuvzQwlKwpdmzZIuC1KNSghDEIXmyU0TKtbM+NafnkICcwx8CECFrZ/sL/1w==",
"license": "Apache-2.0",
"dependencies": {
"invariant": "^2.2.4",
"react-fast-compare": "^3.2.2",
"shallowequal": "^1.1.0"
},
"peerDependencies": {
"react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-hook-form": {
"version": "7.70.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.70.0.tgz",
@@ -8131,6 +8161,12 @@
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+1
View File
@@ -53,6 +53,7 @@
"react": "^19.2.0",
"react-day-picker": "^9.13.0",
"react-dom": "^19.2.0",
"react-helmet-async": "^3.0.0",
"react-hook-form": "^7.70.0",
"react-resizable-panels": "^4.2.2",
"react-router-dom": "^7.14.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+10 -23
View File
@@ -1,26 +1,13 @@
# =============================================================================
# ROBOTS.TXT - VERSAO HOMOLOGACAO/DESENVOLVIMENTO
# =============================================================================
# Este arquivo BLOQUEIA TODOS os crawlers.
#
# Para PRODUCAO (avanzato.com.br), substitua por:
# User-agent: *
# Allow: /
# Sitemap: https://avanzato.com.br/sitemap.xml
# =============================================================================
# ============================================================================
# AVANZATO TECNOLOGIA - ROBOTS.TXT (GENERICO)
# ============================================================================
# O controle real de indexacao e feito pelo <meta name="robots"> no HTML:
# - Producao (avanzato.com.br): index, follow
# - Homologacao (hml.avanzato.com.br): noindex, nofollow
# - Desenvolvimento (localhost): noindex, nofollow
# ============================================================================
User-agent: *
Disallow: /
Allow: /
# Bloqueio explicito de todos os bots
User-agent: Googlebot
Disallow: /
User-agent: Bingbot
Disallow: /
User-agent: Slurp
Disallow: /
# Sitemap comentado (descomente em producao)
# Sitemap: https://avanzato.com.br/sitemap.xml
Sitemap: https://avanzato.com.br/sitemap.xml
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+146
View File
@@ -135,4 +135,150 @@
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://avanzato.com.br/ti-para-contabilidades</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://avanzato.com.br/ti-para-advocacias</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://avanzato.com.br/monitoramento-empresarial</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://avanzato.com.br/firewall-pfsense-empresarial</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://avanzato.com.br/backup-corporativo</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://avanzato.com.br/voip-empresarial</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<!-- SEO Local - Onde Atuamos -->
<url>
<loc>https://avanzato.com.br/onde-atuamos</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-guarulhos</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-mogi-das-cruzes</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-ipiranga</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-vila-formosa</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-cerqueira-cesar</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-santana</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-bras</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-ribeirao-preto</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<!-- Nichos adicionais -->
<url>
<loc>https://avanzato.com.br/suporte-ti-industria</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-portaria-remota</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-clinicas</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-imobiliarias</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/suporte-ti-recrutamento-selecao</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<!-- Avanzato Tools -->
<url>
<loc>https://avanzato.com.br/ferramentas</loc>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/spf-checker</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/dkim-checker</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/dmarc-checker</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/mx-lookup</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/ssl-checker</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/email-security-score</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://avanzato.com.br/ferramentas/microsoft-365-analyzer</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
</urlset>
+5
View File
@@ -0,0 +1,5 @@
{
"version": "1.4.0",
"date": "2026-06-02",
"environment": "auto-detect"
}
+21
View File
@@ -0,0 +1,21 @@
{
"current": "1.4.0",
"versions": [
{
"version": "1.4.0",
"date": "2026-06-02",
"title": "Avanzato Tools - Fase 1",
"changes": [
"7 ferramentas gratuitas: SPF Checker, DKIM Checker, DMARC Checker, MX Lookup, SSL Checker, Email Security Score, Microsoft 365 Analyzer",
"Pagina principal /ferramentas com grid de ferramentas",
"Sistema de 2 consultas gratuitas + cadastro para ilimitado",
"Captura de 'dor de TI' e urgencia para scoring de leads",
"Integracao com avaliacao gratuita e contato",
"Prerender de todas as 8 paginas de ferramentas para SEO",
"Sitemap atualizado com 8 novas URLs",
"Schema.org FAQPage nas paginas de ferramentas"
],
"file": "avanzato-site-v1.4.0.zip"
}
]
}
+119
View File
@@ -0,0 +1,119 @@
#!/bin/bash
# ============================================================================
# Script Post-Build - Gera paginas estaticas para rotas React
# ============================================================================
# Isso permite que URLs como /avaliacao funcionem diretamente,
# mesmo em servidores sem try_files configurado.
#
# Uso: bash scripts/build-static-routes.sh (executa automaticamente apos build)
# ============================================================================
set -e
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
echo -e "${BLUE}[Post-Build]${NC} Gerando paginas estaticas para rotas..."
DIST_DIR="${1:-dist}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [ ! -d "$DIST_DIR" ]; then
echo -e "${YELLOW}Pasta dist/ nao encontrada. Execute npm run build primeiro.${NC}"
exit 1
fi
# Lista de todas as rotas do site
ROUTES=(
"sobre"
"contato"
"cases"
"blog"
"avaliacao"
"tecnologias"
"tecnologia-para-condominios"
"ti-para-contabilidades"
"ti-para-advocacias"
"monitoramento-empresarial"
"firewall-pfsense-empresarial"
"backup-corporativo"
"voip-empresarial"
"servicos/ti-gerenciada"
"servicos/cloud-computing"
"servicos/seguranca"
"servicos/suporte"
"servicos/redes"
"servicos/pabx"
"servicos/backup"
"servicos/servidores"
"servicos/consultoria"
"servicos/noc"
"suporte-windows-server"
"suporte-linux-empresas"
"suporte-pfsense"
"suporte-proxmox"
"suporte-vmware"
"suporte-sharepoint"
"suporte-redes-cisco"
"onde-atuamos"
"suporte-ti-guarulhos"
"suporte-ti-mogi-das-cruzes"
"suporte-ti-ipiranga"
"suporte-ti-vila-formosa"
"suporte-ti-cerqueira-cesar"
"suporte-ti-santana"
"suporte-ti-bras"
"suporte-ti-ribeirao-preto"
"suporte-ti-industria"
"suporte-ti-portaria-remota"
"suporte-ti-clinicas"
"suporte-ti-imobiliarias"
"suporte-ti-recrutamento-selecao"
"privacidade"
"termos"
"ferramentas"
"ferramentas/spf-checker"
"ferramentas/dkim-checker"
"ferramentas/dmarc-checker"
"ferramentas/mx-lookup"
"ferramentas/ssl-checker"
"ferramentas/email-security-score"
"ferramentas/microsoft-365-analyzer"
)
# Copiar index.html como base
cp "$DIST_DIR/index.html" "$DIST_DIR/index.html.bak"
# Gerar pagina para cada rota
for route in "${ROUTES[@]}"; do
mkdir -p "$DIST_DIR/$route"
cp "$DIST_DIR/index.html" "$DIST_DIR/$route/index.html"
done
echo -e "${GREEN}OK${NC} ${#ROUTES[@]} rotas estaticas criadas"
# Criar 404.html (copia do index.html para fallback)
cp "$DIST_DIR/index.html" "$DIST_DIR/404.html"
echo -e "${GREEN}OK${NC} 404.html criado"
# Limpar backup
rm -f "$DIST_DIR/index.html.bak"
echo ""
# Gerar HTML prerenderizado com conteudo real (SEO)
echo ""
echo -e "${BLUE}[Prerender]${NC} Gerando HTML com conteudo real para SEO..."
node "${SCRIPT_DIR}/prerender.cjs"
echo ""
echo -e "${GREEN}Post-build concluido!${NC}"
echo ""
echo "Estrutura gerada:"
echo " dist/index.html (home)"
echo " dist/404.html (pagina nao encontrada)"
echo " dist/{rota}/index.html (${#ROUTES[@]} paginas estaticas)"
echo " dist/{rota}/index.html (HTML com conteudo real para SEO)"
echo ""
+15
View File
@@ -47,6 +47,13 @@ show_banner() {
echo ""
}
# Post-build: gerar paginas estaticas para rotas
run_post_build() {
local dist_dir="${1:-dist}"
echo -e "${BLUE}[Post-Build]${NC} Gerando paginas estaticas para rotas..."
bash "${SCRIPT_DIR}/build-static-routes.sh" "$dist_dir"
}
# Verificar dependencias
check_deps() {
if ! command -v node &> /dev/null; then
@@ -86,6 +93,10 @@ cmd_build() {
echo -e "${BLUE}[2/2]${NC} Configurando robots.txt para producao..."
cp "${PROJECT_DIR}/public/robots-producao.txt" "${DIST_DIR}/robots.txt"
echo -e "${GREEN}OK${NC} - Build de producao pronto em dist/"
# Gerar paginas estaticas para rotas
run_post_build "$DIST_DIR"
echo ""
echo -e "Para preview: ${CYAN}bash scripts/dev.sh preview${NC}"
}
@@ -101,6 +112,10 @@ cmd_build_hom() {
echo -e "${YELLOW}[2/2]${NC} Configurando anti-indexacao..."
# robots.txt ja esta com noindex por padrao
echo -e "${GREEN}OK${NC} - Build de homologacao pronto em dist/"
# Gerar paginas estaticas para rotas
run_post_build "$DIST_DIR"
echo ""
echo -e "Para deploy: ${CYAN}bash scripts/dev.sh deploy-hom${NC}"
}
+260
View File
@@ -0,0 +1,260 @@
// ============================================================================
// PRERENDER - Gera HTML estatico com conteudo real para cada rota
// ============================================================================
// Executa apos npm run build
// Gera index.html com conteudo HTML real (nao vazio) para cada rota
// Isso resolve o problema de SEO: Google recebe conteudo no 1o request
// ============================================================================
const fs = require('fs');
const path = require('path');
const DIST_DIR = path.resolve(__dirname, '../dist');
const INDEX_HTML = path.join(DIST_DIR, 'index.html');
// Verificar se dist existe
if (!fs.existsSync(DIST_DIR)) {
console.error('ERRO: pasta dist/ nao encontrada. Execute npm run build primeiro.');
process.exit(1);
}
// Ler o index.html base
let baseHtml = fs.readFileSync(INDEX_HTML, 'utf-8');
// Rotas para prerenderizar
const ROUTES = [
{ path: '/', title: 'Avanzato Tecnologia | TI Gerenciada, Cloud e Seguranca em Guarulhos/SP', desc: 'Solucoes em TI Gerenciada, Cloud Computing e Seguranca da Informacao. +23 anos de experiencia, +500 clientes atendidos. Suporte 24/7.' },
{ path: '/sobre', title: 'Sobre a Avanzato Tecnologia | +23 Anos de Experiencia em TI', desc: 'Conheca a Avanzato Tecnologia. +23 anos de experiencia em TI Gerenciada, Cloud e Seguranca. +500 clientes satisfeitos em Sao Paulo.' },
{ path: '/cases', title: 'Cases de Sucesso - Avanzato Tecnologia | Projetos de TI', desc: 'Conheca nossos cases de sucesso em TI. Empresas de contabilidade, advocacia, industria, monitoramento e mais. +23 anos de experiencia.' },
{ path: '/onde-atuamos', title: 'Onde Atuamos - Suporte de TI em Sao Paulo e Regiao', desc: 'Avanzato Tecnologia atende em Guarulhos, Mogi das Cruzes, Ipiranga, Zona Leste, Zona Norte, Centro e Ribeirao Preto. Suporte presencial e remoto.' },
{ path: '/avaliacao', title: 'Avaliacao Gratuita de TI - Avanzato Tecnologia', desc: 'Avalie gratuitamente a maturidade da TI da sua empresa. Descubra o nivel e receba recomendacoes personalizadas.' },
{ path: '/contato', title: 'Contato - Avanzato Tecnologia | TI em Guarulhos/SP', desc: 'Entre em contato com a Avanzato Tecnologia. Telefone (11) 4810-1704, WhatsApp, email ou formulario. Atendimento 24/7.' },
{ path: '/servicos/ti-gerenciada', title: 'TI Gerenciada para Empresas - Avanzato Tecnologia', desc: 'TI Gerenciada completa para empresas em Guarulhos e Sao Paulo. Suporte 24/7, monitoramento em tempo real e equipe especializada.' },
{ path: '/servicos/cloud-computing', title: 'Cloud Computing e Virtualizacao - Avanzato', desc: 'Solucoes em Cloud Computing: AWS, Azure, Google Cloud. Migracao, gestao e otimizacao de infraestrutura em nuvem.' },
{ path: '/servicos/seguranca', title: 'Seguranca da Informacao - Avanzato Tecnologia', desc: 'Protecao completa para sua empresa: firewall, antivirus, backup, LGPD. Seguranca de rede e dados com suporte 24/7.' },
{ path: '/servicos/suporte', title: 'Suporte Tecnico 24/7 - Avanzato Tecnologia', desc: 'Suporte tecnico empresarial 24 horas. Help desk, atendimento remoto e presencial em Guarulhos e regiao.' },
{ path: '/ti-para-contabilidades', title: 'TI para Escritorios de Contabilidade - Avanzato', desc: 'Solucoes de TI especializadas para contabilidades: seguranca de dados fiscais, backup de obrigacoes, NF-e. SPED e ECD.' },
{ path: '/ti-para-advocacias', title: 'TI para Escritorios de Advocacia - Avanzato', desc: 'TI para advocacias: seguranca de processos, LGPD para clientes, backup de documentos juridicos. PJe e e-SAJ.' },
{ path: '/monitoramento-empresarial', title: 'Monitoramento Empresarial com CFTV - Avanzato', desc: 'Sistemas de CFTV e monitoramento por camera para empresas. IP cameras, NVR, DVR, acesso remoto via celular.' },
{ path: '/firewall-pfsense-empresarial', title: 'Firewall pfSense Empresarial - Avanzato', desc: 'Implementacao e configuracao de firewall pfSense e OPNsense. Seguranca de rede, VPN, filtro de conteudo.' },
{ path: '/backup-corporativo', title: 'Backup Corporativo e Disaster Recovery - Avanzato', desc: 'Backup automatizado em nuvem e local. Disaster Recovery, recuperacao de dados. Protecao contra ransomware.' },
{ path: '/voip-empresarial', title: 'VoIP Empresarial e PABX IP - Avanzato Tecnologia', desc: 'Telefonia VoIP para empresas: PABX IP, chamadas ilimitadas, integracao com CRM. Reducao de 70% na conta telefonica.' },
{ path: '/suporte-ti-guarulhos', title: 'Suporte de TI em Guarulhos - Matriz Avanzato', desc: 'Suporte de TI empresarial em Guarulhos. Atendemos contabilidades, advocacias, industrias e mais. Matriz da Avanzato. +23 anos.' },
{ path: '/suporte-ti-mogi-das-cruzes', title: 'Suporte de TI em Mogi das Cruzes - Avanzato', desc: 'Suporte de TI empresarial em Mogi das Cruzes. Atendimento presencial e remoto para empresas de todos os portes.' },
{ path: '/suporte-ti-ipiranga', title: 'Suporte de TI em Ipiranga - Avanzato Tecnologia', desc: 'Suporte de TI empresarial em Ipiranga, Sao Paulo. Atendimento a clinicas, escolas e comercios do bairro.' },
{ path: '/suporte-ti-industria', title: 'TI para Industria - Avanzato Tecnologia', desc: 'Solucoes de TI especializadas para industrias. Suporte tecnico, seguranca, cloud e comunicacao. +23 anos de experiencia.' },
{ path: '/suporte-ti-portaria-remota', title: 'TI para Portaria Remota - Avanzato Tecnologia', desc: 'Solucoes de TI para portaria remota: interfornia IP, controle de acesso, cameras. Condominios, empresas, industrias.' },
{ path: '/suporte-ti-clinicas', title: 'TI para Clinicas - Avanzato Tecnologia', desc: 'Solucoes de TI para clinicas: odontologia, fisioterapia, medica, veterinaria. LGPD, prontuario digital, telemedicina.' },
{ path: '/suporte-ti-imobiliarias', title: 'TI para Imobiliarias - Avanzato Tecnologia', desc: 'Solucoes de TI para imobiliarias: sistemas de gestao, CRM, site de imoveis. Residencial, comercial, industrial.' },
{ path: '/tecnologias', title: 'Tecnologias que Trabalhamos - Avanzato Tecnologia', desc: 'Conheca as tecnologias que a Avanzato domina: Cisco, VMware, Microsoft, Linux, pfSense, Proxmox e muito mais.' },
// Avanzato Tools
{ path: '/ferramentas', title: 'Ferramentas Gratuitas de TI - Avanzato Tools', desc: 'Ferramentas gratuitas para diagnosticar seguranca de e-mail, SSL, Microsoft 365 e infraestrutura de TI. SPF Checker, DKIM Checker, DMARC Checker, SSL Checker e mais.' },
{ path: '/ferramentas/spf-checker', title: 'SPF Checker - Verifique sua Protecao de E-mail | Avanzato', desc: 'Verifique se o registro SPF do seu dominio esta configurado corretamente. Proteja contra spoofing e phishing. Ferramenta gratuita.' },
{ path: '/ferramentas/dkim-checker', title: 'DKIM Checker - Valide Assinatura Digital de E-mail | Avanzato', desc: 'Valide a assinatura digital DKIM do seu dominio. Garanta que seus e-mails sejam autenticados e nao vao para spam.' },
{ path: '/ferramentas/dmarc-checker', title: 'DMARC Checker - Analise sua Politica DMARC | Avanzato', desc: 'Analise a politica DMARC do seu dominio. Descubra se voce esta protegido contra phishing e spoofing de e-mail.' },
{ path: '/ferramentas/mx-lookup', title: 'MX Lookup - Consulte seus Servidores de E-mail | Avanzato', desc: 'Consulte os registros MX do seu dominio. Verifique a infraestrutura de recebimento de e-mails e o provedor.' },
{ path: '/ferramentas/ssl-checker', title: 'SSL Checker - Verifique seu Certificado SSL | Avanzato', desc: 'Verifique a validade e configuracao do certificado SSL do seu site. Garanta seguranca HTTPS para seus visitantes.' },
{ path: '/ferramentas/email-security-score', title: 'Email Security Score - Avaliacao Completa | Avanzato', desc: 'Avalie a seguranca completa do e-mail do seu dominio. Score de 0 a 100 com SPF, DKIM, DMARC, MX e recomendacoes.' },
{ path: '/ferramentas/microsoft-365-analyzer', title: 'Microsoft 365 Analyzer - Diagnostico M365 | Avanzato', desc: 'Analise a configuracao do Microsoft 365 do seu dominio. Descubra oportunidades de melhoria e seguranca.' },
];
// Conteudo HTML prerenderizado para cada tipo de pagina
function generateContent(route) {
const isHome = route.path === '/';
const isLocal = route.path.includes('suporte-ti-');
const isService = route.path.includes('servicos/');
const isNicho = route.path.includes('ti-para-') || route.path.includes('suporte-ti-') && !isLocal;
let content = '';
if (isHome) {
content = `
<section class="hero">
<h1>Avanzato Tecnologia - TI Gerenciada, Cloud e Seguranca</h1>
<p>+23 anos de experiencia em TI para empresas. +500 clientes atendidos em Guarulhos, Sao Paulo e regiao.</p>
<p>Servicos: Suporte Tecnico 24/7, Cloud Computing, Seguranca da Informacao, Backup Corporativo, VoIP, Firewall, Monitoramento.</p>
<div><a href="/avaliacao">Avaliacao Gratuita</a> <a href="/contato">Falar com Especialista</a></div>
</section>
<section>
<h2>Por que escolher a Avanzato?</h2>
<ul>
<li>Suporte 24/7 com equipe especializada</li>
<li>+23 anos de experiencia em TI empresarial</li>
<li>+500 clientes atendidos em diversos segmentos</li>
<li>Atendimento em Guarulhos, Grande SP e interior</li>
<li>Relatorios tecnicos mensais detalhados</li>
</ul>
</section>
<section>
<h2>Segmentos que Atendemos</h2>
<p>Contabilidades, Advocacias, Industrias, BPO, Clinicas, Imobiliarias, Condominios, Logistica, Monitoramento.</p>
<p><a href="/onde-atuamos">Ver onde atuamos</a></p>
</section>
<section>
<h2>Onde Atuamos</h2>
<p>Guarulhos (matriz), Mogi das Cruzes, Ipiranga, Vila Formosa, Cerqueira Cesar, Santana, Bras, Ribeirao Preto.</p>
<p><a href="/onde-atuamos">Ver todas as localidades</a></p>
</section>`;
} else if (isLocal) {
const city = route.title.split(' em ')[1]?.split(' - ')[0] || '';
content = `
<article>
<h1>Suporte de TI em ${city}</h1>
<p>Empresa de suporte de TI em ${city} com atendimento presencial e remoto. +23 anos de experiencia.</p>
<h2>Servicos em ${city}</h2>
<ul>
<li>Suporte tecnico 24/7</li>
<li>Administracao de servidores</li>
<li>Backup corporativo</li>
<li>Firewall e seguranca</li>
<li>Microsoft 365</li>
<li>Cloud Computing</li>
<li>VoIP e PABX IP</li>
<li>Monitoramento CFTV</li>
</ul>
<h2>Segmentos Atendidos em ${city}</h2>
<p>Contabilidades, Advocacias, Industrias, Clinicas, Imobiliarias, Condominios, Logistica, Monitoramento.</p>
<div><a href="/avaliacao">Solicitar Avaliacao Gratuita</a></div>
</article>`;
} else if (route.path === '/onde-atuamos') {
content = `
<article>
<h1>Onde a Avanzato Atua</h1>
<p>Presente em toda a regiao metropolitana de Sao Paulo e interior. Atendimento presencial e remoto.</p>
<h2>Cidades Atendidas</h2>
<ul>
<li><a href="/suporte-ti-guarulhos">Guarulhos</a> - Matriz Avanzato</li>
<li><a href="/suporte-ti-mogi-das-cruzes">Mogi das Cruzes</a></li>
<li><a href="/suporte-ti-ipiranga">Ipiranga</a></li>
<li><a href="/suporte-ti-vila-formosa">Vila Formosa</a></li>
<li><a href="/suporte-ti-cerqueira-cesar">Cerqueira Cesar</a></li>
<li><a href="/suporte-ti-santana">Santana</a></li>
<li><a href="/suporte-ti-bras">Bras</a></li>
<li><a href="/suporte-ti-ribeirao-preto">Ribeirao Preto</a></li>
</ul>
<div><a href="/contato">Falar com Especialista</a></div>
</article>`;
} else if (route.path === '/ferramentas') {
content = `
<article>
<h1>Avanzato Tools - Ferramentas Gratuitas de TI</h1>
<p>Ferramentas gratuitas de diagnostico tecnico para empresas. Verifique a seguranca do seu e-mail, SSL, Microsoft 365 e infraestrutura de TI.</p>
<h2>Ferramentas Disponiveis</h2>
<ul>
<li><a href="/ferramentas/spf-checker">SPF Checker</a> - Verifique sua protecao contra spoofing</li>
<li><a href="/ferramentas/dkim-checker">DKIM Checker</a> - Valide assinatura digital de e-mail</li>
<li><a href="/ferramentas/dmarc-checker">DMARC Checker</a> - Analise sua politica anti-phishing</li>
<li><a href="/ferramentas/mx-lookup">MX Lookup</a> - Consulte servidores de e-mail</li>
<li><a href="/ferramentas/ssl-checker">SSL Checker</a> - Verifique certificado SSL</li>
<li><a href="/ferramentas/email-security-score">Email Security Score</a> - Avaliacao completa</li>
<li><a href="/ferramentas/microsoft-365-analyzer">Microsoft 365 Analyzer</a> - Diagnostico M365</li>
</ul>
<p>2 consultas gratuitas por ferramenta. Cadastro rapido para consultas ilimitadas.</p>
<div><a href="/avaliacao">Avaliacao Gratuita de TI</a> <a href="/contato">Falar com Especialista</a></div>
</article>`;
} else if (route.path.startsWith('/ferramentas/')) {
const toolName = route.title.split(' - ')[0];
content = `
<article>
<h1>${toolName}</h1>
<p>${route.desc}</p>
<h2>Como usar</h2>
<ol>
<li>Digite o dominio que deseja verificar (ex: avanzato.com.br)</li>
<li>Clique em "Verificar"</li>
<li>Receba o resultado em segundos com recomendacoes</li>
</ol>
<h2>Perguntas Frequentes</h2>
<p><strong>Preciso cadastrar?</strong> Nao nas primeiras 2 consultas. Apos isso, cadastro rapido gratuito.</p>
<p><strong>Os resultados sao confiaveis?</strong> Sim, consultamos servidores DNS publicos em tempo real.</p>
<div><a href="/ferramentas">Ver todas as ferramentas</a> <a href="/avaliacao">Avaliacao Gratuita</a></div>
</article>`;
} else if (route.path === '/cases') {
content = `
<article>
<h1>Cases de Sucesso - Avanzato Tecnologia</h1>
<p>Conheca nossos cases de sucesso em TI. Empresas de diversos segmentos que confiam na Avanzato.</p>
<h2>Cases Recentes</h2>
<ul>
<li><strong>Apoio 24 Horas</strong> - Monitoramento - Servidores, pfSense, VPN</li>
<li><strong>ONG/Terceiro Setor</strong> - Institucional - Suporte, Cloud, Seguranca</li>
<li><strong>Museu de Faculdade Publica</strong> - Acervo Digital - Tainacan, ICA-AtoM</li>
<li><strong>Thalls</strong> - Industria - Suporte, VoIP</li>
<li><strong>Dellacont</strong> - Contabilidade - Suporte, Servidor, Backup</li>
<li><strong>Mourad Naddi Advogados</strong> - Advocacia - Suporte, Servidor, Backup</li>
<li><strong>GateOne e Fox</strong> - Monitoramento - Servidores, VPN</li>
<li><strong>Lucena Textil</strong> - Textil - Colocation, VPN, Backup</li>
</ul>
<div><a href="/avaliacao">Quero um case de sucesso tambem</a></div>
</article>`;
} else if (isService || isNicho) {
const serviceName = route.title.split(' - ')[0];
content = `
<article>
<h1>${serviceName}</h1>
<p>${route.desc}</p>
<h2>O que oferecemos</h2>
<ul>
<li>Diagnostico inicial gratuito</li>
<li>Projeto personalizado</li>
<li>Implantacao completa</li>
<li>Suporte continuo</li>
<li>Relatorios periodicos</li>
</ul>
<h2>Perguntas Frequentes</h2>
<p><strong>Qual o tempo de resposta?</strong> Nosso SLA garante atendimento em ate 4 horas para chamados criticos.</p>
<p><strong>Atendem empresas de qual porte?</strong> Atendemos de pequenas a grandes empresas, com planos flexiveis.</p>
<div><a href="/avaliacao">Solicitar Avaliacao Gratuita</a> <a href="/contato">Falar com Especialista</a></div>
</article>`;
} else {
content = `
<article>
<h1>${route.title.split(' - ')[0]}</h1>
<p>${route.desc}</p>
<div><a href="/contato">Entrar em Contato</a> <a href="/avaliacao">Avaliacao Gratuita</a></div>
</article>`;
}
return content;
}
// Gerar HTML prerenderizado para cada rota
console.log('[Prerender] Gerando HTML estatico com conteudo real...\n');
let count = 0;
for (const route of ROUTES) {
// Atualizar title e meta description
let html = baseHtml
.replace(/<title>.*?<\/title>/, `<title>${route.title}</title>`)
.replace(/<meta name="title" content=".*?" \/>/, `<meta name="title" content="${route.title}" />`)
.replace(/<meta name="description" content=".*?" \/>/, `<meta name="description" content="${route.desc}" />`)
.replace(/<meta name="app-version" content=".*?" \/>/, `<meta name="app-version" content="${require('../public/version.json').version}" />`);
// Gerar conteudo HTML
const content = generateContent(route);
// Inserir conteudo no div#root (substituir <div id="root"></div>)
html = html.replace(
'<div id="root"></div>',
`<div id="root">${content}</div>`
);
// Determinar caminho de saida
let outputPath;
if (route.path === '/') {
outputPath = path.join(DIST_DIR, 'index.html');
} else {
// Criar pasta e salvar index.html
const dir = path.join(DIST_DIR, route.path.replace(/^\//, ''));
fs.mkdirSync(dir, { recursive: true });
outputPath = path.join(dir, 'index.html');
}
fs.writeFileSync(outputPath, html);
count++;
console.log(` OK: ${route.path}${outputPath.replace(DIST_DIR + '/', '')}`);
}
console.log(`\n[Prerender] ${count} paginas geradas com conteudo HTML real!`);
console.log('[Prerender] Agora o Google recebe conteudo no 1o request, sem precisar de JavaScript.\n');
+169 -102
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { HashRouter as Router, Routes, Route, useLocation } from 'react-router-dom';
import { useEffect, useState, lazy, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import Navigation from './sections/Navigation';
@@ -8,50 +8,80 @@ import WhatsAppWidget from './components/WhatsAppWidget';
import CookieConsent from './components/CookieConsent';
import HomologationBanner from './components/HomologationBanner';
// Páginas
// Página inicial carrega direto (mais acessada)
import Home from './pages/Home';
import Sobre from './pages/Sobre';
import Blog from './pages/Blog';
import BlogPost from './pages/BlogPost';
import Cases from './pages/Cases';
import Contato from './pages/Contato';
import Privacidade from './pages/Privacidade';
import Termos from './pages/Termos';
import NotFound from './pages/NotFound';
// Todas as outras páginas com lazy loading para reduzir bundle inicial
const Sobre = lazy(() => import('./pages/Sobre'));
const Blog = lazy(() => import('./pages/Blog'));
const BlogPost = lazy(() => import('./pages/BlogPost'));
const Cases = lazy(() => import('./pages/Cases'));
const Contato = lazy(() => import('./pages/Contato'));
const Privacidade = lazy(() => import('./pages/Privacidade'));
const Termos = lazy(() => import('./pages/Termos'));
// Páginas de Serviços
import TiGerenciada from './pages/services/TiGerenciada';
import CloudComputing from './pages/services/CloudComputing';
import Seguranca from './pages/services/Seguranca';
import Suporte from './pages/services/Suporte';
import Redes from './pages/services/Redes';
import Pabx from './pages/services/Pabx';
import Backup from './pages/services/Backup';
import Servidores from './pages/services/Servidores';
import Consultoria from './pages/services/Consultoria';
import Noc from './pages/services/Noc';
const TiGerenciada = lazy(() => import('./pages/services/TiGerenciada'));
const CloudComputing = lazy(() => import('./pages/services/CloudComputing'));
const Seguranca = lazy(() => import('./pages/services/Seguranca'));
const Suporte = lazy(() => import('./pages/services/Suporte'));
const Redes = lazy(() => import('./pages/services/Redes'));
const Pabx = lazy(() => import('./pages/services/Pabx'));
const Backup = lazy(() => import('./pages/services/Backup'));
const Servidores = lazy(() => import('./pages/services/Servidores'));
const Consultoria = lazy(() => import('./pages/services/Consultoria'));
const Noc = lazy(() => import('./pages/services/Noc'));
// Páginas de Tecnologias (SEO)
import Tecnologias from './pages/Tecnologias';
import WindowsServer from './pages/tech/WindowsServer';
import Linux from './pages/tech/Linux';
import PfSense from './pages/tech/PfSense';
import Proxmox from './pages/tech/Proxmox';
import Vmware from './pages/tech/Vmware';
import Sharepoint from './pages/tech/Sharepoint';
import Cisco from './pages/tech/Cisco';
const Tecnologias = lazy(() => import('./pages/Tecnologias'));
const WindowsServer = lazy(() => import('./pages/tech/WindowsServer'));
const Linux = lazy(() => import('./pages/tech/Linux'));
const PfSense = lazy(() => import('./pages/tech/PfSense'));
const Proxmox = lazy(() => import('./pages/tech/Proxmox'));
const Vmware = lazy(() => import('./pages/tech/Vmware'));
const Sharepoint = lazy(() => import('./pages/tech/Sharepoint'));
const Cisco = lazy(() => import('./pages/tech/Cisco'));
// Avaliador de Maturidade Tecnológica
import Avaliador from './pages/Avaliador';
import Condominios from './pages/Condominios';
const Avaliador = lazy(() => import('./pages/Avaliador'));
const Condominios = lazy(() => import('./pages/Condominios'));
// Landing Pages por Nicho
import Contabilidade from './pages/nichos/Contabilidade';
import Advocacia from './pages/nichos/Advocacia';
import Monitoramento from './pages/nichos/Monitoramento';
import FirewallPfSense from './pages/nichos/FirewallPfSense';
import BackupCorporativo from './pages/nichos/BackupCorporativo';
import VoipEmpresarial from './pages/nichos/VoipEmpresarial';
const Contabilidade = lazy(() => import('./pages/nichos/Contabilidade'));
const Advocacia = lazy(() => import('./pages/nichos/Advocacia'));
const Monitoramento = lazy(() => import('./pages/nichos/Monitoramento'));
const FirewallPfSense = lazy(() => import('./pages/nichos/FirewallPfSense'));
const BackupCorporativo = lazy(() => import('./pages/nichos/BackupCorporativo'));
const VoipEmpresarial = lazy(() => import('./pages/nichos/VoipEmpresarial'));
// SEO Local - Onde Atuamos
const OndeAtuamos = lazy(() => import('./pages/OndeAtuamos'));
const LocalGuarulhos = lazy(() => import('./pages/local-guarulhos'));
const LocalMogi = lazy(() => import('./pages/local-mogi-das-cruzes'));
const LocalIpiranga = lazy(() => import('./pages/local-ipiranga'));
const LocalVilaFormosa = lazy(() => import('./pages/local-vila-formosa'));
const LocalCerqueiraCesar = lazy(() => import('./pages/local-cerqueira-cesar'));
const LocalSantana = lazy(() => import('./pages/local-santana'));
const LocalBras = lazy(() => import('./pages/local-bras'));
const LocalRibeiraoPreto = lazy(() => import('./pages/local-ribeirao-preto'));
// Nichos adicionais
const NichoIndustria = lazy(() => import('./pages/nicho-industria'));
const NichoPortaria = lazy(() => import('./pages/nicho-portaria-remota'));
const NichoClinicas = lazy(() => import('./pages/nicho-clinicas'));
const NichoImobiliarias = lazy(() => import('./pages/nicho-imobiliarias'));
const NichoRecrutamento = lazy(() => import('./pages/nicho-recrutamento-selecao'));
// Avanzato Tools
const Ferramentas = lazy(() => import('./pages/ferramentas'));
const SpfChecker = lazy(() => import('./pages/ferramentas/SpfChecker'));
const DkimChecker = lazy(() => import('./pages/ferramentas/DkimChecker'));
const DmarcChecker = lazy(() => import('./pages/ferramentas/DmarcChecker'));
const MxLookup = lazy(() => import('./pages/ferramentas/MxLookup'));
const SslChecker = lazy(() => import('./pages/ferramentas/SslChecker'));
const EmailSecurityScore = lazy(() => import('./pages/ferramentas/EmailSecurityScore'));
const Microsoft365Analyzer = lazy(() => import('./pages/ferramentas/Microsoft365Analyzer'));
import './App.css';
@@ -60,20 +90,20 @@ gsap.registerPlugin(ScrollTrigger);
// Componente para scroll ao topo ao mudar de página
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
// Refresh ScrollTrigger ao mudar de página
ScrollTrigger.refresh();
}, [pathname]);
return null;
}
// Componente de Progress Bar
function ProgressBar() {
const [scrollProgress, setScrollProgress] = useState(0);
useEffect(() => {
const handleScroll = () => {
const scrollTop = window.scrollY;
@@ -85,7 +115,7 @@ function ProgressBar() {
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<div
className="progress-bar"
@@ -94,6 +124,13 @@ function ProgressBar() {
);
}
// Loading simples para lazy load
const PageLoader = () => (
<div className="min-h-screen bg-[#0e0e0e] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#cbf400] border-t-transparent rounded-full animate-spin" />
</div>
);
function App() {
return (
<Router>
@@ -101,70 +138,100 @@ function App() {
<div className="relative bg-[#0e0e0e] min-h-screen">
<ProgressBar />
<Navigation />
<main>
<Routes>
{/* Página Inicial */}
<Route path="/" element={<Home />} />
{/* Sobre */}
<Route path="/sobre" element={<Sobre />} />
{/* Serviços */}
<Route path="/servicos/ti-gerenciada" element={<TiGerenciada />} />
<Route path="/servicos/cloud-computing" element={<CloudComputing />} />
<Route path="/servicos/seguranca" element={<Seguranca />} />
<Route path="/servicos/suporte" element={<Suporte />} />
<Route path="/servicos/redes" element={<Redes />} />
<Route path="/servicos/pabx" element={<Pabx />} />
<Route path="/servicos/backup" element={<Backup />} />
<Route path="/servicos/servidores" element={<Servidores />} />
<Route path="/servicos/consultoria" element={<Consultoria />} />
<Route path="/servicos/noc" element={<Noc />} />
{/* Blog */}
<Route path="/blog" element={<Blog />} />
<Route path="/blog/:slug" element={<BlogPost />} />
{/* Cases */}
<Route path="/cases" element={<Cases />} />
{/* Tecnologias (SEO) */}
<Route path="/tecnologias" element={<Tecnologias />} />
<Route path="/suporte-windows-server" element={<WindowsServer />} />
<Route path="/suporte-linux-empresas" element={<Linux />} />
<Route path="/suporte-pfsense" element={<PfSense />} />
<Route path="/suporte-proxmox" element={<Proxmox />} />
<Route path="/suporte-vmware" element={<Vmware />} />
<Route path="/suporte-sharepoint" element={<Sharepoint />} />
<Route path="/suporte-redes-cisco" element={<Cisco />} />
{/* Avaliador de Maturidade */}
<Route path="/avaliacao" element={<Avaliador />} />
{/* Tecnologia para Condomínios */}
<Route path="/tecnologia-para-condominios" element={<Condominios />} />
{/* Landing Pages por Nicho */}
<Route path="/ti-para-contabilidades" element={<Contabilidade />} />
<Route path="/ti-para-advocacias" element={<Advocacia />} />
<Route path="/monitoramento-empresarial" element={<Monitoramento />} />
<Route path="/firewall-pfsense-empresarial" element={<FirewallPfSense />} />
<Route path="/backup-corporativo" element={<BackupCorporativo />} />
<Route path="/voip-empresarial" element={<VoipEmpresarial />} />
{/* Contato */}
<Route path="/contato" element={<Contato />} />
{/* Páginas Legais */}
<Route path="/privacidade" element={<Privacidade />} />
<Route path="/termos" element={<Termos />} />
{/* 404 */}
<Route path="*" element={<NotFound />} />
</Routes>
<Suspense fallback={<PageLoader />}>
<Routes>
{/* Página Inicial - carrega direto */}
<Route path="/" element={<Home />} />
{/* Sobre */}
<Route path="/sobre" element={<Sobre />} />
{/* Serviços */}
<Route path="/servicos/ti-gerenciada" element={<TiGerenciada />} />
<Route path="/servicos/cloud-computing" element={<CloudComputing />} />
<Route path="/servicos/seguranca" element={<Seguranca />} />
<Route path="/servicos/suporte" element={<Suporte />} />
<Route path="/servicos/redes" element={<Redes />} />
<Route path="/servicos/pabx" element={<Pabx />} />
<Route path="/servicos/backup" element={<Backup />} />
<Route path="/servicos/servidores" element={<Servidores />} />
<Route path="/servicos/consultoria" element={<Consultoria />} />
<Route path="/servicos/noc" element={<Noc />} />
{/* Blog */}
<Route path="/blog" element={<Blog />} />
<Route path="/blog/:slug" element={<BlogPost />} />
{/* Cases */}
<Route path="/cases" element={<Cases />} />
{/* Tecnologias (SEO) */}
<Route path="/tecnologias" element={<Tecnologias />} />
<Route path="/suporte-windows-server" element={<WindowsServer />} />
<Route path="/suporte-linux-empresas" element={<Linux />} />
<Route path="/suporte-pfsense" element={<PfSense />} />
<Route path="/suporte-proxmox" element={<Proxmox />} />
<Route path="/suporte-vmware" element={<Vmware />} />
<Route path="/suporte-sharepoint" element={<Sharepoint />} />
<Route path="/suporte-redes-cisco" element={<Cisco />} />
{/* Avaliador de Maturidade */}
<Route path="/avaliacao" element={<Avaliador />} />
{/* Tecnologia para Condomínios */}
<Route path="/tecnologia-para-condominios" element={<Condominios />} />
{/* Landing Pages por Nicho */}
<Route path="/ti-para-contabilidades" element={<Contabilidade />} />
<Route path="/ti-para-advocacias" element={<Advocacia />} />
<Route path="/monitoramento-empresarial" element={<Monitoramento />} />
<Route path="/firewall-pfsense-empresarial" element={<FirewallPfSense />} />
<Route path="/backup-corporativo" element={<BackupCorporativo />} />
<Route path="/voip-empresarial" element={<VoipEmpresarial />} />
{/* SEO Local - Onde Atuamos */}
<Route path="/onde-atuamos" element={<OndeAtuamos />} />
<Route path="/suporte-ti-guarulhos" element={<LocalGuarulhos />} />
<Route path="/suporte-ti-mogi-das-cruzes" element={<LocalMogi />} />
<Route path="/suporte-ti-ipiranga" element={<LocalIpiranga />} />
<Route path="/suporte-ti-vila-formosa" element={<LocalVilaFormosa />} />
<Route path="/suporte-ti-cerqueira-cesar" element={<LocalCerqueiraCesar />} />
<Route path="/suporte-ti-santana" element={<LocalSantana />} />
<Route path="/suporte-ti-bras" element={<LocalBras />} />
<Route path="/suporte-ti-ribeirao-preto" element={<LocalRibeiraoPreto />} />
{/* Nichos adicionais */}
<Route path="/suporte-ti-industria" element={<NichoIndustria />} />
<Route path="/suporte-ti-portaria-remota" element={<NichoPortaria />} />
<Route path="/suporte-ti-clinicas" element={<NichoClinicas />} />
<Route path="/suporte-ti-imobiliarias" element={<NichoImobiliarias />} />
<Route path="/suporte-ti-recrutamento-selecao" element={<NichoRecrutamento />} />
{/* Contato */}
<Route path="/contato" element={<Contato />} />
{/* Páginas Legais */}
<Route path="/privacidade" element={<Privacidade />} />
<Route path="/termos" element={<Termos />} />
{/* Avanzato Tools */}
<Route path="/ferramentas" element={<Ferramentas />} />
<Route path="/ferramentas/spf-checker" element={<SpfChecker />} />
<Route path="/ferramentas/dkim-checker" element={<DkimChecker />} />
<Route path="/ferramentas/dmarc-checker" element={<DmarcChecker />} />
<Route path="/ferramentas/mx-lookup" element={<MxLookup />} />
<Route path="/ferramentas/ssl-checker" element={<SslChecker />} />
<Route path="/ferramentas/email-security-score" element={<EmailSecurityScore />} />
<Route path="/ferramentas/microsoft-365-analyzer" element={<Microsoft365Analyzer />} />
{/* 404 */}
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</main>
<Footer />
<WhatsAppWidget />
<CookieConsent />
+13 -10
View File
@@ -1,31 +1,34 @@
import { useState } from 'react';
import { AlertTriangle, X } from 'lucide-react';
import { IS_HOMOLOGATION } from '@/config/mautic';
import { ENV_CONFIG, IS_PRODUCTION } from '../config/environment';
/**
* Banner visual que aparece em ambiente de homologacao
* Avisa que Mautic, Google Ads e Facebook Pixel estao em modo simulacao
* Banner visual que aparece em ambiente NAO-producao
* Avisa qual ambiente esta rodando
*/
export default function HomologationBanner() {
export default function AmbientBanner() {
const [dismissed, setDismissed] = useState(false);
if (!IS_HOMOLOGATION || dismissed) return null;
// So mostra em producao
if (IS_PRODUCTION || dismissed) return null;
const { bannerText, bannerColor } = ENV_CONFIG;
return (
<div className="fixed bottom-4 right-4 z-[100] max-w-md">
<div className="bg-amber-500 text-[#0e0e0e] rounded-xl shadow-2xl p-4 border-2 border-amber-400">
<div className={`${bannerColor} text-[#0e0e0e] rounded-xl shadow-2xl p-4 border-2 border-white/20`}>
<div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 shrink-0 mt-0.5" />
<div className="flex-1">
<p className="font-bold text-sm">Ambiente de Homologacao</p>
<p className="font-bold text-sm">Ambiente: {bannerText}</p>
<p className="text-xs mt-1 opacity-90">
Mautic, Google Ads e Facebook Pixel estao em modo <strong>simulacao</strong>.
Nenhum dado real e enviado. Verifique o console (F12) para ver o que seria enviado.
Tracking desativado | SEO: noindex |
Nenhum dado enviado a Mautic, GA4, Ads ou Pixel.
</p>
</div>
<button
onClick={() => setDismissed(true)}
className="shrink-0 p-1 hover:bg-amber-600 rounded-lg transition-colors"
className="shrink-0 p-1 hover:bg-black/20 rounded-lg transition-colors"
aria-label="Fechar"
>
<X className="w-4 h-4" />
+121
View File
@@ -0,0 +1,121 @@
// ============================================================================
// SEO - Meta tags dinamicas por pagina
// ============================================================================
// Uso: <SEO title="..." description="..." />
//
// Cada pagina deve usar este componente com suas proprias tags.
// O canonical e gerado automaticamente a partir da URL atual.
// ============================================================================
import { Helmet } from 'react-helmet-async';
import { useLocation } from 'react-router-dom';
import { ENV_CONFIG } from '../config/environment';
interface SEOProps {
title?: string;
description?: string;
keywords?: string;
ogImage?: string;
ogType?: string;
noindex?: boolean; // Se true, forca noindex mesmo em producao
schema?: Record<string, any>;
}
const SITE_URL = 'https://avanzato.com.br';
const DEFAULT_TITLE = 'Avanzato Tecnologia | TI Gerenciada, Cloud e Seguranca';
const DEFAULT_DESC = 'Solucoes em TI Gerenciada, Cloud Computing e Seguranca da Informacao. +23 anos de experiencia, +500 clientes atendidos. Suporte 24/7 em Guarulhos e regiao.';
const DEFAULT_OG_IMAGE = 'https://avanzato.com.br/og-image.jpg';
export default function SEO({
title,
description,
keywords,
ogImage,
ogType = 'website',
noindex = false,
schema,
}: SEOProps) {
const location = useLocation();
const pathname = location.pathname;
const canonicalUrl = `${SITE_URL}${pathname}`;
const fullTitle = title ? `${title} | Avanzato` : DEFAULT_TITLE;
return (
<Helmet>
{/* Title */}
<title>{fullTitle}</title>
<meta name="title" content={fullTitle} />
{/* Description */}
{description && <meta name="description" content={description} />}
{!description && <meta name="description" content={DEFAULT_DESC} />}
{/* Keywords */}
{keywords && <meta name="keywords" content={keywords} />}
{/* Canonical - DINAMICO por pagina */}
<link rel="canonical" href={canonicalUrl} />
{/* Robots - automatico por ambiente, override com prop noindex */}
{noindex || !ENV_CONFIG.allowIndexing ? (
<meta name="robots" content="noindex, nofollow" />
) : (
<meta name="robots" content="index, follow" />
)}
{/* Open Graph */}
<meta property="og:type" content={ogType} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description || DEFAULT_DESC} />
<meta property="og:image" content={ogImage || DEFAULT_OG_IMAGE} />
<meta property="og:locale" content="pt_BR" />
<meta property="og:site_name" content="Avanzato Tecnologia" />
{/* Twitter */}
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={canonicalUrl} />
<meta property="twitter:title" content={fullTitle} />
<meta property="twitter:description" content={description || DEFAULT_DESC} />
<meta property="twitter:image" content={ogImage || DEFAULT_OG_IMAGE} />
{/* Schema.org */}
{schema && (
<script type="application/ld+json">
{JSON.stringify(schema)}
</script>
)}
</Helmet>
);
}
// Schema helpers
export const createServiceSchema = (name: string, description: string, urlPath: string) => ({
'@context': 'https://schema.org',
'@type': 'Service',
name,
description,
provider: {
'@type': 'Organization',
name: 'Avanzato Tecnologia',
url: SITE_URL,
},
url: `${SITE_URL}${urlPath}`,
areaServed: {
'@type': 'City',
name: 'Guarulhos',
},
});
export const createFAQSchema = (questions: Array<{q: string; a: string}>) => ({
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: questions.map(({q, a}) => ({
'@type': 'Question',
name: q,
acceptedAnswer: {
'@type': 'Answer',
text: a,
},
})),
});
@@ -0,0 +1,368 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { ArrowLeft, ArrowRight, CheckCircle, AlertTriangle, XCircle, Info, Loader2 } from 'lucide-react';
export interface ResultadoFerramenta {
status: 'success' | 'warning' | 'error' | 'info';
titulo: string;
mensagem: string;
detalhes?: Record<string, string>;
recomendacoes?: string[];
score?: number;
}
interface FerramentaLayoutProps {
nome: string;
descricao: string;
icone: React.ReactNode;
placeholder: string;
explicacao: string;
comoFunciona: string[];
perguntasUteis: { q: string; a: string }[];
onConsultar: (dominio: string) => Promise<ResultadoFerramenta>;
schema?: Record<string, any>;
}
const contagemKey = (slug: string) => `avanzato-tools-count-${slug}`;
const cadastroKey = 'avanzato-tools-cadastro';
const FerramentaLayout = ({
nome,
descricao,
icone,
placeholder,
explicacao,
comoFunciona,
perguntasUteis,
onConsultar,
}: FerramentaLayoutProps) => {
const [dominio, setDominio] = useState('');
const [resultado, setResultado] = useState<ResultadoFerramenta | null>(null);
const [loading, setLoading] = useState(false);
const [erro, setErro] = useState('');
const [mostrarCadastro, setMostrarCadastro] = useState(false);
const [cadastrado, setCadastrado] = useState(false);
const [consultas, setConsultas] = useState(() => {
const slug = window.location.pathname.split('/').pop() || '';
return parseInt(localStorage.getItem(contagemKey(slug)) || '0', 10);
});
const [feedbackUtil, setFeedbackUtil] = useState<'sim' | 'nao' | null>(null);
const [feedbackTexto, setFeedbackTexto] = useState('');
const [mostrarDor, setMostrarDor] = useState(false);
const [dorTI, setDorTI] = useState('');
const [urgencia, setUrgencia] = useState('');
const slug = window.location.pathname.split('/').pop() || '';
const podeConsultar = () => {
if (cadastrado) return true;
if (consultas < 2) return true;
return false;
};
const handleConsultar = async () => {
if (!dominio.trim()) {
setErro('Digite um dominio valido (ex: avanzato.com.br)');
return;
}
if (!podeConsultar()) {
setMostrarCadastro(true);
return;
}
setErro('');
setLoading(true);
setResultado(null);
try {
const res = await onConsultar(dominio.trim());
setResultado(res);
// Incrementar contagem
const novaContagem = consultas + 1;
setConsultas(novaContagem);
localStorage.setItem(contagemKey(slug), String(novaContagem));
// Se atingiu limite, mostrar cadastro
if (novaContagem >= 2 && !cadastrado) {
setTimeout(() => setMostrarDor(true), 2000);
}
} catch (err) {
setErro('Erro ao consultar. Verifique o dominio e tente novamente.');
} finally {
setLoading(false);
}
};
const handleCadastro = (e: React.FormEvent) => {
e.preventDefault();
setCadastrado(true);
localStorage.setItem(cadastroKey, 'true');
setMostrarCadastro(false);
};
const statusIcon = (status: string) => {
switch (status) {
case 'success': return <CheckCircle className="w-8 h-8 text-green-400" />;
case 'warning': return <AlertTriangle className="w-8 h-8 text-yellow-400" />;
case 'error': return <XCircle className="w-8 h-8 text-red-400" />;
default: return <Info className="w-8 h-8 text-blue-400" />;
}
};
const doresTI = [
'Seguranca da informacao',
'Backup',
'Microsoft 365',
'Firewall',
'Servidores',
'Lentidao da rede',
'VPN / Home Office',
'Suporte aos usuarios',
'Custos de TI',
'LGPD',
'Ransomware',
'Outro',
];
const urgencias = [
'Apenas pesquisando',
'Quero melhorar nos proximos meses',
'Preciso resolver este mes',
'Preciso resolver imediatamente',
];
return (
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="mb-8">
<Link to="/ferramentas" className="inline-flex items-center gap-2 text-gray-400 hover:text-[#cbf400] transition-colors mb-4">
<ArrowLeft className="w-4 h-4" />
Todas as ferramentas
</Link>
<div className="flex items-center gap-4 mb-4">
<div className="p-3 bg-[#cbf400]/10 rounded-xl text-[#cbf400]">{icone}</div>
<div>
<h1 className="text-3xl font-bold text-white">{nome}</h1>
<p className="text-gray-300">{descricao}</p>
</div>
</div>
</div>
{/* Formulario */}
<div className="bg-[#1a1a1a] rounded-2xl p-6 border border-[#2a2a2a] mb-8">
<div className="flex gap-4">
<input
type="text"
value={dominio}
onChange={(e) => setDominio(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleConsultar()}
placeholder={placeholder}
className="flex-1 bg-[#0e0e0e] border border-[#2a2a2a] rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:border-[#cbf400] focus:outline-none"
/>
<button
onClick={handleConsultar}
disabled={loading}
className="bg-[#cbf400] text-[#0e0e0e] px-6 py-3 rounded-xl font-semibold hover:bg-[#b8e000] transition-colors disabled:opacity-50 flex items-center gap-2"
>
{loading && <Loader2 className="w-4 h-4 animate-spin" />}
{loading ? 'Analisando...' : 'Verificar'}
</button>
</div>
{erro && <p className="text-red-400 mt-3 text-sm">{erro}</p>}
{!cadastrado && consultas >= 2 && (
<p className="text-yellow-400 mt-3 text-sm">
Voce usou {consultas} consultas. <button onClick={() => setMostrarCadastro(true)} className="underline hover:text-[#cbf400]">Cadastre-se gratuitamente</button> para consultas ilimitadas.
</p>
)}
</div>
{/* Resultado */}
{resultado && (
<div className={`bg-[#1a1a1a] rounded-2xl p-6 border mb-8 ${
resultado.status === 'success' ? 'border-green-500/30' :
resultado.status === 'warning' ? 'border-yellow-500/30' :
resultado.status === 'error' ? 'border-red-500/30' : 'border-blue-500/30'
}`}>
<div className="flex items-center gap-4 mb-4">
{statusIcon(resultado.status)}
<div>
<h2 className="text-xl font-semibold text-white">{resultado.titulo}</h2>
{resultado.score !== undefined && (
<div className="flex items-center gap-2 mt-1">
<div className="w-32 h-2 bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
resultado.score >= 80 ? 'bg-green-400' :
resultado.score >= 50 ? 'bg-yellow-400' : 'bg-red-400'
}`}
style={{ width: `${resultado.score}%` }}
/>
</div>
<span className="text-sm font-medium text-white">{resultado.score}/100</span>
</div>
)}
</div>
</div>
<p className="text-gray-300 mb-4">{resultado.mensagem}</p>
{resultado.detalhes && Object.entries(resultado.detalhes).length > 0 && (
<div className="bg-[#0e0e0e] rounded-xl p-4 mb-4">
{Object.entries(resultado.detalhes).map(([key, value]) => (
<div key={key} className="flex justify-between py-2 border-b border-[#2a2a2a] last:border-0">
<span className="text-gray-400">{key}</span>
<span className="text-white font-mono text-sm">{value}</span>
</div>
))}
</div>
)}
{resultado.recomendacoes && resultado.recomendacoes.length > 0 && (
<div>
<h3 className="text-sm font-semibold text-[#cbf400] mb-2">Recomendacoes</h3>
<ul className="space-y-2">
{resultado.recomendacoes.map((r, i) => (
<li key={i} className="flex items-start gap-2 text-gray-300 text-sm">
<CheckCircle className="w-4 h-4 text-[#cbf400] mt-0.5 shrink-0" />
{r}
</li>
))}
</ul>
</div>
)}
</div>
)}
{/* Cadastro */}
{mostrarCadastro && !cadastrado && (
<div className="bg-[#1a1a1a] rounded-2xl p-6 border border-[#cbf400]/30 mb-8">
<h2 className="text-xl font-semibold text-white mb-2">Cadastro Rapido Gratuito</h2>
<p className="text-gray-300 text-sm mb-4">
Voce ja usou 2 consultas gratuitas. Cadastre-se para consultas ilimitadas e relatorios completos.
</p>
<form onSubmit={handleCadastro} className="space-y-3">
<input type="text" placeholder="Nome" required className="w-full bg-[#0e0e0e] border border-[#2a2a2a] rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:border-[#cbf400] focus:outline-none" />
<input type="email" placeholder="E-mail corporativo" required className="w-full bg-[#0e0e0e] border border-[#2a2a2a] rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:border-[#cbf400] focus:outline-none" />
<input type="text" placeholder="Empresa" required className="w-full bg-[#0e0e0e] border border-[#2a2a2a] rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:border-[#cbf400] focus:outline-none" />
<p className="text-gray-500 text-xs">
Autorizo a Avanzato Tecnologia a enviar conteudos tecnicos, diagnosticos e informacoes sobre solucoes de TI, seguranca e Microsoft 365.
</p>
<button type="submit" className="bg-[#cbf400] text-[#0e0e0e] px-6 py-3 rounded-xl font-semibold hover:bg-[#b8e000] transition-colors">
Cadastrar e Continuar
</button>
</form>
</div>
)}
{/* Dor de TI */}
{mostrarDor && !cadastrado && (
<div className="bg-[#1a1a1a] rounded-2xl p-6 border border-yellow-500/30 mb-8">
<h2 className="text-lg font-semibold text-white mb-4">Qual e hoje sua maior dor em TI?</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 mb-4">
{doresTI.map(d => (
<button key={d} onClick={() => setDorTI(d)} className={`p-2 rounded-lg text-sm transition-colors ${dorTI === d ? 'bg-[#cbf400] text-[#0e0e0e]' : 'bg-[#0e0e0e] text-gray-300 hover:bg-[#2a2a2a]'}`}>
{d}
</button>
))}
</div>
{dorTI && (
<div className="mt-4">
<h3 className="text-sm font-semibold text-white mb-2">Qual a urgencia?</h3>
<div className="flex flex-wrap gap-2 mb-4">
{urgencias.map(u => (
<button key={u} onClick={() => setUrgencia(u)} className={`px-3 py-2 rounded-lg text-sm transition-colors ${urgencia === u ? 'bg-[#cbf400] text-[#0e0e0e]' : 'bg-[#0e0e0e] text-gray-300 hover:bg-[#2a2a2a]'}`}>
{u}
</button>
))}
</div>
</div>
)}
{dorTI && urgencia && (
<div className="flex gap-4 mt-4">
<Link to="/avaliacao" className="bg-[#cbf400] text-[#0e0e0e] px-6 py-3 rounded-xl font-semibold hover:bg-[#b8e000] transition-colors inline-flex items-center gap-2">
Solicitar Avaliacao Gratuita <ArrowRight className="w-4 h-4" />
</Link>
<button onClick={() => setMostrarCadastro(true)} className="border border-[#cbf400] text-[#cbf400] px-6 py-3 rounded-xl font-semibold hover:bg-[#cbf400]/10 transition-colors">
Cadastrar para Relatorio Completo
</button>
</div>
)}
</div>
)}
{/* Feedback */}
{resultado && (
<div className="bg-[#1a1a1a] rounded-2xl p-6 border border-[#2a2a2a] mb-8">
<h3 className="text-lg font-semibold text-white mb-3">Esta ferramenta foi util?</h3>
{feedbackUtil === null ? (
<div className="flex gap-4">
<button onClick={() => setFeedbackUtil('sim')} className="bg-green-500/20 text-green-400 px-6 py-2 rounded-lg hover:bg-green-500/30 transition-colors">
Sim, foi util
</button>
<button onClick={() => setFeedbackUtil('nao')} className="bg-red-500/20 text-red-400 px-6 py-2 rounded-lg hover:bg-red-500/30 transition-colors">
Nao, faltou algo
</button>
</div>
) : feedbackUtil === 'sim' ? (
<div>
<p className="text-green-400 mb-4">Que otimo! A Avanzato investe em ferramentas gratuitas para ajudar empresas a proteger sua infraestrutura de TI.</p>
<div className="flex flex-wrap gap-3">
<Link to="/avaliacao" className="bg-[#cbf400] text-[#0e0e0e] px-4 py-2 rounded-lg font-medium hover:bg-[#b8e000] transition-colors inline-flex items-center gap-2">
Solicitar Avaliacao Gratuita <ArrowRight className="w-4 h-4" />
</Link>
<button onClick={() => setMostrarCadastro(true)} className="border border-[#cbf400] text-[#cbf400] px-4 py-2 rounded-lg hover:bg-[#cbf400]/10 transition-colors">
Receber Relatorio Completo
</button>
</div>
</div>
) : (
<div>
<p className="text-gray-300 mb-2">O que faltou ou o que podemos melhorar?</p>
<textarea
value={feedbackTexto}
onChange={(e) => setFeedbackTexto(e.target.value)}
className="w-full bg-[#0e0e0e] border border-[#2a2a2a] rounded-xl px-4 py-3 text-white placeholder-gray-500 focus:border-[#cbf400] focus:outline-none h-24"
placeholder="Conte-nos como podemos melhorar..."
/>
</div>
)}
</div>
)}
{/* Conteudo explicativo */}
<div className="bg-[#1a1a1a] rounded-2xl p-6 border border-[#2a2a2a] mb-8">
<h2 className="text-xl font-semibold text-white mb-4">O que e {nome.split(' ')[0]}?</h2>
<p className="text-gray-300 mb-6">{explicacao}</p>
<h3 className="text-lg font-semibold text-white mb-3">Como funciona</h3>
<ol className="space-y-2 mb-6">
{comoFunciona.map((passo, i) => (
<li key={i} className="flex items-start gap-3 text-gray-300">
<span className="w-6 h-6 bg-[#cbf400]/20 text-[#cbf400] rounded-full flex items-center justify-center text-sm font-medium shrink-0">{i + 1}</span>
{passo}
</li>
))}
</ol>
</div>
{/* FAQ da ferramenta */}
{perguntasUteis.length > 0 && (
<div className="bg-[#1a1a1a] rounded-2xl p-6 border border-[#2a2a2a] mb-8">
<h2 className="text-xl font-semibold text-white mb-4">Perguntas Frequentes sobre {nome.split(' ')[0]}</h2>
<div className="space-y-4">
{perguntasUteis.map((faq, i) => (
<div key={i} className="border-b border-[#2a2a2a] pb-4 last:border-0">
<h3 className="text-[#cbf400] font-medium mb-1">{faq.q}</h3>
<p className="text-gray-300 text-sm">{faq.a}</p>
</div>
))}
</div>
</div>
)}
</div>
);
};
export default FerramentaLayout;
+373
View File
@@ -0,0 +1,373 @@
// ============================================================================
// DNS/SSL API - Consultas via APIs publicas
// ============================================================================
// Usa APIs gratuitas para consultar registros DNS, SSL, MX, SPF, DKIM, DMARC
// ============================================================================
import type { ResultadoFerramenta } from './FerramentaLayout';
// Helper para chamar a API do Google DNS
async function queryDNS(name: string, type: string): Promise<any> {
try {
const res = await fetch(`https://dns.google/resolve?name=${encodeURIComponent(name)}&type=${type}`);
return await res.json();
} catch {
return { Status: 2, Answer: [] };
}
}
// Verificar SPF
export async function checkSPF(dominio: string): Promise<ResultadoFerramenta> {
const data = await queryDNS(dominio, 'TXT');
const records = (data.Answer || []).filter((r: any) => r.data && r.data.includes('v=spf1'));
if (records.length === 0) {
return {
status: 'error',
titulo: 'SPF Nao Encontrado',
mensagem: `O dominio ${dominio} nao possui um registro SPF configurado. Sem SPF, qualquer pessoa pode enviar e-mails em nome do seu dominio, o que facilita ataques de phishing e spoofing.`,
detalhes: { Dominio: dominio, Registro: 'Nao encontrado' },
recomendacoes: [
'Crie um registro TXT no DNS com a politica SPF',
'Exemplo: v=spf1 include:_spf.google.com ~all (para Microsoft 365/Google Workspace)',
'Use o mecanismo ~all (softfail) inicialmente para testes',
'Monitore os logs antes de usar -all (hardfail)',
'Consulte a Avanzato para configuracao personalizada',
],
score: 0,
};
}
const spf = records[0].data.replace(/"/g, '');
const hasAll = spf.includes('-all') || spf.includes('~all') || spf.includes('?all');
const isHardFail = spf.includes('-all');
const includes = spf.match(/include:[^\s]+/g) || [];
const ip4 = spf.match(/ip4:[^\s]+/g) || [];
const ip6 = spf.match(/ip6:[^\s]+/g) || [];
let status: 'success' | 'warning' | 'error' = 'warning';
let score = 60;
let recomendacoes: string[] = [];
if (isHardFail) { status = 'success'; score = 90; }
else if (spf.includes('~all')) { status = 'warning'; score = 70; recomendacoes.push('Considere mudar ~all para -all para protecao mais forte'); }
else if (!hasAll) { status = 'error'; score = 30; recomendacoes.push('Adicione ~all ou -all ao final do seu SPF'); }
if (includes.length > 10) { score -= 20; recomendacoes.push('Voce tem muitos includes. Mais de 10 lookups pode quebrar o SPF'); }
return {
status,
titulo: status === 'success' ? 'SPF Configurado Corretamente' : 'SPF Configurado com Ressalvas',
mensagem: `O dominio ${dominio} possui um registro SPF ${isHardFail ? 'com protecao rigida (-all)' : hasAll ? 'mas sem protecao rigida' : 'mas sem mecanismo de falha'}.`,
detalhes: {
Dominio: dominio,
Registro: spf.substring(0, 100),
Mecanismo: isHardFail ? '-all (hardfail)' : spf.includes('~all') ? '~all (softfail)' : 'Sem all',
Includes: String(includes.length),
'IPs v4': String(ip4.length),
'IPs v6': String(ip6.length),
},
recomendacoes: recomendacoes.length > 0 ? recomendacoes : ['Seu SPF esta bem configurado!', 'Monitore regularmente para garantir que todos os servidores legitimos estejam incluidos'],
score,
};
}
// Verificar DKIM
export async function checkDKIM(dominio: string): Promise<ResultadoFerramenta> {
const selectors = ['default', 'google', 'selector1', 'selector2', 'mail', 'dkim'];
let found = false;
let validSelector = '';
let dkimRecord = '';
for (const selector of selectors) {
const data = await queryDNS(`${selector}._domainkey.${dominio}`, 'TXT');
if (data.Answer && data.Answer.length > 0) {
const record = data.Answer[0].data;
if (record.includes('v=DKIM1') || record.includes('k=rsa')) {
found = true;
validSelector = selector;
dkimRecord = record.replace(/"/g, '').substring(0, 80);
break;
}
}
}
if (!found) {
return {
status: 'warning',
titulo: 'DKIM Nao Encontrado',
mensagem: `Nao foi possivel encontrar um registro DKIM valido para ${dominio} com os selectors comuns. Isso nao significa que nao existe, mas pode indicar que nao esta configurado ou usa um selector personalizado.`,
detalhes: { Dominio: dominio, 'Selectors testados': selectors.join(', ') },
recomendacoes: [
'Verifique qual selector seu servidor de e-mail usa',
'Para Microsoft 365: selector1 e selector2',
'Para Google Workspace: google',
'Entre em contato com seu administrador de e-mail',
'A Avanzato pode configurar DKIM para seu dominio',
],
score: 30,
};
}
return {
status: 'success',
titulo: 'DKIM Configurado',
mensagem: `DKIM encontrado com o selector "${validSelector}". Seu dominio possui assinatura digital para autenticacao de e-mails.`,
detalhes: {
Dominio: dominio,
Selector: validSelector,
Registro: dkimRecord + '...',
},
recomendacoes: [
'DKIM esta configurado corretamente!',
'Certifique-se de que todos os servidores de e-mail assinam com DKIM',
'Renove as chaves DKIM periodicamente',
],
score: 85,
};
}
// Verificar DMARC
export async function checkDMARC(dominio: string): Promise<ResultadoFerramenta> {
const data = await queryDNS(`_dmarc.${dominio}`, 'TXT');
const records = (data.Answer || []).filter((r: any) => r.data && r.data.includes('v=DMARC1'));
if (records.length === 0) {
return {
status: 'error',
titulo: 'DMARC Nao Encontrado',
mensagem: `O dominio ${dominio} nao possui DMARC configurado. Sem DMARC, voce nao recebe relatorios sobre falhas de autenticacao e nao pode instruir provedores a rejeitar e-mails falsos.`,
detalhes: { Dominio: dominio, Registro: 'Nao encontrado' },
recomendacoes: [
'Crie um registro TXT em _dmarc.seudominio.com',
'Exemplo: v=DMARC1; p=quarantine; rua=mailto:dmarc@seudominio.com',
'Comece com p=none para monitorar, depois mude para quarantine ou reject',
'Configure rua para receber relatorios agregados',
'Consulte a Avanzato para configuracao completa',
],
score: 0,
};
}
const dmarc = records[0].data.replace(/"/g, '');
const policy = dmarc.match(/p=(\w+)/)?.[1] || 'none';
const rua = dmarc.match(/rua=([^;]+)/)?.[1] || 'Nao configurado';
let status: 'success' | 'warning' | 'error' = 'warning';
let score = 50;
if (policy === 'reject') { status = 'success'; score = 100; }
else if (policy === 'quarantine') { status = 'warning'; score = 75; }
else { status = 'error'; score = 40; }
return {
status,
titulo: policy === 'reject' ? 'DMARC com Protecao Maxima' : 'DMARC Configurado',
mensagem: `DMARC encontrado com politica "${policy}". ${policy === 'reject' ? 'E-mails que falham na autenticacao serao rejeitados.' : policy === 'quarantine' ? 'E-mails suspeitos vao para spam.' : 'Apenas monitorando, sem acao sobre e-mails falsos.'}`,
detalhes: {
Dominio: dominio,
Politica: policy,
'Relatorios (rua)': rua,
Registro: dmarc.substring(0, 80) + '...',
},
recomendacoes: policy === 'reject'
? ['Excelente! Sua protecao DMARC esta no nivel maximo']
: [`Considere mudar p=${policy} para p=reject para protecao total`, 'Monitore os relatorios rua antes de mudar para reject'],
score,
};
}
// MX Lookup
export async function checkMX(dominio: string): Promise<ResultadoFerramenta> {
const data = await queryDNS(dominio, 'MX');
const records = (data.Answer || []).sort((a: any, b: any) => a.preference - b.preference);
if (records.length === 0) {
return {
status: 'error',
titulo: 'Registros MX Nao Encontrados',
mensagem: `O dominio ${dominio} nao possui registros MX. Sem MX, o dominio nao pode receber e-mails.`,
detalhes: { Dominio: dominio, Status: 'Sem MX' },
recomendacoes: [
'Configure registros MX no DNS do seu dominio',
'Para Microsoft 365: siga o assistente de configuracao',
'Para Google Workspace: adicione os MX do Google',
],
score: 0,
};
}
const mxProviders = records.map((r: any) => r.data).join(', ');
const isMicrosoft = mxProviders.toLowerCase().includes('microsoft') || mxProviders.toLowerCase().includes('outlook') || mxProviders.toLowerCase().includes('protection');
const isGoogle = mxProviders.toLowerCase().includes('google') || mxProviders.toLowerCase().includes('aspmx');
const provider = isMicrosoft ? 'Microsoft 365 / Exchange Online' : isGoogle ? 'Google Workspace' : 'Outro / Servidor Proprio';
return {
status: 'success',
titulo: `${records.length} Servidor(es) MX Encontrado(s)`,
mensagem: `O dominio ${dominio} esta configurado para receber e-mails via ${provider}.`,
detalhes: {
Dominio: dominio,
'Servidores MX': String(records.length),
Provedor: provider,
'Prioridade mais alta': records[0].data,
},
recomendacoes: [
'Sua infraestrutura de e-mail esta configurada',
`Voce esta usando: ${provider}`,
'Mantenha pelo menos 2 servidores MX para redundancia',
],
score: 90,
};
}
// SSL Checker
export async function checkSSL(dominio: string): Promise<ResultadoFerramenta> {
try {
// Usar a API do crt.sh para verificar certificados
const res = await fetch(`https://crt.sh/?q=${encodeURIComponent(dominio)}&output=json`);
const data = await res.json();
if (!data || data.length === 0) {
return {
status: 'warning',
titulo: 'Certificado SSL Nao Encontrado',
mensagem: `Nao foi possivel encontrar informacoes de certificado SSL para ${dominio} no banco de dados publico. Isso pode significar que o site nao usa HTTPS ou o certificado nao e publico.`,
score: 30,
};
}
const cert = data[0];
const issuer = cert.issuer_name || 'Desconhecido';
const notAfter = cert.not_after ? new Date(cert.not_after) : null;
const diasRestantes = notAfter ? Math.floor((notAfter.getTime() - Date.now()) / (1000 * 60 * 60 * 24)) : 0;
const isExpired = diasRestantes < 0;
const isExpiringSoon = diasRestantes < 30 && diasRestantes > 0;
let status: 'success' | 'warning' | 'error' = 'success';
let score = 90;
if (isExpired) { status = 'error'; score = 0; }
else if (isExpiringSoon) { status = 'warning'; score = 50; }
return {
status,
titulo: isExpired ? 'Certificado SSL Expirado!' : isExpiringSoon ? 'Certificado SSL Vence em Breve' : 'Certificado SSL Valido',
mensagem: isExpired
? `O certificado SSL de ${dominio} EXPIROU em ${notAfter?.toLocaleDateString('pt-BR')}. Seu site esta inseguro!`
: `Certificado SSL de ${dominio} emitido por ${issuer.split('=').pop()}. ${isExpiringSoon ? `Vence em ${diasRestantes} dias.` : `Valido ate ${notAfter?.toLocaleDateString('pt-BR')}.`}`,
detalhes: {
Dominio: dominio,
Emissor: issuer.split('=').pop() || issuer,
'Valido ate': notAfter?.toLocaleDateString('pt-BR') || 'Desconhecido',
'Dias restantes': String(diasRestantes),
},
recomendacoes: isExpired
? ['RENOVE IMEDIATAMENTE seu certificado SSL', 'Use Let\'s Encrypt (gratuito) ou contrate um certificado pago', 'Entre em contato com a Avanzato para renovacao urgente']
: isExpiringSoon
? [`Renove seu certificado nos proximos ${diasRestantes} dias`, 'Configure renovacao automatica', 'Considere usar Let\'s Encrypt com auto-renew']
: ['Seu certificado SSL esta valido!', 'Configure renovacao automatica para evitar expiracao', 'Monitore a data de vencimento'],
score,
};
} catch {
return {
status: 'info',
titulo: 'Verificacao de SSL',
mensagem: `Nao foi possivel verificar o SSL de ${dominio} externamente. Verifique diretamente no navegador clicando no cadeado ao lado da URL.`,
score: 50,
};
}
}
// Email Security Score
export async function checkEmailSecurity(dominio: string): Promise<ResultadoFerramenta> {
const [spf, dkim, dmarc, mx] = await Promise.all([
checkSPF(dominio),
checkDKIM(dominio),
checkDMARC(dominio),
checkMX(dominio),
]);
const scores = [spf.score || 0, dkim.score || 0, dmarc.score || 0, mx.score || 0];
const media = Math.floor(scores.reduce((a, b) => a + b, 0) / scores.length);
let status: 'success' | 'warning' | 'error' = 'success';
if (media < 50) status = 'error';
else if (media < 80) status = 'warning';
return {
status,
titulo: `Email Security Score: ${media}/100`,
mensagem: media >= 80
? `A seguranca de e-mail de ${dominio} esta boa! Score: ${media}/100.`
: media >= 50
? `A seguranca de e-mail de ${dominio} precisa de atencao. Score: ${media}/100.`
: `A seguranca de e-mail de ${dominio} esta critica! Score: ${media}/100.`,
detalhes: {
Dominio: dominio,
'Score Total': `${media}/100`,
'SPF': `${spf.score}/100 (${spf.status === 'success' ? 'OK' : 'Problema'})`,
'DKIM': `${dkim.score}/100 (${dkim.status === 'success' ? 'OK' : 'Atenção'})`,
'DMARC': `${dmarc.score}/100 (${dmarc.status === 'success' ? 'OK' : 'Problema'})`,
'MX': `${mx.score}/100 (${mx.status === 'success' ? 'OK' : 'Problema'})`,
},
recomendacoes: [
...spf.recomendacoes || [],
...dmarc.recomendacoes || [],
...(dkim.score && dkim.score < 80 ? dkim.recomendacoes || [] : []),
].filter((v, i, a) => a.indexOf(v) === i).slice(0, 6),
score: media,
};
}
// Microsoft 365 Analyzer
export async function checkMicrosoft365(dominio: string): Promise<ResultadoFerramenta> {
const [mx, spf, dmarc, dkim] = await Promise.all([
checkMX(dominio),
checkSPF(dominio),
checkDMARC(dominio),
checkDKIM(dominio),
]);
const isMicrosoft365 = mx.detalhes?.Provedor?.includes('Microsoft');
const m365Score = isMicrosoft365 ? 40 : 0;
const spfScore = spf.score || 0;
const dmarcScore = dmarc.score || 0;
const dkimScore = dkim.score || 0;
const total = Math.min(100, m365Score + Math.floor((spfScore + dmarcScore + dkimScore) / 3 * 0.6));
let status: 'success' | 'warning' | 'error' = 'success';
if (total < 40) status = 'error';
else if (total < 70) status = 'warning';
return {
status,
titulo: `Microsoft 365 Analyzer: ${total}/100`,
mensagem: isMicrosoft365
? `O dominio ${dominio} esta configurado com Microsoft 365. Score de seguranca: ${total}/100.`
: `O dominio ${dominio} nao parece usar Microsoft 365 como servidor de e-mail principal. Score de seguranca: ${total}/100.`,
detalhes: {
Dominio: dominio,
'Usa Microsoft 365': isMicrosoft365 ? 'Sim' : 'Nao detectado',
'Score Total': `${total}/100`,
'SPF': `${spfScore}/100`,
'DKIM': `${dkimScore}/100`,
'DMARC': `${dmarcScore}/100`,
},
recomendacoes: isMicrosoft365
? [
...(total < 80 ? ['Configure DKIM corretamente para Microsoft 365'] : []),
...(dmarcScore < 80 ? ['Fortaleca a politica DMARC'] : []),
'Habilite ATP (Advanced Threat Protection) no Microsoft 365',
'Configure regras de anti-spam e anti-malware',
'Ative a autenticacao de dois fatores para todos os usuarios',
]
: [
'Se voce usa Microsoft 365, verifique se os registros MX estao corretos',
'Configure SPF com include:_spf.microsoft.com',
'Ative DKIM no portal do Microsoft 365',
'Configure DMARC para monitoramento',
],
score: total,
};
}
+73
View File
@@ -0,0 +1,73 @@
// ============================================================================
// DETECAO DE AMBIENTE - Automatico por hostname
// ============================================================================
// Um unico build serve todos os ambientes:
// - PRODUCAO (avanzato.com.br): index, tracking ativo
// - HOMOLOGACAO (hml.avanzato.com.br): noindex, tracking desativado
// - DESENVOLVIMENTO (localhost): noindex, tracking desativado
// ============================================================================
const hostname = typeof window !== 'undefined' ? window.location.hostname : '';
export type Environment = 'production' | 'homologation' | 'development';
function detectEnvironment(): Environment {
// Producao: domínio principal
if (hostname === 'avanzato.com.br' || hostname === 'www.avanzato.com.br') {
return 'production';
}
// Homologacao: subdominio hml
if (hostname === 'hml.avanzato.com.br') {
return 'homologation';
}
// Desenvolvimento: localhost, 127.0.0.1, IPs locais, previews
if (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
hostname.startsWith('172.') ||
hostname.includes('kimi.page') || // previews
hostname === '' // SSR
) {
return 'development';
}
// Fallback: se nao reconhece, trata como homologacao (seguro)
return 'homologation';
}
export const ENV = detectEnvironment();
// Helpers
export const IS_PRODUCTION = ENV === 'production';
export const IS_HOMOLOGATION = ENV === 'homologation';
export const IS_DEVELOPMENT = ENV === 'development';
// Configuracoes por ambiente
export const ENV_CONFIG = {
// SEO / Indexacao
allowIndexing: IS_PRODUCTION, // So producao permite Google indexar
robotsMeta: IS_PRODUCTION ? 'index, follow' : 'noindex, nofollow',
// Tracking / Integracoes
mauticActive: IS_PRODUCTION, // So envia leads reais em producao
googleAnalytics: IS_PRODUCTION, // So trackeia em producao
googleAds: IS_PRODUCTION, // So dispara conversões em producao
facebookPixel: IS_PRODUCTION, // So dispara pixel em producao
// Visual
showBanner: !IS_PRODUCTION, // Banner de ambiente em hom/dev
bannerText: IS_HOMOLOGATION ? 'HOMOLOGACAO' : 'DESENVOLVIMENTO',
bannerColor: IS_HOMOLOGATION ? 'bg-yellow-500' : 'bg-blue-500',
} as const;
// Log para debug
if (typeof window !== 'undefined') {
console.log(`[Ambiente] ${ENV.toUpperCase()} | Host: ${hostname}`);
if (!IS_PRODUCTION) {
console.log('[Ambiente] Tracking desativado | SEO: noindex');
}
}
+11 -16
View File
@@ -1,12 +1,7 @@
// Configuracao do Mautic - Avanzato
// URL: https://mkt.avanzato.com.br
// --- DETECAO DE AMBIENTE ---
// Em homologacao (localhost, IP, ou nao-producao), o Mautic fica em modo simulacao
// Apenas loga no console, nao envia dados reais
const hostname = typeof window !== 'undefined' ? window.location.hostname : '';
const PROD_HOSTS = ['avanzato.com.br'];
export const IS_HOMOLOGATION = !PROD_HOSTS.includes(hostname);
import { IS_PRODUCTION } from './environment';
export const MAUTIC_CONFIG = {
baseUrl: 'https://mkt.avanzato.com.br',
@@ -17,8 +12,8 @@ export const MAUTIC_CONFIG = {
contato: { id: '4', enabled: true },
},
// Desativa envio real em homologacao
enabled: !IS_HOMOLOGATION,
// So envia dados reais em producao
enabled: IS_PRODUCTION,
};
/**
@@ -37,9 +32,9 @@ export const sendLeadToMautic = (
return false;
}
// MODO HOMOLOGACAO: simula envio, nao manda dados reais
if (IS_HOMOLOGATION) {
console.log('%c[HOMOLOGACAO] Mautic em modo simulacao - dados NAO enviados ao servidor real', 'background:#f59e0b;color:#000;font-weight:bold;padding:4px 8px;border-radius:4px');
// MODO NAO-PRODUCAO: simula envio, nao manda dados reais
if (!IS_PRODUCTION) {
console.log('%c[' + (window as any).__AVANZATO_ENV__?.toUpperCase() + '] Mautic em modo simulacao - dados NAO enviados', 'background:#f59e0b;color:#000;font-weight:bold;padding:4px 8px;border-radius:4px');
console.log(`[Mautic] Form: ${formType} | Form ID: ${formConfig.id}`);
console.log(`[Mautic] Dados que seriam enviados:`, JSON.stringify(formData, null, 2));
console.log(`[Mautic] Em producao, enviaria para: ${MAUTIC_CONFIG.baseUrl}/form/submit?formId=${formConfig.id}`);
@@ -123,8 +118,8 @@ export const sendLeadViaMailto = (formData: Record<string, string>): void => {
* Desativado em homologacao para nao poluir dados
*/
export const trackConversion = (): void => {
if (IS_HOMOLOGATION) {
console.log('%c[HOMOLOGACAO] Google Ads conversion NAO disparado (ambiente de teste)', 'color:#f59e0b');
if (!IS_PRODUCTION) {
console.log('%c[' + (window as any).__AVANZATO_ENV__ + '] Google Ads conversion NAO disparado', 'color:#f59e0b');
return;
}
if (typeof window !== 'undefined' && (window as any).gtag) {
@@ -142,12 +137,12 @@ export const trackConversion = (): void => {
* Desativado em homologacao
*/
export const trackFacebookPixel = (eventName: string = 'Lead', params?: Record<string, any>): void => {
if (IS_HOMOLOGATION) {
console.log(`%c[HOMOLOGACAO] Facebook Pixel "${eventName}" NAO disparado (ambiente de teste)`, 'color:#f59e0b', params);
if (!IS_PRODUCTION) {
console.log(`%c[${(window as any).__AVANZATO_ENV__}] Facebook Pixel "${eventName}" NAO disparado`, 'color:#f59e0b', params);
return;
}
if (typeof window !== 'undefined' && (window as any).fbq) {
(window as any).fbq('track', eventName, params || {});
console.log(`[Facebook Pixel] Evento: ${eventName}`, params);
}
};
};

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