Enhanced Links
Cross-references and linkenhancer links in GEDCOM records
Although webtrees replaces XREFs such as @I2@ in notes with a cross-reference and its appropriate display name, this is not very flexible. You can’t determine the display name, which means that in the event of an error (due to XREF renumbering or typing error), it is not easy or possible to resolve the cross-reference to the corresponding data record without any doubt.
Tip: The custom module webtrees-mitalteli-show-xref can help you find the XREF and UID reference of personal data records. Some UIDs (38 characters length) are automatically shortened by webtrees in the standard view — this module shows the unchanged UIDs.
How it works
The enhanced links function is implemented via JavaScript and searches in the rendered output of webtrees for <a> tags whose href attribute values begin with #@. This way it is possible to handle links in markdown and HTML markup. Because it is processed in the browser on the client side, the existence of the cross-referenced data records in webtrees is not checked in advance. Errors only occur when the link is clicked (e.g. if the access to the resource is restricted).
As a bonus, different destinations can be addressed with one link definition, whereby the cross-reference is always the first link (if set) and the others are represented by attached clickable icons only.
Predefined external targets
The parameter keys are listed in parentheses:
Each parameter key can be specified multiple times — but you shouldn’t overdo it for the sake of clarity.
Additional external destinations can be configured on the Admin page.
Syntax
Markdown: An enhanced link looks like this [Link display title](#@param1¶mN), so that one or more targets can be addressed at once. For cross-references in webtrees a record reference (REF can be the XREF or a UID) is expected:
wt=@REF@ or wt=@REF@othertree — standard link, access to record page via redirect URLwt=n@REF@ — legacy link with given record type (here: to a note with REF in the current tree)- Available record types:
i=individual, f=family, s=source, r=repository, n=note, l=sharedPlace
wt=i@REF@othertree+dia — link to record type individual with REF from tree “othertree” and also link to Interactive tree of this person
HTML: The same applies to HTML links: <a href="#@wt=i@I1@">Link display title</a>
This is also usable in cooperation with the name badge function of the Vesta Classic Look & Feel module in the HTML snippet field: <a href="#@fsft=<ref/>"></a> for linking to a record in the Family Search Family Tree. It’s also possible to embed those links in news articles of Tywed’s news-menu module or in FAQ articles.
The syntax of the external targets is listed by the markdown help function of this module. In most cases, only one key-value parameter pair needs to be specified, consisting of the short name of the desired target and the ID of the data record located there.
Example
In the upper part of the image we can see the edit mode of a note record, below how it is rendered in view mode:

1 - Cross-Reference Overview
Cross-reference overview with link index for large trees
There are three types of overviews:
- overview about all data for the admin (admin overview),
- to one tree limited overview (user overview), and
- record-specific overview on the record details view (record overview, as tab or modal).
The admin overview (Control panel → LinkEnhancer → Cross-reference overview) is a server-side paginated DataTable. It lists all records that contain classic @XREF@ cross-references or linkenhancer links, with a per-record link inventory.
Optional filters:
- referenced XREF (only available while a fresh link index is active — in live-scan mode the field is hidden, because it would only be a coarse pre-filter there — no classic references are found),
- record type,
- tree, and
- the number of tokens shown per link class (all / 5 / 10 / 20, default 5).
The XREF cell shows the tree name as a second muted line; the type is its own sortable column. Each inventory token is displayed as a short display context (snippet) with the link token itself in bold.

Optionally you can enable a user overview per tree and a record overview with incoming and outgoing references per GEDCOM record. The record overview also displays the XREF and, if applicable, the UIDs of the record, so that these references can be more easily copied into notes.

Supported engines
| Live overview | Link index (CLI) |
|---|
| MariaDB / MySQL | yes (REGEXP gate) | yes |
| PostgreSQL | yes (REGEXP gate) | yes |
| SQLite / SQL Server | limited mode — automatic fallback to a coarse LIKE gate + full table scan per request (banner on the page); the exact classification still happens in PHP | not available (needs REGEXP + MD5) — the CLI exits with an error |
How it works
On large trees the live regex scan over the GEDCOM tables can be slow, so the page can instead read a link index made of three module tables (the Developer / Project Structure section contains more details about the database schema):
le_record_scan — one row per scanned record with an MD5 fingerprint of the record textle_link_index — one row per (link, target) found in the record (the display context captured at build time) — the foundation for displaying incoming cross-references in the record overviewle_index_meta — single row — the state of the last complete index run
flowchart TD
CLI["🛠 cli/build-link-index.php<br/>Cron: */30 * * * *"]
subgraph Scan["🔍 Incremental scan"]
FP["MD5 Fingerprint<br/>per record"]
DIFF{"🔀 Changed?<br/>(new / removed)"}
end
subgraph Tables["💾 Tables"]
RS[("le_record_scan<br/>file, xref, hash")]
LI[("le_link_index<br/>token, target, snippet")]
META[("le_index_meta<br/>last_run, rows")]
end
PAGE["📄 XREF Overview Page"]
CLI --> Scan
FP --> DIFF
DIFF -->|✅ yes| SCAN["PHP Scan<br/>(XrefsService)"]
DIFF -->|⏭ no| SKIP["skip"]
SCAN --> RS
SCAN --> LI
CLI --> META
META -->|"🟢 fresh (< 2 h)"| PAGE
META -->|"🟠 stale"| LIVE["Live Regex Scan"]
LIVE --> PAGE
classDef scanStyle fill:#e3f2fd,stroke:#1565C0,stroke-width:2px
classDef tablesStyle fill:#e8f5e9,stroke:#2E7D32,stroke-width:2px
classDef decideStyle fill:#fff3e0,stroke:#E65100,stroke-width:2px
classDef pageStyle fill:#fce4ec,stroke:#C2185B,stroke-width:2px
class FP scanStyle
class RS,LI,META tablesStyle
class DIFF decideStyle
class PAGE pageStyle
The index is maintained by build-link-index.php
:
- Initial build (first run, or
--rebuild): both index tables are reset, all scan rows are created server-side in one bulk statement per GEDCOM table (fast, zero row transfer), and only the records that pass the link pre-filter are scanned in PHP. The initial build is a one-time cost — use a higher --limit for it. If it is interrupted by --limit, the next run converges (the build is idempotent). - Incremental runs (the normal cron case): only records whose MD5 fingerprint changed — or which were added/removed — are re-scanned (cursor chunked), so the job stays cheap even on large sites.
Cron example (every 30 minutes, up to 2000 records per run):
*/30 * * * * cd /path/to/webtrees && php modules_v4/linkenhancer/cli/build-link-index.php --limit=2000 >> /var/log/linkenhancer-index.log 2>&1
Only a full run (without --tree) that is not cut by --limit marks the index fresh (le_index_meta.last_run). A run cut by --limit continues where it stopped; a --tree run only refreshes that tree.
Flushing
--flush empties the index tables and exits (no rebuild) — for testing (force the page onto the live scan) and maintenance (clean restart, broken index):
php modules_v4/linkenhancer/cli/build-link-index.php --flush # all trees
php modules_v4/linkenhancer/cli/build-link-index.php --flush --tree=5 # only tree 5
The freshness flag is always reset, so the page reliably falls back to the live scan afterwards. The next full run is an initial build again (full re-scan cost). --flush combined with --rebuild is redundant — --rebuild already resets and rebuilds.
What counts as a link
The overview and the index use one shared pre-filter (single source: XrefsService::linkPrefilterPatterns()):
NOTE (level >= 1) with an @XREF@ or a ](#@ link, with content on the same line. A NOTE value that is exactly one reference (1 NOTE @N5@) is a GEDCOM shared-note pointer — a structural reference, not a text link, and is not listed.CONC/CONT/TEXT/_TODO with an @XREF@ (naked is a link) or a ](#@ link- an LE link may carry an optional
id=@XREF@ parameter (at most one, the position among the parameters does not matter) — like the wt= parameters it is indexed as a link target - a shared note’s own level-0 text (
0 @N1@ NOTE …) - matching is line-based (no cross-line matches), on the same lines the collector reads
The page shows which source it uses:
- index active (green note): a full, complete index run finished less than 2 hours ago (default;
XrefsService::INDEX_FRESH_SECONDS) - live scan (blue note): no fresh index — the page falls back to the live scan
- limited mode (yellow note, SQLite/SQL Server): see the engine table above
Use p1-measure.php
(read-only) on your instance to check how expensive the live scan is for your data and to calibrate the cron schedule.
2 - Renumber XREFs (with links)
Renumber XREFs with automatic link repair
The standard webtrees renumbering (Control panel
Manage family trees → [Family tree] → Renumber XREFs) resolves cross-tree XREF collisions but does not update linkenhancer or classic links. This module provides an extended renumber that repairs all affected links in one step.
Access: Control panel → LinkEnhancer → Renumber XREFs (with links)
Workflow:
- Select the tree that has XREF collisions (duplicate XREFs with other trees).
- The page shows each conflicting XREF, its record type, and the number of inbound linkenhancer references from the link index.
- On submit, each conflicting XREF is:
- renamed in the GEDCOM core tables (same as the standard renumber)
- all
wt=@XREF@ / id=@XREF@ links in notes and blocks are updated - the link index (
le_link_index) and UID index (le_uid_index) are repaired inline or deferred to cron
- Each renumbered record is logged in the edit log (Admin → Changes log), e.g.
Renumbered @I456@ → @I457@ (INDI, 3 core rows, 2 links). - If the PHP time limit is reached, the run stops with partial progress — the remaining conflicts are renumbered on the next run.
Note: The standard webtrees renumber (without links) remains available and is unaffected. Use this module’s renumber when your notes or blocks contain linkenhancer links.
See also: fisharebest/webtrees#5156
3 - Data Fixes
Data-fix handlers for link integrity issues
The module provides a set of data-fix handlers accessible from the webtrees data-fix interface (Control panel → Data fixes → LinkEnhancer). Each handler addresses a specific link integrity issue:
| Fix | What it does |
|---|
| XREF ⇄ UID reference swap | Converts linkenhancer link targets between XREF and UID form. Direction is selectable (XREF→UID or UID→XREF). Requires the UID index to be built. |
Fix wt= type letter | Sets, repairs, or removes the legacy type letter in wt=<type>@REF@ links. Modes: set (add missing type), repair (fix wrong type), remove (strip type letter). |
| Report dangling link targets | Report-only: lists linkenhancer links whose target record no longer exists. No changes are made. |
| Report ambiguous UID targets | Report-only: lists links whose UID target resolves to more than one record. No changes are made. |
| Report duplicate UIDs in index | Report-only: lists UIDs assigned to multiple records in the UID index. No changes are made. |
Each fix follows the webtrees data-fix workflow: select fix type → preview → apply. The data-fix page also shows the current status of the link and UID indexes, and whether index updates will be deferred to the cron job.