Metadata Fetching¶
When a bookmark is saved, tldrq fetches its title, description, thumbnail and, for repositories, stats.
Everything lives in src/lib/utils/metadata.ts, with image URL handling in src/lib/utils/ui.ts.
Where it runs¶
fetchOGMetadata(url) runs on the server, once per bookmark:
- from
POST /api/bookmarks, before the insert; - from
GET /api/metadata?url=…, which nothing in the UI calls but which is handy for trying the fetcher on a URL (see the API Reference).
Metadata is never fetched again for a saved bookmark.
The result¶
export interface OGMetadata {
title: string | null;
description: string | null;
image_url: string | null;
site_name: string | null;
repo_stars?: number | null;
repo_forks?: number | null;
repo_language?: string | null;
}
The fields are stored on the bookmark row as they are (see Database).
Dispatch¶
fetchOGMetadata tries the platform detectors in order and uses the first that matches:
flowchart TD
U[URL] --> Y{YouTube video ID?}
Y -- yes --> YH[YouTube oEmbed]
Y -- no --> X{X or Twitter status?}
X -- yes --> XH[X oEmbed and page image]
X -- no --> V{Vimeo?}
V -- yes --> VH[Vimeo oEmbed]
V -- no --> TT{TikTok?}
TT -- yes --> TH[TikTok oEmbed]
TT -- no --> SP{Spotify?}
SP -- yes --> SPH[Spotify oEmbed]
SP -- no --> R{Reddit?}
R -- yes --> RH[Reddit oEmbed and scraping]
R -- no --> SC{SoundCloud?}
SC -- yes --> SCH[SoundCloud oEmbed]
SC -- no --> F{Flickr?}
F -- yes --> FH[Flickr oEmbed]
F -- no --> SS{SlideShare?}
SS -- yes --> SSH[SlideShare oEmbed]
SS -- no --> GH{github.com?}
GH -- yes --> GHH[GitHub REST API]
GH -- no --> GL{gitlab.com?}
GL -- yes --> GLH[GitLab REST API]
GL -- no --> I{Instagram?}
I -- yes --> IH[Scraping, site name Instagram]
I -- no --> G[Generic scraping]
Platform handlers¶
Most oEmbed handlers use fetchOEmbedMetadata(oembedUrl, siteName), the standard mapping: title from the
oEmbed title, description as By plus author_name, image_url from thumbnail_url, and a fixed site_name.
| Platform | Detected by | Source | Mapping |
|---|---|---|---|
| YouTube | Host youtu.be, or youtube.com, www.youtube.com or m.youtube.com with /watch?v= or /shorts/, /embed/, /v/ |
https://www.youtube.com/oembed?url=…&format=json |
Standard, but the image is always https://img.youtube.com/vi/ID/hqdefault.jpg; site YouTube |
| X | Host x.com or twitter.com (with or without www.), path /name/status/digits |
https://publish.twitter.com/oembed?url=…&omit_script=true, plus the page's og:image, fetched in parallel |
Title @ plus author_name; description is the text of the embed's first blockquote; site X |
| Vimeo | vimeo.com or www.vimeo.com, path starting with digits |
https://vimeo.com/api/oembed.json?url=… |
Standard |
| TikTok | tiktok.com or www.tiktok.com, path containing /video/ |
https://www.tiktok.com/oembed?url=… |
Standard |
| Spotify | open.spotify.com, path starting /track/, /album/, /playlist/, /episode/ or /show/ |
https://open.spotify.com/oembed?url=… |
Standard, with description forced to null |
reddit.com, www.reddit.com or old.reddit.com, path /r/name/comments/ |
https://www.reddit.com/oembed?url=…&format=json and generic scraping, in parallel |
Title and image from scraping; description By u/ plus author_name, else the scraped one; site Reddit |
|
| SoundCloud | soundcloud.com or www., at least two path segments |
https://soundcloud.com/oembed?url=…&format=json |
Standard |
| Flickr | flickr.com or www., path starting /photos/ |
https://www.flickr.com/services/oembed/?url=…&format=json |
Image from url (the photo), else thumbnail_url; site Flickr |
| SlideShare | slideshare.net or www., at least two path segments |
https://www.slideshare.net/api/oembed/2?url=…&format=json |
Standard |
| GitHub | Host github.com or www.github.com |
https://api.github.com/repos/owner/repo, unauthenticated, Accept: application/vnd.github.v3+json |
full_name, description, owner.avatar_url, stargazers_count, forks_count, language; site GitHub |
| GitLab | Host gitlab.com or www.gitlab.com |
https://gitlab.com/api/v4/projects/ plus the URL-encoded first two path segments |
path_with_namespace, description, avatar_url, star_count, forks_count, and the first of topics as repo_language; site GitLab |
instagram.com or www., path starting /p/, /reel/ or /tv/ |
Generic scraping (there's no public oEmbed) | As scraped, with site_name defaulting to Instagram |
GitHub and GitLab match on the host alone. Their handlers then need at least two path segments (owner/repo); with
fewer, a non-OK response, or an error, they fall back to generic scraping. GitLab projects in subgroups
(group/subgroup/project) are looked up by their first two segments, so they fall back too.
Generic scraping¶
fetchGenericOGMetadata(url) fetches the page with User-Agent: tldrqBot/1.0. A non-OK response gives all nulls.
Otherwise Cheerio parses the HTML:
| Field | Taken from |
|---|---|
title |
og:title, else <title> |
description |
og:description, else meta[name="description"] |
image_url |
og:image, through resolveImageUrl |
site_name |
og:site_name |
Values are trimmed, and empty strings become null.
Timeouts and failures¶
fetchWithTimeout aborts a request whose response hasn't started arriving within 5000 ms (5 seconds). The timer is
cleared as soon as fetch resolves with the headers, so reading the body has no limit. The limit is per request: a
GitHub lookup that fails and falls back to scraping can wait about 10 seconds, while X and Reddit make their two
requests in parallel.
tldrqBot/1.0 is sent for generic scraping, X's page fetch and the GitHub and GitLab APIs; oEmbed requests use
the default user agent.
Every handler returns nulls when something fails, and fetchOGMetadata wraps the whole dispatch in a try/catch
that returns all nulls, so saving a bookmark never fails because of metadata. The GitHub API is called without
authentication, so GitHub rate-limits it per IP address; a rate-limited request isn't OK, and the bookmark gets
scraped metadata without stats.
Images¶
resolveImageUrl(src, pageUrl) in src/lib/utils/ui.ts trims the value, resolves a relative path against the page
URL, and returns null unless the result is an http: or https: URL. The scrapers pass response.url, the final
URL after redirects, as the page URL. Both bookmark cards call it again when rendering, which fixes rows saved
before it existed.
Adding a platform¶
- Write an
isXUrl(url)detector: parse withnew URL(url)inside atry/catch, and check the host and path. - Write
fetchXMetadata(url). If the site has oEmbed,fetchOEmbedMetadata(oembedUrl, "Name")may be all it needs. - Add it to the chain in
fetchOGMetadata, before any check that would also match its URLs. The GitHub and GitLab checks match by host only. - If its links belong in Videos, Tweets or Repos, export the detector and add it to the auto-categorization in
src/app/api/bookmarks/route.ts. Otherwise they go to Articles. - Update the table on this page and the one in the User Guide's Saving Bookmarks.
Try it with GET /api/metadata?url=… before saving real bookmarks.