Creating a Link Preview App: An Easy Step-by-Step Guide
Tags: webdev, javascript, beginners, programming
Published: 2023-03-19 22:28:22


A link preview app is a valuable tool that displays a preview of a website, including its title, description, and associated image. This helps users get an idea of what they can expect when they click on the link. In today's social media-dominated world, having such an app is becoming increasingly essential. Although there are existing services on the internet that provide this functionality, this blog will walk you through creating your own link preview app. The most amazing part of this project is that we will not use any existing APIs. Instead, we will create our own API and connect it to our frontend later on.

So, we have mainly two things to do,

### **Step 1 : Creating the API/backend**

The backend is a crucial component of the app as it is responsible for retrieving link preview data from the URL entered by the user in the frontend. To achieve this, we will utilize Open Graph.

_**What is Open Graph ?**_

Open Graph is an internet protocol developed by Facebook in 2010 to standardize how webpages are shared on social media platforms. Website owners can use metadata tags within the webpage's HTML code to specify how their content should be displayed when shared on social media. Open Graph metadata tags provide information about the webpage's title, description, image, URL, and other relevant details. Social media platforms can then use this information to generate more engaging and informative posts when users share the webpage.

We'll be using Node.js, Express and Cheerio for our backend.

### **Step 2 : Creating the frontend**

This is the user interface for the app, which consists of two main sections: the input form for submitting the link and the preview field for displaying the link preview. I have chosen to use React.js as our frontend framework and Tailwind CSS for styling. However, you are welcome to use any frontend framework or library that you prefer. 

My styling skills are not the strongest, so please excuse any mistakes and feel free to make any necessary adjustments. 😅

Enough theory, let's get our hand dirty and start coding. Inside your project folder make two folders one for client and another for api.

## **Creating the backend**

To start developing the backend, we need to install the necessary dependencies in the `api` folder. Begin by creating a `package.json` file using the `yarn init -y` command. Then, add the required dependencies to the file. The primary dependencies we need are `express`, `cheerio`, `cors` and `axios`. You may also want to include `nodemon` as a dev dependency to help run the node app.

Despite the fact that this is a small app, I would still prefer to organize it into different folders for improved readability and more organized code. However, if you prefer to keep the code in a single file, that's perfectly fine as well. Here's my folder structure:
![Folder structure of my backend setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u1oikfirkzvyvkgtz31q.png)




Let's write the code for `api.controller.js` first

```js
import axios from "axios"
import cheerio from "cheerio"


export const getLinksHandler = async (req, res) => {
    const { url } = req.query;
    try {

        const response = await axios.get(url);
        const $ = cheerio.load(response.data);
        const title = $('title').text();
        const description = $('meta[name=description]').attr('content');
        const image = $('meta[property="og:image"]').attr('content');
        res.json({ title, description, image });
    } catch (error) {
        res.status(500).json({ error: 'Failed to fetch preview data' });
    }
}
```

To extract the URL provided by the user in the frontend, we are using destructuring to access the `url` parameter of the `req` object in our backend. We then use the `axios` library to make an HTTP GET request to the URL. Once the response is received, we utilize the `cheerio` library to load the HTML content of the response. Using `cheerio`, we then extracting the metadata from the HTML content.

Writing the route, in `api.routes.js`:

```js
import express from "express"
import { getLinksHandler } from "../controllers/api.controllers.js"
  
const router = express.Router()
router.route("/link").get(getLinksHandler)

export default router
```

This code sets up a route for the `/link` endpoint using the `GET` HTTP method. When a GET request is made to this endpoint, the `getLinksHandler` function is called to handle the request and provide a response.


It's time to complete our backend with the `app.js` code:

```js
import cors from "cors"
import express from "express"

import apiRoute from "./routes/api.routes.js"

const app = express();
app.use(cors())

app.use("/api/v1", apiRoute)

const PORT = 3000;

app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);

});
```

The code here is quite simple. Just don't forget to use `cors` package to avoid CORS error later in the frontend.


## **Let's get started with the frontend**

I have created a frontend project using `vite` and `tailwindcss`. Although it is a small project with minimal code, I still prefer to break down the code into smaller chunks for better readability and organization. When using a library like React.js, it is recommended to write code in smaller components. Here is the file and folder structure I have used for this project:
![frontend folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/shy0zo0eqjt1ddxjvl62.png)


Here's the code for `App.jsx`:

