ScreenshotNeo

BlogHow-to

Building Link Preview Components with React, Vue, Svelte, and Astro

Build accessible link preview cards in React, Vue, Svelte, and Astro. Learn where to fetch metadata, how to handle fallbacks, and when to render at build time or on request.

By the ScreenshotNeo team29 September 202611 min read

Building Link Preview Components with React, Vue, Svelte, and Astro

A link preview card is ordinary page UI: a destination URL, a title, a short description, and optionally an image. To build one in React, Vue, Svelte, or Astro, keep two jobs separate: fetching metadata for the destination and rendering that metadata as a card. Fetch metadata on a trusted server endpoint, pass a small typed object into the framework component, and render useful fallback content when fields are missing or retrieval fails.

This separation lets you change the metadata source, add caching, or move retrieval from build time to request time without rewriting the card in every framework. It also prevents a common mix-up: a page’s <meta> tags describe that page; they do not fetch information about a URL and do not render a visible preview card.

1. Define the metadata contract and the card states

Start with a small shared shape. Treat remote values as optional: many pages have no description or preview image, and a page may not provide a usable title. The shape below is application guidance, not a schema required by a metadata service.

export type LinkMetadata = {
  requestedUrl: string;
  resolvedUrl?: string;
  title?: string;
  description?: string;
  imageUrl?: string;
  siteName?: string;
};

In addition to successful metadata, model loading and failure. The card should still give the user a meaningful destination when extraction fails.

type PreviewState =
  | { status: 'loading'; url: string }
  | { status: 'ready'; metadata: LinkMetadata }
  | { status: 'error'; url: string };
  • Loading: show a compact placeholder or the ordinary link.
  • Ready: use the extracted title and description when present; show an image only when a valid image URL exists.
  • Error or unsupported page: fall back to the submitted URL, a host label, and a normal clickable link.

Normalize and validate the URL at the boundary where user input enters your application. Escape text in the rendering framework as usual, and be cautious about loading remote image URLs. The reviewed framework documentation does not prescribe a complete security policy for remote URL fetching, so your server endpoint must apply the protections appropriate to your application and hosting environment.

2. Choose where metadata is fetched

There are three common places to obtain metadata. Choose based on freshness, page generation, and whether the interaction needs to happen in the browser.

Keep metadata retrieval separate from the framework component that renders the preview.
Keep metadata retrieval separate from the framework component that renders the preview.
Approach Good fit Tradeoff
Build time Known links on pages generated ahead of deployment Data is available during generation but does not refresh for every visitor
Request time Server-rendered pages that need current or request-specific metadata Retrieval adds work and latency to the request path
Browser-triggered A user pastes a URL and expects a preview without reloading Requires a server endpoint or suitable metadata service; browser code should not fetch arbitrary destination HTML directly

A hosted metadata API is one possible source. LinkMetadata documents metadata extraction, framework integrations, rendering pre-fetched metadata, caching, image/CORS, and safety-tag topics. Check its current documentation and terms before depending on specific fields or behavior. You can also use an application-owned server endpoint or existing backend. Keep the card component independent of whichever source you choose.

3. Build a reusable React card

React’s built-in <meta> component places document metadata in the head. Its <link> component is also intended for document links and metadata, generally in the head subject to documented exceptions. Neither is the visible preview UI. Render the card in the page body using ordinary elements:

type LinkPreviewProps = {
  metadata: LinkMetadata;
};

export function LinkPreview({ metadata }: LinkPreviewProps) {
  const href = metadata.resolvedUrl ?? metadata.requestedUrl;
  const title = metadata.title?.trim() || metadata.siteName || href;
  const description = metadata.description?.trim();

  return (
    <a className="link-preview" href={href}>
      {metadata.imageUrl ? (
        <img className="link-preview__image" src={metadata.imageUrl} alt="" loading="lazy" />
      ) : null}
      <span className="link-preview__content">
        <strong>{title}</strong>
        {description ? <span>{description}</span> : null}
        <small>{metadata.siteName || new URL(href).hostname}</small>
      </span>
    </a>
  );
}

The image uses empty alternative text because the whole card is already a link and the image is decorative in this pattern. If the image conveys information that is not present in the card text, revisit the accessible name and content structure. For untrusted or malformed URLs, do URL parsing and validation before rendering; do not assume new URL() can parse every arbitrary string without throwing.

Here is a minimal client retrieval pattern. The endpoint is your own application route; it should validate input, retrieve or look up metadata on the server, and return the agreed JSON shape.

