Extract metadata from single-page apps and client-rendered pages
To extract metadata from single-page apps, the page has to run first. Frameworks that set document.title and the meta tags at runtime leave the initial HTML empty, so crawlers that only fetch see the app name and no image. The Metadata API can render the page in a headless browser and read the tags the app actually produces.
Single-page apps set their metadata after the HTML is served
React Helmet, Vue Meta and their equivalents write title, description and og:image on the client. A fetch-based extractor reads the server response, which contains only the shell, and reports the same app name and the same generic description for every route.
The textbook fix is server-side rendering, or a prerendering service in front of the app, and that is a project for the site’s owner. When you are unfurling links to other people’s apps, you cannot change how they render. Sleeping for a fixed time before reading the DOM is the other common hack, and it is too short on slow days and wasted time on fast ones.
Microlink picks the fetch mode on its own with prerender set to auto. For pages you know are client-rendered, set prerender to true and add waitForSelector for an element that only exists once the route has rendered. The metadata is then read from the live DOM and normalized like any other page.
How to prerender a page before reading its metadata
prerender decides whether a browser is used, and the wait options decide when the tags are trustworthy. The page preparation guide has the full decision table.
import createClient from 'microlink.io'
const microlink = createClient({
apiKey: process.env.MICROLINK_API_KEY
})
const { title, description, image } = await microlink.metadata(
'https://app.example.com/items/42',
{ prerender: true, waitForSelector: 'main h1' }
)The browser executes the app’s JavaScript, the route renders its h1, and only then is the metadata read. You get the same normalized fields as for a static page.
import createClient from 'microlink.io'
const microlink = createClient({
apiKey: process.env.MICROLINK_API_KEY
})
const { image } = await microlink.metadata('https://app.example.com/items/42', {
prerender: true,
waitForSelector: 'meta[property="og:image"]'
})When the app injects the tag late, wait for the tag rather than for visible content. Any CSS selector works, including attribute selectors on meta elements.
curl 'https://api.microlink.io/?url=https%3A%2F%2Fapp.example.com%2Fitems%2F42&prerender=true&waitForSelector=main+h1'The x-fetch-mode response header reports prerender when a browser was used and fetch when a plain request was enough. x-fetch-time shows how long that step took.
- prerender auto (default) lets the service decide, true forces a headless browser, false forces a plain HTTP GET.
- waitForSelector Pauses until the CSS selector appears in the DOM. Works for meta tags too.
- waitUntil Lifecycle event that marks navigation as done: auto (default), load, domcontentloaded, networkidle0 or networkidle2.
- waitForTimeout A fixed wait, as a last resort when the page has no stable selector.
- meta Restrict detection to the fields you need to keep renders fast.
- ttl Cache the rendered result for up to 31 days; browser renders are slower than fetches. Pro plans.
Rendered requests take longer than plain fetches, so keep meta scoped to the fields you use and let the cache serve the repeats. When nothing else works, navigate with waitUntil set to domcontentloaded and then wait for the element you need, as the waitUntil reference suggests.
Why a headless browser is the only source for SPA Open Graph tags
If the tags are produced by code, only executing that code reveals them.
Combine with override rules when the app still gets a field wrong after rendering.
x-fetch-mode tells you which path was taken, so you can build a per-domain setting from your logs.
When not to: if the site serves correct tags in its HTML, a plain fetch is faster and prerender adds nothing. The same render-then-read approach applies to Markdown from JavaScript-rendered pages and screenshots of dynamic content.
FAQ
Why does the metadata show the app name instead of the page title?
How do I get metadata from a React app that sets its tags with JavaScript?
Does prerender slow down metadata extraction from single-page apps?
Can the metadata request wait for the og:image tag specifically?
How do I confirm a browser was used to extract the metadata?
Solve the next problem with the same API
Fix missing or wrong og:image
Only the fields you need
Custom fields alongside metadata
Markdown from JavaScript-rendered pages
Screenshots of JavaScript-rendered pages
PDFs of JavaScript-rendered pages
Ready to read client-rendered metadata?
Render first, then extract. Start on the free tier and get the right title and image from your first single-page app today.