+

Glimpse

Glimpse is a simple proof of concept screenshot tool Hayden Bleasel made to create link previews! The idea was to effectively create a serverless API endpoint deployed on a hobby Vercel plan that takes a URL and returns a screenshot of the page.

Can I use it on my site?

Sure if you fork the repo and deploy it to Vercel, you can use it on your site, but I wouldn't expect it to work reliably without a custom server or paid Vercel account or something.

Why is it in a seperate repo?

Long story short, Vercel serverless functions have a 50MB limit which does not always calculate the way you think it would. Other dependencies get mixed in the bundle size, even if they are not used in the API handler. It also needs Node 14 to run, which is a pretty specific requirement. So, I made it a separate repo to avoid all that drama and i just fetch() it from the API handler in my personal sites.

Can I see a demo?

You're looking at it! All the external links on this site use Glimpse to generate a preview.

Once I clone it, how do I make it work?

Like so!

Client-side

Really depends on your implementation method but I used a useAsync hook from @react-hookz/web to fetch the screenshot on-request client-side.

import Image from 'next/future/image';
import Link from 'next/link';
import type { FC } from 'react';
import { useAsync, useMountEffect } from '@react-hookz/web';
import type { ScreenshotResponse } from '../pages/api/screenshot';
import Placeholder from './placeholder';

const PreviewLink: FC<{ href: string }> = ({ children, href, ...props }) => {
  const [screenshot, { execute }] = useAsync(async () => {
    const response = await fetch('/api/screenshot', {
      method: 'POST',
      body: JSON.stringify({
        url: href,
      }),
    });

    const data = (await response.json()) as ScreenshotResponse;

    return data;
  });

  useMountEffect(async () => {
    await execute();
  });

  return (
    <span className="group relative inline-block">
      {!screenshot.error && !screenshot.result?.error && (
        <span className="pointer-events-none absolute left-0 bottom-full ml-[50%] flex h-[203px] w-[316px] -translate-x-2/4 -translate-y-0 rounded-lg border border-gray-50 bg-white p-2 opacity-0 shadow-lg transition-all group-hover:-translate-y-2 group-hover:opacity-100 dark:border-gray-700 dark:bg-gray-800">
          {screenshot.result?.image ? (
            <Image
              src={`data:image/png;base64,${screenshot.result.image}`}
              width={300}
              height={187}
              alt=""
            />
          ) : (
            <Placeholder className="h-full w-full rounded-md" />
          )}
        </span>
      )}
      <Link
        href={href}
        target="_blank"
        rel="noopener noreferrer"
        className="inline text-md font-normal text-gray-900 transition-colors hover:text-gray-600 dark:text-white dark:hover:text-gray-300"
        {...props}
      >
        {children}
      </Link>
    </span>
  );
};

export default PreviewLink;

/api/screenshot.ts

import type { NextApiHandler } from 'next';

export type ScreenshotResponse = {
  error?: string;
  image?: string;
};

/* Replace this with your own data */
const glimpse = 'https://glimpse.haydenbleasel.com/api/screenshot';

const handler: NextApiHandler<ScreenshotResponse> = async (req, res) => {
  const { url } = JSON.parse(req.body as string) as { url: string };

  if (!url) {
    res.status(400).json({ error: 'No URL specified' });
    return;
  }

  try {
    const response = await fetch(
      glimpse,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          url,
          width: 1200,
          height: 750,
        }),
      }
    );

    const { image, error } = (await response.json()) as ScreenshotResponse;

    if (error) {
      res.status(400).json({ error });
      return;
    }

    if (!image) {
      res.status(400).json({ error: 'No image found' });
      return;
    }

    res.status(200).json({ image });
  } catch (error) {
    const message = error instanceof Error ? error.message : (error as string);

    res.status(400).json({ error: message });
  }
};

export default handler;

What are the limitations?

Three primarily, which I will work on fixing over time:

  1. Vercel serverless functions are limited to a 10-second timeout. You would think this is fine, but I have an inkling that doing multiple concurrent requests causes the function to stay on which causes later requests to timeout. 🤷‍♀️
  2. Puppeteer itself has a timeout which I may need to rework to fit Vercel's timeout.
  3. The screenshot tends to be taken prematurely, based on the fact we're waiting for networkidle0. For more complex websites with intro animations and custom fonts, the screenshot may be taken in an interim state. networkidle2 would be a better choice but tends to timeout the function a lot more often.

If a screenshot fails to load, it returns a nice error and you can handle this on the client-side like I've done above, by removing the entire preview.

This seems super complex. Why not just use an iframe?

Many websites on the internet use an HTTP Response Header called X-Frame-Options to indicate whether or not a browser should be allowed to render a page in an iframe (or similar elements). If this header is set to DENY, it can help avoid click-jacking attacks by ensuring that their content is not embedded into other sites. Unfortunately, it also means using iframes in link previews would be a no-go.

Enough with the preamble. Gimme the code!

Fine! You can check out the source code on the GitHub repo. Feel free to fork it and do whatever, but a would be appreciated!