DEV Community

Cover image for Creating a Link Preview App: An Easy Step-by-Step Guide
Sanket
Sanket

Posted on

Creating a Link Preview App: An Easy Step-by-Step Guide

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

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

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' });
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

import express from "express"
import { getLinksHandler } from "../controllers/api.controllers.js"

const router = express.Router()
router.route("/link").get(getLinksHandler)

export default router
Enter fullscreen mode Exit fullscreen mode

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:

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}`);

});
Enter fullscreen mode Exit fullscreen mode

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

Here's the code for App.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;
Enter fullscreen mode Exit fullscreen mode

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.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

CSS code for the overflow_text class:

.overflow_text {
  word-wrap: break-word;
  white-space: pre-wrap;
  word-break: break-word;
}
Enter fullscreen mode Exit fullscreen mode

Here's the Error.jsx component:


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;
Enter fullscreen mode Exit fullscreen mode

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,

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


export { Form, LinkPreviewCard, Error }
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)