Skip to content

Build your own
link preview component

Design a link preview card, then copy a zero-dependency component for React, TypeScript, Vue, Angular, Svelte, Astro, Vanilla JS. It fetches metadata from the Microlink API itself — pass an apiKey to go Pro.

Width
px
Image height
auto
A single self-contained file — no SDK, no build step, no npm install. Pick your framework, copy it in, and render it with a url.
  • React
  • TypeScript
  • Vue
  • Angular
  • Svelte
  • Astro
  • Vanilla
/*
 * Link preview — generated by microlink.io/integrations/builder
 * Zero dependencies, fully typed. Drop this file in and render it with a url.
 *
 *   import LinkPreview from './LinkPreview'
 *
 *   <LinkPreview url='https://github.com' />
 *   <LinkPreview url='https://github.com' apiKey='YOUR_KEY' />   // Pro
 */
import { useEffect, useState } from 'react'

type LinkPreviewProps = {
  url: string
  apiKey?: string
}

const STYLE = {
  "palette": {
    "headline": "#000000",
    "description": "#000000",
    "meta": "#999999",
    "background": "#ffffff",
    "border": "#dedede"
  },
  "fontFamily": "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif",
  "fontWeight": 400,
  "lineHeight": 1.4,
  "headlineSize": 16,
  "descriptionSize": 13,
  "metaSize": 11,
  "border": "1px solid #dedede",
  "radius": "12px",
  "shadow": "0 1px 4px rgba(0,0,0,0.1)",
  "elements": {
    "description": true,
    "siteIcon": true,
    "siteName": true,
    "authorTopic": false,
    "date": false
  },
  "metaBefore": true,
  "width": 460,
  "mediaHeight": null,
  "imagePosition": "top",
  "variant": "large"
}

