Classify Citation Sources in AI Answers: Rules, Then AI

How to classify citation sources in AI answers: rules typed 40% of 2,525 citations from 24 domains. The long tail needs better types, not just a bigger model.

· BugViso Team

14 min read

To classify citation sources in AI answers, type each cited domain with fixed rules first (your site, competitors, known platforms) and send only the long tail to a model, caching every verdict. That's the right architecture, and it leaves a problem most tools don't admit. In 2,525 citations from four engines, rules typed 40% of citations using just 24 domains. The other 638 domains went to a model, and 626 of them came back "other".

So the hard part isn't the model. It's the type list. By the end of this post you'll know how a two-stage classifier works, where rules and models each fail, which two types most taxonomies are missing, and how to label the long tail so the numbers mean something. A script implements the rules and a label file.

Data used: 2,525 citations on 662 registered domains from 400 answers (50 buyer prompts × ChatGPT, Gemini, Perplexity and Claude × 2 runs on 2026-10-20, web search on), collected and typed by BugViso's AI Visibility Tracker for bugviso.com. We re-ran the tracker's rule step to separate rule and model verdicts, and hand-labelled the 60 most-cited "other" domains on 2026-10-29 using each homepage's title. Third-party domains aren't named.

Why Citation Types Drive Decisions

Citation types decide which actions an AI visibility report recommends, so a wrong type sends work to the wrong place. A report that says "review sites are 5% of your citations" leads to a review-site programme. One that says "forums are cited" leads to community work. Both depend on a label attached to a domain.

A usable taxonomy has types that map to different actions:

TypeWhat the page isThe action it implies
OwnYour siteMake the cited pages accurate and quotable
CompetitorA tracked rival's siteCompare their cited pages with yours
ReviewA multi-vendor review platformKeep your profile complete and current
ForumA discussion thread or communityAnswer genuinely where threads are cited
NewsA news publisherPR and coverage
ReferenceAn encyclopedia or standards bodyMake sure facts about you are right there
DirectoryA listing, marketplace or company databaseClaim and complete the listing
OtherEverything elseNothing, until you look

"Other" is where classifiers hide their uncertainty, and in our data it was most of the citations. The source mix itself, engine by engine, is in our study of AI search citation sources.

Stage 1: Rules Type Few Domains and Many Citations

Rules work because citations are concentrated: a handful of domains carry a large share. The first rules need no list at all: your own registered domain, and each tracked competitor's. Then fixed lists of known platforms per type.

Here's what the rule stage decided on our 2,525 citations:

Rule-typed asCitationsDomains
Own site5551
Competitors3015
Forum486
Directory483
Review245
Reference211
News103
Rules total1,007 (39.9%)24
Left for the model1,518 (60.1%)638

Twenty-four domains covered 40% of citations. Rules are free, instant and auditable, so every domain you can type by rule should be.

Three engineering details keep rules correct:

  1. Compare registered domains, not hostnames. blog.example.co.uk and www.example.co.uk are one site. Getting the registered domain right needs the Public Suffix List, because co.uk is a suffix and example.co.uk isn't.
  2. Parse URLs properly. Hosts, ports and paths follow RFC 3986; use a real parser such as Python's urllib.parse, not string splitting.
  3. Let paths override domains where they matter. A GitHub issue is a thread; a GitHub repository is documentation. A community. subdomain of a vendor is a forum, while the vendor's main site isn't.

Stage 2: The Model Got 638 Domains and Typed 12

The model step sees only the long tail, and the long tail is mostly sites that fit none of the types. BugViso classifies each unknown domain once with a language model and caches the answer. Of the 638 domains it received, it typed 12 as something other than "other":

Model verdictDomainsCitationsChecked by hand
Review9681 real review platform; 8 vendors with free graders, speed tests or accessibility scanners
Reference13A documentation platform: plausible
News11A regional newspaper: correct
Directory11An agency directory: correct
Other6261,445See the next section

Four of the 12 non-"other" verdicts held up. The 8 wrong ones share a pattern: a software vendor that publishes a free "grade your site" tool looks, to a model reading a domain name and a short description, like a site that reviews websites. We found the same effect when we audited review sites in AI citations: most "review" citations were vendors.

The model wasn't careless. The taxonomy gave it no right answer for "a vendor's free tool page", so it picked the nearest type.

What's Really in "Other"

Most of "other" is two groups your taxonomy should name: untracked vendors and official documentation. We hand-labelled the 60 most-cited "other" domains, which hold 616 of its 1,445 citations, by fetching each homepage and reading its title:

Hand labelDomainsCitationsShare of the 616
Untracked vendors (product sites in or near the category)3731952%
Official docs (platform, browser, framework and standards documentation)921034%
Agencies and consultants6417%
Publishers and blogs4213%
Marketplace141%
Couldn't tell (site unreachable or unclear)3213%

Both big groups imply actions. Untracked vendors are competitors you aren't tracking: 37 product sites cited in answers about your category, each one a name buyers may be reading next to yours. Official docs are the reference pages engines quote on how-to questions; they're unwinnable as citations, but they tell you which explanations an engine trusts, which matters for your own content.

