How to Generate Rich Link Preview for Website Link / URL based on the Meta Tags Present in the Web Page with Spring Boot and Jsoup

What you’ll build

Home page

Enter URL for Rich Link Preview

Link Preview

Rich Link Preview

API for fetching link preview info by URL

API for fetching Rich Link Preview

What you’ll need

  • Spring Tool Suite 4
  • JDK 11

Tech Stack

  • Spring Boot 2.2
  • JSP and Bootstrap 4.1.3
  • JQuery 3.4.1
  • Jsoup 1.12.1

What is Rich Link Preview ?

Rich Link Preview allows you to see a peek of the link location and usually generated realtime by visiting the website, and reading the meta tags present in the web page.

Bootstrap Your Application

You can create your spring boot application with the web & devtools dependencies and download it from here

Project Structure

+---pom.xml
|   
|       
\---src
    \---main
        +---java
        |   \---com
        |       \---javachinna
        |           \---linkpreview
        |               |   SpringBootLinkPreviewApplication.java
        |               |   
        |               +---controller
        |               |       PagesController.java
        |               |       
        |               \---model
        |                       Link.java
        |                       
        +---resources
        |       application.properties
        |       
        \---webapp
            \---WEB-INF
                \---views
                    \---pages
                            link.jsp
                            previewLink.jsp

Project Dependencies

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.javachinna</groupId>
	<artifactId>spring-boot-link-preview</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-boot-link-preview</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
		</dependency>

		<!-- To compile JSP files -->
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-jasper</artifactId>
			<scope>provided</scope>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
		<dependency>
			<groupId>org.jsoup</groupId>
			<artifactId>jsoup</artifactId>
			<version>1.12.1</version>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
		</dependency>
		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>27.1-jre</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<!-- <scope>runtime</scope> -->
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

Create Controller

PagesController.java

@RestController
public class PagesController {

	private final Logger logger = LogManager.getLogger(getClass());

	@GetMapping({ "/", "/home" })
	public ModelAndView home(@RequestParam(value = "view", required = false) String view) {
		logger.info("Entering home page");
		ModelAndView model = new ModelAndView("previewLink");
		model.addObject("title", "Home");
		model.addObject("view", view);
		return model;
	}

	/**
	 * Fetches rich link preview info for the given URL based on the meta tags
	 * present in the web page
	 * 
	 * 
	 * @param url
	 * @return
	 */
	@GetMapping("/api/link/preview")
	public Link getLinkPreviewInfo(@RequestParam(value = "url", required = true) String url) {
		Link link = null;
		try {
			link = extractLinkPreviewInfo(url);
		} catch (IOException e) {
			logger.error("Unable to connect to : {}", url);
		}
		return link;
	}

	/**
	 * Generates rich link preview for the given URL based on the meta tags present
	 * in the web page
	 * 
	 * @param url
	 * @return
	 */
	@GetMapping("/link/preview")
	public ModelAndView linkPreview(@RequestParam(value = "url", required = true) String url) {
		ModelAndView model = new ModelAndView("link");
		try {
			model.addObject("link", extractLinkPreviewInfo(url));
		} catch (IOException e) {
			logger.error("Unable to connect to : {}", url);
			model.addObject("css", "danger");
			model.addObject("msg", "Unable to connect to '" + url + "': " + e.getMessage());
		}
		return model;
	}

	/**
	 * Parses the web page and extracts the info from meta tags required for preview
	 * 
	 * @param url
	 * @return
	 * @throws IOException
	 */
	private Link extractLinkPreviewInfo(String url) throws IOException {
		if (!url.startsWith("http")) {
			url = "http://" + url;
		}
		Document document = Jsoup.connect(url).get();
		String title = getMetaTagContent(document, "meta[name=title]");
		String desc = getMetaTagContent(document, "meta[name=description]");
		String ogUrl = StringUtils.defaultIfBlank(getMetaTagContent(document, "meta[property=og:url]"), url);
		String ogTitle = getMetaTagContent(document, "meta[property=og:title]");
		String ogDesc = getMetaTagContent(document, "meta[property=og:description]");
		String ogImage = getMetaTagContent(document, "meta[property=og:image]");
		String ogImageAlt = getMetaTagContent(document, "meta[property=og:image:alt]");
		String domain = ogUrl;
		try {
			domain = InternetDomainName.from(new URL(ogUrl).getHost()).topPrivateDomain().toString();
		} catch (Exception e) {
			logger.warn("Unable to connect to extract domain name from : {}", url);
		}
		return new Link(domain, url, StringUtils.defaultIfBlank(ogTitle, title), StringUtils.defaultIfBlank(ogDesc, desc), ogImage, ogImageAlt);
	}

