Multilabel Text Classification using Hugging Face Models for TensorFlow

Introduction

This tutorial explains how to perform multiple-label text classification using the Hugging Face transformers library. Hugging Face library implements advanced transformer architectures, proven to be state-of-the-art for various natural language processing tasks, including text classification.

Hugging Face library provides trainable transformer models in three flavors:

  1. Via the Trainer Class API
  2. Via PyTorch Models
  3. Via TensorFlow Models

The HuggingFace documentation for Trainer Class API is very clear and easy to use. However, I wanted to train my text classification model in TensorFlow. After some research, I found that the Hugginface API lacks documentation on fine-tuning transformers models for multilabel text classification in TensorFlow.

In this tutorial, I will explain how I fine-tuned a Hugging Face transformers model for multilabel text classification in TensorFlow.

Dataset

I will use the Toxic Comment Dataset From Kaggle to fine-tune my transformer model. Download the dataset's CSV file and import it into your Python script using the Pandas dataframe, as shown in the following script:

import pandas as pd

dataset = pd.read_csv('/content/fake-and-real-news-dataset/train.csv')
print(dataset.shape)
dataset.head()

Output:

image_1.JPG

The above output shows that the dataset contains more than 159k records. The dataset consists of 8 columns. The text comment_text column contains user comments. A comment can be categorized into one or more categories: toxic, severe toxic, obscene, threat, insult, or identity hate. A one is added in a column if a comment belongs to the column category, else a zero is added.

Several comments in the dataset do not fall into any of the comment categories. The following script returns these records:

no_toxic_comments_df = dataset[(dataset[['toxic', 
                                         'severe_toxic',
                                         'obscene', 
                                         'threat', 
                                         'insult',
                                         'identity_hate']] == 0).all(axis=1)]

print(no_toxic_comments_df.shape)

Output:

(143346, 8)

The above output shows that more than 143k records do not fall into any comment category. I will remove these records since I am only interested in comments assigned to at least one category.

toxic_comments_df = dataset[(dataset[['toxic', 
                                         'severe_toxic',
                                         'obscene', 
                                         'threat', 
                                         'insult',
                                         'identity_hate']] != 0).any(axis=1)]

print(toxic_comments_df.shape)

Output:

(16225, 8)
Data Preprocessing

Like every machine learning problem, we need to divide our dataset into features and labels set before model training. Subsequently, we need to divide our dataset into training and test sets. The following script does that.

X = list(toxic_comments_df['comment_text'])
y = toxic_comments_df.drop(['id', 'comment_text'], axis = 1).values.tolist()


from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)

Hugging Face transformers model accept data input data to be in a particular format. You can use tokenizers to convert raw text into the Hugging Face complaint format.

The script below installs the Hugging Face library.

! pip install datasets transformers[sentencepiece]

The following script defines the transformer model (English Distil Bert), and the tokenizer for the model.

model_name = "distilbert-base-uncased-finetuned-sst-2-english"

from transformers import AutoTokenizer, TFAutoModel
tokenizer = AutoTokenizer.from_pretrained(model_name, do_lowercase = True)
bert = TFAutoModel.from_pretrained(model_name, from_pt = True)

The Tokenizer class object converts the input training and test sets into Distil Bert complaint input format in the following script.

train_encodings = tokenizer(X_train, truncation=True, padding="max_length", max_length=512)
test_encodings = tokenizer(X_test, truncation=True, padding="max_length", max_length=512)

And the script below creates TensorFlow datasets (including output labels) for TensorFlow model training and testing.

train_dataset = tf.data.Dataset.from_tensor_slices((
    dict(train_encodings),
    y_train
))
test_dataset = tf.data.Dataset.from_tensor_slices((
    dict(test_encodings),
    y_test
))
Model Training

The Hugging Face model can be added as an encoder layer to the TensorFlow model. The input is passed to the encoder layer.

Following are the steps to incorporate a Hugging Face transformer model for fine-tuning as a TensorFlow model:

  1. Create a Python Class that inherits from the Keras.Model class.
  2. Pass the Hugging Face transformer model to the constructor of the Python class you created in the first step. The encoder variable stores the Huggin Face model in the following script.
  3. In the Call() method of the class, pass the input (TF datasets) to the encoder layer.
  4. Subsequently, add the standard TensorFlow layer to define the overall model architecture.

The following script defines the TensorFlow model that fine-tunes the Hugging Face transformer. I added three dense layers after the encoder layer. The final dense layer contains six nodes since we have six comment categories in the output.

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

import numpy as np

class TextClassificationModel(keras.Model):
  def __init__(self, encoder):
    super(TextClassificationModel, self).__init__()
    self.encoder = encoder
    self.encoder.trainable = True
    self.dropout1 = layers.Dropout(0.1)
    self.dropout2 = layers.Dropout(0.1)
    self.dropout3 = layers.Dropout(0.1)
    self.dense1 = layers.Dense(100, activation="relu")
    self.dense2 = layers.Dense(50, activation="relu")
    self.dense3 = layers.Dense(6, activation="sigmoid")

  def call(self, input):
    x = self.encoder(input)
    x = x['last_hidden_state'][:, 0, :]
    x = self.dropout1(x)
    x = self.dense1(x)
    #x = self.dropout2(x)
    x = self.dense2(x)
    #x = self.dropout3(x)
    x = self.dense3(x)
    return x

The script below initializes and compiles our TensorFlow model.

text_classification_model = TextClassificationModel(bert)

metric = "binary_crossentropy"

text_classification_model.compile(
    optimizer = tf.keras.optimizers.Adam(learning_rate=5e-5), 
    loss = tf.keras.losses.BinaryCrossentropy(from_logits=True), 
    metrics=tf.keras.metrics.BinaryCrossentropy(
    name="binary_crossentropy", dtype=None, from_logits=False, label_smoothing=0))

Finally, you can train the model using the TensorFlow model class's fit() method.

history = text_classification_model.fit(
    train_dataset.shuffle(1000).batch(16), 
    epochs=3, 
    validation_data=test_dataset.batch(16)
)

Let's plot the model loss against the number of epochs.

import seaborn as sns
import matplotlib.pyplot as plt

sns.set(rc={'figure.figsize':(10,8)})


sns.set_context('poster', font_scale = 1)

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss - ' + metric)
plt.ylabel('loss - ' + metric)
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

Output:

image3.JPG

The above plot shows that we achieved the lowest loss after the first epoch, and the model started to overfit after that.

Predictions and Evaluations

The following script makes predictions on the test set.

y_pred = text_classification_model.predict(test_dataset.batch(16))
y_pred[0]

Output:

array([9.9412137e-01, 2.0126806e-04, 1.7747579e-03, 7.2536163e-04,
       3.4322389e-03, 9.6116390e-04], dtype=float32)

The dataset input labels are binary, while the output predictions are continuous numeric values. We will convert continuous output values to binary to compare the test labels with output predictions. All the output values greater than 0.5 are converted to 1, while the values less than or equal to 0.5 are converted to 0.

The following script evaluates the model performance.

from sklearn.metrics import roc_auc_score, classification_report

y_pred = (y_pred >0.50)


print(classification_report(y_test, y_pred))

roc_auc = roc_auc_score(y_test, y_pred, average = 'macro')
print(roc_auc)

Output:

image4.JPG

Translating CSV Files using DeepL and Pandas Dataframes in Python

Introduction

In this tutorial, you will see how to convert the text in CSV file columns to other languages using the DeepL API in the Python programing language.

DeepL is one of the most popular and accurate text translation platforms. DeepL, as the name suggests, incorporates advanced deep learning algorithms for training text translation models.

In addition to raw text strings, DeepL supports translating documents in PDF, MS Word, and PowerPoint formats. However, I wanted to translate text in CSV file columns, which DeepL does not support.

In this tutorial, I will explain how I achieved translating text in CSV columns using the DeepL API. The resultant CSV will have new columns containing translated text.

Translating Text with DeepL in Python

