PDF extraction

OpenGraph links for PDF citations

How to turn page, target, and bbox evidence into shareable citation links with hover previews, GIF unfurls, and exact source pixels.

May 6, 2026 10 min read okraPDF

Most document citations are too thin.

They point to a PDF, a page number, or a chunk of extracted text. That is useful, but it still makes the reader do the hard part: open the file, find the page, scan the table, and verify that the answer actually came from the source.

For PDFs, the better citation is not just a link to the file. It is a link to the evidence.

That means the citation needs to carry:

  • the source URL or document id
  • the PDF page number
  • the exact bounding box on that page, or enough target text to locate it
  • a preview image that shows the highlighted region
  • a normal URL that can unfurl in Slack, Docs, Linear, Notion, or a chatbot UI

OpenGraph is the practical bridge. Almost every app already knows how to preview links. If a citation URL emits the right metadata, the citation can travel through ordinary tools without every tool needing a custom PDF viewer.

The mistake is to make citations an app-only component.

A custom hover card is useful inside your product, but the moment an answer leaves that product, the evidence disappears. Someone pastes the answer into Slack. A support engineer links it in Linear. A finance team adds it to a memo. A chatbot returns it through an SDK.

The citation should survive all of those surfaces as a normal URL.

<meta property="og:image" content="https://res.okrapdf.com/citation-previews/v1/preview.gif" />
<meta property="og:image:type" content="image/gif" />
<meta property="og:image:width" content="960" />
<meta property="og:image:height" content="504" />

The URL is a branded shortlink. Normal browser requests get a server-side redirect to the source PDF page. Link unfurlers get an HTML metadata response with OpenGraph tags, a canonical URL, a visible preview, and a machine-readable citation payload.

Generic clients see a normal link preview. Okra-aware clients can read the JSON and render a richer source card.

The object model

A pixel-backed citation is small:

{
  "type": "okra_document_citation",
  "source_kind": "external",
  "document_name": "Apple 2025 Form 10-K",
  "page": 22,
  "bbox": { "x": 0.08, "y": 0.271, "width": 0.838, "height": 0.229 },
  "bbox_source": "gemini_flash_3",
  "locator": {
    "model": "google/gemini-3-flash-preview",
    "label": "Table",
    "confidence": 1,
    "bbox_1k": [271, 80, 500, 918],
    "prompt_kind": "table_region"
  },
  "text": "Apple share repurchase table showing total shares purchased and approximate dollar value remaining under the plans",
  "citation_href": "https://link.okrapdf.com/ofBNNsm",
  "source_href": "https://s2.q4cdn.com/.../_10-K-2025-As-Filed.pdf#page=22",
  "preview_href": "https://res.okrapdf.com/citation-previews/v1/c6334e8....svg",
  "og_image_href": "https://res.okrapdf.com/citation-previews/v1/989d63f....gif",
  "og_image_type": "image/gif"
}

The important field is page. The bbox can be supplied by the caller, or it can be generated from target or text.

The citation does not only say “see the 10-K.” It says “see this rectangle on page 22.” The preview then renders that region so the reader can inspect the actual pixels behind the answer before opening the full PDF.

Here is a real generated citation for the share repurchase table in Apple’s 2025 Form 10-K. The request passed page and target, but it did not pass pageImage or bbox. Okra fetched the public PDF, rendered only page 22, and used Gemini Flash 3 to locate the table on that page.

Open the source PDF page

Apple 2025 Form 10-K share repurchase table citation preview

Hover or focus the citation link below.

Apple reported 89.498 million shares purchased during the quarter, with approximately $99.779 billion remaining under the plans share repurchase table Apple 2025 Form 10-K share repurchase table highlighted on page 22 Apple share repurchase table Apple 2025 Form 10-K · page 22 · focused ParseBench prompt · bbox x=0.08 y=0.271 w=0.838 h=0.229 Open source PDF .

