metagrab
Open source · Go · v0.1.0

Paste a URL.
Get a link preview.

metagrab reads a web page and returns what a preview card needs: title, description, image, favicon, author, date, Open Graph, Twitter Cards, JSON-LD and oEmbed. Use it as a Go library, a CLI, or a ready-made HTTP API.

3.9x to 10.8x faster on big pages SSRF-safe only golang.org/x deps Apache-2.0
go get github.com/josephgoksu/metagrab@latest
// then, in Go
link, err := metagrab.Fetch(ctx, "https://go.dev", metagrab.PreviewFields)
fmt.Println(link.Title, link.Image, link.Favicon)
See it

One URL in, one tidy object out

You choose how much to extract with a field mask. Pick one and watch what comes back, and what the card can show.

1 · Input

https://blog.example.com/posts/fast-link-previews

2 · metagrab.Link (JSON)

3 · Your preview card

Example Blog
Making link previews fast

How to unfurl a link in under a millisecond without a headless browser.

Ada Lovelace · Article · 2026-11-07
How it works

It reads only what it needs

Most preview data lives in <head>. metagrab streams the page, extracts as it goes, and stops at </head> unless you asked for JSON-LD. Every connection passes a safety check first.

URL validate, policy EVERY CONNECTION Safety check no private IPs, redirects, rebinding STREAMING Read <head> decode charset, stop at </head> Extract OG, Twitter, meta, favicon, canonical optional: JSON-LD in body, oEmbed (23 providers) metagrab.Link absolute URLs, or a coded error
  1. URL. Validated and checked against your URL policy.
  2. Safety check, every connection. No private or cloud-metadata IPs, also after redirects and DNS rebinding.
  3. Read <head>, streaming. Charset decoded, stops at </head>.
  4. Extract. Open Graph, Twitter, meta, favicon, canonical. Optional JSON-LD and oEmbed.
  5. metagrab.Link. Absolute URLs, or a FetchError with a code.
On a 1.8 MB shop product page, metagrab reads 445 KB.
■ read by metagrab: 445 KB (24%)whole page: 1.8 MB, which DOM-based libraries download and parse
Use it

Four ways in

Same extractor everywhere. Start with the library; reach for the CLI or the HTTP API when your stack isn't Go.

Go library most common

One call with a context and a field mask. Errors are *FetchError with a machine-readable code.

client := metagrab.NewClient(
    metagrab.WithTimeout(5 * time.Second),
    metagrab.WithRetries(2),
    metagrab.WithDenyPrivateNetworks(), // URLs from users
)
link, err := client.Fetch(ctx, url, metagrab.PreviewFields)

Parse HTML you have no network

From a crawler, a cache or a test fixture. Relative URLs resolve against the page URL.

link, err := metagrab.Parse(resp.Body,
    "https://example.com/post",
    metagrab.RichFields) // + JSON-LD
fmt.Println(link.ContentType) // "Article"

CLI

JSON to stdout. Several URLs run concurrently. Exit code 2 when any URL fails, for scripts.

metagrab https://go.dev
metagrab -fields=rich https://a.com https://b.com
metagrab -compact https://go.dev | jq .title

HTTP API

For any language. Private networks blocked by default, API key, JSON logs, graceful shutdown.

docker run -p 8080:8080 -e METAGRAB_API_KEY=secret \
  ghcr.io/josephgoksu/metagrab

curl -H "X-API-Key: secret" localhost:8080/fetch \
  -d '{"url":"https://go.dev","fields":111}'
Benchmarks

How it compares

Time to fetch one link preview from a local server, median of 5 runs. Lower is better. Same pages, same machine, every library.

metagrab vs the fastest alternative
3.9x
faster on the shop product page
83 KB
allocated to parse a typical page (others: up to 533 KB)
24%
of a 1.8 MB page read before it has the answer

The libraries don't extract the same fields; the table below shows what each one returns. Reproduce on your machine: cd bench && go run . · method and raw numbers

Featuremetagrabotiai10/opengraphdyatlov/go-opengraphbadoux/goscraper

yes · partial · no · – not applicable (parser only). The first nine rows are checked by running each library against test pages; the rest come from reading their APIs. Versions and details in the benchmark report.

In production

Running it behind a product

metagrab fetches pages that strangers pick. These six habits keep that boring.

Block private networksWithDenyPrivateNetworks() whenever URLs come from users. metagrab serve does it by default.
Short timeouts3 to 10 seconds, plus a request context so cancelled requests stop fetching.
Cache resultsPreviews rarely change. Cache each Link by URL, for example for 24 hours.
Key and rate limitSet METAGRAB_API_KEY and rate-limit at your edge. Bound bulk work with WithConcurrency.
Retry politelyWithRetries(1-2) covers 429 and 5xx, and respects Retry-After up to a cap.
Treat output as untrustedEscape every string. Never inject OEmbed.HTML without a sandboxed iframe.
Improve it

Make it better with me

metagrab is small on purpose: one package, no heavy dependencies, every change measured. Pull requests are welcome.

Get running in a minute

git clone https://github.com/josephgoksu/metagrab
cd metagrab
go test -short -race ./...   # unit tests, no network
cd bench && go run .         # benchmarks + charts

Read CONTRIBUTING.md and ARCHITECTURE.md first. Keep the API additive, add a test for each fix, and run the benchmarks when you touch the parser. Report security issues privately, as SECURITY.md describes.

Good places to start

  • Add an oEmbed provider. One entry with host names and an endpoint, plus a test. oembed.go · oembedRegistry
  • Add a real-world page to the benchmark corpus. More page shapes make the numbers more honest. bench/corpus.go
  • Deployment examples. Fly.io, Cloud Run or Kubernetes, next to the Lambda and Cloudflare ones. examples/
  • A caching wrapper. An example of putting a TTL cache in front of Client.Fetch. examples/
  • Report a page that previews badly. Send the URL and what you expected; it becomes a test. open an issue