Neither group appears in an eight-type taxonomy, so both disappear into "other". That's why a report can show "57% other" and still recommend review-site work for a 1.4% type.

💡 Rule of thumb: if "other" is over a third of your citations, the classifier isn't the problem; the type list is. Add types for what the long tail actually contains.

A Better Pipeline to Classify Citation Sources

Rules, then a label file you control, then a model with definitions, and an audit of the types that trigger actions.

  1. Rules for own, competitors and known platforms, with path overrides for threads and docs subdomains.
  2. Add "vendor" and "official docs" as types. Rules catch many docs (docs., developer., /docs/ paths, known platforms). Vendors need a model or a label.
  3. Keep a label file that beats every other source. One line per domain, written by a person. It's the cheapest accuracy you can buy: on our data, 65 lines moved 442 citations out of "other".
  4. Give the model definitions and counter-examples, not just type names: "a vendor that offers a free grader is a vendor, not a review site". Allow "unknown" as an answer rather than forcing a type.
  5. Cache per domain, log the reason, and revisit. A cached wrong verdict repeats in every report until someone overrides it.
  6. Audit by type before acting. Sample 10–20 domains from any type that would trigger work (review, forum, directory) and check them by hand.

Classify Your Own Citations

This script applies the rule stage (own domain, competitors, known platforms per type, GitHub and community. path rules, and docs rules), then a CSV label file, and prints the type mix, how many citations each stage decided, and the unlabelled domains worth labelling next. Standard library only; the registered-domain function is a rough stand-in for the Public Suffix List.

python
#!/usr/bin/env python3
"""Classify the domains AI answers cite: rules first (your site, competitors, known platforms,
docs paths), then your own label file for the long tail. Prints the type mix and the
unlabelled domains worth labelling next, by citations.

Usage: python3 classify_sources.py answers.csv --own acme.com --competitor rival.com ... [--labels labels.csv]
answers.csv columns: cited_urls (space-separated).   labels.csv columns: domain, type.
"""
import argparse, csv, re
from collections import Counter
from urllib.parse import urlparse

RULES = {
    "review": {"g2.com", "capterra.com", "trustpilot.com", "trustradius.com", "getapp.com", "softwareadvice.com",
               "gartner.com", "yelp.com", "clutch.co", "sitejabber.com", "softwaresuggest.com", "tripadvisor.com"},
    "forum": {"reddit.com", "quora.com", "stackoverflow.com", "stackexchange.com", "ycombinator.com"},
    "reference": {"wikipedia.org", "wikidata.org", "britannica.com", "w3.org", "mozilla.org", "rfc-editor.org"},
    "news": {"nytimes.com", "reuters.com", "bbc.com", "theverge.com", "techcrunch.com", "searchengineland.com"},
    "directory": {"crunchbase.com", "linkedin.com", "producthunt.com", "apps.shopify.com", "wordpress.org"},
    "official docs": {"web.dev", "react.dev", "developer.chrome.com", "developers.google.com", "support.google.com",
                      "learn.microsoft.com", "developers.cloudflare.com", "platform.openai.com", "docs.perplexity.ai"},
}
TWO_PART = {"co", "com", "org", "net", "ac", "gov", "edu"}

def registered(host):
    """Rough registered domain (use the Public Suffix List in production)."""
    p = host.split(".")
    return ".".join(p[-3:]) if len(p) > 2 and p[-2] in TWO_PART and len(p[-1]) == 2 else ".".join(p[-2:])

def rule_type(url, own, rivals):
    u = urlparse(url)
    host = (u.hostname or "").lower().removeprefix("www.")
    dom, path = registered(host), u.path.lower()
    if dom in own:
        return dom, "own"
    if dom in rivals:
        return dom, "competitor"
    if dom == "github.com":  # the path decides: a thread or a code/docs page
        return dom, "forum" if re.match(r"^/[^/]+/[^/]+/(issues|discussions)/", path) else "official docs"
    if re.match(r"^(community|forum|forums|discuss)\.", host):
        return dom, "forum"
    for t, names in RULES.items():
        if host in names or dom in names:
            return dom, t
    if re.match(r"^(docs|developer|developers|learn|help|support)\.", host) or path.startswith("/docs/"):
        return dom, "official docs"
    return dom, None

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("csv"); ap.add_argument("--own", action="append", default=[])
    ap.add_argument("--competitor", action="append", default=[]); ap.add_argument("--labels")
    a = ap.parse_args()
    labels = {}
    if a.labels:
        labels = {r["domain"].lower(): r["type"] for r in csv.DictReader(open(a.labels, newline="", encoding="utf-8"))}
    by_type, by_source, tail = Counter(), Counter(), Counter()
    domains = {}
    for r in csv.DictReader(open(a.csv, newline="", encoding="utf-8")):
        for url in r["cited_urls"].split():
            dom, t = rule_type(url, set(a.own), set(a.competitor))
            src = "rule" if t else ("label file" if dom in labels else "unlabelled")
            t = t or labels.get(dom) or "other"
            by_type[t] += 1; by_source[src] += 1; domains.setdefault(t, set()).add(dom)
            if src == "unlabelled":
                tail[dom] += 1
    n = sum(by_type.values())
    print(f"{n} citations on {len(set().union(*domains.values()))} domains")
    print("decided by: " + ", ".join(f"{k} {v} ({100 * v / n:.1f}%)" for k, v in by_source.most_common()))
    for t, v in by_type.most_common():
        print(f"  {t:<14}{v:>6}  {100 * v / n:5.1f}%  on {len(domains[t])} domains")
    covered = sum(c for _, c in tail.most_common(60))
    print(f"\nlabel these next: the top 60 unlabelled domains hold {covered} of {sum(tail.values())} unlabelled citations")
    for d, c in tail.most_common(10):
        print(f"  {c:>4}  {d}")

