This post first appeared on my blog at www.mikenikles.com. Photo by JJ Ying on Unsplash
The inspiration for this post came from reading Ilona's fantastic post on dev.to titled "Frontend Shorts: How to create link content previewer with React, Vue, and Vanilla JavaScript". She did an amazing job explaining all the details, for VanillaJS, React and Vue. Please have a look at her post for full details.
In summary, she developed a feature that displays a content preview when a user moves their mouse over a link.
Below is an animated gif Ilona created to illustrate how this looks in practice:
Link content previewer in action
With all code in place as described in her post, the solution can be used as follows:
<a href="https://dev.to"
onmouseover="showLinkPreview()"
onmouseleave="hideLinkPreview()"
class="link-with-preview"
data-image="https://thepracticaldev.s3.amazonaws.com/i/6hqmcjaxbgbon8ydw93z.png"
data-title="DEV Community 👩‍💻👨‍💻"
data-text="Where programmers share ideas and help each other grow—A constructive and inclusive social network."
>
dev.to.
</a>
There must be a way to dynamically create the preview... 🤔
This was my first thought when I read the blog post and looked at the source code. What if we fetched the link's href
source in the onmouseover
handler and dynamically set the title, text and image?
A sample Sapper application is available on my Github profile at https://github.com/mikenikles/sapper-link-preview.
Shouldn't be too hard, eh? Use the browser's built-in fetch
in the onmouseover
handler, parse the returned HTML and look for the page's <title>
, some sort of meta tag for the text and image.
Note: Continue to read before you try this out ;-)
❌ Cross-Origin Resource Sharing (CORS)
From the MDN web docs:
Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell browsers to give a web application running at one origin, access to selected resources from a different origin. A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, or port) from its own.
In other words, our website on https://domain-a.com can't easily fetch content from https://domain-b.com. However, the whole point of the link content previewer is to do exactly that.
A backend service to the rescue
Alright then, if we can't fetch the content on the client-side, let's do it on the backend. There's no CORS issues there and we can easily fetch any URL, parse its HTML and create the link preview.
Sapper server routes
In a Sapper application, we can use server routes to do the job for our link conten previewer.
All we need is the following server route at src/routes/link-preview.json.js
:
import got from "got";
import * as cheerio from "cheerio";
// TODO: Expand to include other meta tags (e.g. Facebook, Twitter, etc.)
const getTitle = ($) => $("head title").text()
const getDescription = ($) => $("meta[name=description]").attr("content")
const getImgSrc = ($) => $("meta[property='og:image']").attr("content")
export const get = async (req, res) => {
const { href } = req.query;
const fetchResponse = await got(href);
const $ = cheerio.load(fetchResponse.body);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
title: getTitle($),
description: getDescription($),
imgSrc: getImgSrc($)
}));
}
It exports a get
function, which maps to a GET request to /link-preview.json
. Since that endpoints is served from the same domain as the frontend application, we have no CORS issues.
We need two libraries:
-
[got](https://www.npmjs.com/package/got)
: A request library for Node.js -
[cheerio](https://www.npmjs.com/package/cheerio)
: An implementation of core jQuery designed for the server
In the get
function, we first read the href
request query parameter. The frontend can provide that when calling the endpoint with /link-preview.json?href=https://dev.to
.
We send a GET request to that URL using got
. Once the response comes back, we load the response body into cheerio
, this helps us query the HTML in a way that's similar to how jQuery works.
Lastly, the get
function returns a JSON object with the title
, description
and imgSrc
, all read from the HTML.
Note: As commented in the code, the getTitle
, getDescription
and getImgSrc
functions can be extended at will to read different meta tags.
Test the backend service
In your Sapper project, run npm install
and npm run dev
to start the server. If you access http://localhost:3000/link-preview.json?href=https://dev.to
, you'll see the JSON response as expected.
Create a Svelte component
To use the above backend service, let's create a <LinkPreview>
Svelte component. The goal is to end up with the following usage of this component:
<LinkPreview href="https://dev.to">dev.to</LinkPreview>
Expressed as a user story:
As a frontend developer,
I want to use the<LinkPreview>
component the same way I would use an<a>
tag,
so that I can leverage all supported<a>
attributes.
With these goals in mind, let's start with a new src/components/link-preview.svelte
file:
<a href={$$props.href} {...$$props}>
<slot />
</a>
This gives us a <LinkPreview>
component that simply wraps an <a>
tag and passes all props to the underlying <a>
tag. For example, <LinkPreview href="https://dev.to">dev.to</LinkPreview>
renders as <a href="https://dev.to">dev.to</a>
.
Add the preview card to display the link content preview
We could modify the existing link-preview.svelte
component and add the HTML for the card and the mouseover
and mouseleave
event handlers. However, let's instead create a separate component for that to encapsulate the behavior and design.
Create a src/components/link-preview-card.svelte
file with basic Svelte boilerplate, plus the card HTML and CSS Ilona provided in her blog post:
<script>
</script>
<style>
.card {
width: 150px;
font-size: 10px;
color: black;
position: absolute;
z-index: 100;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
}
.card img {
width: 150px;
}
.card-title {
font-size: 14px;
}
</style>
<div class="card">
<img src="" class="card-img-top">
<div class="card-body">
<h5 class="card-title"></h5>
<p class="card-text"></p>
</div>
</div>
Next, let's define the title
, description
and imgSrc
variables. For now, they're empty, but we will soon use the backend service we created earlier to provide actual values. While we're at it, we also update the HTML to use the new variables.
<script>
let title = "";
let description;
let imgSrc;
</script>
<style>
...
</style>
<div class="card">
<img src={imgSrc} alt={title} class="card-img-top">
<div class="card-body">
<h5 class="card-title">{title}</h5>
<p class="card-text">{description}</p>
</div>
</div>
With that skeleton in place, it's time to populate the three variables dynamically, based on the link's href
attribute.
We start with Svelte's onMount()
lifecycle method to call the backend service. What this means is that when the component gets mounted, we immediately call the backend service, which in turns fetches the href
's content and parses it. Once done, the preview card's values are populated so that by the time a user moves their mouse over a link, the preview card displays the content.
<script>
import { onMount } from "svelte";
export let href;
...
onMount(() => {
fetch("/link-preview.json?href=" + href)
.then(response => response.json())
.then(linkData => {
title = linkData.title;
description = linkData.description;
imgSrc = linkData.imgSrc;
});
});
</script>
You notice the new href
prop we defined. We will pass that in when we use this component.
Now, the HTML needs a few updates for better user experience. Not every website provides the meta tags our backend service looks for. For now, we simply hide the parts that are missing.
<div class="card">
{#if imgSrc}
<img src={imgSrc} alt={title} class="card-img-top">
{/if}
<div class="card-body">
<h5 class="card-title">{title}</h5>
{#if description}
<p class="card-text">{description}</p>
{/if}
</div>
</div>
To wrap up the development of this component, we need to indicate whether to show or hide this card. Remember, the preview card should only be displayed as long as the user's mouse is over a particular link. The only place we can determine that is in the already created <LinkPreview>
component where the <a>
tag is. So let's expose a show
prop from the <LinkPreviewCard>
component and wrap the entire HTML in an if
statement.
<script>
...
export href;
export show = false;
...
</script>
{#if show}
<div class="card">
{#if imgSrc}
<img src={imgSrc} alt={title} class="card-img-top">
{/if}
<div class="card-body">
<h5 class="card-title">{title}</h5>
{#if description}
<p class="card-text">{description}</p>
{/if}
</div>
</div>
{/if}
By default, the card is hidden (export show = false;
).
Use the new component
Back in the src/components/link-preview.svelte
component, we need to import the new preview card component first and add it inside the <a>
tag.
<script>
import LinkPreviewCard from "./link-preview-card.svelte";
</script>
<a href={$$props.href} {...$$props}>
<slot />
<LinkPreviewCard href={$$props.href} />
</a>
The href
prop we pass to the <LinkPreviewCard>
component is the one we send to the backend service to fetch the preview content.
To properly style the preview card, the <a>
tag needs a tiny bit of styling:
<style>
a {
position: relative;
}
</style>
Deal with the mouseover and mouseleave events
Lastly, the preview card needs to be displayed when the mouse is over a link and hidden otherwise. We track this in a new variable:
<script>
...
let showPreviewCard = false;
</script>
Now, let's add the mouseover
and mouseleave
handlers and pass the show
prop to the <LinkPreviewCard>
component.
<a {href} {...$$restProps}
on:mouseover={() => showPreviewCard = true}
on:mouseleave={() => showPreviewCard = false}
>
<slot />
<LinkPreviewCard {href} show={showPreviewCard} />
</a>
Use the component
The <LinkPreview>
component can now be used anywhere in the application, for example in src/routes/index.svelte
:
<script>
import LinkPreview from "../components/link-preview.svelte";
</script>
<p>
Move your mouse over the following link to see a preview of the link's content:
<LinkPreview href="https://dev.to">dev.to.</LinkPreview>
</p>
Conclusion
Thanks to Ilona for posting the initial blog post on this topic!
As we saw in my addition, with a simple backend service, it is possible to load a link's preview dynamically. The benefit of that is an always up-to-date preview even when a website we link to changes their title, description or main image.
If you like this kind of content, make sure to follow me on Twitter @mikenikles to get notified of new blog posts.
I'm writing a book, Cloud Native Web Development
If you want to learn how to develop modern, cloud-native web applications end-to-end, please sign up for updates at https://gumroad.com/mikenikles. I am currently writing a book with detailed instructions.