| name | wikipedia-reference-verifiability |
| description | Analyze whether a Wikipedia page's references contain URLs — detect bare plain-text citations, template-based citations without url= parameters, shortened footnotes, and named ref reuse. Useful for article quality assessment, NPP triage, and citation maintenance |
| depends_on | ["wikimedia-api-access","wikimedia-wikitext"] |
| license | MIT |
| compatibility | opencode |
| skill_discovery_hints | [{"keywords":["reference URLs","citation URLs","bare ref","URL detection","verifiability check"]},{"keywords":["named ref","shortened footnote","url= parameter","reference analysis"]}] |
| last_verified | "2026-06-10T00:00:00.000Z" |
⚠️ User-Agent required: The API calls in this skill need a descriptive User-Agent
header. See the wikimedia-api-access skill for
the correct format.
⚠️ Prerequisite: This skill assumes you can parse wikitext with mwparserfromhell.
See the wikimedia-wikitext skill for AST parsing
patterns.
Reference: Why Reference URLs Matter for Verifiability
Wikipedia's core policy, Verifiability (WP:V), requires that readers can
verify that content is backed by reliable sources. A reference that provides
a clickable URL is immediately verifiable by any reader. A reference
without a URL requires the reader to:
- Own the cited book or journal
- Have access to a library with the cited work
- Trust the author's claim about the source
This is one of the most common red flags during New Page Patrol (NPP) review,
and a frequent target for citation maintenance.
Reference Types and Verifiability
| Type | Wikitext Example | Has URL? | Verifiable Online? |
|---|
| Citation template with URL | {{cite web|url=https://...}} | ✅ | ✅ Yes |
| Citation template without URL | {{cite book|title=X|year=2020}} | ❌ | ❌ (print only) |
| Plain-text ref | <ref>Smith, 2020, p.42</ref> | ❌ | ❌ (no link) |
| Shortened footnote | <ref>{{harvsp|Smith|2020}}</ref> | ❌ | ❌ (may resolve in bibliography) |
| Named ref (reuse) | <ref name="x" /> | resolves to definition | depends on definition |
SOP: URL Detection Strategy
The analysis proceeds in several passes:
Pass 1 — Collect all <ref> tags
import mwparserfromhell
parsed = mwparserfromhell.parse(wikitext)
ref_tags = parsed.filter_tags(
matches=lambda t: str(t.tag).strip().lower() == "ref"
)
Pass 2 — Index named ref definitions
Named refs can be defined anywhere on the page, even after their first use.
Index all definitions before evaluating individual refs:
named_defs: dict[str, str] = {}
for tag in ref_tags:
name = _named_ref_name(tag)
content = _tag_content(tag)
if name and content and name not in named_defs:
named_defs[name] = content
Pass 3 — Evaluate each reference
For each ref tag:
- If it has content, use that. If it's a named ref reuse (no content but
has a name attribute), resolve from the index.
- Count it only once per name group (avoid double-counting reused named refs).
- Check for URLs in the content via two strategies (see below).
Pass 4 — Check inline citation templates outside <ref> tags
Some pages use citation templates directly in the text (old-style inline
citations). Check for {{cite ...}} templates that aren't nested inside
any <ref> tag:
for template in parsed.filter_templates():
name = str(template.name).strip().lower()
if name.startswith("cite "):
if not _is_inside_ref(template, ref_tags):
SOP: URL Detection — Two Strategies
Strategy A — Raw URL in text
A simple regex check catches bare URLs and links inside plain text:
import re
URL_RE = re.compile(r"https?://", re.IGNORECASE)
def _contains_raw_url(text: str) -> bool:
return bool(URL_RE.search(text))
This catches:
<ref>https://example.com/article</ref> (bare URL)
<ref>Available at https://example.com</ref> (URL in prose)
<ref>{{cite web|url=https://...|title=...}}</ref> (URL inside template)
<ref>[https://example.com Article]</ref> (external link syntax)
Strategy B — Citation template URL parameters
When a reference uses a CS1/CS2 citation template, the URL may be in a
named parameter, not in the raw text. Check for the following parameter names:
URL_PARAM_NAMES = frozenset({
"url",
"chapter-url",
"conference-url",
"contribution-url",
"transcript-url",
"archive-url",
"chapterurl",
"conferenceurl",
"contributionurl",
"transcripturl",
"archiveurl",
"accessdate",
})
Implementation:
def _template_has_url_param(template: mwparserfromhell.nodes.Template) -> bool:
for param in template.params:
name = str(param.name).strip().lower()
if name in URL_PARAM_NAMES:
value = str(param.value).strip()
if value:
return True
if not name and _contains_raw_url(str(param.value)):
return True
return False
Known citation template families that accept URL parameters:
| Template Family | URL Parameter | Notes |
|---|
{{cite web}} | url | Primary parameter |
{{cite news}} | url | |
{{cite journal}} | url | Often has DOI instead |
{{cite book}} | url | Rarely used (print sources) |
{{cite magazine}} | url | |
{{cite encyclopedia}} | url | |
{{cite report}} | url | |
{{cite thesis}} | url | |
{{cite conference}} | url | |
{{cite podcast}} | url | |
{{cite episode}} | url | |
{{cite map}} | url | |
{{citation}} | url | Generic citation template |
SOP: Handling Edge Cases
Named Ref Reuse
seen_named: set[str] = set()
for tag in ref_tags:
name = _named_ref_name(tag)
content = _tag_content(tag)
if not content and name:
content = named_defs.get(name, "")
elif not content:
continue
if name:
if name in seen_named:
continue
seen_named.add(name)
...
Shortened Footnotes ({{harvsp}}, {{sfn}}, {{harvnb}})
These author-date templates point to a bibliography entry elsewhere on the
page. The inline ref has no URL, but the bibliography entry might. To check
the bibliography:
- Scan the page for a
== Bibliography == or == References == section.
- Look for full
{{cite ...}} templates in that section.
- Check if the author/year from the shortened footnote matches a biblio entry.
- Determine if that entry has a URL.
This is heuristic — a simplified approach is to flag the inline ref and
report the bibliography entries separately:
SHORTENED_FOOTNOTE_TEMPLATES = frozenset({
"harvsp", "harvnb", "harv", "sfn", "harvard citation",
})
def _is_shortened_footnote(name: str) -> bool:
return name in SHORTENED_FOOTNOTE_TEMPLATES or \
name.startswith("harv") or name.startswith("sfn")
Nested Templates
A citation template may be nested inside another template inside a <ref>:
<ref>{{citation|title=X|url=https://...}}</ref> ← simple nest
<ref>{{harvsp|Smith|2020}} ← shortened footnote
The inner {{citation|url=...}} must still be detected. After extracting
the ref content, parse it recursively:
for template in parsed.filter_templates():
if _template_has_url_param(template):
return True
for param in template.params:
inner = mwparserfromhell.parse(str(param.value))
for nt in inner.filter_templates():
if _template_has_url_param(nt):
return True
Infobox Presence
Infoboxes are templates whose name starts with Infobox:
def has_infobox(wikitext: str) -> bool:
parsed = mwparserfromhell.parse(wikitext)
for template in parsed.filter_templates():
name = str(template.name).strip()
if name.lower().startswith("infobox"):
return True
return False
Reference: Summarizing References for Display
When displaying a URL-free reference to a reviewer, produce a human-readable
snippet that identifies the type of reference:
def _summarize_ref(content: str) -> str:
cleaned = re.sub(
r"\{\{(\s*[Cc]ite\s+\w+|\s*[Hh]arv[np]?\w*|\s*[Ss]fn)\b[^}]*\}\}",
lambda m: "[" + m.group(1).strip() + "]",
content,
)
cleaned = re.sub(r"\{\{[^}]*\}\}", "[template]", cleaned)
cleaned = re.sub(r"\[\[([^\]|\]]+)\|([^\]]+)\]\]", r"\2", cleaned)
cleaned = re.sub(r"\[\[([^\]|]+)\]\]", r"\1", cleaned)
cleaned = re.sub(r"'''''|'''|''", "", cleaned)
cleaned = re.sub(r"\s+", " ", cleaned).strip()
if len(cleaned) > 80:
cleaned = cleaned[:77] + "..."
return cleaned or "(empty ref)"
This produces readable labels like:
[harvsp] — shortened footnote
[Cite journal] — cite journal template without URL
[cite Instagram] — Instagram citation
Smith, J. The Book... — plain text
Tooling
🔧 Check References for a Single Page (scripts/check-ref-urls.sh)
./scripts/check-ref-urls.sh "Betka Ait Mokran"
./scripts/check-ref-urls.sh "LOL: Slutty Bass"
Outputs a per-reference summary showing whether each ref has a URL.
🔧 Batch Scan New Pages (scripts/batch-ref-audit.sh)
./scripts/batch-ref-audit.sh --days 7 --limit 100
./scripts/batch-ref-audit.sh --days 1 --limit 50 --no-quality
Combines the PageTriage two-pass pipeline with reference analysis. Equivalent
to the npp-finder tool.
🐍 Reference URL Checker Library (assets/ref_url_checker.py)
The full reference analysis library — importable, with all edge cases handled:
from assets.ref_url_checker import has_any_url_refs, has_infobox
has_url, total, url_count, samples = has_any_url_refs(wikitext)
has_box = has_infobox(wikitext)
🐍 Test Suite (assets/test_ref_checker.py)
python3 -m pytest assets/test_ref_checker.py -v
Tests cover: raw URLs, citation templates with/without URL, named ref
resolution, shortened footnotes, nested templates, empty pages, self-closing
tags, and reused named refs.
SOP: New Page Patrol with PageTriage
⚠️ Deployment: PageTriage is in active production on enwiki and testwiki only
(ruwiki in progress). Requires the patrol right for write operations.
Verify availability: check Special:Version or the API sandbox for pagetriagelist.
Review Status Codes
The pagetriage_page SQL table stores review status:
| Value | Meaning |
|---|
0 | Unreviewed — needs NPP review |
1 | Reviewed |
2 | Patrolled (admin) |
3 | Autopatrolled (creator has the right) |
API Endpoints
List unreviewed pages (requires patrol right):
GET /w/api.php?action=pagetriagelist&showunreviewed=1&limit=50&format=json
Check review status (no auth needed):
GET /w/api.php?action=query&prop=info&inprop=protection&titles=Page_Title&format=json
Mark reviewed (POST, requires CSRF token):
POST /w/api.php?action=pagetriageaction&pageid=12345&reviewed=1&token=...
Legacy API (no patrol right required for reading):
GET /w/api.php?action=query&list=unreviewedpages&filterlevel=0&namespace=0&limit=50&format=json
Database Access (for bulk analysis)
When you have Toolforge SQL access, query the pagetriage_page table directly:
SELECT ptrp_page_id, ptrp_reviewed, ptrp_created
FROM pagetriage_page
WHERE ptrp_reviewed = 0 AND ptrp_deleted = 0
AND ptrp_created >= NOW() - INTERVAL 7 DAY
ORDER BY ptrp_created DESC LIMIT 100;
Tooling
See also: wikimedia-database for SQL access,
wikimedia-eventstreams for real-time page-create detection.
Cross-References