I chose Python language for translating text in CSV files since DeepL has an official Python client that you can exploit for text translation in your code.

The official GitHub repository explains installing the DeepL API along with sample scripts.

Here I will provide a simple example for your reference. The following are the steps:

Create an Object of the Translator class and pass it your DeepL authorization key.
Pass the text you want to translate to the translate_text() method of the Translator class. You must also pass the ISO 639-1 standard language code to the target_lang attribute of the translate_text() method.
To get the translated text, access the text attribute of the object returned by the translate_text() function.

Here is an example:

import os
from deepl import Translator

auth_key = os.environ['deepL-key']
dl_translator = Translator(auth_key)

result = dl_translator.translate_text("Python is awesome", target_lang="FR")
print(result.text) 

Output:

Python est gnial
Translating Text in a CSV File using a Pandas Dataframe

Lets now see how to translate a CSV file. For example, I will translate a CSV file containing Yelp Reviews. The file looks like the one in the following screenshot. The text column contains reviews in the English language.

image_1.JPG

A Pandas Dataframe is ideal for reading, manipulating, and saving CSV files in Python. The following script imports the yelp.csv file into a Pandas dataframe.

import pandas as pd

dataset = pd.read_csv("D:\Datasets\yelp.csv")

dataset.head()

Output:

image_2.JPG

I will remove all the columns except the text column. You can only translate 500,000 characters with the free DeepL API. Therefore, for the sake of demonstration, I will keep the first 20 rows of the dataset.

import pandas as pd

dataset = pd.read_csv("D:\Datasets\yelp.csv")

dataset = dataset.filter(["text"], axis = 1).head(20)
dataset.head()

Output:

image_3.JPG

Lets now translate reviews in the text column. I want to add a new column that contains a translation of the reviews in the corresponding row in the text column.

To do so, I will define a translate_df() function that accepts a Pandas dataframe. The function will call the translate_text() method to translate text in the individual rows of the text column.

Next, I can use the apply() method of Pandas dataframe and pass the function variable translate_df to this method. The apply() method applies the translate_df() function to each row of the input Pandas dataframe. I will add a new column named text_fr, which contains the translated text.

The following script translates text into the French language.

import os
import pandas as pd
from deepl import Translator

auth_key = os.environ['deepL-key']
dl_translator = Translator(auth_key)

def translate_df(df):
     return dl_translator.translate_text(df["text"], target_lang="FR").text


dataset["text_fr"] = dataset.apply(translate_df, axis = 1)
dataset.head()

Output:

image_4.JPG

Instead of hardcoding the target language in the translate_df() function, you can pass the target language as a parameter value to the translate_df() function. Here is an example that translates the input text into the Spanish language.

import os
import pandas as pd
from deepl import Translator

auth_key = os.environ['deepL-key']
dl_translator = Translator(auth_key)

def translate_df(df, lang):
     return dl_translator.translate_text(df["text"], target_lang= lang).text

language = "es"
dataset["text_es"] = dataset.apply(translate_df, 
                                   lang = language, 
                                   axis = 1)
dataset.head()

Output:

image_5.JPG

You might want to convert text into multiple languages. To do so, you can modify the translate_df() and pass it a list of target languages. You can then iterate through the target languages list to translate the input text.

The translated text for each language is appended to a list that you can convert to a Pandas series. Finally, you can return the Pandas series to the calling function. The following script modifies the translate_text() function.

import os
import pandas as pd
from deepl import Translator

auth_key = os.environ['deepL-key']
dl_translator = Translator(auth_key)

def translate_df(df, lang):

    translate_list = []
    for i in lang:
        translate_list.append(dl_translator.translate_text(df["text"], target_lang= i).text)

    return pd.Series(translate_list)

The script below converts the text in the text column of the input Pandas dataframe to Spanish and French.

import numpy as np
languages = ["es", "fr"]
col_names = ["text_es", "text_fr"]

for newcol in col_names:
    dataset[newcol]=np.nan

dataset[col_names] = dataset.apply(translate_df, 
                                   lang = languages, 
                                   axis = 1)

dataset.head()

In the output below, you can see that the input text is converted to Spanish and French.

Output:

image_6.JPG

Once you translate text in a Pandas dataframe, you can easily store the Pandas dataframe as a CSV file using the to_csv() function. Here is an example:

dataset.to_csv("D:\Datasets\yelp_translated.csv", index = False)

The output below shows the CSV file containing the input text and corresponding translations in Spanish and French.

Output:

image_7.JPG

DeepL Library is a convenient library for text translation. It provides raw text and document translation services for PDF, MS Word, and PowerPoint documents. However, you cannot translate text in CSV file columns by default. In this tutorial, I explained how you could achieve that using DeepL API Python client and Pandas dataframes. I hope you will find it helpful. If you have any suggestions or improvements, feel free to comment.

How to Create Pandas DataFrames with Tweets Scraped by Locations

Introduction

I was working on a problem where I had to scrape tweets related to the T20 Cricket World Cup 2022, which is currently taking place in Australia.

I wanted tweets containing location names (cities) and the keyword T20. In the response, I want the user names of tweet authors, tweet texts, creation time, and the location keyword used to search the tweet. Finally, I wanted to create a Python Pandas Dataframe that contains these values in columns.

In this article, I will explain how you can return scrape tweets containing location information and how to store these tweets in a Pandas Dataframe.

Developers can scrape tweets from Twitter using the Twitter REST API. In most cases, the Twitter API returns tweet-type objects that contain various attributes for extracting tweet information. However, by default, the Twitter API doesn't return a Pandas Dataframe.

Simple Example of Scraping Tweets

You must sign up with Twitter Developer Account and create your API Key and Token to access the Twitter REST API. The official documentation explains signing up for the Twitter Developer Account.

I will use the Python Tweepy library for accessing the Twitter API. Tweepy is an unofficial Python client for accessing the Twitter API.

The following script demonstrates a basic example of Twitter scraping with the Python Tweepy library.

I use the search_all_tweets() function to search 100 English language tweets containing keywords Sydney and T20. I set a filter for removing retweets.

import pandas as pd
import os
import tweepy

bt = os.environ['twitter-bt']

client = tweepy.Client(bearer_token = bt,
                      wait_on_rate_limit= False)

location = "Sydney T20"
language = "lang:en "
no_tweet = "-is:retweet "


query = '"'+location+'" ' + language + no_tweet

tweets = client.search_all_tweets(query = query, 
                                  max_results=100)

tweets.data

You can use the data attribute of the response object to print the returned tweets. You can access tweet text using the text attribute as shown below:

Output:

image_1.JPG

Scraping Tweets into Pandas DataFrames

Now you know how to scrape tweets with Tweepy, lets scrape tweets with the following information and store them in a Pandas dataframe:

  1. Username of the tweet author.
  2. Tweet text.
  3. Creation time of the tweet.
Scraping Tweets with a Single Location

I will scrape tweets with the following filters:

  1. Location keywords, e.g., Sydney T20
  2. English language
  3. No Retweets
  4. Between October 01, 2022, to October 30, 2022.

In this case, I passed values for the tweet_fields, user_fields, and expansions attributes of the search_all_tweets() function. These attributes extract tweet creation time and user information, e.g., username.

location = "Sydney T20"
language = "lang:en "
no_tweet = "-is:retweet "

start_time = '2022-10-01T00:00:00Z'
end_time = '2022-10-30T00:00:00Z'


query = '"'+location+'" ' + language + no_tweet

tweets = client.search_all_tweets(query = query, 
                                  tweet_fields=['created_at'],
                                  user_fields=['username'],
                                  expansions=['author_id'],
                                  start_time=start_time,
                                  end_time=end_time, 
                                  max_results=100)

tweets.includes['users']

In addition to the data attribute, which contains tweet information, you can retrieve user information using the tweets.includes[users] list.

Output:

image_2.JPG

You can iterate through lists of tweet data and user data to extract user name, tweet, text, and tweet creation time and append these values to Python lists.

Finally, you can create a Pandas dataframe using these lists. Here is an example script:

