DEV Community

Cover image for I built a chrome extension using Wikipedia API 🔥
Mateusz Iwaniuk
Mateusz Iwaniuk

Posted on

I built a chrome extension using Wikipedia API 🔥

You can find code here: GitHub repo
Try it out: Live link

Intro

In this article, I am going to show you step by step, how I have built a full working chrome extension. My extension is called "Random Wikipedia Pages", which shows random Wikipedia articles and counts how many of them have already been shown or clicked by user. You can see the final result here.

Wikipedia chrome final effect

Technological stack

I made the extension with the use of:

  • React
  • Styled Components
  • Sweet State
  • Fetching data

In order to enjoy my article, you should know at least basics of React. Being familiar with Styled Components and any State Management library is welcome, but not obligatory.
Of course, you should also understand how fetching data from external API works.

Table of contents

  1. Getting started + Project plan
    • Create React App
    • Load your extension
    • Explore the folder structure
  2. Creating layout and components
  3. Working with Wikipedia API and creating a store reducer
  4. Building full extension from top to bottom
    • Article
    • Buttons
  5. Conclusion

Step 1 - Getting started + Project plan

At first I'm going to explain more precisely how this extension actually works.
When opens the extension, an app fetches the random article and displays it.
Wikipedia extension working

User can read full article when he clicks a blue button - Then he is being redirected to full Wikipedia page but also he can draw the next article when he clicks a light button.
Wikipedia extension working

Every time user clicks any button, stats are getting updated.
Wikipedia extension button stats

At the bottom there is located a link to the article you currently read and to the GitHub repo.


Now let's start with coding!

1. Create react app

At first, create react app using the dedicated template to make chrome extensions.

npm init react-app my-first-extension --scripts-version react-browser-extension-scripts --template browser-extension
Enter fullscreen mode Exit fullscreen mode

and then

cd my-first-extension
Enter fullscreen mode Exit fullscreen mode

2. Load your extension

Before explaining the structure of project, let's load the extension in chrome.

  1. Go to chrome://extensions
  2. Click on the "Developer Mode" button, on the upper right
  3. Click "Load unpacked" button and select dev folder from our project

Now, when turning on your extension, you should have the following view:
Create React App

And...that's it! This is the way, how to create a very basic extension. Later on, we will just operate with Wikipedia API and store configuration (which is kinda harder), because the whole extension logic is almost done.

3. Explaining the folder structure

Let's go back to the code stuff.

If you are keen on React, the folder structure should be
known for you.

my-first-extension
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
    ├── img
    │   ├── icon-16.png
    │   ├── icon-48.png
    │   ├── icon-128.png
    ├── popup.html
    ├── options.html
    └── manifest.json
└── src
    ├── background
    │   ├── index.js
    ├── contentScripts
    │   ├── index.js
    ├── options
    │   ├── index.js
    │   ├── Options.js
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
Enter fullscreen mode Exit fullscreen mode

There are few folders that are actually not necessary and you can ignore them.
These folders are:

  • src/background - Responsible for working in the background and watch if user for example clicks any keyword shortcut. We don't need that in this project.
  • src/contentScripts - Responsible for managing a webpage (for example styles change) on which user currently is. We don't need that in this project.
  • src/options - Automatically generated page for user, when he can manage his options. Our app doesn't have that feature. So that, you can also ignore the public/options.html which is a template for that page.

However, you should get familiar with following files:

  • public/manifest.json - It is a primary file which describes your app. You put here information like title, description, version etc.
  • public/popup.html - A template for your extension. Taking advantage of the fact that we are here, let's import our basic font "Titilium Web" (weight 300 and 600) <link href="https://fonts.googleapis.com/css2?family=Titillium+Web:wght@300;600&display=swap" rel="stylesheet">

Additionally, I have added a .prettierrc file, which is responsible for formatting my code.

Step 2 - Creating layout and components

Now that you have created a project folder, it's time to prepare layout and components.

Layout

At first, let's make a layout folder.

In order to do that, I create theme.js file in that and add basic colors.

// src/layout/theme.js
export default {
  colorBlue: '#00A8FF',
  colorGrey: '#414141',
  colorWhite: '#fff',
}
Enter fullscreen mode Exit fullscreen mode

Because of the fact that I want those color variables to be available in every section of the app, I must use ThemeProvider , which provides theme variables to every component.

