react-simple-snap
v1.0.6
Published
Biblioteca React para pré renderzar páginas estáticas (snapshots), SEO e performace de buscas otimizados!
Downloads
25
Maintainers
Keywords
Readme
react-simple-snap
Otimize o SEO e a Performace da sua SPA
- Pré renderiza seu Web App em uma página HTML estática.
- Usa Headless Chrome para procurar por links para outras rotas à partir da raiz (Root).
- React 18+
- Esta é uma versão da biblioteca react-snap
(fork at V1.23.0)
Pronta para usar. veja o que ele faz por debaixo dos panos.
🤖 Funcionalidades
Habilita SEO (Google, Bing, DuckDuckGo...) e SMO (Twitter, Facebook, Instagram) para SPAs.
Funciona com create-react-app - sem alterações de código necessárias.
Funciona com vite - sem alterações de código necessárias.
Usa um browser real (puppeteer) por de baixo dos panos, eliminando problemas de funcionalidades de HTML5 não suportadas , como WebGL or Blobs.
Várias otimizações de desempenho de decarregamento da página. Details,
english
.Independente do React. Funciona com qualquer tecnologia (ex. Vue)
A biblioteca npm não tem etapa de compilação, então você pode fazer um fork, mudar o que quiser, e instalar com a URL do GitHub do seu repositório.
💭 Motivação
Ao publicar meu primeiro site em React em um servidor público, me deparei com vários problemas de SEO, principalmente na indexação correta das páginas pelo Google.
Inspirado nesse vídeo do Filipe Deschamps, onde ele fala sobre páginas pré renderizadas com NextJS, procurei uma ferramenta que não precisasse de um servidor node rodando e eu pudesse usar em uma hospedagem compartilhada.
E a solução foi encontrada com a biblioteca react-snap, que tráz muitas opções para configuração e melhoria de desempenho de carregamento da página e SEO.
Mas com a versão 18 do React, o react-snap parou de funcionar, e sua ultima atualização foi há 4 anos.
🔥 Como usar
Instalação
yarn add --dev react-simple-snap
Adicione o Script
package.json
{
"scripts": {
"postbuild": "react-simple-snap"
}
}
Altere o arquivo src/index.js
ou src/main.jsx
:
import { createRoot, hydrateRoot } from "react-dom/client";
const rootElement = document.getElementById("root");
if (rootElement.hasChildNodes()) {
hydrateRoot(rootElement, <App />);
} else {
createRoot(rootElement).render(<App />);
}
Execute no terminal
yarn build
E o react-simple-snap trabalhará após o build
no processo de postbuild
com o diretório padrão /dist
⚙️ Configurações
⚠️ Praticamente Todas as configurações presentes em react-snap foram mantidas, porém o arquivo de configurações foi alterado de /package.json
para .snap.json
Você pode conferir muito mais funcionalidades e explicações no repositório do react-snap
Veja aqui todas as configurações disponíveis.
Principais
.snap.json
{
//diretório fonte do projeto após build
"source": "dist",
// default; Define user agent acessado por navigator.userAgent
"userAgent": "react-simple-snap",
// rotas a serem pré-rendereizadas
"include": ["/"],
//transforma o css em uma linha para reduzir tamanho do arquivo (pode ocorrer erros)
"inlineCss": false,
// procura por links para outras rotas e as renderiza
"crawl": true
}
Alterações de configurações react-snap
{
/*
* Removida
* "saveAs": "html" // | "png" | "jpg,
* // salva a renderização como screenshot
*/
"screenshot": false, // default | ("png" | true) | "jpeg"
}
⚠️ Atenção
Para maioria dos apps, a instalação padrão do react-simple-snap ja é suficiente para tudo funcionar bem sem alterar nada no seu código.
Porém dependendo das estratégias e funcionalidades voce escolheu para seu app, alguns problemas podem acontecer.
A correção de vários possíveis problemas encontrados está presente na documentação do react-snap.
💤 Problemas com coisas dinâmicas
Se as coisas ´se mexerem sozinhas´ no seu app, como com o uso de animações, requisições, uso de importações dinâmicas como react.lazy()
ou outras técicas de code-splitting, você deve tomar alguns cuidados ao criar seus projetos.
App Exemplo
src/main.jsx
import React from "react";
import { createRoot, hydrateRoot } from "react-dom/client";
import axios from "axios";
import styled from "styled-components"; // Funciona com CSS-IN-JS!
const rootElement = document.getElementById("root");
//Suporta classes feitas por js!
const AppContainer = styled.div`
color: #09c;
`;
//1 - Incia a tela com o texto "Conteúdo1"
//2 - Altera a string para "Conteúdo2"
//3 - Requisita uma frase aleatória e imprime na tela
const App = () => {
const [string, setString] = React.useState("Conteúdo1");
React.useEffect(() => {
//Altera a string para "Conteúdo"
useState("Conteúdo2");
//Requisita uma frase aleatória
axios
.get("https://baconipsum.com/api/?type=bacon")
.then((data) => setString(data.data[0]));
}, []);
return <AppContainer>{string}</AppContainer>;
};
if (rootElement.hasChildNodes()) {
hydrateRoot(rootElement, <App />);
} else {
createRoot(rootElement).render(<App />);
}
src/index.html
<html>
<head>
<title>Title</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Geração das páginas estáticas
Após o processo de build, o simple-snap acessa as páginas do app com um navegador headless e espera até todos os recursos serem carregados + 0.5 segundos e então gera um código estático html daquela página.
hydrateRoot
A função hydrateRoot é uma função de renderização do React que é usada para atualizar o conteúdo do elemento #root
existente com base em um novo elemento renderizado.
o React renderiza seu app, como faz a função renderToString()
de react-dom/server
e compara esse html em texto com o conteúdo do elemento div#root
do html gerado pelo simple-snap
.
Por que usar hydrateRoot?
Quando o usuário carrega a página de um app sem pré renderização, ele recebe um arquivo html com o conteúdo do body sendo somente uma div vazia
conteúdo do body
/index.html
sem pré renderização
<div id="root"></div>
Mas ao pré renderizar seu app, a div #root já vai estar com o conteúdo correto a ser mostrado para o usuário.
conteúdo do body
/index.html
com pré renderização
<div id="root">
<div class="ackr"><!--{...ConteúdoDoApp}--></div>
</div>
Assim o conteúdo é entregue ao usuário sem que ele espere o tempo que leva para a primeira renderização.
Problema
O problema acontece uma vez que o hydrateRoot()
não espera os recursos da página carregarem antes de efetuar a comparação. Então se qualquer elemento for adicionado, removido ou tiver alguma propriedade alterada entre o tempo que o simple-snap leva para renderizar a página (networkIdle + 0.5s), um erro será retornado.
Conteúdo renderizado pelo hydrateRoot()
<div id="root"><div class="ackr">Conteúdo1</div></div>
Conteúdo do arquivo index.html
gerado pelo simple-snap
<div id="root"><div class="ackr">Jowl landjaeger andouille belly...</div></div>
Note que o conteúdo esperado pelo hydrateRoot()
é o conteúdo do estado inicial da aplicação, antes do useEffect() ser executado.
O conteúdo arquivo pré renderizado contém uma frase obtida por uma requisição get.
Durante a etapa de desenvolvimento nenhum erro deve acontecer, uma vez que o arquivo html utilizado durante essa etapa no diretório raiz do projeto estará sempre com a div#id
vazia.
Após o build
e postbuild
, você pode usar o comando yarn preview
para ver como o projeto compilado e pré-renderizado ficou, e nesse momento você pode notar um erro no console.
Uncaught Error: Minified React error #425; visit https://reactjs.org/docs/error-decoder.html?invariant=425
O erro descrito na mensagem é:
Text content does not match server-rendered HTML.
Soluções
Existem diversas soluções que você pode utilizar pra resolver esse tipo de problema.
Várias delas estão na documentação do react-snap.
Sabendo Que
simple-snap
- Espera a página carregar.
hydrateRoot()
- renderiza instantâneamente a página. (antes do useEffect)
Você pode
Esconder coisas do simple-snap
Você pode configurar o userAgent no arquivo .snap.json
{
"userAgent": "react-simple=snap" // default
}
E verificar se o browser que está acessando o app é o pré renderizador ou não.
const App = () => {
const [string, setString] = React.useState("Conteúdo1");
React.useEffect(() => {
//Se o userAgent NÃO for o pré renderizador
if (window.userAgent != "react-simple-snap") {
setString("Conteúdo2");
axios
.get("https://baconipsum.com/api/?type=bacon")
.then((data) => setString(data.data[0]));
}
}, []);
return <AppContainer>{string}</AppContainer>;
};
Resultado do simple-snap
<div id="root"><div class="ackr">Conteúdo1</div></div>
Note que o setState("Conteudo2")
também ficou dentro da condicional, pois se estivesse fora, seria executado na renderização;
Esconder coisas do hydrateRoot()
Como o hydrateRoot()
renderiza a página antes do useEffect()
const App = () => {
const [show, setShow] = useState(false);
useEffect(() => setShow(true), []);
return <AppContainer>{show && "hydrateRoot Não me vê"}</AppContainer>;
};
Resultado do hydrateRoot()
<div id="root"><div class="ackr"></div></div>
Nada Resolveu?
Se você procurou em todos os lugares que podia e continua dando erro de hyration?
Remova o hydrateRoot()
Você continua com as otimizaçoões de SEO, só "deixa de ganhar" performace e UX.