user_name = []
text = []
date_time = []


for tweet, user  in zip(tweets.data, tweets.includes['users']):
    text.append(tweet.text)
    date_time.append(tweet.created_at)
    if user is not None:
        user_name.append(user.name)


df = pd.DataFrame(list(zip(user_name, text, date_time)),
               columns =['User', 'Text', 'Date Time'])

df.head(20)

Output:

image_3.JPG

Scraping Tweets with Multiple Locations

Since I wanted to extract tweets by multiple location keywords, I defined a Python function that accepts a list of locations, along with the start and end date of tweets. The function iterates through all the locations and appends the extracted tweet information in Python lists.

The get_location_tweets() function in the following script returns a Pandas dataframe containing the tweet text, the tweet author's username, the tweet's creation time, and the location keyword that parameterizes the tweet search.

import time
def get_location_tweets(loc_list, st, et):


    user_name = []
    text = []
    date_time = []
    loc_keyword = []

    i = 0

    for loc in loc_list:

        i = i + 1

        location = loc + " T20"
        language = "lang:en "
        no_tweet = "-is:retweet "

        start_time = st
        end_time = et


        query = '"'+location+'" ' + language + no_tweet

        tweets = client.search_all_tweets(query = query, 
                                          tweet_fields=['created_at'],
                                          user_fields=['username'],
                                          expansions=['author_id'],
                                          start_time=start_time,
                                          end_time=end_time, 
                                          max_results=100)


        user_name = []
        text = []
        date_time = []


        for tweet, user  in zip(tweets.data, tweets.includes['users']):
            text.append(tweet.text)
            date_time.append(tweet.created_at)
            if user is not None:
                user_name.append(user.name)

            loc_keyword.append(loc)

        time.sleep(3)


    df = pd.DataFrame(list(zip(user_name, text, date_time, loc_keyword)),
                   columns =['User', 'Text', 'Date Time', 'Loc Keyword'])


    return df

The following script calls the get_location_tweets() function to search for tweets containing either Sydney, Perth, Melbourne and the keyword T20.

loc_list = ["Sydney", "Perth", "Melbourne"]

start_time = '2022-10-01T00:00:00Z'
end_time = '2022-10-30T00:00:00Z'

locations_df = get_location_tweets(loc_list, start_time, end_time)

print(locations_df.shape)
locations_df.sample(frac=1).head(10)

Output:

image_4.JPG

Extracting tweets using Twitter API is straightforward. However, storing desired information from tweets in a data structure, e.g., a Pandas dataframe, is tricky. In this tutorial, I explained how you could scrape tweets using Twitter API and store them in a Pandas Dataframe in your desired format.

DoublePrecision

Help me!
I have this number 1726.16723958045672098127.
How Can I register in my db MySql.
Why is the number truncated?

Why Affective Computing and Mindless AI Will Conquer the World

Emotion and Technology

Affective computing is a term that refers to the synergy between AI and psychology in order to understand and affect emotions. Another term for it could be emotional AI. It is a niche within AI that is not very popular, but I really believe it is going to become increasingly in popularity in the near future.

Until very recently, machine learning did not have many things to say about psychology. We, of course, had sentiment analysis, but that’s that. In the last few years, some companies tried to combine psychometrics with machine learning in order to create improved psychometric questionnaires. However, I really believe that the advancements in effective computing and, what is now called mindless AI, are what is going to cause a potential paradigm shift.

8 Steps to a Winning Product Strategy Made Simple: What You Need to Know

You built a product, and it’s working well. You might even achieve your goals and start making a profit. But how do you know when you’re done building it? When do you need to create a new feature or add a new marketing channel?

When you bring a product to market, you solve a problem for your users. But you’re not the only person who can solve that problem. In fact, you’re one piece in a much larger ecosystem — a network of developers, customers, and manufacturers — all working together to build a better product. It’s a complex ecosystem, and you need a product strategy that can help you navigate it. 

How to Stop Search Engines from Crawling a WordPress Site

Recently, one of our users asked us how they can stop search engines from crawling and indexing their WordPress site.

There are several scenarios when you would want to stop search engines from crawling your website and listing it in search results.

In this article, we will show you how to stop search engines from crawling a WordPress site.

How to Stop Search Engines From Crawling a WordPress Site

Why Stop Search Engines From Crawling a WordPress Site?

For most WordPress websites, search engines are the biggest source of traffic. You may ask, why would anyone want to block search engines?

Here are some situations when you won’t want search engines to index your website:

  • When starting out, you may not know how to create a local development environment or a staging site, and instead develop your website while it’s live. You won’t want Google to index your site when it’s under construction or in maintenance mode.
  • There are also many people who use WordPress to create private blogs. They don’t want them indexed by search engines because they’re private.
  • You can use WordPress for project management or an intranet. In these cases, you won’t want your internal documents to be publicly accessible.

A common misconception is that if you don’t have links pointing to your domain, then search engines will probably never find your website. This is not completely true.

For example, there may be links pointing to your site because the domain name was previously owned by someone else. Also, there are thousands of pages on the internet that simply list domain names. Your site may appear on one of those.

With that being said, let’s take a look at how to stop search engines from crawling your website. We’ll cover four methods:

Method 1: Asking Search Engines not to Crawl Your WordPress Site

This is the simplest method but does not fully protect your website from being crawled.

WordPress comes with a built-in feature that allows you to instruct search engines not to index your site. All you need to do is visit Settings » Reading and check the box next to ‘Search Engine Visibility’.

Search Engine Visibility Setting in WordPress

When this box is checked, WordPress adds this line to your website’s header:

<meta name='robots' content='noindex,follow' />

WordPress also modifies your site’s robots.txt file and adds these lines to it:

User-agent: *
Disallow: /

These lines ask robots (web crawlers) not to index your pages. However, it is totally up to search engines to accept this request or ignore it. Even though most search engines respect this, there’s still a chance that some pages or images from your site may get indexed.

If you want to make it impossible for search engines to index or crawl your website, then you will need to password protect your WordPress site using Methods 3 or 4.

Method 2: Asking Search Engines not to Crawl Individual Pages

You might want search engines to crawl and index your website, but not include certain posts or pages in search results pages.

The easiest way to do that is using the All in One SEO (AIOSEO) plugin. It is the best SEO tool for WordPress and is trusted by over 3 million businesses.

For this tutorial, we’ll be using the AIOSEO free version as it includes the SEO Analysis tool. There is also a premium version of AIOSEO that offers more features like sitemap tools, redirection manager, schema markup, robots.txt editor, and more.

The first thing you’ll need to do is install and activate the AIOSEO plugin on your website. You can learn how to install and configure the plugin by following our step by step guide on how to set up All in One SEO for WordPress.

Once the plugin is set up, you can use it to ask search engines not to index certain posts and pages. Again, it is totally up to search engines to accept this request or ignore it.

Simply edit the post or page that you don’t want to be indexed. You need to scroll down to AIOSEO Settings at the bottom of the WordPress editor and then click the ‘Advanced’ tab.

AIOSEO Advanced Settings

Notice that the article is using the default robots settings. To change this, you need to switch the ‘Use Default Settings’ toggle to the off position.

Asking Search Engines Not to Index a Single Page

Now you can click the ‘No Index’ checkbox. Once the post is published or updated, search engines will be asked not to index it.

Method 3: Password Protecting an Entire Site Using cPanel

If your WordPress hosting provider offers cPanel access to manage your hosting account, then you can protect your entire site using cPanel. All you have to do is log in to your cPanel dashboard and then click on the ‘Directory Privacy’ icon in the ‘Files’ section.

Using cPanel's Directory Privacy Feature

Next, you need to find the folder where you installed WordPress. Usually, it is the public_html folder. After that, you need to click the ‘Edit’ button next to that folder.

Edit the Root Folder

Note: If you have multiple WordPress sites installed under public_html directory, then you need to click on the public_html link to browse those sites, and then edit the folder for the website you want to password protect.

This brings you to a screen where you can turn on password protection.

