Hovering external links now shows a preview tooltip with favicon, title, and description. A Cloudflare Worker fetches and caches metadata, while the client uses a singleton tooltip and Floating UI for positioning.
The feature
Hover over any external link on this page to see it in action: Floating UI, Astro, or Cloudflare Workers.
The tooltip shows the page’s favicon, title, description, and domain. Metadata comes from the linked page’s Open Graph tags and falls back to the <title> element and meta description.
How it works
The architecture splits work between server and client.
Server-side: Cloudflare Worker
A Pages Function at /api/link-metadata handles fetching. When it receives a URL:
- Check Cloudflare KV for cached metadata
- If cache miss, fetch the page HTML (5s timeout)
- Parse
og:title,og:description, and favicon using linkedom - Cache the result in KV with a 7-day TTL
- Return JSON with 24-hour browser cache headers
The Worker identifies itself as LinkPreviewBot/1.0 in the User-Agent. Sites that block bots get a graceful fallback: domain name plus Google’s favicon service.
Client-side: Web Component
The <link-preview> custom element manages a singleton tooltip that gets reused across all links. Using a Web Component means event listeners survive Astro’s View Transitions. The connectedCallback() fires each time the element enters the DOM, re-registering link listeners after navigation.
Positioning uses Floating UI with:
flip()to reposition when hitting viewport edgesshift()to keep tooltip in viewoffset()for consistent spacingarrow()for the pointer triangle
A session cache (plain Map on window.__linkPreviewState) prevents redundant fetches when hovering the same link twice.
Design decisions
Instant show with progressive enhancement. The tooltip appears immediately with a domain fallback (favicon + hostname), then updates when the full metadata loads. No waiting around for network requests.
External links only. Internal link previews are stubbed out but disabled for now. External links have higher value since visitors can’t easily preview those destinations. Internal previews would need a build-time JSON index of all pages.
Domain fallback. When metadata extraction fails (blocked by robots, slow response, malformed HTML), the tooltip shows the domain name with a Google favicon. Something is better than nothing.
Square corners, tight spacing. The tooltip matches the site’s overall aesthetic: no border-radius, subtle 1px border, compact padding. It should feel like part of the page, not a popup from elsewhere.
Toggle system
Not every link needs a preview. The component provides opt-out controls:
<!-- Disable on a single link -->
<a href="..." data-no-preview>Skip this one</a>
<!-- Disable for a section -->
<div data-link-preview="disabled">
<a href="...">No preview</a>
<a href="...">Also no preview</a>
</div>
<!-- Enable for a section (usually not needed) -->
<div data-link-preview="enabled">
...
</div>Previews are enabled by default in these zones:
.prose(article content).colophon-sidebarand.colophon-resources.resource-links.field-notes-containerand.month-notes- Any element with
data-link-preview="enabled"
Links outside these zones don’t get preview treatment. Navigation, footers, and other chrome stay clean.
Trade-offs
What this approach costs:
- Extra network request per unique link. Mitigated by KV caching (server) and session caching (client).
- ~3KB of client JS. Floating UI is lightweight, but it’s still code.
- Metadata can be stale. The 7-day cache means changes on external sites take up to a week to reflect.
What it provides:
- Zero layout shift. The tooltip appears in a fixed position, never pushing content.
- Works with View Transitions. The Web Component pattern handles DOM swaps gracefully.
- Accessible. Keyboard focus triggers tooltips too, not just hover.
- Privacy-preserving. The Worker fetches metadata server-side. Visitors don’t make direct requests to external sites on hover.