	/**
	 * Returns the given meta tag content
	 * 
	 * @param document
	 * @return
	 */
	private String getMetaTagContent(Document document, String cssQuery) {
		Element elm = document.select(cssQuery).first();
		if (elm != null) {
			return elm.attr("content");
		}
		return "";
	}
}

For simplicity, I’ve added the link preview info fetching logic in the controller itself instead of putting them into the service layer.

@RestController annotation is a convenience annotation that is itself annotated with @Controller and @ResponseBody . This annotation is applied to a class to mark it as a request handler. Spring RestController annotation is used to create RESTful web services using Spring MVC

@GetMapping is a composed annotation that acts as a shortcut for @RequestMapping(method = RequestMethod. GET)

Note: The getLinkPreviewInfo() method is the REST service mapped with path /api/link/preview. So If you are just looking for a REST service which will return the preview info for the given URL as a JSON response, then you can get it by htting http://localhost:8080/api/link/preview?url=<website url>

Create Model Class

Link.java

public class Link {

	private static final long serialVersionUID = -706243242873257798L;

	public Link() {
	}

	public Link(String domain, String url, String title, String desc, String image, String imageAlt) {
		this.domain = domain;
		this.url = url;
		this.title = title;
		this.desc = desc;
		this.image = image;
		this.imageAlt = imageAlt;
	}

	private String domain;

	private String url;

	private String title;

	private String desc;

	private String image;

	private String imageAlt;

	// Setters and Getters goes here
}

Create Spring Boot Application Class

SpringBootLinkPreviewApplication.java

@SpringBootApplication Indicates a configuration class that declares one or more @Bean methods and also triggers auto-configuration and component scanning. This is a convenience annotation that is equivalent to declaring @Configuration@EnableAutoConfiguration and @ComponentScan

@SpringBootApplication
public class SpringBootLinkPreviewApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootLinkPreviewApplication.class, args);
	}
}

Create View Pages

linkPreview.jsp

<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<title>Demo</title>
</head>
<body>
	<div class="container-fluid mt-2">
		<div class="row">
			<div class="col-12 col-md-5">
				<c:url value="/postLink" var="actionUrl" />
				<div class="form-group">
					<label for="title">Link </label> <input type="text" class="form-control" id="url" aria-describedby="url" placeholder="Enter URL here" />
				</div>
				<div class="mb-2">
					<button class="btn btn-sm btn-dark" value="preview" onclick="preview()">Preview Link</button>
				</div>
				<div class="form-group" id="link-preview"></div>
			</div>
			<div class="col-md-7 d-none d-md-block"></div>
		</div>
	</div>
	<script >
		function preview() {
			var url = $('#url').val();
			if (url) {
				$.get('/link/preview?url=' + url, function(data, status) {
					$('#link-preview').html(data);
				});
			}
		}
	</script>
</body>
</html>

link.jsp

<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<c:choose>
	<c:when test="${empty msg}">
		<div class="card shadow mb-2">
			<div class="card-body">
				<h6 class="text-muted pb-2 m-0">
					<i class="fas fa-link"></i> Link Preview
				</h6>
				<a href="${link.url}" class="custom-card" target="_blank" data-image="${link.image}" data-image-alt="${link.imageAlt}" data-title="${link.title}"
					data-desc="${link.desc}" data-domain="${link.domain}">
					<div class="card">
						<img src="${link.image}" class="card-img-top" alt="${link.imageAlt}" style="max-height: 300px; object-fit: fill;">
						<div class="card-footer">
							<h5 class="card-title">${link.title}</h5>
							<p class="card-text">${link.desc}</p>
							<p class="card-text">
								<small class="text-muted">${link.domain}</small>
							</p>
						</div>
					</div>
				</a>
			</div>
		</div>
	</c:when>
	<c:otherwise>
		<div class="alert alert-${css} alert-dismissible" role="alert">
			<button type="button" class="close" data-dismiss="alert" aria-label="Close">
				<span aria-hidden="true">×</span>
			</button>
			<strong>${msg}</strong>
		</div>
	</c:otherwise>
</c:choose>

Create application.properties file

spring.mvc.view.prefix=/WEB-INF/views/pages/
spring.mvc.view.suffix=.jsp

Run with Maven

You can run the application using mvn clean spring-boot:run and visit to http://localhost:8080

Source Code

https://github.com/Chinnamscit/spring-boot-link-preview

Conclusion

That’s all folks! In this tutorial, you’ve learned how to get link preview info for a given url and generate the preview using spring boot application.

I hope you enjoyed this tutorial. Thank you for reading.

Leave a Reply