Simply check the box that says ‘Password protect this directory’ and click the ‘Save’ button. If you like, you can also customize the name for the protected directory.

Check the Box to Password Protect This Directory

You will see a confirmation message saying that the access permissions for the directory have been changed.

Next, you should click the ‘Go Back’ button.

cPanel's Directory Privacy Success Message

You’ll be taken to a screen where you can create a username and password that will need to be used when accessing this directory.

You need to enter a username and password and then confirm the password. Make sure to note your username and password in a safe place, such as a password manager app.

Add a Username and Password

Once you click the ‘Save’ button you have successfully added password protection to your WordPress site.

Now, whenever a user or search engine visits your website they will be prompted to enter the username and password you created earlier to view the site.

A Username and Password Are Now Needed to Access Your Website

Method 4: Password Protecting WordPress With a Plugin

If you are using a managed WordPress hosting solution, then you may not have access to cPanel. In that case, you can use a WordPress plugin to password protect your site.

Here are the two most popular solutions:

  1. SeedProd is the #1 coming soon and maintenance mode plugin for WordPress, used on over 800,000 websites. It comes with complete access control and permissions features that you can use to hide your website from everyone including search engines. We have a step by step how-to guide for SeedProd.
  2. Password Protected is a very simple way to password protect your WordPress site with a single password (no user creation needed). See our step by step guide on how to password protect a WordPress site.

We hope this article helped you stop search engines from crawling or indexing your WordPress site. You may also want to learn how to speed up your website performance, or see our ultimate step by step WordPress security guide for beginners.

If you liked this article, then please subscribe to our YouTube Channel for WordPress video tutorials. You can also find us on Twitter and Facebook.

The post How to Stop Search Engines from Crawling a WordPress Site first appeared on WPBeginner.

Top Blockchain APIs for the Developer

Blockchain technology is applied in many industries and is steadily gaining support and users' trust. Many start-ups and businesses try to develop unique web platforms or mobile apps with a wide range of blockchain usage. As the blockchain and crypto markets thrive, developers have many opportunities and projects to develop, like trading bots, payment applications, or business settlement solutions. In this article, we'll figure out how API is used for blockchain and how API integration might benefit the app. 

What Is Blockchain API?

Application Programming Interface (API) integration is used to communicate with blockchain nodes or client networks. By doing so, it gets data and displays it to the user. APIs can be used for transactions, managing accounts, crypto trading, making analytics, and providing security. Top blockchain APIs are considered secure, easy to use, and reliable in the long run. Distributing data securely might be along with blockchain technology, but it's safer and more convenient. Choosing one is no easy feat, as the diversity of APIs is truly impressive.

Compare the Best Link Building Tools

Our recommendation for the best link building tool is Ahrefs because it has several valuable features to simplify and streamline link research and prospecting. Start your 7-day trial for just $7.

Link building takes a lot of time, effort, and technical capability. But using the right tool can certainly make your life a lot easier. 

This guide compares the best link building tools that are purpose-built to streamline every step in the link building process, from identifying opportunities for new backlinks to automating email outreach, and beyond. 

The Top 6 Best Link Building Tools

Here’s a quick rundown of tools you can use to score powerful backlinks and automate your link-building campaigns. Ahrefs edges forward over other tools because of its significantly large database of live backlinks and several other link-building-focused features. Begin your 7-day trial by paying $7.

Next, we’ll break down the features of each tool to help you find the best fit for your needs.

Company logos for our best link building tools reviews.

Match Your Scenario to the Right Link Building Solution

While there’s no shortage of reliable link building tools, they won’t necessarily meet your expectations or match your use case.

You’re looking for an all-in-one SEO tool

Best option: Ahrefs

Ahrefs has one of the most diverse SEO suites that come with several SEO-focused tools. 

In addition to Site Explorer and Content Explorer tools that are great for link building, you also get a Keyword Explorer and Rank Tracker to identify and keep track of your target keyword and how your website ranks in the SERPs.

Another great choice: Semrush

Besides executing end-to-end link building, Semrush has tons of dedicated tools to boost your site’s organic ranking.

Alongside a backlink gap tool that performs in-depth backlink gap analysis of your site and your competitors, it also has tools to perform keyword research, audit sites, perform comparative analysis, and track rankings of your target keywords. 

If you want an all-in-one SEO solution with link building capabilities, look for tools that:

  • Are full-fledged SEO suites
  • Offers multiple tools to optimize your website as a whole
  • Let you track crucial metrics to keep a tab of your SEO efforts

You want to simplify link prospecting and outreach

Best choice: Buzzstream 

With Buzzstream, you can easily reach out to relevant websites and influencers to earn quality backlinks.

It gives you a centralized place to organize, personalize, send, and track emails, keeping you on top of your link building efforts. An integrated CRM is again useful to streamline your outreach and assign related tasks to teammates.

Another great choice: Hunter

Hunter is one of the more prominent email finder and verification tools, so it’s a no-brainer option when it comes to outreach. 

It provides you with verified email addresses of contacts working in reputable sources, letting you reach out for guest posting and podcast feature opportunities without extra hassle.

If you want to streamline your link building outreach, look for tools that:

  • Have built-in tools to find and verify email addresses
  • Let you organize and track link building emails
  • Integrate with leading cold email software (good to have)

You want a complete breakdown of your website link profile

Best option: Moz Link Explorer 

Moz Link Explorer gives you a high-level overview of inbound links, linking domains, and ranking keywords. It also provides you with charts showing your domain and page authority, along with a sub-reporting functionality, to keep you on top of your website’s as well as your competitors’ backlink profiles.

Another great option: Linkody

Linkody is the most affordable way to keep track of your and your competitors’ backlink profile. It helps you identify and eliminate bad links and provides valuable insights into most linked web pages, thanks to its 24/7 link monitoring.

It also has an intuitive dashboard that allows you to easily track key SEO metrics.

If you want a detailed breakdown of any website’s backlink profile, look for tools that:

  • Offers tools to check the quantity, anchor, and quality of links pointing to your website
  • Has an intuitive dashboard to filter and view results as per your preference
  • Identify and eliminate toxic backlinks hurting your ranking

Link Building Company Reviews 

It’s certainly possible to build backlinks without tools, but leveraging them can significantly enhance your efforts and facilitate better decision-making. However, not every link building tool is worth your time. 

Check out my top recommendations to make the right choice.

Ahrefs — Best for Competitor Research and Link Analysis

Ahrefs, one of the best link building tools

Ahrefs offers tons of excellent SEO and marketing tools that go beyond link building, but its end-to-end link building capabilities are certainly worth mentioning.

Site Explorer, for example, lets you uncover useful insights into your competitor’s backlinks. It comes with a link intersect feature that provides a list of all the websites that link to your competitors but not you. 

You can also use the Content Explorer feature to identify the most popular articles on a given keyword/topic based on the number of referring domains linking to them. Add these domains to your own link building outreach lists if they make a good match for your needs.

What Makes Ahrefs Great

Screenshot of Ahrefs backlink checker webpage.
Ahrefs’ backlink index has over 3 trillion live backlinks.

Ahrefs has a built-in site auditing tool and an excellent keyword research tool, but it’s the Site Explorer and Content Explorer functionalities that make Ahrefs your best bet when it comes to link building.

But these features are just the tip of the iceberg. 

For instance, Ahrefs’ crawler is the one of most active tools (second to Google) and has a backlink index containing over 3 trillion live backlinks. You can also sort your web pages by link growth to determine which pages are getting the highest number of backlinks and then replicate that strategy for other web pages.

Ahrefs offers plans starting at $99, but if you choose the annual plan, you get two months free. You can also pay a small $7 fee to get access to all features for seven days.

Buzzstream — Best for Link Prospecting and Outreach

BuzzStream, one of the best link building tools

A crucial part of effective link building is finding publishers and influencers who have genuine interest in your content asset and are willing to link back to your page. With Buzzstream, you can do just that, thanks to its powerful link building tools that make it easier than ever to discover link opportunities.

