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.
go get github.com/josephgoksu/metagrab@latest
brew install --cask josephgoksu/tap/metagrab
go install github.com/josephgoksu/metagrab/cmd/metagrab@latest
docker run --rm -p 8080:8080 ghcr.io/josephgoksu/metagrab
// then, in Go link, err := metagrab.Fetch(ctx, "https://go.dev", metagrab.PreviewFields) fmt.Println(link.Title, link.Image, link.Favicon)
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
2 · metagrab.Link (JSON)
3 · Your preview card
How to unfurl a link in under a millisecond without a headless browser.
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. Validated and checked against your URL policy.
- Safety check, every connection. No private or cloud-metadata IPs, also after redirects and DNS rebinding.
- Read <head>, streaming. Charset decoded, stops at </head>.
- Extract. Open Graph, Twitter, meta, favicon, canonical. Optional JSON-LD and oEmbed.
- metagrab.Link. Absolute URLs, or a FetchError with a code.
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}'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.
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
| Feature | metagrab | otiai10/opengraph | dyatlov/go-opengraph | badoux/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.
Running it behind a product
metagrab fetches pages that strangers pick. These six habits keep that boring.
WithDenyPrivateNetworks() whenever URLs come from users. metagrab serve does it by default.Link by URL, for example for 24 hours.METAGRAB_API_KEY and rate-limit at your edge. Bound bulk work with WithConcurrency.WithRetries(1-2) covers 429 and 5xx, and respects Retry-After up to a cap.OEmbed.HTML without a sandboxed iframe.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