Extract the real citation URLs from a ChatGPT deep research page by reading React props, then map them back to exported markdown cite blocks.
- Do not treat visible citation numbers and exported markdown cite blocks as the same thing.
- Use any rendered citation badge as the entry point.
- Walk up the React fiber tree until you find
memoizedProps.contentReferences. - Use
contentReferences[*].matched_textas the join key. - Use
contentReferences[*].safe_urlsas the resolved URL list.
- You do not need the page to expose the full report text again.
- If the page still exposes citation elements and
contentReferences, that is enough. - If your exported or copied markdown still contains the original
cite...blocks, that is enough. - Join
matched_textto those markdown cite blocks, then usesafe_urlsfor the real URLs.
- Open the ChatGPT conversation in chrome-devtools.
- Find any citation badge in the report.
- Run an evaluate-script against that element.
- Discover the React fiber key.
- Walk ancestor fibers until
contentReferencesappears. - Extract
matched_textandsafe_urls. - Match
matched_textto the markdown'scite...blocks.
(el) => {
return Reflect.ownKeys(el)
.map(String)
.filter((key) => key.startsWith("__reactFiber") || key.startsWith("__reactProps"));
}(el) => {
const fiberKey = Reflect.ownKeys(el).find((key) => String(key).startsWith("__reactFiber"));
let fiber = fiberKey ? el[fiberKey] : null;
for (let depth = 0; fiber && depth < 25; depth += 1, fiber = fiber.return) {
const props = fiber.memoizedProps;
if (props && typeof props === "object" && Array.isArray(props.contentReferences)) {
return {
depth,
count: props.contentReferences.length,
keys: Object.keys(props),
};
}
}
return { error: "contentReferences not found" };
}(el) => {
const fiberKey = Reflect.ownKeys(el).find((key) => String(key).startsWith("__reactFiber"));
let fiber = fiberKey ? el[fiberKey] : null;
let contentReferences = null;
for (let depth = 0; fiber && depth < 25; depth += 1, fiber = fiber.return) {
const props = fiber.memoizedProps;
if (props && typeof props === "object" && Array.isArray(props.contentReferences)) {
contentReferences = props.contentReferences;
break;
}
}
if (!contentReferences) {
return { error: "contentReferences not found" };
}
return contentReferences.map((ref) => ({
matched_text: ref.matched_text,
safe_urls: ref.safe_urls,
alt: ref.alt,
type: ref.type,
}));
}Use this field:
matched_text
Do not join on these alone:
- visible citation number
- single
turn...ID - tooltip text
If the markdown no longer contains the original cite... blocks, the strict join becomes much weaker.
matched_textpreserves the exact exported markdown cite block, such asciteturn23view0turn19view0turn43view0safe_urlspreserves the resolved URLs for that cite block- one visible citation can collapse multiple internal IDs
- one cite block can resolve to multiple URLs
- a collapsed deep research card can still be enough, as long as citation elements still expose
contentReferences
{
"matched_text": "citeturn23view0turn19view0turn43view0",
"safe_urls": [
"https://example.com/a",
"https://example.com/b"
],
"alt": "([example.com](https://example.com/a))",
"type": "grouped_webpages"
}- Do not hardcode the badge number, DOM UID, or ancestor depth.
- The React property prefix is stable enough, but the suffix changes.
- Prefer
safe_urlsover justitems[0].url. - Use tooltip hover only for spot-checking, not full extraction.
Start from any citation badge, find __reactFiber..., walk upward to memoizedProps.contentReferences, then use matched_text -> safe_urls to map real URLs back to exported markdown cites.