Frontend for Beginners

Building a React.js powered blog with automated code quality control

Context

Frontend for Beginners is a blog about front-end development, tailored to beginners. It is a joint effort of the team of 4 people and was in the active content and web development phase in May - June 2019. My roles in the project were the developer and articles writer.

Overview

The goal was to build a blog about web development where different authors can add their articles in code and overall collaborate on enhancing the blog’s functionality.

It was achieved by using current technologies:
React.js, Gatsby.js, Mocha testing engine
and collaborative tools:
Github, CircleCI.

Blog preparation and development

Problem

When planning the blog, I wanted to build it with modern technologies, which are also easy to pick up by other developers who may be collaborating with me on the project. Also, I didn’t want to lock down into solutions that add too much complexity from the very beginning.

However, I also looked for basic blog functionality to be present as well, such as templates for the articles and automatic web pages generation for new articles.

Solution

The solution for the modern and easy to start collaborating blog was the Gatsby js framework. It leverages React.js and Webpack for frontend development, GraphQL query language for data management, and an extensive system of plugins.

To set up the blog, I performed the following steps:

  1. Generate a basic Gatsby.js project;
  2. Add main layout components: header, footer, column structure for the articles, code the about page;
  3. Setup the templates for the blog post pages, set up an automatic generation of the new blog pages and new tags pages

    exports.createPages = ({ graphql, actions }) => {
    const { createPage } = actions
    return graphql(`
    {
    allMarkdownRemark {
    edges {
    node {
    fields {
    slug
    }
    frontmatter {
    tags
    }
    }
    }
    }
    }
    `).then(result => {
    const posts = result.data.allMarkdownRemark.edges;
    posts.forEach(({ node }) => {
    createPage({
    path: node.fields.slug,
    component: path.resolve(`./src/templates/blog-post.js`),
    context: {
    // Data passed to context is available
    // in page queries as GraphQL variables.
    slug: node.fields.slug,
    },
    })
    })
    // Tag pages:
    let tags = []
    // Iterate through each post, putting all found tags into `tags`
    posts.forEach(edge => {
    if (edge.node.frontmatter.tags) {
    edge.node.frontmatter.tags.forEach(tag => {
    if (tags.indexOf(tag) === -1) {
    tags.push(tag);
    }
    })
    }
    })
    // Make tag pages
    tags.forEach(tag => {
    createPage({
    path: `/tags/${tag.toLowerCase()}/`,
    component: path.resolve(`./src/templates/tags.js`),
    context: {
    tag,
    },
    })
    })
    })
    }
    view raw gatsby-node.js hosted with ❤ by GitHub
  4. Group the articles into main website categories - articles and tutorials on the home page and respective articles and tutorials pages with Array.map() method;
  5. Setup the template for the social sharing meta tags automatically generated for each blog page;

    import React from "react";
    import { Helmet } from "react-helmet";
    import Header from "../components/Header";
    import Footer from "../components/Footer";
    import HeroBanner from "../components/HeroBanner";
    export default ({ children, title, bannerMessage, dir, imagePreview, metaDescr, location, nolinks }) => (
    <React.Fragment>
    <Helmet>
    <meta charSet="utf-8" />
    <title>{title}</title>
    <link rel="canonical" href={location ? location : "https://highstart.dev"} />
    <link rel="stylesheet" href="https://unpkg.com/tachyons@4.10.0/css/tachyons.min.css"/>
    <meta http-equiv="X-UA-Compatible" content="IE=9"/>
    <meta property="og:site_name" content="Frontend for Beginners"/>
    <meta property="og:type" content="website"/>
    <meta name="twitter:site" content="@FrontendBegins"/>
    <meta name="twitter:card" content="summary_large_image"/>
    <meta itemprop="image" content={imagePreview ? 'https://highstart.dev' + imagePreview : 'https://highstart.dev/general_preview.png'}/>
    <meta property="og:image" content={imagePreview ? 'https://highstart.dev' + imagePreview : 'https://highstart.dev/general_preview.png'}/>
    <meta name="twitter:image" content={imagePreview ? 'https://highstart.dev' + imagePreview : 'https://highstart.dev/general_preview.png'}/>
    <meta property="og:title" content={`${title} • Frontend for Beginners`}/>
    <meta itemprop="name" content={`${title} • Frontend for Beginners`}/>
    <meta name="twitter:title" content={`${title} • Frontend for Beginners`}/>
    <meta property="og:description" content={metaDescr ? metaDescr : 'Frontend for Beginner is a blog for aspiring frontend developers with Javascript, HTML and CSS tutorials and work processes overviews'}/>
    <meta name="twitter:description" content={metaDescr ? metaDescr : 'Frontend for Beginner is a blog for aspiring frontend developers with Javascript, HTML and CSS tutorials and work processes overviews'}/>
    <meta name="description" content={metaDescr ? metaDescr : 'Frontend for Beginner is a blog for aspiring frontend developers with Javascript, HTML and CSS tutorials and work processes overviews'}/>
    <meta itemprop="description" content={metaDescr ? metaDescr : 'Frontend for Beginner is a blog for aspiring frontend developers with Javascript, HTML and CSS tutorials and work processes overviews'}/>
    </Helmet>
    <Header/>
    <HeroBanner message={bannerMessage} dir={dir}/>
    {children}
    <Footer nolinks={nolinks}/>
    </React.Fragment>
    );
    view raw Layout.js hosted with ❤ by GitHub
  6. Setup automatic deployment to Netlify;