// src/layout.layout.js
import React from 'react';
import { ThemeProvider } from "styled-components";
import theme from './theme'
const Theme = props => {
    return (<ThemeProvider theme={theme}>{props.children}</ThemeProvider>  );
}
Enter fullscreen mode Exit fullscreen mode
── src
   ├── layout
        ├── layout.js
        ├── theme.js
   ├── wrap.js
Enter fullscreen mode Exit fullscreen mode

At the end, I create a simple Wrapper, which wraps the entire content of every section.

// src/layout/wrap.js
import styled from 'styled-components'

const Wrap = styled.section`
  width: 280px;
  margin: auto;
  position: relative;
`
export default Wrap

Enter fullscreen mode Exit fullscreen mode

Components

Some elements will certainly be used more than once, hence they should be stored in different files.
So let's do that for Button, Desc and Header.

── src
    ├── components
     ├── desc
     │   ├── desc.js
     ├── header
     │   ├── header.js
     ├── button
     │   ├── button.js
Enter fullscreen mode Exit fullscreen mode

Step 3 - Working with Wikipedia API and creating a store reducer

Well, despite I don't find this project unusually hard, this is the most difficult part of it.
In this section I fetch data from Wikipedia API and I configure the state management store, which is responsible for making request to Wikipedia endpoint, saving received data to state and updating local statistics (so here goes the local storage stuff, which is especially uncomfortable when it comes to chrome browser).

Making a Wikipedia request

At first I will show you how to fetch data from Wikipedia API.
The goal of my request is to achieve some english random article. Only title and beginning field is necessary.

The request should look like this:

https://en.wikipedia.org/w/api.php?format=json&action=query&generator=random&grnnamespace=0&prop=extracts|description&grnlimit=1&explaintext=
Enter fullscreen mode Exit fullscreen mode

There I describe for what specific param stands for:

Request part Value Role
https://en.wikipedia.org/w/api.php - Api URL
format json Response format
action query The goal is to query some data (not to update f.e)
generator random Declaring, I need a random page
prop extract Field, I want to receive (extract stands for description)
explaintext - Returns extracts field in txt style (instead of html)
grnlimit 1 Quantity of pages
grnamespace 0 **

** - I won't lie. I'm not sure what this tagged param is supposed to be responsible for. Understanding Wikipedia API is very hard, documentation is barely user-friendly. I have just found this param on StackOverflow and so the request can work.

An example of response:


