Blog Feb 20, 2018 · 14 min read

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 ficheiro posts/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>
);

Passo 3: Gerir a head do documento

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👋