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:
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' });
  }
}
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
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}`);
});
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:
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;
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>
 );
}
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;
CSS code for the overflow_text
class:
.overflow_text {
 word-wrap: break-word;
 white-space: pre-wrap;
 word-break: break-word;
}
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;
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 }
Our application is now complete and ready to display the link previews! 😃
Top comments (0)