Identify top-performing and relevant influencers and publishers based on your niche, conduct outreach, and manage relationships with them. Easily organize, personalize, send, and track emails from a single place as you expand your brand’s reach.

There’s also an integrated CRM that keeps your team in sync. Add multiple users to your account and assign tasks between them to ensure your link building campaigns run like a well-oiled machine.

What Makes Buzzstream Great

Screenshot of an email for backlinking through BuzzStream.
With Buzzstream, you can find publishers and influencers willing to link back to your page.

Buzzstream is an excellent option to scale your link building campaigns faster, saving you time and energy. 

A digital PR feature lets you build a list of journalists and bloggers to identify and earn high-quality links, while custom fields are handy for organizing and customizing campaigns to your exact preferences. 

You can also use its Chrome extension called Buzzmarker to accelerate your link prospecting efforts. It’s a handy tool to find contact information, add prospects to campaigns, and send emails from readymade templates—all while you continue browsing the internet.

Buzzstream’s pricing starts from $24 per month. A 14-day trial is also available on all plans.

Semrush — Best for Agencies 

Semrush, one of the best link building tools

Semrush is a comprehensive backlink software designed to create and execute end-to-end link building to ensure concrete results.

While it makes a great tool for individual use, I recommend it for agencies. Semrush is an all-in-one SEO suite with efficient link building capabilities that makes it easy to track top-performing links based on geographical region. 

The fact that its dedicated link building tool identifies the best backlink opportunities using its extensive database of over 43 trillion backlinks ensures agency owners can deliver the best possible results.

What Makes Semrush Great

Screenshot of Semrush backlink gap analysis tool.
Run a comprehensive backlink gap analysis with Semrush’s Backlink Gap Tool.

Semrush has a Backlink Gap Tool that allows you to run a comprehensive backlink gap analysis that serves as a great starting point for any link building strategy. Use it to compare your link profile with the competition and discover potential opportunities to score high-quality links.