function escA (v: any) {
  return String(v == null ? '' : v).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
function escT (v: any) {
  return String(v == null ? '' : v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
function trunc (t: any, max: any) {
  var str = String(t == null ? '' : t)
  if (str.length <= max) return str
  var slice = str.slice(0, max - 1)
  var trimmed = slice.replace(/\s+\S*$/, '')
  return (trimmed || slice) + ''
}
function fmtDate (v: any) {
  if (!v) return ''
  try {
    var d = new Date(v)
    if (isNaN(d.getTime())) return ''
    return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
  } catch (e) { return '' }
}
function fallbackBg (data: any) {
  return (data.image && data.image.palette && data.image.palette[0]) || 'rgba(0,0,0,0.05)'
}
function logoFallbackAttr (logoUrl: any) {
  return logoUrl ? 'onerror="this.onerror=null;this.src=\'' + logoUrl.replace(/'/g, '&#39;') + '\';this.style.objectFit=\'contain\';this.style.padding=\'15%\'" ' : ''
}
function buildMeta (data: any, s: any) {
  var pieces = []
  if (s.elements.siteIcon && data.logo && data.logo.url) {
    pieces.push('<img src="' + escA(data.logo.url) + '" alt="" style="width:' + (s.metaSize + 4) + 'px;height:' + (s.metaSize + 4) + 'px;border-radius:4px;flex-shrink:0" />')
  }
  if (s.elements.siteName && data.publisher) {
    pieces.push('<span style="font-size:' + s.metaSize + 'px;font-weight:' + s.fontWeight + ';color:' + s.palette.meta + ';letter-spacing:0.5px;text-transform:uppercase">' + escT(data.publisher) + '</span>')
  }
  if (s.elements.authorTopic && data.author) {
    pieces.push('<span style="font-size:' + s.metaSize + 'px;color:' + s.palette.meta + '">' + escT(data.author) + '</span>')
  }
  if (s.elements.date && data.date) {
    var dateStr = fmtDate(data.date)
    if (dateStr) {
      pieces.push('<span style="font-size:' + s.metaSize + 'px;color:' + s.palette.meta + '">' + escT(dateStr) + '</span>')
    }
  }
  if (!pieces.length) return ''
  return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-height:' + (s.metaSize + 4) + 'px">' + pieces.join('') + '</div>'
}
function buildLarge (data: any, s: any) {
  var href = escA((data && data.url) || '')
  var imageUrl = (data.image && data.image.url) ? escA(data.image.url) : ''
  var logoUrl = (data.logo && data.logo.url) ? escA(data.logo.url) : ''
  var title = escT(trunc((data && data.title) || '', 90))
  var description = s.elements.description ? escT(trunc((data && data.description) || '', 220)) : ''
  var bg = escA(fallbackBg(data))
  var metaHtml = buildMeta(data, s)
  var maxWidth = s.width || 460
  var mediaBox = s.mediaHeight ? 'width:100%;height:' + s.mediaHeight + 'px;background:' + bg + ';overflow:hidden' : 'width:100%;aspect-ratio:16 / 9;background:' + bg + ';overflow:hidden'
  var mediaInner = imageUrl ? '<img src="' + imageUrl + '" alt="" ' + logoFallbackAttr(logoUrl) + 'style="width:100%;height:100%;object-fit:cover;display:block" />' : ''
  var titleHtml = '<div style="font-size:' + s.headlineSize + 'px;font-weight:' + s.fontWeight + ';color:' + s.palette.headline + ';line-height:' + s.lineHeight + ';margin:0">' + title + '</div>'
  var descriptionHtml = description ? '<div style="font-size:' + s.descriptionSize + 'px;color:' + s.palette.description + ';line-height:' + s.lineHeight + ';display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;margin:0">' + description + '</div>' : ''
  var body = s.metaBefore ? (metaHtml + titleHtml + descriptionHtml) : (titleHtml + descriptionHtml + metaHtml)
  return '<a href="' + href + '" target="_blank" rel="noopener noreferrer" style="display:block;text-decoration:none;color:inherit;width:100%;max-width:' + maxWidth + 'px;background:' + s.palette.background + ';border-radius:' + s.radius + ';overflow:hidden;border:' + s.border + ';box-shadow:' + s.shadow + ';font-family:' + s.fontFamily + '">\n  <div style="' + mediaBox + '">' + mediaInner + '</div>\n  <div style="padding:14px 16px;display:flex;flex-direction:column;gap:6px">' + body + '</div>\n</a>'
}
function buildWide (data: any, s: any) {
  var href = escA((data && data.url) || '')
  var imageUrl = (data.image && data.image.url) ? escA(data.image.url) : ''
  var logoUrl = (data.logo && data.logo.url) ? escA(data.logo.url) : ''
  var title = escT((data && data.title) || '')
  var description = s.elements.description ? escT((data && data.description) || '') : ''
  var bg = escA(fallbackBg(data))
  var metaHtml = buildMeta(data, s)
  var maxWidth = s.width || 460
  var minHeight = s.mediaHeight || 140
  var flexDirection = s.imagePosition === 'right' ? 'flex-direction:row-reverse;' : ''
  var mediaInner = imageUrl ? '<img src="' + imageUrl + '" alt="" ' + logoFallbackAttr(logoUrl) + 'style="width:100%;height:100%;object-fit:cover;display:block" />' : ''
  var titleHtml = '<div style="font-size:' + s.headlineSize + 'px;font-weight:' + s.fontWeight + ';color:' + s.palette.headline + ';line-height:' + s.lineHeight + ';display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">' + title + '</div>'
  var descriptionHtml = description ? '<div style="font-size:' + s.descriptionSize + 'px;color:' + s.palette.description + ';line-height:' + s.lineHeight + ';display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">' + description + '</div>' : ''
  var body = s.metaBefore ? (metaHtml + titleHtml + descriptionHtml) : (titleHtml + descriptionHtml + metaHtml)
  return '<a href="' + href + '" target="_blank" rel="noopener noreferrer" style="display:flex;' + flexDirection + 'text-decoration:none;color:inherit;width:100%;max-width:' + maxWidth + 'px;min-height:' + minHeight + 'px;background:' + s.palette.background + ';border-radius:' + s.radius + ';overflow:hidden;border:' + s.border + ';box-shadow:' + s.shadow + ';font-family:' + s.fontFamily + '">\n  <div style="width:140px;flex-shrink:0;align-self:stretch;background:' + bg + ';overflow:hidden">' + mediaInner + '</div>\n  <div style="padding:14px;display:flex;flex-direction:column;gap:4px;flex:1;min-width:0;justify-content:center">' + body + '</div>\n</a>'
}
function buildSmall (data: any, s: any) {
  var href = escA((data && data.url) || '')
  var logoUrl = (data.logo && data.logo.url) ? escA(data.logo.url) : ''
  var title = escT(trunc((data && data.title) || '', 60))
  var description = s.elements.description ? escT(trunc((data && data.description) || '', 140)) : ''
  var bg = escA(fallbackBg(data))
  var maxWidth = s.width || 380
  var iconNode = !s.elements.siteIcon ? '' : (logoUrl ? '<img src="' + logoUrl + '" alt="" style="width:36px;height:36px;border-radius:8px;flex-shrink:0" />' : '<div style="width:36px;height:36px;border-radius:8px;flex-shrink:0;background:' + bg + '"></div>')
  var publisherText = (s.elements.siteName && data.publisher) ? '<span style="font-size:' + (s.metaSize + 1) + 'px;font-weight:' + s.fontWeight + ';color:' + s.palette.meta + '">' + escT(data.publisher) + '</span>' : ''
  var authorText = (s.elements.authorTopic && data.author) ? '<span aria-hidden="true" style="font-size:' + s.metaSize + 'px;color:' + s.palette.meta + '">· </span><span style="font-size:' + s.metaSize + 'px;color:' + s.palette.meta + '">' + escT(data.author) + '</span>' : ''
  var dateText = (s.elements.date && data.date) ? '<span style="font-size:' + s.metaSize + 'px;color:' + s.palette.meta + '">' + escT(fmtDate(data.date)) + '</span>' : ''
  var metaRow = (publisherText || authorText) ? '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:2px;gap:8px">\n      <span style="display:flex;align-items:center;gap:4px;min-width:0;overflow:hidden">' + publisherText + authorText + '</span>\n      ' + dateText + '\n    </div>' : ''
  var titleHtml = '<div style="font-size:' + (s.headlineSize - 3) + 'px;font-weight:' + s.fontWeight + ';color:' + s.palette.headline + ';line-height:' + s.lineHeight + ';margin-bottom:2px">' + title + '</div>'
  var descriptionHtml = description ? '<div style="font-size:' + (s.descriptionSize - 1) + 'px;color:' + s.palette.description + ';line-height:' + s.lineHeight + ';display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">' + description + '</div>' : ''
  var body = s.metaBefore ? (metaRow + titleHtml + descriptionHtml) : (titleHtml + descriptionHtml + metaRow)
  return '<a href="' + href + '" target="_blank" rel="noopener noreferrer" style="display:flex;text-decoration:none;color:inherit;gap:10px;align-items:flex-start;width:100%;max-width:' + maxWidth + 'px;padding:12px 14px;border-radius:' + s.radius + ';background:' + s.palette.background + ';border:' + s.border + ';box-shadow:' + s.shadow + ';font-family:' + s.fontFamily + '">\n  ' + iconNode + '\n  <div style="flex:1;min-width:0">' + body + '</div>\n</a>'
}
function renderCard (data: any, s: any) {
  if (!data) return ''
  if (s.variant === 'small') return buildSmall(data, s)
  if (s.variant === 'wide') return buildWide(data, s)
  return buildLarge(data, s)
}

function withProtocol (url: any) {
  var str = String(url == null ? '' : url).trim()
  if (!str || /^https?:\/\//i.test(str)) return str
  var normalized = 'https://' + str.replace(/^\/+/, '')
  console.warn('[microlink] "' + str + '" has no protocol — assuming "' + normalized + '". Pass a full URL with an explicit https:// (or http://) to avoid this.')
  return normalized
}
function microlinkFetch (url: any, apiKey: any) {
  var endpoint = apiKey ? 'https://pro.microlink.io/' : 'https://api.microlink.io/'
  var qs = new URLSearchParams({ url: withProtocol(url), palette: 'true' }).toString()
  var headers = apiKey ? { 'x-api-key': apiKey } : {}
  return fetch(endpoint + '?' + qs, { headers: headers })
    .then(function (r: any) { return r.ok ? r.json() : null })
    .then(function (res: any) { return res && res.data })
    .catch(function () { return null })
}

export default function LinkPreview ({ url, apiKey }: LinkPreviewProps) {
  const [html, setHtml] = useState('')

  useEffect(() => {
    let active = true
    microlinkFetch(url, apiKey).then(data => {
      if (active && data) setHtml(renderCard(data, STYLE))
    })
    return () => { active = false }
  }, [url, apiKey])

  return <div dangerouslySetInnerHTML={{ __html: html }} />
}
Every framework except Vanilla JS
The component is self-contained: pass a url and it fetches the metadata from Microlink and renders the card. Add an apiKey to go Pro. These are the only props it accepts:
url
required
string
The link to preview. The component fetches its metadata from the Microlink API and renders the card you designed.
apiKey
optional
string
Your Microlink Pro key. Switches requests to pro.microlink.io for higher rate limits and Pro features. Omit it to use the free tier.
How it works

Three steps to ship

Design a card, copy the component for your framework, and drop it into your app. Each one calls the link preview API directly — no SDK and no build step in between.
01

Design it

Pick a size, place the image, set colors, fonts, border, and shadow. The preview updates as you go.
02

Copy it

Grab a zero-dependency component for React, TypeScript, Vue, Angular, Svelte, Astro, or Vanilla JS — no SDK, no build step.
03

Ship it

Drop it in, pass a url. It fetches metadata from the Microlink API itself. Add an apiKey for Pro.
Use your AI coding agent

Generate it with a prompt

Prefer to stay in your editor? Paste this prompt into Cursor, Claude Code, Copilot, or any LLM. It interviews you, inspects your repo’s styling, and builds a Microlink preview component tailored to your stack.
Prompt
You are an expert frontend engineer. Add a link preview component to THIS project — a component that takes a url and renders a rich preview card (image, title, description, favicon, site name) from the Microlink API.

Microlink API basics:
- Free: GET https://api.microlink.io/?url=THE_URL&palette=true
- Pro: GET https://pro.microlink.io/?url=THE_URL&palette=true with an "x-api-key" header
- The JSON response exposes data.title, data.description, data.url, data.publisher, data.author, data.date, data.image.url, data.logo.url and data.image.palette.

Do NOT write any code yet. First interview me and inspect this repository so the component matches my stack and design system exactly.

1. Detect my stack. Inspect package.json, config files, file extensions and existing components to infer the framework (React, Vue, Svelte, Angular, Astro, Solid or vanilla JS), the language (JavaScript or TypeScript) and the styling approach (Tailwind, CSS Modules, styled-components, Emotion, vanilla CSS, CSS variables, …). Tell me what you found and ask me to confirm. If you cannot tell, ask me — never assume.

2. Learn my design system. Read my theme/tokens, global styles, Tailwind or theme config and a few existing components to extract my color palette, font families and sizes, border radius, spacing scale, shadows and how I handle light/dark mode. Reuse my tokens and utility classes instead of hardcoding values. If the design language is ambiguous, show me 2-3 concrete options and ask which I prefer.

3. Confirm the details, one question at a time, waiting for my answer before moving on:
   - Which framework and language should the component be written in? (default: match the repo)
   - Which styling method should it use? (default: match the repo)
   - Layout: large image on top, horizontal (image beside the text) or compact?
   - Which fields to show: image, title, description, favicon, site name, author, date?
   - Light theme, dark theme or both?
   - Free Microlink API or Pro with an API key? If Pro, where should the key live — an env var, a prop or a server-side proxy?
   - Any loading and error states you want?

4. Implement it only after I have confirmed framework, language and styling. Generate a single, self-contained, dependency-free component that:
   - Accepts a required "url" prop and an optional "apiKey" prop.
   - Fetches metadata from Microlink, prepending "https://" to bare domains and warning once if it had to.
   - Renders the card using MY project's styling conventions and tokens — not generic styles.
   - Handles loading and failed requests gracefully (render a fallback or nothing; never crash).
   - Is accessible: the whole card is one link, images have alt text and the semantics are correct.
   - Comes with a short usage example.

5. Keep iterating. After generating it, ask me what to refine and adjust the layout, styling and props until the preview looks exactly how I want it inside my app.

Rule: whenever anything about my framework, language or design system is unclear, ask me a question instead of guessing.

FAQ

What does the generated component depend on?

Nothing. Each component is a single self-contained file with no npm dependencies. It calls the Microlink REST API directly with fetch and renders the card with inline styles, so there is no SDK to install, no CSS to import, and no build step to configure.

Which frameworks can I export to?

React, TypeScript, Vue, Angular, Svelte, Astro, and Vanilla JS. Pick a tab under the preview to see that framework’s component, then copy it or download the file. The card markup and your design are identical across every target — only the framework wrapper changes.

Can I edit the component after I copy it?

Yes — it’s plain source that you own. Your design is baked into a STYLE object at the top of the file, so you can tweak a single color or size by hand, or rework the markup entirely. Come back to the builder whenever you want to regenerate from scratch.

Does it work with server-side rendering?

The Astro component fetches on the server at render time, so the card ships as static HTML — crawlable, with zero client-side JavaScript. The React, Vue, Svelte, and Angular components run on the client and render the card as soon as the metadata resolves.

Can I design separate light and dark themes?

Under Colors you can switch between Light and Dark and tune every color independently — headline, description, metadata, background, and border. The generated component bakes the palette you designed, so it renders with exactly those colors wherever you drop it in.

What happens if a URL is invalid or missing its protocol?

If you pass a bare domain like github.com, the component prepends https:// for you and logs a one-time console warning recommending you pass an explicit protocol. If the metadata cannot be fetched at all, the component renders nothing rather than breaking your layout.

Is my design saved when I come back?

Your settings are stored locally in your browser, so the builder reopens exactly where you left off — no account needed. Hit Reset anytime to restore every control to its defaults.

How do free and Pro requests work?

Without an apiKey the component queries api.microlink.io (free tier). Pass an apiKey prop and it switches to pro.microlink.io with your key, unlocking higher rate limits and Pro features. Both endpoints are the same Microlink link preview API.

How does the Vanilla JS version work?

It exposes a global microlink(selector, options) function that replaces every matched element with the card you designed — e.g. microlink('a') turns every link into a preview, and microlink('.link-previews', { size: 'large' }) targets a class with options.

How is this different from the SDK?

The SDK is a prebuilt, batteries-included component you install from npm. This builder generates source you own and can edit — handy when you want a specific look with zero dependencies. Either way, the component renders data from the link preview API.