async function getLinkMetadata(url: string): Promise<LinkMetadata> {
  const response = await fetch(`/api/link-metadata?url=${encodeURIComponent(url)}`);
  if (!response.ok) throw new Error(`Metadata request failed: ${response.status}`);
  return response.json();
}

// In a React component, call this from an event handler or an effect,
// then render loading, ready, and error states explicitly.

In production, cancel or ignore stale requests when the user changes the URL, set a request timeout in the server layer, and avoid issuing duplicate lookups for the same URL when a cache can serve them.

4. Render the same card in Vue and Svelte

Keep the same data contract and fallback behavior across frameworks. This makes the user experience consistent even when the implementation syntax differs.

Vue

A Vue card can accept metadata as a prop and render the visible anchor in its template:

<script setup lang="ts">
 type LinkMetadata = {
  requestedUrl: string;
  resolvedUrl?: string;
  title?: string;
  description?: string;
  imageUrl?: string;
  siteName?: string;
};

 const props = defineProps<{ metadata: LinkMetadata }>();
 const href = props.metadata.resolvedUrl || props.metadata.requestedUrl;
</script>

<template>
  <a class="link-preview" :href="href">
    <img v-if="metadata.imageUrl" class="link-preview__image"
      :src="metadata.imageUrl" alt="" loading="lazy" />
    <span class="link-preview__content">
      <strong>{{ metadata.title || metadata.siteName || href }}</strong>
      <span v-if="metadata.description">{{ metadata.description }}</span>
      <small>{{ metadata.siteName || metadata.requestedUrl }}</small>
    </span>
  </a>
</template>

For a paste-to-preview flow, call your server endpoint from the component or a composable and represent loading and error as explicit state. Keep URL validation and remote page retrieval on the server side rather than treating the browser as a general-purpose HTML fetcher.

Svelte

In Svelte, accept the metadata object as a prop. This example uses Svelte’s current runes-style prop declaration:

<script lang="ts">
  type LinkMetadata = {
    requestedUrl: string;
    resolvedUrl?: string;
    title?: string;
    description?: string;
    imageUrl?: string;
    siteName?: string;
  };

  let { metadata }: { metadata: LinkMetadata } = $props();
  let href = $derived(metadata.resolvedUrl || metadata.requestedUrl);
</script>