If you have a weaker link profile than your competitors, focus on acquiring links so they don’t have to close this “gap.” View the link gap prospects based on four categories: Weak (domains that point to you less than competitors), Strong (domains that point to you but not any of the competitors), Shared (domains that link to all the entered domains, and Unique (domains that only link to one domain). 

Semrush’s subscriptions start from $119.95 and are payable monthly. A limited free account and a free 7-day trial are both available.

Moz Link Explorer — Best for Link Tracking

Moz, one of the best link building tools

This list would be incomplete without mentioning Moz Link Explorer.

It allows you to search URLs and domains and p can also download CSV files for all these data points to hold detailed discussions with your team. 

In addition to viewing a breakdown of lost and newly discovered links, Moz also generates charts showing how your domain and page authority has changed over a specific time period. A sub-reporting feature lets you see which of your web pages are getting the most links and identify anchor texts your linking sites use the most.

What Makes Moz Link Explorer Great

Screenshot of Moz link building tool how it works infographic.
Moz keeps track of lost and newly discovered links.

While Moz Link Explorer is great for analyzing the backlink profile of any website, it’s also handy for finding link opportunities, repairing broken lines, and removing bad links. Check the Spam Score of your backlinks to remove spammy links pointing to your website and increase the quality of your links.

Other than this, you get access to several other tools such as a keyword explorer tool and an on-page grader to further enhance your SEO strategy. There’s also a Moz reporting integration that lets you create detailed dashboards featuring all your backlink data, alongside over 70 other marketing platforms.

Moz’s pricing starts from $99 per month, but you can start with a generous 30-day free trial of its Pro plan.

Hunter — Best for Easy Email Verification

Hunter, one of the best link building tools

When doing outreach for potential link building opportunities, you’ve likely struggled to find the right email address. This is a huge advantage that ends your campaign before you even begin. 

Luckily, Hunter can help you out of this situation. 

Simply enter the domain name or company you’re targeting, and Hunter will provide you with a list of emails for people working at the website or publication. What’s more, its unique email verification feature automatically verifies each email it gives you to ensure your efforts don’t go in vain. 

What Makes Hunter Great

Screenshot of Hunter's email address finder.
Hunter’s Domain Search feature allows you to find and verify email addresses at the touch of a button.

Hunter makes reaching out to reputable sources for podcast features, guest posts, and other marketing strategies easy, thereby improving your on-page SEO and scoring those valuable backlinks.

It has a Domain Search feature that allows you to find the email addresses of specific companies, and an Email Finder tool to identify the right email address of a particular professional.

I also like its Google Sheets add-on that lets you match emails for all your link building prospects—all with a single click.

Hunter offers a free plan, as well as paid plans starting from $49 per month.

Linkody — Best for Affordability 

Linkody, one of the best link building tools

Linkody is one of the most user-friendly link building tools with very affordable pricing plans, providing excellent value for your money.

Despite the lower prices, it offers several features that are useful for analyzing your backlink profile and identifying and eliminating bad links. It provides you with all the information you need to easily manage all your links and domains in one place and understand when you lose or gain links.

An easy-to-understand dashboard allows you to easily track top SEO metrics (for example, DA, TF, PA) to stay on top of your link building campaign. 

What Makes Linkody Great

Screenshot from Linkody website showing a dashboard with essential metrics.
Linkody has several affordable pricing plans, ensuring excellent value for your money.

Full disclosure: Linkody isn’t as advanced as the previous tools I mentioned, but it’s certainly worth a shot for those with a limited budget.

Use it to discover your competitors’ backlinks and identify which pages point to you and your competitors—or only to them. You can also view their most linked pages to find opportunities to get more quality links. Linkody also offers 24/7 link monitoring for your website and your competitors and sends email reports in case of any status changes.

As mentioned, Linkody is one of the cheapest link building tools right now, with plans starting from only $14.90 per month. If you choose to pay annually, you get three months free. A free 30-day trial—no credit card required—is also available.

Quick Sprout Link Building Related Content

If you want to learn more about effective link building, check out our other guides for more in-depth information.

Link Building Guides and How-Tos

The Top Link Building in Summary

Link building tools make your backlinking process more efficient. If you use them correctly, you’ll find yourself in the perfect position to earn high-quality links to boost your website presence.

Ahrefs is the best link building tool that comes with plenty of features to help you better understand your link profile and identify new link prospects.

Rendering External API Data in WordPress Blocks on the Back End

This is a continuation of my last article about “Rendering External API Data in WordPress Blocks on the Front End”. In that last one, we learned how to take an external API and integrate it with a block that renders the fetched data on the front end of a WordPress site.

The thing is, we accomplished this in a way that prevents us from seeing the data in the WordPress Block Editor. In other words, we can insert the block on a page but we get no preview of it. We only get to see the block when it’s published.

Let’s revisit the example block plugin we made in the last article. Only this time, we’re going to make use of the JavaScript and React ecosystem of WordPress to fetch and render that data in the back-end Block Editor as well.

Where we left off

As we kick this off, here’s a demo where we landed in the last article that you can reference. You may have noticed that I used a render_callback method in the last article so that I can make use of the attributes in the PHP file and render the content.

Well, that may be useful in situations where you might have to use some native WordPress or PHP function to create dynamic blocks. But if you want to make use of just the JavaScript and React (JSX, specifically) ecosystem of WordPress to render the static HTML along with the attributes stored in the database, you only need to focus on the Edit and Save functions of the block plugin.

  • The Edit function renders the content based on what you want to see in the Block Editor. You can have interactive React components here.
  • The Save function renders the content based on what you want to see on the front end. You cannot have the the regular React components or the hooks here. It is used to return the static HTML that is saved into your database along with the attributes.

The Save function is where we’re hanging out today. We can create interactive components on the front-end, but for that we need to manually include and access them outside the Save function in a file like we did in the last article.

So, I am going to cover the same ground we did in the last article, but this time you can see the preview in the Block Editor before you publish it to the front end.

The block props

I intentionally left out any explanations about the edit function’s props in the last article because that would have taken the focus off of the main point, the rendering.

If you are coming from a React background, you will likely understand what is that I am talking about, but if you are new to this, I would recommend checking out components and props in the React documentation.

If we log the props object to the console, it returns a list of WordPress functions and variables related to our block:

Console log of the block properties.

We only need the attributes object and the setAttributes function which I am going to destructure from the props object in my code. In the last article, I had modified RapidAPI’s code so that I can store the API data through setAttributes(). Props are only readable, so we are unable to modify them directly.

Block props are similar to state variables and setState in React, but React works on the client side and setAttributes() is used to store the attributes permanently in the WordPress database after saving the post. So, what we need to do is save them to attributes.data and then call that as the initial value for the useState() variable.

The edit function

I am going to copy-paste the HTML code that we used in football-rankings.php in the last article and edit it a little to shift to the JavaScript background. Remember how we created two additional files in the last article for the front end styling and scripts? With the way we’re approaching things today, there’s no need to create those files. Instead, we can move all of it to the Edit function.

Full code
import { useState } from "@wordpress/element";
export default function Edit(props) {
  const { attributes, setAttributes } = props;
  const [apiData, setApiData] = useState(null);
    function fetchData() {
      const options = {
        method: "GET",
        headers: {
          "X-RapidAPI-Key": "Your Rapid API key",
          "X-RapidAPI-Host": "api-football-v1.p.rapidapi.com",
        },
      };
      fetch(
        "https://api-football-v1.p.rapidapi.com/v3/standings?season=2021&league=39",
          options
      )
      .then((response) => response.json())
      .then((response) => {
        let newData = { ...response }; // Deep clone the response data
        setAttributes({ data: newData }); // Store the data in WordPress attributes
        setApiData(newData); // Modify the state with the new data
      })
      .catch((err) => console.error(err));
    }
    return (
      <div {...useBlockProps()}>
        <button onClick={() => getData()}>Fetch data</button>
        {apiData && (
          <>
          <div id="league-standings">
            <div
              className="header"
              style={{
                backgroundImage: `url(${apiData.response[0].league.logo})`,
              }}
            >
              <div className="position">Rank</div>
              <div className="team-logo">Logo</div>
              <div className="team-name">Team name</div>
              <div className="stats">
                <div className="games-played">GP</div>
                <div className="games-won">GW</div>
                <div className="games-drawn">GD</div>
                <div className="games-lost">GL</div>
                <div className="goals-for">GF</div>
                <div className="goals-against">GA</div>
                <div className="points">Pts</div>
              </div>
              <div className="form-history">Form history</div>
            </div>
            <div className="league-table">
              {/* Usage of [0] might be weird but that is how the API structure is. */}
              {apiData.response[0].league.standings[0].map((el) => {
                
                {/* Destructure the required data from all */}
                const { played, win, draw, lose, goals } = el.all;
                  return (
                    <>
                    <div className="team">
                      <div class="position">{el.rank}</div>
                      <div className="team-logo">
                        <img src={el.team.logo} />
                      </div>
                      <div className="team-name">{el.team.name}</div>
                      <div className="stats">
                        <div className="games-played">{played}</div>
                        <div className="games-won">{win}</div>
                        <div className="games-drawn">{draw}</div>
                        <div className="games-lost">{lose}</div>
                        <div className="goals-for">{goals.for}</div>
                        <div className="goals-against">{goals.against}</div>
                        <div className="points">{el.points}</div>
                      </div>
                      <div className="form-history">
                        {el.form.split("").map((result) => {
                          return (
                            <div className={`result-${result}`}>{result}</div>
                          );
                        })}
                      </div>
                    </div>
                    </>
                  );
                }
              )}
            </div>
          </div>
        </>
      )}
    </div>
  );
}

I have included the React hook useState() from @wordpress/element rather than using it from the React library. That is because if I were to load the regular way, it would download React for every block that I am using. But if I am using @wordpress/element it loads from a single source, i.e., the WordPress layer on top of React.

This time, I have also not wrapped the code inside useEffect() but inside a function that is called only when clicking on a button so that we have a live preview of the fetched data. I have used a state variable called apiData to render the league table conditionally. So, once the button is clicked and the data is fetched, I am setting apiData to the new data inside the fetchData() and there is a rerender with the HTML of the football rankings table available.

You will notice that once the post is saved and the page is refreshed, the league table is gone. That is because we are using an empty state (null) for apiData‘s initial value. When the post saves, the attributes are saved to the attributes.data object and we call it as the initial value for the useState() variable like this:

const [apiData, setApiData] = useState(attributes.data);

The save function

We are going to do almost the same exact thing with the save function, but modify it a little bit. For example, there’s no need for the “Fetch data” button on the front end, and the apiData state variable is also unnecessary because we are already checking it in the edit function. But we do need a random apiData variable that checks for attributes.data to conditionally render the JSX or else it will throw undefined errors and the Block Editor UI will go blank.

Full code
export default function save(props) {
  const { attributes, setAttributes } = props;
  let apiData = attributes.data;
  return (
    <>
      {/* Only render if apiData is available */}
      {apiData && (
        <div {...useBlockProps.save()}>
        <div id="league-standings">
          <div
            className="header"
            style={{
              backgroundImage: `url(${apiData.response[0].league.logo})`,
            }}
          >
            <div className="position">Rank</div>
            <div className="team-logo">Logo</div>
            <div className="team-name">Team name</div>
            <div className="stats">
              <div className="games-played">GP</div>
              <div className="games-won">GW</div>
              <div className="games-drawn">GD</div>
              <div className="games-lost">GL</div>
              <div className="goals-for">GF</div>
              <div className="goals-against">GA</div>
              <div className="points">Pts</div>
            </div>
            <div className="form-history">Form history</div>
          </div>
          <div className="league-table">
            {/* Usage of [0] might be weird but that is how the API structure is. */}
            {apiData.response[0].league.standings[0].map((el) => {
              const { played, win, draw, lose, goals } = el.all;
                return (
                  <>
                  <div className="team">
                    <div className="position">{el.rank}</div>
                      <div className="team-logo">
                        <img src={el.team.logo} />
                      </div>
                      <div className="team-name">{el.team.name}</div>
                      <div className="stats">
                        <div className="games-played">{played}</div>
                        <div className="games-won">{win}</div>
                        <div className="games-drawn">{draw}</div>
                        <div className="games-lost">{lose}</div>
                        <div className="goals-for">{goals.for}</div>
                        <div className="goals-against">{goals.against}</div>
                        <div className="points">{el.points}</div>
                      </div>
                      <div className="form-history">
                        {el.form.split("").map((result) => {
                          return (
                            <div className={`result-${result}`}>{result}</div>
                          );
                        })}
                      </div>
                    </div>
                  </>
                );
              })}
            </div>
          </div>
        </div>
      )}
    </>
  );
}

If you are modifying the save function after a block is already present in the Block Editor, it would show an error like this:

The football rankings block in the WordPress block Editor with an error message that the block contains an unexpected error.

That is because the markup in the saved content is different from the markup in our new save function. Since we are in development mode, it is easier to remove the bock from the current page and re-insert it as a new block — that way, the updated code is used instead and things are back in sync.

This situation of removing it and adding it again can be avoided if we had used the render_callback method since the output is dynamic and controlled by PHP instead of the save function. So each method has it’s own advantages and disadvantages.

Tom Nowell provides a thorough explanation on what not to do in a save function in this Stack Overflow answer.

Styling the block in the editor and the front end

Regarding the styling, it is going to be almost the same thing we looked at in the last article, but with some minor changes which I have explained in the comments. I’m merely providing the full styles here since this is only a proof of concept rather than something you want to copy-paste (unless you really do need a block for showing football rankings styled just like this). And note that I’m still using SCSS that compiles to CSS on build.

Editor styles
/* Target all the blocks with the data-title="Football Rankings" */
.block-editor-block-list__layout 
.block-editor-block-list__block.wp-block[data-title="Football Rankings"] {
  /* By default, the blocks are constrained within 650px max-width plus other design specific code */
  max-width: unset;
  background: linear-gradient(to right, #8f94fb, #4e54c8);
  display: grid;
  place-items: center;
  padding: 60px 0;

  /* Button CSS - From: https://getcssscan.com/css-buttons-examples - Some properties really not needed :) */
  button.fetch-data {
    align-items: center;
    background-color: #ffffff;
    border: 1px solid rgb(0 0 0 / 0.1);
    border-radius: 0.25rem;
    box-shadow: rgb(0 0 0 / 0.02) 0 1px 3px 0;
    box-sizing: border-box;
    color: rgb(0 0 0 / 0.85);
    cursor: pointer;
    display: inline-flex;
    font-family: system-ui, -apple-system, system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 16px;
    font-weight: 600;
    justify-content: center;
    line-height: 1.25;
    margin: 0;
    min-height: 3rem;
    padding: calc(0.875rem - 1px) calc(1.5rem - 1px);
    position: relative;
    text-decoration: none;
    transition: all 250ms;
    user-select: none;
    -webkit-user-select: none;
    touch-action: manipulation;
    vertical-align: baseline;
    width: auto;
    &:hover,
    &:focus {
      border-color: rgb(0, 0, 0, 0.15);
      box-shadow: rgb(0 0 0 / 0.1) 0 4px 12px;
      color: rgb(0, 0, 0, 0.65);
    }
    &:hover {
      transform: translateY(-1px);
    }
    &:active {
      background-color: #f0f0f1;
      border-color: rgb(0 0 0 / 0.15);
      box-shadow: rgb(0 0 0 / 0.06) 0 2px 4px;
      color: rgb(0 0 0 / 0.65);
      transform: translateY(0);
    }
  }
}
Front-end styles
/* Front-end block styles */
.wp-block-post-content .wp-block-football-rankings-league-table {
  background: linear-gradient(to right, #8f94fb, #4e54c8);
  max-width: unset;
  display: grid;
  place-items: center;
}

#league-standings {
  width: 900px;
  margin: 60px 0;
  max-width: unset;
  font-size: 16px;
  .header {
    display: grid;
    gap: 1em;
    padding: 10px;
    grid-template-columns: 1fr 1fr 3fr 4fr 3fr;
    align-items: center;
    color: white;
    font-size: 16px;
    font-weight: 600;
    background-color: transparent;
    background-repeat: no-repeat;
    background-size: contain;
    background-position: right;

    .stats {
      display: flex;
      gap: 15px;
      &amp; &gt; div {
        width: 30px;
      }
    }
  }
}
.league-table {
  background: white;
  box-shadow:
    rgba(50, 50, 93, 0.25) 0px 2px 5px -1px,
    rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
  padding: 1em;
  .position {
    width: 20px;
  }
  .team {
    display: grid;
    gap: 1em;
    padding: 10px 0;
    grid-template-columns: 1fr 1fr 3fr 4fr 3fr;
    align-items: center;
  }
  .team:not(:last-child) {
    border-bottom: 1px solid lightgray;
  }
  .team-logo img {
    width: 30px;
    top: 3px;
    position: relative;
  }
  .stats {
    display: flex;
    gap: 15px;
    &amp; &gt; div {
      width: 30px;
      text-align: center;
    }
  }
  .last-5-games {
    display: flex;
    gap: 5px;
    &amp; &gt; div {
      width: 25px;
      height: 25px;
      text-align: center;
      border-radius: 3px;
      font-size: 15px;
    &amp; .result-W {
      background: #347d39;
      color: white;
    }
    &amp; .result-D {
      background: gray;
      color: white;
    }
    &amp; .result-L {
      background: lightcoral;
      color: white;
    }
  }
}

We add this to src/style.scss which takes care of the styling in both the editor and the frontend. I will not be able to share the demo URL since it would require editor access but I have a video recorded for you to see the demo:


Pretty neat, right? Now we have a fully functioning block that not only renders on the front end, but also fetches API data and renders right there in the Block Editor — with a refresh button to boot!

But if we want to take full advantage of the WordPress Block Editor, we ought to consider mapping some of the block’s UI elements to block controls for things like setting color, typography, and spacing. That’s a nice next step in the block development learning journey.


Rendering External API Data in WordPress Blocks on the Back End originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Putting The Graph In GraphQL With The Neo4j GraphQL Library

This article is a sponsored by Neo4j

GraphQL enables an API developer to model application data as a graph, and API clients that request that data to easily traverse this data graph to retrieve it. These are powerful game-changing capabilities. But if your backend isn’t graph-ready, these capabilities could become liabilities by putting additional pressure on your database, consuming greater time and resources.

In this article, I’ll shed some light on ways you can mitigate these issues when you use a graph database as the backend for your next GraphQL API by taking advantage of the capabilities offered by the open-source Neo4j GraphQL Library.

What Graphs Are, And Why They Need A Database

Fundamentally, a graph is a data structure composed of nodes (the entities in the data model) along with the relationships between nodes. Graphs are all about the connections in your data. For this reason, relationships are first-class citizens in the graph data model.

Graphs are so important that an entire category of databases was created to work with graphs: graph databases. Unlike relational or document databases that use tables or documents, respectively, as their data models, the data model of a graph database is (you guessed it!) a graph.

GraphQL is not and was never intended to be a database query language. It is indeed a query language, yet it lacks much of the semantics we would expect from a true database query language like SQL or Cypher. That’s on purpose. You don’t want to be exposing our entire database to all our client applications out there in the world.

Instead, GraphQL is an API query language, modeling application data as a graph and purpose-built for exposing and querying that data graph, just as SQL and Cypher were purpose-built for working with relational and graph databases, respectively. Since one of the primary functions of an API application is to interact with a database, it makes sense that GraphQL database integrations should help build GraphQL APIs that are backed by a database. That’s exactly what the Neo4j GraphQL Library does — makes it easier to build GraphQL APIs backed by Neo4j.

One of GraphQL’s most powerful capabilities enables the API designer to express the entire data domain as a graph using nodes and relationships. This way, API clients can traverse the data graph to find the relevant data. This makes better sense because most API interactions are done in the context of relationships. For example, if we want to fetch all orders placed by a specific customer or all the products in a given order, we’re traversing the pattern of relationships to find those connections in our data.

Soon after GraphQL was open-sourced by Facebook in 2015, a crop of GraphQL database integrations sprung up, evidently in an effort to address the n+1 conundrum and similar problems. Neo4j GraphQL Library was one of these integrations.

Common GraphQL Implementation Problems

Building a GraphQL API service requires you to perform two steps:

  1. Define the schema and type definitions.
  2. Create resolver functions for each type and field in the schema that will be responsible for fetching or updating data in our data layer.

Combining these schema and resolver functions gives you an executable GraphQL schema object. You may then attach the schema object to a networking layer, such as a Node.js web server or lambda function, to expose the GraphQL API to clients. Often developers will use tools like Apollo Server or GraphQL Yoga to help with this process, but it’s still up to them to handle the first two steps.

If you’ve ever written resolver functions, you’ll recall they can be a bit tedious, as they’re typically filled with boilerplate data fetching code. But even worse than lost developer productivity is the dreaded n+1 query problem. Because of the nested way that GraphQL resolver functions are called, a single GraphQL request can result in multiple round-trip requests to the database. Addressing this typically involves a batching and caching strategy, adding additional complexity to your GraphQL application.

Doubling Down On GraphQL-First Development

Originally, the term GraphQL-First Development described a collaborative process. Frontend and backend teams would agree on a GraphQL schema, then go to work independently building their respective pieces of the codebase. Database integrations extend the idea of GraphQL-First development by applying this concept to the database as well. GraphQL-type definitions can now drive the database.

You can find the full code examples presented here on GitHub.

Let’s say you’re building a business reviews application where you want to keep track of businesses, users, and user reviews. GraphQL-type definitions to describe this API might look something like this:

type Business {
  businessId: ID!
  name: String!
  city: String!
  state: String!
  address: String!
  location: Point!
  reviews: [Review!]! @relationship(type: "REVIEWS", direction: IN)
  categories: [Category!]!
    @relationship(type: "IN_CATEGORY", direction: OUT)
}

type User {
  userID: ID!
  name: String!
  reviews: [Review!]! @relationship(type: "WROTE", direction: OUT)
}

type Review {
  reviewId: ID!
  stars: Float!
  date: Date!
  text: String
  user: User! @relationship(type: "WROTE", direction: IN)
  business: Business! @relationship(type: "REVIEWS", direction: OUT)
}

type Category {
  name: String!
  businesses: [Business!]!
    @relationship(type: "IN_CATEGORY", direction: IN)
}

Note the use of the GraphQL schema directive @relationship in our type definitions. GraphQL schema directives are the language’s built-in extension mechanism and key components for extending and configuring GraphQL APIs — especially with database integrations like Neo4j GraphQL Library. In this case, the @relationship directive encodes the relationship type and direction (in or out) for pairs of nodes in the database.

Type definitions are then used to define the property graph data model in Neo4j. Instead of maintaining two schemas (one for our database and another for our API), you can now use type definitions to define both the API and the database’s data model. Furthermore, since Neo4j is schema-optional, using GraphQL to drive the database adds a layer of type safety to your application.

From GraphQL Type Definitions To Complete API Schemas

In GraphQL, you use fields on special types (Query, Mutation, and Subscription) to define the entry points for the API. In addition, you may want to define field arguments that can be passed at query time, for example, for sorting or filtering. Neo4j GraphQL Library handles this by creating entry points in the GraphQL API for create, read, update, and delete operations for each type, as well as field arguments for sorting and filtering.

Let’s look at some examples. For our business reviews application, suppose you want to show a list of businesses sorted alphabetically by name. Neo4j GraphQL Library automatically adds field arguments to accomplish just this.

{
  businesses(options: { limit: 10, sort: { name: ASC } }) {
    name
  }
}

Perhaps you want to allow the users to filter this list of businesses by searching for companies by name or keyword. The where argument handles this kind of filtering:

{
  businesses(where: { name_CONTAINS: "Brew" }) {
    name
    address
  }

You can then combine these filter arguments to express very complex operations. Say you want to find businesses that are in either the Coffee or Breakfast category and filter for reviews containing the keyword “breakfast sandwich:”

{
  businesses(
    where: {
      OR: [
        { categories_SOME: { name: "Coffee" } }
        { categories_SOME: { name: "Breakfast" } }
      ]
    }
  ) {
    name
    address
    reviews(where: { text_CONTAINS: "breakfast sandwich" }) {
    stars
    text
  }
 }
}

Using location data, for example, you can even search for businesses within 1 km of our current location:

{
  businesses(
    where: {
      location_LT: {
        distance: 1000
        point: { latitude: 37.563675, longitude: -122.322243 }
      }
    }
  ) {
  name
  address
  city
  state
  }
}

As you can see, this functionality is extremely powerful, and the generated API can be configured through the use of GraphQL schema directives.

We Don’t Need No Stinking Resolvers

As we noted earlier, GraphQL server implementations require resolver functions where the logic for interacting with the data layer lives. With database integrations such as Neo4j GraphQL Library, resolvers are generated for you at query time for translating arbitrary GraphQL requests into singular, encapsulated database queries. This is a huge developer productivity win (we don’t have to write boilerplate data fetching code — yay!). It also addresses the n+1 query problem by making a single round-trip request to the database.

Moreover, graph databases like Neo4j are optimized for exactly the kind of nested graph data traversals commonly expressed in GraphQL. Let’s see this in action. Once you’ve defined your GraphQL type definitions, here’s all the code necessary to spin up your fully functional GraphQL API:

const { ApolloServer } = require("apollo-server");
const neo4j = require("neo4j-driver");
const { Neo4jGraphQL } = require("@neo4j/graphql");

// Connect to your Neo4j instance.
const driver = neo4j.driver(
  "neo4j+s://my-neo4j-db.com",
  neo4j.auth.basic("neo4j", "letmein")
);

// Pass our GraphQL type definitions and Neo4j driver instance.
const neoSchema = new Neo4jGraphQL({ typeDefs, driver });

// Generate an executable GraphQL schema object and start
// Apollo Server.
neoSchema.getSchema().then((schema) => {
  const server = new ApolloServer({
    schema,
  });
  server.listen().then(({ url }) => {
    console.log(`GraphQL server ready at ${url}`);
  });
});

That’s it! No resolvers.

Extend GraphQL With The Power Of Cypher

So far, we’ve only been talking about basic create, read, update, and delete operations. How can you handle custom logic with Neo4j GraphQL Library?

Let’s say you want to show recommended businesses to your users based on their search or review history. One way would be to implement your own resolver function with the logic for generating those personalized recommendations built in. Yet there’s a better way to maintain the one-to-one, GraphQL-to-database operation performance guarantee: You can leverage the power of the Cypher query language using the @cypher GraphQL schema directive to define your custom logic within your GraphQL type definitions.

Cypher is an extremely powerful language that enables you to express complex graph patterns using ASCII-art-like declarative syntax. I won’t go into detail about Cypher in this article, but let’s see how you could accomplish our personalized recommendation task by adding a new field to your GraphQL-type definitions:

extend type Business {
  recommended(first: Int = 1): [Business!]!
    @cypher(
      statement: """
        MATCH (this)<-[:REVIEWS]-(:Review)<-[:WROTE]-(u:User)
        MATCH (u)-[:WROTE]->(:Review)-[:REVIEWS]->(rec:Business)
        WITH rec, COUNT(*) AS score
        RETURN rec ORDER BY score DESC LIMIT $first
      """
    )
}

Here, the Business type has a recommended field, which uses the Cypher query defined above to show recommended businesses whenever requested in the GraphQL query. You didn’t need to write a custom resolver to accomplish this. Neo4j GraphQL Library is still able to generate a single database request even when using a custom recommended field.

GraphQL Database Integrations Under The Hood

GraphQL database integrations like Neo4j GraphQL Library are powered by the GraphQLResolveInfo object. This object is passed to all resolvers, including the ones generated for us by Neo4j GraphQL Library. It contains information about both the GraphQL schema and GraphQL operation being resolved. By closely inspecting this object, GraphQL database integrations can generate database queries at the time queries are placed.

If you’re interested, I recently gave a talk at GraphQL Summit that goes into much more detail.

Powering Low-Code, Open Source-Powered GraphQL Tools

An open-source library that works with any JavaScript GraphQL implementation can conceivably power an entire ecosystem of low-code GraphQL tools. Collectively, these tools leverage the functionality of Neo4j GraphQL Library to help make it easier for you to build, test, and deploy GraphQL APIs backed by a real graph database.

For example, GraphQL Mesh uses Neo4j GraphQL Library to enable Neo4j as a data source for data federation. Don’t want to write the code necessary to build a GraphQL API for testing and development? The Neo4j GraphQL Toolbox is an open-source, low-code web UI that wraps Neo4j GraphQL Library. This way, it can generate a GraphQL API from an existing Neo4j database with a single click.

Where From Here

If building a GraphQL API backed by a native graph database sounds interesting or at all helpful for the problems you’re trying to solve as a developer, I would encourage you to give the Neo4j GraphQL Library a try. Also, the Neo4j GraphQL Library landing page is a good starting point for documentation, further examples, and comprehensive workshops.

I’ve also written a book Full Stack GraphQL Applications, published by Manning, that covers this topic in much more depth. My book covers handling authorization, working with the frontend application, and using cloud services like Auth0, Netlify, AWS Lambda, and Neo4j Aura to deploy a full-stack GraphQL application. In fact, I’ve built out the very business reviews application from this article as an example in the book! Thanks to Neo4j, this book is now available as a free download.

Last but not least, I will be presenting a live session entitled “Making Sense of Geospatial Data with Knowledge Graphs” during the NODES 2022 virtual conference on Wednesday, November 16, produced by Neo4j. Registration is free to all attendees.