Existing chat clients do not need a new citation protocol

Most chat clients already render links. That is enough.

Let the model produce a normal answer with short markers:

Apple reported 89.498 million shares purchased during the quarter [A].

After the citation link is created, the client maps [A] to the Okra shortlink:

const citationA = {
  label: "A",
  title: "Apple share repurchase table",
  href: citation.citationHref ?? citation.citation_href,
  ogImageHref: citation.ogImageHref ?? citation.og_image_href,
  hoverImageHref:
    citation.previewVariants?.hoverImageHref ??
    citation.preview_variants?.hover_image_href ??
    citation.ogImageHref ??
    citation.og_image_href,
  previewHref: citation.previewHref ?? citation.preview_href,
};

const markdown = "Apple reported 89.498 million shares purchased during the quarter [A].".replace(
  `[${citationA.label}]`,
  `[\\[${citationA.label}\\]](${citationA.href})`,
);

The output is ordinary Markdown:

Apple reported 89.498 million shares purchased during the quarter [\[A\]](https://link.okrapdf.com/ofBNNsm).

That works in Markdown-based chat surfaces such as Vercel AI SDK chat UIs, LibreChat, Open WebUI, and internal React chat renderers. The link click goes to link.okrapdf.com/ofBNNsm; a browser gets redirected to the source PDF page, while an unfurler gets the OpenGraph image. Rich clients can use hoverImageHref for the bbox-cropped WebP preview without putting crop params in the visible link.

Slack-style clients use their own link syntax, but the source object stays the same:

const slackText = "Apple reported 89.498 million shares purchased during the quarter [A].".replace(
  `[${citationA.label}]`,
  `<${citationA.href}|[${citationA.label}]>`,
);
Apple reported 89.498 million shares purchased during the quarter <https://link.okrapdf.com/ofBNNsm|[A]>.

If the chat client is Okra-aware, it can keep the okra_document_citation object beside the text and render a hover card from og_image_href or preview_href. If the client is generic, it still gets a normal link and a normal OpenGraph preview.

Render the page the user already found

Citation generation should not be a research operation by default.

If the user already knows the evidence is on page 22, the system should not scan all 80 pages of the filing. If the user already has a bounding box, the system should not call a vision model to rediscover it. That wastes tokens, adds latency, and makes a deterministic action feel unpredictable.

Treat citation generation like an image transform:

const citation = await okra.documentCitations.create({
  sourceHref: "https://s2.q4cdn.com/470004039/files/doc_financials/2025/ar/_10-K-2025-As-Filed.pdf#page=22",
  documentName: "Apple 2025 Form 10-K",
  page: 22,
  target: "share repurchase table with total 89,498 shares purchased and $99,779 remaining under the plans",
  text: "Share repurchase activity during the three months ended September 27, 2025",
  title: "Apple share repurchase table"
});

This is the Cloudinary mental model: known source plus known page plus known target becomes a durable delivery URL. If the caller already has the crop, it can pass bbox directly and skip the locator.

For citations, the final crop is a normalized PDF bbox:

x      left edge, 0..1
y      top edge, 0..1
width  box width, 0..1
height box height, 0..1

When sourceHref, page, and target are supplied, Okra fetches the PDF, renders only the requested page, localizes the bbox on that page, and builds the visual artifacts:

  • an animated SVG highlight
  • a GIF for OpenGraph unfurls
  • a branded shortlink with metadata for unfurlers
  • the structured okra_document_citation payload

The prompt changes only when the bbox is missing. A known bbox takes the deterministic render path. A missing bbox uses a focused ParseBench locator prompt: it parses only the evidence region the user is looking for and returns the best visible bbox. The prompt is tuned by target type: table-region prompts for table citations, total/balance prompts for receipts and invoices, quoted-text prompts for exact snippets, and a generic citation-target prompt for everything else.

If the caller already has a bbox, the locator is skipped. The key is scope. “Find the share repurchase table on page 22” is a bounded page-level task. “Find it somewhere in this 80-page filing” is a document research task and should be explicit.

Direct PDF links are weak citations.

https://s2.q4cdn.com/470004039/files/doc_financials/2025/ar/_10-K-2025-As-Filed.pdf#page=22

That link may open the right page in some browsers. It does not show the evidence in Slack. It does not create a useful hover card in a chat UI. It does not preserve a bbox. It does not give another agent enough structure to render a source preview.

The citation link solves that by splitting the surfaces:

FieldUse
citation_hrefThe normal clickable link
source_hrefThe original PDF or authenticated source view
preview_hrefStatic SVG preview for hover cards
og_image_hrefGIF or bitmap for link unfurls

That lets generic clients and rich clients cooperate. A generic client opens citation_href. A richer client can show og_image_href on hover and offer “open source” with source_href.

The same citation URL has two server-side responses. Browsers get redirected to the source PDF page. Unfurlers get HTML:

<link rel="canonical" href="https://link.okrapdf.com/ofBNNsm" />
<meta property="og:type" content="article" />
<meta property="og:title" content="Apple share repurchase table | okraPDF" />
<meta property="og:description" content="Review the share repurchase table from Apple 2025 Form 10-K on page 22." />
<meta property="og:image" content="https://res.okrapdf.com/citation-previews/v1/989d63f....gif" />
<meta property="og:image:type" content="image/gif" />
<meta property="og:image:width" content="960" />
<meta property="og:image:height" content="504" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://res.okrapdf.com/citation-previews/v1/989d63f....gif" />

<script type="application/json" id="okra-citation">
{
  "type": "okra_document_citation",
  "source_kind": "external",
  "page": 22,
  "bbox": { "x": 0.08, "y": 0.271, "width": 0.838, "height": 0.229 },
  "bbox_source": "gemini_flash_3",
  "locator": { "prompt_kind": "table_region" },
  "source_href": "https://s2.q4cdn.com/.../_10-K-2025-As-Filed.pdf#page=22"
}
</script>

The page is durable and cacheable. The preview assets are content-addressed. The original PDF does not have to be hosted by Okra.

pageImage is still useful, but it is not the default. Pass it only when the source PDF is gated, private, or rendered by your own application. In those cases the public source_href may not be enough for Okra to fetch the file, so the caller can provide the rendered page image directly.

What this changes for LLM citations

Most LLM citations today point at chunks. That is a reasonable start, but chunks are not the source document. They are an intermediate representation of the source document.

For financial filings, invoices, bank statements, insurance claims, and contracts, the reviewer often needs the rendered source:

  • Was the value in a table or a footnote?
  • Was the value a subtotal, a total, or a per-share number?
  • Did the parser merge two rows?
  • Did the model quote a value that was visually near the target but not part of it?

A pixel citation gives the answer a review path. The model can say:

Apple reported 89.498 million shares purchased during the quarter, with approximately $99.779 billion remaining under the plans.

And the citation can show the exact table region where those values appear.

That is the difference between a source link and source evidence.

A useful default

Make the cheap path the default:

  1. If sourceHref, page, and bbox are provided, rasterize only that page, then render.
  2. If sourceHref, page, and target or text are provided, rasterize only that page, locate the bbox there, then render.
  3. If pageImage and bbox are provided, skip PDF fetching and render from the supplied image.
  4. If page is missing, require an explicit research mode.

This keeps the citation pipeline fast enough for UI hovers and agent workflows. The user already did the research. The citation service should package the evidence.

The takeaway

OpenGraph citation links make source evidence portable.

They work as ordinary URLs, but they carry enough structure for richer clients to render the exact pixels behind an answer. They let Slack, Docs, chatbots, SDKs, and internal QA tools all point at the same canonical citation object.

For document AI, that matters. The output is not just the answer. It is the answer plus the evidence required to trust it.