Code Quality

Problem 1: Linting code

Code quality revolves mainly about code readability and styling conventions used in the repository. In the case of this project, maintaining code quality is important both for myself and any other collaborators.

Commonly it is achieved by setting up linters, that highlight all the syntax errors and styling inconsistencies.

Solution 1: Linting code

One of the best linters for Javascript is JSLint. Gatsby already uses JSLint, but by default, it lints everything during the development process and does not restrict pushing poorly formatted code.

Running JSLint commands in the project as is checks the whole project’s codebase and may also automatically fix minor errors.

To check only the code that was changed and staged for the commit:

  • I added the husky package that allows using git hooks, which may prevent the commits. I tested it to show the commit message when a commit is made and then configured to run lint-staged:
    
      "husky": {
        "hooks": {
          "pre-commit": "lint-staged"
        }
      },
  • Then I added the lint-staged package and set it to lint only the committed code:
    
      "lint-staged": {
        "*.js": [
          "eslint --fix",
          "git add"
        ]
      },

Problem 2: Linting commits

When committing the added or changed code, developers may use different conventions or no conventions at all. This may result in huge commits or small commits, which, most importantly, may be hard to debug by the author or teammates.

Therefore it is one of the best practices to structure the commits in the form of “[optional scope]: ”, where type is a category of the code change, and description brings more details to it.

Solution 2: Linting commits

Forcing developers to remember the styling convention may make life just harder. Therefore I added “commit-lint” that checks the commit message to conform to the “[optional scope]: ” format and reminds of the existing types if the type is off.

For that, I added another package “commit-lint” and configured the husky setup to check the text of the commit, additionally to checking the files being committed.

Commit-lint setup in the package.json:


  "commitlint": {
    "rules": {
      "type-empty": [ // to prevent adding commits with no category, no type
        2,
        "never"
      ],
      "header-min-length": [
        2,
        "always",
        3
      ],
      "type-enum": [ // to specify types that can be used
        2,
        "always",
        [
          "feat",
          "fix",
          "docs",
          "style",
          "refactor",
          "test",
          "revert",
          "setup"
        ]
      ]
    }
  },

New husky setup:


  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
      "pre-commit": "lint-staged"
    }
  },