```jsx
import React, { useState } from "react";
import axios from "axios";
import { Error, Form, LinkPreviewCard } from "./components";

  

const App = () => {

  const [url, setUrl] = useState("");
  const [error, setError] = useState(null);
  const [linkPreview, setLinkPreview] = useState(null);

const urlSubmitHandler = async (e) => {
  // Prevent the default form submission behavior (i.e., refreshing the page)
  e.preventDefault();

  // Send a GET request to the link preview API endpoint using axios
  try {
    const response = await axios.get(
      `http://localhost:5000/api/v1/link?url=${url}`
    );

    // If the request succeeds, set the link preview state to the response data and clear any previous error message
    setLinkPreview(response.data);
    setError(null);
  } catch (error) {
    // If the request fails, clear the link preview state and set an error message
    setLinkPreview(null);
    setError("Oops! Failed to fetch data, check your internet connection or refresh the page and try again");
  }
};


  return (
    <main className='pb-10'>
      <Form url={url} setUrl={setUrl} urlSubmitHandler={urlSubmitHandler} />
      <section className='flex items-center justify-center px-3 mt-3 lg:mt-5'>
            {linkPreview && (
              <LinkPreviewCard
                linkPreview={linkPreview}
                url={url}
              />
            )}
        {error && <Error error={error} />}
      </section>
    </main>
  );
};

  

export default App;
```

We are using three components in this code snippet - `Form`, `LinkPreviewCard`, and `Error`. 

The `url`, `setUrl`, and `urlSubmitHandler` props are being passed to the `Form.jsx` component. Let's take a closer look.

```jsx
import { BsStars } from "react-icons/bs";

export default function Form({ url, setUrl, urlSubmitHandler }) {
  return (
    <section className='mt-3 md:mt-6 flex items-center justify-center px-3 py-3 font-poppins'>
      <form className='flex flex-col gap-3 w-full sm:w-3/4 md:w-4/5 lg:w-3/5 xl:w-2/5'>
        <input
          type='text'
          value={url}
          onChange={(event) => setUrl(event.target.value)}
          placeholder='Enter the URL you wanna preview'
          className='px-2 py-2.5 rounded-sm border border-zinc-300 focus:outline-violet-500 text-xs md:text-sm xl:text-base placeholder:text-sm placeholder:text-gray-300'
        />
        <button
          type='submit'
          onClick={urlSubmitHandler}
          className='bg-violet-500 hover:bg-violet-600 py-2.5 rounded-sm text-xs md:text-sm xl:text-base font-medium flex items-center justify-center gap-1 text-zinc-50'>
          Here we go
          <BsStars className='text-lg lg:text-xl' />
        </button>
      </form>
    </section>
  );
}
```



The `linkPreview` data and the `url` are being sent from the `App.jsx` component to the `LinkPreviewCard.jsx` component via props:

```jsx
const LinkPreviewCard = ({ linkPreview, url }) => {
  return (
    <div className='flex flex-col items-center justify-start gap-4 bg-neutral-50 py-3 md:px-4 lg:px-5 px-2 w-full sm:w-3/4 md:w-4/5 lg:w-3/5 xl:w-2/5 rounded-md text-zinc-800 border shadow-lg shadow-zinc-400'>
      {linkPreview.image && (
        <a href={url} target='_blank' className='w-full'>
          <img
            src={linkPreview.image}
            alt={linkPreview.title}
            className='border border-neutral-600 rounded-sm lg:rounded-md object-cover h-64 w-full hover:shadow-lg shadow-zinc-600'
          />
        </a>
      )}
      <div className='flex flex-col gap-1.5 w-full'>
        <a
          href={url}
          target='_blank'
          className='text-sm lg:text-base font-medium overflow_text hover:underline'>
          {linkPreview.title}
        </a>
        {linkPreview.description && (
          <p className='text-xs md:text-sm text-zinc-500'>
            {linkPreview.description}
          </p>
        )}
      </div>
    </div>
  );
};

  

export default LinkPreviewCard;
```

CSS code for the `overflow_text` class:

```css
.overflow_text {
  word-wrap: break-word;
  white-space: pre-wrap;
  word-break: break-word;
}
```

Here's the `Error.jsx` component:
```jsx

const Error = ({ error }) => {
  return (
    <div className='py-3 px-4 w-full sm:w-3/4 md:w-4/5 lg:w-3/5 xl:w-2/5 rounded-sm text-center bg-red-500'>
      <h1 className='text-xs lg:text-base font-medium text-zinc-100'>
       {error}
      </h1>
    </div>
  );
};

export default Error;
```

By the way, I have created another file named `index.js` in the `components` folder, where I imported all the `jsx` components' files and exported them. This does not make any functional difference in our application, but it makes the import statement in `App.jsx` look neater. Code in `components/index.js`,

```js
import Form from "./Form";
import LinkPreviewCard from "./LinkPreviewCard";
import Error from "./Error";


export { Form, LinkPreviewCard, Error }
```

Our application is now complete and ready to display the link previews! 😃