if __name__ == "__main__":
    main()

Run with rules only, then with a 65-line label file built from the hand labels above:

text
$ python3 classify_sources.py answers.csv --own bugviso.com --competitor ...
2525 citations on 662 domains
decided by: unlabelled 1297 (51.4%), rule 1228 (48.6%)
  other           1297   51.4%  on 627 domains
  official docs    253   10.0%  on 26 domains

$ python3 classify_sources.py answers.csv --own bugviso.com --competitor ... --labels labels.csv
decided by: rule 1228 (48.6%), unlabelled 855 (33.9%), label file 442 (17.5%)
  vendor           355   14.1%  on 45 domains
  official docs    274   10.9%  on 27 domains

The script's docs and path rules took the rule stage from 40% to 48.6% of citations. Sixty-five labels cut "other" from 51.4% to 33.9%. The remaining 855 "other" citations sit on 567 domains, an average of 1.5 citations each: a long tail you can leave unlabelled without changing any decision.

How BugViso Classifies Citation Sources

BugViso's AI Visibility Tracker types every cited URL by its registered domain, handling multi-label suffixes such as co.uk. Your own domain and each tracked competitor's are matched first, then fixed lists of known review, forum, news, reference and directory platforms. Domains no rule matches are classified once by an AI model at the end of a run and cached, so each new domain costs one lookup ever.

The sources view lists the domains behind every type with their citation counts, and the source-gaps view ranks third-party domains cited in answers that name a rival but not you. Because the domains are listed, you can check any type before acting on it, which is how we found the vendor sites typed as review sites above. Citation typing feeds the citation part of the index, the diagnosis and the action plan's source-specific items; it's part of the AI visibility tracker.

Classification Mistakes That Mislead Reports

Typing by hostname. support.vendor.com and vendor.com are one organisation. Without registered domains, one site shows up as several sources.

Forcing a type. A model with no "vendor" option will call vendors review sites. Add the type or allow "unknown".

Typing platforms, not pages, where pages differ. GitHub, Medium and YouTube host everything from documentation to discussion. Use path rules for the big platforms; our forum citation analysis shows how much that changes the count.

Never revisiting the cache. A cached verdict is reused in every future report. Keep an override file and check high-impact types after the first few runs.

Reporting "other" as a slice of the pie. A 57% "other" slice isn't information. Report the labelled types and say how much is unlabelled.

Frequently Asked Questions

How do you classify the sources cited in AI answers?

Type each cited domain with rules first (your site, competitors, known review, forum, news, reference and directory platforms), then classify the remaining domains with a model or by hand, and cache each verdict. In our data, rules alone typed 40% of 2,525 citations from just 24 domains.

What types should I use for AI citation sources?

At least: own, competitor, review, forum, news, reference, directory and other, plus two our data says most lists miss: untracked vendors and official documentation. Those two made up 86% of the most-cited "other" citations we hand-labelled.

Can an LLM classify citation domains accurately?

For clear cases, yes; for sites that fit no type, it guesses. In our data, 8 of the 9 domains a model typed as review sites were software vendors with free graders. Give the model definitions, counter-examples and an "unknown" option, and audit its output.

Why does my AI visibility report show so much "other"?

Because the long tail of cited domains is mostly vendors, documentation and blogs that the report's types don't cover. Label the most-cited "other" domains first: in our data, 60 domains held 616 of 1,445 "other" citations.

Should GitHub count as a forum in AI citation tracking?

Only GitHub issues and discussions. Repositories and files are documentation. In our data, 7 of 28 GitHub citations were issues or discussions; the rest were code and docs pages.

Fix the Type List Before the Model

A two-stage classifier is the right design, but its output is only as useful as its types: add vendor and official-docs types, keep a label file for the most-cited long tail, and audit any type that triggers work. BugViso's AI visibility tracking lists the domains behind every citation type, so you can check a type in a minute before you plan a quarter around it.

Written by

· BugViso Team

Kazi writes BugViso’s guides on website audits, Core Web Vitals, technical SEO, accessibility and AI search. The data in them comes from audits BugViso runs in a real Chromium browser, including our 219-site study of website health.

Found this useful? Share it.

Summarise with AI

Open this guide in your assistant.

Follow BugViso

New guides and audit data as we publish them.

See where your site stands

Run a free BugViso audit for SEO, speed, accessibility and AI search readiness — with fixes you can ship today.