After all the setup, the commit checks looks like the following:

  • Commit staged files with a message:

    
    $ git commit -m 'test: check if lint-staged and commit-lint are working'
    husky > pre-commit (node v10.15.3)
                    
  • Linter runs and checks selected files:

    $ git commit -m 'test: check if lint-staged and commit-lint are working'
    husky > pre-commit (node v10.15.3)
      ↓ Stashing changes... [skipped]
        → No partially saved files found...
      ❯ Running linters...
        ❯ Running tasks for *.js
          ∷ eslint --fix
            git add
  • Commit-lint checks if the commit message is formatted properly:

    $ git commit -m 'test: check if lint-staged and commit-lint are working'
    husky > pre-commit (node v10.15.3)
      ↓ Stashing changes... [skipped]
        → No partially saved files found...
      ✓ Running linters...
    husky > commit-msg (node v10.15.3)
    
    ⧗  input: test: check if lint-staged and commit-lint are working
    ✓  found 0 problems, 0 warnings
       (Need help? -> https://github.com/conventional-changelog/commitlint#what-is-commitlint)
    
    [master 6e36fcf] test: check if lint-staged and commit-lint are working
     1 file changed, 7 insertions(+)
     create mode 100644 app.js
                    
  • If formatting is off, commit-lint raises a warning:

    $ git commit -m 'wrongtype: test wrong commit format'
    husky > pre-commit (node v10.15.3)
    No staged files match any of provided globs.
    husky > commit-msg (node v10.15.3)
    
    ⧗  input: wrongtype: test wrong commit format
    ✖  type must be one of [feat, fix, docs, style, refactor, test, revert, setup] [type-enum]
    ✖  found 1 problems, 0 warnings
       (Need help? -> https://github.com/conventional-changelog/commitlint#what-is-commitlint)
    
    husky > commit-msg hook failed (add --no-verify to bypass)
                    

Automated Testing

Problem

Programming collaboratively is based on trust. Certainly, you are reviewing the teammate’s code in the pull request, and optionally even pull the branch they worked in to test changes locally. However, it is great to have the tests run automatically to see new changes do not break that main functionality.

Solution

The result I was approaching was the following: when a pull request is made to the master branch, the automated tests run to see whether existing pages are displayed correctly. This is done with 3 main components:

  • CircleCi project setup that runs tests when a pull request is made to the master branch, and displays pass or fail flag on the pull request page;
  • Mocha testing engine, that is used for writing the tests, ensuring the web pages contain correct content when loaded;
  • Puppeteer, the Headless Chrome Node API that opens web pages in Chrome and gets their content

The steps to set up were the following:

  1. Setup the CircleCI project in the CircleCI project with the frontendForBeginners repository;
  2. Add the .circleci/config.yml configuration file and validate it with circleCI CLI. Validation was necessary as I ran into syntax errors when copying the config code into VIM code editor and the .yml whitespace syntax is pretty strict;
    config.yml file:
    # Javascript Node CircleCI 2.0 configuration file
    #
    # Check https://circleci.com/docs/2.0/language-javascript/ for more details
    #
    version: 2
    jobs:
      build:
        docker:
          # specify the version you desire here
          - image: circleci/node:latest-browsers
    
            # Specify service dependencies here if necessary
            # CircleCI maintains a library of pre-built images
            # documented at https://circleci.com/docs/2.0/circleci-images/
            # - image: circleci/mongo:3.4.4
    
        working_directory: ~/repo
    
        steps:
            - checkout
    
              # Download and cache dependencies
            - restore_cache:
                keys:
                  - v1-dependencies-{{ checksum "package.json" }}
                    # fallback to using the latest cache if no exact match is found
                  - v1-dependencies-
    
            - run: npm install
    
            - save_cache:
                paths:
                  - node_modules
                key: v1-dependencies-{{ checksum "package.json" }}
  3. Serve the built version of the blog (static website, build folder) with Express.js. server-test.js:
    const express = require('express');
    const app = express();
    
    const ejs = require('ejs');
    
    let server;
    
    app.use(express.static('public'));
    server = app.listen(3000, () => console.log('Example app listening on port 3000!'));
                    
  4. Write Mocha tests, that run the website on a local server and open the headless chrome instance. Then get specific pages with Puppeteer and check if they contain the title element of the page.
    describe('puppeteer checks the blog', () => {
      before(async () => {
        app.use(express.static('public'));
        server = app.listen(3000, () => console.log('Example app listening on port 3000!'));
        browser = await puppeteer.launch({
          headless: true,
          args: ['--no-sandbox', '--disable-setuid-sandbox']
        });
      });
      it('open home page successfully', async () => {
        const page = await browser.newPage();
        await page.goto('http://127.0.0.1:3000/', {waitUntil: 'networkidle2'});
        const bodyHTML = await page.evaluate(() => document.body.innerHTML);
    
        const regex = /<span class="code-right">(.*?)\</span>/g;
        let found = bodyHTML.match(regex);
        expect(found[0]).to.equal("<span class=\"code-right\">Hello World!</span>");
      });
                    

    Close the Chrome browser and server when the tests end

    after(async () => {
      await browser.close();
      server.close();
    });
  5. As the tests rely on the built version of the website, add the build and then test command to the .circleci/config.yml file
            - run: npm install
    
            # add build step to be able to run tests
            - run: npm run build
    
            - save_cache:
                paths:
                  - node_modules
                key: v1-dependencies-{{ checksum "package.json" }}
    
            # run tests!
            - run: npm run test

As a result, the pull request can be merged into master after the code review and after the CircleCI tests have passed

And this is how the tests are passing.

Collaboration

Problem

It may be hard to communicate the project structure and development process, as well as the code quality assurance measure.

Solution

Up-to-date documentation in the README.md file, which describes the folder structure and main commands used to run the development server and teste. It was tested and then improved with one of the collaborators.

Overall result

The extensible blog is written with React.js, it leverages rich Gatsby.js plugins ecosystem and documentation. Code linting and automated testing ensure code quality.