Create React App é uma ferramenta que permite criar aplicações React com maior facilidade. Possui no entanto algumas limitações que deves ter em conta:
-
A aplicação é renderizada apenas no cliente. Apesar de actualmente muitos motores de busca serem capazes de executar JavaScript, se a tua aplicação é maioritariamente estática (como é o caso dum blog) há vantagens em servir simplesmente ficheiros HTML renderizados por um servidor.
-
É possível manter múltiplos ficheiros HTML mas o código JavaScript é apenas injectado no ficheiro
index.html
. Se estás a criar um blog por exemplo, queres provavelmente um<title>
e metatags Open Graph diferentes para cada artigo e, apesar de o conseguires fazer com JavaScript usando o react-helmet, a maioria das plataformas (como o Twitter, Facebook e Slack) não as utilizarão, uma vez que não executam o código JavaScript. -
Se planeias fazer deploy para o GitHub Pages ou outro serviço de static hosting similar, tem em atenção que se o teu roteador do cliente utilizar a API pushState history, os teus visitantes podem encontrar erros 404 ao fazerem refresh.
http://user.github.io/posts/hello-world
faz com que o servidor do GitHub Pages procure pelo ficheiroposts/hello-world/index.html
que não existe. Isto é importante para qualquer tipo de aplicação, incluindo blogs.
Agora que estamos a par das limitações, podemos criar um novo blog e tentar solucioná-las.
Começamos com os ficheiros auto-gerados habituais. Verifica se tens versões de Node e NPM recentes instaladas na tua máquina.
npx create-react-app my-blog
cd my-blog
npm start
A aplicação deve agora estar a correr normalmente. Vamos remover alguns dos ficheiros gerados, incluindo o registerServiceWorker.js
.
Service Workers são muito úteis, mas é fácil encontrar problemas se se não tiver experiência com estes.
Remove e simplifica tudo até que a pasta src
esteja assim:
/* index.js */
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "./index.css";
ReactDOM.render(<App />, document.getElementById("root"));
/* index.css */
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
/* App.js */
import React from "react";
export default () => <div />;
Agora podemos executar o comando npm install react-router-dom
de forma a podermos ter múltiplas páginas.
Teremos uma página para a lista de artigos e ainda uma página para cada um desses artigos.
/* App.js */
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Posts from "./Posts";
import Post from "./Post";
import NotFound from "./NotFound";
import data from "./data";
export default () => (
<Router>
<Switch>
<Route exact path="/" render={routeProps => <Posts {...data} />} />
{Object.entries(data.posts).map(([slug, post]) => (
<Route
key={slug}
exact
path={`/${slug}`}
render={({ match }) => <Post {...post} />}
/>
))}
<Route render={routeProps => <NotFound />} />
</Switch>
</Router>
);
/* data.js */
export default {
posts: {
"creating-blog-with-cra-and-github": {
date: "2018-02-18",
title: "Creating a blog with create-react-app and GitHub",
summary:
"Create React App is a great tool that lets you start a new React application very easily. There are some limitations though that you need to be aware of.",
},
"dear-hume": {
date: "1958-04-22",
title: "Dear Hume",
summary:
"You ask advice: ah, what a very human and very dangerous thing to do! For to give advice to a man who asks what to do with his life implies something very close to egomania. To presume to point a man to the right and ultimate goal -- to point with a trembling finger in the right direction is something only a fool would take upon himself.",
},
},
};
/* NotFound.js */
import React, { Fragment } from "react";
import { NavLink } from "react-router-dom";
export default () => (
<Fragment>
The page you are looking for was moved, removed,
renamed or might never existed. <br />
<NavLink to="/">Back to blog</NavLink>
</Fragment>
);
/* Posts.js */
import React, { Fragment } from "react";
import { NavLink } from "react-router-dom";
export default ({ posts }) => (
<Fragment>
<h1>Blog</h1>
<ol>
{Object.entries(posts).map(([slug, post]) => (
<li key={slug}>
<h2>
<NavLink to={slug}>{post.title}</NavLink>
</h2>
<p>{post.summary}</p>
<em>{post.date}</em>
</li>
))}
</ol>
</Fragment>
);
/* Post.js */
import React, { Fragment } from "react";
import { NavLink } from "react-router-dom";
export default ({ date, title }) => (
<Fragment>
<h1>
<NavLink to="/">Blog</NavLink>
</h1>
<h2>{title}</h2>
<em>{date}</em>
</Fragment>
);
Se tudo correu bem, deves agora conseguir navegar pelas várias páginas.
Vamos também executar npm install react-helmet
de forma a poder gerir tags na head
.
Neste exemplo apenas é criado o <title>
mas podes também utilizá-lo para tags Open Graph ou Twitter.
# App.js
-import React from "react";
+import React, { Fragment } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
+import { Helmet } from "react-helmet";
import Posts from "./Posts";
import Post from "./Post";
import NotFound from "./NotFound";
import data from "./data";
export default () => (
+ <Fragment>
+ <Helmet titleTemplate="%s | My Blog" />
+
<Router>
<Switch>
<Route exact path="/" render={routeProps => <Posts {...data} />} />
...
<Route render={routeProps => <NotFound {...data} />} />
</Switch>
</Router>
+ </Fragment>
);
# NotFound.js
import React, { Fragment } from "react";
import { NavLink } from "react-router-dom";
+import { Helmet } from "react-helmet";
export default ({ nav }) => (
<Fragment>
+ <Helmet>
+ <title>404</title>
+ </Helmet>
+
The page you are looking for was moved, removed,
renamed or might never existed. <br />
<NavLink to="/">Back to blog</NavLink>
</Fragment>
# Posts.js
import React, { Fragment } from "react";
import { NavLink } from "react-router-dom";
+import { Helmet } from "react-helmet";
export default ({ posts }) => (
<Fragment>
+ <Helmet>
+ <title>Posts</title>
+ </Helmet>
+
<h1>Blog</h1>
<ol>
# Post.js
import React, { Fragment } from "react";
import { NavLink } from "react-router-dom";
+import { Helmet } from "react-helmet";
export default ({ gist, date, title, summary }) => (
<Fragment>
+ <Helmet>
+ <title>{title}</title>
+ </Helmet>
+
<h1>
<NavLink to="/">Blog</NavLink>
</h1>
Eu costumo escrever com Markdown. Também preciso de coloração da sintaxe e alguns estilos básicos para os títulos, imagens, listas, e outros que estamos habituados a ver no GitHub. Felizmente o GitHub já faz tudo isto por nós e oferece uma API, por isso utilizaremos o GitHub para:
- Escrever e armazenar os nossos artigos através de GitHub Gists
- Obter artigos e renderizá-los usando a API do GitHub
- Utilizar a folha de estilos que o GitHub utiliza para os seus ficheiros Markdown
Corremos npm install github-markdown-css
e actualizamos os componentes em conformidade:
# data.js
export default {
posts: {
"creating-blog-with-cra-and-github": {
+ gist: "f4f5311ad2ec25147bc458d791fdaeb5",
date: "2018-02-18",
title: "Creating a blog with create-react-app and GitHub",
summary:
"Create React App is a great tool that lets you start a new React application very easily. There are some limitations though that you need to be aware of.",
},
"dear-hume": {
+ gist: "150ed6aa20f9b72ef3fcaf39ac2f89c6",
date: "1958-04-22",
title: "Dear Hume",
summary:
# index.css
+@import "~github-markdown-css";
+
body {
margin: 0;
padding: 0;
/* Post.js */
import React, { Component, Fragment } from "react";
import { NavLink } from "react-router-dom";
import { Helmet } from "react-helmet";
const headers = { Accept: "application/vnd.github.v3.json" };
export default class Post extends Component {
state = {
content: null,
};
fetchData() {
return this.fetchGistMarkdownUrl(this.props.gist)
.then(this.fetchGistMarkdownText)
.then(this.fetchRenderedMarkdown);
}
fetchGistMarkdownUrl(id) {
return fetch(`https://api.github.com/gists/${id}`, { headers })
.then(response => response.json())
.then(json => Object.values(json.files)[0].raw_url);
}
fetchGistMarkdownText(rawUrl) {
return fetch(rawUrl).then(response => response.text());
}
fetchRenderedMarkdown(text) {
return fetch("https://api.github.com/markdown", {
headers,
method: "POST",
body: JSON.stringify({ text }),
}).then(response => response.text());
}
componentDidMount() {
this.fetchData().then(content => this.setState({ content }));
}
render() {
const { date, title } = this.props;
const { content } = this.state;
return (
<Fragment>
<Helmet>
<title>{title}</title>
</Helmet>
<h1>
<NavLink to="/">Blog</NavLink>
</h1>
<h2>{title}</h2>
<em>{date}</em>
<div
className="markdown-body"
dangerouslySetInnerHTML={{ __html: content }}
/>
</Fragment>
);
}
}
Estamos prontos a enviar o nosso código. Vou fazer npm install gh-pages
para tornar o processo de deploy para o GitHub Pages mais fácil.
{
"name": "my-blog",
+ "homepage": "https://myusername.github.io/my-blog",
"version": "0.1.0",
"private": true,
"dependencies": {
+ "gh-pages": "^1.1.0",
"github-markdown-css": "^2.10.0",
...
},
"scripts": {
+ "predeploy": "npm run build",
+ "deploy": "gh-pages -d build",
"start": "react-scripts start",
Se não estás a utilizar um domínio personalizado, ou se esta GitHub Page é de um projecto (que vive num caminho como /my-blog
)
terás de especificar isso no roteador. Podemos usar uma variável de ambiente para isso:
# .env.production (na raíz do projecto, onde se encontra o package.json)
REACT_APP_BASENAME="/my-blog"
# App.js
<Fragment>
<Helmet titleTemplate="%s | My Blog" />
- <Router>
+ <Router basename={process.env.REACT_APP_BASENAME}>
<Switch>
<Route exact path="/" render={routeProps => <Posts {...data} />} />
Executa npm run deploy
e o blog deve agora estar publicamente disponível.
Está na altura de ter em conta as limitações que foram referidas no início do artigo. Existe um projecto muito interessante chamado React Snap que usa o Puppeteer, um headless Chrome criado pela equipa do Google Chrome, que permite renderizar uma aplicação para ficheiros HTML estáticos.
Vamos correr npm install react-snap
e actualizar o nosso package.json
:
// package.json
"react-router-dom": "^4.2.2",
- "react-scripts": "1.1.1"
+ "react-scripts": "1.1.1",
+ "react-snap": "^1.11.4"
},
"scripts": {
"predeploy": "npm run build",
"deploy": "gh-pages -d build",
"start": "react-scripts start",
- "build": "react-scripts build",
+ "build": "react-scripts build && react-snap",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
+ "reactSnap": {
+ "waitFor": 1000,
+ "preconnectThirdParty": false
}
}
Isto é útil porque resolve também o problema da API pushState history, uma vez que agora temos ficheiros HTML independentes. Irrelevante para este exemplo mas, no caso de precisares de caminhos dinâmicos numa aplicação, lê a documentação, especialmente a secção que menciona o projecto spa-github-pages.
Isto significa também que não precisamos de JavaScript no cliente, e portanto podemos remover esses scripts com:
// package.json
"reactSnap": {
"waitFor": 1000,
- "preconnectThirdParty": false
+ "preconnectThirdParty": false,
+ "removeScriptTags": true
}
}
Finalmente, de forma a evitar que se atinja os limites de número de pedidos à API do GitHub, cria uma token pessoal sem scopes (uma vez que esta aparecerá algures no ficheiro de código minificado) e usa-a na build:
# .env.local (na raíz do projecto, deve estar listado no .gitignore)
REACT_APP_ACCESS_TOKEN="your-github-access-token"
# Post.js
import React, { Component, Fragment } from "react";
import { NavLink } from "react-router-dom";
import { Helmet } from "react-helmet";
+import base64 from "base-64"; // Instala com `npm install base-64`
-const headers = { Accept: "application/vnd.github.v3.json" };
+const accessToken = process.env.REACT_APP_ACCESS_TOKEN;
+const headers = {
+ Accept: "application/vnd.github.v3.json",
+ Authorization: `Basic ${base64.encode(accessToken + ":")}`,
+};
E é tudo! Deves agora ter um blog criado com JavaScript que os visitantes podem ler, mesmo que tenham JavaScript desactivado.
Se este setup for suficiente para ti, óptimo! Mas considera ler sobre o projecto Gatsby. É muito provavelmente uma solução mais apropriada para um blog e pode ajudar-te com funcionalidades mais avançadas, nomeadamente RSS.
Obrigado por leres. Espero que te tenha ajudado de alguma forma 👋