<a class="link-preview" href={href}>
  {#if metadata.imageUrl}
    <img class="link-preview__image" src={metadata.imageUrl} alt="" loading="lazy" />
  {/if}
  <span class="link-preview__content">
    <strong>{metadata.title || metadata.siteName || href}</strong>
    {#if metadata.description}
      <span>{metadata.description}</span>
    {/if}
    <small>{metadata.siteName || metadata.requestedUrl}</small>
  </span>
</a>

If your application uses a different Svelte version or component style, adapt the prop declaration to that version while preserving the data shape and visible states. The extraction endpoint remains framework-independent.

5. Use React, Vue, or Svelte components in Astro

Astro supports components written in React, Vue, and Svelte. Astro components render static HTML; framework components become interactive islands when hydrated, with the selected client:* directive controlling hydration. Astro notes that framework components add the JavaScript their framework needs. Only Astro components can contain components from multiple frameworks.

Astro’s rendering mode determines when fetched metadata is available and whether a framework island needs hydration.
Astro’s rendering mode determines when fetched metadata is available and whether a framework island needs hydration.

For a static preview, fetch metadata in an Astro component and render the card as Astro markup. Astro documents that component fetch() runs at build time for generated pages and at runtime when SSR is enabled. Pass fetched data into the template when the deployment mode and freshness needs fit.

---
const target = 'https://example.com/article';
const response = await fetch(
  `https://your-app.example/api/link-metadata?url=${encodeURIComponent(target)}`
);
const metadata = response.ok ? await response.json() : { requestedUrl: target };
const href = metadata.resolvedUrl || metadata.requestedUrl;
---

<a class="link-preview" href={href}>
  {metadata.imageUrl && <img class="link-preview__image" src={metadata.imageUrl} alt="" loading="lazy" />}
  <span class="link-preview__content">
    <strong>{metadata.title || metadata.siteName || href}</strong>
    {metadata.description && <span>{metadata.description}</span>}
    <small>{metadata.siteName || metadata.requestedUrl}</small>
  </span>
</a>

Replace the example endpoint with your application’s actual server route. For a generated page, this retrieval occurs during the build, so the resulting HTML contains the card content. With SSR enabled it can run per request, subject to your cache and hosting choices.

If a card needs client-side refresh or interaction, use an installed framework integration and the appropriate directive, for example client:visible when hydration should begin as the component becomes visible. Pass serializable metadata as props. Use a plain Astro component when static HTML is enough; this avoids adding a framework island solely to show text and an image.

6. Make the card accessible and consistent

A preview should remain understandable when the image is absent, slow, or blocked. Make the whole card one link when it represents one destination, provide visible focus styling, and keep the title, description, and site label in a readable order.

.link-preview {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr);
  gap: 1rem;
  padding: 1rem;
  border: 1px solid #cbd5e1;
  border-radius: .75rem;
  color: inherit;
  text-decoration: none;
}
.link-preview:hover { border-color: #475569; }
.link-preview:focus-visible { outline: 3px solid #2563eb; outline-offset: 3px; }
.link-preview__image { width: 7rem; height: 5rem; object-fit: cover; border-radius: .35rem; }
.link-preview__content { display: grid; align-content: start; gap: .35rem; min-width: 0; }
.link-preview__content span { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
@media (max-width: 32rem) {
  .link-preview { grid-template-columns: 1fr; }
  .link-preview__image { width: 100%; height: auto; aspect-ratio: 16 / 9; }
}

Do not make the image the only meaningful part of the card. Consider long titles, right-to-left text, tiny screens, keyboard navigation, and a missing or broken thumbnail. If the card contains additional controls, avoid nesting interactive elements inside the anchor; use a structure with separate controls and clear keyboard order.

7. Or skip the browser setup

If the preview task is really to capture how a destination page looks, a screenshot can complement metadata or stand in for a thumbnail. ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns PNG, JPEG, WebP, or PDF output. See the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed, and response headers report the page verdict and billing status. Its MCP server gives AI agents screenshot, page-info, and PDF tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Sign up for 1,000 free screenshots a month, with no card.

8. Troubleshooting and edge cases

Symptom Likely cause Fix
No title or description The destination provides sparse metadata or extraction could not read it Fall back to the host or URL; keep the destination link visible
Preview image does not load Missing image, remote host failure, or browser image restrictions Hide the image when absent, use a neutral visual fallback, and verify the returned image URL from the client context
Browser request is blocked The destination does not allow cross-origin retrieval or client code targets the remote page directly Use a server endpoint or metadata service; have it return the narrow JSON contract
Astro content is stale The route was generated at build time Rebuild when source data changes, or use SSR and an appropriate cache when runtime freshness is needed
Astro framework component is inert The component was rendered without a client hydration directive Add the appropriate client:* directive only when browser interaction is required
Malformed input throws URL parsing or endpoint validation failed Validate before retrieval and return a controlled error state rather than crashing the card
Old result replaces a newer URL A slower prior request completed last Abort obsolete requests or compare a request identifier before applying results
Duplicate requests increase load Several cards or visitors request the same destination Cache by normalized URL and define freshness based on how often destination metadata changes

9. Performance, reliability, and cost

Metadata extraction can involve network work, so avoid doing it repeatedly in the rendering path without a cache strategy. Normalize equivalent URLs carefully, set finite timeouts in your server, and use stale data or a plain link as a graceful fallback if retrieval fails. The right cache duration depends on the destination and product needs; the sources reviewed here do not establish a universally correct value.

For static Astro output, fetching during the build moves work out of visitor requests but makes updates dependent on a later build. Request-time SSR can provide fresher data while adding retrieval and hosting work to requests. Browser-triggered retrieval can make paste flows feel immediate, but still relies on a backend or hosted extraction service. These are architecture tradeoffs, not measured performance claims.

For cost, compare the ongoing work of operating and caching an owned endpoint with the current terms of any hosted metadata API. The reviewed material does not establish LinkMetadata prices, uptime, or privacy terms; verify those directly before choosing it. If your card needs an actual page image rather than descriptive metadata, ScreenshotNeo pricing is Free for 1,000 shots monthly, Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; annual billing gives two months free, and all features are on every plan. Treat screenshot capture and metadata extraction as different needs when estimating usage.

10. FAQ

Does adding Open Graph tags create a preview card?

No. Page-level metadata describes the document carrying those tags. A visible preview card is body UI populated with information retrieved about another URL.

Should every card be a hydrated framework component in Astro?

No. If it only displays server-provided fields, Astro can render static markup. Hydrate a React, Vue, or Svelte island when the card needs browser interaction.

Can metadata be fetched once and shared by all four frameworks?

Yes. Return one framework-neutral JSON contract from a backend or metadata provider, then render that object in each framework.

What if the destination has no usable preview image?

Omit the image and retain the title, host, and destination link. A robust card should not depend on a thumbnail to remain useful.

Sources