Criar um blog com Create React App e GitHub
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.
Passo 1: Create React App e limpeza
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 />;
Passo 2: Adicionar um roteador e alguns componentes básicos
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>
);
head
do documento
Passo 3: Gerir a 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>
Passo 4: Obter e renderizar o Markdown
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>
);
}
}
Passo 5: Hospedar no GitHub Pages
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.
Passo 6: Pré-renderizar com React Snap
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