{
    "batchcomplete": "",
    "continue": {
        "grncontinue": "0.911401741762|0.911401757734|60118531|0",
        "continue": "grncontinue||"
    },
    "query": {
        "pages": {
            "38142141": {
                "pageid": 38142141,
                "ns": 14,
                "title": "Category:Parks on the National Register of Historic Places in Minnesota",
                "extract": "Parks on the National Register of Historic Places in the U.S. state of Minnesota."
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, everything works fine. We have all necessary fields.

Working with reducer

In order to manage state in my app I used React Sweet State. I decided to use this library due to its easiness. I managed to keep my whole reducer logic in one file, because there are only two actions necessary:

  • IncrementOpen (after clicking blue button)- Responsible for getting stats data from chrome about total clicked articles and updating them
  • FetchArticle (after clicking light button) - Responsible for fetching article, sending it to state, getting statistics data from storage (how many articles have been already fetched and how many clicked) and updating stats after every fetch

Wikipedia Chrome Extension buttons

Reducer file is located in the "reducer" folder.


── src
    ├── reducer
     ├── store.js

Enter fullscreen mode Exit fullscreen mode

At first, installing library via NPM is required.

npm i react-sweet-state
Enter fullscreen mode Exit fullscreen mode

So, let's start! At the beginning, I import installed library and create initialState, which contains all basic fields
src/reducer/store.js

// src/reducer/store.js

import { createStore, createHook } from  'react-sweet-state'
const  initialState = {
  title:  '', //Stands for the tittle of article
  desc:  '', // Contains article text
  id:  '', // Id of article (useful when you want to make a link)
  isTooLong:  false, //Informs if fetched text was longer than 250 chars
}
Enter fullscreen mode Exit fullscreen mode

Now it's time to create a store.

// src/reducer/store.js
const  Store = createStore({
  initialState, //our basic state
  actions:{ //here go the actions, that I described earlier
    fetchArticle : ()=> 
    // My fetchArticle code
  }
})
Enter fullscreen mode Exit fullscreen mode

In order to make my notes more readable, my entire code below is located in the exact place, where the My fetchArticle code comment is placed.
At first I must create one more function, which destructs setState and getState function and at the very beginning I'm setting state as initial state (so that when fetching new article, state has no values and the loading effect is being shown then).

As mentioned, in this function I must get user stats, which are located in the chrome storage.
At first, I initial all the variables that are necessary:

const  keyShown = 'allTimeShown' // Key of total shown articles
const  keyOpen = 'allTimeOpened'//Key of tot clicked articles
let  counterAllTimeShown = 1 //Value of total shown articles
let  counterAllTimeOpen = 0 //Value of total clicked articles

let  isFound = false //Checking if chrome storage contains those keys (necessary if user runs this extansion first time)
Enter fullscreen mode Exit fullscreen mode

Before we fall into working with Chrome storage, we must add global chrome object into our file.
It is very simple, you must only this simple line of code at the beginning of reducer.js

// src/store/reducer.js 

/*global chrome*/
import { createStore, createHook } from  'react-sweet-state'
.
.
Enter fullscreen mode Exit fullscreen mode

Note, that in order to have an access to chrome storage, user must allow it. In order to do that, putting this line into our manfiest.json is necessary.

// public/manifest.json
{  
  "permissions": ["storage"],
}
Enter fullscreen mode Exit fullscreen mode

Now we must get stats values from chrome storage. At first, I feel obliged to instruct you how it works. I have spent a lot of time in order to understand chrome storage logic.

Instinctively, if you fetch data asynchronously, usually you expect it to look like this:

//How it usually looks
const res = await library.getData()
Enter fullscreen mode Exit fullscreen mode

And so, when working with chrome storage you would probably expect it to look this way:

// What you would expect
const res = await chrome.storage.sync.get([keyShown,keyOpen])
Enter fullscreen mode Exit fullscreen mode

Unfortunately, chrome storage doesn't work so simple. The only way to receive your response is to pass a callback a function as an argument when getting data from chrome storage.

// This is the only correct way
chrome.storage.sync.get([keyShown, keyOpen], async  res  => {
//Here goes the rest of logic:( this is the only way to have access to the chrome response
}
Enter fullscreen mode Exit fullscreen mode

Instead of splitting the rest of code of fetchArticle action into smaller pieces of code, I will show you the final efect now.

  chrome.storage.sync.get([keyShown, keyOpen], async res => {
          counterAllTimeOpen = res[keyOpen] || 0 //Checking if response contains my totalOpen key
          if (keyShown in res) { //If contains, get total shown value
            counterAllTimeShown = res[keyShown]
            isFound = true
          }

          if (isFound) //If contains, increment totalShownStats 
            chrome.storage.sync.set({ [keyShown]: counterAllTimeShown + 1 })
          else { //If not, set default
            chrome.storage.sync.set({ [keyShown]: 2 })
          }

          //Fetch data section
          const url =
            'https://en.wikipedia.org/w/api.php?format=json&action=query&generator=random&grnnamespace=0&prop=extracts&grnlimit=1&explaintext='
          let resp = await fetch(url) //Fetching article
          resp = await resp.json() 

        //Getting title, extract and Id values from response
          const response = { ...resp }
          const id = Object.keys(response.query.pages)[0]
          const title = response.query.pages[id].title
          let desc = response.query.pages[id].extract

          let isTooLong = false //Some articles might be very very long - There is no enough place in that litle extension. So that, I set limit to 250. 
          if (desc.length >= 252) {
            desc = desc.substring(0, 250)
            isTooLong = true
          }

          //Final - setting state!
          setState({
            id,
            title,
            desc,
            isTooLong,
            [keyShown]: counterAllTimeShown,
            [keyOpen]: counterAllTimeOpen,
          })
        })
Enter fullscreen mode Exit fullscreen mode

I know, there was a lot of stuff in this part. If you don't understand it - Go again through this part. If you want to see the final effect of this part of code- click here.

The whole fetchArticle action is described in these steps:

  1. Setting State fields to falsify values
  2. Initializing key and value variables
  3. Getting data from chrome storage
  4. Checking if stats values are not nullable
  5. Saving incremented stat (allTimeShown) or the default value
  6. Making a Wikipedia request
  7. Getting necessary data from Wikipedia response
  8. Checking if text isn't too long (250 chars max)
  9. Updating state

If you went through this, you have already got the worst part behind you. Now it will be only easier.

The only thing left is to create an incrementOpen action but rust me - It very easy. It takes literally 4 lines of code.

 actions:{
    incrementOpen:
        () =>
        ({ setState, getState }) => {
          const key = 'allTimeOpened'
          const counter = getState()[key] + 1 || 0
          setState({ ...getState(), [key]: counter })
          chrome.storage.sync.set({ [key]: counter })
          }
     }
Enter fullscreen mode Exit fullscreen mode

This action is invoked when user clicks a blue button. Then he is redirected to the full Wikipedia webpage and "allTimeOpened" stat is increased.

Step 4 - Building full extension from top to bottom

Now that all the components have been created and whole app logic has been done, it's time to put all the pieces together.
My folder structure from the partial folder looks like this:


── src
    ├── partials
         ├── banner
     │   ├── banner.js
     ├── article
     │   ├── article.js
     ├── buttons
     │   ├── buttons.js
     ├── stats
     │   ├── stats.js
     ├── footer
     │   ├── footer.js

Enter fullscreen mode Exit fullscreen mode

Banner and Footer are totally stateless parts, so I won't describe their structure here, it's literally part of few components. Moreover, paradoxically, there is no big logic in Stats - they only show values comping from states.

Let's focus on the parts, which use actions coming from storage then.
In order to use use and manage my state properly, I import my state and treat it as a hook.

import { useCounter } from  '../../store/reducer'
Enter fullscreen mode Exit fullscreen mode

In order to use a Skeleton loading when waiting for fetching data, I must install a react-loading-skeleton package

npm i react-loading-skeleton
Enter fullscreen mode Exit fullscreen mode

Skeleton loading

Article.js

Now look at my article component. It is a place, where all the data coming from Wikipedia are being shown.

// src/partials/article/article.js 

const Article = props => {
  const [state, actions] = useCounter()

  useEffect(() => {
    actions.fetchArticle()
  }, [])

  return (
    <Layout>
      <Wrap as="article">
        <Header bold margin>
          {state.title || <Skeleton />}
        </Header>
        <StyledDesc>
          {state.desc ? (
            state.isTooLong ? (
              `${state.desc}...`
            ) : (
              state.desc
            )
          ) : (
            <Skeleton count={5} />
          )}
        </StyledDesc>
        {state.isTooLong && <Whiter />}
      </Wrap>
    </Layout>
  )
}
Enter fullscreen mode Exit fullscreen mode

As you can see, if data isn't already fetched, the Skeleton will be shown instead of empty text.
Furthermore - If text is too long, then after the description come "..." sign in order to signalize, that text has been shorted.

Note, that I have used a <Whiter> component. Thanks to that, when text is too long, this component gives an effect of disappearance of text.
text dissaperance

const Whiter = styled.div`
  background: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.1) 0%,
    rgba(255, 255, 255, 0.8) 93.23%
  );
  width: 100%;
  height: 65px;
  position: absolute;
  bottom: 0;
  left: 0;
`
Enter fullscreen mode Exit fullscreen mode

Buttons.js

This partial is responsible for having two buttons and managing stats system.
Reminder: After clicking a blue button, user is redirected to full Wikipedia article (and total clicked stats is increased) and after clicking a light button a new article is fetched (and total shown is increased, though).

// src/partials/buttons/buttons.js

const Buttons = () => {
  const [state, actions] = useCounter()

  const linkClickHandler = () => {
    actions.incrementOpen()
    window.open(`http://en.wikipedia.org/?curid=${state.id}`, '_blank').focus()
  }

  return (
    <Layout>
      <StyledWrap>
        <Button full first active={!!state.title} onClick={linkClickHandler}>
          Read full on Wikipedia
        </Button>
        <Button
          active={!!state.title}
          disabled={!state.title}
          onClick={actions.fetchArticle}
        >
          Find another article
        </Button>
      </StyledWrap>
    </Layout>
  )
}
Enter fullscreen mode Exit fullscreen mode

App.js

The only thing left is to import all partials and place it in the app component.

// src/App.js
function App() {
  return (
    <div className="App">
      <Wrap>
        <Banner />
        <Article />
        <Buttons />
        <Stats />
        <Footer />
      </Wrap>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Wikipedia chrome final effect
And so it works. I firmly believe that I described in detail process of creating my Wikipedia extension.
It's breathtaking, that the entire logic could have been done with React only.

If you have any questions - Write comments and send messages to communicate with me;)

You can find final code here: GitHub repo
Try it out: Live link

Feel free to rate my extension or give a star to my repo!

Top comments (0)