Skip to main content

Crate ai_answer_citations

Crate ai_answer_citations 

Source
Expand description

Pull the cited sources out of an AI answer.

ChatGPT, Perplexity, Claude and Google AI Overviews all attach sources to the answers they write, but they attach them differently: an HTML anchor with a site-name chip, a numbered marker like [3] that links out, a Markdown link in a copied answer, a footnote definition at the bottom, or a bare URL in plain text. Anyone measuring which pages AI answers cite has to read all of those forms before counting anything.

This crate does that reading. Give extract the HTML or Markdown of an answer and it returns every absolute http(s) link it can find, in document order, each tagged with the CitationForm it was found in, its visible label, and the numeric marker when the label is one. canonical then strips the fragment and the click-tracking query keys (utm_*, fbclid, gclid, srsltid and the like) so that two citations of the same page compare equal, and tally counts citations per host.

What it deliberately does not do: fetch anything, resolve relative links (an answer copied out of a chat has no base URL, and a relative link in one is almost never a citation), or guess a public-suffix list. host strips a leading www. and nothing else; if you need co.uk-aware grouping, map the hosts yourself.

No dependencies, no unsafe, no allocation beyond the returned strings.

Written as the measurement side of Seen in AI Answers, a page engine built around the question of what AI answers cite. The engine shapes pages to be cited; this crate reads what an answer actually cited, so the two can be compared once a citation sample exists.

§Example

use ai_answer_citations::{extract, canonical, tally, CitationForm};

let answer = r#"
<p>Boil the kettle first.<a href="https://example.com/tea?utm_source=chatgpt.com">[1]</a>
A second view is at <a href="https://www.example.com/tea#steep">Example</a>.</p>
Also see [the guide](https://guide.example.org/tea) and https://example.com/tea.
"#;

let cites = extract(answer);
assert_eq!(cites.len(), 4);
assert_eq!(cites[0].form, CitationForm::HtmlAnchor);
assert_eq!(cites[0].marker, Some(1));
assert_eq!(cites[2].form, CitationForm::MarkdownLink);
assert_eq!(cites[3].form, CitationForm::BareUrl);

// Tracking keys and fragments go; the three example.com links are one page.
assert_eq!(canonical(&cites[0].url), "https://example.com/tea");
assert_eq!(canonical(&cites[1].url), "https://www.example.com/tea");

let counts = tally(&cites);
assert_eq!(counts[0].host, "example.com");
assert_eq!(counts[0].citations, 3);
assert_eq!(counts[1].host, "guide.example.org");

An answer can only cite a page that its engine’s crawler was allowed to fetch. What OpenAI, Cloudflare, Perplexity and Google document about AI crawler access, with a robots.txt checker that runs in the browser, is collected at https://seeninaianswers.com/how-to-get-cited-in-ai-overviews/.

Structs§

Citation
One cited link, as found.
HostCount
A host and how many citations pointed at it.

Enums§

CitationForm
Where in the answer a citation was found.

Constants§

TRACKING_KEYS
Query keys that identify a click, not a page. Compared ASCII case-insensitively; any key beginning utm_ is also dropped.

Functions§

canonical
Lower-case scheme and host, drop a default port, drop the fragment, drop tracking query keys. Everything else — path case, remaining query order, trailing slash — is kept, because on a real server those can name different pages.
chatgpt_tagged
Was this link tagged by ChatGPT? ChatGPT appends utm_source=chatgpt.com to the links it cites, so the tag survives into whatever the user copied or the referrer log shows.
distinct_pages
Distinct pages cited, by canonical URL, in first-seen order.
extract
Every absolute http(s) link in text, in document order.
host
The host of an absolute http(s) URL, lower-cased, without port, userinfo or a leading www.. None for anything else.
tally
Citations per host, most-cited first, ties broken by host name.